/*
 *  logging.c:  Logging facilities for nwipe.
 *
 *  Copyright Darik Horn <dajhorn-dban@vanadac.com>.
 *
 *  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

#ifndef _POSIX_SOURCE
#define _POSIX_SOURCE
#endif

#include "stdio.h"
#include "stdlib.h"
#include "string.h"
#include "stdarg.h"
#include "nwipe.h"
#include "context.h"
#include "method.h"
#include "prng.h"
#include "options.h"
#include "logging.h"
#include "create_pdf.h"
#include "miscellaneous.h"

/* Global array to hold log values to print when logging to STDOUT */
char** log_lines;
int log_current_element = 0;
int log_elements_allocated = 0;
int log_elements_displayed = 0;
pthread_mutex_t mutex1 = PTHREAD_MUTEX_INITIALIZER;

void nwipe_log( nwipe_log_t level, const char* format, ... )
{
    /**
     *  Writes a message to the program log file.
     *
     */

    extern int terminate_signal;
    extern int user_abort;

    char** result;
    char* malloc_result;
    char message_buffer[MAX_LOG_LINE_CHARS * sizeof( char )];
    int chars_written;

    int message_buffer_length;
    int r; /* result buffer */

    /* A time buffer. */
    time_t t;

    /* A pointer to the system time struct. */
    struct tm* p;
    r = pthread_mutex_lock( &mutex1 );
    if( r != 0 )
    {
        fprintf( stderr, "nwipe_log: pthread_mutex_lock failed. Code %i \n", r );
        return;
    }

    /* Get the current time. */
    t = time( NULL );
    p = localtime( &t );

    /* Position of writing to current log string */
    int line_current_pos = 0;

    /* initialise characters written */
    chars_written = 0;

    /* Only log messages with the debug label if the command line --verbose
     * options has been specified, otherwise just return
     */
    if( level == NWIPE_LOG_DEBUG && nwipe_options.verbose == 0 )
    {
        r = pthread_mutex_unlock( &mutex1 );
        if( r != 0 )
        {
            fprintf( stderr, "nwipe_log: pthread_mutex_unlock failed. Code %i \n", r );
        }
        return;
    }

    /* Print the date. The rc script uses the same format. */
    if( level != NWIPE_LOG_NOTIMESTAMP )
    {
        chars_written = snprintf( message_buffer,
                                  MAX_LOG_LINE_CHARS,
                                  "[%i/%02i/%02i %02i:%02i:%02i] ",
                                  1900 + p->tm_year,
                                  1 + p->tm_mon,
                                  p->tm_mday,
                                  p->tm_hour,
                                  p->tm_min,
                                  p->tm_sec );
    }

    /*
     * Has the end of the buffer been reached ?, snprintf returns the number of characters that would have been
     * written if MAX_LOG_LINE_CHARS had not been reached, it does not return the actual characters written in
     * all circumstances, hence why we need to check whether it's greater than MAX_LOG_LINE_CHARS and if so set
     * it to MAX_LOG_LINE_CHARS, preventing a buffer overrun further down this function.
     */

    /* check if there was a complete failure to write this part of the message, in which case return */
    if( chars_written < 0 )
    {
        fprintf( stderr, "nwipe_log: snprintf error when writing log line to memory.\n" );
        r = pthread_mutex_unlock( &mutex1 );
        if( r != 0 )
        {
            fprintf( stderr, "nwipe_log: pthread_mutex_unlock failed. Code %i \n", r );
            return;
        }
    }
    else
    {
        if( ( line_current_pos + chars_written ) > MAX_LOG_LINE_CHARS )
        {
            fprintf( stderr,
                     "nwipe_log: Warning! The log line has been truncated as it exceeded %i characters\n",
                     MAX_LOG_LINE_CHARS );
            line_current_pos = MAX_LOG_LINE_CHARS;
        }
        else
        {
            line_current_pos += chars_written;
        }
    }

    if( line_current_pos < MAX_LOG_LINE_CHARS )
    {
        switch( level )
        {

            case NWIPE_LOG_NONE:
            case NWIPE_LOG_NOTIMESTAMP:
                /* Do nothing. */
                break;

                /* NOTE! The debug labels, i.e. debug, info, notice etc should be left padded with spaces, in order
                 * to maintain column alignment. Pad a label to achieve the length of whatever the longest label happens
                 * to be. Important to know if you are thinking of adding another label.
                 */

            case NWIPE_LOG_DEBUG:
                chars_written =
                    snprintf( message_buffer + line_current_pos, MAX_LOG_LINE_CHARS - line_current_pos, "  debug: " );
                break;

            case NWIPE_LOG_INFO:
                chars_written =
                    snprintf( message_buffer + line_current_pos, MAX_LOG_LINE_CHARS - line_current_pos, "   info: " );
                break;

            case NWIPE_LOG_NOTICE:
                chars_written =
                    snprintf( message_buffer + line_current_pos, MAX_LOG_LINE_CHARS - line_current_pos, " notice: " );
                break;

            case NWIPE_LOG_WARNING:
                chars_written =
                    snprintf( message_buffer + line_current_pos, MAX_LOG_LINE_CHARS - line_current_pos, "warning: " );
                break;

            case NWIPE_LOG_ERROR:
                chars_written =
                    snprintf( message_buffer + line_current_pos, MAX_LOG_LINE_CHARS - line_current_pos, "  error: " );
                break;

            case NWIPE_LOG_FATAL:
                chars_written =
                    snprintf( message_buffer + line_current_pos, MAX_LOG_LINE_CHARS - line_current_pos, "  fatal: " );
                break;

            case NWIPE_LOG_SANITY:
                /* TODO: Request that the user report the log. */
                chars_written =
                    snprintf( message_buffer + line_current_pos, MAX_LOG_LINE_CHARS - line_current_pos, " sanity: " );
                break;

            default:
                chars_written = snprintf(
                    message_buffer + line_current_pos, MAX_LOG_LINE_CHARS - line_current_pos, "level %i: ", level );
        }

        /*
         * Has the end of the buffer been reached ?
         */
        if( chars_written < 0 )
        {
            fprintf( stderr, "nwipe_log: snprintf error when writing log line to memory.\n" );
            r = pthread_mutex_unlock( &mutex1 );
            if( r != 0 )
            {
                fprintf( stderr, "nwipe_log: pthread_mutex_unlock failed. Code %i \n", r );
                return;
            }
        }
        else
        {
            if( ( line_current_pos + chars_written ) > MAX_LOG_LINE_CHARS )
            {
                fprintf( stderr,
                         "nwipe_log: Warning! The log line has been truncated as it exceeded %i characters\n",
                         MAX_LOG_LINE_CHARS );
                line_current_pos = MAX_LOG_LINE_CHARS;
            }
            else
            {
                line_current_pos += chars_written;
            }
        }
    }

    /* The variable argument pointer. */
    va_list ap;

    /* Fetch the argument list. */
    va_start( ap, format );

    /* Print the event. */
    if( line_current_pos < MAX_LOG_LINE_CHARS )
    {
        chars_written =
            vsnprintf( message_buffer + line_current_pos, MAX_LOG_LINE_CHARS - line_current_pos - 1, format, ap );

        if( chars_written < 0 )
        {
            fprintf( stderr, "nwipe_log: snprintf error when writing log line to memory.\n" );
            r = pthread_mutex_unlock( &mutex1 );
            if( r != 0 )
            {
                fprintf( stderr, "nwipe_log: pthread_mutex_unlock failed. Code %i \n", r );
                va_end( ap );
                return;
            }
        }
        else
        {
            if( ( line_current_pos + chars_written ) > MAX_LOG_LINE_CHARS )
            {
                fprintf( stderr,
                         "nwipe_log: Warning! The log line has been truncated as it exceeded %i characters\n",
                         MAX_LOG_LINE_CHARS );
                line_current_pos = MAX_LOG_LINE_CHARS;
            }
            else
            {
                line_current_pos += chars_written;
            }
        }
    }

    fflush( stdout );
    /* Increase the current log element pointer - we will write here, deallocation is done in cleanup() in nwipe.c */
    if( log_current_element == log_elements_allocated )
    {
        log_elements_allocated++;
        result = realloc( log_lines, ( log_elements_allocated ) * sizeof( char* ) );
        if( result == NULL )
        {
            fprintf( stderr, "nwipe_log: realloc failed when adding a log line.\n" );
            r = pthread_mutex_unlock( &mutex1 );
            if( r != 0 )
            {
                fprintf( stderr, "nwipe_log: pthread_mutex_unlock failed. Code %i \n", r );
                va_end( ap );
                return;
            }
        }
        log_lines = result;

        /* Allocate memory for storing a single log message, deallocation is done in cleanup() in nwipe.c */
        message_buffer_length = strlen( message_buffer ) * sizeof( char );
        malloc_result = malloc( ( message_buffer_length + 1 ) * sizeof( char ) );
        if( malloc_result == NULL )
        {
            fprintf( stderr, "nwipe_log: malloc failed when adding a log line.\n" );
            r = pthread_mutex_unlock( &mutex1 );
            if( r != 0 )
            {
                fprintf( stderr, "nwipe_log: pthread_mutex_unlock failed. Code %i \n", r );
                va_end( ap );
                return;
            }
        }
        log_lines[log_current_element] = malloc_result;
    }

    strcpy( log_lines[log_current_element], message_buffer );

    /*
        if( level >= NWIPE_LOG_WARNING )
        {
            vfprintf( stderr, format, ap );
        }
    */

    /* Release the argument list. */
    va_end( ap );

    /*
        if( level >= NWIPE_LOG_WARNING )
        {
            fprintf( stderr, "\n" );
        }
    */

    /* The log file pointer. */
    FILE* fp;

    /* The log file descriptor. */
    int fd;

    if( nwipe_options.logfile[0] == '\0' )
    {
        if( nwipe_options.nogui )
        {
            printf( "%s\n", log_lines[log_current_element] );
            log_elements_displayed++;
        }
    }
    else
    {
        /* Open the log file for appending. */
        fp = fopen( nwipe_options.logfile, "a" );

        if( fp != NULL )
        {

            /* Get the file descriptor of the log file. */
            fd = fileno( fp );

            /* Block and lock. */
            r = flock( fd, LOCK_EX );

            if( r != 0 )
            {
                perror( "nwipe_log: flock:" );
                fprintf( stderr, "nwipe_log: Unable to lock '%s' for logging.\n", nwipe_options.logfile );
                r = pthread_mutex_unlock( &mutex1 );
                if( r != 0 )
                {
                    fprintf( stderr, "nwipe_log: pthread_mutex_unlock failed. Code %i \n", r );

                    /* Unlock the file. */
                    r = flock( fd, LOCK_UN );
                    fclose( fp );
                    return;
                }
            }

            fprintf( fp, "%s\n", log_lines[log_current_element] );

            /* Unlock the file. */
            r = flock( fd, LOCK_UN );

            if( r != 0 )
            {
                perror( "nwipe_log: flock:" );
                fprintf( stderr, "Error: Unable to unlock '%s' after logging.\n", nwipe_options.logfile );
            }

            /* Close the stream. */
            r = fclose( fp );

            if( r != 0 )
            {
                perror( "nwipe_log: fclose:" );
                fprintf( stderr, "Error: Unable to close '%s' after logging.\n", nwipe_options.logfile );
            }
        }
        else
        {
            /* Tell user we can't create/open the log and terminate nwipe */
            fprintf(
                stderr, "\nERROR:Unable to create/open '%s' for logging, permissions?\n\n", nwipe_options.logfile );
            r = pthread_mutex_unlock( &mutex1 );
            if( r != 0 )
            {
                fprintf( stderr, "nwipe_log: pthread_mutex_unlock failed. Code %i \n", r );
            }
            user_abort = 1;
            terminate_signal = 1;
        }
    }

    log_current_element++;

    r = pthread_mutex_unlock( &mutex1 );
    if( r != 0 )
    {
        fprintf( stderr, "nwipe_log: pthread_mutex_unlock failed. Code %i \n", r );
    }
    return;

} /* nwipe_log */

