/*
 *  hpa_dco.c: functions that handle the host protected area (HPA) and
 *  device configuration overlay (DCO)
 *
 *  Copyright PartialVolume <https://github.com/PartialVolume>.
 *
 *  This program is free software; you can redistribute it and/or modify it under
 *  the terms of the GNU General Public License as published by the Free Software
 *  Foundation, version 2.
 *
 *  This program is distributed in the hope that it will be useful, but WITHOUT
 *  ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 *  FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
 *  details.
 *
 *  You should have received a copy of the GNU General Public License along with
 *  this program; if not, write to the Free Software Foundation, Inc.,
 *  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 */

#ifndef _DEFAULT_SOURCE
#define _DEFAULT_SOURCE
#endif

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <stdarg.h>
#include <string.h>
#include <scsi/sg.h>
#include <scsi/scsi_ioctl.h>
#include "nwipe.h"
#include "context.h"
#include "version.h"
#include "method.h"
#include "logging.h"
#include "options.h"
#include "hpa_dco.h"
#include "miscellaneous.h"

/* This function makes use of both the hdparm program to determine HPA/DCO status and we also access
 * the device configuration overlay identify data structure via the sg driver with ioctl calls.
 * I would prefer to write these functions without any reliance on hdparm however for the time being
 * we will utilize both methods. However, I don't like doing it like this as a change in formatted
 * output of hdparm could potentially break HPA/DCO detection requiring a fix. Time permitting I may
 * come back to this and fully implement it without any reliance on hdparm.
 */

