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.
This commit is contained in:
Ian Flanigan 2022-09-18 15:40:08 +02:00 committed by GitHub
parent 2793c25c9f
commit e280c3d7b8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 751 additions and 57 deletions

View File

@ -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<State>, MassStorage<NibbleFormat> {
};
}
// 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<State>, MassStorage<NibbleFormat> {
this.callbacks.label(drive, name, side);
}
// TODO(flan): Does not work with WOZ or D13 disks
getBinary(drive: DriveNumber, ext?: Exclude<NibbleFormat, 'woz' | 'd13'>): 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<NibbleFormat, 'woz'>): MassStorageData | null {
const curDisk = this.disks[drive];
if (!isNibbleDisk(curDisk)) {
return null;
@ -979,15 +988,26 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
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<State>, MassStorage<NibbleFormat> {
}
return {
ext: extension,
ext,
metadata: { name },
data: data.buffer,
readOnly,

View File

@ -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,

View File

@ -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));
}
}
}

View File

@ -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<number>();
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<number>();
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);
}
});
});