void nwipe_perror( int nwipe_errno, const char* f, const char* s )
{
    /**
     * Wrapper for perror().
     */

    nwipe_log( NWIPE_LOG_ERROR, "%s: %s: %s", f, s, strerror( nwipe_errno ) );

} /* nwipe_perror */

void nwipe_log_OSinfo()
{
    /* Read /proc/version, format and write to the log */

    FILE* fp = NULL;
    char OS_info_temp[MAX_SIZE_OS_STRING + 1];
    char OS_info[MAX_SIZE_OS_STRING + 1];
    int idx;
    int idx2;
    int idx3;
    int idx4;

    /* initialise OS_info & OS_info_temp strings */
    idx = 0;
    while( idx < MAX_SIZE_OS_STRING + 1 )
    {
        OS_info_temp[idx] = 0;
        OS_info[idx] = 0;
        idx++;
    }

    /* Open a pipe to /proc/version for reading */
    fp = popen( "cat /proc/version", "r" );
    if( fp == NULL )
    {
        nwipe_log( NWIPE_LOG_WARNING, "Unable to create a pipe to /proc/version" );
        return;
    }

    /* Read the OS info */
    if( fgets( OS_info_temp, MAX_SIZE_OS_STRING, fp ) == NULL )
    {
        nwipe_log( NWIPE_LOG_WARNING, "fget failed to read /proc/version" );
        fclose( fp );
        return;
    }

    /* Format the string for the log, place on multiple lines as necessary,
     * column aligned, left offset with n (OS_info_Line_offset) spaces */
    idx = 0;
    idx2 = 0;
    idx3 = OS_info_Line_Length;

    while( OS_info_temp[idx] != 0 )
    {
        while( idx2 < idx3 && idx2 < MAX_SIZE_OS_STRING )
        {
            /* remove newlines from the source */
            if( OS_info_temp[idx] == 0x0a )
            {
                idx++;
            }

            /* copy the character */
            OS_info[idx2++] = OS_info_temp[idx++];
        }
        if( OS_info_temp[idx] != 0 )
        {
            OS_info[idx2++] = 0x0a;
            idx4 = 0;

            /* left indent with spaces */
            while( idx4 < OS_info_Line_offset && idx2 < MAX_SIZE_OS_STRING )
            {
                OS_info[idx2++] = ' ';
                idx4++;
            }

            /* calculate idx3 ready for next line */
            idx3 += OS_info_Line_offset + OS_info_Line_Length;
        }
        else
        {
            continue;
        }
    }

    nwipe_log( NWIPE_LOG_INFO, "%s", OS_info );
    fclose( fp );
    return;
}

