apple2js/test/js/formats/2mg.spec.ts

202 lines
7.0 KiB
TypeScript

import {
create2MGFragments,
create2MGFromBlockDisk,
FORMAT,
HeaderData,
read2MGHeader
} from 'js/formats/2mg';
import { BlockDisk, ENCODING_BLOCK } from 'js/formats/types';
import { concat } from 'js/util';
import { BYTES_BY_SECTOR_IMAGE } from './testdata/16sector';
const INVALID_SIGNATURE_IMAGE = new Uint8Array([
0x11, 0x22, 0x33, 0x44
]);
const INVALID_HEADER_LENGTH_IMAGE = new Uint8Array([
// ID
0x32, 0x49, 0x4d, 0x47,
// Creator ID
0x58, 0x47, 0x53, 0x21,
// Header size
0x0a, 0x00
]);
const VALID_PRODOS_IMAGE = concat(new Uint8Array([
// ID
0x32, 0x49, 0x4d, 0x47,
// Creator ID
0x58, 0x47, 0x53, 0x21,
// Header size
0x40, 0x00,
// Version number
0x01, 0x00,
// Image format (ProDOS)
0x01, 0x00, 0x00, 0x00,
// Flags
0x00, 0x00, 0x00, 0x00,
// ProDOS blocks
0x18, 0x01, 0x00, 0x00,
// Data offset
0x40, 0x00, 0x00, 0x00,
// Data length (in bytes)
0x00, 0x30, 0x02, 0x00,
// Comment offset
0x00, 0x00, 0x00, 0x00,
// Comment length
0x00, 0x00, 0x00, 0x00,
// Creator data offset
0x00, 0x00, 0x00, 0x00,
// Creator data length
0x00, 0x00, 0x00, 0x00,
// Padding
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
]), BYTES_BY_SECTOR_IMAGE);
describe('2mg format', () => {
describe('read2MGHeader', () => {
it('throws if the signature is invalid', () => {
expect(() => read2MGHeader(INVALID_SIGNATURE_IMAGE.buffer)).toThrow(/signature/);
});
it('throws if the header length is invalid', () => {
expect(() => read2MGHeader(INVALID_HEADER_LENGTH_IMAGE.buffer)).toThrowError(/header length/);
});
it('throws if block count is not correct for ProDOS image', () => {
const image = new Uint8Array(VALID_PRODOS_IMAGE);
image[0x14] = image[0x14] + 1;
expect(() => read2MGHeader(image.buffer)).toThrowError(/blocks/);
});
it('throws if comment comes before end of disk data', () => {
const image = new Uint8Array(VALID_PRODOS_IMAGE);
image[0x20] = 1;
expect(() => read2MGHeader(image.buffer)).toThrowError(/is before/);
});
it('throws if creator data comes before end of disk data', () => {
const image = new Uint8Array(VALID_PRODOS_IMAGE);
image[0x28] = 1;
expect(() => read2MGHeader(image.buffer)).toThrowError(/is before/);
});
it('throws if data length is too big for file', () => {
const image = new Uint8Array(VALID_PRODOS_IMAGE);
image[0x1D] += 2; // Increment byte length by 512
image[0x14] += 1; // Increment block length by 1
expect(() => read2MGHeader(image.buffer)).toThrowError(/extends beyond/);
});
it('returns a header for a valid ProDOS image', () => {
expect(read2MGHeader(VALID_PRODOS_IMAGE.buffer)).not.toBeNull();
});
it('returns a filled-in header for a valid ProDOS image', () => {
const header = read2MGHeader(VALID_PRODOS_IMAGE.buffer);
expect(header.creator).toBe('XGS!');
expect(header.bytes).toBe(143_360);
expect(header.offset).toBe(64);
expect(header.format).toBe(1);
expect(header.readOnly).toBeFalsy();
expect(header.volume).toBe(0);
expect(header.comment).toBeUndefined();
expect(header.creatorData).toBeUndefined();
});
});
describe('create2MGFragments', () => {
it('creates a valid image from header data and blocks', () => {
const header = read2MGHeader(VALID_PRODOS_IMAGE.buffer);
const { prefix, suffix } = create2MGFragments(header, { blocks: header.bytes / 512 });
expect(prefix).toEqual(VALID_PRODOS_IMAGE.slice(0, 64));
expect(suffix).toEqual(new Uint8Array());
});
it('throws an error if block count does not match byte count', () => {
const headerData: HeaderData = {
creator: 'A2JS',
bytes: 32768,
format: FORMAT.ProDOS,
readOnly: false,
offset: 64,
volume: 0,
};
expect(() => create2MGFragments(headerData, { blocks: 63 })).toThrowError(/does not match/);
});
it('throws an error if not a ProDOS volume', () => {
const headerData: HeaderData = {
creator: 'A2JS',
bytes: 143_360,
format: FORMAT.DOS,
readOnly: false,
offset: 64,
volume: 254,
};
expect(() => create2MGFragments(headerData, { blocks: 280 })).toThrowError(/not supported/);
});
it('uses defaults', () => {
const { prefix, suffix } = create2MGFragments(null, { blocks: 280 });
const image = concat(prefix, BYTES_BY_SECTOR_IMAGE, suffix);
const headerData = read2MGHeader(image.buffer);
expect(headerData).toEqual({
creator: 'A2JS',
bytes: 143_360,
format: FORMAT.ProDOS,
readOnly: false,
offset: 64,
volume: 0,
});
});
it.each([
['Hello, sailor', undefined],
['Hieyz wizka', new Uint8Array([4, 3, 2, 1])],
[undefined, new Uint8Array([4, 3, 2, 1])]
])('can create comment %p and creator data %p', (testComment, testData) => {
const headerData: HeaderData = {
creator: 'A2JS',
bytes: 0,
format: FORMAT.ProDOS,
readOnly: false,
offset: 64,
volume: 254,
};
if (testComment) {
headerData.comment = testComment;
}
if (testData) {
headerData.creatorData = testData;
}
const { prefix, suffix } = create2MGFragments(headerData, { blocks: 0 });
const image = concat(prefix, suffix);
const { comment, creatorData } = read2MGHeader(image.buffer);
expect(comment).toEqual(testComment);
expect(creatorData).toEqual(testData);
});
});
describe('create2MGFromBlockDisk', () => {
it('can create a 2mg disk', () => {
const header = read2MGHeader(VALID_PRODOS_IMAGE.buffer);
const blocks = [];
for (let idx = 0; idx < BYTES_BY_SECTOR_IMAGE.length; idx += 512) {
blocks.push(BYTES_BY_SECTOR_IMAGE.slice(idx, idx + 512));
}
const disk: BlockDisk = {
blocks,
name: 'Good disk',
readOnly: false,
encoding: ENCODING_BLOCK,
};
const image = create2MGFromBlockDisk(header, disk);
expect(VALID_PRODOS_IMAGE.buffer).toEqual(image);
});
});
});