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 { const byteArray = new Uint8Array( data.buffer.slice(data.byteOffset + start, data.byteOffset + end) ); return String.fromCharCode(...byteArray); } 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); if (trackNo === 0) { // debug(`First bytes: ${toHex(trackData[0])} ${toHex(trackData[1])} ${toHex(trackData[2])} ${toHex(trackData[3])}`); } 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]: unknown; info?: InfoChunk; tmap?: TMapChunk; trks?: TrksChunk; meta?: MetaChunk; } /** * Returns a `Disk` object from Woz image data. * @param options the disk image and options * @returns A bitstream disk */ export default function createDiskFromWoz(options: DiskOptions): WozDisk { const { rawData } = options; if (!rawData) { throw new Error('Requires rawData'); } const dv = new DataView(rawData, 0); let dvOffset = 0; 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); const { meta, tmap, trks, info } = chunks; const disk: WozDisk = { encoding: ENCODING_BITSTREAM, format: 'woz', trackMap: tmap?.trackMap || [], rawTracks: trks?.rawTracks || [], readOnly: true, //chunks.info.writeProtected === 1; metadata: { name: meta?.values['title'] || options.name, side: meta?.values['side_name'] || meta?.values['side'] }, info }; return disk; }