2022-06-22 03:34:19 +00:00
|
|
|
import {
|
|
|
|
create2MGFragments,
|
|
|
|
create2MGFromBlockDisk,
|
|
|
|
FORMAT,
|
|
|
|
HeaderData,
|
|
|
|
read2MGHeader
|
|
|
|
} from 'js/formats/2mg';
|
|
|
|
import { BlockDisk, ENCODING_BLOCK } from 'js/formats/types';
|
2022-06-07 01:10:06 +00:00
|
|
|
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();
|
2022-06-22 03:34:19 +00:00
|
|
|
expect(header.volume).toBe(0);
|
2022-06-07 01:10:06 +00:00
|
|
|
expect(header.comment).toBeUndefined();
|
|
|
|
expect(header.creatorData).toBeUndefined();
|
|
|
|
});
|
|
|
|
});
|
2022-06-22 03:34:19 +00:00
|
|
|
|
|
|
|
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);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|