This commit is contained in:
Ian Flanigan 2023-10-30 15:45:55 +08:00 committed by GitHub
commit 7cbb027ab1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 853 additions and 23 deletions

View File

@ -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.
@ -19,24 +17,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;

148
js/formats/sources.ts Normal file
View File

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

View File

@ -1,4 +1,4 @@
import type { byte, memory, MemberOf, word } from '../types';
import type { byte, memory, MemberOf, word, ReadonlyUint8Array } from '../types';
import type { GamepadConfiguration } from '../ui/types';
import { InfoChunk } from './woz';
@ -8,6 +8,55 @@ export type SupportedSectors = MemberOf<typeof SUPPORTED_SECTORS>;
export const DRIVE_NUMBERS = [1, 2] as const;
export type DriveNumber = MemberOf<typeof DRIVE_NUMBERS>;
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.
*/

View File

@ -0,0 +1,68 @@
/* eslint-disable @typescript-eslint/no-namespace */
import '';
interface CustomMatchers<R = unknown> {
equalsUint8Array(other: Uint8Array): R;
}
declare global {
namespace jest {
interface Expect extends CustomMatchers {}
interface Matchers<R> extends CustomMatchers<R> {}
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,
};
}
}
});

View File

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

View File

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