From 72ecce113aff86874ca3b23a9c35f044908c9710 Mon Sep 17 00:00:00 2001 From: Ian Flanigan Date: Tue, 29 Dec 2020 15:40:40 +0100 Subject: [PATCH] DOS 3.3 image format tests and fixes (#49) * Adds an initial test for DOS format (.do) files * Fix physical sector order when nibblizing DOS 3.3 ordered images Before, when `.dsk` or `.do` images were nibblized, the resulting track had the sectors in the wrong physical layout. Now the nibblized track has the correct physical layout (all sectors in order) which results in the correct DOS 3.3 layout as well. There is also a test that verifies the order. * Add another test for a non-zero sector The new test checks that the values in physical sector 1 are those for DOS sector 7. * Add test for all physical sectors on all tracks This change also removes a few stray console.log calls in the test. --- js/formats/do.js | 18 +- js/formats/format_utils.ts | 10 +- test/js/formats/do.spec.ts | 338 ++++++++++++++++++++++ test/js/formats/testdata/16sector.spec.ts | 88 ++++++ test/js/formats/testdata/16sector.ts | 55 ++++ 5 files changed, 498 insertions(+), 11 deletions(-) create mode 100644 test/js/formats/do.spec.ts create mode 100644 test/js/formats/testdata/16sector.spec.ts create mode 100644 test/js/formats/testdata/16sector.ts diff --git a/js/formats/do.js b/js/formats/do.js index f84253a..00857e8 100644 --- a/js/formats/do.js +++ b/js/formats/do.js @@ -9,9 +9,14 @@ * implied warranty. */ -import { explodeSector16, _DO } from './format_utils'; +import { explodeSector16, DO } from './format_utils'; import { bytify } from '../util'; +/** + * Returns a `Disk` object from DOS-ordered image data. + * @param {*} options the disk image and options + * @returns {import('./format_utils').Disk} + */ export default function DOS(options) { var { data, name, rawData, volume, readOnly } = options; var disk = { @@ -26,21 +31,20 @@ export default function DOS(options) { for (var t = 0; t < 35; t++) { var track = []; - for (var s = 0; s < 16; s++) { - var _s = 15 - s; + for (var physical_sector = 0; physical_sector < 16; physical_sector++) { + const dos_sector = DO[physical_sector]; var sector; if (rawData) { - var off = (16 * t + _s) * 256; + const off = (16 * t + dos_sector) * 256; sector = new Uint8Array(rawData.slice(off, off + 256)); } else { - sector = data[t][_s]; + sector = data[t][dos_sector]; } track = track.concat( - explodeSector16(volume, t, _DO[_s], sector) + explodeSector16(volume, t, physical_sector, sector) ); } disk.tracks[t] = bytify(track); } - return disk; } diff --git a/js/formats/format_utils.ts b/js/formats/format_utils.ts index 88d91a5..51d1599 100644 --- a/js/formats/format_utils.ts +++ b/js/formats/format_utils.ts @@ -17,7 +17,7 @@ export type Disk = { format: string, name: string, volume: byte, - tracks: byte[], + tracks: memory[], readOnly: boolean, }; @@ -30,8 +30,10 @@ export type Drive = { } -// var DO = [0x0,0x7,0xE,0x6,0xD,0x5,0xC,0x4, -// 0xB,0x3,0xA,0x2,0x9,0x1,0x8,0xF]; +export const DO = [ + 0x0, 0x7, 0xE, 0x6, 0xD, 0x5, 0xC, 0x4, + 0xB, 0x3, 0xA, 0x2, 0x9, 0x1, 0x8, 0xF]; + export const _DO = [ 0x0, 0xD, 0xB, 0x9, 0x7, 0x5, 0x3, 0x1, 0xE, 0xC, 0xA, 0x8, 0x6, 0x4, 0x2, 0xF @@ -183,7 +185,7 @@ export function explodeSector16(volume: byte, track: byte, sector: byte, data: m } buf.push(_trans62[last]); - buf = buf.concat([0xde, 0xaa, 0xf2]); // Epilog DE AA F2 + buf = buf.concat([0xde, 0xaa, 0xeb]); // Epilog DE AA EB /* * Gap 3 diff --git a/test/js/formats/do.spec.ts b/test/js/formats/do.spec.ts new file mode 100644 index 0000000..252657c --- /dev/null +++ b/test/js/formats/do.spec.ts @@ -0,0 +1,338 @@ +import DOS from '../../../js/formats/do'; +import { memory } from '../../../js/types'; +import { BYTES_BY_SECTOR, BYTES_BY_TRACK } from './testdata/16sector'; + +function skipGap(track: memory, start: number = 0): number { + const end = start + 0x100; // no gap is this big + let i = start; + while (i < end && track[i] == 0xFF) { + i++; + } + if (i == end) { + fail(`found more than 0x100 0xFF bytes after ${start}`); + } + return i; +} + +function compareSequences(track: memory, bytes: number[], pos: number): boolean { + for (let i = 0; i < bytes.length; i++) { + if (track[i + pos] != bytes[i]) { + return false; + } + } + return true; +} + +function expectSequence(track: memory, pos: number, bytes: number[]): number { + if (!compareSequences(track, bytes, pos)) { + const track_slice = track.slice(pos, Math.min(track.length, pos + bytes.length)); + fail(`expected ${bytes} got ${track_slice}`); + } + return pos + bytes.length; +} + +function findBytes(track: memory, bytes: number[], start: number = 0): number { + for (let i = start; i < track.length; i++) { + if (compareSequences(track, bytes, i)) { + return i + bytes.length; + } + } + return -1; +} + +describe('compareSequences', () => { + it('matches at pos 0', () => { + expect( + compareSequences([0x01, 0x02, 0x03], [0x01, 0x02, 0x03], 0) + ).toBeTruthy(); + }); + + it('matches at pos 1', () => { + expect( + compareSequences([0x00, 0x01, 0x02, 0x03], [0x01, 0x02, 0x03], 1) + ).toBeTruthy(); + }); +}); + +describe('DOS format', () => { + it('is callable', () => { + const disk = DOS({ + name: 'test disk', + data: BYTES_BY_TRACK, + volume: 10, + readOnly: true, + }); + expect(disk).not.toBeNull(); + }); + + it('has correct number of tracks', () => { + const disk = DOS({ + 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 = DOS({ + name: 'test disk', + data: BYTES_BY_TRACK, + volume: 10, + readOnly: true, + }); + expect(disk.tracks[0].length).toEqual(6632); + }); + + it('has correct number of bytes in all tracks', () => { + const disk = DOS({ + 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(6632); + for (let i = 1; i < disk.tracks.length; i++) { + expect(disk.tracks[i].length).toEqual(6602); + } + }); + + it('has correct GAP 1', () => { + const disk = DOS({ + 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 = DOS({ + 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, 0x96]); + // 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 = DOS({ + 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 0x96 with 6 and 2 encoding) + for (let j = 0; j < 342; j++) { + expect(track[i++]).toBe(0x96); + } + // checksum (also zero) + expect(track[i++]).toBe(0x96); + // 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 = DOS({ + 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, 0x96]); + + // second sector prologue + i = findBytes(track, [0xD5, 0xAA, 0x96], 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 1 = 0b00000001 + expect(track[i++]).toBe(0b10101010); + expect(track[i++]).toBe(0b10101011); + // checksum = 0b00000101 + 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 0, sector 1 (BYTES_BY_SECTOR)', () => { + // _Beneath Apple DOS_, DATA FIELD ENCODING, pp. 3-13 to 3-21 + const disk = DOS({ + 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 DOS sector 7. + // In 6 x 2 encoding, the lowest 2 bits of all the bytes come first. + // 0x07 is 0b00000111, so the lowest two bits are 0b11, reversed and + // repeated would be 0b111111 (3F -> 0xFF), but since each byte is + // XOR'd with the previous, this means there are 85 0b00000000 (00 -> + // 0x96) bytes. + expect(track[i++]).toBe(0xFF); + for (let j = 0; j < 85; j++) { + expect(track[i++]).toBe(0x96); + } + // Next we get 256 instances of the top bits, 0b000001. Again, with + // the XOR, this means one 0b000001 XOR 0b111111 = 0b111110 + // (3E -> 0xFE) followed by 255 0b0000000 (00 -> 0x96). + expect(track[i++]).toBe(0xFE); + for (let j = 0; j < 255; j++) { + expect(track[i++]).toBe(0x96); + } + // checksum + expect(track[i++]).toBe(0x97); + // 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 = DOS({ + 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, 0x96]); + // 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 = DOS({ + 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]); + // In 6 x 2 encoding, the lowest 2 bits of all the bytes come first. + // This would normally mean 86 instances of 0b101010 (2A -> 0xE6), + // but each byte is XOR'd with the previous. Since all of the bits + // are the same, this means there are 85 0b000000 (00 -> 0x96). + expect(track[i++]).toBe(0xE6); + for (let j = 0; j < 85; j++) { + expect(track[i++]).toBe(0x96); + } + // Next we get 256 instances of the top bits, 0b000000. Again, with + // the XOR, this means one 0x101010 (2A -> 0xE6) followed by 255 + // 0b0000000 (00 -> 0x96). + expect(track[i++]).toBe(0xE6); + for (let j = 0; j < 255; j++) { + expect(track[i++]).toBe(0x96); + } + // checksum (also zero) + expect(track[i++]).toBe(0x96); + // 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 = DOS({ + 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, 0x96]); + for (let s = 0; s <= 15; 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 sector4x4XX = ((s & 0b10101010) >> 1) | 0b10101010; + const sector4x4YY = (s & 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, 0x96], i); + } + } + }); +}); \ No newline at end of file diff --git a/test/js/formats/testdata/16sector.spec.ts b/test/js/formats/testdata/16sector.spec.ts new file mode 100644 index 0000000..96c9e0a --- /dev/null +++ b/test/js/formats/testdata/16sector.spec.ts @@ -0,0 +1,88 @@ +import { BYTES_BY_SECTOR, BYTES_BY_TRACK, BYTES_IN_ORDER } from './16sector'; + +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/16sector.ts b/test/js/formats/testdata/16sector.ts new file mode 100644 index 0000000..1d82948 --- /dev/null +++ b/test/js/formats/testdata/16sector.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 < 16; 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 < 16; 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 < 16; 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();