From dc13b6a59a5adcadfa00180d18b046bb731eafaa Mon Sep 17 00:00:00 2001 From: Ian Flanigan Date: Mon, 4 Jan 2021 00:01:30 +0100 Subject: [PATCH] DOS 13-sector tests and fixes (#53) Like the DOS 3.3 and ProDOS sector order issues, this change fixes the physical order of the sectors on 13-sector disks when nibblized. This change also adds tests for the 13-sector format to verify the sector order. One of the crazy things is that _Beneath Apple DOS_ failed me in this instance because it doesn't discuss what happens to the last byte in "5 and 3" encoding anywhere (AFAICT). I went back to the DOS 3.1 source released by the Computer History Museum here: https://computerhistory.org/blog/apple-ii-dos-source-code/ The code is in `appdos31.lst` in the `POSTNIB` routine on line 4777. --- js/formats/d13.js | 28 +- js/formats/format_utils.ts | 11 +- test/js/formats/d13.spec.ts | 350 ++++++++++++++++++++++ test/js/formats/testdata/13sector.spec.ts | 88 ++++++ test/js/formats/testdata/13sector.ts | 55 ++++ 5 files changed, 523 insertions(+), 9 deletions(-) create mode 100644 test/js/formats/d13.spec.ts create mode 100644 test/js/formats/testdata/13sector.spec.ts create mode 100644 test/js/formats/testdata/13sector.ts diff --git a/js/formats/d13.js b/js/formats/d13.js index 7413e80..2259864 100644 --- a/js/formats/d13.js +++ b/js/formats/d13.js @@ -9,9 +9,14 @@ * implied warranty. */ -import { explodeSector13, _D13O } from './format_utils'; +import { explodeSector13, D13O } from './format_utils'; -export default function Nibble(options) { +/** + * Returns a `Disk` object from DOS 3.2-ordered image data. + * @param {*} options the disk image and options + * @returns {import('./format_utils').Disk} + */ +export default function DOS13(options) { var { data, name, rawData, volume, readOnly } = options; var disk = { format: 'd13', @@ -23,18 +28,29 @@ export default function Nibble(options) { rawTracks: null }; + /* + * DOS 13-sector disks have the physical sectors skewed on the track. The skew + * between physical sectors is 10 (A), resulting in the following physical order: + * + * 0 A 7 4 1 B 8 5 2 C 9 6 3 + * + * Note that because physical sector == logical sector, this works slightly + * differently from the DOS and ProDOS nibblizers. + */ + for (var t = 0; t < 35; t++) { var track = []; - for (var s = 0; s < 13; s++) { + for (var disk_sector = 0; disk_sector < 13; disk_sector++) { + var physical_sector = D13O[disk_sector]; var sector; if (rawData) { - var off = (13 * t + _D13O[s]) * 256; + var off = (13 * t + physical_sector) * 256; sector = new Uint8Array(rawData.slice(off, off + 256)); } else { - sector = data[t][_D13O[s]]; + sector = data[t][physical_sector]; } track = track.concat( - explodeSector13(volume, t, _D13O[s], sector) + explodeSector13(volume, t, physical_sector, sector) ); } disk.tracks.push(track); diff --git a/js/formats/format_utils.ts b/js/formats/format_utils.ts index 0952c49..e43c4c4 100644 --- a/js/formats/format_utils.ts +++ b/js/formats/format_utils.ts @@ -61,9 +61,14 @@ export const _PO = [ 0x1, 0x3, 0x5, 0x7, 0x9, 0xb, 0xd, 0xf ]; -// var D13O = [ -// 0x0, 0xa, 0x7, 0x4, 0x1, 0xb, 0x8, 0x5, 0x2, 0xc, 0x9, 0x6, 0x3 -// ]; +/** + * DOS 13-sector disk physical sector order (index is disk sector, value is + * physical sector). + */ +export const D13O = [ + 0x0, 0xa, 0x7, 0x4, 0x1, 0xb, 0x8, 0x5, 0x2, 0xc, 0x9, 0x6, 0x3 +]; + export const _D13O = [ 0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xa, 0xb, 0xc ]; diff --git a/test/js/formats/d13.spec.ts b/test/js/formats/d13.spec.ts new file mode 100644 index 0000000..81959c6 --- /dev/null +++ b/test/js/formats/d13.spec.ts @@ -0,0 +1,350 @@ +import DOS13 from '../../../js/formats/d13'; +import { D13O } from '../../../js/formats/format_utils'; +import { memory } from '../../../js/types'; +import { BYTES_BY_SECTOR, BYTES_BY_TRACK } from './testdata/13sector'; +import { expectSequence, findBytes, skipGap } from './util'; + +describe('DOS-13 format', () => { + it('is callable', () => { + const disk = DOS13({ + name: 'test disk', + data: BYTES_BY_TRACK, + volume: 10, + readOnly: true, + }); + expect(disk).not.toBeNull(); + }); + + it('has correct number of tracks', () => { + const disk = DOS13({ + name: 'test disk', + data: BYTES_BY_TRACK, + volume: 10, + readOnly: true, + }); + expect(disk.tracks.length).toEqual(35); + }); + + it('has correct number of bytes in track 0', () => { + const disk = DOS13({ + name: 'test disk', + data: BYTES_BY_TRACK, + volume: 10, + readOnly: true, + }); + expect(disk.tracks[0].length).toEqual(6289); + }); + + it('has correct number of bytes in all tracks', () => { + const disk = DOS13({ + name: 'test disk', + data: BYTES_BY_TRACK, + volume: 10, + readOnly: true, + }); + // Track 0 is slightly longer for some reason. + expect(disk.tracks[0].length).toEqual(6289); + for (let i = 1; i < disk.tracks.length; i++) { + expect(disk.tracks[i].length).toEqual(6265); + } + }); + + it('has correct GAP 1', () => { + const disk = DOS13({ + name: 'test disk', + data: BYTES_BY_TRACK, + volume: 10, + readOnly: true, + }); + // From Beneith Apple DOS, GAP 1 should have 12-85 0xFF bytes + const track = disk.tracks[0]; + let numFF = 0; + while (track[numFF] == 0xFF && numFF < 0x100) { + numFF++; + } + expect(numFF).toBeGreaterThanOrEqual(40); + expect(numFF).toBeLessThanOrEqual(128); + }); + + it('has correct Address Field for track 0, sector 0', () => { + // _Beneath Apple DOS_, TRACK FORMATTING, p. 3-12 + const disk = DOS13({ + name: 'test disk', + data: BYTES_BY_TRACK, + volume: 10, + readOnly: true, + }); + const track = disk.tracks[0]; + let i = skipGap(track); + // prologue + i = expectSequence(track, i, [0xD5, 0xAA, 0xB5]); + // volume 10 = 0b00001010 + expect(track[i++]).toBe(0b10101111); + expect(track[i++]).toBe(0b10101010); + // track 0 = 0b00000000 + expect(track[i++]).toBe(0b10101010); + expect(track[i++]).toBe(0b10101010); + // sector 0 = 0b00000000 + expect(track[i++]).toBe(0b10101010); + expect(track[i++]).toBe(0b10101010); + // checksum = 0b00000101 + expect(track[i++]).toBe(0b10101111); + expect(track[i++]).toBe(0b10101010); + // epilogue + i = expectSequence(track, i, [0xDE, 0xAA, 0xEB]); + }); + + it('has correct Data Field for track 0, sector 0 (BYTES_BY_TRACK)', () => { + // _Beneath Apple DOS_, DATA FIELD ENCODING, pp. 3-13 to 3-21 + const disk = DOS13({ + name: 'test disk', + data: BYTES_BY_TRACK, + volume: 10, + readOnly: true, + }); + const track: memory = disk.tracks[0]; + // skip to the first address epilogue + let i = findBytes(track, [0xDE, 0xAA, 0xEB]); + expect(i).toBeGreaterThan(50); + i = skipGap(track, i); + // prologue + i = expectSequence(track, i, [0xD5, 0xAA, 0xAD]); + // data (all zeros, which is 0xAB with 5 and 3 encoding) + for (let j = 0; j < 410; j++) { + expect(track[i++]).toBe(0xAB); + } + // checksum (also zero) + expect(track[i++]).toBe(0xAB); + // epilogue + i = expectSequence(track, i, [0xDE, 0xAA, 0xEB]); + }); + + it('has correct Address Field for track 0, sector 1', () => { + // _Beneath Apple DOS_, TRACK FORMATTING, p. 3-12 + const disk = DOS13({ + name: 'test disk', + data: BYTES_BY_TRACK, + volume: 10, + readOnly: true, + }); + const track = disk.tracks[0]; + // first sector prologue + let i = findBytes(track, [0xD5, 0xAA, 0xB5]); + + // second sector prologue + i = findBytes(track, [0xD5, 0xAA, 0xB5], i); + // volume 10 = 0b00001010 + expect(track[i++]).toBe(0b10101111); + expect(track[i++]).toBe(0b10101010); + // track 0 = 0b00000000 + expect(track[i++]).toBe(0b10101010); + expect(track[i++]).toBe(0b10101010); + // sector A = 0b00001010 + expect(track[i++]).toBe(0b10101111); + expect(track[i++]).toBe(0b10101010); + // checksum = 0b00000101 + expect(track[i++]).toBe(0b10101010); + expect(track[i++]).toBe(0b10101010); + // epilogue + i = expectSequence(track, i, [0xDE, 0xAA, 0xEB]); + }); + + it('has correct Data Field for track 0, disk sector 1 (BYTES_BY_SECTOR)', () => { + // _Beneath Apple DOS_, DATA FIELD ENCODING, pp. 3-13 to 3-21 + const disk = DOS13({ + name: 'test disk', + data: BYTES_BY_SECTOR, + volume: 10, + readOnly: true, + }); + const track: memory = disk.tracks[0]; + // First data field prologue + let i = findBytes(track, [0xD5, 0xAA, 0xAD]); + // Second data field prologue + i = findBytes(track, [0xD5, 0xAA, 0xAD], i); + + // Sector 1 is physical/DOS sector A. + // In 5 x 3 encoding, the lowest 3 bits of all the bytes come first, + // all mixed up in a crazy order. 0x0A is 0b00001010, so the lowest + // 3 bits are 0b010. With mixing (see Figure 3.18), this becomes: + // 0b01000, 0b01011, 0b01000 + // repeated. These chunks come in repeated blocks of 0x33 (51) bytes. + // + // Because 51 * 5 is 255, there is one odd byte that is treated + // specially at the beginning. + // + // Lower 3 bits of last byte: + // 0b00010 = 0b00010 (02 -> AE) + expect(track[i++]).toBe(0xAE); + // + // Bottom 3 bits in block 1 (08 block): + // 0b01000 XOR 0b00010 = 0b01010 (0A -> BE) + // 0b01000 XOR 0b01000 = 0b00000 (00 -> AB) x 50 + expect(track[i++]).toBe(0xBE); + for (let j = 0; j < 50; j++) { + expect(track[i++]).toBe(0xAB); + } + // + // Bottom 3 bits in block 2 (0B block): + // 0b01011 XOR 0b01000 = 0b00011 (03 -> AF) + // 0b01011 XOR 0b01011 = 0b00000 (00 -> AB) x 50 + expect(track[i++]).toBe(0xAF); + for (let j = 0; j < 50; j++) { + expect(track[i++]).toBe(0xAB); + } + // + // Bottom 3 bits in block 1 (08 block): + // 0b01000 XOR 0b01011 = 0b00011 (03 -> AF) + // 0b01000 XOR 0b01000 = 0b00000 (00 -> AB) x 50 + expect(track[i++]).toBe(0xAF); + for (let j = 0; j < 50; j++) { + expect(track[i++]).toBe(0xAB); + } + // Upper 5 bits of 0x0A are 0x00001: + // 0b00001 XOR 0b01000 = 0b01001 (09 -> BD) + // 0b00001 XOR 0b00001 = 0b00000 (00 -> AB) x 255 + expect(track[i++]).toBe(0xBD); + for (let j = 0; j < 255; j++) { + expect(track[i++]).toBe(0xAB); + } + + // checksum 0b00001 (01 -> AD) + expect(track[i++]).toBe(0xAD); + // epilogue + i = expectSequence(track, i, [0xDE, 0xAA, 0xEB]); + }); + + it('has correct Address Field for track 1, sector 0', () => { + // _Beneath Apple DOS_, TRACK FORMATTING, p. 3-12 + const disk = DOS13({ + name: 'test disk', + data: BYTES_BY_TRACK, + volume: 10, + readOnly: true, + }); + const track = disk.tracks[1]; + let i = skipGap(track); + // prologue + i = expectSequence(track, i, [0xD5, 0xAA, 0xB5]); + // volume 10 = 0b00001010 + expect(track[i++]).toBe(0b10101111); + expect(track[i++]).toBe(0b10101010); + // track 1 = 0b00000001 + expect(track[i++]).toBe(0b10101010); + expect(track[i++]).toBe(0b10101011); + // sector 0 = 0b00000000 + expect(track[i++]).toBe(0b10101010); + expect(track[i++]).toBe(0b10101010); + // checksum = 0b00000100 + expect(track[i++]).toBe(0b10101111); + expect(track[i++]).toBe(0b10101011); + // epilogue + i = expectSequence(track, i, [0xDE, 0xAA, 0xEB]); + }); + + it('has correct Data Field for track 1, sector 0 (BYTES_BY_TRACK)', () => { + // _Beneath Apple DOS_, DATA FIELD ENCODING, pp. 3-13 to 3-21 + const disk = DOS13({ + name: 'test disk', + data: BYTES_BY_TRACK, + volume: 10, + readOnly: true, + }); + const track: memory = disk.tracks[1]; + let i = findBytes(track, [0xDE, 0xAA, 0xEB]); + expect(i).toBeGreaterThan(50); + i = skipGap(track, i); + // prologue + i = expectSequence(track, i, [0xD5, 0xAA, 0xAD]); + + // Expect data to be all 1s (track number). + + // In 5 x 3 encoding, the lowest 3 bits of all the bytes come first, + // all mixed up in a crazy order. 0x01 is 0b00000001, so the lowest + // 3 bits are 0b001. With mixing (see Figure 3.18), this becomes: + // 0b00111, 0b00100, 0b00100 + // repeated. These chunks come in repeated blocks of 0x33 (51) bytes. + // + // Because 51 * 5 is 255, there is one odd byte that is treated + // specially at the beginning. + // + // Lower 3 bits of last byte: + // 0b00001 = 0b00001 (01 -> AD) + expect(track[i++]).toBe(0xAD); + // + // Bottom 3 bits in block 1 (07 block): + // 0b00111 XOR 0b00001 = 0b00110 (06 -> B7) + // 0b00111 XOR 0b00111 = 0b00000 (00 -> AB) x 50 + expect(track[i++]).toBe(0xB7); + for (let j = 0; j < 50; j++) { + expect(track[i++]).toBe(0xAB); + } + // + // Bottom 3 bits in block 2 (04 block): + // 0b00111 XOR 0b00100 = 0b00011 (03 -> AF) + // 0b00100 XOR 0b00100 = 0b00000 (00 -> AB) x 50 + expect(track[i++]).toBe(0xAF); + for (let j = 0; j < 50; j++) { + expect(track[i++]).toBe(0xAB); + } + // + // Bottom 3 bits in block 1 (04 block): + // 0b00100 XOR 0b00100 = 0b00011 (00 -> AB) + // 0b00100 XOR 0b00100 = 0b00000 (00 -> AB) x 50 + expect(track[i++]).toBe(0xAB); + for (let j = 0; j < 50; j++) { + expect(track[i++]).toBe(0xAB); + } + // Upper 5 bits of 0x01 are 0x00000: + // 0b00000 XOR 0b00100 = 0b00100 (04 -> B5) + // 0b00000 XOR 0b00000 = 0b00000 (00 -> AB) x 255 + expect(track[i++]).toBe(0xB5); + for (let j = 0; j < 255; j++) { + expect(track[i++]).toBe(0xAB); + } + + // checksum 0b00000 (00 -> AB) + expect(track[i++]).toBe(0xAB); + // epilogue + i = expectSequence(track, i, [0xDE, 0xAA, 0xEB]); + }); + + it('has correct Address Fields for all tracks', () => { + // _Beneath Apple DOS_, TRACK FORMATTING, p. 3-12 + const disk = DOS13({ + name: 'test disk', + data: BYTES_BY_TRACK, + volume: 10, + readOnly: true, + }); + + for (let t = 0; t < disk.tracks.length; t++) { + // We essentially seek through the track for the Address Fields + const track = disk.tracks[t]; + let i = findBytes(track, [0xD5, 0xAA, 0xB5]); + for (let s = 0; s <= 12; s++) { + // volume 10 = 0b00001010 + expect(track[i++]).toBe(0b10101111); + expect(track[i++]).toBe(0b10101010); + // convert track to 4x4 encoding + const track4x4XX = ((t & 0b10101010) >> 1) | 0b10101010; + const track4x4YY = (t & 0b01010101) | 0b10101010; + expect(track[i++]).toBe(track4x4XX); + expect(track[i++]).toBe(track4x4YY); + // convert sector to 4x4 encoding + const ss = D13O[s]; + const sector4x4XX = ((ss & 0b10101010) >> 1) | 0b10101010; + const sector4x4YY = (ss & 0b01010101) | 0b10101010; + expect(track[i++]).toBe(sector4x4XX); + expect(track[i++]).toBe(sector4x4YY); + // checksum + expect(track[i++]).toBe(0b10101111 ^ track4x4XX ^ sector4x4XX); + expect(track[i++]).toBe(0b10101010 ^ track4x4YY ^ sector4x4YY); + // epilogue + i = expectSequence(track, i, [0xDE, 0xAA, 0xEB]); + // next sector + i = findBytes(track, [0xD5, 0xAA, 0xB5], i); + } + } + }); +}); \ No newline at end of file diff --git a/test/js/formats/testdata/13sector.spec.ts b/test/js/formats/testdata/13sector.spec.ts new file mode 100644 index 0000000..8fb8f0f --- /dev/null +++ b/test/js/formats/testdata/13sector.spec.ts @@ -0,0 +1,88 @@ +import { BYTES_BY_SECTOR, BYTES_BY_TRACK, BYTES_IN_ORDER } from './13sector'; + +describe('BYTES_IN_ORDER', () => { + it('has the correct bytes in track 0, sector 0, byte 0 and byte 1', () => { + const disk = BYTES_IN_ORDER; + expect(disk[0][0][0]).toBe(0); + expect(disk[0][0][1]).toBe(1); + }); + + it('has the correct bytes in track 0, sector 0', () => { + const disk = BYTES_IN_ORDER; + for (let i = 0; i < 256; i++) { + expect(disk[0][0][i]).toBe(i); + } + }); + + it('has the correct bytes in track 1, sector 0', () => { + const disk = BYTES_IN_ORDER; + for (let i = 0; i < 256; i++) { + expect(disk[1][0][i]).toBe(i); + } + }); + + it('has the correct bytes in track 30, sector 11', () => { + const disk = BYTES_IN_ORDER; + for (let i = 0; i < 256; i++) { + expect(disk[30][11][i]).toBe(i); + } + }); +}); + +describe('BYTES_BY_SECTOR', () => { + it('has the correct bytes in track 0, sector 0, byte 0 and byte 1', () => { + const disk = BYTES_BY_SECTOR; + expect(disk[0][0][0]).toBe(0); + expect(disk[0][0][1]).toBe(0); + }); + + it('has the correct bytes in track 0, sector 0', () => { + const disk = BYTES_BY_SECTOR; + for (let i = 0; i < 256; i++) { + expect(disk[0][0][i]).toBe(0); + } + }); + + it('has the correct bytes in track 1, sector 0', () => { + const disk = BYTES_BY_SECTOR; + for (let i = 0; i < 256; i++) { + expect(disk[1][0][i]).toBe(0); + } + }); + + it('has the correct bytes in track 30, sector 11', () => { + const disk = BYTES_BY_SECTOR; + for (let i = 0; i < 256; i++) { + expect(disk[30][11][i]).toBe(11); + } + }); +}); + +describe('BYTES_BY_TRACK', () => { + it('has the correct bytes in track 0, sector 0, byte 0 and byte 1', () => { + const disk = BYTES_BY_TRACK; + expect(disk[0][0][0]).toBe(0); + expect(disk[0][0][1]).toBe(0); + }); + + it('has the correct bytes in track 0, sector 0', () => { + const disk = BYTES_BY_TRACK; + for (let i = 0; i < 256; i++) { + expect(disk[0][0][i]).toBe(0); + } + }); + + it('has the correct bytes in track 1, sector 0', () => { + const disk = BYTES_BY_TRACK; + for (let i = 0; i < 256; i++) { + expect(disk[1][0][i]).toBe(1); + } + }); + + it('has the correct bytes in track 30, sector 11', () => { + const disk = BYTES_BY_TRACK; + for (let i = 0; i < 256; i++) { + expect(disk[30][11][i]).toBe(30); + } + }); +}); \ No newline at end of file diff --git a/test/js/formats/testdata/13sector.ts b/test/js/formats/testdata/13sector.ts new file mode 100644 index 0000000..b7c7722 --- /dev/null +++ b/test/js/formats/testdata/13sector.ts @@ -0,0 +1,55 @@ +import { byte } from '../../../../js/types'; + +function generateBytesInOrder() { + const data: byte[][][] = []; + for (let t = 0; t < 35; t++) { + const track: byte[][] = []; + for (let s = 0; s < 13; s++) { + const sector: byte[] = []; + for (let b = 0; b < 256; b++) { + sector[b] = b; + } + track[s] = sector; + } + data[t] = track; + } + return data; +} + +export const BYTES_IN_ORDER: byte[][][] = generateBytesInOrder(); + +function generateBytesBySector() { + const data: byte[][][] = []; + for (let t = 0; t < 35; t++) { + const track: byte[][] = []; + for (let s = 0; s < 13; s++) { + const sector: byte[] = []; + for (let b = 0; b < 256; b++) { + sector[b] = s; + } + track[s] = sector; + } + data[t] = track; + } + return data; +} + +export const BYTES_BY_SECTOR: byte[][][] = generateBytesBySector(); + +function generateBytesByTrack() { + const data: byte[][][] = []; + for (let t = 0; t < 35; t++) { + const track: byte[][] = []; + for (let s = 0; s < 13; s++) { + const sector: byte[] = []; + for (let b = 0; b < 256; b++) { + sector[b] = t; + } + track[s] = sector; + } + data[t] = track; + } + return data; +} + +export const BYTES_BY_TRACK: byte[][][] = generateBytesByTrack();