From e280c3d7b8ba09efdf68c2b4617d53d3cf9c8b32 Mon Sep 17 00:00:00 2001 From: Ian Flanigan Date: Sun, 18 Sep 2022 15:40:08 +0200 Subject: [PATCH] Add 13 sector disk support to `getBinary` (#159) Before, `getBinary` did not work for 13 sector disks because it assumed that all tracks had 16 sectors. On top of that, `readSector` could not decode sectors with 5 and 3 encoding. And finally, `findSector` couldn't even find sectors with DOS 3.2 address fields. All of these have been fixed and some tests added to make sure that they keep working. --- js/cards/disk2.ts | 40 ++- js/formats/dos/dos33.ts | 10 +- js/formats/format_utils.ts | 238 +++++++++--- test/js/formats/format_utils.spec.ts | 520 +++++++++++++++++++++++++++ 4 files changed, 751 insertions(+), 57 deletions(-) create mode 100644 test/js/formats/format_utils.spec.ts diff --git a/js/cards/disk2.ts b/js/cards/disk2.ts index e04cb30..1b16d0d 100644 --- a/js/cards/disk2.ts +++ b/js/cards/disk2.ts @@ -37,7 +37,7 @@ import { } from '../formats/create_disk'; import { toHex } from '../util'; -import { jsonDecode, jsonEncode, readSector } from '../formats/format_utils'; +import { jsonDecode, jsonEncode, readSector, _D13O, _DO, _PO } from '../formats/format_utils'; import { BOOTSTRAP_ROM_16, BOOTSTRAP_ROM_13 } from '../roms/cards/disk2'; import Apple2IO from '../apple2io'; @@ -842,9 +842,9 @@ export default class DiskII implements Card, MassStorage { }; } - // TODO(flan): Does not work on WOZ disks - rwts(drive: DriveNumber, track: byte, sector: byte) { - const curDisk = this.disks[drive]; + /** Reads the given track and physical sector. */ + rwts(disk: DriveNumber, track: byte, sector: byte) { + const curDisk = this.disks[disk]; if (!isNibbleDisk(curDisk)) { throw new Error('Can\'t read WOZ disks'); } @@ -966,8 +966,17 @@ export default class DiskII implements Card, MassStorage { this.callbacks.label(drive, name, side); } - // TODO(flan): Does not work with WOZ or D13 disks - getBinary(drive: DriveNumber, ext?: Exclude): MassStorageData | null { + /** + * Returns the binary image of the non-WOZ disk in the given drive. + * For WOZ disks, this method returns `null`. If the `ext` parameter + * is supplied, the returned data will match that format or an error + * will be thrown. If the `ext` parameter is not supplied, the + * original image format for the disk in the drive will be used. If + * the current data on the disk is no longer readable in that format, + * an error will be thrown. Using `ext == 'nib'` will always return + * an image. + */ + getBinary(drive: DriveNumber, ext?: Exclude): MassStorageData | null { const curDisk = this.disks[drive]; if (!isNibbleDisk(curDisk)) { return null; @@ -979,15 +988,26 @@ export default class DiskII implements Card, MassStorage { this.sectors * tracks.length * 256; const data = new Uint8Array(len); - const extension = ext ?? format; + ext = ext ?? format; let idx = 0; for (let t = 0; t < tracks.length; t++) { if (ext === 'nib') { data.set(tracks[t], idx); idx += tracks[t].length; } else { - for (let s = 0; s < 0x10; s++) { - const sector = readSector({ ...curDisk, format: extension }, t, s); + let maxSector: SupportedSectors; + let sectorMap: typeof _PO | typeof _DO | typeof _D13O; + if (ext === 'd13') { + maxSector = 13; + sectorMap = _D13O; + } else { + maxSector = 16; + sectorMap = format === 'po' ? _PO : _DO; + } + + for (let s = 0; s < maxSector; s++) { + const _s = sectorMap[s]; + const sector = readSector({ ...curDisk, format: ext }, t, _s); data.set(sector, idx); idx += sector.length; } @@ -995,7 +1015,7 @@ export default class DiskII implements Card, MassStorage { } return { - ext: extension, + ext, metadata: { name }, data: data.buffer, readOnly, diff --git a/js/formats/dos/dos33.ts b/js/formats/dos/dos33.ts index 62bed6c..86deb5a 100644 --- a/js/formats/dos/dos33.ts +++ b/js/formats/dos/dos33.ts @@ -3,7 +3,7 @@ import { debug, toHex } from 'js/util'; import ApplesoftDump from 'js/applesoft/decompiler'; import IntegerBASICDump from 'js/intbasic/decompiler'; import { MassStorageData, NibbleDisk } from '../types'; -import { readSector, writeSector } from '../format_utils'; +import { readSector, writeSector, _DO } from '../format_utils'; /** Usual track for VTOC */ export const DEFAULT_VTOC_TRACK = 0x11; @@ -135,7 +135,7 @@ export class DOS33 { /** * Method to read or write a sector, could be overloaded to support other - * data types. + * data types. This uses the DOS logical to physical sector mapping. * * @param track Track to read/write * @param sector Sector to read/write @@ -146,14 +146,14 @@ export class DOS33 { rwts(track: byte, sector: byte, data?: Uint8Array): Uint8Array { if (data) { if (isNibbleDisk(this.disk)) { - writeSector(this.disk, track, sector, data); + writeSector(this.disk, track, _DO[sector], data); } else { const offset = track * 0x1000 + sector * 0x100; new Uint8Array(this.disk.data).set(data, offset); } } else { if (isNibbleDisk(this.disk)) { - data = readSector(this.disk, track, sector); + data = readSector(this.disk, track, _DO[sector]); } else { const offset = track * 0x1000 + sector * 0x100; // Slice new array so modifications to apply to original track @@ -741,7 +741,7 @@ export class DOS33 { export function isMaybeDOS33(disk: NibbleDisk | MassStorageData) { let data; if (isNibbleDisk(disk)) { - data = readSector(disk, DEFAULT_VTOC_TRACK, DEFAULT_VTOC_SECTOR); + data = readSector(disk, DEFAULT_VTOC_TRACK, _DO[DEFAULT_VTOC_SECTOR]); } else if (disk.data.byteLength > 0) { data = new Uint8Array( disk.data, diff --git a/js/formats/format_utils.ts b/js/formats/format_utils.ts index a81a57e..fd2be68 100644 --- a/js/formats/format_utils.ts +++ b/js/formats/format_utils.ts @@ -1,7 +1,7 @@ import { bit, byte, memory } from '../types'; import { base64_decode, base64_encode } from '../base64'; import { bytify, debug, toHex } from '../util'; -import { NibbleDisk, ENCODING_NIBBLE, JSONDisk, isNibbleDiskFormat } from './types'; +import { NibbleDisk, ENCODING_NIBBLE, JSONDisk, isNibbleDiskFormat, SupportedSectors } from './types'; /** * DOS 3.3 Physical sector order (index is physical sector, value is DOS sector). @@ -47,14 +47,29 @@ export const _D13O = [ 0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xa, 0xb, 0xc ] as const; -const _trans53 = [ +const TRANS53 = [ 0xab, 0xad, 0xae, 0xaf, 0xb5, 0xb6, 0xb7, 0xba, 0xbb, 0xbd, 0xbe, 0xbf, 0xd6, 0xd7, 0xda, 0xdb, 0xdd, 0xde, 0xdf, 0xea, 0xeb, 0xed, 0xee, 0xef, 0xf5, 0xf6, 0xf7, 0xfa, 0xfb, 0xfd, 0xfe, 0xff ] as const; -const _trans62 = [ +export const DETRANS53 = [ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // A0 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, // A8 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x05, 0x06, // B0 + 0x00, 0x00, 0x07, 0x08, 0x00, 0x09, 0x0A, 0x0B, // B8 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // C0 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // C8 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x0D, // D0 + 0x00, 0x00, 0x0E, 0x0F, 0x00, 0x10, 0x11, 0x12, // D8 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // E0 + 0x00, 0x00, 0x13, 0x14, 0x00, 0x15, 0x16, 0x17, // E8 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x19, 0x1A, // F0 + 0x00, 0x00, 0x1B, 0x1C, 0x00, 0x1D, 0x1E, 0x1F, // F8 +] as const; + +const TRANS62 = [ 0x96, 0x97, 0x9a, 0x9b, 0x9d, 0x9e, 0x9f, 0xa6, 0xa7, 0xab, 0xac, 0xad, 0xae, 0xaf, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7, 0xb9, 0xba, 0xbb, 0xbc, @@ -65,7 +80,7 @@ const _trans62 = [ 0xf7, 0xf9, 0xfa, 0xfb, 0xfc, 0xfd, 0xfe, 0xff ] as const; -export const detrans62 = [ +export const DETRANS62 = [ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, @@ -194,10 +209,10 @@ export function explodeSector16(volume: byte, track: byte, sector: byte, data: m let last = 0; for (let idx = 0; idx < 0x156; idx++) { const val = nibbles[idx]; - buf.push(_trans62[last ^ val]); + buf.push(TRANS62[last ^ val]); last = val; } - buf.push(_trans62[last]); + buf.push(TRANS62[last]); buf = buf.concat([0xde, 0xaa, 0xeb]); // Epilog DE AA EB @@ -298,15 +313,15 @@ export function explodeSector13(volume: byte, track: byte, sector: byte, data: m let last = 0; for (let idx = 0x199; idx >= 0x100; idx--) { const val = nibbles[idx]; - buf.push(_trans53[last ^ val]); + buf.push(TRANS53[last ^ val]); last = val; } for (let idx = 0x0; idx < 0x100; idx++) { const val = nibbles[idx]; - buf.push(_trans53[last ^ val]); + buf.push(TRANS53[last ^ val]); last = val; } - buf.push(_trans53[last]); + buf.push(TRANS53[last]); buf = buf.concat([0xde, 0xaa, 0xeb]); // Epilog DE AA EB @@ -323,24 +338,46 @@ export interface TrackNibble { track: byte; sector: byte; nibble: byte; + sectors: SupportedSectors; +} + +enum LookingFor { + START_OF_FIELD_MARKER_FIRST_NIBBLE, + START_OF_FIELD_MARKER_SECOND_NIBBLE, + FIELD_TYPE_MARKER, + ADDRESS_FIELD, + ADDRESS_FIELD_13, + DATA_FIELD_6AND2, + DATA_FIELD_5AND3, +} + +export class FindSectorError extends Error { + constructor(track: byte, sector: byte, e: unknown | Error | string) { + super(`Error finding track ${track} (${toHex(track)}), sector ${sector} (${toHex(sector)}): ` + + (e instanceof Error + ? `${e.message}` + : `${String(e)}`)); + } } /** - * Finds a sector of data from a nibblized disk - * - * TODO(flan): Does not work on WOZ disks + * Finds a sector of data from a nibblized disk. The sector given should be the + * "physical" sector number, meaning the one that appears in the address field. + * The first sector with the right sector number and data whose checksum matches + * is returned. This means that for a dual-boot disk (DOS 3.2 and DOS 3.3), + * whichever sector is found first wins. * * @param disk Nibble disk * @param track track number to read * @param sector sector number to read - * @returns An array of sector data bytes. + * @returns the track, sector, nibble offset, and detected sectors */ export function findSector(disk: NibbleDisk, track: byte, sector: byte): TrackNibble { - const _sector = disk.format === 'po' ? _PO[sector] : _DO[sector]; - let val, state = 0; + const cur = disk.tracks[track]; + let sectors: SupportedSectors = 16; + let state = LookingFor.START_OF_FIELD_MARKER_FIRST_NIBBLE; let idx = 0; let retry = 0; - const cur = disk.tracks[track]; function _readNext() { const result = cur[idx++]; @@ -359,23 +396,42 @@ export function findSector(disk: NibbleDisk, track: byte, sector: byte): TrackNi } let t = 0, s = 0, v = 0, checkSum; while (retry < 4) { + let val: byte; switch (state) { - case 0: + case LookingFor.START_OF_FIELD_MARKER_FIRST_NIBBLE: val = _readNext(); - state = (val === 0xd5) ? 1 : 0; + state = (val === 0xd5) + ? LookingFor.START_OF_FIELD_MARKER_SECOND_NIBBLE + : LookingFor.START_OF_FIELD_MARKER_FIRST_NIBBLE; break; - case 1: + case LookingFor.START_OF_FIELD_MARKER_SECOND_NIBBLE: val = _readNext(); - state = (val === 0xaa) ? 2 : 0; + state = (val === 0xaa) + ? LookingFor.FIELD_TYPE_MARKER + : LookingFor.START_OF_FIELD_MARKER_FIRST_NIBBLE; break; - case 2: + case LookingFor.FIELD_TYPE_MARKER: val = _readNext(); - state = (val === 0x96) ? 3 : (val === 0xad ? 4 : 0); + switch (val) { + case 0x96: + state = LookingFor.ADDRESS_FIELD; + sectors = 16; + break; + case 0xB5: + state = LookingFor.ADDRESS_FIELD; + sectors = 13; + break; + case 0xAD: + state = sectors === 16 ? LookingFor.DATA_FIELD_6AND2 : LookingFor.DATA_FIELD_5AND3; + break; + default: + state = LookingFor.START_OF_FIELD_MARKER_FIRST_NIBBLE; + } break; - case 3: // Address + case LookingFor.ADDRESS_FIELD: v = defourXfour(_readNext(), _readNext()); // Volume - t = defourXfour(_readNext(), _readNext()); - s = defourXfour(_readNext(), _readNext()); + t = defourXfour(_readNext(), _readNext()); // Track + s = defourXfour(_readNext(), _readNext()); // Sector checkSum = defourXfour(_readNext(), _readNext()); if (checkSum !== (v ^ t ^ s)) { debug('Invalid header checksum:', toHex(v), toHex(t), toHex(s), toHex(checkSum)); @@ -383,20 +439,20 @@ export function findSector(disk: NibbleDisk, track: byte, sector: byte): TrackNi _skipBytes(3); // Skip footer state = 0; break; - case 4: // Data - if (s === _sector && t === track) { + case LookingFor.DATA_FIELD_6AND2: + if (s === sector && t === track) { // Save start of data const nibble = idx; // Do checksum on data let last = 0; for (let jdx = 0; jdx < 0x156; jdx++) { - last = detrans62[_readNext() - 0x80] ^ last; + last = DETRANS62[_readNext() - 0x80] ^ last; } - const checkSum = detrans62[_readNext() - 0x80] ^ last; + const checkSum = DETRANS62[_readNext() - 0x80] ^ last; // Validate checksum before returning if (!checkSum) { - return { track, sector, nibble }; + return { track, sector, nibble, sectors }; } else { debug('Invalid data checksum:', toHex(last), toHex(track), toHex(sector), toHex(checkSum)); } @@ -404,19 +460,66 @@ export function findSector(disk: NibbleDisk, track: byte, sector: byte): TrackNi } else _skipBytes(0x159); // Skip data, checksum and footer - state = 0; + state = LookingFor.START_OF_FIELD_MARKER_FIRST_NIBBLE; + break; + case LookingFor.DATA_FIELD_5AND3: + if (s === sector && t === track) { + // Save start of data + const nibble = idx; + + // Do checksum on data + let last = 0; + for (let jdx = 0; jdx < 0x19A; jdx++) { + last = DETRANS53[_readNext() - 0xA0] ^ last; + } + const checkSum = DETRANS53[_readNext() - 0xA0] ^ last; + // Validate checksum before returning + if (!checkSum) { + return { track, sector, nibble, sectors }; + } else { + debug('Invalid data checksum:', toHex(last), toHex(track), toHex(sector), toHex(checkSum)); + } + _skipBytes(3); // Skip footer + } + else { + _skipBytes(0x19A); // Skip data, checksum and footer + } + state = LookingFor.START_OF_FIELD_MARKER_FIRST_NIBBLE; break; default: + state = LookingFor.START_OF_FIELD_MARKER_FIRST_NIBBLE; break; } } - throw new Error(`Unable to locate track ${track}, sector ${sector}`); + throw new FindSectorError(track, sector, `too many retries (${retry})`); +} + +export class InvalidChecksum extends Error { + constructor(expected: byte, received: byte) { + super(`Expected: ${toHex(expected)}, received: ${toHex(received)}`); + } +} + +export class ReadSectorError extends Error { + constructor(track: byte, sector: byte, e: unknown | Error) { + super(`Error reading track ${track} (${toHex(track)}), sector ${sector} (${toHex(sector)}): ` + + (e instanceof Error + ? `${e.message}` + : `${String(e)}`)); + } } /** - * Reads a sector of data from a nibblized disk + * Reads a sector of data from a nibblized disk. The sector given should be the + * "physical" sector number, meaning the one that appears in the address field. + * Like `findSector`, the first sector with the right sector number and data + * whose checksum matches is returned. This means that for a dual-boot disk + * (DOS 3.2 and DOS 3.3), whichever sector is found first wins. * - * TODO(flan): Does not work on WOZ disks + * This does not work for WOZ disks. + * + * If the given track and sector combination is not found, a `ReadSectorError` + * will be thrown. * * @param disk Nibble disk * @param track track number to read @@ -425,7 +528,7 @@ export function findSector(disk: NibbleDisk, track: byte, sector: byte): TrackNi */ export function readSector(disk: NibbleDisk, track: byte, sector: byte): Uint8Array { const trackNibble = findSector(disk, track, sector); - const { nibble } = trackNibble; + const { nibble, sectors } = trackNibble; const cur = disk.tracks[track]; let idx = nibble; @@ -437,25 +540,32 @@ export function readSector(disk: NibbleDisk, track: byte, sector: byte): Uint8Ar return result; }; + try { + return sectors === 13 ? readSector13(_readNext) : readSector16(_readNext); + } catch (e: unknown) { + throw new ReadSectorError(track, sector, e); + } +} + +function readSector16(_readNext: () => byte): Uint8Array { const data = new Uint8Array(256); const data2 = []; - let last = 0; + let last: byte = 0; let val; for (let jdx = 0x55; jdx >= 0; jdx--) { - val = detrans62[_readNext() - 0x80] ^ last; + val = DETRANS62[_readNext() - 0x80] ^ last; data2[jdx] = val; last = val; } for (let jdx = 0; jdx < 0x100; jdx++) { - val = detrans62[_readNext() - 0x80] ^ last; + val = DETRANS62[_readNext() - 0x80] ^ last; data[jdx] = val; last = val; } - const checkSum = detrans62[_readNext() - 0x80] ^ last; + const checkSum = DETRANS62[_readNext() - 0x80] ^ last; if (checkSum) { - debug('Invalid data checksum:', toHex(last), toHex(track), toHex(sector), toHex(checkSum)); - throw new Error(`Unable to read track ${track}, sector ${sector}`); + throw new InvalidChecksum(last, checkSum ^ last); } for (let kdx = 0, jdx = 0x55; kdx < 0x100; kdx++) { data[kdx] <<= 1; @@ -475,6 +585,49 @@ export function readSector(disk: NibbleDisk, track: byte, sector: byte): Uint8Ar return data; } +function readSector13(_readNext: () => byte) { + const data = new Uint8Array(256); + let val: byte; + let last: byte = 0; + + // special low 3-bits of 0xFF + val = DETRANS53[_readNext() - 0xA0] ^ last; + last = val; + data[0xff] = val & 0b111; + + // expect 0x99 nibbles of packed lower 3-bits in reverse order + for (let i = 0x98; i >= 0x00; i--) { + val = DETRANS53[_readNext() - 0xA0] ^ last; + last = val; + const off = Math.floor(i / 0x33) + 5 * (0x32 - (i % 0x33)); + const dOff = 3 + 5 * (0x32 - (i % 0x33)); + const eOff = 4 + 5 * (0x32 - (i % 0x33)); + const bit = 2 - Math.floor(i / 0x33); + data[off] = (val & 0b11100) >> 2; + data[dOff] ^= ((val & 0b00010) >> 1) << bit; + data[eOff] ^= (val & 0b1) << bit; + } + + // expect 0xFE nibbles of upper 5-bits + for (let i = 0; i < 0xFF; i++) { + val = DETRANS53[_readNext() - 0xA0] ^ last; + last = val; + const off = Math.floor(i / 0x33) + 5 * (0x32 - (i % 0x33)); + data[off] ^= val << 3; + } + + // and the last special nibble for 0xFF + val = DETRANS53[_readNext() - 0xA0] ^ last; + last = val; + data[0xFF] ^= val << 3; + + const checkSum = DETRANS53[_readNext() - 0xA0] ^ last; + if (checkSum) { + throw new InvalidChecksum(last, checkSum ^ last); + } + return data; +} + /** * Reads a sector of data from a nibblized disk * @@ -515,7 +668,8 @@ export function jsonEncode(disk: NibbleDisk, pretty: boolean): string { data[t] = base64_encode(disk.tracks[t]); } else { for (let s = 0; s < 0x10; s++) { - (data[t] as string[])[s] = base64_encode(readSector(disk, t, s)); + const _sector = disk.format === 'po' ? _PO[s] : _DO[s]; + (data[t] as string[])[s] = base64_encode(readSector(disk, t, _sector)); } } } diff --git a/test/js/formats/format_utils.spec.ts b/test/js/formats/format_utils.spec.ts new file mode 100644 index 0000000..b8f9a43 --- /dev/null +++ b/test/js/formats/format_utils.spec.ts @@ -0,0 +1,520 @@ +import createDiskFromDOS13 from 'js/formats/d13'; +import createDiskFromDOS from 'js/formats/do'; +import { defourXfour, DO, explodeSector13, findSector, fourXfour, readSector } from 'js/formats/format_utils'; +import { BYTES_BY_SECTOR as BYTES_BY_SECTOR_13, BYTES_IN_ORDER as BYTES_IN_ORDER_13 } from './testdata/13sector'; +import { BYTES_BY_SECTOR as BYTES_BY_SECTOR_16 } from './testdata/16sector'; + +describe('fourXfour', () => { + // d7 d6 d5 d4 d3 d2 d1 d0 + // => 1 d7 1 d5 1 d3 1 d1 + // 1 d6 1 d4 1 d2 1 d0 + + it('converts 0x00 correctly', () => { + // 0000 0000 => 1010 1010, 1010 1010 + expect(fourXfour(0x00)).toEqual([0b1010_1010, 0b1010_1010]); + }); + + it('converts 0xff correctly', () => { + // 1111 1111 => 1111 1111, 1111 1111 + expect(fourXfour(0xFF)).toEqual([0b1111_1111, 0b1111_1111]); + }); + + it('converts 0x55 correctly', () => { + // 0101 0101 => 1010 1010, 1111 1111 + expect(fourXfour(0x55)).toEqual([0b1010_1010, 0b1111_1111]); + }); + + it('converts 0xAA correctly', () => { + // 1010 1010 => 1111 1111, 1010 1010 + expect(fourXfour(0xAA)).toEqual([0b1111_1111, 0b1010_1010]); + }); + + it('converts 0xA5 correctly', () => { + // 1010 0101 => 1111 1010, 1010 1111 + expect(fourXfour(0xA5)).toEqual([0b1111_1010, 0b1010_1111]); + }); + + it('converts 0x5A correctly', () => { + // 0101 1010 => 1010 1111, 1111 1010 + expect(fourXfour(0x5A)).toEqual([0b1010_1111, 0b1111_1010]); + }); + + it('converts 0xC3 (0b1100_0011) correctly', () => { + // 1100 0011 => 1110 1011, 1110 1011 + expect(fourXfour(0b1100_0011)).toEqual([0b1110_1011, 0b1110_1011]); + }); + + it('converts 0x3C (0b0011_1100) correctly', () => { + // 0011 1100 => 1011 1110, 1011 1110 + expect(fourXfour(0b0011_1100)).toEqual([0b1011_1110, 0b1011_1110]); + }); +}); + +describe('defourXfour', () => { + it('converts to 0x00 correctly', () => { + // 1010 1010, 1010 1010 => 0000 0000 + expect(defourXfour(0b1010_1010, 0b1010_1010)).toEqual(0x00); + }); + + it('converts to 0xff correctly', () => { + // 1111 1111, 1111 1111 => 1111 1111 + expect(defourXfour(0b1111_1111, 0b1111_1111)).toEqual(0xFF); + }); + + it('converts to 0x55 correctly', () => { + // 1010 1010, 1111 1111 => 0101 0101 + expect(defourXfour(0b1010_1010, 0b1111_1111)).toEqual(0x55); + }); + + it('converts to 0xAA correctly', () => { + // 1111 1111, 1010 1010 => 1010 1010 + expect(defourXfour(0b1111_1111, 0b1010_1010)).toEqual(0xAA); + }); + + it('converts to 0xA5 correctly', () => { + // 1111 1010, 1010 1111 => 1010 0101 + expect(defourXfour(0b1111_1010, 0b1010_1111)).toEqual(0xA5); + }); + + it('converts to 0x5A correctly', () => { + // 1010 1111, 1111 1010 => 0101 1010 + expect(defourXfour(0b1010_1111, 0b1111_1010)).toEqual(0x5A); + }); + + it('converts to 0xC3 (0b1100_0011) correctly', () => { + // 1110 1011, 1110 1011 => 1100 0011 + expect(defourXfour(0b1110_1011, 0b1110_1011)).toEqual(0b1100_0011); + }); + + it('converts to 0x3C (0b0011_1100) correctly', () => { + // 1011 1110, 1011 1110 => 0011 1100 + expect(defourXfour(0b1011_1110, 0b1011_1110)).toEqual(0b0011_1100); + }); +}); + +describe('findSector', () => { + describe('for a 16 sector DOS disk', () => { + it('correctly finds track 0, sector 0', () => { + const disk = createDiskFromDOS({ + name: 'Disk by sector', + volume: 254, + data: BYTES_BY_SECTOR_16, + readOnly: true, + }); + const { nibble, track, sector, sectors } = findSector(disk, 0, 0); + expect(track).toBe(0); + expect(sector).toBe(0); + expect(nibble).toBe( + 128 /* GAP1 nibbles */ + + 14 /* Address Field nibbles */ + + 5 /* GAP2 nibbles */ + + 3 /* prologue nibbles */); + expect(sectors).toBe(16); + }); + + it('correctly finds track 0, sector 1', () => { + const disk = createDiskFromDOS({ + name: 'Disk by sector', + volume: 254, + data: BYTES_BY_SECTOR_16, + readOnly: true, + }); + const { nibble, track, sector, sectors } = findSector(disk, 0, 1); + expect(track).toBe(0); + expect(sector).toBe(1); + expect(nibble).toBe( + 128 /* GAP1 nibbles */ + + 1 * ( + + 14 /* Address Field nibbles */ + + 5 /* GAP2 nibbles */ + + 3 /* prologue nibbles */ + + 342 /* data 6 & 2 */ + + 1 /* checksum nibble */ + + 3 /* epilogue nibbles */ + + 41 /* GAP3 nibbles for track 0 */) + + 14 /* Address Field nibbles */ + + 5 /* GAP2 nibbles */ + + 3 /* prologue nibbles */ + ); + expect(sectors).toBe(16); + }); + + it('correctly finds track 0, sector 2', () => { + const disk = createDiskFromDOS({ + name: 'Disk by sector', + volume: 254, + data: BYTES_BY_SECTOR_16, + readOnly: true, + }); + const { nibble, track, sector, sectors } = findSector(disk, 0, 2); + expect(track).toBe(0); + expect(sector).toBe(2); + expect(nibble).toBe( + 128 /* GAP1 nibbles */ + + 2 * ( + + 14 /* Address Field nibbles */ + + 5 /* GAP2 nibbles */ + + 3 /* prologue nibbles */ + + 342 /* data 6 & 2 */ + + 1 /* checksum nibble */ + + 3 /* epilogue nibbles */ + + 41 /* GAP3 nibbles for track 0 */) + + 14 /* Address Field nibbles */ + + 5 /* GAP2 nibbles */ + + 3 /* prologue nibbles */ + ); + expect(sectors).toBe(16); + }); + + it('correctly finds track 0, sector 15', () => { + const disk = createDiskFromDOS({ + name: 'Disk by sector', + volume: 254, + data: BYTES_BY_SECTOR_16, + readOnly: true, + }); + const { nibble, track, sector, sectors } = findSector(disk, 0, 15); + expect(track).toBe(0); + expect(sector).toBe(15); + expect(nibble).toBe( + 128 /* GAP1 nibbles */ + + 15 * ( + + 14 /* Address Field nibbles */ + + 5 /* GAP2 nibbles */ + + 3 /* prologue nibbles */ + + 342 /* data 6 & 2 */ + + 1 /* checksum nibble */ + + 3 /* epilogue nibbles */ + + 41 /* GAP3 nibbles for track 0 */) + + 14 /* Address Field nibbles */ + + 5 /* GAP2 nibbles */ + + 3 /* prologue nibbles */ + ); + expect(sectors).toBe(16); + }); + + it('correctly finds track 1, sector 0', () => { + const disk = createDiskFromDOS({ + name: 'Disk by sector', + volume: 254, + data: BYTES_BY_SECTOR_16, + readOnly: true, + }); + const { nibble, track, sector, sectors } = findSector(disk, 1, 0); + expect(track).toBe(1); + expect(sector).toBe(0); + expect(nibble).toBe( + 128 /* GAP1 nibbles */ + + 14 /* Address Field nibbles */ + + 5 /* GAP2 nibbles */ + + 3 /* prologue nibbles */); + expect(sectors).toBe(16); + }); + + it('correctly finds track 1, sector 1', () => { + const disk = createDiskFromDOS({ + name: 'Disk by sector', + volume: 254, + data: BYTES_BY_SECTOR_16, + readOnly: true, + }); + const { nibble, track, sector, sectors } = findSector(disk, 1, 1); + expect(track).toBe(1); + expect(sector).toBe(1); + expect(nibble).toBe( + 128 /* GAP1 nibbles */ + + 1 * ( + + 14 /* Address Field nibbles */ + + 5 /* GAP2 nibbles */ + + 3 /* prologue nibbles */ + + 342 /* data 6 & 2 */ + + 1 /* checksum nibble */ + + 3 /* epilogue nibbles */ + + 39 /* GAP3 nibbles for track > 0 */) + + 14 /* Address Field nibbles */ + + 5 /* GAP2 nibbles */ + + 3 /* prologue nibbles */ + ); + expect(sectors).toBe(16); + }); + + it('correctly finds track 1, sector 15', () => { + const disk = createDiskFromDOS({ + name: 'Disk by sector', + volume: 254, + data: BYTES_BY_SECTOR_16, + readOnly: true, + }); + const { nibble, track, sector, sectors } = findSector(disk, 1, 15); + expect(track).toBe(1); + expect(sector).toBe(15); + expect(nibble).toBe( + 128 /* GAP1 nibbles */ + + 15 * ( + + 14 /* Address Field nibbles */ + + 5 /* GAP2 nibbles */ + + 3 /* prologue nibbles */ + + 342 /* data 6 & 2 */ + + 1 /* checksum nibble */ + + 3 /* epilogue nibbles */ + + 39 /* GAP3 nibbles for track > 0 */) + + 14 /* Address Field nibbles */ + + 5 /* GAP2 nibbles */ + + 3 /* prologue nibbles */ + ); + expect(sectors).toBe(16); + }); + }); + + describe('for a 13 sector disk', () => { + it('correctly finds track 0, sector 0 of a 13 sector disk', () => { + const disk = createDiskFromDOS13({ + name: 'Disk by sector', + volume: 254, + data: BYTES_BY_SECTOR_13, + readOnly: true, + }); + const { nibble, track, sector, sectors } = findSector(disk, 0, 0); + expect(track).toBe(0); + expect(sector).toBe(0); + expect(nibble).toBe( + 128 /* GAP1 nibbles */ + + 14 /* Address Field nibbles */ + + 5 /* GAP2 nibbles */ + + 3 /* prologue nibbles */); + expect(sectors).toBe(13); + }); + + it('correctly finds track 0, sector 1 of a 13 sector disk', () => { + const disk = createDiskFromDOS13({ + name: 'Disk by sector', + volume: 254, + data: BYTES_BY_SECTOR_13, + readOnly: true, + }); + const { nibble, track, sector, sectors } = findSector(disk, 0, 1); + expect(track).toBe(0); + expect(sector).toBe(1); + expect(nibble).toBe( + 128 /* GAP1 nibbles */ + + 4 * ( + + 14 /* Address Field nibbles */ + + 5 /* GAP2 nibbles */ + + 3 /* prologue nibbles */ + + 410 /* data 5 & 3 */ + + 1 /* checksum nibble */ + + 3 /* epilogue nibbles */ + + 41 /* GAP3 nibbles for track 0 */) + + 14 /* Address Field nibbles */ + + 5 /* GAP2 nibbles */ + + 3 /* prologue nibbles */ + ); + expect(sectors).toBe(13); + }); + + it('correctly finds track 1, sector 6 of a 13 sector disk', () => { + const disk = createDiskFromDOS13({ + name: 'Disk by sector', + volume: 254, + data: BYTES_BY_SECTOR_13, + readOnly: true, + }); + const { nibble, track, sector, sectors } = findSector(disk, 1, 6); + expect(track).toBe(1); + expect(sector).toBe(6); + expect(nibble).toBe( + 128 /* GAP1 nibbles */ + + 11 * ( + + 14 /* Address Field nibbles */ + + 5 /* GAP2 nibbles */ + + 3 /* prologue nibbles */ + + 410 /* data 5 & 3 */ + + 1 /* checksum nibble */ + + 3 /* epilogue nibbles */ + + 39 /* GAP3 nibbles for track > 0 */) + + 14 /* Address Field nibbles */ + + 5 /* GAP2 nibbles */ + + 3 /* prologue nibbles */ + ); + expect(sectors).toBe(13); + }); + }); +}); + +describe('readSector', () => { + describe('for a 16 sector disk', () => { + it('correctly reads track 0, sector 0', () => { + const disk = createDiskFromDOS({ + name: 'Disk by sector', + volume: 254, + data: BYTES_BY_SECTOR_16, + readOnly: true, + }); + const data = readSector(disk, 0, 0); + expect(data).toEqual(new Uint8Array(256)); + }); + + it('correctly reads track 0, sector 1', () => { + const disk = createDiskFromDOS({ + name: 'Disk by sector', + volume: 254, + data: BYTES_BY_SECTOR_16, + readOnly: true, + }); + const data = readSector(disk, 0, 1); + expect(data).toEqual(new Uint8Array(256).fill(DO[1])); + }); + }); + + describe('for a 13 sector disk', () => { + it('correctly reads track 0, sector 0', () => { + const disk = createDiskFromDOS13({ + name: 'Disk by sector', + volume: 254, + data: BYTES_BY_SECTOR_13, + readOnly: true, + }); + const data = readSector(disk, 0, 0); + expect(data).toEqual(new Uint8Array(256)); + }); + + it('correctly reads track 0, sector 1', () => { + const disk = createDiskFromDOS13({ + name: 'Disk by sector', + volume: 254, + data: BYTES_BY_SECTOR_13, + readOnly: true, + }); + const data = readSector(disk, 0, 1); + expect(data).toEqual(new Uint8Array(256).fill(1)); + }); + + it('correctly reads track 0, sector 0 bytes in order', () => { + const disk = createDiskFromDOS13({ + name: 'Disk by sector', + volume: 254, + data: BYTES_IN_ORDER_13, + readOnly: true, + }); + const data = readSector(disk, 0, 0); + const expected = new Uint8Array(256); + for (let i = 0; i < 256; i++) { + expected[i] = i; + } + expect(data).toEqual(expected); + }); + }); + +}); + +describe('explodeSector13', () => { + it('correctly encodes all 1s', () => { + const sector = explodeSector13(256, 0, 0, new Uint8Array(256).fill(1)); + expect(sector[0]).toBe(0xFF); + // Address prologue + expect(sector[0x80]).toBe(0xD5); + expect(sector[0x81]).toBe(0xAA); + expect(sector[0x82]).toBe(0xB5); + + // Data prologue + expect(sector[0x93]).toBe(0xD5); + expect(sector[0x94]).toBe(0xAA); + expect(sector[0x95]).toBe(0xAD); + + // Data + expect(sector[0x96]).toBe(0xAD); // 01 special low bit of 0xFF + expect(sector[0x97]).toBe(0xB7); // C:001 D0:1 E0:1 -> 07 -> 07 ^ 01 -> 06 -> B7 + expect(sector[0x98]).toBe(0xAB); // G:001 H0:1 I0:1 -> 07 -> 07 ^ 07 -> 00 -> AB + expect(sector[0x99]).toBe(0xAB); // J:001 K0:1 L0:1 -> 07 -> 07 ^ 07 -> 00 -> AB + for (let i = 0x9A; i <= 0x96 + 0x33; i++) { + expect(sector[i]).toBe(0xAB); // same as above + } + + expect(sector[0x96 + 0x34]).toBe(0xAF); // B:001 D1:0 E1:0 -> 04 ^ 07 -> 03 -> AF + expect(sector[0x96 + 0x35]).toBe(0xAB); // X:001 Y1:0 Z1:0 -> 04 ^ 04 -> 00 -> AB + for (let i = 0x96 + 0x36; i <= 0x96 + 0x33 + 0x33; i++) { + expect(sector[i]).toBe(0xAB); // same as above + } + + // expect(sector[0x98]).toBe(0xAB); // B:001 D1:0 E1:0 -> 04 -> 04 ^ 07 -> 03 -> AF + // expect(sector[0x97]).toBe(0xB7); // A:001 D2:0 E2:0 -> 04 -> 04 ^ 02 -> 06 -> B7 + }); +}); + +describe('test', () => { + it('5-bit nibble to data offset', () => { + // const off = (i: number) => 0x33 * (i % 5) + (0x32 - Math.floor(i / 5)); + const off = (i: number) => Math.floor(i / 0x33) + 5 * (0x32 - (i % 0x33)); + expect(off(0x32)).toBe(0); + expect(off(0x31)).toBe(5); + expect(off(0x30)).toBe(10); + expect(off(0x65)).toBe(1); + expect(off(0x64)).toBe(6); + expect(off(0x63)).toBe(11); + expect(off(0x98)).toBe(2); + expect(off(0x97)).toBe(7); + expect(off(0x96)).toBe(12); + expect(off(0xCB)).toBe(3); + expect(off(0xCA)).toBe(8); + expect(off(0xC9)).toBe(13); + expect(off(0xFE)).toBe(4); + expect(off(0xFD)).toBe(9); + expect(off(0xFC)).toBe(14); + + const seen = new Set(); + for (let i = 0; i < 0xFF; i++) { + seen.add(off(i)); + } + for (let i = 0; i < 0xFF; i++) { + expect(seen).toContain(i); + } + }); + it('3-bit nibble to data offset', () => { + // const off = 0x33 * (i % 3) + (0x32 - Math.floor(i / 3)); + // const off = (i: number) => Math.floor(i / 0x33) + 3 * (0x32 - (i % 0x33)); + const off = (i: number) => Math.floor(i / 0x33) + 5 * (0x32 - (i % 0x33)); + const dOff = (i: number) => 3 + 5 * (0x32 - (i % 0x33)); + const eOff = (i: number) => 4 + 5 * (0x32 - (i % 0x33)); + const bit = (i: number) => 2 - Math.floor(i / 0x33); + expect(off(0x32)).toBe(0); + expect(dOff(0x32)).toBe(3); + expect(eOff(0x32)).toBe(4); + expect(bit(0x32)).toBe(2); + + expect(off(0x65)).toBe(1); + expect(dOff(0x65)).toBe(3); + expect(eOff(0x65)).toBe(4); + expect(bit(0x65)).toBe(1); + + expect(off(0x98)).toBe(2); + expect(dOff(0x98)).toBe(3); + expect(eOff(0x98)).toBe(4); + expect(bit(0x98)).toBe(0); + + expect(off(0x31)).toBe(5); + expect(dOff(0x31)).toBe(8); + expect(eOff(0x31)).toBe(9); + + expect(off(0x64)).toBe(6); + expect(dOff(0x64)).toBe(8); + expect(eOff(0x64)).toBe(9); + + expect(off(0x97)).toBe(7); + expect(dOff(0x97)).toBe(8); + expect(eOff(0x97)).toBe(9); + + expect(off(0x30)).toBe(10); + expect(dOff(0x30)).toBe(13); + expect(eOff(0x30)).toBe(14); + + const seen = new Set(); + for (let i = 0; i < 0x99; i++) { + seen.add(off(i)); + seen.add(dOff(i)); + seen.add(eOff(i)); + } + for (let i = 0; i < 0xFF; i++) { + expect(seen).toContain(i); + } + }); +});