Merge 9d4a265daf
into eab7de7db3
This commit is contained in:
commit
7cbb027ab1
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
|
@ -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();
|
||||
|
|
Loading…
Reference in New Issue