Kjetil's Information Center: A Blog About My Projects

T64 PRG Dumper

This is almost the same as my previous D64 PRG Dumper except this time operating on T64 files. These are supposed to be tape images instead of disk images, commonly used by Commodore 64 emulators. It attempts to extract all files as programs (PRG files) and forcefully adds the included start address (typically 0x0801) at the beginning of each file.

Take a look:

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <limits.h>
#include <errno.h>
#include <ctype.h>

#define HEADER_SIZE 64
#define FILE_NAME_SIZE 16

typedef struct entry_s {
  uint8_t type;
  uint16_t start_address;
  uint16_t end_address;
  uint16_t unused1;
  uint16_t offset;
  uint32_t unused2;
  uint8_t petscii_file_name[FILE_NAME_SIZE];
} entry_t;

static void dump_program(FILE *t64, char *name, uint16_t offset,
  uint16_t start_address, size_t size)
{
  char path[PATH_MAX];
  FILE *prg;
  size_t i;
  int byte;

  snprintf(path, PATH_MAX, "%s.prg", name);
  prg = fopen(path, "wbx"); /* GNU extension. */
  if (prg == NULL) {
    fprintf(stderr, "Warning: fopen(%s) failed: %s\n", path, strerror(errno));
    return;
  }

  if (fseek(t64, offset, SEEK_SET) == -1) {
    fprintf(stderr, "Warning: fseek() failed: %s\n", strerror(errno));
    fclose(prg);
    return;
  }

  /* Inject loading/start address at the beginning of the file: */
  fputc(start_address & 0xFF, prg);
  fputc(start_address >> 8, prg);

  for (i = 0; i < size; i++) {
    byte = fgetc(t64);
    if (byte == EOF) {
      fprintf(stderr, "Warning: EOF reached.\n");
      break;
    }
    fputc(byte, prg);
  }

  printf("Extracted: \"%s\" (%d bytes)\n", name, size);
  fclose(prg);
}

int main(int argc, char *argv[])
{
  FILE *t64;
  uint8_t header[HEADER_SIZE];
  size_t size;
  uint16_t no_of_entries;
  int i;
  int n;
  uint8_t petscii;
  char ascii_name[FILE_NAME_SIZE + 1];
  entry_t entry;

  if (argc != 2) {
    fprintf(stderr, "Usage: %s <t64 image>\n", argv[0]);
    return 1;
  }

  t64 = fopen(argv[1], "rb");
  if (t64 == NULL) {
    fprintf(stderr, "Error: fopen(%s) failed: %s\n", argv[1], strerror(errno));
    return 1;
  }

  size = fread(&header[0], sizeof(uint8_t), HEADER_SIZE, t64);
  if (size != HEADER_SIZE) {
    fprintf(stderr, "Error: Unable to read complete header.\n");
    fclose(t64);
    return 1;
  }

  if (header[1] != '6' || header[2] != '4') {
    fprintf(stderr, "Error: Expected '64' in header.\n");
    fclose(t64);
    return 1;
  }

  no_of_entries  = header[0x24];
  no_of_entries += header[0x25] << 8;

  for (i = 0; i < no_of_entries; i++) {
    if (fseek(t64, HEADER_SIZE + (i * sizeof(entry_t)), SEEK_SET) == -1) {
      fprintf(stderr, "Error: fseek() failed: %s\n", strerror(errno));
      fclose(t64);
      return 1;
    }

    size = fread(&entry, sizeof(entry_t), 1, t64);
    if (size != 1) {
      fprintf(stderr, "Error: Unable to read entry.\n");
      fclose(t64);
      return 1;
    }

    /* Only extract normal files: */
    if (entry.type != 1) {
      continue;
    }

    /* Convert file name from PETSCII to ASCII: */
    memset(ascii_name, '\0', FILE_NAME_SIZE + 1);
    for (n = 0; n < FILE_NAME_SIZE; n++) {
      petscii = entry.petscii_file_name[n];
      if (isalnum(petscii)) {
        ascii_name[n] = tolower(petscii);
      } else if (petscii == ' ') {
        ascii_name[n] = '_';
      } else {
        ascii_name[n] = '.';
      }
    }
    
    /* Strip ending spaces: */
    for (n = FILE_NAME_SIZE - 1; n >= 0; n--) {
      if (ascii_name[n] == '_') {
        ascii_name[n] = '\0';
      } else {
        break;
      }
    }

    dump_program(t64, ascii_name, entry.offset, entry.start_address,
      entry.end_address - entry.start_address);
  }

  fclose(t64);
}
          


Topic: Scripts and Code, by Kjetil @ 02/08-2024, Article Link