StaticCodeReview / clean / giftcardreader.c
giftcardreader.c
Raw
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <stdbool.h>
#include <assert.h>

#include "giftcard.h"

// Interpret a THX-1138 program. We have to be careful since
// this is effectively arbitrary code! We will check:
//   - All register numbers are less than NUM_REGS
//   - All accesses to the message buffer are in bounds
//   - The program counter never goes outside the code buffer
//   - The program never executes more than MAX_STEPS instructions
//     (to prevent infinite loops)
#define NUM_REGS 16
#define MAX_STEPS 100000
#define REG_OK(reg) (reg < NUM_REGS)
#define MPTR_OK() (msg <= mptr && mptr < msg + GC_PROGMSG_SIZE)
// Each instruction is 3 bytes, so the last valid instruction is at GC_PROGRAM_SIZE-3
#define PC_OK() (program <= pc && pc < program + GC_PROGRAM_SIZE - 3)
void animate(char *msg, unsigned char *program) {
    unsigned char regs[NUM_REGS];
    char *mptr = msg;
    unsigned char *pc = program;
    uint64_t steps = 0;
    bool zf = false;
    while (steps < MAX_STEPS) {
        // Ensure program counter is in bounds before fetching instruction
        if (!PC_OK()) break;
        // Decode the current instruction
        unsigned char arg1, arg2;
        enum gift_card_program_op op;
        op = *pc;
        arg1 = *(pc+1);
        arg2 = *(pc+2);
        // Execute
        switch (op) {
            case GC_OP_NOP:
                break;
            case GC_OP_GET:
                if (REG_OK(arg1) && MPTR_OK()) {
                    regs[arg1] = *mptr;
                }
                break;
            case GC_OP_PUT:
                if (REG_OK(arg1) && MPTR_OK()) {
                    *mptr = regs[arg1];
                }
                break;
            case GC_OP_MOV:
                if (MPTR_OK()) {
                    mptr += (char)arg1;
                }
                break;
            case GC_OP_CON:
                if (REG_OK(arg2)) {
                    regs[arg2] = arg1;
                }
                break;
            case GC_OP_XOR:
                if (REG_OK(arg1) && REG_OK(arg2)) {
                    regs[arg1] ^= regs[arg2];
                    zf = !regs[arg1];
                }
                break;
            case GC_OP_ADD:
                if (REG_OK(arg1) && REG_OK(arg2)) {
                    regs[arg1] += regs[arg2];
                    zf = !regs[arg1];
                }
                break;
            case GC_OP_PRN:
                printf("%.*s\n", GC_PROGMSG_SIZE, msg);
                break;
            case GC_OP_END:
                goto done;
            case GC_OP_JMP:
                pc += (char)arg1;
                break;
            case GC_OP_JCC:
                if (zf) pc += (char)arg1;
                break;
            default:
                fprintf(stderr, "invalid opcode %#02x encountered in gift card program\n", op);
                goto done;
        }
        pc += 3;
    }
done:
    return;
}

int get_gift_card_value(struct gift_card *gc) {
    int total = 0;
	for(int i=0; i < gc->number_of_gift_card_records; i++) {
        if (gc->records[i]->rec_type == GC_AMOUNT) {
            total += gc->records[i]->amount.amount;
        }
    }
    return total;
}

void print_gift_card_text(struct gift_card *gc) {
    printf("   Merchant ID: %32.*s\n", GC_MERCHANT_SIZE, gc->merchant_id);
    printf("   Customer ID: %32.*s\n", GC_CUSTOMER_SIZE, gc->customer_id);
    printf("   Num records: %d\n", gc->number_of_gift_card_records);
    for (int i = 0; i < gc->number_of_gift_card_records; i++) {
        struct gift_card_record *gcr = gc->records[i];
        printf("      record:type: %s\n", gift_card_type_str[gcr->rec_type]);
        if (gcr->rec_type == GC_AMOUNT) {
			printf("      amount_added: %d\n", gcr->amount.amount);
            if (gcr->amount.amount > 0) {
                printf("      signature: %32.*s\n", GC_SIGNATURE_SIZE, gcr->amount.signature);
            }
        }
        else if (gcr->rec_type == GC_MESSAGE) {
			printf("      message: %s\n", gcr->message.message_str);
        }
        else if (gcr->rec_type == GC_PROGRAM) {
            printf("      message: %.*s\n", GC_PROGMSG_SIZE, gcr->program.message);
            printf("  [running embedded program]\n");
            animate(gcr->program.message, gcr->program.program);
        }
        else {
            // We should never reach this because errors are checked during
            // parsing. So assert here if we do.
            assert(false && "Unknown record type in printing stage!");
        }
    }
	printf("  Total value: %d\n\n", get_gift_card_value(gc));
}