int hpa_dco_status( nwipe_context_t* ptr )
{
    nwipe_context_t* c;
    c = ptr;

    int r;  // A result buffer.
    int set_return_value;
    int exit_status;
    int hpa_line_found;
    int dco_line_found;

    FILE* fp;
    char path_hdparm_cmd10_get_hpa[] = "hdparm --verbose -N";
    char path_hdparm_cmd20_get_hpa[] = "/sbin/hdparm --verbose -N";
    char path_hdparm_cmd30_get_hpa[] = "/usr/bin/hdparm --verbose -N";
    char path_hdparm_cmd31_get_hpa[] = "/usr/sbin/hdparm --verbose -N";

    char path_hdparm_cmd40_get_dco[] = "hdparm --verbose --dco-identify";
    char path_hdparm_cmd50_get_dco[] = "/sbin/hdparm --verbose --dco-identify";
    char path_hdparm_cmd60_get_dco[] = "/usr/bin/hdparm --verbose --dco-identify";
    char path_hdparm_cmd61_get_dco[] = "/usr/sbin/hdparm --verbose --dco-identify";

    char pipe_std_err[] = "2>&1";

    char result[512];

    u64 nwipe_dco_real_max_sectors;

    char* p;

    /* Use the longest of the 'path_hdparm_cmd.....' strings above to
     *determine size in the strings below
     */
    char hdparm_cmd_get_hpa[sizeof( path_hdparm_cmd30_get_hpa ) + sizeof( c->device_name ) + sizeof( pipe_std_err )];
    char hdparm_cmd_get_dco[sizeof( path_hdparm_cmd60_get_dco ) + sizeof( c->device_name ) + sizeof( pipe_std_err )];

    /* Initialise return value */
    set_return_value = 0;

    /* Construct the command including path to the binary if required, I do it like this to cope
     * with distros that don't setup their paths in a standard way or maybe don't even define a
     * path. By doing this we avoid the 'No such file or directory' message you would otherwise
     * get on some distros. -> debian SID
     */

    if( system( "which hdparm > /dev/null 2>&1" ) )
    {
        if( system( "which /sbin/hdparm > /dev/null 2>&1" ) )
        {
            if( system( "which /usr/bin/hdparm > /dev/null 2>&1" ) )
            {
                if( system( "which /usr/sbin/hdparm > /dev/null 2>&1" ) )
                {
                    nwipe_log( NWIPE_LOG_WARNING, "hdparm command not found." );
                    nwipe_log( NWIPE_LOG_WARNING,
                               "Required by nwipe for HPA/DCO detection & correction and ATA secure erase." );
                    nwipe_log( NWIPE_LOG_WARNING, "** Please install hdparm **\n" );
                    cleanup();
                    exit( 1 );
                }
                else
                {
                    snprintf( hdparm_cmd_get_hpa,
                              sizeof( hdparm_cmd_get_hpa ),
                              "%s %s %s\n",
                              path_hdparm_cmd31_get_hpa,
                              c->device_name,
                              pipe_std_err );
                    snprintf( hdparm_cmd_get_dco,
                              sizeof( hdparm_cmd_get_dco ),
                              "%s %s %s\n",
                              path_hdparm_cmd61_get_dco,
                              c->device_name,
                              pipe_std_err );
                }
            }
            else
            {
                snprintf( hdparm_cmd_get_hpa,
                          sizeof( hdparm_cmd_get_hpa ),
                          "%s %s %s\n",
                          path_hdparm_cmd30_get_hpa,
                          c->device_name,
                          pipe_std_err );
                snprintf( hdparm_cmd_get_dco,
                          sizeof( hdparm_cmd_get_dco ),
                          "%s %s %s\n",
                          path_hdparm_cmd60_get_dco,
                          c->device_name,
                          pipe_std_err );
            }
        }
        else
        {
            snprintf( hdparm_cmd_get_hpa,
                      sizeof( hdparm_cmd_get_hpa ),
                      "%s %s %s\n",
                      path_hdparm_cmd20_get_hpa,
                      c->device_name,
                      pipe_std_err );
            snprintf( hdparm_cmd_get_dco,
                      sizeof( hdparm_cmd_get_dco ),
                      "%s %s %s\n",
                      path_hdparm_cmd50_get_dco,
                      c->device_name,
                      pipe_std_err );
        }
    }
    else
    {
        snprintf( hdparm_cmd_get_hpa,
                  sizeof( hdparm_cmd_get_hpa ),
                  "%s %s %s\n",
                  path_hdparm_cmd10_get_hpa,
                  c->device_name,
                  pipe_std_err );
        snprintf( hdparm_cmd_get_dco,
                  sizeof( hdparm_cmd_get_dco ),
                  "%s %s %s\n",
                  path_hdparm_cmd40_get_dco,
                  c->device_name,
                  pipe_std_err );
    }

    /* Initialise the results buffer, so we don't some how inadvertently process a past result */
    memset( result, 0, sizeof( result ) );

    if( hdparm_cmd_get_hpa[0] != 0 )
    {

        fp = popen( hdparm_cmd_get_hpa, "r" );

        if( fp == NULL )
        {
            nwipe_log( NWIPE_LOG_WARNING, "hpa_dco_status: Failed to create stream to %s", hdparm_cmd_get_hpa );

            set_return_value = 1;
        }

        if( fp != NULL )
        {
            hpa_line_found = 0;  //* init */

            /* Read the output a line at a time - output it. */
            while( fgets( result, sizeof( result ) - 1, fp ) != NULL )
            {
                if( nwipe_options.verbose )
                {
                    nwipe_log( NWIPE_LOG_DEBUG, "%s \n%s", hdparm_cmd_get_hpa, result );
                }

                /* Change the output of hdparm to lower case and search using lower case strings, to try
                 * to avoid minor changes in case in hdparm's output from breaking HPA/DCO detection
                 */
                strlower( result );  // convert the result to lower case

                /* Scan the hdparm results for HPA is disabled
                 */
                if( strstr( result, "sg_io: bad/missing sense data" ) != 0 )
                {
                    c->HPA_status = HPA_UNKNOWN;
                    nwipe_log( NWIPE_LOG_ERROR, "SG_IO bad/missing sense data %s", hdparm_cmd_get_hpa );
                    break;
                }
                else
                {
                    if( strstr( result, "hpa is disabled" ) != 0 )
                    {
                        c->HPA_status = HPA_DISABLED;

                        nwipe_log( NWIPE_LOG_DEBUG,
                                   "hdparm says the host protected area is disabled on %s but this information may or "
                                   "may not be correct, as occurs when you get a SG_IO error and 0/1 sectors and it "
                                   "says HPA is enabled. Further checks are conducted below..",
                                   c->device_name );
                        hpa_line_found = 1;
                        break;
                    }
                    else
                    {
                        if( strstr( result, "hpa is enabled" ) != 0 )
                        {
                            c->HPA_status = HPA_ENABLED;
                            nwipe_log( NWIPE_LOG_DEBUG,
                                       "hdparm says the host protected area is enabled on %s but this information may "
                                       "or may not be correct, as occurs when you get a SG_IO error and 0/1 sectors "
                                       "and it says HPA is enabled. Further checks are conducted below..",
                                       c->device_name );
                            hpa_line_found = 1;
                            break;
                        }
                        else
                        {
                            if( strstr( result, "accessible max address disabled" ) != 0 )
                            {
                                c->HPA_status = HPA_DISABLED;
                                nwipe_log( NWIPE_LOG_DEBUG,
                                           "hdparm says the accessible max address disabled on %s"
                                           "this means that there are no hidden sectors,  "
                                           "",
                                           c->device_name );
                                hpa_line_found = 1;
                                break;
                            }
                            else
                            {
                                if( strstr( result, "accessible max address enabled" ) != 0 )
                                {
                                    c->HPA_status = HPA_ENABLED;
                                    nwipe_log( NWIPE_LOG_DEBUG,
                                               "hdparm says the accessible max address enabled on %s"
                                               "this means that there are hidden sectors",
                                               c->device_name );
                                    hpa_line_found = 1;
                                    break;
                                }
                                else
                                {
                                    if( strstr( result, "invalid" ) != 0 )
                                    {
                                        nwipe_log(
                                            NWIPE_LOG_WARNING,
                                            "hdparm reports invalid output, sector information may be invalid, buggy "
                                            "drive firmware on %s?",
                                            c->device_name );
                                        // We'll assume the HPA values are in the string as we may be able to extract
                                        // something meaningful
                                        hpa_line_found = 1;
                                        break;
                                    }
                                }
                            }
                        }
                    }
                }
            }

            /* if the HPA line was found then process the line,
             * extracting the 'hpa set' and 'hpa real' values.
             */
            if( hpa_line_found == 1 )
            {
                /* Extract the 'HPA set' value, the first value in the line and convert
                 * to binary and save in context */

                nwipe_log( NWIPE_LOG_INFO, "HPA: %s on %s", result, c->device_name );

                c->HPA_reported_set = str_ascii_number_to_ll( result );

                /* Check whether the number was too large or no number found & log */
                if( c->HPA_reported_set == -1 )
                {
                    nwipe_log( NWIPE_LOG_INFO, "HPA_set_value: HPA set value too large on %s", c->device_name );
                    c->HPA_reported_set = 0;
                }
                else
                {
                    if( c->HPA_reported_set == -2 )
                    {
                        nwipe_log( NWIPE_LOG_INFO, "HPA_set_value: No HPA set value found %s", c->device_name );
                        c->HPA_reported_set = 0;
                    }
                }

                /* Extract the 'HPA real' value, the second value in the line and convert
                 * to binary and save in context, this is a little more difficult as sometimes
                 * a odd value is returned so instead of nnnnn/nnnnn you get nnnnnn/1(nnnnnn).
                 * So first we scan for a open bracket '(' then if there is no '(' we then start the
                 * search immediately after the '/'.
                 */
                if( ( p = strstr( result, "(" ) ) )
                {
                    c->HPA_reported_real = str_ascii_number_to_ll( p + 1 );
                }
                else
                {
                    if( ( p = strstr( result, "/" ) ) )
                    {
                        c->HPA_reported_real = str_ascii_number_to_ll( p + 1 );
                    }
                }

                /* Check whether the number was too large or no number found & log */
                if( c->HPA_reported_real == -1 )
                {
                    nwipe_log( NWIPE_LOG_INFO, "HPA_set_value: HPA real value too large on %s", c->device_name );
                    c->HPA_reported_real = 0;
                }
                else
                {
                    if( c->HPA_reported_real == -2 )
                    {
                        nwipe_log( NWIPE_LOG_INFO, "HPA_set_value: No HPA real value found %s", c->device_name );
                        c->HPA_reported_real = 0;
                    }
                }

                nwipe_log( NWIPE_LOG_INFO,
                           "HPA values %lli / %lli on %s",
                           c->HPA_reported_set,
                           c->HPA_reported_real,
                           c->device_name );
            }
            else
            {
                c->HPA_status = HPA_UNKNOWN;
                nwipe_log( NWIPE_LOG_WARNING,
                           "[UNKNOWN] We can't find the HPA line, has hdparm ouput unknown/changed? %s",
                           c->device_name );
            }

            /* close */
            r = pclose( fp );
            if( r > 0 )
            {
                exit_status = WEXITSTATUS( r );
                if( nwipe_options.verbose )
                {
                    nwipe_log( NWIPE_LOG_WARNING,
                               "hpa_dco_status(): hdparm failed, \"%s\" exit status = %u",
                               hdparm_cmd_get_hpa,
                               exit_status );
                }

                if( exit_status == 127 )
                {
                    nwipe_log( NWIPE_LOG_WARNING, "Command not found. Installing hdparm is mandatory !" );
                    set_return_value = 2;
                    if( nwipe_options.nousb )
                    {
                        return set_return_value;
                    }
                }
            }
        }
    }

    /* Initialise the results buffer again, so we don't
     * some how inadvertently process a past result */
    memset( result, 0, sizeof( result ) );

    /* -----------------------------------------------
     * Run the dco identify command and determine the
     * real max sectors, store it in the drive context
     * for comparison against the hpa reported drive
     * size values.
     */

    dco_line_found = 0;

    if( hdparm_cmd_get_dco[0] != 0 )
    {

        fp = popen( hdparm_cmd_get_dco, "r" );

        if( fp == NULL )
        {
            nwipe_log( NWIPE_LOG_WARNING, "hpa_dco_status: Failed to create stream to %s", hdparm_cmd_get_dco );

            set_return_value = 1;
        }

        if( fp != NULL )
        {
            /* Read the output a line at a time - output it. */
            while( fgets( result, sizeof( result ) - 1, fp ) != NULL )
            {
                /* Change the output of hdparm to lower case and search using lower case strings, to try
                 * to avoid minor changes in case in hdparm's output from breaking HPA/DCO detection */
                strlower( result );

                if( nwipe_options.verbose )
                {
                    nwipe_log( NWIPE_LOG_DEBUG, "%s \n%s", hdparm_cmd_get_dco, result );
                }

                if( strstr( result, "real max sectors" ) != 0 )
                {
                    /* extract the real max sectors, convert to binary and store in drive context */
                    dco_line_found = 1;
                    break;
                }
            }
            /* DCO line found, now process it */
            if( dco_line_found == 1 )
            {
                c->DCO_reported_real_max_sectors = str_ascii_number_to_ll( result );
                nwipe_log( NWIPE_LOG_INFO,
                           "hdparm:DCO Real max sectors reported as %lli on %s",
                           c->DCO_reported_real_max_sectors,
                           c->device_name );

                /* Validate the real max sectors to detect extreme or impossible
                 * values, so the size must be greater than zero but less than
                 * 200TB (429496729600 sectors). As its 2023 and the largest drive
                 * available is 20TB I wonder if somebody in the future will be looking
                 * at this and thinking, yep we need to increase that value... and I'm
                 * wondering what year that will be. This validation is necessary all
                 * because of a bug in hdparm v9.60 (and maybe other versions) which
                 * produced wildly inaccurate values, often negative.
                 */
                if( c->DCO_reported_real_max_sectors > 0 && c->DCO_reported_real_max_sectors < 429496729600 )
                {
                    nwipe_log( NWIPE_LOG_INFO,
                               "NWipe: DCO Real max sectors reported as %lli on %s",
                               c->DCO_reported_real_max_sectors,
                               c->device_name );
                }
                else
                {
                    /* Call nwipe's own low level function to retrieve the drive configuration
                     * overlay and retrieve the real max sectors. We may remove reliance on hdparm
                     * if nwipes own low level drive access code works well.
                     */
                    c->DCO_reported_real_max_sectors = nwipe_read_dco_real_max_sectors( c->device_name );

                    /* Check our real max sectors function is returning sensible data too */
                    if( c->DCO_reported_real_max_sectors > 0 && c->DCO_reported_real_max_sectors < 429496729600 )
                    {
                        nwipe_log( NWIPE_LOG_INFO,
                                   "NWipe: DCO Real max sectors reported as %lli on %s",
                                   c->DCO_reported_real_max_sectors,
                                   c->device_name );
                    }
                    else
                    {
                        c->DCO_reported_real_max_sectors = 0;
                        nwipe_log( NWIPE_LOG_INFO, "DCO Real max sectors not found" );
                    }
                }
            }
            else
            {
                c->DCO_reported_real_max_sectors = 0;
                nwipe_log( NWIPE_LOG_INFO, "DCO Real max sectors not found" );
            }

            nwipe_log(
                NWIPE_LOG_INFO,
                "libata: apparent max sectors reported as %lli with sector size as %i/%i (logical/physical) on %s",
                c->device_size_in_sectors,
                c->device_sector_size,  // logical
                c->device_phys_sector_size,  // physical
                c->device_name );

            /* close */
            r = pclose( fp );
            if( r > 0 )
            {
                exit_status = WEXITSTATUS( r );
                if( nwipe_options.verbose )
                {
                    nwipe_log( NWIPE_LOG_WARNING,
                               "hpa_dco_status(): hdparm failed, \"%s\" exit status = %u",
                               hdparm_cmd_get_dco,
                               exit_status );
                }

                if( exit_status == 127 )
                {
                    nwipe_log( NWIPE_LOG_WARNING, "Command not found. Installing hdparm is mandatory !" );
                    set_return_value = 2;
                    if( nwipe_options.nousb )
                    {
                        return set_return_value;
                    }
                }
            }
        }
    }

    /* Compare the results of hdparm -N (HPA set / HPA real)
     * and hdparm --dco-identidy (real max sectors). All three
     * values may be different or perhaps 'HPA set' and 'HPA real' are
     * different and 'HPA real' matches 'real max sectors'.
     *
     * A perfect HPA disabled result would be where all three
     * values are the same. It can then be considered that the
     * HPA is disabled.
     *
     * If 'HPA set' and 'HPA real' are different then it
     * can be considered that HPA is enabled, assuming 'HPA set'
     * and 'HPA real' are not 0/1 which occurs when a SG_IO error
     * occurs. That also is checked for as it often indicates a
     * poor USB device that does not have ATA pass through support.
     *
     * However we also need to consider that more recent drives
     * no longer support HPA/DCO such as the Seagate ST10000NM0016,
     * ST4000NM0033 and ST1000DM003. If you try to issue those drives
     * with the ATA command code 0xB1 (device configuration overlay)
     * you will get a generic illegal request in the returned sense data.
     *
     * One other thing to note, we use HPA enabled/disabled to mean
     * hidden area detected or not detected, this could be caused by
     * either the dco-setmax being issued or Np, either way an area
     * of the disc can be hidden. From the user interface we just call
     * it a HPA/DCO hidden area detected (or not) which is more
     * meaningful than just saying HDA enabled or disabled and a user
     * not familiar with the term HPA or DCO not understanding why a
     * HDA being detected could be significant.
     */

    /* Determine, based on the values of 'HPA set', 'HPA real and
     * 'real max sectors' whether we set the HPA flag as HPA_DISABLED,
     * HPA_ENABLED, HPA_UNKNOWN or HPA_NOT_APPLICABLE. The HPA flag
     * will be displayed in the GUI and on the certificate and is
     * used to determine whether to reset the HPA.
     *
     */

    /* WARNING temp assignments WARNING
     * s=28,r=28,rm=0
     *
     */
#if 0
    c->HPA_reported_set = 10;
    c->HPA_reported_real = 28;
    c->DCO_reported_real_max_sectors = 0;

    c->HPA_reported_set = 28;
    c->HPA_reported_real = 28;
    c->DCO_reported_real_max_sectors = 0;

    c->HPA_reported_set = 1000;
    c->HPA_reported_real = 2048;
    c->DCO_reported_real_max_sectors = 2048;
#endif

    /* If any of the HPA or DCO values are larger than the apparent size then HPA is enabled. */
    if( /*c->HPA_reported_set > c->device_size_in_sectors || */ c->HPA_reported_real > c->device_size_in_512byte_sectors
        || c->DCO_reported_real_max_sectors > c->device_size_in_512byte_sectors )
    {
        c->HPA_status = HPA_ENABLED;
        nwipe_log( NWIPE_LOG_WARNING, " *********************************" );
        nwipe_log( NWIPE_LOG_WARNING, " *** HIDDEN SECTORS DETECTED ! *** on %s", c->device_name );
        nwipe_log( NWIPE_LOG_WARNING, " *********************************" );
    }
    else
    {
        /* This occurs when a SG_IO error occurs with USB devices that don't support ATA pass
         * through */
        if( c->HPA_reported_set == 0 && c->HPA_reported_real == 0 && c->DCO_reported_real_max_sectors <= 1 )
        {
            if( c->device_bus == NWIPE_DEVICE_SAS )
            {
                /* SAS SCSI doesn't appear to support HPA/DCO commands */
                c->HPA_status = HPA_NOT_APPLICABLE;
                nwipe_log( NWIPE_LOG_INFO, "No hidden sectors on %s", c->device_name );
            }
            else
            {
                c->HPA_status = HPA_UNKNOWN;
                if( c->device_bus == NWIPE_DEVICE_USB )
                {
                    nwipe_log( NWIPE_LOG_WARNING,
                               "HIDDEN SECTORS INDETERMINATE! on %s, Some USB adapters & memory sticks don't support "
                               "ATA pass through",
                               c->device_name );
                }
            }
        }
        else
        {
            c->HPA_status = HPA_DISABLED;
            nwipe_log( NWIPE_LOG_INFO, "No hidden sectors on %s", c->device_name );
        }
    }

    c->DCO_reported_real_max_size = c->DCO_reported_real_max_sectors * c->device_sector_size;

    nwipe_dco_real_max_sectors = nwipe_read_dco_real_max_sectors( c->device_name );

    /* Analyse all the variations to produce the final real max bytes which takes into
     * account drives that don't support DCO or HPA. This result is used in the PDF
     * creation functions.
     */

    if( c->device_type == NWIPE_DEVICE_NVME || c->device_type == NWIPE_DEVICE_VIRT
        || c->HPA_status == HPA_NOT_APPLICABLE )
    {
        c->Calculated_real_max_size_in_bytes = c->device_size;
    }
    else
    {
        /* If the DCO is reporting a real max sectors > the apparent size
         * as reported by libata then that is what we will use as the real disc size
         */
        if( c->DCO_reported_real_max_size > c->device_size_in_512byte_sectors )
        {
            c->Calculated_real_max_size_in_bytes = c->DCO_reported_real_max_sectors * c->device_sector_size;
        }
        else
        {
            /* If HPA is enabled and DCO real max sectors did not exist, then we have to assume - c->HPA_reported_real
             * is the value we need, however if that is zero, then c->HPA_reported_set and if that is zero then
             * c->device_size as reported by libata
             */
            if( c->HPA_reported_real > c->device_size_in_512byte_sectors )
            {
                c->Calculated_real_max_size_in_bytes = c->HPA_reported_real * c->device_sector_size;
            }
            else
            {
                if( c->HPA_reported_set > c->device_size_in_512byte_sectors )
                {
                    c->Calculated_real_max_size_in_bytes = c->HPA_reported_set * c->device_sector_size;
                }
                else
                {
                    c->Calculated_real_max_size_in_bytes = c->device_size;
                }
            }
        }
    }

    /* -------------------------------------------------------------------
     * create two variables for later use by the PDF creation function
     * based on real max sectors and calculated real max size in bytes.
     *
     * DCO_reported_real_max_size = real max sectors * sector size = bytes
     * DCO_reported_real_max_size_text = human readable string, i.e 1TB etc.
     */

    Determine_C_B_nomenclature(
        c->DCO_reported_real_max_size, c->DCO_reported_real_max_size_text, NWIPE_DEVICE_SIZE_TXT_LENGTH );
    Determine_C_B_nomenclature(
        c->Calculated_real_max_size_in_bytes, c->Calculated_real_max_size_in_bytes_text, NWIPE_DEVICE_SIZE_TXT_LENGTH );

    /* ----------------------------------------------------------------------------------
     * Determine the size of the HPA if it's enabled and store the results in the context
     */

    if( c->HPA_status == HPA_ENABLED )
    {
        if( c->Calculated_real_max_size_in_bytes != c->device_size )
        {
            c->HPA_sectors =
                ( (u64) ( c->Calculated_real_max_size_in_bytes - c->device_size ) / c->device_sector_size );
        }
        else
        {
            c->HPA_sectors = 0;
        }

        /* Convert the size to a human readable format and save in context */
        Determine_C_B_nomenclature( c->HPA_sectors, c->HPA_size_text, NWIPE_DEVICE_SIZE_TXT_LENGTH );
    }
    else
    {
        /* HPA not enabled so initialise these values */
        c->HPA_sectors = 0;
        c->HPA_size_text[0] = 0;
    }

    nwipe_log( NWIPE_LOG_DEBUG,
               "c->Calculated_real_max_size_in_bytes=%lli, c->device_size=%lli, c->device_sector_size=%lli, "
               "c->DCO_reported_real_max_size=%lli, c->DCO_reported_real_max_sectors=%lli, c->HPA_sectors=%lli, "
               "c->HPA_reported_set=%lli, c->HPA_reported_real=%lli, c->device_type=%i, "
               "libata:c->device_size_in_sectors=%lli ",
               "libata:c->device_size_in_512byte_sectors=%lli ",
               c->Calculated_real_max_size_in_bytes,
               c->device_size,
               c->device_sector_size,
               c->DCO_reported_real_max_size,
               c->DCO_reported_real_max_sectors,
               c->HPA_sectors,
               c->HPA_reported_set,
               c->HPA_reported_real,
               c->device_type,
               c->device_size_in_sectors,
               c->device_size_in_512byte_sectors );

    return set_return_value;
}