int nwipe_log_sysinfo()
{
    FILE* fp;
    char path[256];
    int len;
    int r;  // A result buffer.

    /*
     * Remove or add keywords to be searched, depending on what information is to
     * be logged, making sure the last entry in the array is a NULL string. To remove
     * an entry simply comment out the keyword with //
     */

    /* The 0/1 after the keyword determines whether the data for this
     * keyword is displayed when -q (anonymize) has been specified
     * by the user. An quick reminder about multi dimensional arrays, the first
     * []=the keyword (0-21) including the empty string. The second [] is the
     * 1 or 0 value (0 or 1). The third [] is the index value into either string.
     */
    char dmidecode_keywords[][2][24] = {
        { "bios-version", "1" },
        { "bios-release-date", "1" },
        { "system-manufacturer", "1" },
        { "system-product-name", "1" },
        { "system-version", "1" },
        { "system-serial-number", "0" },
        { "system-uuid", "0" },
        { "baseboard-manufacturer", "1" },
        { "baseboard-product-name", "1" },
        { "baseboard-version", "1" },
        { "baseboard-serial-number", "0" },
        { "baseboard-asset-tag", "0" },
        { "chassis-manufacturer", "1" },
        { "chassis-type", "1" },
        { "chassis-version", "1" },
        { "chassis-serial-number", "0" },
        { "chassis-asset-tag", "0" },
        { "processor-family", "1" },
        { "processor-manufacturer", "1" },
        { "processor-version", "1" },
        { "processor-frequency", "1" },
        { "", "" }  // terminates the keyword array. DO NOT REMOVE
    };

    char dmidecode_command[] = "dmidecode -s %s";
    char dmidecode_command2[] = "/sbin/dmidecode -s %s";
    char dmidecode_command3[] = "/usr/bin/dmidecode -s %s";
    char* p_dmidecode_command;

    char cmd[sizeof( dmidecode_keywords ) + sizeof( dmidecode_command2 )];

    unsigned int keywords_idx;

    keywords_idx = 0;

    p_dmidecode_command = 0;

    if( system( "which dmidecode > /dev/null 2>&1" ) )
    {
        if( system( "which /sbin/dmidecode > /dev/null 2>&1" ) )
        {
            if( system( "which /usr/bin/dmidecode > /dev/null 2>&1" ) )
            {
                nwipe_log( NWIPE_LOG_WARNING, "Command not found. Install dmidecode !" );
            }
            else
            {
                p_dmidecode_command = &dmidecode_command3[0];
            }
        }
        else
        {
            p_dmidecode_command = &dmidecode_command2[0];
        }
    }
    else
    {
        p_dmidecode_command = &dmidecode_command[0];
    }

    if( p_dmidecode_command != 0 )
    {

        /* Run the dmidecode command to retrieve each dmidecode keyword, one at a time */
        while( dmidecode_keywords[keywords_idx][0][0] != 0 )
        {
            sprintf( cmd, p_dmidecode_command, &dmidecode_keywords[keywords_idx][0][0] );
            fp = popen( cmd, "r" );
            if( fp == NULL )
            {
                nwipe_log( NWIPE_LOG_WARNING, "nwipe_log_sysinfo: Failed to create stream to %s", cmd );
                return 1;
            }
            /* Read the output a line at a time - output it. */
            while( fgets( path, sizeof( path ) - 1, fp ) != NULL )
            {
                /* Remove any trailing return from the string, as nwipe_log automatically adds a return */
                len = strlen( path );
                if( path[len - 1] == '\n' )
                {
                    path[len - 1] = 0;
                }
                if( nwipe_options.quiet )
                {
                    if( *( &dmidecode_keywords[keywords_idx][1][0] ) == '0' )
                    {
                        nwipe_log(
                            NWIPE_LOG_INFO, "%s = %s", &dmidecode_keywords[keywords_idx][0][0], "XXXXXXXXXXXXXXX" );
                    }
                    else
                    {
                        nwipe_log( NWIPE_LOG_INFO, "%s = %s", &dmidecode_keywords[keywords_idx][0][0], path );
                    }
                }
                else
                {
                    nwipe_log( NWIPE_LOG_INFO, "%s = %s", &dmidecode_keywords[keywords_idx][0][0], path );
                }
            }
            /* close */
            r = pclose( fp );
            if( r > 0 )
            {
                nwipe_log( NWIPE_LOG_WARNING,
                           "nwipe_log_sysinfo(): dmidecode failed, \"%s\" exit status = %u",
                           cmd,
                           WEXITSTATUS( r ) );
                return 1;
            }
            keywords_idx++;
        }
    }
    return 0;
}