// Hex encode binary data. Note that out must have sufficient space
// to hold 2*size+1 characters (including the NULL terminator)
void hex_encode(unsigned char *data, int size, char *out) {
    const char *hexchars = "01234567890abcdef";
    for(int i = 0; i < size; i++) {
        out[i*2] = hexchars[(data[i] & 0xf0) >> 4];
        out[i*2+1] = hexchars[data[i] & 0x0f];
    }
    out[size*2] = '\0';
}

// Escapes a JSON string. Note that this dynamically allocates
// the output string, and the caller is responsible for freeing.
char *json_escape(char *s, int s_len) {
    // Conservatively assume that the output may be escaped. Since each
    // escaped character looks like \uXXXX, allocate 6 times the size of
    // the input string, plus 1 for NULL terminator.
    char *output = calloc(6 * s_len + 1, 1);
    char *output_ptr = output;
    if (!output) {
        // If we fail here we can't really recover, so just exit
        perror("calloc");
        exit(1);
    }
    for (int i = 0; i < s_len; i++) {
        int bytes_written = 0;
        switch (s[i]) {
            case '"':
                bytes_written = sprintf(output_ptr, "\\\"");
                break;
            case '\\':
                bytes_written = sprintf(output_ptr, "\\\\");
                break;
            case '\b': 
                bytes_written = sprintf(output_ptr, "\\b");
                break;
            case '\f':
                bytes_written = sprintf(output_ptr, "\\f");
                break;
            case '\n':
                bytes_written = sprintf(output_ptr, "\\n");
                break;
            case '\r':
                bytes_written = sprintf(output_ptr, "\\r");
                break;
            case '\t':
                bytes_written = sprintf(output_ptr, "\\t");
                break;
            default:
                if ('\x00' <= s[i] && s[i] <= '\x1f') {
                    bytes_written = sprintf(output_ptr, "\\u%04d", (int)s[i]);
                }
                else if ((unsigned char)s[i] > '\x7f') {
                    // These aren't valid string characters, so drop them
                    bytes_written = 0;
                }
                else {
                    // Character can go directly in the output
                    *output_ptr = s[i];
                    bytes_written = 1;
                }
        }
        output_ptr += bytes_written;
    }
    // NULL-terminate
    *output_ptr = '\0';
    return output;
}

// Output in JSON format. Somewhat messy because we have to ensure
// any strings we print are properly escaped.
void print_gift_card_json(struct gift_card *gc) {
    char *escaped = NULL;
    printf("{\n");
    escaped = json_escape(gc->merchant_id, GC_MERCHANT_SIZE);
    printf("  \"merchant_id\": \"%32s\",\n", escaped);
    free(escaped);
    escaped = json_escape(gc->customer_id, GC_CUSTOMER_SIZE);
    printf("  \"customer_id\": \"%32s\",\n", escaped);
    free(escaped);
    printf("  \"total_value\": %d,\n", get_gift_card_value(gc));
    printf("  \"records\": [\n");
    for (int i = 0; i < gc->number_of_gift_card_records; i++) {
        struct gift_card_record *gcr = gc->records[i];
        printf("    {\n");
        printf("      \"record_type\": \"%s\",\n", gift_card_type_str[gcr->rec_type]);
        if (gcr->rec_type == GC_AMOUNT) {
            printf("      \"amount_added\": %d,\n",gcr->amount.amount);
            if (gcr->amount.amount > 0) {
                escaped = json_escape(gcr->amount.signature, GC_SIGNATURE_SIZE);
                printf("      \"signature\": \"%32s\"\n", escaped);
                free(escaped);
            }
        }
        else if (gcr->rec_type == GC_MESSAGE) {
            escaped = json_escape(gcr->message.message_str, strlen(gcr->message.message_str));
			printf("      \"message\": \"%s\"\n", escaped);
            free(escaped);
        }
        else if (gcr->rec_type == GC_PROGRAM) {
            escaped = json_escape(gcr->program.message, GC_PROGMSG_SIZE);
			printf("      \"message\": \"%s\",\n", escaped);
            free(escaped);
            char program_hex[GC_PROGRAM_SIZE*2+1];
            hex_encode(gcr->program.program, GC_PROGRAM_SIZE, program_hex);
            printf("      \"program\": \"%s\"\n", program_hex);
        }
        else {
            // We should never reach this because errors are checked during
            // parsing. So assert here if we do.
            assert(false && "Unknown record type in printing stage!");
        }

        // JSON forbids trailing commas
        if (i < gc->number_of_gift_card_records-1)
            printf("    },\n");
        else
            printf("    }\n");
    }
    printf("  ]\n");
    printf("}\n");
}