u64 nwipe_read_dco_real_max_sectors( char* device )
{
    /* This function sends a device configuration overlay identify command 0xB1 (dco-identify)
     * to the drive and extracts the real max sectors. The value is incremented by 1 and
     * then returned. We rely upon this function to determine real max sectors as there
     * is a bug in hdparm 9.60, including possibly earlier or later versions but which is
     * fixed in 9.65, that returns a incorrect (negative) value
     * for some drives that are possibly over a certain size.
     */

    /* TODO Add checks in case of failure, especially with recent drives that may not
     * support drive configuration overlay commands.
     */

#define LBA_SIZE 512
#define CMD_LEN 16
#define BLOCK_MAX 65535
#define LBA_MAX ( 1 << 30 )
#define SENSE_BUFFER_SIZE 32

    u64 nwipe_real_max_sectors;

    /* This command issues command 0xb1 (dco-identify) 15th byte */
    unsigned char cmd_blk[CMD_LEN] = { 0x85, 0x08, 0x0e, 0x00, 0xc2, 0, 0x01, 0, 0, 0, 0, 0, 0, 0x40, 0xb1, 0 };

    sg_io_hdr_t io_hdr;
    unsigned char buffer[LBA_SIZE];  // Received data block
    unsigned char sense_buffer[SENSE_BUFFER_SIZE];  // Sense data

    /* Zero the data block prior to use */
    memset( buffer, 0, LBA_SIZE );

    /* three characters represent one byte of sense data, i.e
     * two characters and a space "01 AE 67"
     */
    char sense_buffer_hex[( SENSE_BUFFER_SIZE * 3 ) + 1];

    int i, i2;  // index
    int fd;  // file descripter

    if( ( fd = open( device, O_RDWR ) ) < 0 )
    {
        /* Unable to open device */
        return -1;
    }

    /******************************************
     * Initialise the sg header for reading the
     * device configuration overlay identify data
     */
    memset( &io_hdr, 0, sizeof( sg_io_hdr_t ) );
    io_hdr.interface_id = 'S';
    io_hdr.cmd_len = sizeof( cmd_blk );
    io_hdr.mx_sb_len = sizeof( sense_buffer );
    io_hdr.dxfer_direction = SG_DXFER_FROM_DEV;
    io_hdr.dxfer_len = LBA_SIZE;
    io_hdr.dxferp = buffer;
    io_hdr.cmdp = cmd_blk;
    io_hdr.sbp = sense_buffer;
    io_hdr.timeout = 20000;

    if( ioctl( fd, SG_IO, &io_hdr ) < 0 )
    {
        nwipe_log( NWIPE_LOG_ERROR, "IOCTL command failed retrieving DCO" );
        i2 = 0;
        for( i = 0, i2 = 0; i < SENSE_BUFFER_SIZE; i++, i2 += 3 )
        {
            /* IOCTL returned an error */
            snprintf( &sense_buffer_hex[i2], sizeof( sense_buffer_hex ), "%02x ", sense_buffer[i] );
        }
        sense_buffer_hex[i2] = 0;  // terminate string
        nwipe_log( NWIPE_LOG_DEBUG, "Sense buffer from failed DCO identify cmd:%s", sense_buffer_hex );
        return -2;
    }

    /* Close the device */
    close( fd );

    /***************************************************************
     * Extract the real max sectors from the returned 512 byte block.
     * Assuming the first word/byte is 0. We extract the bytes & switch
     * the endian. Words 3-6(bytes 6-13) contain the max sector address
     */
    nwipe_real_max_sectors = (u64) ( (u64) buffer[13] << 56 ) | ( (u64) buffer[12] << 48 ) | ( (u64) buffer[11] << 40 )
        | ( (u64) buffer[10] << 32 ) | ( (u64) buffer[9] << 24 ) | ( (u64) buffer[8] << 16 ) | ( (u64) buffer[7] << 8 )
        | buffer[6];

    /* Don't really understand this but hdparm adds 1 to
     * the real max sectors too, counting zero as sector?
     * but only increment if it's already greater than zero
     */
    if( nwipe_real_max_sectors > 0 )
    {
        nwipe_real_max_sectors++;
    }

    nwipe_log(
        NWIPE_LOG_INFO, "func:nwipe_read_dco_real_max_sectors(), DCO real max sectors = %lli", nwipe_real_max_sectors );

    return nwipe_real_max_sectors;
}

