Mimic linux xxd command in C

369 Views Asked by At

I have the problem mostly solved, I just can't get the last line to be formatted correctly with spaces. I am close, but I need another set of eyes on this

#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
#include <string.h>

const int MAX_CHAR = 16;

int main(int argc, char *argv[])
{
    //command line argument for filename
    char *fileName = argv[1];
    
    //open file and check for validity
    FILE *file = fopen(fileName, "r");
    if (file == NULL)
    {
        printf("The file was not opened correctly\n");
        return -1;
    }

    
    char line[MAX_CHAR];   //array to store characters
    int lineCount = 0;     //counter for hexadecimal line display
    int charCount = 0;     //counter for spacing of hex ascii values
    int breakIndex = MAX_CHAR;  //integer to save what index the array is stopped at EOF
    
    //cycle through every 16 characters
    while(!feof(file)) 
    {
        //read 16 characters
        for(int i = 0; i<breakIndex ; i++)
        {
            
            line[i]=fgetc(file);
            if (feof(file))
            {
                breakIndex = i;
                line[i]='\0';
                break;
            }
        }
       
        //print line count in hexadecimal
        printf("%.08x: ",lineCount);
        
        //print hexadecimal for each character
        for(int i = 0; i<breakIndex; i++)
        {
            printf("%.02x",line[i]);
            charCount++;
            if(charCount % 2 == 0)
            {
                printf(" ");
            }
        }

        //spaces for formatting output
        if(breakIndex == MAX_CHAR)
        {
            printf(" ");
        }
        else
        {
            int spaces = (MAX_CHAR - breakIndex) * 2 ;
            //printf("----%d-----",spaces);
            for(int i = 0 ; i< spaces ; i++)    
            {
                printf(" ");
            }
        }
        
        //print actual characters
        for(int i = 0; i<breakIndex ; i++)
        {
            if(!isprint(line[i]))
            {
                line[i]= '.';
            }
        }
        line[16] = '\0';
        printf("%s",line);
        printf("\n");

        //increment line count
        lineCount+=16;
    }

    //close file
    fclose(file);
    return 0;
}

My problem lies in the "spaces for formatting output section of code. I have accounted for the hexadecimal digits but I can't formulate how to account for the other spaces needed.

this is my output this is the actual xxd output I need this to work for any length file given

1

There are 1 best solutions below

0
Oka On

To largely summarize the comments:

const int MAX_CHAR = 16;
char line[MAX_CHAR];

line is an array (variable-length) of length 16, with the valid indicies [0, 15].

line[16] = '\0' indexes a seventeenth element, that does not exist. This is out-of-bounds array access, which invokes Undefined Behaviour.

while(!feof(file)) is always wrong - avoid it. Test the results of your I/O functions directly (important: use int type to hold the value returned by fgetc, if you want to reliably test for EOF. See below.).

char *fileName = argv[1];
FILE *file = fopen(fileName, "r");

If argv[1] is NULL (i.e., argc < 2), this also invokes UB. (Additionally, xxd mimicry would involve reading from stdin if no input file is specified, but we can put that aside for now.)


Generally speaking, errors should be written to stderr.

If you would like to avoid null-terminating your buffer, you can print up to a fixed amount of characters from a buffer with printf("%.*s", (int) n, buffer).


One important thing to note is that the %x specifier expects an unsigned int argument, and printf arguments are subject to integer promotions.

char is a unique type, distinct from signed char or unsigned char, and its signed-ness is implementation defined. This uniqueness is the same reason you cannot use char to reliably test for the negative value of EOF (as in the return value from fgetc), as the value may become indistinguishable from another valid value in the event that char is unsigned.

Use unsigned char for your buffer (and %hhx for your specifier, if you want to be super-correct about it).

(Without this, you may get large hexadecimal values from signed bytes in your file. Think binary.)

lineCount should also be unsigned int (or use size_t, and %zx).


As for spacing, the total amount of characters to write per line for the hexadecimal pairs block is

(MAX_CHAR) * 2 + (MAX_CHAR / 2) + (MAX_CHAR & 1)

(Assuming integer division.)

Broken down, the format is:

[file offset][:][space][hexadecimal octet pairs, delimiters][space][printable data][newline]

That said, a simple way to do this is to is to always iterate MAX_CHAR times, and decide if you are printing a 2-character hex octet, or two spaces when you run out of valid octets. A delimiting space is printed on every odd-iteration or always on the final iteration.

Following this, a modified version of your example looks like:

#include <ctype.h>
#include <stdio.h>

#define MAX_CHAR 16

int main(int argc, char *argv[])
{
    if (argc < 2) {
        fputs("Insufficient arguments.\n", stderr);
        return 1;
    }

    char *fileName = argv[1];
    FILE *file = fopen(fileName, "r");

    if (!file) {
        perror(fileName);
        return 1;
    }

    unsigned lineCount = 0;
    int reading = 1;

    while (reading) {
        unsigned char line[MAX_CHAR] = { 0 };
        int bytes_read = 0;

        for (int i = 0; i < MAX_CHAR; i++) {
            int ch = fgetc(file);

            if (EOF == ch) {
                reading = 0;
                break;
            }

            line[i] = ch;
            bytes_read++;
        }

        if (!bytes_read)
            break;

        printf("%08x: ", lineCount);

        for (int i = 0; i < MAX_CHAR; i++) {
            if (i < bytes_read) {
                printf("%02hhx", line[i]);

                if (!isprint(line[i])) {
                    line[i] = '.';
                }
            } else {
                printf("  ");
            }

            if (i % 2 != 0 || i == MAX_CHAR - 1) {
                printf(" ");
            }
        }

        putchar(' ');
        printf("%.*s\n", (int) bytes_read, line);
        lineCount += MAX_CHAR;
    }

    fclose(file);
}

Alternatively, this can be done with fread, reading chunks of data:

#include <ctype.h>
#include <stdio.h>

#define CHUNK_SIZE 16

static void xxd_default(FILE *file)
{
    unsigned char buffer[CHUNK_SIZE];
    size_t offset = 0;
    size_t bytes;

    while ((bytes = fread(buffer, 1, sizeof buffer, file))) {
        printf("%08zx: ", offset++ * sizeof buffer);

        for (size_t i = 0; i < sizeof buffer; i++) {
            if (i < bytes)
                printf("%02hhx", buffer[i]);
            else
                fputs("  ", stdout);

            /* every other, always last */
            if (i & 1 || sizeof buffer - 1 == i)
                putchar(' ');
        }

        putchar(' ');

        for (size_t i = 0; i < bytes; i++)
            putchar(isprint(buffer[i]) ? buffer[i] : '.');

        putchar('\n');
    }
}

int main(int argc, char **argv)
{
    FILE *file = stdin;

    if (argc > 1) {
        file = fopen(argv[1], "r");

        if (!file) {
            perror(argv[1]);
            return 1;
        }
    }

    xxd_default(file);

    if (argc > 1)
        return 0 != fclose(file);
}