void free_gift_card(struct gift_card *gc) {
    // Free everything associated with a gift card
    if (!gc) return;
    if (gc->records) {
        for (int i = 0; i < gc->number_of_gift_card_records; i++) {
            struct gift_card_record *gcr = gc->records[i];
            if (gcr) {
                // The only one that has dynamically allocated data is the
                // GC_MESSAGE type, so handle it specially
                if (gcr->rec_type == GC_MESSAGE) {
                    if(gcr->message.message_str) free(gcr->message.message_str);
                    gcr->message.message_str = NULL;
                }
                free(gcr);
                gcr = NULL;
                gc->records[i] = NULL;
            }
        }
        free(gc->records);
        gc->records = NULL;
    }
    free(gc);
}

/* Parses the file into an in-memory data structure 
 * Parameters:
 *    fp: an open FILE pointer
 * Return value:
 *    a struct gift_card pointer representing the parsed data,
 *    or NULL if an error occurred during parsing
 */
struct gift_card * parse_gift_card(FILE *fp) {
    struct gift_card *gc = NULL;

    // We don't want to rely on the sizes in the file, so instead
    // we will get the filesize from the OS here.
    if (-1 == fseek(fp, 0, SEEK_END)) {
        perror("fseek");
        return NULL;
    }
    long file_size = ftell(fp);
    if (file_size < 0) {
        perror("ftell");
        return NULL;
    }
    rewind(fp);

    // Keep track of how much we've parsed
    long bytes_remaining = file_size;

    // Size as reported by the file
    uint32_t reported_file_size = 0;
    if (1 != fread(&reported_file_size, sizeof(uint32_t), 1, fp)) {
        perror("fread");
        goto error_ret;
    }
    // Check that it actually matches, to detect corrupt files
    if (reported_file_size != file_size) {
        fprintf(stderr, "error: file size on disk (%ld) does not match size in header (%d), aborting\n",
                file_size, reported_file_size);
        goto error_ret;
    }
    bytes_remaining -= sizeof(uint32_t);

    gc = calloc(sizeof(struct gift_card), 1);
    if (!gc) {
        goto error_ret;
    }

    // Header info: merchant and customer IDs
    if (1 != fread(gc->merchant_id, GC_MERCHANT_SIZE, 1, fp)) {
        perror("fread");
        goto error_ret;
    }
    bytes_remaining -= GC_MERCHANT_SIZE;
    if (1 != fread(gc->customer_id, GC_CUSTOMER_SIZE, 1, fp)) {
        perror("fread");
        goto error_ret;
    }
    bytes_remaining -= GC_CUSTOMER_SIZE;

    // Get number of records reported. We won't use this but we will check
    // at the end to make sure it matches.
    uint32_t num_records = 0;
    if (1 != fread(&num_records, sizeof(uint32_t), 1, fp)) {
        perror("fread");
        goto error_ret;
    }
    bytes_remaining -= sizeof(uint32_t);