int ascii2binary_array( char* input, unsigned char* output_bin, int bin_size )
{
    /* Converts ascii sense data output by hdparm to binary.
     * Scans a character string that contains hexadecimal ascii data, ignores spaces
     * and extracts and converts the hexadecimal ascii data to binary and places in a array.
     * Typically for dco_identify sense data the bin size will be 512 bytes but for error
     * sense data this would be 32 bytes.
     */
    int idx_in;  // Index into ascii input string
    int idx_out;  // Index into the binary output array
    int byte_count;  // Counts which 4 bit value we are working on
    char upper4bits;
    char lower4bits;

    byte_count = 0;
    idx_in = 0;
    idx_out = 0;
    while( input[idx_in] != 0 )
    {
        if( input[idx_in] >= '0' && input[idx_in] <= '9' )
        {
            if( byte_count == 0 )
            {
                upper4bits = input[idx_in] - 0x30;
                byte_count++;
            }
            else
            {
                lower4bits = input[idx_in] - 0x30;
                output_bin[idx_out++] = ( upper4bits << 4 ) | ( lower4bits );
                byte_count = 0;

                if( idx_out >= bin_size )
                {
                    return 0;  // output array full.
                }
            }
        }
        else
        {
            if( input[idx_in] >= 'a' && input[idx_in] <= 'f' )
            {
                if( byte_count == 0 )
                {
                    upper4bits = input[idx_in] - 0x57;
                    byte_count++;
                }
                else
                {
                    lower4bits = input[idx_in] - 0x57;
                    output_bin[idx_out++] = ( upper4bits << 4 ) | ( lower4bits );
                    byte_count = 0;

                    if( idx_out >= bin_size )
                    {
                        return 0;  // output array full.
                    }
                }
            }
        }
        idx_in++;  // next byte in the input string
    }
    return 0;
}
