From 044e28e050b80dbe75da8d97dec9fdbf11887317 Mon Sep 17 00:00:00 2001 From: Will Scullin Date: Fri, 9 Jul 2021 17:54:27 -0700 Subject: [PATCH] Woz to TypeScript (#84) Woz to TypeScript, with tests added before conversion. --- js/cards/disk2.ts | 13 +- js/formats/create_disk.ts | 8 +- js/formats/format_utils.ts | 43 +++- js/formats/types.ts | 18 +- js/formats/woz.js | 282 ------------------------ js/formats/woz.ts | 313 +++++++++++++++++++++++++++ package-lock.json | 221 ++++++++++--------- package.json | 4 +- test/js/formats/d13.spec.ts | 6 +- test/js/formats/do.spec.ts | 4 +- test/js/formats/po.spec.ts | 4 +- test/js/formats/testdata/13sector.ts | 2 +- test/js/formats/testdata/16sector.ts | 2 +- test/js/formats/testdata/woz.ts | 169 +++++++++++++++ test/js/formats/util.ts | 42 +++- test/js/formats/woz.spec.ts | 91 ++++++++ 16 files changed, 810 insertions(+), 412 deletions(-) delete mode 100644 js/formats/woz.js create mode 100644 js/formats/woz.ts create mode 100644 test/js/formats/testdata/woz.ts create mode 100644 test/js/formats/woz.spec.ts diff --git a/js/cards/disk2.ts b/js/cards/disk2.ts index 22d1965..57cbe5d 100644 --- a/js/cards/disk2.ts +++ b/js/cards/disk2.ts @@ -11,7 +11,6 @@ import { base64_encode} from '../base64'; import type { - bit, byte, Card, memory, @@ -198,7 +197,7 @@ interface WozDrive extends BaseDrive { /** Maps quarter tracks to data in rawTracks; `0xFF` = random garbage. */ trackMap: byte[]; /** Unique track bitstreams. The index is arbitrary; it is NOT the track number. */ - rawTracks: bit[][]; + rawTracks: Uint8Array[]; } /** Nibble format track data. */ @@ -231,7 +230,7 @@ interface DriveState { readOnly: boolean, dirty: boolean, trackMap: number[], - rawTracks: bit[][], + rawTracks: Uint8Array[], } interface State { @@ -267,7 +266,7 @@ function getDriveState(drive: Drive): DriveState { if (isWozDrive(drive)) { result.trackMap = [...drive.trackMap]; for (let idx = 0; idx < drive.rawTracks.length; idx++) { - result.rawTracks.push([...drive.rawTracks[idx]]); + result.rawTracks.push(new Uint8Array(drive.rawTracks[idx])); } } return result; @@ -306,7 +305,7 @@ function setDriveState(state: DriveState) { rawTracks: [], }; for (let idx = 0; idx < state.rawTracks.length; idx++) { - result.rawTracks.push([...state.rawTracks[idx]]); + result.rawTracks.push(new Uint8Array(state.rawTracks[idx])); } } return result; @@ -415,11 +414,11 @@ export default class DiskII implements Card { if (!isWozDrive(this.cur)) { return; } - const track: bit[] = + const track = this.cur.rawTracks[this.cur.trackMap[this.cur.track]] || [0]; while (workCycles-- > 0) { - let pulse: bit = 0; + let pulse: number = 0; if (this.clock == 4) { pulse = track[this.cur.head]; if (!pulse) { diff --git a/js/formats/create_disk.ts b/js/formats/create_disk.ts index 58feaa6..a52abb1 100644 --- a/js/formats/create_disk.ts +++ b/js/formats/create_disk.ts @@ -1,6 +1,6 @@ import { includes, memory } from '../types'; import { base64_decode } from '../base64'; -import { Disk, NibbleFormat, DiskOptions, JSONDisk, NIBBLE_FORMATS, NibbleDisk } from './types'; +import { DiskOptions, FloppyDisk, JSONDisk, NibbleFormat, NIBBLE_FORMATS } from './types'; import createDiskFrom2MG from './2mg'; import createDiskFromD13 from './d13'; import createDiskFromDOS from './do'; @@ -14,8 +14,8 @@ import createDiskFromNibble from './nib'; * @param options * @returns A nibblized disk */ -export function createDisk(fmt: NibbleFormat, options: DiskOptions): NibbleDisk | null { - let disk: NibbleDisk | null = null; +export function createDisk(fmt: NibbleFormat, options: DiskOptions): FloppyDisk | null { + let disk: FloppyDisk | null = null; switch (fmt) { case '2mg': @@ -42,7 +42,7 @@ export function createDisk(fmt: NibbleFormat, options: DiskOptions): NibbleDisk return disk; } -export function createDiskFromJsonDisk(disk: JSONDisk): Disk | null { +export function createDiskFromJsonDisk(disk: JSONDisk): FloppyDisk | null { const fmt = disk.type; const readOnly = disk.readOnly; const name = disk.name; diff --git a/js/formats/format_utils.ts b/js/formats/format_utils.ts index 2934a63..661d99e 100644 --- a/js/formats/format_utils.ts +++ b/js/formats/format_utils.ts @@ -9,7 +9,7 @@ * implied warranty. */ -import { byte, memory } from '../types'; +import { bit, byte, memory } from '../types'; import { base64_decode, base64_encode } from '../base64'; import { bytify, debug, toHex } from '../util'; import { NibbleDisk, ENCODING_NIBBLE } from './types'; @@ -501,6 +501,12 @@ export function jsonDecode(data: string): NibbleDisk { 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)}: `; @@ -555,5 +561,38 @@ export function analyseDisk(disk: NibbleDisk) { } debug(outStr); } - return new Uint8Array(); +} + +/** + * + * @param bits Bitstream containing nibbles + * @param offset Offset into bitstream to start nibblizing + * @returns The next 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 + }; } diff --git a/js/formats/types.ts b/js/formats/types.ts index c11bbf1..2640ae9 100644 --- a/js/formats/types.ts +++ b/js/formats/types.ts @@ -9,7 +9,7 @@ * implied warranty. */ -import type { bit, byte, memory, MemberOf } from '../types'; +import type { byte, memory, MemberOf } from '../types'; import type { GamepadConfiguration } from '../ui/types'; export const DRIVE_NUMBERS = [1, 2] as const; @@ -42,18 +42,20 @@ export const ENCODING_NIBBLE = 'nibble'; export const ENCODING_BITSTREAM = 'bitstream'; export const ENCODING_BLOCK = 'block'; -export interface NibbleDisk extends Disk { - encoding: typeof ENCODING_NIBBLE - format: DiskFormat - volume: byte +export interface FloppyDisk extends Disk { tracks: memory[] } -export interface WozDisk extends Disk { +export interface NibbleDisk extends FloppyDisk { + encoding: typeof ENCODING_NIBBLE + format: DiskFormat + volume: byte +} + +export interface WozDisk extends FloppyDisk { encoding: typeof ENCODING_BITSTREAM trackMap: number[] - rawTracks: bit[][] - tracks: memory[] + rawTracks: Uint8Array[] } export interface BlockDisk extends Disk { diff --git a/js/formats/woz.js b/js/formats/woz.js deleted file mode 100644 index c960c88..0000000 --- a/js/formats/woz.js +++ /dev/null @@ -1,282 +0,0 @@ -/* Copyright 2010-2019 Will Scullin - * - * Permission to use, copy, modify, distribute, and sell this software and its - * documentation for any purpose is hereby granted without fee, provided that - * the above copyright notice appear in all copies and that both that - * copyright notice and this permission notice appear in supporting - * documentation. No representations are made about the suitability of this - * software for any purpose. It is provided "as is" without express or - * implied warranty. - */ - -import { debug, toHex } from '../util'; -import { ENCODING_BITSTREAM } from './types'; - -const WOZ_HEADER_START = 0; -const WOZ_HEADER_SIZE = 12; - -const WOZ1_SIGNATURE = 0x315A4F57; -const WOZ2_SIGNATURE = 0x325A4F57; -const WOZ_INTEGRITY_CHECK = 0x0a0d0aff; - -function stringFromBytes(data, start, end) { - return String.fromCharCode.apply( - null, - new Uint8Array(data.buffer.slice(data.byteOffset + start, data.byteOffset + end)) - ); -} - -function grabNibble(bits, offset) { - 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 - }; -} - -function InfoChunk(data) { - Object.assign(this, { - version: data.getUint8(0), - diskType: data.getUint8(1), - writeProtected: data.getUint8(2), - synchronized: data.getUint8(3), - cleaned: data.getUint8(4), - creator: stringFromBytes(data, 5, 37) - }); - - if (this.version === 2) { - Object.assign(this, { - sides: data.getUint8(37), - bootSector: data.getUint8(38), - bitTiming: data.getUint8(39), - compatibleHardware: data.getUint16(40, true), - requiredRAM: data.getUint16(42, true), - largestTrack: data.getUint16(44, true) - }); - } - - return this; -} - -function TMapChunk(data) { - this.trackMap = []; - - for (let idx = 0; idx < 160; idx++) { - this.trackMap.push(data.getUint8(idx)); - } - - return this; -} - -function TrksChunk(data) { - const WOZ_TRACK_SIZE = 6656; - const WOZ_TRACK_INFO_BITS = 6648; - - this.rawTracks = []; - this.tracks = []; - for (let trackNo = 0, idx = 0; idx < data.byteLength; idx += WOZ_TRACK_SIZE, trackNo++) { - let track = []; - const rawTrack = []; - const slice = data.buffer.slice(data.byteOffset + idx, data.byteOffset + idx + WOZ_TRACK_SIZE); - const trackData = new Uint8Array(slice); - const trackInfo = new DataView(slice); - const trackBitCount = trackInfo.getUint16(WOZ_TRACK_INFO_BITS, true); - for (let jdx = 0; jdx < trackBitCount; jdx++) { - const byteIndex = jdx >> 3; - const bitIndex = 7 - (jdx & 0x07); - rawTrack[jdx] = (trackData[byteIndex] >> bitIndex) & 0x1; - } - - track = []; - let offset = 0; - while (offset < rawTrack.length) { - const result = grabNibble(rawTrack, offset); - if (!result.nibble) { break; } - track.push(result.nibble); - offset = result.offset + 1; - } - - this.tracks[trackNo] = track; - this.rawTracks[trackNo] = rawTrack; - } - - return this; -} - -function TrksChunk2(data) { - let trackNo; - this.trks = []; - for (trackNo = 0; trackNo < 160; trackNo++) { - const startBlock = data.getUint16(trackNo * 8, true); - const blockCount = data.getUint16(trackNo * 8 + 2, true); - const bitCount = data.getUint32(trackNo * 8 + 4, true); - if (bitCount === 0) { break; } - this.trks.push({ - startBlock: startBlock, - blockCount: blockCount, - bitCount: bitCount - }); - } - this.tracks = []; - this.rawTracks = []; - - const bits = data.buffer; - for (trackNo = 0; trackNo < this.trks.length; trackNo++) { - const trk = this.trks[trackNo]; - let track = []; - const rawTrack = []; - const start = trk.startBlock * 512; - const end = start + trk.blockCount * 512; - const slice = bits.slice(start, end); - const trackData = new Uint8Array(slice); - for (let jdx = 0; jdx < trk.bitCount; jdx++) { - const byteIndex = jdx >> 3; - const bitIndex = 7 - (jdx & 0x07); - rawTrack[jdx] = (trackData[byteIndex] >> bitIndex) & 0x1; - } - - track = []; - let offset = 0; - while (offset < rawTrack.length) { - const result = grabNibble(rawTrack, offset); - if (!result.nibble) { break; } - track.push(result.nibble); - offset = result.offset + 1; - } - - this.tracks[trackNo] = new Uint8Array(track); - this.rawTracks[trackNo] = new Uint8Array(rawTrack); - } - - return this; -} - -function MetaChunk(data) { - const infoStr = stringFromBytes(data, 0, data.byteLength); - const parts = infoStr.split('\n'); - const info = parts.reduce(function(acc, part) { - const subParts = part.split('\t'); - acc[subParts[0]] = subParts[1]; - return acc; - }, {}); - - Object.assign(this, info); - - return this; -} - -/** - * Returns a `Disk` object from Woz image data. - * @param {*} options the disk image and options - * @returns {import('./format_utils').Disk} - */ -export default function createDiskFromWoz(options) { - const { rawData } = options; - const dv = new DataView(rawData, 0); - let dvOffset = 0; - const disk = { - format: 'woz', - encoding: ENCODING_BITSTREAM, - }; - - let wozVersion; - const chunks = {}; - - function readHeader() { - const wozSignature = dv.getUint32(WOZ_HEADER_START + 0, true); - - switch (wozSignature) { - case WOZ1_SIGNATURE: - wozVersion = 1; - break; - case WOZ2_SIGNATURE: - wozVersion = 2; - break; - default: - return false; - } - - if (dv.getUint32(WOZ_HEADER_START + 4, true) !== WOZ_INTEGRITY_CHECK) { - return false; - } - - return true; - } - - function readChunk() { - if (dvOffset >= dv.byteLength) { - return null; - } - - const type = dv.getUint32(dvOffset, true); - const size = dv.getUint32(dvOffset + 4, true); - const data = new DataView(dv.buffer, dvOffset + 8, size); - dvOffset += size + 8; - - return { - type: type, - size: size, - data: data - }; - } - - if (readHeader()) { - dvOffset = WOZ_HEADER_SIZE; - let chunk = readChunk(); - while (chunk) { - switch (chunk.type) { - case 0x4F464E49: // INFO - chunks.info = new InfoChunk(chunk.data); - break; - case 0x50414D54: // TMAP - chunks.tmap = new TMapChunk(chunk.data); - break; - case 0x534B5254: // TRKS - if (wozVersion === 1) { - chunks.trks = new TrksChunk(chunk.data); - } else { - chunks.trks = new TrksChunk2(chunk.data); - } - break; - case 0x4154454D: // META - chunks.meta = new MetaChunk(chunk.data); - break; - case 0x54495257: // WRIT - // Ignore - break; - default: - debug('Unsupported chunk', toHex(chunk.type, 8)); - } - chunk = readChunk(); - } - } - - debug(chunks); - - disk.trackMap = chunks.tmap.trackMap; - disk.tracks = chunks.trks.tracks; - disk.rawTracks = chunks.trks.rawTracks; - disk.readOnly = true; //chunks.info.writeProtected === 1; - disk.name = chunks.meta?.title || options.name; - - return disk; -} diff --git a/js/formats/woz.ts b/js/formats/woz.ts new file mode 100644 index 0000000..d3f434e --- /dev/null +++ b/js/formats/woz.ts @@ -0,0 +1,313 @@ +/* Copyright 2010-2021 Will Scullin + * + * Permission to use, copy, modify, distribute, and sell this software and its + * documentation for any purpose is hereby granted without fee, provided that + * the above copyright notice appear in all copies and that both that + * copyright notice and this permission notice appear in supporting + * documentation. No representations are made about the suitability of this + * software for any purpose. It is provided "as is" without express or + * implied warranty. + */ + +import { debug, toHex } from '../util'; +import { bit, byte, word } from '../types'; +import { grabNibble } from './format_utils'; +import { DiskOptions, ENCODING_BITSTREAM, WozDisk } from './types'; + +const WOZ_HEADER_START = 0; +const WOZ_HEADER_SIZE = 12; + +const WOZ1_SIGNATURE = 0x315A4F57; +const WOZ2_SIGNATURE = 0x325A4F57; +const WOZ_INTEGRITY_CHECK = 0x0a0d0aff; + +/** + * Converts a range of bytes from a DataView into an ASCII string + * + * @param data DataView containing string + * @param start start index of string + * @param end end index of string + * @returns ASCII string + */ + +function stringFromBytes(data: DataView, start: number, end: number): string { + return String.fromCharCode.apply( + null, + new Uint8Array(data.buffer.slice(data.byteOffset + start, data.byteOffset + end)) + ); +} + +export class InfoChunk { + version: byte + + // Version 1 + diskType: byte + writeProtected: byte + synchronized: byte + cleaned: byte + creator: string + + // Version 2 + sides: byte = 0 + bootSector: byte = 0 + bitTiming: byte = 0 + compatibleHardware: word = 0 + requiredRAM: word = 0 + largestTrack: word = 0 + + constructor(data: DataView) { + this.version = data.getUint8(0); + this.diskType = data.getUint8(1); + this.writeProtected = data.getUint8(2); + this.synchronized = data.getUint8(3); + this.cleaned = data.getUint8(4); + this.creator = stringFromBytes(data, 5, 37); + + if (this.version > 1) { + this.sides = data.getUint8(37); + this.bootSector = data.getUint8(38); + this.bitTiming = data.getUint8(39); + this.compatibleHardware = data.getUint16(40, true); + this.requiredRAM = data.getUint16(42, true); + this.largestTrack = data.getUint16(44, true); + } + } +} + +export class TMapChunk { + trackMap: byte[] + + constructor(data: DataView) { + this.trackMap = []; + + for (let idx = 0; idx < 160; idx++) { + this.trackMap.push(data.getUint8(idx)); + } + } +} + +const WOZ_TRACK_SIZE = 6656; +const WOZ_TRACK_INFO_BITS = 6648; + +export class TrksChunk { + rawTracks: Uint8Array[] + tracks: Uint8Array[] +} + +export class TrksChunk1 extends TrksChunk { + constructor(data: DataView) { + super(); + + this.rawTracks = []; + this.tracks = []; + + for (let trackNo = 0, idx = 0; idx < data.byteLength; idx += WOZ_TRACK_SIZE, trackNo++) { + let track = []; + const rawTrack: bit[] = []; + const slice = data.buffer.slice(data.byteOffset + idx, data.byteOffset + idx + WOZ_TRACK_SIZE); + const trackData = new Uint8Array(slice); + const trackInfo = new DataView(slice); + const trackBitCount = trackInfo.getUint16(WOZ_TRACK_INFO_BITS, true); + for (let jdx = 0; jdx < trackBitCount; jdx++) { + const byteIndex = jdx >> 3; + const bitIndex = 7 - (jdx & 0x07); + rawTrack[jdx] = (trackData[byteIndex] >> bitIndex) & 0x01 ? 1 : 0; + } + + track = []; + let offset = 0; + while (offset < rawTrack.length) { + const result = grabNibble(rawTrack, offset); + if (!result.nibble) { break; } + track.push(result.nibble); + offset = result.offset + 1; + } + + this.tracks[trackNo] = new Uint8Array(track); + this.rawTracks[trackNo] = new Uint8Array(rawTrack); + } + } +} + +export interface Trk { + startBlock: word + blockCount: word + bitCount: number +} + +export class TrksChunk2 extends TrksChunk { + trks: Trk[] + + constructor (data: DataView) { + super(); + + let trackNo; + this.trks = []; + for (trackNo = 0; trackNo < 160; trackNo++) { + const startBlock = data.getUint16(trackNo * 8, true); + const blockCount = data.getUint16(trackNo * 8 + 2, true); + const bitCount = data.getUint32(trackNo * 8 + 4, true); + if (bitCount === 0) { break; } + this.trks.push({ + startBlock: startBlock, + blockCount: blockCount, + bitCount: bitCount + }); + } + this.tracks = []; + this.rawTracks = []; + + const bits = data.buffer; + for (trackNo = 0; trackNo < this.trks.length; trackNo++) { + const trk = this.trks[trackNo]; + + let track = []; + const rawTrack: bit[] = []; + const start = trk.startBlock * 512; + const end = start + trk.blockCount * 512; + const slice = bits.slice(start, end); + const trackData = new Uint8Array(slice); + for (let jdx = 0; jdx < trk.bitCount; jdx++) { + const byteIndex = jdx >> 3; + const bitIndex = 7 - (jdx & 0x07); + rawTrack[jdx] = (trackData[byteIndex] >> bitIndex) & 0x01 ? 1 : 0; + } + + track = []; + let offset = 0; + while (offset < rawTrack.length) { + const result = grabNibble(rawTrack, offset); + if (!result.nibble) { break; } + track.push(result.nibble); + offset = result.offset + 1; + } + + this.tracks[trackNo] = new Uint8Array(track); + this.rawTracks[trackNo] = new Uint8Array(rawTrack); + } + } +} + + +export class MetaChunk { + values: Record + + constructor (data: DataView) { + const infoStr = stringFromBytes(data, 0, data.byteLength); + const parts = infoStr.split('\n'); + this.values = parts.reduce(function(acc: Record, part) { + const subParts = part.split('\t'); + acc[subParts[0]] = subParts[1]; + return acc; + }, {}); + } +} + +interface Chunks { + [key: string]: any + info?: InfoChunk + tmap?: TMapChunk + trks?: TrksChunk + meta?: MetaChunk +} + +/** + * Returns a `Disk` object from Woz image data. + * @param {*} options the disk image and options + * @returns {import('./format_utils').Disk} + */ +export default function createDiskFromWoz(options: DiskOptions) { + const { rawData } = options; + if (!rawData) { + throw new Error('Requires rawData'); + } + const dv = new DataView(rawData, 0); + let dvOffset = 0; + const disk: Partial = { + encoding: ENCODING_BITSTREAM, + }; + + let wozVersion; + const chunks: Chunks = {}; + + function readHeader() { + const wozSignature = dv.getUint32(WOZ_HEADER_START + 0, true); + + switch (wozSignature) { + case WOZ1_SIGNATURE: + wozVersion = 1; + break; + case WOZ2_SIGNATURE: + wozVersion = 2; + break; + default: + return false; + } + + if (dv.getUint32(WOZ_HEADER_START + 4, true) !== WOZ_INTEGRITY_CHECK) { + return false; + } + + return true; + } + + function readChunk() { + if (dvOffset >= dv.byteLength) { + return null; + } + + const type = dv.getUint32(dvOffset, true); + const size = dv.getUint32(dvOffset + 4, true); + const data = new DataView(dv.buffer, dvOffset + 8, size); + dvOffset += size + 8; + + return { + type: type, + size: size, + data: data + }; + } + + if (readHeader()) { + dvOffset = WOZ_HEADER_SIZE; + let chunk = readChunk(); + while (chunk) { + switch (chunk.type) { + case 0x4F464E49: // INFO + chunks.info = new InfoChunk(chunk.data); + break; + case 0x50414D54: // TMAP + chunks.tmap = new TMapChunk(chunk.data); + break; + case 0x534B5254: // TRKS + if (wozVersion === 1) { + chunks.trks = new TrksChunk1(chunk.data); + } else { + chunks.trks = new TrksChunk2(chunk.data); + } + break; + case 0x4154454D: // META + chunks.meta = new MetaChunk(chunk.data); + break; + case 0x54495257: // WRIT + // Ignore + break; + default: + debug('Unsupported chunk', toHex(chunk.type, 8)); + } + chunk = readChunk(); + } + } else { + debug('Invalid woz header'); + } + + debug(chunks); + + disk.trackMap = chunks.tmap?.trackMap || []; + disk.tracks = chunks.trks?.tracks || []; + disk.rawTracks = chunks.trks?.rawTracks || []; + disk.readOnly = true; //chunks.info.writeProtected === 1; + disk.name = chunks.meta?.values['title'] || options.name; + + return disk as WozDisk; +} diff --git a/package-lock.json b/package-lock.json index 459c7ab..0a6b6d8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,8 +19,8 @@ "@types/jest": "^26.0.14", "@types/jest-image-snapshot": "^4.3.0", "@types/micromodal": "^0.3.2", - "@typescript-eslint/eslint-plugin": "^4.6.1", - "@typescript-eslint/parser": "^4.6.1", + "@typescript-eslint/eslint-plugin": "^4.28.2", + "@typescript-eslint/parser": "^4.28.2", "ajv": "^6.12.0", "babel-jest": "^26.6.3", "canvas": "^2.7.0", @@ -2365,19 +2365,18 @@ "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "4.19.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.19.0.tgz", - "integrity": "sha512-CRQNQ0mC2Pa7VLwKFbrGVTArfdVDdefS+gTw0oC98vSI98IX5A8EVH4BzJ2FOB0YlCmm8Im36Elad/Jgtvveaw==", + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.28.2.tgz", + "integrity": "sha512-PGqpLLzHSxq956rzNGasO3GsAPf2lY9lDUBXhS++SKonglUmJypaUtcKzRtUte8CV7nruwnDxtLUKpVxs0wQBw==", "dev": true, "dependencies": { - "@typescript-eslint/experimental-utils": "4.19.0", - "@typescript-eslint/scope-manager": "4.19.0", - "debug": "^4.1.1", + "@typescript-eslint/experimental-utils": "4.28.2", + "@typescript-eslint/scope-manager": "4.28.2", + "debug": "^4.3.1", "functional-red-black-tree": "^1.0.1", - "lodash": "^4.17.15", - "regexpp": "^3.0.0", - "semver": "^7.3.2", - "tsutils": "^3.17.1" + "regexpp": "^3.1.0", + "semver": "^7.3.5", + "tsutils": "^3.21.0" }, "engines": { "node": "^10.12.0 || >=12.0.0" @@ -2412,17 +2411,17 @@ } }, "node_modules/@typescript-eslint/experimental-utils": { - "version": "4.19.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.19.0.tgz", - "integrity": "sha512-9/23F1nnyzbHKuoTqFN1iXwN3bvOm/PRIXSBR3qFAYotK/0LveEOHr5JT1WZSzcD6BESl8kPOG3OoDRKO84bHA==", + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.28.2.tgz", + "integrity": "sha512-MwHPsL6qo98RC55IoWWP8/opTykjTp4JzfPu1VfO2Z0MshNP0UZ1GEV5rYSSnZSUI8VD7iHvtIPVGW5Nfh7klQ==", "dev": true, "dependencies": { - "@types/json-schema": "^7.0.3", - "@typescript-eslint/scope-manager": "4.19.0", - "@typescript-eslint/types": "4.19.0", - "@typescript-eslint/typescript-estree": "4.19.0", - "eslint-scope": "^5.0.0", - "eslint-utils": "^2.0.0" + "@types/json-schema": "^7.0.7", + "@typescript-eslint/scope-manager": "4.28.2", + "@typescript-eslint/types": "4.28.2", + "@typescript-eslint/typescript-estree": "4.28.2", + "eslint-scope": "^5.1.1", + "eslint-utils": "^3.0.0" }, "engines": { "node": "^10.12.0 || >=12.0.0" @@ -2435,16 +2434,34 @@ "eslint": "*" } }, - "node_modules/@typescript-eslint/parser": { - "version": "4.19.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.19.0.tgz", - "integrity": "sha512-/uabZjo2ZZhm66rdAu21HA8nQebl3lAIDcybUoOxoI7VbZBYavLIwtOOmykKCJy+Xq6Vw6ugkiwn8Js7D6wieA==", + "node_modules/@typescript-eslint/experimental-utils/node_modules/eslint-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", + "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "4.19.0", - "@typescript-eslint/types": "4.19.0", - "@typescript-eslint/typescript-estree": "4.19.0", - "debug": "^4.1.1" + "eslint-visitor-keys": "^2.0.0" + }, + "engines": { + "node": "^10.0.0 || ^12.0.0 || >= 14.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=5" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.28.2.tgz", + "integrity": "sha512-Q0gSCN51eikAgFGY+gnd5p9bhhCUAl0ERMiDKrTzpSoMYRubdB8MJrTTR/BBii8z+iFwz8oihxd0RAdP4l8w8w==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "4.28.2", + "@typescript-eslint/types": "4.28.2", + "@typescript-eslint/typescript-estree": "4.28.2", + "debug": "^4.3.1" }, "engines": { "node": "^10.12.0 || >=12.0.0" @@ -2463,13 +2480,13 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "4.19.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.19.0.tgz", - "integrity": "sha512-GGy4Ba/hLXwJXygkXqMzduqOMc+Na6LrJTZXJWVhRrSuZeXmu8TAnniQVKgj8uTRKe4igO2ysYzH+Np879G75g==", + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.28.2.tgz", + "integrity": "sha512-MqbypNjIkJFEFuOwPWNDjq0nqXAKZvDNNs9yNseoGBB1wYfz1G0WHC2AVOy4XD7di3KCcW3+nhZyN6zruqmp2A==", "dev": true, "dependencies": { - "@typescript-eslint/types": "4.19.0", - "@typescript-eslint/visitor-keys": "4.19.0" + "@typescript-eslint/types": "4.28.2", + "@typescript-eslint/visitor-keys": "4.28.2" }, "engines": { "node": "^8.10.0 || ^10.13.0 || >=11.10.1" @@ -2480,9 +2497,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "4.19.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.19.0.tgz", - "integrity": "sha512-A4iAlexVvd4IBsSTNxdvdepW0D4uR/fwxDrKUa+iEY9UWvGREu2ZyB8ylTENM1SH8F7bVC9ac9+si3LWNxcBuA==", + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.28.2.tgz", + "integrity": "sha512-Gr15fuQVd93uD9zzxbApz3wf7ua3yk4ZujABZlZhaxxKY8ojo448u7XTm/+ETpy0V0dlMtj6t4VdDvdc0JmUhA==", "dev": true, "engines": { "node": "^8.10.0 || ^10.13.0 || >=11.10.1" @@ -2493,18 +2510,18 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "4.19.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.19.0.tgz", - "integrity": "sha512-3xqArJ/A62smaQYRv2ZFyTA+XxGGWmlDYrsfZG68zJeNbeqRScnhf81rUVa6QG4UgzHnXw5VnMT5cg75dQGDkA==", + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.28.2.tgz", + "integrity": "sha512-86lLstLvK6QjNZjMoYUBMMsULFw0hPHJlk1fzhAVoNjDBuPVxiwvGuPQq3fsBMCxuDJwmX87tM/AXoadhHRljg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "4.19.0", - "@typescript-eslint/visitor-keys": "4.19.0", - "debug": "^4.1.1", - "globby": "^11.0.1", + "@typescript-eslint/types": "4.28.2", + "@typescript-eslint/visitor-keys": "4.28.2", + "debug": "^4.3.1", + "globby": "^11.0.3", "is-glob": "^4.0.1", - "semver": "^7.3.2", - "tsutils": "^3.17.1" + "semver": "^7.3.5", + "tsutils": "^3.21.0" }, "engines": { "node": "^10.12.0 || >=12.0.0" @@ -2535,12 +2552,12 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "4.19.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.19.0.tgz", - "integrity": "sha512-aGPS6kz//j7XLSlgpzU2SeTqHPsmRYxFztj2vPuMMFJXZudpRSehE3WCV+BaxwZFvfAqMoSd86TEuM0PQ59E/A==", + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.28.2.tgz", + "integrity": "sha512-aT2B4PLyyRDUVUafXzpZFoc0C9t0za4BJAKP5sgWIhG+jHECQZUEjuQSCIwZdiJJ4w4cgu5r3Kh20SOdtEBl0w==", "dev": true, "dependencies": { - "@typescript-eslint/types": "4.19.0", + "@typescript-eslint/types": "4.28.2", "eslint-visitor-keys": "^2.0.0" }, "engines": { @@ -15649,19 +15666,18 @@ "dev": true }, "@typescript-eslint/eslint-plugin": { - "version": "4.19.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.19.0.tgz", - "integrity": "sha512-CRQNQ0mC2Pa7VLwKFbrGVTArfdVDdefS+gTw0oC98vSI98IX5A8EVH4BzJ2FOB0YlCmm8Im36Elad/Jgtvveaw==", + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.28.2.tgz", + "integrity": "sha512-PGqpLLzHSxq956rzNGasO3GsAPf2lY9lDUBXhS++SKonglUmJypaUtcKzRtUte8CV7nruwnDxtLUKpVxs0wQBw==", "dev": true, "requires": { - "@typescript-eslint/experimental-utils": "4.19.0", - "@typescript-eslint/scope-manager": "4.19.0", - "debug": "^4.1.1", + "@typescript-eslint/experimental-utils": "4.28.2", + "@typescript-eslint/scope-manager": "4.28.2", + "debug": "^4.3.1", "functional-red-black-tree": "^1.0.1", - "lodash": "^4.17.15", - "regexpp": "^3.0.0", - "semver": "^7.3.2", - "tsutils": "^3.17.1" + "regexpp": "^3.1.0", + "semver": "^7.3.5", + "tsutils": "^3.21.0" }, "dependencies": { "semver": { @@ -15676,60 +15692,71 @@ } }, "@typescript-eslint/experimental-utils": { - "version": "4.19.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.19.0.tgz", - "integrity": "sha512-9/23F1nnyzbHKuoTqFN1iXwN3bvOm/PRIXSBR3qFAYotK/0LveEOHr5JT1WZSzcD6BESl8kPOG3OoDRKO84bHA==", + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.28.2.tgz", + "integrity": "sha512-MwHPsL6qo98RC55IoWWP8/opTykjTp4JzfPu1VfO2Z0MshNP0UZ1GEV5rYSSnZSUI8VD7iHvtIPVGW5Nfh7klQ==", "dev": true, "requires": { - "@types/json-schema": "^7.0.3", - "@typescript-eslint/scope-manager": "4.19.0", - "@typescript-eslint/types": "4.19.0", - "@typescript-eslint/typescript-estree": "4.19.0", - "eslint-scope": "^5.0.0", - "eslint-utils": "^2.0.0" + "@types/json-schema": "^7.0.7", + "@typescript-eslint/scope-manager": "4.28.2", + "@typescript-eslint/types": "4.28.2", + "@typescript-eslint/typescript-estree": "4.28.2", + "eslint-scope": "^5.1.1", + "eslint-utils": "^3.0.0" + }, + "dependencies": { + "eslint-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", + "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^2.0.0" + } + } } }, "@typescript-eslint/parser": { - "version": "4.19.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.19.0.tgz", - "integrity": "sha512-/uabZjo2ZZhm66rdAu21HA8nQebl3lAIDcybUoOxoI7VbZBYavLIwtOOmykKCJy+Xq6Vw6ugkiwn8Js7D6wieA==", + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.28.2.tgz", + "integrity": "sha512-Q0gSCN51eikAgFGY+gnd5p9bhhCUAl0ERMiDKrTzpSoMYRubdB8MJrTTR/BBii8z+iFwz8oihxd0RAdP4l8w8w==", "dev": true, "requires": { - "@typescript-eslint/scope-manager": "4.19.0", - "@typescript-eslint/types": "4.19.0", - "@typescript-eslint/typescript-estree": "4.19.0", - "debug": "^4.1.1" + "@typescript-eslint/scope-manager": "4.28.2", + "@typescript-eslint/types": "4.28.2", + "@typescript-eslint/typescript-estree": "4.28.2", + "debug": "^4.3.1" } }, "@typescript-eslint/scope-manager": { - "version": "4.19.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.19.0.tgz", - "integrity": "sha512-GGy4Ba/hLXwJXygkXqMzduqOMc+Na6LrJTZXJWVhRrSuZeXmu8TAnniQVKgj8uTRKe4igO2ysYzH+Np879G75g==", + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.28.2.tgz", + "integrity": "sha512-MqbypNjIkJFEFuOwPWNDjq0nqXAKZvDNNs9yNseoGBB1wYfz1G0WHC2AVOy4XD7di3KCcW3+nhZyN6zruqmp2A==", "dev": true, "requires": { - "@typescript-eslint/types": "4.19.0", - "@typescript-eslint/visitor-keys": "4.19.0" + "@typescript-eslint/types": "4.28.2", + "@typescript-eslint/visitor-keys": "4.28.2" } }, "@typescript-eslint/types": { - "version": "4.19.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.19.0.tgz", - "integrity": "sha512-A4iAlexVvd4IBsSTNxdvdepW0D4uR/fwxDrKUa+iEY9UWvGREu2ZyB8ylTENM1SH8F7bVC9ac9+si3LWNxcBuA==", + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.28.2.tgz", + "integrity": "sha512-Gr15fuQVd93uD9zzxbApz3wf7ua3yk4ZujABZlZhaxxKY8ojo448u7XTm/+ETpy0V0dlMtj6t4VdDvdc0JmUhA==", "dev": true }, "@typescript-eslint/typescript-estree": { - "version": "4.19.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.19.0.tgz", - "integrity": "sha512-3xqArJ/A62smaQYRv2ZFyTA+XxGGWmlDYrsfZG68zJeNbeqRScnhf81rUVa6QG4UgzHnXw5VnMT5cg75dQGDkA==", + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.28.2.tgz", + "integrity": "sha512-86lLstLvK6QjNZjMoYUBMMsULFw0hPHJlk1fzhAVoNjDBuPVxiwvGuPQq3fsBMCxuDJwmX87tM/AXoadhHRljg==", "dev": true, "requires": { - "@typescript-eslint/types": "4.19.0", - "@typescript-eslint/visitor-keys": "4.19.0", - "debug": "^4.1.1", - "globby": "^11.0.1", + "@typescript-eslint/types": "4.28.2", + "@typescript-eslint/visitor-keys": "4.28.2", + "debug": "^4.3.1", + "globby": "^11.0.3", "is-glob": "^4.0.1", - "semver": "^7.3.2", - "tsutils": "^3.17.1" + "semver": "^7.3.5", + "tsutils": "^3.21.0" }, "dependencies": { "semver": { @@ -15744,12 +15771,12 @@ } }, "@typescript-eslint/visitor-keys": { - "version": "4.19.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.19.0.tgz", - "integrity": "sha512-aGPS6kz//j7XLSlgpzU2SeTqHPsmRYxFztj2vPuMMFJXZudpRSehE3WCV+BaxwZFvfAqMoSd86TEuM0PQ59E/A==", + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.28.2.tgz", + "integrity": "sha512-aT2B4PLyyRDUVUafXzpZFoc0C9t0za4BJAKP5sgWIhG+jHECQZUEjuQSCIwZdiJJ4w4cgu5r3Kh20SOdtEBl0w==", "dev": true, "requires": { - "@typescript-eslint/types": "4.19.0", + "@typescript-eslint/types": "4.28.2", "eslint-visitor-keys": "^2.0.0" } }, diff --git a/package.json b/package.json index 856c1ca..ca2e729 100644 --- a/package.json +++ b/package.json @@ -31,8 +31,8 @@ "@types/jest": "^26.0.14", "@types/jest-image-snapshot": "^4.3.0", "@types/micromodal": "^0.3.2", - "@typescript-eslint/eslint-plugin": "^4.6.1", - "@typescript-eslint/parser": "^4.6.1", + "@typescript-eslint/eslint-plugin": "^4.28.2", + "@typescript-eslint/parser": "^4.28.2", "ajv": "^6.12.0", "babel-jest": "^26.6.3", "canvas": "^2.7.0", diff --git a/test/js/formats/d13.spec.ts b/test/js/formats/d13.spec.ts index b430259..7d3334d 100644 --- a/test/js/formats/d13.spec.ts +++ b/test/js/formats/d13.spec.ts @@ -1,6 +1,6 @@ -import DOS13 from '../../../js/formats/d13'; -import { D13O } from '../../../js/formats/format_utils'; -import { memory } from '../../../js/types'; +import DOS13 from 'js/formats/d13'; +import { D13O } from 'js/formats/format_utils'; +import { memory } from 'js/types'; import { BYTES_BY_SECTOR, BYTES_BY_TRACK } from './testdata/13sector'; import { expectSequence, findBytes, skipGap } from './util'; diff --git a/test/js/formats/do.spec.ts b/test/js/formats/do.spec.ts index fe1f94e..94dc20c 100644 --- a/test/js/formats/do.spec.ts +++ b/test/js/formats/do.spec.ts @@ -1,5 +1,5 @@ -import DOS from '../../../js/formats/do'; -import { memory } from '../../../js/types'; +import DOS from 'js/formats/do'; +import { memory } from 'js/types'; import { BYTES_BY_SECTOR, BYTES_BY_TRACK } from './testdata/16sector'; import { expectSequence, findBytes, skipGap } from './util'; diff --git a/test/js/formats/po.spec.ts b/test/js/formats/po.spec.ts index ad18438..16aa02a 100644 --- a/test/js/formats/po.spec.ts +++ b/test/js/formats/po.spec.ts @@ -1,5 +1,5 @@ -import ProDOS from '../../../js/formats/po'; -import { memory } from '../../../js/types'; +import ProDOS from 'js/formats/po'; +import { memory } from 'js/types'; import { BYTES_BY_SECTOR, BYTES_BY_TRACK } from './testdata/16sector'; import { expectSequence, findBytes, skipGap } from './util'; diff --git a/test/js/formats/testdata/13sector.ts b/test/js/formats/testdata/13sector.ts index e77376a..6babb10 100644 --- a/test/js/formats/testdata/13sector.ts +++ b/test/js/formats/testdata/13sector.ts @@ -1,4 +1,4 @@ -import { memory } from '../../../../js/types'; +import { memory } from 'js/types'; function generateBytesInOrder() { const data: memory[][] = []; diff --git a/test/js/formats/testdata/16sector.ts b/test/js/formats/testdata/16sector.ts index defc009..f28d61b 100644 --- a/test/js/formats/testdata/16sector.ts +++ b/test/js/formats/testdata/16sector.ts @@ -1,4 +1,4 @@ -import { memory } from '../../../../js/types'; +import { memory } from 'js/types'; function generateBytesInOrder() { const data: memory[][] = []; diff --git a/test/js/formats/testdata/woz.ts b/test/js/formats/testdata/woz.ts new file mode 100644 index 0000000..f61d360 --- /dev/null +++ b/test/js/formats/testdata/woz.ts @@ -0,0 +1,169 @@ +import { + numberToBytes, + stringToBytes, +} from '../util'; + +/** + * Version 1 INFO segment + */ + +const mockInfo1 = [ + 0x01, // Version + 0x01, // Disk Type (5.25") + 0x00, // Write protected + 0x01, // Synchronized + 0x00, // Cleaned + ...stringToBytes('Apple2JS', ' ', 32), + 0x00, 0x00, 0x00, 0x00, // 23 Unused + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, +]; + +/** + * Version 2 INFO segment + */ + +const mockInfo2 = [ + 0x02, // Version + 0x01, // Disk Type (5.25") + 0x00, // Write protected + 0x01, // Synchronized + 0x00, // Cleaned + ...stringToBytes('Apple2JS', ' ', 32), + 0x01, // sides + 0x00, // bootSector + 0x00, // bitTiming + 0x00, 0x00, // compatibleHardware + 0x00, 0x00, // requiredRAM + 0x00, 0x00, // largest track + 0x00, 0x00, 0x00, 0x00, // 14 unused + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, +]; + +/** + * Track map all pointing to track 0 + */ + +export const mockTMAP = new Array(160); +mockTMAP.fill(0); + +/** + * One very small track + */ + +// 24 bits of track data, padded + +const mockTrackData = new Array(6646); +mockTrackData.fill(0); +mockTrackData[0] = 0xd5; +mockTrackData[1] = 0xaa; +mockTrackData[2] = 0x96; + +/** + * Version 1 TRKS structure + */ + +const mockTRKS = [ + ...mockTrackData, + ...numberToBytes(3, 2), // Number of bytes + ...numberToBytes(24, 2), // Number of bits + ...numberToBytes(0xffff, 2), // Splice point + 0, // Splice nibble + 0, // Splice bit count + ...numberToBytes(0, 2), // Reserved +]; + +/** + * Version 2 TRKS structure + */ + +const mockTrackMap = new Array(160 * 8); +mockTrackMap.fill(0); +mockTrackMap[0x00] = 0x03; +mockTrackMap[0x01] = 0x00; +mockTrackMap[0x02] = 0x01; +mockTrackMap[0x03] = 0x00; +mockTrackMap[0x04] = 0x18; +mockTrackMap[0x07] = 0x00; +mockTrackMap[0x08] = 0x00; +mockTrackMap[0x09] = 0x00; + +const mockTrackData2 = new Array(512); +mockTrackData2.fill(0); +mockTrackData2[0] = 0xd5; +mockTrackData2[1] = 0xaa; +mockTrackData2[2] = 0x96; + +const mockTRKS2 = [ + ...mockTrackMap, + ...mockTrackData2, +]; + +/** + * META structures + */ + +const mockMETA1 = 'title\tMock Woz 1'; +const mockMETA2 = 'title\tMock Woz 2'; + +/** + * Woz Version 1 + */ + +export const mockWoz1: ArrayBuffer = new Uint8Array([ + // Header + ...stringToBytes('WOZ1'), + 0xff, // 7 bit detection + 0x0a, 0x0d, 0x0a, // LF detection + 0x00, 0x00, 0x00, 0x00, // CRC + // Info chunk + ...stringToBytes('INFO'), + ...numberToBytes(60, 4), // Size + ...mockInfo1, + // TMAP chunk + ...stringToBytes('TMAP'), + ...numberToBytes(mockTMAP.length, 4), // Size + ...mockTMAP, + // TRKS chunk + ...stringToBytes('TRKS'), + ...numberToBytes(mockTRKS.length, 4), // Size + ...mockTRKS, + // META chunk + ...stringToBytes('META'), + ...numberToBytes(mockMETA1.length, 4), // Size + ...stringToBytes(mockMETA1), +]).buffer; + +/** + * Woz Version 2 + */ + +export const mockWoz2: ArrayBuffer = new Uint8Array([ + // Header + ...stringToBytes('WOZ2'), + 0xff, // 7 bit detection + 0x0a, 0x0d, 0x0a, // LF detection + 0x00, 0x00, 0x00, 0x00, // CRC + + // Info chunk + ...stringToBytes('INFO'), + ...numberToBytes(mockInfo2.length, 4), // Size + ...mockInfo2, + // TMAP chunk + ...stringToBytes('TMAP'), + ...numberToBytes(mockTMAP.length, 4), // Size + ...mockTMAP, + // TRKS chunk + ...stringToBytes('TRKS'), + ...numberToBytes(mockTRKS2.length, 4), // Size + ...mockTRKS2, + // META chunk + ...stringToBytes('META'), + ...numberToBytes(mockMETA2.length, 4), // Size + ...stringToBytes(mockMETA2), +]).buffer; diff --git a/test/js/formats/util.ts b/test/js/formats/util.ts index 8eef292..b1ced14 100644 --- a/test/js/formats/util.ts +++ b/test/js/formats/util.ts @@ -1,4 +1,4 @@ -import { memory } from '../../../js/types'; +import { memory } from 'js/types'; export function skipGap(track: memory, start: number = 0): number { const end = start + 0x100; // no gap is this big @@ -40,3 +40,43 @@ export function findBytes(track: memory, bytes: number[], start: number = 0): nu } return -1; } + +/** + * Convert an ASCII character string into an array of bytes, with optional + * padding. + * + * @param val ASCII character string + * @param pad ASCII character to fill reach padded length + * @param padLength padded length + * @returns an array of bytes + */ +export const stringToBytes = (val: string, pad: string = '\0', padLength: number = 0) => { + const result = []; + let idx = 0; + while (idx < val.length) { + result.push(val.charCodeAt(idx) & 0x7f); + idx++; + } + while (idx++ < padLength) { + result.push(pad.charCodeAt(0) & 0x7f); + } + return result; +}; + +/** + * Convert a number into a little endian array of bytes. + * + * @param val a number + * @param count number of bytes in result + * @returns an array of bytes + */ +export const numberToBytes = (val: number, count: number) => { + const result = []; + let idx = 0; + while (idx < count) { + result.push(val & 0xff); + val >>= 8; + idx++; + } + return result; +}; diff --git a/test/js/formats/woz.spec.ts b/test/js/formats/woz.spec.ts new file mode 100644 index 0000000..c9fa4f7 --- /dev/null +++ b/test/js/formats/woz.spec.ts @@ -0,0 +1,91 @@ +import { WozDisk, ENCODING_BITSTREAM } from 'js/formats/types'; +import createDiskFromWoz from 'js/formats/woz'; +import { + mockWoz1, + mockWoz2, + mockTMAP +} from './testdata/woz'; + +describe('woz', () => { + beforeEach(() => { + spyOn(console, 'log'); + }); + + it('can parse Woz version 1', () => { + const options = { + name: 'Unknown', + volume: 254, + readOnly: true, + rawData: mockWoz1 + }; + + const disk = createDiskFromWoz(options) as WozDisk; + expect(disk).toEqual({ + name: 'Mock Woz 1', + readOnly: true, + encoding: ENCODING_BITSTREAM, + trackMap: mockTMAP, + rawTracks: [new Uint8Array([ + 1, 1, 0, 1, 0, 1, 0, 1, + 1, 0, 1, 0, 1, 0, 1, 0, + 1, 0, 0, 1, 0, 1, 1, 0, + ])], + tracks: [new Uint8Array([0xD5, 0xAA, 0x96])], + }); + expect(console.log).toHaveBeenCalledWith(expect.objectContaining({ + info: { + bitTiming: 0, + bootSector: 0, + cleaned: 0, + compatibleHardware: 0, + creator: 'Apple2JS ', + diskType: 1, + largestTrack: 0, + requiredRAM: 0, + sides: 0, + synchronized: 1, + version: 1, + writeProtected: 0 + } + })); + }); + + it('can parse Woz version 2', () => { + const options = { + name: 'Unknown', + volume: 254, + readOnly: true, + rawData: mockWoz2 + }; + + const disk = createDiskFromWoz(options) as WozDisk; + expect(disk).toEqual({ + name: 'Mock Woz 2', + readOnly: true, + encoding: ENCODING_BITSTREAM, + trackMap: mockTMAP, + rawTracks: [new Uint8Array([ + 1, 1, 0, 1, 0, 1, 0, 1, + 1, 0, 1, 0, 1, 0, 1, 0, + 1, 0, 0, 1, 0, 1, 1, 0, + ])], + tracks: [new Uint8Array([0xD5, 0xAA, 0x96])], + }); + expect(console.log).toHaveBeenCalledWith(expect.objectContaining({ + info: { + bitTiming: 0, + bootSector: 0, + cleaned: 0, + compatibleHardware: 0, + creator: 'Apple2JS ', + diskType: 1, + largestTrack: 0, + requiredRAM: 0, + sides: 1, + synchronized: 1, + version: 2, + writeProtected: 0 + } + })); + }); +});