aboutsummaryrefslogtreecommitdiff
/*
  ihex
  Copyright (C) 2025 Thomas Albers Raviola <thomas@thomaslabs.org>

  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, either version 3 of the License, or
  (at your option) any later version.

  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, see <https://www.gnu.org/licenses/>.
 */

#include <ihex.h>
#include <util.h>
#include <array.h>

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

#define MAX_IHEX_BYTE_COUNT 255

enum record_type {
	REC_DATA = 0x00,
	REC_EOF = 0x01,
	REC_EXT_SEG_ADDR = 0x02,
	REC_START_SEG_ADDR = 0x03,
	REC_EXT_LIN_ADDR = 0x04,
	REC_START_LIN_ADDR = 0x05,
	REC_UNKNOWN = 0xFF
};

struct record {
	unsigned int length;
	unsigned int offset;
	unsigned int type;
	unsigned int checksum;
};

static const char *current_file;

static int
skip_to_record(FILE *file)
{
	int c;
	while ((c = fgetc(file)) != EOF) {
		if (c == ':')
			return 0;
	}

	return EOF;
}

#define HEX_UINT8 2
#define HEX_UINT16 4

static unsigned int
read_hex(int digits, FILE *file)
{
	int c;
	int i;
	unsigned int ret = 0;

	for (i = digits - 1; i >= 0; --i) {
		c = fgetc(file);

		if (c == EOF)
			die(current_file, "Unexpected end of line\n");

		if (!isxdigit(c)) {
			die(current_file, "Invalid character at position %ld\n",
			    ftell(file));
		}

		if (c >= 'a')
			c = c - 'a' + 10;
		else if (c >= 'A')
			c = c - 'A' + 10;
		else
			c = c - '0';

		ret |= c << (4 * i);
	}

	return ret;
}

static unsigned int
read_data_record(const struct record *record, struct array *data, FILE *file)
{
	char *p;
	unsigned int i;
	unsigned int b;
	unsigned int checksum = 0;

	for (i = 0; i < record->length; ++i) {
		p = array_append(data, NULL);
		b = read_hex(HEX_UINT8, file);
		*p = b;
		checksum += b;
	}

	return checksum;
}

#define MAX(a, b) ((a) > (b) ? (a) : (b))
#define MIN(a, b) ((a) > (b) ? (b) : (a))

static void
rebase_blocks(size_t num_blocks, struct block *blocks, long base_address)
{
	size_t i;
	long offset = LONG_MAX;

	if (base_address < 0)
		return;

	for (i = 0; i < num_blocks; ++i)
		offset = MIN(offset, blocks[i].address);

	offset = base_address - offset;

	for (i = 0; i < num_blocks; ++i)
		blocks[i].address += offset;
}

int
read_ihex_file(const char *filename, struct array *blocks, struct array *data,
	       long base_address)
{
	size_t num_blocks;
	size_t block_offset = blocks->length;
	struct block *new_blocks;

	FILE *file = fopen(filename, "r");
	current_file = filename;

	while (!skip_to_record(file)) {
		unsigned int checksum = 0;
		struct record record;
		struct block *block;

		record.length = read_hex(HEX_UINT8, file);
		record.offset = read_hex(HEX_UINT16, file);
		record.type = read_hex(HEX_UINT8, file);

		checksum += record.length;
		checksum += ((record.offset >> 8) + record.offset) & 0xFF;
		checksum += record.type;

		if (record.type == REC_EOF)
			break;

		switch (record.type) {
		case REC_DATA:
			block = array_append(blocks, NULL);
			block->address = record.offset;
			block->length = record.length;
			block->offset = data->length;
			block->filename = filename;
			checksum += read_data_record(&record, data, file);
			break;
		default:
			die(current_file, "Invalid record type\n");
		}

		record.checksum = read_hex(HEX_UINT8, file);

		if ((checksum + record.checksum) & 0xFF)
			die(current_file, "Checksum mismatch\n", filename);
	}

	fclose(file);

	num_blocks = blocks->length - block_offset;
	new_blocks = &((struct block *)blocks->data)[block_offset];
	rebase_blocks(num_blocks, new_blocks, base_address);
	return 0;
}

static int
blocks_collide(const struct block *a, const struct block *b)
{
	return (((a->address >= b->address)
		 && (a->address < b->address + b->length))
		|| ((b->address >= a->address)
		    && (b->address < a->address + a->length)));
}

static int
block_compar(const void *a, const void *b) {
	unsigned long x = ((const struct block *)a)->address;
	unsigned long y = ((const struct block *)b)->address;

	if (x < y)
		return -1;
	if (x > y)
		return 1;
	return 0;
}

int
write_hex(struct array *_blocks, struct array *_data, FILE *file)
{
	unsigned int i;
	unsigned long pos = 0;
	struct block *b;
	struct block *blocks;
	size_t num_blocks = _blocks->length;
	unsigned char *data;

	array_sort(_blocks, block_compar);
	blocks = (struct block *)_blocks->data;
	data = (unsigned char *)_data->data;

	for (i = 0; i < num_blocks; ++i) {
		b = &blocks[i];

		if (i < num_blocks - 1 && blocks_collide(&b[0], &b[1])) {
			fprintf(stderr,
				"Block collision between files %s and %s\n",
				b[0].filename, b[1].filename);
			return -1;
		}

		for (; pos < b->address; ++pos)
			fputc(0xFF, file);

		fwrite(&data[b->offset], 1, b->length, file);
		pos += b->length;
	}

	return 0;
}