mirror of
https://github.com/whscullin/apple2js.git
synced 2024-01-12 14:14:38 +00:00
04ae0327c2
This adds both the recommended TypeScript checks, plus the recommended TypeScript checks that require type checking. This latter addition means that eslint essentially has to compile all of the TypeScript in the project, causing it to be slower. This isn't much of a problem in VS Code because there's a lot of caching being done, but it's clearly slower when run on the commandline. All of the errors are either fixed or suppressed. Some errors are suppressed because fixing them would be too laborious for the little value gained. The eslint config is also slightly refactored to separate the strictly TypeScript checks from the JavaScript checks.
590 lines
17 KiB
TypeScript
590 lines
17 KiB
TypeScript
import { bit, byte, memory } from '../types';
|
|
import { base64_decode, base64_encode } from '../base64';
|
|
import { bytify, debug, toHex } from '../util';
|
|
import { NibbleDisk, ENCODING_NIBBLE, JSONDisk } from './types';
|
|
|
|
/**
|
|
* DOS 3.3 Physical sector order (index is physical sector, value is DOS sector).
|
|
*/
|
|
export const DO = [
|
|
0x0, 0x7, 0xE, 0x6, 0xD, 0x5, 0xC, 0x4,
|
|
0xB, 0x3, 0xA, 0x2, 0x9, 0x1, 0x8, 0xF
|
|
] as const;
|
|
|
|
/**
|
|
* DOS 3.3 Logical sector order (index is DOS sector, value is physical sector).
|
|
*/
|
|
export const _DO = [
|
|
0x0, 0xD, 0xB, 0x9, 0x7, 0x5, 0x3, 0x1,
|
|
0xE, 0xC, 0xA, 0x8, 0x6, 0x4, 0x2, 0xF
|
|
] as const;
|
|
|
|
/**
|
|
* ProDOS Physical sector order (index is physical sector, value is ProDOS sector).
|
|
*/
|
|
export const PO = [
|
|
0x0, 0x8, 0x1, 0x9, 0x2, 0xa, 0x3, 0xb,
|
|
0x4, 0xc, 0x5, 0xd, 0x6, 0xe, 0x7, 0xf
|
|
] as const;
|
|
|
|
/**
|
|
* ProDOS Logical sector order (index is ProDOS sector, value is physical sector).
|
|
*/
|
|
export const _PO = [
|
|
0x0, 0x2, 0x4, 0x6, 0x8, 0xa, 0xc, 0xe,
|
|
0x1, 0x3, 0x5, 0x7, 0x9, 0xb, 0xd, 0xf
|
|
] as const;
|
|
|
|
/**
|
|
* DOS 13-sector disk physical sector order (index is disk sector, value is
|
|
* physical sector).
|
|
*/
|
|
export const D13O = [
|
|
0x0, 0xa, 0x7, 0x4, 0x1, 0xb, 0x8, 0x5, 0x2, 0xc, 0x9, 0x6, 0x3
|
|
] as const;
|
|
|
|
export const _D13O = [
|
|
0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xa, 0xb, 0xc
|
|
] as const;
|
|
|
|
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 = [
|
|
0x96, 0x97, 0x9a, 0x9b, 0x9d, 0x9e, 0x9f, 0xa6,
|
|
0xa7, 0xab, 0xac, 0xad, 0xae, 0xaf, 0xb2, 0xb3,
|
|
0xb4, 0xb5, 0xb6, 0xb7, 0xb9, 0xba, 0xbb, 0xbc,
|
|
0xbd, 0xbe, 0xbf, 0xcb, 0xcd, 0xce, 0xcf, 0xd3,
|
|
0xd6, 0xd7, 0xd9, 0xda, 0xdb, 0xdc, 0xdd, 0xde,
|
|
0xdf, 0xe5, 0xe6, 0xe7, 0xe9, 0xea, 0xeb, 0xec,
|
|
0xed, 0xee, 0xef, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6,
|
|
0xf7, 0xf9, 0xfa, 0xfb, 0xfc, 0xfd, 0xfe, 0xff
|
|
] as const;
|
|
|
|
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,
|
|
0x00, 0x00, 0x02, 0x03, 0x00, 0x04, 0x05, 0x06,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x08,
|
|
0x00, 0x00, 0x00, 0x09, 0x0A, 0x0B, 0x0C, 0x0D,
|
|
0x00, 0x00, 0x0E, 0x0F, 0x10, 0x11, 0x12, 0x13,
|
|
0x00, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x1B, 0x00, 0x1C, 0x1D, 0x1E,
|
|
0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x20, 0x21,
|
|
0x00, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x29, 0x2A, 0x2B,
|
|
0x00, 0x2C, 0x2D, 0x2E, 0x2F, 0x30, 0x31, 0x32,
|
|
0x00, 0x00, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38,
|
|
0x00, 0x39, 0x3A, 0x3B, 0x3C, 0x3D, 0x3E, 0x3F
|
|
] as const;
|
|
|
|
/**
|
|
* Converts a byte into its 4x4 encoded representation
|
|
*
|
|
* @param val byte to encode.
|
|
* @returns A two byte array of representing the 4x4 encoding.
|
|
*/
|
|
export function fourXfour(val: byte): [xx: byte, yy: byte] {
|
|
let xx = val & 0xaa;
|
|
let yy = val & 0x55;
|
|
|
|
xx >>= 1;
|
|
xx |= 0xaa;
|
|
yy |= 0xaa;
|
|
|
|
return [xx, yy];
|
|
}
|
|
|
|
/**
|
|
* Converts 2 4x4 encoded bytes into a byte value
|
|
*
|
|
* @param xx First encoded byte.
|
|
* @param yy Second encoded byte.
|
|
* @returns The decoded value.
|
|
*/
|
|
export function defourXfour(xx: byte, yy: byte): byte {
|
|
return ((xx << 1) | 0x01) & yy;
|
|
}
|
|
|
|
/**
|
|
* Converts a raw sector into a nibblized representation to be combined into a
|
|
* nibblized 16 sector track.
|
|
*
|
|
* @param volume volume number
|
|
* @param track track number
|
|
* @param sector sector number
|
|
* @param data sector data
|
|
* @returns a nibblized representation of the sector data
|
|
*/
|
|
export function explodeSector16(volume: byte, track: byte, sector: byte, data: memory): byte[] {
|
|
let buf = [];
|
|
let gap;
|
|
|
|
/*
|
|
* Gap 1/3 (40/0x28 bytes)
|
|
*/
|
|
|
|
if (sector === 0) // Gap 1
|
|
gap = 0x80;
|
|
else { // Gap 3
|
|
gap = track === 0 ? 0x28 : 0x26;
|
|
}
|
|
|
|
for (let idx = 0; idx < gap; idx++) {
|
|
buf.push(0xff);
|
|
}
|
|
|
|
/*
|
|
* Address Field
|
|
*/
|
|
|
|
const checksum = volume ^ track ^ sector;
|
|
buf = buf.concat([0xd5, 0xaa, 0x96]); // Address Prolog D5 AA 96
|
|
buf = buf.concat(fourXfour(volume));
|
|
buf = buf.concat(fourXfour(track));
|
|
buf = buf.concat(fourXfour(sector));
|
|
buf = buf.concat(fourXfour(checksum));
|
|
buf = buf.concat([0xde, 0xaa, 0xeb]); // Epilog DE AA EB
|
|
|
|
/*
|
|
* Gap 2 (5 bytes)
|
|
*/
|
|
|
|
for (let idx = 0; idx < 0x05; idx++) {
|
|
buf.push(0xff);
|
|
}
|
|
|
|
/*
|
|
* Data Field
|
|
*/
|
|
|
|
buf = buf.concat([0xd5, 0xaa, 0xad]); // Data Prolog D5 AA AD
|
|
|
|
const nibbles: byte[] = [];
|
|
const ptr2 = 0;
|
|
const ptr6 = 0x56;
|
|
|
|
for (let idx = 0; idx < 0x156; idx++) {
|
|
nibbles[idx] = 0;
|
|
}
|
|
|
|
let idx2 = 0x55;
|
|
for (let idx6 = 0x101; idx6 >= 0; idx6--) {
|
|
let val6 = data[idx6 % 0x100];
|
|
let val2: byte = nibbles[ptr2 + idx2];
|
|
|
|
val2 = (val2 << 1) | (val6 & 1);
|
|
val6 >>= 1;
|
|
val2 = (val2 << 1) | (val6 & 1);
|
|
val6 >>= 1;
|
|
|
|
nibbles[ptr6 + idx6] = val6;
|
|
nibbles[ptr2 + idx2] = val2;
|
|
|
|
if (--idx2 < 0)
|
|
idx2 = 0x55;
|
|
}
|
|
|
|
let last = 0;
|
|
for (let idx = 0; idx < 0x156; idx++) {
|
|
const val = nibbles[idx];
|
|
buf.push(_trans62[last ^ val]);
|
|
last = val;
|
|
}
|
|
buf.push(_trans62[last]);
|
|
|
|
buf = buf.concat([0xde, 0xaa, 0xeb]); // Epilog DE AA EB
|
|
|
|
/*
|
|
* Gap 3
|
|
*/
|
|
|
|
buf.push(0xff);
|
|
|
|
return buf;
|
|
}
|
|
|
|
/**
|
|
* Converts a raw sector into a nibblized representation to be combined into
|
|
* a nibblized 13 sector track.
|
|
*
|
|
* @param volume volume number
|
|
* @param track track number
|
|
* @param sector sector number
|
|
* @param data sector data
|
|
* @returns a nibblized representation of the sector data
|
|
*/
|
|
export function explodeSector13(volume: byte, track: byte, sector: byte, data: memory): byte[] {
|
|
let buf = [];
|
|
let gap;
|
|
|
|
/*
|
|
* Gap 1/3 (40/0x28 bytes)
|
|
*/
|
|
|
|
if (sector === 0) // Gap 1
|
|
gap = 0x80;
|
|
else { // Gap 3
|
|
gap = track === 0 ? 0x28 : 0x26;
|
|
}
|
|
|
|
for (let idx = 0; idx < gap; idx++) {
|
|
buf.push(0xff);
|
|
}
|
|
|
|
/*
|
|
* Address Field
|
|
*/
|
|
|
|
const checksum = volume ^ track ^ sector;
|
|
buf = buf.concat([0xd5, 0xaa, 0xb5]); // Address Prolog D5 AA B5
|
|
buf = buf.concat(fourXfour(volume));
|
|
buf = buf.concat(fourXfour(track));
|
|
buf = buf.concat(fourXfour(sector));
|
|
buf = buf.concat(fourXfour(checksum));
|
|
buf = buf.concat([0xde, 0xaa, 0xeb]); // Epilog DE AA EB
|
|
|
|
/*
|
|
* Gap 2 (5 bytes)
|
|
*/
|
|
|
|
for (let idx = 0; idx < 0x05; idx++) {
|
|
buf.push(0xff);
|
|
}
|
|
|
|
/*
|
|
* Data Field
|
|
*/
|
|
|
|
buf = buf.concat([0xd5, 0xaa, 0xad]); // Data Prolog D5 AA AD
|
|
|
|
const nibbles = [];
|
|
|
|
let jdx = 0;
|
|
for (let idx = 0x32; idx >= 0; idx--) {
|
|
const a5 = data[jdx] >> 3;
|
|
const a3 = data[jdx] & 0x07;
|
|
jdx++;
|
|
const b5 = data[jdx] >> 3;
|
|
const b3 = data[jdx] & 0x07;
|
|
jdx++;
|
|
const c5 = data[jdx] >> 3;
|
|
const c3 = data[jdx] & 0x07;
|
|
jdx++;
|
|
const d5 = data[jdx] >> 3;
|
|
const d3 = data[jdx] & 0x07;
|
|
jdx++;
|
|
const e5 = data[jdx] >> 3;
|
|
const e3 = data[jdx] & 0x07;
|
|
jdx++;
|
|
nibbles[idx + 0x00] = a5;
|
|
nibbles[idx + 0x33] = b5;
|
|
nibbles[idx + 0x66] = c5;
|
|
nibbles[idx + 0x99] = d5;
|
|
nibbles[idx + 0xcc] = e5;
|
|
nibbles[idx + 0x100] = a3 << 2 | (d3 & 0x4) >> 1 | (e3 & 0x4) >> 2;
|
|
nibbles[idx + 0x133] = b3 << 2 | (d3 & 0x2) | (e3 & 0x2) >> 1;
|
|
nibbles[idx + 0x166] = c3 << 2 | (d3 & 0x1) << 1 | (e3 & 0x1);
|
|
}
|
|
nibbles[0xff] = data[jdx] >> 3;
|
|
nibbles[0x199] = data[jdx] & 0x07;
|
|
|
|
let last = 0;
|
|
for (let idx = 0x199; idx >= 0x100; idx--) {
|
|
const val = nibbles[idx];
|
|
buf.push(_trans53[last ^ val]);
|
|
last = val;
|
|
}
|
|
for (let idx = 0x0; idx < 0x100; idx++) {
|
|
const val = nibbles[idx];
|
|
buf.push(_trans53[last ^ val]);
|
|
last = val;
|
|
}
|
|
buf.push(_trans53[last]);
|
|
|
|
buf = buf.concat([0xde, 0xaa, 0xeb]); // Epilog DE AA EB
|
|
|
|
/*
|
|
* Gap 3
|
|
*/
|
|
|
|
buf.push(0xff);
|
|
|
|
return buf;
|
|
}
|
|
|
|
/**
|
|
* Reads a sector of data from a nibblized disk
|
|
*
|
|
* TODO(flan): Does not work on WOZ disks
|
|
*
|
|
* @param disk Nibble disk
|
|
* @param track track number to read
|
|
* @param sector sector number to read
|
|
* @returns An array of sector data bytes.
|
|
*/
|
|
export function readSector(disk: NibbleDisk, track: byte, sector: byte): memory {
|
|
const _sector = disk.format === 'po' ? _PO[sector] : _DO[sector];
|
|
let val, state = 0;
|
|
let idx = 0;
|
|
let retry = 0;
|
|
const cur = disk.tracks[track];
|
|
|
|
function _readNext() {
|
|
const result = cur[idx++];
|
|
if (idx >= cur.length) {
|
|
idx = 0;
|
|
retry++;
|
|
}
|
|
return result;
|
|
}
|
|
function _skipBytes(count: number) {
|
|
idx += count;
|
|
if (idx >= cur.length) {
|
|
idx %= cur.length;
|
|
retry++;
|
|
}
|
|
}
|
|
let t = 0, s = 0, v = 0, checkSum;
|
|
const data = new Uint8Array(256);
|
|
while (retry < 4) {
|
|
switch (state) {
|
|
case 0:
|
|
val = _readNext();
|
|
state = (val === 0xd5) ? 1 : 0;
|
|
break;
|
|
case 1:
|
|
val = _readNext();
|
|
state = (val === 0xaa) ? 2 : 0;
|
|
break;
|
|
case 2:
|
|
val = _readNext();
|
|
state = (val === 0x96) ? 3 : (val === 0xad ? 4 : 0);
|
|
break;
|
|
case 3: // Address
|
|
v = defourXfour(_readNext(), _readNext()); // Volume
|
|
t = defourXfour(_readNext(), _readNext());
|
|
s = defourXfour(_readNext(), _readNext());
|
|
checkSum = defourXfour(_readNext(), _readNext());
|
|
if (checkSum !== (v ^ t ^ s)) {
|
|
debug('Invalid header checksum:', toHex(v), toHex(t), toHex(s), toHex(checkSum));
|
|
}
|
|
_skipBytes(3); // Skip footer
|
|
state = 0;
|
|
break;
|
|
case 4: // Data
|
|
if (s === _sector && t === track) {
|
|
const data2 = [];
|
|
let last = 0;
|
|
for (let jdx = 0x55; jdx >= 0; jdx--) {
|
|
val = detrans62[_readNext() - 0x80] ^ last;
|
|
data2[jdx] = val;
|
|
last = val;
|
|
}
|
|
for (let jdx = 0; jdx < 0x100; jdx++) {
|
|
val = detrans62[_readNext() - 0x80] ^ last;
|
|
data[jdx] = val;
|
|
last = val;
|
|
}
|
|
checkSum = detrans62[_readNext() - 0x80] ^ last;
|
|
if (checkSum) {
|
|
debug('Invalid data checksum:', toHex(v), toHex(t), toHex(s), toHex(checkSum));
|
|
}
|
|
for (let kdx = 0, jdx = 0x55; kdx < 0x100; kdx++) {
|
|
data[kdx] <<= 1;
|
|
if ((data2[jdx] & 0x01) !== 0) {
|
|
data[kdx] |= 0x01;
|
|
}
|
|
data2[jdx] >>= 1;
|
|
|
|
data[kdx] <<= 1;
|
|
if ((data2[jdx] & 0x01) !== 0) {
|
|
data[kdx] |= 0x01;
|
|
}
|
|
data2[jdx] >>= 1;
|
|
|
|
if (--jdx < 0) jdx = 0x55;
|
|
}
|
|
return data;
|
|
}
|
|
else
|
|
_skipBytes(0x159); // Skip data, checksum and footer
|
|
state = 0;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
return new Uint8Array();
|
|
}
|
|
|
|
/**
|
|
* Convert a nibblized disk into a JSON string for storage.
|
|
*
|
|
* @param disk Nibblized disk
|
|
* @param pretty Whether to format the output string
|
|
* @returns A JSON string representing the disk
|
|
*/
|
|
export function jsonEncode(disk: NibbleDisk, pretty: boolean): string {
|
|
// For 'nib', tracks are encoded as strings. For all other formats,
|
|
// tracks are arrays of sectors which are encoded as strings.
|
|
const data: string[] | string[][] = [];
|
|
let format = 'dsk';
|
|
for (let t = 0; t < disk.tracks.length; t++) {
|
|
data[t] = [];
|
|
if (disk.format === 'nib') {
|
|
format = 'nib';
|
|
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));
|
|
}
|
|
}
|
|
}
|
|
return JSON.stringify({
|
|
'type': format,
|
|
'encoding': 'base64',
|
|
'volume': disk.volume,
|
|
'data': data,
|
|
'readOnly': disk.readOnly,
|
|
}, undefined, pretty ? ' ' : undefined);
|
|
}
|
|
|
|
/**
|
|
* Convert a JSON string into a nibblized disk.
|
|
*
|
|
* @param data JSON string representing a disk image, created by [jsonEncode].
|
|
* @returns A nibblized disk
|
|
*/
|
|
|
|
export function jsonDecode(data: string): NibbleDisk {
|
|
const tracks: memory[] = [];
|
|
const json = JSON.parse(data) as JSONDisk;
|
|
const v = json.volume || 254;
|
|
const readOnly = json.readOnly || false;
|
|
for (let t = 0; t < json.data.length; t++) {
|
|
let track: byte[] = [];
|
|
for (let s = 0; s < json.data[t].length; s++) {
|
|
const _s = json.type === 'po' ? PO[s] : DO[s];
|
|
const sector: string = json.data[t][_s] as string;
|
|
const d = base64_decode(sector);
|
|
track = track.concat(explodeSector16(v, t, s, d));
|
|
}
|
|
tracks[t] = bytify(track);
|
|
}
|
|
const disk: NibbleDisk = {
|
|
volume: v,
|
|
format: json.type,
|
|
encoding: ENCODING_NIBBLE,
|
|
name: json.name,
|
|
tracks,
|
|
readOnly,
|
|
};
|
|
|
|
return disk;
|
|
}
|
|
|
|
/**
|
|
* Debugging method that displays the logical sector ordering of a nibblized disk
|
|
*
|
|
* @param disk
|
|
*/
|
|
|
|
export function analyseDisk(disk: NibbleDisk) {
|
|
for (let track = 0; track < 35; track++) {
|
|
let outStr = `${toHex(track)}: `;
|
|
let val, state = 0;
|
|
let idx = 0;
|
|
const cur = disk.tracks[track];
|
|
|
|
const _readNext = () => {
|
|
const result = cur[idx++];
|
|
return result;
|
|
};
|
|
|
|
const _skipBytes = (count: number) => {
|
|
idx += count;
|
|
};
|
|
|
|
let t = 0, s = 0, v = 0, checkSum;
|
|
while (idx < cur.length) {
|
|
switch (state) {
|
|
case 0:
|
|
val = _readNext();
|
|
state = (val === 0xd5) ? 1 : 0;
|
|
break;
|
|
case 1:
|
|
val = _readNext();
|
|
state = (val === 0xaa) ? 2 : 0;
|
|
break;
|
|
case 2:
|
|
val = _readNext();
|
|
state = (val === 0x96) ? 3 : (val === 0xad ? 4 : 0);
|
|
break;
|
|
case 3: // Address
|
|
v = defourXfour(_readNext(), _readNext()); // Volume
|
|
t = defourXfour(_readNext(), _readNext());
|
|
s = defourXfour(_readNext(), _readNext());
|
|
checkSum = defourXfour(_readNext(), _readNext());
|
|
if (checkSum !== (v ^ t ^ s)) {
|
|
debug('Invalid header checksum:', toHex(v), toHex(t), toHex(s), toHex(checkSum));
|
|
} else {
|
|
outStr += toHex(s, 1);
|
|
}
|
|
_skipBytes(3); // Skip footer
|
|
state = 0;
|
|
break;
|
|
case 4: // Valid header
|
|
_skipBytes(0x159); // Skip data, checksum and footer
|
|
state = 0;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
debug(outStr);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Debugging utility to convert a bitstream into a nibble. Does not wrap.
|
|
*
|
|
* @param bits Bitstream containing nibbles
|
|
* @param offset Offset into bitstream to start nibblizing
|
|
* @returns nibble, the next nibble in the bitstream,
|
|
* and offset, the end of that nibble in the bitstream
|
|
*/
|
|
|
|
export function grabNibble(bits: bit[], offset: number) {
|
|
let nibble = 0;
|
|
let waitForOne = true;
|
|
|
|
while (offset < bits.length) {
|
|
const bit = bits[offset];
|
|
if (bit) {
|
|
nibble = (nibble << 1) | 0x01;
|
|
waitForOne = false;
|
|
} else {
|
|
if (!waitForOne) {
|
|
nibble = nibble << 1;
|
|
}
|
|
}
|
|
if (nibble & 0x80) {
|
|
// nibble complete return it
|
|
break;
|
|
}
|
|
offset += 1;
|
|
}
|
|
|
|
return {
|
|
nibble: nibble,
|
|
offset: offset
|
|
};
|
|
}
|