From 9d4a265dafe969d1bc663113827482352d3c59a4 Mon Sep 17 00:00:00 2001 From: Ian Flanigan Date: Sun, 5 Jun 2022 23:19:38 +0200 Subject: [PATCH] Refactor storage types Today, lots of information about how a file or JSON becomes a disk image is embedded in the metadata for the image and/or disk. This makes it hard to write back to the source when the in-memory disk image changes. This refactoring is an attempt to break out all of the bits of logic into composable pieces. While this is mestly concerned with reading right now, the idea is that it will eventually allow configuring writing as well. The main goal is to allow round-tripping to the same file on disk, but, in theory, it could also save to a different file or the local database, too. Note that this is a work in progress. --- js/formats/po.ts | 41 +- js/formats/sources.ts | 148 ++++++ js/formats/types.ts | 54 ++- js/util.ts | 11 + test/js/formats/matchers.ts | 68 +++ test/js/formats/sources.spec.ts | 545 ++++++++++++++++++++++ test/js/formats/testdata/13sector.ts | 23 + test/js/formats/testdata/16sector.spec.ts | 62 ++- test/js/formats/testdata/16sector.ts | 23 + 9 files changed, 947 insertions(+), 28 deletions(-) create mode 100644 js/formats/sources.ts create mode 100644 test/js/formats/matchers.ts create mode 100644 test/js/formats/sources.spec.ts diff --git a/js/formats/po.ts b/js/formats/po.ts index 8308def..4eb0a07 100644 --- a/js/formats/po.ts +++ b/js/formats/po.ts @@ -1,7 +1,5 @@ -import { explodeSector16, PO } from './format_utils'; -import { bytify } from '../util'; -import type { byte } from '../types'; -import { NibbleDisk, DiskOptions, ENCODING_NIBBLE } from './types'; +import { NibbleDisk, DiskOptions, ENCODING_NIBBLE, TrackSectorSource } from './types'; +import { ByteArrayArrayTrackSectorSource, ByteArrayByteSource, ByteTrackSectorSource, ProdosOrderedTrackSectorSource, TrackSector6x2NibbleTrackSource } from './sources'; /** * Returns a `Disk` object from ProDOS-ordered image data. @@ -20,24 +18,23 @@ export default function createDiskFromProDOS(options: DiskOptions) { readOnly: readOnly || false, }; - for (let physical_track = 0; physical_track < 35; physical_track++) { - let track: byte[] = []; - for (let physical_sector = 0; physical_sector < 16; physical_sector++) { - const prodos_sector = PO[physical_sector]; - let sector; - if (rawData) { - const off = (16 * physical_track + prodos_sector) * 256; - sector = new Uint8Array(rawData.slice(off, off + 256)); - } else if (data) { - sector = data[physical_track][prodos_sector]; - } else { - throw new Error('Requires data or rawData'); - } - track = track.concat( - explodeSector16(volume, physical_track, physical_sector, sector) - ); - } - disk.tracks[physical_track] = bytify(track); + let trackSectorSource: TrackSectorSource; + if (rawData) { + trackSectorSource = + new ByteTrackSectorSource( + new ByteArrayByteSource(new Uint8Array(rawData))); + } else if (data) { + trackSectorSource = new ByteArrayArrayTrackSectorSource(data); + } else { + throw new Error('Requires data or rawData'); + } + + const nibbleTrackSource = + new TrackSector6x2NibbleTrackSource( + new ProdosOrderedTrackSectorSource(trackSectorSource), volume); + + for (let physical_track = 0; physical_track < nibbleTrackSource.numTracks(); physical_track++) { + disk.tracks[physical_track] = nibbleTrackSource.read(physical_track); } return disk; diff --git a/js/formats/sources.ts b/js/formats/sources.ts new file mode 100644 index 0000000..29191eb --- /dev/null +++ b/js/formats/sources.ts @@ -0,0 +1,148 @@ +import { byte } from 'js/types'; +import { concat } from 'js/util'; +import { D13O, DO, explodeSector13, explodeSector16, PO } from './format_utils'; +import { ByteSource, Metadata, MetadataSource, NibbleTrackSource, TrackSectorSource } from './types'; + +export class FileHandleFileSource { + constructor(private readonly fileHandle: FileSystemFileHandle) { } + + getFile(): Promise { + return this.fileHandle.getFile(); + } +} + +export class FileHandleMetadataSource implements MetadataSource { + constructor(private readonly fileHandle: FileSystemFileHandle) { } + + getMetadata(): Metadata { + return { + name: this.fileHandle.name, + }; + } +} + +export class FileByteArraySource { + constructor(private readonly file: File) { } + + async getBytes() { + return new Uint8Array(await this.file.arrayBuffer()); + } +} + +export class ByteArrayByteSource implements ByteSource { + constructor(private readonly byteArray: Uint8Array) { } + + read(offset: number, length: number): Uint8Array { + return this.byteArray.subarray(offset, offset + length); + } + + length(): number { + return this.byteArray.length; + } +} + +export class ByteArrayArrayTrackSectorSource implements TrackSectorSource { + constructor( + protected readonly data: Uint8Array[][]) { } + + read(track: number, sector: number): Uint8Array { + return this.data[track][sector]; + } + + numTracks(): number { + return this.data.length; + } +} + +export class ByteTrackSectorSource implements TrackSectorSource { + constructor( + protected readonly byteSource: ByteSource, + protected readonly sectors = 16) { } + + read(track: number, sector: number): Uint8Array { + return this.byteSource.read((track * this.sectors + sector) * 256, 256); + } + + numTracks(): number { + let tracks = this.byteSource.length() / (this.sectors * 256); + if (tracks !== Math.floor(tracks)) { + tracks = Math.floor(tracks + 1); + } + return tracks; + } +} + +export class ProdosOrderedTrackSectorSource implements TrackSectorSource { + constructor(private readonly trackSectorSource: TrackSectorSource) { } + + read(track: number, sector: number): Uint8Array { + return this.trackSectorSource.read(track, PO[sector]); + } + + numTracks(): number { + return this.trackSectorSource.numTracks(); + } +} + +export class DosOrderedTrackSectorSource implements TrackSectorSource { + constructor(private readonly trackSectorSource: TrackSectorSource) { } + + read(track: number, sector: number): Uint8Array { + return this.trackSectorSource.read(track, DO[sector]); + } + + numTracks(): number { + return this.trackSectorSource.numTracks(); + } +} + +export class TrackSector6x2NibbleTrackSource implements NibbleTrackSource { + constructor( + private readonly trackSectorSource: TrackSectorSource, + private readonly volume: byte = 254) { + } + + read(track: number): Uint8Array { + const sectors: byte[][] = []; + for (let sector = 0; sector < 16; sector++) { + const data = this.trackSectorSource.read(track, sector); + sectors.push(explodeSector16(this.volume, track, sector, data)); + } + return concat(...sectors); + } + + numTracks(): number { + return this.trackSectorSource.numTracks(); + } +} + +export class TrackSector5x3NibbleTrackSource implements NibbleTrackSource { + constructor( + private readonly trackSectorSource: TrackSectorSource, + private readonly volume: byte = 254) { + } + + /* + * 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. + */ + read(track: number): Uint8Array { + const sectors: byte[][] = []; + for (let sector = 0; sector < 13; sector++) { + const physical_sector = D13O[sector]; + const data = this.trackSectorSource.read(track, physical_sector); + sectors.push(explodeSector13(this.volume, track, physical_sector, data)); + } + return concat(...sectors); + } + + numTracks(): number { + return this.trackSectorSource.numTracks(); + } +} + diff --git a/js/formats/types.ts b/js/formats/types.ts index 33bca9f..9762f68 100644 --- a/js/formats/types.ts +++ b/js/formats/types.ts @@ -1,13 +1,61 @@ -import type { byte, memory, MemberOf, word } from '../types'; +import type { byte, memory, MemberOf, word, ReadonlyUint8Array } from '../types'; import type { GamepadConfiguration } from '../ui/types'; export const DRIVE_NUMBERS = [1, 2] as const; export type DriveNumber = MemberOf; +export interface Metadata { + name: string; + side?: string; +} + +export interface MetadataSource { + getMetadata(): Metadata; +} + +export interface ByteSource { + read(offset: number, length: number): Uint8Array; + length(): number; +} + +export interface ByteSink { + write(offset: number, data: ReadonlyUint8Array): void; +} + +export interface JsonSource { + read(): string; +} + +export interface TrackSectorSource { + /** Returns the logical sector data for the given physical sector. */ + read(track: byte, sector: byte): Uint8Array; + numTracks(): byte; +} + +export interface TrackSectorSink { + write(track: byte, sector: byte, data: ReadonlyUint8Array): void; +} + +export interface NibbleTrackSource { + read(track: byte): Uint8Array; + numTracks(): byte; +} + +export interface NibbleTrackSink { + write(track: byte, data: Uint8Array): void; +} + +export interface BlockSource { + read(block: word): Uint8Array; +} + +export interface BlockSink { + write(block: word, data: Uint8Array): void; +} + /** * Arguments for the disk format processors. */ - export interface DiskOptions { name: string; side?: string | undefined; @@ -32,7 +80,6 @@ export interface DiskDescriptor { /** * JSON binary image (not used?) */ - export interface JSONBinaryImage { type: 'binary'; start: word; @@ -45,7 +92,6 @@ export interface JSONBinaryImage { * Return value from disk format processors. Describes raw disk * data which the DiskII card can process. */ - export interface Disk { name: string; side?: string | undefined; diff --git a/js/util.ts b/js/util.ts index 3302ca3..3b51d22 100644 --- a/js/util.ts +++ b/js/util.ts @@ -44,6 +44,17 @@ export function bytify(ary: number[]): memory { return new Uint8Array(ary); } +/** Returns a new Uint8Array with the concatenated data from the inputs. */ +export function concat(...arys: Array) { + const result = new Uint8Array(arys.reduce((l, ary) => l + ary.length, 0)); + let offset = 0; + for (let i = 0; i < arys.length; i++) { + result.set(arys[i], offset); + offset += arys[i].length; + } + return result; +} + /** Writes to the console. */ export function debug(...args: unknown[]): void { console.log(...args); diff --git a/test/js/formats/matchers.ts b/test/js/formats/matchers.ts new file mode 100644 index 0000000..70c2eaf --- /dev/null +++ b/test/js/formats/matchers.ts @@ -0,0 +1,68 @@ +/* eslint-disable @typescript-eslint/no-namespace */ +import ''; + +interface CustomMatchers { + equalsUint8Array(other: Uint8Array): R; +} + +declare global { + namespace jest { + interface Expect extends CustomMatchers {} + interface Matchers extends CustomMatchers {} + interface InverseAsymmetricMatchers extends CustomMatchers {} + } +} + +function short(o: { toString(): string }): string { + const result = o.toString(); + return result.length > 8 ? result.substring(0, 5) + '...' : result; +} + +function smallDiff(a: Uint8Array, b: Uint8Array): string { + let result = ''; + + if (!(a instanceof Uint8Array)) { + result += `${short(a)} is not a Uint8Array`; + } + if (!(b instanceof Uint8Array)) { + result += `${short(b)} is not a Uint8Array`; + } + if (result.length) { + return result; + } + + if (a.length !== b.length) { + return `${short(a)} is not the same length as ${short(b)}: ${a.length} !== ${b.length}`; + } + + for (let i = 0; i < a.length; i++) { + if (a[i] !== b[i]) { + result += `first diff at ${i}:\n`; + result += ` ${a.subarray(i, Math.min(i + 5, a.length)).toString()}\n`; + result += ` ${b.subarray(i, Math.min(i + 5, b.length)).toString()}`; + return result; + } + } + + return 'no differences found'; +} + +expect.extend({ + /** + * Jest matcher for large Uint8Arrays + */ + equalsUint8Array(received: Uint8Array, other: Uint8Array) { + const pass = received.length === other.length && received.every((value, i) => other[i] === value); + if (pass) { + return { + message: () => 'expected arrays not to be equal', + pass: true, + }; + } else { + return { + message: () => `expected arrays to be equal: ${smallDiff(received, other)}`, + pass: false, + }; + } + } +}); diff --git a/test/js/formats/sources.spec.ts b/test/js/formats/sources.spec.ts new file mode 100644 index 0000000..7039829 --- /dev/null +++ b/test/js/formats/sources.spec.ts @@ -0,0 +1,545 @@ +import './matchers'; +import { ByteArrayByteSource, ProdosOrderedTrackSectorSource, ByteTrackSectorSource, TrackSector5x3NibbleTrackSource, TrackSector6x2NibbleTrackSource, DosOrderedTrackSectorSource } from 'js/formats/sources'; +import { BYTES_BY_SECTOR_IMAGE, BYTES_BY_TRACK_IMAGE } from './testdata/16sector'; +import { BYTES_BY_SECTOR_IMAGE as S13_BYTES_BY_SECTOR_IMAGE, BYTES_BY_TRACK_IMAGE as S13_BYTES_BY_TRACK_IMAGE } from './testdata/13sector'; +import { D13O, DO, PO } from 'js/formats/format_utils'; +import { expectSequence, findBytes, skipGap } from './util'; + +describe('ProdosOrderedTrackSectorSource', () => { + it('returns the correct logical sector for each physical sector', () => { + const byteSource = new ByteArrayByteSource(BYTES_BY_SECTOR_IMAGE); + const byteTrackSectorSource = new ByteTrackSectorSource(byteSource); + const prodosTrackSectorSource = new ProdosOrderedTrackSectorSource(byteTrackSectorSource); + for (let s = 0; s < 16; s++) { + const sector = prodosTrackSectorSource.read(0, s); + const expected = new Uint8Array(256).fill(PO[s]); + expect(sector).equalsUint8Array(expected); + } + }); +}); + +describe('DosOrderedByteTrackSectorSource', () => { + it('returns the correct logical sector for each physical sector', () => { + const byteSource = new ByteArrayByteSource(BYTES_BY_SECTOR_IMAGE); + const byteTrackSectorSource = new ByteTrackSectorSource(byteSource); + const dosTrackSectorSource = new DosOrderedTrackSectorSource(byteTrackSectorSource); + for (let s = 0; s < 16; s++) { + const sector = dosTrackSectorSource.read(0, s); + const expected = new Uint8Array(256).fill(DO[s]); + expect(sector).equalsUint8Array(expected); + } + }); +}); + +describe('TrackSector6x2NibbleTrackSource', () => { + const nibbleTrackSource = (image: Uint8Array = BYTES_BY_TRACK_IMAGE) => { + const byteSource = new ByteArrayByteSource(image); + const byteTrackSectorSource = new ByteTrackSectorSource(byteSource); + const prodosTrackSectorSource = new ProdosOrderedTrackSectorSource(byteTrackSectorSource); + return new TrackSector6x2NibbleTrackSource(prodosTrackSectorSource, 10); + }; + + it('has correct number of tracks', () => { + const trackSectorNibbleTrackSource = nibbleTrackSource(); + expect(trackSectorNibbleTrackSource.numTracks()).toBe(35); + }); + + it('has correct number of bytes in all tracks', () => { + // Track 0 is slightly longer for some reason. + const trackSectorNibbleTrackSource = nibbleTrackSource(); + expect(trackSectorNibbleTrackSource.read(0).length).toEqual(6632); + for (let i = 1; i < trackSectorNibbleTrackSource.numTracks(); i++) { + expect(trackSectorNibbleTrackSource.read(i).length).toEqual(6602); + } + }); + + it('has correct GAP 1', () => { + // From Beneith Apple DOS, GAP 1 should have 12-85 0xFF bytes + const trackSectorNibbleTrackSource = nibbleTrackSource(); + const track = trackSectorNibbleTrackSource.read(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 trackSectorNibbleTrackSource = nibbleTrackSource(); + const track = trackSectorNibbleTrackSource.read(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 trackSectorNibbleTrackSource = nibbleTrackSource(); + const track = trackSectorNibbleTrackSource.read(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 trackSectorNibbleTrackSource = nibbleTrackSource(); + const track = trackSectorNibbleTrackSource.read(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 trackSectorNibbleTrackSource = nibbleTrackSource(BYTES_BY_SECTOR_IMAGE); + const track = trackSectorNibbleTrackSource.read(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 trackSectorNibbleTrackSource = nibbleTrackSource(); + const track = trackSectorNibbleTrackSource.read(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 trackSectorNibbleTrackSource = nibbleTrackSource(BYTES_BY_TRACK_IMAGE); + const track = trackSectorNibbleTrackSource.read(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 trackSectorNibbleTrackSource = nibbleTrackSource(BYTES_BY_TRACK_IMAGE); + + for (let t = 0; t < trackSectorNibbleTrackSource.numTracks(); t++) { + // We essentially seek through the track for the Address Fields + const track = trackSectorNibbleTrackSource.read(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); + } + } + }); +}); + +describe('TrackSector5x3NibbleTrackSource', () => { + const nibbleTrackSource = (image: Uint8Array = S13_BYTES_BY_TRACK_IMAGE) => { + const byteSource = new ByteArrayByteSource(image); + const dos32TrackSectorSource = new ByteTrackSectorSource(byteSource, 13); + return new TrackSector5x3NibbleTrackSource(dos32TrackSectorSource, 10); + }; + + it('has correct number of tracks', () => { + const trackSectorNibbleTrackSource = nibbleTrackSource(); + expect(trackSectorNibbleTrackSource.numTracks()).toBe(35); + }); + + it('has correct number of bytes in all tracks', () => { + // Track 0 is slightly longer for some reason. + const trackSectorNibbleTrackSource = nibbleTrackSource(); + expect(trackSectorNibbleTrackSource.read(0).length).toEqual(6289); + for (let i = 1; i < trackSectorNibbleTrackSource.numTracks(); i++) { + expect(trackSectorNibbleTrackSource.read(i).length).toEqual(6265); + } + }); + + it('has correct GAP 1', () => { + // From Beneith Apple DOS, GAP 1 should have 12-85 0xFF bytes + const trackSectorNibbleTrackSource = nibbleTrackSource(); + const track = trackSectorNibbleTrackSource.read(0); + let numFF = 0; + while (track[numFF] === 0xFF && numFF < 0x100) { + numFF++; + } + expect(numFF).toBeGreaterThanOrEqual(40); + expect(numFF).toBeLessThanOrEqual(128); + }); + + // eslint-disable-next-line jest/no-identical-title + it('has correct GAP 1', () => { + // From Beneith Apple DOS, GAP 1 should have 12-85 0xFF bytes + const trackSectorNibbleTrackSource = nibbleTrackSource(); + const track = trackSectorNibbleTrackSource.read(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 trackSectorNibbleTrackSource = nibbleTrackSource(); + const track = trackSectorNibbleTrackSource.read(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 trackSectorNibbleTrackSource = nibbleTrackSource(S13_BYTES_BY_TRACK_IMAGE); + const track = trackSectorNibbleTrackSource.read(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 trackSectorNibbleTrackSource = nibbleTrackSource(S13_BYTES_BY_TRACK_IMAGE); + const track = trackSectorNibbleTrackSource.read(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 trackSectorNibbleTrackSource = nibbleTrackSource(S13_BYTES_BY_SECTOR_IMAGE); + const track = trackSectorNibbleTrackSource.read(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 trackSectorNibbleTrackSource = nibbleTrackSource(); + const track = trackSectorNibbleTrackSource.read(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 trackSectorNibbleTrackSource = nibbleTrackSource(S13_BYTES_BY_TRACK_IMAGE); + const track = trackSectorNibbleTrackSource.read(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 trackSectorNibbleTrackSource = nibbleTrackSource(); + + for (let t = 0; t < trackSectorNibbleTrackSource.numTracks(); t++) { + // We essentially seek through the track for the Address Fields + const track = trackSectorNibbleTrackSource.read(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); + } + } + }); +}); diff --git a/test/js/formats/testdata/13sector.ts b/test/js/formats/testdata/13sector.ts index 6babb10..d35764a 100644 --- a/test/js/formats/testdata/13sector.ts +++ b/test/js/formats/testdata/13sector.ts @@ -1,4 +1,5 @@ import { memory } from 'js/types'; +import { concat } from 'js/util'; function generateBytesInOrder() { const data: memory[][] = []; @@ -53,3 +54,25 @@ function generateBytesByTrack() { } export const BYTES_BY_TRACK: memory[][] = generateBytesByTrack(); + +function toImage(disk: memory[][]) { + const tracks: Uint8Array[] = []; + for (let t = 0; t < disk.length; t++) { + const track = concat(...disk[t]); + tracks.push(track); + } + return concat(...tracks); +} + +export const BYTES_BY_SECTOR_IMAGE = toImage(BYTES_BY_SECTOR); +export const BYTES_BY_TRACK_IMAGE = toImage(BYTES_BY_TRACK); + +function randomImage() { + const result = new Uint8Array(35 * 13 * 256); + for (let i = 0; i < result.length; i++) { + result[i] = Math.floor(Math.random() * 256); + } + return result; +} + +export const RANDOM_IMAGE = randomImage(); diff --git a/test/js/formats/testdata/16sector.spec.ts b/test/js/formats/testdata/16sector.spec.ts index 96c9e0a..7308406 100644 --- a/test/js/formats/testdata/16sector.spec.ts +++ b/test/js/formats/testdata/16sector.spec.ts @@ -1,4 +1,4 @@ -import { BYTES_BY_SECTOR, BYTES_BY_TRACK, BYTES_IN_ORDER } from './16sector'; +import { BYTES_BY_SECTOR, BYTES_BY_SECTOR_IMAGE, BYTES_BY_TRACK, BYTES_BY_TRACK_IMAGE, BYTES_IN_ORDER } from './16sector'; describe('BYTES_IN_ORDER', () => { it('has the correct bytes in track 0, sector 0, byte 0 and byte 1', () => { @@ -58,6 +58,35 @@ describe('BYTES_BY_SECTOR', () => { }); }); +describe('BYTES_BY_SECTOR_IMAGE', () => { + it('has the correct bytes in track 0, sector 0, byte 0 and byte 1', () => { + const image = BYTES_BY_SECTOR_IMAGE; + expect(image[0]).toBe(0); + expect(image[1]).toBe(0); + }); + + it('has the correct bytes in track 0, sector 0', () => { + const image = BYTES_BY_SECTOR_IMAGE; + for (let i = 0; i < 256; i++) { + expect(image[i]).toBe(0); + } + }); + + it('has the correct bytes in track 1, sector 0', () => { + const image = BYTES_BY_SECTOR_IMAGE; + for (let i = 0; i < 256; i++) { + expect(image[1 * 16 * 256 + i]).toBe(0); + } + }); + + it('has the correct bytes in track 30, sector 11', () => { + const disk = BYTES_BY_SECTOR_IMAGE; + for (let i = 0; i < 256; i++) { + expect(disk[((30 * 16) + 11) * 256 + 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; @@ -85,4 +114,33 @@ describe('BYTES_BY_TRACK', () => { expect(disk[30][11][i]).toBe(30); } }); -}); \ No newline at end of file +}); + +describe('BYTES_BY_TRACK_IMAGE', () => { + it('has the correct bytes in track 0, sector 0, byte 0 and byte 1', () => { + const image = BYTES_BY_TRACK_IMAGE; + expect(image[0]).toBe(0); + expect(image[1]).toBe(0); + }); + + it('has the correct bytes in track 0, sector 0', () => { + const image = BYTES_BY_TRACK_IMAGE; + for (let i = 0; i < 256; i++) { + expect(image[i]).toBe(0); + } + }); + + it('has the correct bytes in track 1, sector 0', () => { + const image = BYTES_BY_TRACK_IMAGE; + for (let i = 0; i < 256; i++) { + expect(image[i + 256 * 16]).toBe(1); + } + }); + + it('has the correct bytes in track 30, sector 11', () => { + const image = BYTES_BY_TRACK_IMAGE; + for (let i = 0; i < 256; i++) { + expect(image[i + ((30 * 16) + 11) * 256]).toBe(30); + } + }); +}); diff --git a/test/js/formats/testdata/16sector.ts b/test/js/formats/testdata/16sector.ts index f28d61b..a67fd5b 100644 --- a/test/js/formats/testdata/16sector.ts +++ b/test/js/formats/testdata/16sector.ts @@ -1,4 +1,5 @@ import { memory } from 'js/types'; +import { concat } from 'js/util'; function generateBytesInOrder() { const data: memory[][] = []; @@ -53,3 +54,25 @@ function generateBytesByTrack() { } export const BYTES_BY_TRACK: memory[][] = generateBytesByTrack(); + +function toImage(disk: memory[][]) { + const tracks: Uint8Array[] = []; + for (let t = 0; t < disk.length; t++) { + const track = concat(...disk[t]); + tracks.push(track); + } + return concat(...tracks); +} + +export const BYTES_BY_SECTOR_IMAGE = toImage(BYTES_BY_SECTOR); +export const BYTES_BY_TRACK_IMAGE = toImage(BYTES_BY_TRACK); + +function randomImage() { + const result = new Uint8Array(35 * 16 * 256); + for (let i = 0; i < result.length; i++) { + result[i] = Math.floor(Math.random() * 256); + } + return result; +} + +export const RANDOM_IMAGE = randomImage();