/* 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'; var WOZ_HEADER_START = 0; var WOZ_HEADER_SIZE = 12; var WOZ1_SIGNATURE = 0x315A4F57; var WOZ2_SIGNATURE = 0x325A4F57; var 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) { var nibble = 0; var waitForOne = true; while (offset < bits.length) { var 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 (var idx = 0; idx < 160; idx++) { this.trackMap.push(data.getUint8(idx)); } return this; } function TrksChunk(data) { var WOZ_TRACK_SIZE = 6656; var WOZ_TRACK_INFO_BITS = 6648; this.rawTracks = []; this.tracks = []; for (var trackNo = 0, idx = 0; idx < data.byteLength; idx += WOZ_TRACK_SIZE, trackNo++) { var jdx; var track = []; var rawTrack = []; var slice = data.buffer.slice(data.byteOffset + idx, data.byteOffset + idx + WOZ_TRACK_SIZE); var trackData = new Uint8Array(slice); var trackInfo = new DataView(slice); var trackBitCount = trackInfo.getUint16(WOZ_TRACK_INFO_BITS, true); for (jdx = 0; jdx < trackBitCount; jdx++) { var byteIndex = jdx >> 3; var bitIndex = 7 - (jdx & 0x07); rawTrack[jdx] = (trackData[byteIndex] >> bitIndex) & 0x1; } track = []; var offset = 0; while (offset < rawTrack.length) { var 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) { var trackNo; this.trks = []; for (trackNo = 0; trackNo < 160; trackNo++) { var startBlock = data.getUint16(trackNo * 8, true); var blockCount = data.getUint16(trackNo * 8 + 2, true); var bitCount = data.getUint32(trackNo * 8 + 4, true); if (bitCount === 0) { break; } this.trks.push({ startBlock: startBlock, blockCount: blockCount, bitCount: bitCount }); } this.tracks = []; this.rawTracks = []; var bits = data.buffer; for (trackNo = 0; trackNo < this.trks.length; trackNo++) { var trk = this.trks[trackNo]; var track = []; var rawTrack = []; var start = trk.startBlock * 512; var end = start + trk.blockCount * 512; var slice = bits.slice(start, end); var trackData = new Uint8Array(slice); for (var jdx = 0; jdx < trk.bitCount; jdx++) { var byteIndex = jdx >> 3; var bitIndex = 7 - (jdx & 0x07); rawTrack[jdx] = (trackData[byteIndex] >> bitIndex) & 0x1; } track = []; var offset = 0; while (offset < rawTrack.length) { var 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 MetaChunk(data) { var infoStr = stringFromBytes(data, 0, data.byteLength); var parts = infoStr.split('\n'); var info = parts.reduce(function(acc, part) { var subParts = part.split('\t'); acc[subParts[0]] = subParts[1]; return acc; }, {}); Object.assign(this, info); return this; } export default function Woz(options) { var { rawData } = options; var dv = new DataView(rawData, 0); var dvOffset = 0; var disk = { format: 'woz' }; var wozVersion; var chunks = {}; function readHeader() { var 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; } var type = dv.getUint32(dvOffset, true); var size = dv.getUint32(dvOffset + 4, true); var data = new DataView(dv.buffer, dvOffset + 8, size); dvOffset += size + 8; return { type: type, size: size, data: data }; } if (readHeader()) { dvOffset = WOZ_HEADER_SIZE; var 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.info.title; return disk; }