void nwipe_log_summary( nwipe_context_t** ptr, int nwipe_selected )
{
    /* Prints two summary tables, the first is the device pass and verification summary
     * and the second is the main summary table detaining the drives, status, throughput,
     * model and serial number.
     *
     * This function also calls the create_pdf() function that creates the PDF erasure
     * report file. A page report on the success or failure of the erasure operation
     */

    int i;
    int idx_src;
    int idx_dest;
    char device[18];
    char status[9];
    char throughput[13];
    char total_throughput_string[13];
    char summary_top_border[256];
    char summary_top_column_titles[256];
    char blank[3];
    char verify[3];
    // char duration[5];
    char duration[314];
    char model[18];
    char serial_no[NWIPE_SERIALNUMBER_LENGTH + 1];
    char exclamation_flag[2];
    int hours;
    int minutes;
    int seconds;
    u64 total_duration_seconds;
    u64 total_throughput;
    nwipe_context_t** c;
    c = ptr;

    exclamation_flag[0] = 0;
    device[0] = 0;
    status[0] = 0;
    throughput[0] = 0;
    summary_top_border[0] = 0;
    summary_top_column_titles[0] = 0;
    blank[0] = 0;
    verify[0] = 0;
    duration[0] = 0;
    model[0] = 0;
    serial_no[0] = 0;
    hours = 0;
    minutes = 0;
    seconds = 0;

    /* A time buffer. */
    time_t t;

    /* A pointer to the system time struct. */
    struct tm* p;

    /* Nothing to do, user never started a wipe so no summary table required. */
    if( global_wipe_status == 0 )
    {
        return;
    }

    /* Print the pass and verifications table */

    /* IMPORTANT: Keep maximum columns (line length) to 80 characters for use with 80x30 terminals, Shredos, ALT-F2 etc
     * --------------------------------01234567890123456789012345678901234567890123456789012345678901234567890123456789-*/
    nwipe_log( NWIPE_LOG_NOTIMESTAMP, "" );
    nwipe_log( NWIPE_LOG_NOTIMESTAMP,
               "******************************** Error Summary *********************************" );
    nwipe_log( NWIPE_LOG_NOTIMESTAMP, "!   Device | Pass Errors | Verifications Errors | Fdatasync I\\O Errors" );
    nwipe_log( NWIPE_LOG_NOTIMESTAMP,
               "--------------------------------------------------------------------------------" );

    for( i = 0; i < nwipe_selected; i++ )
    {
        if( c[i]->pass_errors != 0 || c[i]->verify_errors != 0 || c[i]->fsyncdata_errors != 0 )
        {
            strncpy( exclamation_flag, "!", 1 );
            exclamation_flag[1] = 0;
        }
        else
        {
            strncpy( exclamation_flag, " ", 1 );
            exclamation_flag[1] = 0;
        }

        /* Device name, strip any prefixed /dev/.. leaving up to 6 right justified
         * characters eg "   sda", prefixed with space to 6 characters, note that
         * we are processing the strings right to left */

        idx_dest = 6;
        device[idx_dest--] = 0;
        idx_src = strlen( c[i]->device_name );
        idx_src--;

        nwipe_strip_path( device, c[i]->device_name );

        nwipe_log( NWIPE_LOG_NOTIMESTAMP,
                   "%s %s |  %10llu |           %10llu |           %10llu",
                   exclamation_flag,
                   device,
                   c[i]->pass_errors,
                   c[i]->verify_errors,
                   c[i]->fsyncdata_errors );
    }

    nwipe_log( NWIPE_LOG_NOTIMESTAMP,
               "********************************************************************************" );

    /* Print the main summary table */

    /* initialise */
    total_throughput = 0;

    /* Get the current time. */
    t = time( NULL );
    p = localtime( &t );
    /* IMPORTANT: Keep maximum columns (line length) to 80 characters for use with 80x30 terminals, Shredos, ALT-F2 etc
     * --------------------------------01234567890123456789012345678901234567890123456789012345678901234567890123456789-*/
    nwipe_log( NWIPE_LOG_NOTIMESTAMP, "" );
    nwipe_log( NWIPE_LOG_NOTIMESTAMP,
               "********************************* Drive Status *********************************" );
    nwipe_log( NWIPE_LOG_NOTIMESTAMP, "!   Device | Status | Thru-put | HH:MM:SS | Model/Serial Number" );
    nwipe_log( NWIPE_LOG_NOTIMESTAMP,
               "--------------------------------------------------------------------------------" );
    /* Example layout:
     *    "!     sdb |--FAIL--|  120MB/s | 01:22:01 | WD6788.8488YNHj/ZX677888388-N       "
     * ); "      sdc | Erased |  120MB/s | 01:25:04 | WD6784.8488JKGG/ZX677888388-N       " ); "     sdv | Erased |
     * 120MB/s | 01:19:07 | WD6788.848HHDDR/ZX677888388-N       " ); End of Example layout */

    for( i = 0; i < nwipe_selected; i++ )
    {
        /* Device name, strip any prefixed /dev/.. leaving up to 8 right justified
         * characters eg "   sda", prefixed with space to 8 characters, note that
         * we are processing the strings right to left */

        nwipe_strip_path( device, c[i]->device_name );

        extern int user_abort;

        /* Any errors ? if so set the exclamation_flag and fail message,
         * All status messages should be eight characters EXACTLY !
         */
        if( c[i]->pass_errors != 0 || c[i]->verify_errors != 0 || c[i]->fsyncdata_errors != 0 )
        {
            strncpy( exclamation_flag, "!", 1 );
            exclamation_flag[1] = 0;

            strncpy( status, "-FAILED-", 8 );
            status[8] = 0;

            strcpy( c[i]->wipe_status_txt, "FAILED" );  // copy to context for use by certificate
        }
        else
        {
            if( c[i]->wipe_status == 0 /* && user_abort != 1 */ )
            {
                strncpy( exclamation_flag, " ", 1 );
                exclamation_flag[1] = 0;

                strncpy( status, " Erased ", 8 );
                status[8] = 0;

                strcpy( c[i]->wipe_status_txt, "ERASED" );  // copy to context for use by certificate
            }
            else
            {
                if( c[i]->wipe_status == 1 && user_abort == 1 )
                {
                    strncpy( exclamation_flag, "!", 1 );
                    exclamation_flag[1] = 0;

                    strncpy( status, "UABORTED", 8 );
                    status[8] = 0;

                    strcpy( c[i]->wipe_status_txt, "ABORTED" );  // copy to context for use by certificate
                }
                else
                {
                    /* If this ever happens, there is a bug ! */
                    strncpy( exclamation_flag, " ", 1 );
                    exclamation_flag[1] = 0;

                    strncpy( status, "INSANITY", 8 );
                    status[8] = 0;

                    strcpy( c[i]->wipe_status_txt, "INSANITY" );  // copy to context for use by certificate
                }
            }
        }

        /* Determine the size of throughput so that the correct nomenclature can be used */
        Determine_C_B_nomenclature( c[i]->throughput, throughput, 13 );

        /* write the duration string to the drive context for later use by create_pdf() */
        snprintf( c[i]->throughput_txt, sizeof( c[i]->throughput_txt ), "%s", throughput );

        /* Add this devices throughput to the total throughput */
        total_throughput += c[i]->throughput;

        /* Retrieve the duration of the wipe in seconds and convert to hours and minutes and seconds */

        if( c[i]->start_time != 0 && c[i]->end_time != 0 )
        {
            /* For a summary when the wipe has finished */
            c[i]->duration = difftime( c[i]->end_time, c[i]->start_time );
        }
        else
        {
            if( c[i]->start_time != 0 && c[i]->end_time == 0 )
            {
                /* For a summary in the event of a system shutdown, user abort */
                c[i]->duration = difftime( t, c[i]->start_time );

                /* If end_time is zero, which may occur if the wipe is aborted, then set
                 * end_time to current time. Important to do as endtime is used by
                 * the PDF report function */
                c[i]->end_time = time( &t );
            }
        }

        total_duration_seconds = (u64) c[i]->duration;

        /* Convert binary seconds into three binary variables, hours, minutes and seconds */
        convert_seconds_to_hours_minutes_seconds( total_duration_seconds, &hours, &minutes, &seconds );

        /* write the duration string to the drive context for later use by create_pdf() */
        snprintf( c[i]->duration_str, sizeof( c[i]->duration_str ), "%02i:%02i:%02i", hours, minutes, seconds );

        /* Device Model */
        strncpy( model, c[i]->device_model, 17 );
        model[17] = 0;

        /* Serial No. */
        strncpy( serial_no, c[i]->device_serial_no, NWIPE_SERIALNUMBER_LENGTH );
        serial_no[NWIPE_SERIALNUMBER_LENGTH] = 0;
        model[17] = 0;

        nwipe_log( NWIPE_LOG_NOTIMESTAMP,
                   "%s %s |%s| %s/s | %02i:%02i:%02i | %s/%s",
                   exclamation_flag,
                   device,
                   status,
                   throughput,
                   hours,
                   minutes,
                   seconds,
                   model,
                   serial_no );

        /* Create the PDF report/certificate */
        if( nwipe_options.PDF_enable == 1 )
        // if( strcmp( nwipe_options.PDFreportpath, "noPDF" ) != 0 )
        {
            /* to have some progress indication. can help if there are many/slow disks */
            fprintf( stderr, "." );
            create_pdf( c[i] );
        }
    }

    /* Determine the size of throughput so that the correct nomenclature can be used */
    Determine_C_B_nomenclature( total_throughput, total_throughput_string, 13 );

    /* Blank abbreviations used in summary table B=blank, NB=no blank */
    if( nwipe_options.noblank )
    {
        strcpy( blank, "NB" );
    }
    else
    {
        strcpy( blank, "B" );
    }

    /* Verify abbreviations used in summary table */
    switch( nwipe_options.verify )
    {
        case NWIPE_VERIFY_NONE:
            strcpy( verify, "NV" );
            break;

        case NWIPE_VERIFY_LAST:
            strcpy( verify, "VL" );
            break;

        case NWIPE_VERIFY_ALL:
            strcpy( verify, "VA" );
            break;
    }

    nwipe_log( NWIPE_LOG_NOTIMESTAMP,
               "--------------------------------------------------------------------------------" );
    nwipe_log( NWIPE_LOG_NOTIMESTAMP,
               "[%i/%02i/%02i %02i:%02i:%02i] Total Throughput %s/s, %s, %iR+%s+%s",
               1900 + p->tm_year,
               1 + p->tm_mon,
               p->tm_mday,
               p->tm_hour,
               p->tm_min,
               p->tm_sec,
               total_throughput_string,
               nwipe_method_label( nwipe_options.method ),
               nwipe_options.rounds,
               blank,
               verify );
    nwipe_log( NWIPE_LOG_NOTIMESTAMP,
               "********************************************************************************" );
    nwipe_log( NWIPE_LOG_NOTIMESTAMP, "" );

    /* Log information regarding where the PDF certificate is saved but log after the summary table so
     * this information is only printed once.
     */
    if( strcmp( nwipe_options.PDFreportpath, "noPDF" ) != 0 )
    {
        nwipe_log( NWIPE_LOG_NOTIMESTAMP, "Creating PDF report in %s\n", nwipe_options.PDFreportpath );
    }
}
