apple2js/js/formats/2mg.ts

190 lines
6.5 KiB
TypeScript
Raw Normal View History

import DOS from './do';
import Nibble from './nib';
import ProDOS from './po';
import { DiskOptions } from './types';
import { numToString } from '../util';
import { ReadonlyUint8Array } from 'js/types';
/**
* Offsets in bytes to the various header fields. All number fields are
* in little-endian order (least significant byte first). These values
* come from the spec at:
*
* https://apple2.org.za/gswv/a2zine/Docs/DiskImage_2MG_Info.txt
*/
const OFFSETS = {
/** File signature ('2IMG', 4 bytes) */
SIGNATURE: 0x00,
/** Creator ID (4 bytes) */
CREATOR: 0x04,
/** Header length (2 bytes) */
HEADER_LENGTH: 0x08,
/** Version number (2 bytes). (Version of what? Format? Image?). */
VERSION: 0x0A,
/** Image format ID (4 bytes) */
FORMAT: 0x0C,
/** Flags and DOS 3.3 volume number */
FLAGS: 0x10,
/**
* Number of ProDOS blocks (4 bytes). ProDOS blocks are 512 bytes each.
* This field must be zero if the image format is not 0x01 (ProDOS).
* (ASIMOV2 always fills in this field.)
*/
BLOCKS: 0x14,
/**
* Disk data start in bytes from the beginning of the image file
* (4 bytes).
*/
DATA_OFFSET: 0x18,
/**
* Length of disk data in bytes (4 bytes). (143,360 bytes for 5.25"
* floppies; 512 × blocks for ProDOS volumes.)
*/
DATA_LENGTH: 0x1C,
/**
* Comment start in bytes from the beginning of the image file (4 bytes).
* Must be zero if there is no comment. The comment must come after the
* disk data and before the creator data. The comment should be "raw text"
* with no terminating null. By "raw text", we assume UTF-8.
*/
COMMENT: 0x20,
/**
* Comment length in bytes (4 bytes). Must be zero if there is no comment.
*/
COMMENT_LENGTH: 0x24,
/**
* Optional creator data start in bytes from the beginning of the image
* file (4 bytes). Must be zero if there is no creator data.
*/
CREATOR_DATA: 0x28,
/**
* Creator data length in bytes (4 bytes). Must be zero if there is no
* creator data.
*/
CREATOR_DATA_LENGTH: 0x2C,
/** Padding (16 bytes). Must be zero. */
PADDING: 0x30,
} as const;
const FLAGS = {
READ_ONLY: 0x80000000,
VOLUME_VALID: 0x00000100,
VOLUME_MASK: 0x000000FF
} as const;
enum FORMAT {
DOS = 0,
ProDOS = 1,
NIB = 2,
}
export function read2MGHeader(rawData: ArrayBuffer) {
const prefix = new DataView(rawData);
const signature = numToString(prefix.getInt32(OFFSETS.SIGNATURE, true));
if (signature !== '2IMG') {
throw new Error(`Unrecognized 2mg signature: ${signature}`);
}
const headerLength = prefix.getInt16(OFFSETS.HEADER_LENGTH, true);
if (headerLength !== 64) {
throw new Error(`2mg header length is incorrect ${headerLength} !== 63`);
}
const creator = numToString(prefix.getInt32(OFFSETS.CREATOR, true));
const format = prefix.getInt32(OFFSETS.FORMAT, true);
const flags = prefix.getInt32(OFFSETS.FLAGS, true);
const blocks = prefix.getInt32(OFFSETS.BLOCKS, true);
const offset = prefix.getInt32(OFFSETS.DATA_OFFSET, true);
const bytes = prefix.getInt32(OFFSETS.DATA_LENGTH, true);
const commentOffset = prefix.getInt32(OFFSETS.COMMENT, true);
const commentLength = prefix.getInt32(OFFSETS.COMMENT_LENGTH, true);
const creatorDataOffset = prefix.getInt32(OFFSETS.CREATOR_DATA, true);
const creatorDataLength = prefix.getInt32(OFFSETS.CREATOR_DATA_LENGTH, true);
// Though the spec says that it should be zero if the format is not
// ProDOS, we don't check that since we know that it is violated.
// However we do check that it's correct if the image _is_ ProDOS.
if (format === FORMAT.ProDOS && blocks * 512 !== bytes) {
throw new Error(`2mg blocks does not match disk data length: ${blocks} * 512 !== ${bytes}`);
}
if (offset < headerLength) {
throw new Error(`2mg data offset is less than header length: ${offset} < ${headerLength}`);
}
if (offset + bytes > prefix.byteLength) {
throw new Error(`2mg data extends beyond disk image: ${offset} + ${bytes} > ${prefix.byteLength}`);
}
const dataEnd = offset + bytes;
if (commentOffset && commentOffset < dataEnd) {
throw new Error(`2mg comment is before the end of the disk data: ${commentOffset} < ${offset} + ${bytes}`);
}
const commentEnd = commentOffset ? commentOffset + commentLength : dataEnd;
if (commentEnd > prefix.byteLength) {
throw new Error(`2mg comment extends beyond disk image: ${commentEnd} > ${prefix.byteLength}`);
}
if (creatorDataOffset && creatorDataOffset < commentEnd) {
throw new Error(`2mg creator data is before the end of the comment: ${creatorDataOffset} < ${commentEnd}`);
}
const creatorDataEnd = creatorDataOffset ? creatorDataOffset + creatorDataLength : commentEnd;
if (creatorDataEnd > prefix.byteLength) {
throw new Error(`2mg creator data extends beyond disk image: ${creatorDataEnd} > ${prefix.byteLength}`);
}
const extras: { comment?: string; creatorData?: ReadonlyUint8Array } = {};
if (commentOffset) {
extras.comment = new TextDecoder('utf-8').decode(
new Uint8Array(rawData, commentOffset, commentLength));
}
if (creatorDataOffset) {
extras.creatorData = new Uint8Array(rawData, creatorDataOffset, creatorDataLength);
}
const readOnly = (flags & FLAGS.READ_ONLY) !== 0;
let volume = 254;
if (flags & FLAGS.VOLUME_VALID) {
volume = flags & FLAGS.VOLUME_MASK;
}
return {
bytes,
creator,
format,
offset,
readOnly,
volume,
...extras
};
}
/**
* Returns a `Disk` object from a 2mg image.
* @param options the disk image and options
*/
export default function createDiskFrom2MG(options: DiskOptions) {
let { rawData } = options;
let disk;
if (!rawData) {
throw new Error('Requires rawData');
}
const { bytes, format, offset, readOnly, volume } = read2MGHeader(rawData);
rawData = rawData.slice(offset, offset + bytes);
options = { ...options, rawData, readOnly, volume };
// Check image format.
// Sure, it's really 64 bits. But only 2 are actually used.
switch (format) {
case FORMAT.ProDOS: // PO
disk = ProDOS(options);
break;
case FORMAT.NIB: // NIB
disk = Nibble(options);
break;
case FORMAT.DOS: // dsk
default: // Something hinky, assume 'dsk'
disk = DOS(options);
break;
}
return disk;
}