diff --git a/js/formats/format_utils.ts b/js/formats/format_utils.ts index 51d1599..0952c49 100644 --- a/js/formats/format_utils.ts +++ b/js/formats/format_utils.ts @@ -29,18 +29,33 @@ export type Drive = { dirty: false } - +/** + * DOS 3.3 Physical sector order (index is physical sector, value is DOS sector). + */ export const DO = [ 0x0, 0x7, 0xE, 0x6, 0xD, 0x5, 0xC, 0x4, - 0xB, 0x3, 0xA, 0x2, 0x9, 0x1, 0x8, 0xF]; + 0xB, 0x3, 0xA, 0x2, 0x9, 0x1, 0x8, 0xF +]; +/** + * DOS 3.3 Logical sector order (index is DOS sector, value is physical sector). + */ export const _DO = [ 0x0, 0xD, 0xB, 0x9, 0x7, 0x5, 0x3, 0x1, 0xE, 0xC, 0xA, 0x8, 0x6, 0x4, 0x2, 0xF ]; -// var PO = [0x0,0x8,0x1,0x9,0x2,0xa,0x3,0xb, -// 0x4,0xc,0x5,0xd,0x6,0xe,0x7,0xf]; +/** + * ProDOS Physical sector order (index is physical sector, value is ProDOS sector). + */ +export const PO = [ + 0x0, 0x8, 0x1, 0x9, 0x2, 0xa, 0x3, 0xb, + 0x4, 0xc, 0x5, 0xd, 0x6, 0xe, 0x7, 0xf +]; + +/** + * ProDOS Logical sector order (index is ProDOS sector, value is physical sector). + */ export const _PO = [ 0x0, 0x2, 0x4, 0x6, 0x8, 0xa, 0xc, 0xe, 0x1, 0x3, 0x5, 0x7, 0x9, 0xb, 0xd, 0xf diff --git a/js/formats/po.js b/js/formats/po.js index f7a025b..1bec268 100644 --- a/js/formats/po.js +++ b/js/formats/po.js @@ -9,9 +9,14 @@ * implied warranty. */ -import { explodeSector16, _PO } from './format_utils'; +import { explodeSector16, PO } from './format_utils'; import { bytify } from '../util'; +/** + * Returns a `Disk` object from ProDOS-ordered image data. + * @param {*} options the disk image and options + * @returns {import('./format_utils').Disk} + */ export default function ProDOS(options) { var { data, name, rawData, volume, readOnly } = options; var disk = { @@ -24,21 +29,22 @@ export default function ProDOS(options) { rawTracks: null }; - for (var t = 0; t < 35; t++) { + for (var physical_track = 0; physical_track < 35; physical_track++) { var track = []; - for (var s = 0; s < 16; s++) { + for (var physical_sector = 0; physical_sector < 16; physical_sector++) { + const prodos_sector = PO[physical_sector]; var sector; if (rawData) { - var off = (16 * t + s) * 256; + var off = (16 * physical_track + prodos_sector) * 256; sector = new Uint8Array(rawData.slice(off, off + 256)); } else { - sector = data[t][s]; + sector = data[physical_track][prodos_sector]; } track = track.concat( - explodeSector16(volume, t, _PO[s], sector) + explodeSector16(volume, physical_track, physical_sector, sector) ); } - disk.tracks[t] = bytify(track); + disk.tracks[physical_track] = bytify(track); } return disk; diff --git a/test/js/formats/do.spec.ts b/test/js/formats/do.spec.ts index 252657c..cea92c0 100644 --- a/test/js/formats/do.spec.ts +++ b/test/js/formats/do.spec.ts @@ -1,58 +1,7 @@ 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(); - }); -}); +import { expectSequence, findBytes, skipGap } from './util'; describe('DOS format', () => { it('is callable', () => { diff --git a/test/js/formats/po.spec.ts b/test/js/formats/po.spec.ts new file mode 100644 index 0000000..e9e2854 --- /dev/null +++ b/test/js/formats/po.spec.ts @@ -0,0 +1,287 @@ +import ProDOS from '../../../js/formats/po'; +import { memory } from '../../../js/types'; +import { BYTES_BY_SECTOR, BYTES_BY_TRACK } from './testdata/16sector'; +import { expectSequence, findBytes, skipGap } from './util'; + +describe('ProDOS format', () => { + it('is callable', () => { + const disk = ProDOS({ + name: 'test disk', + data: BYTES_BY_TRACK, + volume: 10, + readOnly: true, + }); + expect(disk).not.toBeNull(); + }); + + it('has correct number of tracks', () => { + const disk = ProDOS({ + 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 = ProDOS({ + 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 = ProDOS({ + 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 = ProDOS({ + 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 = ProDOS({ + 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 = ProDOS({ + 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 = ProDOS({ + 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 = ProDOS({ + 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 ProDOS sector 8. + // In 6 x 2 encoding, the lowest 2 bits of all the bytes come first. + // 0x07 is 0b00001000, so the lowest two bits are 0b00, reversed and + // repeated would be 0b000000 (00 -> 0x96). Even though each byte is + // XOR'd with the previous, they are all the same. This means there + // are 86 0b00000000 (00 -> 0x96) bytes. + for (let j = 0; j < 86; j++) { + expect(track[i++]).toBe(0x96); + } + // Next we get 256 instances of the top bits, 0b000010. Again, with + // the XOR, this means one 0b000010 XOR 0b000000 = 0b000010 + // (02 -> 0x9A) followed by 255 0b0000000 (00 -> 0x96). + expect(track[i++]).toBe(0x9A); + for (let j = 0; j < 255; j++) { + expect(track[i++]).toBe(0x96); + } + // checksum 0b000010 XOR 0b000000 -> 9A + expect(track[i++]).toBe(0x9A); + // 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 = ProDOS({ + 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 = ProDOS({ + 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 = ProDOS({ + 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/util.spec.ts b/test/js/formats/util.spec.ts new file mode 100644 index 0000000..4a6cf79 --- /dev/null +++ b/test/js/formats/util.spec.ts @@ -0,0 +1,15 @@ +import { compareSequences } from './util'; + +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(); + }); +}); diff --git a/test/js/formats/util.ts b/test/js/formats/util.ts new file mode 100644 index 0000000..8eef292 --- /dev/null +++ b/test/js/formats/util.ts @@ -0,0 +1,42 @@ +import { memory } from '../../../js/types'; + +export 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; +} + +export 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; +} + +export 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; +} + +export function findBytes(track: memory, bytes: number[], start: number = 0): number { + if (start + bytes.length > track.length) { + return -1; + } + for (let i = start; i < track.length; i++) { + if (compareSequences(track, bytes, i)) { + return i + bytes.length; + } + } + return -1; +}