--- /dev/null
+/*
+ * Manipulate BBC micro disc images include Watford DDFS and
+ * to an extent HDFS image types
+ */
+
+#include <stdio.h>
+#include <stdint.h>
+#include <string.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <sys/stat.h>
+
+/*
+ * Disk format (256 byte logical sectors)
+ *
+ * Sector 0 is a header then 31 directory entries
+ * Sector 1 is a header then 31 file info headers
+ * Sector 2+ are data
+ *
+ * Entry 0 is the header in each sector
+ * Entry 1 to 31 are the file entries
+ *
+ * Each entry in sector 0 is
+ * char name[7] space padded
+ * char prefix directory a-z etc
+ *
+ * The top bit of each byte is used for
+ * 0 HDFS:sector bit 10
+ * 1 HDFS length bit 18
+ * 2 -
+ * 3 HDFS file/dir
+ * 4 HDFS not readable
+ * 5 HDFS not writeable (WDFS length bit 18)
+ * 6 HDFS not executable (WDFS sector bit 10)
+ * 7 locked
+ *
+ *
+ * Sector 1 holds file info headers
+ *
+ * 0-1 load address bits 0-15
+ * 2-3 execution address bits 0-15
+ * 4-5 length bits 0-15
+ * 6
+ * bit 0-1: sector bit 8/9
+ * bit 2-3: load bit 16/17
+ * bit 4-5: length bit 16/17
+ * bit 6-7: execution bit 16/17
+ * 7 sector start b0-7
+ *
+ * Sector 0 starts with an 8 byte block holding the
+ * title (space padded). HDFS uses bit 7 of byte 0 for
+ * bit 10 of total sectors otherwise top bits are 0
+ *
+ * Sector 1 starts with an 8 byte block holding
+ * 100-103 Last four bytes of title (space padded)
+ * 104 disk cycle number (key number for HDFS)
+ * 105 number of catalogu entries * 8
+ * 106 bit 0/1 total sectors bit8-9
+ * bit 2 0 (WDFS total sectors bit 10) (HDFS sides - 1)
+ * bit 3 0 (HDFS 1 - HDFS detect)
+ * bit 4-5 !boot option
+ * bit 6-7 0
+ * 107 total logical sectors b0-b7
+ *
+ * Watford DFS may have a second catalogue in sector 2-3 if
+ * sector2 starts AA * 8
+ *
+ * If so then 8-FF are 31 more directory entries
+ * 108-1FF are 31 more file info entries
+ *
+ * 100-103 are 0
+ * 104 is copy of disk cycle number
+ * 105 number of catalogue entries *8
+ * 106-107 copy of info in sector 1
+ *
+ */
+
+struct bbc_file {
+ char name[8]; /* 7 bytes */
+ char prefix;
+ uint32_t load;
+ uint32_t exec;
+ uint32_t length;
+ uint32_t sector;
+ uint32_t flags;
+#define HDFS_DIR 1
+#define NO_READ 2
+#define NO_WRITE 4
+#define NO_EXECUTE 8
+#define LOCKED 16
+#define DIRTY 128 /* Entry needs writing back */
+};
+
+struct freelist {
+ uint32_t start;
+ uint32_t end;
+};
+
+struct bbc_disk {
+ uint8_t type;
+#define TYPE_INVALID 0
+#define TYPE_DFS 1
+#define TYPE_WDFS 2
+#define TYPE_WDFS_2 3
+#define TYPE_HDFS 4
+ uint8_t num_dirents;
+ uint8_t sides;
+ uint8_t cycle;
+ uint8_t plingboot;
+ uint8_t flags;
+#define DISK_CORRUPT_FREE 1
+#define DATA_DIRTY 64 /* Data area needs writing back */
+#define DISK_DIRTY 128 /* Hader needs writing back */
+ uint32_t sectors;
+ char label[13]; /* 12 byte label */
+ struct bbc_file files[62]; /* Max allowed */
+ struct freelist free[63]; /* one hole after each entry and one at the
+ front */
+ int nextfree;
+};
+
+static const char *typestr[5] = {
+ "Invalid",
+ "DFS",
+ "WDFS",
+ "WDFS (2 directories)",
+ "HDFS"
+};
+
+struct bbc_disk disk;
+uint8_t *diskdata;
+long disksize;
+char *diskname;
+
+int within(struct freelist *f, struct bbc_file *fn)
+{
+ uint32_t size = (fn->length + 255) / 256;
+ if (f->start <= fn->sector && f->end >= fn->sector + size - 1)
+ return 1;
+ return 0;
+}
+
+void split(struct freelist *f, struct bbc_file *fn)
+{
+ uint32_t size = (fn->length + 255) / 256;
+ struct freelist *n;
+
+ if (f->start == fn->sector) {
+ /* shrink current - may shrink to 0 */
+ f->start += size;
+ return;
+ }
+ if (fn->sector + size - 1 == f->end) {
+ /* shrink current tail - may shrink to 0 */
+ f->end = fn->sector - 1;
+ return;
+ }
+
+ /* Grab a new entry for the tail space */
+ n = disk.free + disk.nextfree++;
+ n->start = fn->sector + size - 1;
+ n->end = f->end;
+ /* Space at front */
+ f->end = fn->sector - 1;
+}
+
+void insert_used(struct bbc_file *fn)
+{
+ /* This must be within a current free entry or the disk is corrupt */
+ struct freelist *f = disk.free;
+ int n = 0;
+ while (n < disk.nextfree) {
+ if (within(f, fn)) {
+ split(f, fn);
+ return;
+ }
+ f++;
+ n++;
+ }
+ /* Out of range */
+ fprintf(stderr, "Corrupt sector range map when analyzing %c.%s\n",
+ fn->prefix, fn->name);
+ disk.flags |= DISK_CORRUPT_FREE;
+}
+
+/* First fit for now - best fit might be better FIXME ? */
+uint32_t find_space(struct bbc_file * fn)
+{
+ uint32_t size = (fn->length + 255) / 256;
+ struct freelist *f = disk.free;
+ int n = 0;
+
+ while (n < disk.nextfree) {
+ if (f->end >= f->start && f->end - f->start >= size - 1) {
+ return f->start;
+ }
+ f++;
+ n++;
+ }
+ return 0;
+}
+
+void init_free_list(void)
+{
+ disk.nextfree = 1;
+ if (disk.type == TYPE_WDFS_2)
+ disk.free[0].start = 4;
+ else
+ disk.free[0].start = 2;
+ disk.free[0].end = disk.sectors - 1;
+}
+
+void rebuild_free_list(void)
+{
+ struct bbc_file *f = disk.files;
+ int i;
+ init_free_list();
+ for (i = 0; i < disk.num_dirents; i++) {
+ if (*f->name && f->sector && f->length)
+ insert_used(f);
+ f++;
+ }
+}
+
+void strim(char *c)
+{
+ while (*c)
+ *c++ &= 0x7F;
+}
+
+void set_disk_type(void)
+{
+ disk.sides = 1;
+ disk.num_dirents = 31;
+ switch ((diskdata[0x106] >> 2) & 3) {
+ case 0:
+ /* Could also be WDFS but small so same as DFS */
+ disk.type = TYPE_DFS;
+ break;
+ case 1:
+ disk.type = TYPE_WDFS;
+ break;
+ case 3:
+ disk.sides = 2;
+ /* Fall through */
+ case 2:
+ disk.type = TYPE_HDFS;
+ }
+ if (memcmp(diskdata + 0x200, "\xAA\xAA\xAA\xAA\xAA\xAA\xAA\xAA", 8)
+ == 0 && memcmp(diskdata + 0x300, "\0\0\0\0", 4) == 0) {
+ disk.type = TYPE_WDFS_2;
+ disk.num_dirents = 62;
+ }
+}
+
+void load_headers(void)
+{
+ uint8_t *p = diskdata;
+ memcpy(disk.label, p, 8);
+ memcpy(disk.label + 8, p + 0x100, 4);
+ disk.label[12] = 0;
+ strim(disk.label);
+ disk.cycle = p[0x104];
+ /* FIXME: 0x105 catalog entries * 8 ??? */
+ disk.sectors = p[0x107];
+ disk.sectors |= (p[0x106] & 3) << 8;
+ disk.plingboot = (p[0x106] & 0x30) >> 4;
+ if ((p[0] & 0x80) && disk.type == TYPE_HDFS)
+ disk.sectors += 0x400;
+ init_free_list();
+}
+
+void recode_headers(void)
+{
+ uint8_t *p = diskdata;
+ memcpy(p, disk.label, 8);
+ memcpy(p + 0x100, disk.label + 8, 4);
+ p[0x0104] = disk.cycle;
+ if (disk.type == TYPE_WDFS_2)
+ p[0x204] = disk.cycle;
+ p[0x106] &= ~0x30;
+ p[0x106] |= (disk.plingboot) << 4;
+ if (disk.type == TYPE_HDFS && disk.sectors >= 0x400)
+ p[0x0] |= 0x80;
+}
+
+void load_directory(void)
+{
+ int i;
+ for (i = 0; i < disk.num_dirents; i++) {
+ uint8_t *p = diskdata + 0x08 + 0x08 * i;
+ struct bbc_file *f = disk.files + i;
+ uint8_t b;
+
+ /* Skip second headers */
+ if (i > 31)
+ p = diskdata += 0x08;
+
+ f->load = p[0x100] + (p[0x101] << 8);
+ f->exec = p[0x102] + (p[0x103] << 8);
+ f->length = p[0x104] + (p[0x105] << 8);
+ f->sector = p[0x107];
+ b = p[0x106];
+ f->sector |= (b & 3) << 8;
+ f->load |= (b & 0xC) << 14;
+ f->length |= (b & 0x30) << 12;
+ f->exec |= (b & 0xC0) << 10;
+
+ if (f->load & 0x20000)
+ f->load |= 0xFFFC0000;
+ if (f->exec & 0x20000)
+ f->exec |= 0xFFFC0000;
+
+ memcpy(f->name, p, 7);
+ f->name[8] = 0;
+ f->prefix = p[7];
+ if (p[7] & 0x80)
+ f->flags |= LOCKED;
+ if (p[6] & 0x80) {
+ if (disk.type == TYPE_WDFS
+ || disk.type == TYPE_WDFS_2)
+ f->sector += 0x400;
+ else if (disk.type == TYPE_HDFS)
+ f->flags |= NO_EXECUTE;
+ }
+ if (p[5] & 0x80) {
+ if (disk.type == TYPE_WDFS
+ || disk.type == TYPE_WDFS_2)
+ f->length += 0x40000;
+ else if (disk.type == TYPE_HDFS)
+ f->flags |= NO_WRITE;
+ }
+ if (p[4] & 0x80) {
+ if (disk.type == TYPE_HDFS)
+ f->flags |= NO_READ;
+ }
+ if (p[3] & 0x80) {
+ if (disk.type == TYPE_HDFS)
+ f->flags |= HDFS_DIR;
+ }
+ if (p[1] & 0x80) {
+ if (disk.type == TYPE_HDFS)
+ f->length += 0x40000;
+ }
+ if (p[0] & 0x80) {
+ if (disk.type == TYPE_HDFS)
+ f->sector += 0x400;
+ }
+ strim(f->name);
+ }
+ rebuild_free_list();
+}
+
+void recode_dirent(int i, struct bbc_file *f)
+{
+ uint8_t *p = diskdata + 0x08 + 0x08 * i;
+ if (i > 31)
+ p += 0x08; /* Skip second header */
+
+ memcpy(p, f->name, 7);
+ p[7] = f->prefix;
+
+ if (f->flags & LOCKED)
+ p[7] |= 0x80;
+
+ if (f->flags & NO_EXECUTE)
+ p[6] |= 0x80;
+ else if (disk.type != TYPE_HDFS && f->sector >= 0x0400)
+ p[6] |= 0x80;
+
+ if (f->flags & NO_WRITE)
+ p[5] |= 0x80;
+ else if (disk.type != TYPE_HDFS && f->length >= 0x40000)
+ p[5] |= 0x80;
+
+ if (f->flags & NO_READ)
+ p[4] |= 0x80;
+
+ if (f->flags & HDFS_DIR)
+ p[3] |= 0x80;
+
+ if (disk.type == TYPE_HDFS && f->sector >= 0x400)
+ p[0] |= 0x80;
+
+ p[0x100] = f->load & 0xFF;
+ p[0x101] = (f->load >> 8) & 0xFF;
+ p[0x102] = f->exec & 0xFF;
+ p[0x103] = (f->exec >> 8) & 0xFF;
+ p[0x104] = f->length & 0xFF;
+ p[0x105] = (f->length >> 8) & 0xFF;
+ p[0x106] = (f->sector >> 8) & 0x03;
+ p[0x107] = f->sector & 0xFF;
+ p[0x106] |= (f->load >> 14) & 0x0C;
+ p[0x106] |= (f->length >> 12) & 0x30;
+ p[0x106] |= (f->exec >> 10) & 0xC0;
+}
+
+void load_disk(void)
+{
+ set_disk_type();
+ load_headers();
+ load_directory();
+}
+
+struct bbc_file *lookup(const char *p)
+{
+ char buf[7];
+ char prefix = 0;
+ int i;
+
+ if (!*p)
+ return NULL;
+ memset(buf, ' ', 7);
+ if (p[1] == '.') {
+ prefix = *p;
+ p += 2;
+ }
+ if (!*p)
+ return NULL;
+ if (strlen(p) > 7)
+ return NULL;
+ memcpy(buf, p, strlen(p));
+
+ for (i = 0; i < disk.num_dirents; i++) {
+ struct bbc_file *f = disk.files + i;
+ if (memcmp(buf, f->name, 7) == 0 &&
+ (prefix == 0 || prefix == f->prefix))
+ return f;
+ }
+ return NULL;
+}
+
+struct bbc_file *lookup_free(const char *p)
+{
+ char buf[7];
+ char prefix = '$';
+ int i;
+
+ if (!*p)
+ return NULL;
+ memset(buf, ' ', 7);
+ if (p[1] == '.') {
+ prefix = *p;
+ p += 2;
+ }
+ if (!*p)
+ return NULL;
+ if (strlen(p) > 7)
+ return NULL;
+ memcpy(buf, p, strlen(p));
+ strim(buf); /* No top bits set */
+
+ for (i = 0; i < disk.num_dirents; i++) {
+ struct bbc_file *f = disk.files + i;
+ if (f->sector == 0 || *f->name == 0) {
+ memcpy(f->name, buf, 7);
+ f->prefix = prefix;
+ f->flags |= DIRTY;
+ return f;
+ }
+ }
+ return NULL;
+}
+
+void directory(int argc, char *argv[])
+{
+ int i;
+ printf("Disk: %s (%d sectors, %d sides)\n",
+ disk.label, disk.sectors, disk.sides);
+ printf("Type %s (%d directories, !boot %d, cycles %d)\n",
+ typestr[disk.type], disk.num_dirents, disk.plingboot,
+ disk.cycle);
+ for (i = 0; i < disk.num_dirents; i++) {
+ struct bbc_file *f = disk.files + i;
+ if (f->sector && memcmp(f->name, " ", 7)) {
+ printf
+ ("%c.%s %c%c%c%c%c %-6d@%-6d L%08X E%08X\n",
+ f->prefix, f->name,
+ f->flags & HDFS_DIR ? 'D' : ' ',
+ f->flags & NO_READ ? ' ' : 'r',
+ f->flags & NO_WRITE ? ' ' : 'w',
+ f->flags & NO_EXECUTE ? ' ' : 'x',
+ f->flags & LOCKED ? 'L' : ' ', f->length,
+ f->sector, f->load, f->exec);
+ }
+ }
+}
+
+void export(int argc, char *argv[])
+{
+ FILE *fp;
+ uint8_t *p;
+ struct bbc_file *f;
+
+ if (argc != 5) {
+ fprintf(stderr, "%s: export diskfile bbcname unixname.\n",
+ argv[0]);
+ exit(1);
+ }
+ f = lookup(argv[3]);
+ if (f == NULL) {
+ fprintf(stderr, "%s: '%s' not found.\n", argv[0], argv[3]);
+ exit(1);
+ }
+ p = diskdata + 256 * f->sector;
+ printf("Offset %p v %p\n", p, diskdata);
+ fp = fopen(argv[4], "w");
+ if (fp == NULL) {
+ perror(argv[4]);
+ exit(1);
+ }
+ if (fwrite(p, f->length, 1, fp) != 1) {
+ perror("write");
+ exit(1);
+ }
+ fclose(fp);
+}
+
+/* FIXME: we need to open this in the same directory as the
+ original image so that rename is reliable */
+FILE *fopen_tmp(void)
+{
+ FILE *fp = fopen(".tmpdisk", "w");
+ if (fp == NULL) {
+ perror(".tmpdisk");
+ exit(1);
+ }
+ return fp;
+}
+
+void free_tmp(void)
+{
+ if (unlink(".tmpdisk"))
+ perror("unlink");
+}
+
+void rename_tmp(void)
+{
+ if (rename(".tmpdisk", diskname)) {
+ perror("rename");
+ exit(1);
+ }
+}
+
+void disk_writeback(void)
+{
+ FILE *fp;
+ struct bbc_file *f;
+ int hsize;
+ int i;
+
+ if (disk.flags & DISK_CORRUPT_FREE) {
+ fprintf(stderr, "Corrupt disk: not rewriting.\n");
+ exit(1);
+ }
+
+ fp = fopen_tmp();
+
+ if (disk.type == TYPE_WDFS_2)
+ hsize = 4 * 256;
+ else
+ hsize = 2 * 256;
+
+ /* Always do this as we need the data in the new media */
+ if (1 /*disk.flags & DATA_DIRTY */ ) {
+ if (fseek(fp, hsize, 0)) {
+ perror("fseek");
+ free_tmp();
+ exit(1);
+ }
+ if (fwrite
+ (diskdata + hsize, disk.sectors * 256 - hsize, 1,
+ fp) != 1) {
+ perror("fwrite");
+ free_tmp();
+ exit(1);
+ }
+ disk.flags &= ~DATA_DIRTY;
+ disk.flags |= DISK_DIRTY;
+ }
+ f = disk.files;
+ for (i = 0; i < disk.num_dirents; i++) {
+ if (f->flags & DIRTY) {
+ disk.flags |= DISK_DIRTY;
+ recode_dirent(i, f);
+ }
+ f++;
+ }
+ if (disk.flags & DISK_DIRTY) {
+ disk.cycle++;
+ recode_headers();
+ }
+ if (fseek(fp, 0L, 0)) {
+ perror("fseek");
+ free_tmp();
+ exit(1);
+ }
+ if (fwrite(diskdata, hsize, 1, fp) != 1 || fclose(fp) == -1) {
+ perror("fwrite");
+ free_tmp();
+ exit(1);
+ }
+ rename_tmp();
+ disk.flags &= ~DISK_DIRTY;
+}
+
+void compact(int argc, char *argv[])
+{
+ if (argc != 3) {
+ fprintf(stderr, "%s compact diskfile.\n", argv[0]);
+ exit(1);
+ }
+ fprintf(stderr, "%s: compact is not yet supported.\n", argv[0]);
+ exit(1);
+}
+
+void setproperties(int argc, char *argv[])
+{
+ struct bbc_file *f;
+
+ if (argc != 6) {
+ fprintf(stderr,
+ "%s: setprop diskfile bbcname load exec.\n",
+ argv[0]);
+ exit(1);
+ }
+ f = lookup(argv[3]);
+ if (f == NULL) {
+ fprintf(stderr, "%s: '%s' not found.\n", argv[0], argv[3]);
+ exit(1);
+ }
+ f->load = atol(argv[4]);
+ f->exec = atol(argv[5]);
+ f->flags |= DIRTY;
+ disk_writeback();
+}
+
+void deletefile(int argc, char *argv[])
+{
+ struct bbc_file *f;
+
+ if (argc != 4) {
+ fprintf(stderr, "%s: rm diskfile bbcname.\n", argv[0]);
+ exit(1);
+ }
+ f = lookup(argv[3]);
+ if (f == NULL) {
+ fprintf(stderr, "%s: '%s' not found.\n", argv[0], argv[3]);
+ exit(1);
+ }
+ memset(f->name, 0, 7);
+ f->prefix = 0;
+ f->sector = 0;
+ f->flags |= DIRTY;
+ disk_writeback();
+ rebuild_free_list();
+}
+
+void import(int argc, char *argv[])
+{
+ struct bbc_file *f;
+ FILE *fp;
+ struct stat st;
+ uint32_t len;
+
+ if (argc != 5) {
+ fprintf(stderr, "%s: import diskfile unixname bbcname.\n",
+ argv[0]);
+ exit(1);
+ }
+ f = lookup(argv[4]);
+ if (f) {
+ fprintf(stderr, "%s: '%s' already exists.\n", argv[0],
+ argv[4]);
+ exit(1);
+ }
+ f = lookup_free(argv[4]);
+ if (f == NULL) {
+ fprintf(stderr, "%s: unable to create file '%s'.\n",
+ argv[0], argv[4]);
+ exit(1);
+ }
+
+ f->load = 0;
+ f->exec = 0;
+ f->flags |= DIRTY;
+
+ fp = fopen(argv[3], "r");
+ if (fp == NULL) {
+ perror(argv[3]);
+ exit(1);
+ }
+
+ if (fstat(fileno(fp), &st) == -1) {
+ perror("fstat");
+ exit(1);
+ }
+ len = st.st_size;
+ f->length = len;
+
+ f->sector = find_space(f);
+ if (f->sector == 0) {
+ fprintf(stderr, "Disc full.\n");
+ exit(1);
+ }
+
+ if (fread(diskdata + 256 * f->sector, len, 1, fp) != 1) {
+ perror("read");
+ exit(1);
+ }
+ fclose(fp);
+
+ insert_used(f);
+ disk.flags |= DATA_DIRTY;
+ disk_writeback();
+}
+
+void setplingboot(int argc, char *argv[])
+{
+ if (argc != 4) {
+ fprintf(stderr, "%s: boot diskfile mode .\n", argv[0]);
+ exit(1);
+ }
+ disk.flags |= DISK_DIRTY;
+ disk.plingboot = atoi(argv[3]) & 3;
+ disk_writeback();
+}
+
+int main(int argc, char *argv[])
+{
+ FILE *f;
+ struct stat st;
+ if (argc < 3) {
+ fprintf(stderr, "%s: op diskfile ...\n", argv[0]);
+ exit(1);
+ }
+ f = fopen(argv[2], "r");
+ if (f == NULL) {
+ perror(argv[2]);
+ exit(1);
+ }
+ if (fstat(fileno(f), &st) == -1) {
+ perror("fstat");
+ exit(1);
+ }
+ /* FIXME: non regular files */
+ diskdata = malloc(st.st_size);
+ if (diskdata == NULL) {
+ fprintf(stderr, "%s: out of memory.\n", argv[0]);
+ exit(1);
+ }
+ disksize = st.st_size;
+ diskname = argv[2];
+ if (fread(diskdata, 1, disksize, f) != disksize) {
+ perror("read");
+ exit(1);
+ }
+ fclose(f);
+ load_disk();
+
+ /* Some people pack incomplete SSD files and assume the rest is zero */
+ if (disksize < disk.sectors * 256) {
+ diskdata = realloc(diskdata, disk.sectors * 256);
+ memset(diskdata + disksize, 0,
+ disk.sectors * 256 - disksize);
+ disksize = disk.sectors * 256;
+ printf("Expanded truncated SSD to %ld bytes\n", disksize);
+ }
+ if (strcmp(argv[1], "ls") == 0)
+ directory(argc, argv);
+ else if (strcmp(argv[1], "export") == 0)
+ export(argc, argv);
+ else if (strcmp(argv[1], "set") == 0)
+ setproperties(argc, argv);
+ else if (strcmp(argv[1], "rm") == 0)
+ deletefile(argc, argv);
+ else if (strcmp(argv[1], "import") == 0)
+ import(argc, argv);
+ else if (strcmp(argv[1], "boot") == 0)
+ setplingboot(argc, argv);
+ else
+ fprintf(stderr, "%s: unknown operation '%s'.\n", argv[0],
+ argv[1]);
+ return 0;
+}