    // Main record parse loop. Keep going until we run out of data
    // or hit EOF
    while (!feof(fp) && bytes_remaining > 0) {
        uint32_t rec_size = 0;
        uint32_t rec_type = 0;
        if (1 != fread(&rec_size, sizeof(uint32_t), 1, fp)) {
            perror("fread");
            goto error_ret;
        }
        bytes_remaining -= sizeof(uint32_t);
        if (1 != fread(&rec_type, sizeof(uint32_t), 1, fp)) {
            perror("fread");
            goto error_ret;
        }
        bytes_remaining -= sizeof(uint32_t);

        // Grow the array so we can store a pointer to this record
        gc->number_of_gift_card_records++;
        gc->records = realloc(gc->records, gc->number_of_gift_card_records * sizeof(struct gift_card_record));
        if (!gc->records) {
            perror("realloc");
            goto error_ret;
        }
        // Allocate the record itself. Since we are using a union each record is the same
        // size, so we can just allocate it here.
        int idx = gc->number_of_gift_card_records - 1;
        gc->records[idx] = calloc(sizeof(struct gift_card_record), 1);
        if (!gc->records[idx]) {
            perror("calloc");
            // We didn't actually successfully allocate, so reduce the number of records
            // by one so that the cleanup function doesn't try to free it
            gc->number_of_gift_card_records--;
            goto error_ret;
        }
        
        // Read in the record
        struct gift_card_record *gcr = gc->records[idx];
        gcr->rec_type = rec_type;
        if (gcr->rec_type == GC_AMOUNT) {
            if (1 != fread(&gcr->amount.amount, sizeof(int32_t), 1, fp)) {
                perror("fread");
                goto error_ret;
            }
            bytes_remaining -= sizeof(int32_t);

            if (gcr->amount.amount >= 0) {
                if (1 != fread(gcr->amount.signature, GC_SIGNATURE_SIZE, 1, fp)) {
                    perror("fread");
                    goto error_ret;
                }
                bytes_remaining -= GC_SIGNATURE_SIZE;
            }
        }
        else if (gcr->rec_type == GC_MESSAGE) {
            // Slightly tricky since we don't trust the record size to be accurate. 
            // Take the min of the declared record size and the amount of data
            // left in the file to read.
            // Also note that the rec_size includes the record header, which is
            // two uint32_t fields (8 bytes)
            int message_len = 0;
            int rec_remaining = rec_size - 2*sizeof(uint32_t);
            if (rec_remaining > bytes_remaining) {
                message_len = bytes_remaining;
            }
            else {
                message_len = rec_remaining;
            }
            gcr->message.message_str = calloc(message_len, 1);
            if (!gcr->message.message_str) {
                perror("calloc");
                goto error_ret;
            }
            if (1 != fread(gcr->message.message_str, message_len, 1, fp)) {
                perror("fread");
                goto error_ret;
            }
            bytes_remaining -= message_len;
            // Ensure the string is NULL-terminated
            gcr->message.message_str[message_len-1] = '\0';
        }
        else if (gcr->rec_type == GC_PROGRAM) {
            if (1 != fread(gcr->program.message, GC_PROGMSG_SIZE, 1, fp)) {
                perror("fread");
                goto error_ret;
            }
            bytes_remaining -= GC_PROGMSG_SIZE;
            if (1 != fread(gcr->program.program, GC_PROGRAM_SIZE, 1, fp)) {
                perror("fread");
                goto error_ret;
            }
            bytes_remaining -= GC_PROGRAM_SIZE;
        }
        else {
            fprintf(stderr, "unknown record type encountered: %d; aborting\n", gcr->rec_type);
            goto error_ret;
        }
    }

    // Check that the number of records we read is correct
    if (gc->number_of_gift_card_records != num_records) {
        fprintf(stderr, "Number of records reported in file (%u) does not match"
                        "number actually read (%d). Aborting.\n",
                        num_records, gc->number_of_gift_card_records);
        goto error_ret;
    }

    // If we get here, everything is okay! Return the gift card data.
    return gc;

error_ret:
    // Free resources in case of error
    free_gift_card(gc);
    return NULL;
}

int main(int argc, char **argv) {
    if (argc != 3) {
        fprintf(stderr, "usage: %s <1|2> <filename>\n", argv[0]);
        fprintf(stderr, "   use 1 for text output, 2 for json output\n");
        return 1;
    }

    FILE *fp = fopen(argv[2], "rb");
    if (!fp) {
        fprintf(stderr, "couldn't open %s\n", argv[2]);
        return 1;
    }

    struct gift_card *gc = parse_gift_card(fp);
    if (!gc) {
        fprintf(stderr, "error reading gift card %s\n", argv[2]);
        return 1;
    }

    // Print either text or JSON format
    if (strcmp(argv[1], "1") == 0) {
        print_gift_card_text(gc);
    }
    else if (strcmp(argv[1], "2") == 0) {
        print_gift_card_json(gc);
    }
    else {
        fprintf(stderr, "invalid output format: %s\n", argv[1]);
        free_gift_card(gc);
        return 1;
    }

    free_gift_card(gc);
    return 0;
}