mirror of
https://github.com/whscullin/apple2js.git
synced 2024-01-12 14:14:38 +00:00
ProDOS image format tests and fixes (#51)
Like the DOS 3.3 sector order issues in #49, this change fixes the order of the physical sectors on the ProDOS disk when nibblized. This change also adds tests similar to the DOS 3.3 tests to verify the sector order. Because the DOS 3.3 and ProDOS tests are so similar, the utility methods have been refactored into their own file.
This commit is contained in:
parent
565da09575
commit
715ea6ffaa
@ -29,18 +29,33 @@ export type Drive = {
|
||||
dirty: false
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* DOS 3.3 Physical sector order (index is physical sector, value is DOS sector).
|
||||
*/
|
||||
export const DO = [
|
||||
0x0, 0x7, 0xE, 0x6, 0xD, 0x5, 0xC, 0x4,
|
||||
0xB, 0x3, 0xA, 0x2, 0x9, 0x1, 0x8, 0xF];
|
||||
0xB, 0x3, 0xA, 0x2, 0x9, 0x1, 0x8, 0xF
|
||||
];
|
||||
|
||||
/**
|
||||
* DOS 3.3 Logical sector order (index is DOS sector, value is physical sector).
|
||||
*/
|
||||
export const _DO = [
|
||||
0x0, 0xD, 0xB, 0x9, 0x7, 0x5, 0x3, 0x1,
|
||||
0xE, 0xC, 0xA, 0x8, 0x6, 0x4, 0x2, 0xF
|
||||
];
|
||||
|
||||
// var PO = [0x0,0x8,0x1,0x9,0x2,0xa,0x3,0xb,
|
||||
// 0x4,0xc,0x5,0xd,0x6,0xe,0x7,0xf];
|
||||
/**
|
||||
* ProDOS Physical sector order (index is physical sector, value is ProDOS sector).
|
||||
*/
|
||||
export const PO = [
|
||||
0x0, 0x8, 0x1, 0x9, 0x2, 0xa, 0x3, 0xb,
|
||||
0x4, 0xc, 0x5, 0xd, 0x6, 0xe, 0x7, 0xf
|
||||
];
|
||||
|
||||
/**
|
||||
* ProDOS Logical sector order (index is ProDOS sector, value is physical sector).
|
||||
*/
|
||||
export const _PO = [
|
||||
0x0, 0x2, 0x4, 0x6, 0x8, 0xa, 0xc, 0xe,
|
||||
0x1, 0x3, 0x5, 0x7, 0x9, 0xb, 0xd, 0xf
|
||||
|
@ -9,9 +9,14 @@
|
||||
* implied warranty.
|
||||
*/
|
||||
|
||||
import { explodeSector16, _PO } from './format_utils';
|
||||
import { explodeSector16, PO } from './format_utils';
|
||||
import { bytify } from '../util';
|
||||
|
||||
/**
|
||||
* Returns a `Disk` object from ProDOS-ordered image data.
|
||||
* @param {*} options the disk image and options
|
||||
* @returns {import('./format_utils').Disk}
|
||||
*/
|
||||
export default function ProDOS(options) {
|
||||
var { data, name, rawData, volume, readOnly } = options;
|
||||
var disk = {
|
||||
@ -24,21 +29,22 @@ export default function ProDOS(options) {
|
||||
rawTracks: null
|
||||
};
|
||||
|
||||
for (var t = 0; t < 35; t++) {
|
||||
for (var physical_track = 0; physical_track < 35; physical_track++) {
|
||||
var track = [];
|
||||
for (var s = 0; s < 16; s++) {
|
||||
for (var physical_sector = 0; physical_sector < 16; physical_sector++) {
|
||||
const prodos_sector = PO[physical_sector];
|
||||
var sector;
|
||||
if (rawData) {
|
||||
var off = (16 * t + s) * 256;
|
||||
var off = (16 * physical_track + prodos_sector) * 256;
|
||||
sector = new Uint8Array(rawData.slice(off, off + 256));
|
||||
} else {
|
||||
sector = data[t][s];
|
||||
sector = data[physical_track][prodos_sector];
|
||||
}
|
||||
track = track.concat(
|
||||
explodeSector16(volume, t, _PO[s], sector)
|
||||
explodeSector16(volume, physical_track, physical_sector, sector)
|
||||
);
|
||||
}
|
||||
disk.tracks[t] = bytify(track);
|
||||
disk.tracks[physical_track] = bytify(track);
|
||||
}
|
||||
|
||||
return disk;
|
||||
|
@ -1,58 +1,7 @@
|
||||
import DOS from '../../../js/formats/do';
|
||||
import { memory } from '../../../js/types';
|
||||
import { BYTES_BY_SECTOR, BYTES_BY_TRACK } from './testdata/16sector';
|
||||
|
||||
function skipGap(track: memory, start: number = 0): number {
|
||||
const end = start + 0x100; // no gap is this big
|
||||
let i = start;
|
||||
while (i < end && track[i] == 0xFF) {
|
||||
i++;
|
||||
}
|
||||
if (i == end) {
|
||||
fail(`found more than 0x100 0xFF bytes after ${start}`);
|
||||
}
|
||||
return i;
|
||||
}
|
||||
|
||||
function compareSequences(track: memory, bytes: number[], pos: number): boolean {
|
||||
for (let i = 0; i < bytes.length; i++) {
|
||||
if (track[i + pos] != bytes[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function expectSequence(track: memory, pos: number, bytes: number[]): number {
|
||||
if (!compareSequences(track, bytes, pos)) {
|
||||
const track_slice = track.slice(pos, Math.min(track.length, pos + bytes.length));
|
||||
fail(`expected ${bytes} got ${track_slice}`);
|
||||
}
|
||||
return pos + bytes.length;
|
||||
}
|
||||
|
||||
function findBytes(track: memory, bytes: number[], start: number = 0): number {
|
||||
for (let i = start; i < track.length; i++) {
|
||||
if (compareSequences(track, bytes, i)) {
|
||||
return i + bytes.length;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
describe('compareSequences', () => {
|
||||
it('matches at pos 0', () => {
|
||||
expect(
|
||||
compareSequences([0x01, 0x02, 0x03], [0x01, 0x02, 0x03], 0)
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it('matches at pos 1', () => {
|
||||
expect(
|
||||
compareSequences([0x00, 0x01, 0x02, 0x03], [0x01, 0x02, 0x03], 1)
|
||||
).toBeTruthy();
|
||||
});
|
||||
});
|
||||
import { expectSequence, findBytes, skipGap } from './util';
|
||||
|
||||
describe('DOS format', () => {
|
||||
it('is callable', () => {
|
||||
|
287
test/js/formats/po.spec.ts
Normal file
287
test/js/formats/po.spec.ts
Normal file
@ -0,0 +1,287 @@
|
||||
import ProDOS from '../../../js/formats/po';
|
||||
import { memory } from '../../../js/types';
|
||||
import { BYTES_BY_SECTOR, BYTES_BY_TRACK } from './testdata/16sector';
|
||||
import { expectSequence, findBytes, skipGap } from './util';
|
||||
|
||||
describe('ProDOS format', () => {
|
||||
it('is callable', () => {
|
||||
const disk = ProDOS({
|
||||
name: 'test disk',
|
||||
data: BYTES_BY_TRACK,
|
||||
volume: 10,
|
||||
readOnly: true,
|
||||
});
|
||||
expect(disk).not.toBeNull();
|
||||
});
|
||||
|
||||
it('has correct number of tracks', () => {
|
||||
const disk = ProDOS({
|
||||
name: 'test disk',
|
||||
data: BYTES_BY_TRACK,
|
||||
volume: 10,
|
||||
readOnly: true,
|
||||
});
|
||||
expect(disk.tracks.length).toEqual(35);
|
||||
});
|
||||
|
||||
it('has correct number of bytes in track 0', () => {
|
||||
const disk = ProDOS({
|
||||
name: 'test disk',
|
||||
data: BYTES_BY_TRACK,
|
||||
volume: 10,
|
||||
readOnly: true,
|
||||
});
|
||||
expect(disk.tracks[0].length).toEqual(6632);
|
||||
});
|
||||
|
||||
it('has correct number of bytes in all tracks', () => {
|
||||
const disk = ProDOS({
|
||||
name: 'test disk',
|
||||
data: BYTES_BY_TRACK,
|
||||
volume: 10,
|
||||
readOnly: true,
|
||||
});
|
||||
// Track 0 is slightly longer for some reason.
|
||||
expect(disk.tracks[0].length).toEqual(6632);
|
||||
for (let i = 1; i < disk.tracks.length; i++) {
|
||||
expect(disk.tracks[i].length).toEqual(6602);
|
||||
}
|
||||
});
|
||||
|
||||
it('has correct GAP 1', () => {
|
||||
const disk = ProDOS({
|
||||
name: 'test disk',
|
||||
data: BYTES_BY_TRACK,
|
||||
volume: 10,
|
||||
readOnly: true,
|
||||
});
|
||||
// From Beneith Apple DOS, GAP 1 should have 12-85 0xFF bytes
|
||||
const track = disk.tracks[0];
|
||||
let numFF = 0;
|
||||
while (track[numFF] == 0xFF && numFF < 0x100) {
|
||||
numFF++;
|
||||
}
|
||||
expect(numFF).toBeGreaterThanOrEqual(40);
|
||||
expect(numFF).toBeLessThanOrEqual(128);
|
||||
});
|
||||
|
||||
it('has correct Address Field for track 0, sector 0', () => {
|
||||
// _Beneath Apple DOS_, TRACK FORMATTING, p. 3-12
|
||||
const disk = ProDOS({
|
||||
name: 'test disk',
|
||||
data: BYTES_BY_TRACK,
|
||||
volume: 10,
|
||||
readOnly: true,
|
||||
});
|
||||
const track = disk.tracks[0];
|
||||
let i = skipGap(track);
|
||||
// prologue
|
||||
i = expectSequence(track, i, [0xD5, 0xAA, 0x96]);
|
||||
// volume 10 = 0b00001010
|
||||
expect(track[i++]).toBe(0b10101111);
|
||||
expect(track[i++]).toBe(0b10101010);
|
||||
// track 0 = 0b00000000
|
||||
expect(track[i++]).toBe(0b10101010);
|
||||
expect(track[i++]).toBe(0b10101010);
|
||||
// sector 0 = 0b00000000
|
||||
expect(track[i++]).toBe(0b10101010);
|
||||
expect(track[i++]).toBe(0b10101010);
|
||||
// checksum = 0b00000101
|
||||
expect(track[i++]).toBe(0b10101111);
|
||||
expect(track[i++]).toBe(0b10101010);
|
||||
// epilogue
|
||||
i = expectSequence(track, i, [0xDE, 0xAA, 0xEB]);
|
||||
});
|
||||
|
||||
it('has correct Data Field for track 0, sector 0 (BYTES_BY_TRACK)', () => {
|
||||
// _Beneath Apple DOS_, DATA FIELD ENCODING, pp. 3-13 to 3-21
|
||||
const disk = ProDOS({
|
||||
name: 'test disk',
|
||||
data: BYTES_BY_TRACK,
|
||||
volume: 10,
|
||||
readOnly: true,
|
||||
});
|
||||
const track: memory = disk.tracks[0];
|
||||
// skip to the first address epilogue
|
||||
let i = findBytes(track, [0xDE, 0xAA, 0xEB]);
|
||||
expect(i).toBeGreaterThan(50);
|
||||
i = skipGap(track, i);
|
||||
// prologue
|
||||
i = expectSequence(track, i, [0xD5, 0xAA, 0xAD]);
|
||||
// data (all zeros, which is 0x96 with 6 and 2 encoding)
|
||||
for (let j = 0; j < 342; j++) {
|
||||
expect(track[i++]).toBe(0x96);
|
||||
}
|
||||
// checksum (also zero)
|
||||
expect(track[i++]).toBe(0x96);
|
||||
// epilogue
|
||||
i = expectSequence(track, i, [0xDE, 0xAA, 0xEB]);
|
||||
});
|
||||
|
||||
it('has correct Address Field for track 0, sector 1', () => {
|
||||
// _Beneath Apple DOS_, TRACK FORMATTING, p. 3-12
|
||||
const disk = ProDOS({
|
||||
name: 'test disk',
|
||||
data: BYTES_BY_TRACK,
|
||||
volume: 10,
|
||||
readOnly: true,
|
||||
});
|
||||
const track = disk.tracks[0];
|
||||
// first sector prologue
|
||||
let i = findBytes(track, [0xD5, 0xAA, 0x96]);
|
||||
|
||||
// second sector prologue
|
||||
i = findBytes(track, [0xD5, 0xAA, 0x96], i);
|
||||
// volume 10 = 0b00001010
|
||||
expect(track[i++]).toBe(0b10101111);
|
||||
expect(track[i++]).toBe(0b10101010);
|
||||
// track 0 = 0b00000000
|
||||
expect(track[i++]).toBe(0b10101010);
|
||||
expect(track[i++]).toBe(0b10101010);
|
||||
// sector 1 = 0b00000001
|
||||
expect(track[i++]).toBe(0b10101010);
|
||||
expect(track[i++]).toBe(0b10101011);
|
||||
// checksum = 0b00000101
|
||||
expect(track[i++]).toBe(0b10101111);
|
||||
expect(track[i++]).toBe(0b10101011);
|
||||
// epilogue
|
||||
i = expectSequence(track, i, [0xDE, 0xAA, 0xEB]);
|
||||
});
|
||||
|
||||
it('has correct Data Field for track 0, sector 1 (BYTES_BY_SECTOR)', () => {
|
||||
// _Beneath Apple DOS_, DATA FIELD ENCODING, pp. 3-13 to 3-21
|
||||
const disk = ProDOS({
|
||||
name: 'test disk',
|
||||
data: BYTES_BY_SECTOR,
|
||||
volume: 10,
|
||||
readOnly: true,
|
||||
});
|
||||
const track: memory = disk.tracks[0];
|
||||
// First data field prologue
|
||||
let i = findBytes(track, [0xD5, 0xAA, 0xAD]);
|
||||
// Second data field prologue
|
||||
i = findBytes(track, [0xD5, 0xAA, 0xAD], i);
|
||||
// Sector 1 is ProDOS sector 8.
|
||||
// In 6 x 2 encoding, the lowest 2 bits of all the bytes come first.
|
||||
// 0x07 is 0b00001000, so the lowest two bits are 0b00, reversed and
|
||||
// repeated would be 0b000000 (00 -> 0x96). Even though each byte is
|
||||
// XOR'd with the previous, they are all the same. This means there
|
||||
// are 86 0b00000000 (00 -> 0x96) bytes.
|
||||
for (let j = 0; j < 86; j++) {
|
||||
expect(track[i++]).toBe(0x96);
|
||||
}
|
||||
// Next we get 256 instances of the top bits, 0b000010. Again, with
|
||||
// the XOR, this means one 0b000010 XOR 0b000000 = 0b000010
|
||||
// (02 -> 0x9A) followed by 255 0b0000000 (00 -> 0x96).
|
||||
expect(track[i++]).toBe(0x9A);
|
||||
for (let j = 0; j < 255; j++) {
|
||||
expect(track[i++]).toBe(0x96);
|
||||
}
|
||||
// checksum 0b000010 XOR 0b000000 -> 9A
|
||||
expect(track[i++]).toBe(0x9A);
|
||||
// epilogue
|
||||
i = expectSequence(track, i, [0xDE, 0xAA, 0xEB]);
|
||||
});
|
||||
|
||||
it('has correct Address Field for track 1, sector 0', () => {
|
||||
// _Beneath Apple DOS_, TRACK FORMATTING, p. 3-12
|
||||
const disk = ProDOS({
|
||||
name: 'test disk',
|
||||
data: BYTES_BY_TRACK,
|
||||
volume: 10,
|
||||
readOnly: true,
|
||||
});
|
||||
const track = disk.tracks[1];
|
||||
let i = skipGap(track);
|
||||
// prologue
|
||||
i = expectSequence(track, i, [0xD5, 0xAA, 0x96]);
|
||||
// volume 10 = 0b00001010
|
||||
expect(track[i++]).toBe(0b10101111);
|
||||
expect(track[i++]).toBe(0b10101010);
|
||||
// track 1 = 0b00000001
|
||||
expect(track[i++]).toBe(0b10101010);
|
||||
expect(track[i++]).toBe(0b10101011);
|
||||
// sector 0 = 0b00000000
|
||||
expect(track[i++]).toBe(0b10101010);
|
||||
expect(track[i++]).toBe(0b10101010);
|
||||
// checksum = 0b00000100
|
||||
expect(track[i++]).toBe(0b10101111);
|
||||
expect(track[i++]).toBe(0b10101011);
|
||||
// epilogue
|
||||
i = expectSequence(track, i, [0xDE, 0xAA, 0xEB]);
|
||||
});
|
||||
|
||||
it('has correct Data Field for track 1, sector 0 (BYTES_BY_TRACK)', () => {
|
||||
// _Beneath Apple DOS_, DATA FIELD ENCODING, pp. 3-13 to 3-21
|
||||
const disk = ProDOS({
|
||||
name: 'test disk',
|
||||
data: BYTES_BY_TRACK,
|
||||
volume: 10,
|
||||
readOnly: true,
|
||||
});
|
||||
const track: memory = disk.tracks[1];
|
||||
let i = findBytes(track, [0xDE, 0xAA, 0xEB]);
|
||||
expect(i).toBeGreaterThan(50);
|
||||
i = skipGap(track, i);
|
||||
// prologue
|
||||
i = expectSequence(track, i, [0xD5, 0xAA, 0xAD]);
|
||||
// In 6 x 2 encoding, the lowest 2 bits of all the bytes come first.
|
||||
// This would normally mean 86 instances of 0b101010 (2A -> 0xE6),
|
||||
// but each byte is XOR'd with the previous. Since all of the bits
|
||||
// are the same, this means there are 85 0b000000 (00 -> 0x96).
|
||||
expect(track[i++]).toBe(0xE6);
|
||||
for (let j = 0; j < 85; j++) {
|
||||
expect(track[i++]).toBe(0x96);
|
||||
}
|
||||
// Next we get 256 instances of the top bits, 0b000000. Again, with
|
||||
// the XOR, this means one 0x101010 (2A -> 0xE6) followed by 255
|
||||
// 0b0000000 (00 -> 0x96).
|
||||
expect(track[i++]).toBe(0xE6);
|
||||
for (let j = 0; j < 255; j++) {
|
||||
expect(track[i++]).toBe(0x96);
|
||||
}
|
||||
// checksum (also zero)
|
||||
expect(track[i++]).toBe(0x96);
|
||||
// epilogue
|
||||
i = expectSequence(track, i, [0xDE, 0xAA, 0xEB]);
|
||||
});
|
||||
|
||||
|
||||
it('has correct Address Fields for all tracks', () => {
|
||||
// _Beneath Apple DOS_, TRACK FORMATTING, p. 3-12
|
||||
const disk = ProDOS({
|
||||
name: 'test disk',
|
||||
data: BYTES_BY_TRACK,
|
||||
volume: 10,
|
||||
readOnly: true,
|
||||
});
|
||||
|
||||
for (let t = 0; t < disk.tracks.length; t++) {
|
||||
// We essentially seek through the track for the Address Fields
|
||||
const track = disk.tracks[t];
|
||||
let i = findBytes(track, [0xD5, 0xAA, 0x96]);
|
||||
for (let s = 0; s <= 15; s++) {
|
||||
// volume 10 = 0b00001010
|
||||
expect(track[i++]).toBe(0b10101111);
|
||||
expect(track[i++]).toBe(0b10101010);
|
||||
// convert track to 4x4 encoding
|
||||
const track4x4XX = ((t & 0b10101010) >> 1) | 0b10101010;
|
||||
const track4x4YY = (t & 0b01010101) | 0b10101010;
|
||||
expect(track[i++]).toBe(track4x4XX);
|
||||
expect(track[i++]).toBe(track4x4YY);
|
||||
// convert sector to 4x4 encoding
|
||||
const sector4x4XX = ((s & 0b10101010) >> 1) | 0b10101010;
|
||||
const sector4x4YY = (s & 0b01010101) | 0b10101010;
|
||||
expect(track[i++]).toBe(sector4x4XX);
|
||||
expect(track[i++]).toBe(sector4x4YY);
|
||||
// checksum
|
||||
expect(track[i++]).toBe(0b10101111 ^ track4x4XX ^ sector4x4XX);
|
||||
expect(track[i++]).toBe(0b10101010 ^ track4x4YY ^ sector4x4YY);
|
||||
// epilogue
|
||||
i = expectSequence(track, i, [0xDE, 0xAA, 0xEB]);
|
||||
// next sector
|
||||
i = findBytes(track, [0xD5, 0xAA, 0x96], i);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
15
test/js/formats/util.spec.ts
Normal file
15
test/js/formats/util.spec.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { compareSequences } from './util';
|
||||
|
||||
describe('compareSequences', () => {
|
||||
it('matches at pos 0', () => {
|
||||
expect(
|
||||
compareSequences([0x01, 0x02, 0x03], [0x01, 0x02, 0x03], 0)
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it('matches at pos 1', () => {
|
||||
expect(
|
||||
compareSequences([0x00, 0x01, 0x02, 0x03], [0x01, 0x02, 0x03], 1)
|
||||
).toBeTruthy();
|
||||
});
|
||||
});
|
42
test/js/formats/util.ts
Normal file
42
test/js/formats/util.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import { memory } from '../../../js/types';
|
||||
|
||||
export function skipGap(track: memory, start: number = 0): number {
|
||||
const end = start + 0x100; // no gap is this big
|
||||
let i = start;
|
||||
while (i < end && track[i] == 0xFF) {
|
||||
i++;
|
||||
}
|
||||
if (i == end) {
|
||||
fail(`found more than 0x100 0xFF bytes after ${start}`);
|
||||
}
|
||||
return i;
|
||||
}
|
||||
|
||||
export function compareSequences(track: memory, bytes: number[], pos: number): boolean {
|
||||
for (let i = 0; i < bytes.length; i++) {
|
||||
if (track[i + pos] != bytes[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export function expectSequence(track: memory, pos: number, bytes: number[]): number {
|
||||
if (!compareSequences(track, bytes, pos)) {
|
||||
const track_slice = track.slice(pos, Math.min(track.length, pos + bytes.length));
|
||||
fail(`expected ${bytes} got ${track_slice}`);
|
||||
}
|
||||
return pos + bytes.length;
|
||||
}
|
||||
|
||||
export function findBytes(track: memory, bytes: number[], start: number = 0): number {
|
||||
if (start + bytes.length > track.length) {
|
||||
return -1;
|
||||
}
|
||||
for (let i = start; i < track.length; i++) {
|
||||
if (compareSequences(track, bytes, i)) {
|
||||
return i + bytes.length;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
Loading…
Reference in New Issue
Block a user