cscg22-gearboy

CSCG 2022 Challenge 'Gearboy'
git clone https://git.sinitax.com/sinitax/cscg22-gearboy
Log | Files | Refs | sfeed.txt

ihx_file.c (9743B)


      1// This is free and unencumbered software released into the public domain.
      2// For more information, please refer to <https://unlicense.org>
      3// bbbbbr 2020
      4
      5#include <stdio.h>
      6#include <string.h>
      7#include <stdlib.h>
      8#include <unistd.h>
      9#include <stdbool.h>
     10#include <stdint.h>
     11
     12#include "areas.h"
     13#include "ihx_file.h"
     14
     15// Example data to parse from a .ihx file
     16// No area names
     17// : 01 0020 00 E9 F6
     18// S BB AAAA RR DD CC
     19//
     20// S:    Start (":") (1 char)
     21// BB:   ByteCount   (2 chars, one byte)
     22// AAAA: Address     (4 chars, two bytes)
     23// RR:   Record Type (2 chars, one byte. 00=data, 01=EOF, Others)
     24// DD:   Data        (<ByteCount>  bytes)
     25// CC:   Checksum    (2 chars, one byte. Sum record bytes, 2's complement of LSByte)
     26/*
     27:01002000E9F6
     28:05002800220D20FCC9BF
     29:070030001A22130D20FAC98A
     30.
     31.
     32.
     33:00000001FF (EOF indicator)
     34*/
     35
     36/* TODO/WARNING/BUG: 100% full banks
     37It may not be possible to easily tell the difference between a perfectly filled
     38bank (100% and no more) and an adjacent partially filled bank that starts at
     39zero - versus - the first bank overflowed into the second that is empty.
     40
     41Currently the 100% bank will get merged into the next one and present as overflow
     42*/
     43
     44
     45#define BANK_NUM(addr)  ((addr & 0xFFFFC000U) >> 14)
     46#define ADDR_UNSET      0xFFFFFFFEU
     47
     48#define MAX_STR_LEN     4096
     49#define IHX_DATA_LEN_MAX 255
     50#define IHX_REC_LEN_MIN  (1 + 2 + 4 + 2 + 0 + 2) // Start(1), ByteCount(2), Addr(4), Rec(2), Data(0..255x2), Checksum(2)
     51
     52// IHX record types
     53#define IHX_REC_DATA     0x00U
     54#define IHX_REC_EOF      0x01U
     55#define IHX_REC_EXTSEG   0x02U
     56#define IHX_REC_STARTSEG 0x03U
     57#define IHX_REC_EXTLIN   0x04U
     58#define IHX_REC_STARTLIN 0x05U
     59
     60typedef struct ihx_record {
     61    uint16_t length;
     62    uint32_t byte_count;
     63    uint32_t address;
     64    uint32_t address_end;
     65    uint32_t type;
     66    uint32_t checksum; // Would prefer this be a uint8_t, but mingw sscanf("%2hhx") has a buffer overflow that corrupts adjacent data
     67} ihx_record;
     68
     69uint32_t g_address_upper;
     70bool     g_option_warnings_as_errors = false;
     71
     72void set_option_warnings_as_errors(bool new_val) {
     73    g_option_warnings_as_errors = new_val;
     74}
     75
     76
     77// Return false if any character isn't a valid hex digit
     78int check_hex(char * c) {
     79    while (*c != '\0') {
     80        if ((*c >= '0') && (*c <= '9'))
     81            c++;
     82        if ((*c >= 'A') && (*c <= 'F'))
     83            c++;
     84        if ((*c >= 'a') && (*c <= 'f'))
     85            c++;
     86        else
     87            return false;
     88    }
     89
     90    return true;
     91}
     92
     93
     94// Parse and validate an IHX record
     95int ihx_parse_and_validate_record(char * p_str, ihx_record * p_rec) {
     96
     97        int calc_length = 0;
     98        int c;
     99        uint32_t ctemp, checksum_calc = 0; // Avoid mingw sscanf("%2hhx") buffer overflow with uint8_t
    100
    101        // Remove trailing CR and LF
    102        p_rec->length = strlen(p_str);
    103        for (c = 0;c < p_rec->length;c++) {
    104            if (p_str[c] == '\n' || p_str[c] == '\r') {
    105                p_str[c] = '\0';   // Replace char with string terminator
    106                p_rec->length = c; // Shrink length to truncated size
    107                break;             // Exit loop after finding first CR or LF
    108            }
    109        }
    110
    111        // Only parse lines that start with ':' character (Start token for IHX record)
    112        if (p_str[0] != ':') {
    113            printf("Warning: IHX: Invalid start of line token for line: %s \n", p_str);
    114            return false;
    115        }
    116
    117       // Require minimum length
    118        if (p_rec->length < IHX_REC_LEN_MIN) {
    119            printf("Warning: IHX: Invalid line, too few characters: %s. Is %d, needs at least %d \n", p_str, p_rec->length, IHX_REC_LEN_MIN);
    120            return false;
    121        }
    122
    123        // Only hex characters are allowed after start token
    124        p_str++; // Advance past Start code
    125        if (check_hex(p_str)) {
    126            printf("Warning: IHX: Invalid line, non-hex characters present: %s\n", p_str);
    127            return false;
    128        }
    129
    130        // Read record header: byte count, start address, type
    131        sscanf(p_str, "%2x%4x%2x", &p_rec->byte_count, &p_rec->address, &p_rec->type);
    132        p_str += (2 + 4 + 2);
    133
    134
    135        // Require expected data byte count to fit within record length (at 2 chars per hex byte)
    136        calc_length = IHX_REC_LEN_MIN + (p_rec->byte_count * 2);
    137        if (p_rec->length != calc_length) {
    138            printf("Warning: IHX: byte count doesn't match length available in record! Record length = %d, Calc length = %d, bytecount = %d \n", p_rec->length, calc_length, p_rec->byte_count);
    139            return false;
    140        }
    141
    142        // Is this an extended linear address record? Read in offset address if so
    143        if (p_rec->type == IHX_REC_EXTLIN) {
    144            sscanf(p_str, "%4x", &g_address_upper);
    145            g_address_upper <<= 16; // Shift into upper 16 bits of address space
    146        }
    147        else if (p_rec->type == IHX_REC_DATA) {
    148
    149            // Don't process records with zero bytes of length
    150            if (p_rec->byte_count == 0) {
    151                printf("Warning: IHX: Zero length record starting at %x\n", p_rec->address);
    152                return false;
    153            }
    154
    155            // Apply extended linear address (upper 16 bits of address space)
    156            // Calculate end address
    157            p_rec->address |= g_address_upper;
    158            p_rec->address_end = p_rec->address + p_rec->byte_count - 1;
    159        }
    160
    161
    162        // Read data segment and calculate checsum of data + headers
    163        checksum_calc = p_rec->byte_count + (p_rec->address & 0xFF) + ((p_rec->address >> 8) & 0xFF) + p_rec->type;
    164        for (c = 0;c < p_rec->byte_count;c++) {
    165            sscanf(p_str, "%2x", &ctemp);
    166            p_str += 2;
    167            checksum_calc += ctemp;
    168        }
    169
    170        // Final calculated checeksum is 2's complement of LSByte
    171        checksum_calc = (((checksum_calc & 0xFF) ^ 0xFF) + 1) & 0xFF;
    172
    173        // Read checksum from data
    174        sscanf(p_str, "%2x", &p_rec->checksum);
    175        p_str += 2;
    176
    177        if (p_rec->checksum != checksum_calc) {
    178            printf("Warning: IHX: record checksum %x didn't match calculated checksum %x\n", p_rec->checksum, checksum_calc);
    179            return false;
    180        }
    181
    182        // For records that start in banks above the unbanked region (0x000 - 0x3FFF)
    183        // Warn (but don't error) if they cross the boundary between different banks
    184        if ((p_rec->address >= 0x00004000U) &&
    185            ((p_rec->address & 0xFFFFC000U) != (p_rec->address_end & 0xFFFFC000U))) {
    186            printf("Warning: Write from one bank spans into the next. %x -> %x (bank %d -> %d)\n",
    187                   p_rec->address, p_rec->address_end, BANK_NUM(p_rec->address), BANK_NUM(p_rec->address_end));
    188        }
    189
    190        return true;
    191}
    192
    193
    194int ihx_file_process_areas(char * filename_in) {
    195
    196    int  ret = EXIT_SUCCESS; // default to success
    197    char cols;
    198    char strline_in[MAX_STR_LEN] = "";
    199    FILE * ihx_file = fopen(filename_in, "r");
    200    area_item area;
    201    ihx_record ihx_rec;
    202
    203    areas_init();
    204
    205    // Initialize global upper address modifier
    206    g_address_upper = 0x0000;
    207
    208    // Initialize area record
    209    area.start = ADDR_UNSET;
    210    area.end   = ADDR_UNSET;
    211
    212
    213    if (ihx_file) {
    214
    215        // Read one line at a time into \0 terminated string
    216        while (fgets(strline_in, sizeof(strline_in), ihx_file) != NULL) {
    217
    218            // Parse record, skip if fails validation
    219            if (!ihx_parse_and_validate_record(strline_in, &ihx_rec))
    220                continue;
    221
    222            // Process the pending record and exit if last record (EOF)
    223            // Also ignore non-default data records (don't seem to occur for gbz80)
    224            if (ihx_rec.type == IHX_REC_EOF) {
    225                if (!areas_add(&area) && g_option_warnings_as_errors)
    226                    ret = EXIT_FAILURE;
    227                continue;
    228            } else if (ihx_rec.type == IHX_REC_EXTLIN) {
    229                // printf("Extended linear address changed to %08x %s\n\n\n", g_address_upper, strline_in);
    230                continue;
    231            } else if (ihx_rec.type != IHX_REC_DATA) {
    232                printf("Warning: IHX: dropped record %s of type %d\n", strline_in, ihx_rec.type);
    233                continue;
    234            }
    235
    236            // Records are left pending (non-processed) until they don't merge
    237            // with the current incoming record *or* the final (EOF) record is found.
    238
    239            // Try to merge with (pending) previous record if it's address-adjacent,
    240            // except when the new record starts or ends on a bank boundary
    241            // (this reduces count from 1000's since most are only 32 bytes long)
    242            if ((ihx_rec.address == area.end + 1) && ((ihx_rec.address & 0x00003FFFU) != 0x00000000U)) {
    243                area.end = ihx_rec.address_end;  // append to previous area
    244            } else if ((ihx_rec.address_end == area.start + 1) && !((ihx_rec.address_end & 0x00003FFFU) != 0x00003FFFU)) {
    245                area.start = ihx_rec.address;    // pre-pend to previous area
    246            } else {
    247                // New record was *not* adjacent to last,
    248                // so process the last/pending record
    249                if (area.start != ADDR_UNSET) {
    250                    if (!areas_add(&area) && g_option_warnings_as_errors)
    251                        ret = EXIT_FAILURE;
    252                }
    253                // Now queue current record as pending for next loop
    254                area.start = ihx_rec.address;
    255                area.end   = ihx_rec.address + ihx_rec.byte_count - 1;
    256            }
    257
    258        } // end: while still lines to process
    259
    260        fclose(ihx_file);
    261
    262    } // end: if valid file
    263    else {
    264        printf("Problem with filename or unable to open file! %s\n", filename_in);
    265        ret = EXIT_FAILURE;
    266    }
    267
    268    areas_cleanup();
    269    return ret;
    270}
    271