Skip to content

Commit 5fa0dc2

Browse files
committed
Restructure tests and add options
1 parent 481f189 commit 5fa0dc2

File tree

3 files changed

+131
-44
lines changed

3 files changed

+131
-44
lines changed
Lines changed: 37 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,46 @@
11
import {getLocalPathByDirAndFile} from '../Examples.js';
22
import {BufferAccess} from '../common/BufferAccess.js';
3+
import {type OptionValues} from '../encoding/Options.js';
34
import {convert} from './ConverterManager.js';
45
import * as fs from 'fs';
56

6-
test('Format kctap is converted correctly', () => {
7-
const data = BufferAccess.createFromNodeBuffer(fs.readFileSync(getLocalPathByDirAndFile('kc851_tap', 'rl.com')));
7+
type TestDefinition = {
8+
name: string;
9+
dir: string;
10+
input: string;
11+
expected: string;
12+
options: OptionValues;
13+
};
814

9-
const result = convert(data, 'kctap', {});
15+
const formatTests: TestDefinition[] = [
16+
{
17+
name: 'ataricas',
18+
dir: 'atari_bin',
19+
input: 'rl.bin',
20+
expected: 'rl.cas',
21+
options: {},
22+
},
23+
{
24+
name: 'kctap',
25+
dir: 'kc851_tap',
26+
input: 'rl.com',
27+
expected: 'rl.tap',
28+
options: {load: '0300', entry: '0300', name: 'RL', kctype: 'COM'},
29+
},
30+
];
1031

11-
const expectedData = BufferAccess.createFromNodeBuffer(fs.readFileSync(getLocalPathByDirAndFile('kc851_tap', 'rl.tap')));
12-
expect(result.asHexDump()).toBe(expectedData.asHexDump());
32+
describe('Formats are converted correctly', () => {
33+
it.each(formatTests.map((t: TestDefinition) => ({label: getTestLabel(t), definition: t})))(
34+
'$label',
35+
(test) => {
36+
const data = BufferAccess.createFromNodeBuffer(fs.readFileSync(getLocalPathByDirAndFile(test.definition.dir, test.definition.input)));
37+
const result = convert(data, test.definition.name, test.definition.options);
38+
const expectedData = BufferAccess.createFromNodeBuffer(fs.readFileSync(getLocalPathByDirAndFile(test.definition.dir, test.definition.expected)));
39+
expect(result.asHexDump()).toBe(expectedData.asHexDump());
40+
},
41+
);
1342
});
1443

15-
test('Format ataricas is converted correctly', () => {
16-
const data = BufferAccess.createFromNodeBuffer(fs.readFileSync(getLocalPathByDirAndFile('atari_bin', 'rl.bin')));
17-
18-
const result = convert(data, 'ataricas', {});
19-
20-
const expectedData = BufferAccess.createFromNodeBuffer(fs.readFileSync(getLocalPathByDirAndFile('atari_bin', 'rl.cas')));
21-
expect(result.asHexDump()).toBe(expectedData.asHexDump());
22-
});
44+
function getTestLabel(test: TestDefinition): string {
45+
return `${test.name}: ${test.dir}/${test.input} --> ${test.dir}/${test.expected}, options: ${JSON.stringify(test.options)}`;
46+
}
Lines changed: 51 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,29 @@
11
import {BufferAccess} from '../../common/BufferAccess.js';
2+
import {InvalidArgumentError} from '../../common/Exceptions.js';
23
import {calculateChecksum8WithCarry} from '../../common/Utils.js';
3-
import {type OptionContainer} from '../../encoding/Options.js';
4+
import {type OptionContainer, type ArgumentOptionDefinition} from '../../encoding/Options.js';
45
import {type ConverterDefinition} from './ConverterDefinition.js';
56

7+
const irgLengthOption: ArgumentOptionDefinition<number | undefined> = {
8+
name: 'irglength',
9+
label: 'Intra record gap length',
10+
description: 'Gap between blocks in ms (default: 250)',
11+
common: false,
12+
required: false,
13+
type: 'text',
14+
parse(value: string) {
15+
if (value === '') {
16+
return undefined;
17+
}
18+
const casted = Number(value);
19+
if (isNaN(casted)) {
20+
throw new InvalidArgumentError(this.name, `Option ${this.name} is expected to be a number in decimal or hexadecimal notation.`);
21+
}
22+
23+
return casted;
24+
},
25+
};
26+
627
const definition: ConverterDefinition = {
728
name: 'Atari .CAS-File',
829
identifier: 'ataricas',
@@ -20,7 +41,9 @@ const dataBytesPerBlock = 128;
2041
const pilotIrgLength = 20000;
2142
const defaultIrgLength = 250; // TODO: longer for 'ENTER'-loading
2243

23-
function convert(data: BufferAccess, _options: OptionContainer): BufferAccess {
44+
function convert(data: BufferAccess, options: OptionContainer): BufferAccess {
45+
const irgLength = options.getArgument(irgLengthOption) ?? defaultIrgLength;
46+
2447
const chunks = data.chunks(dataBytesPerBlock);
2548

2649
// FUJI-Header, baud-block, data blocks, end of file block
@@ -36,36 +59,43 @@ function convert(data: BufferAccess, _options: OptionContainer): BufferAccess {
3659

3760
for (let i = 0; i < chunks.length; i++) {
3861
const chunk = chunks[i];
39-
4062
outBa.writeAsciiString('data');
4163
outBa.writeUint16Le(132);
42-
outBa.writeUint16Le(i === 0 ? pilotIrgLength : defaultIrgLength);
43-
44-
const blockBa = BufferAccess.create(132);
45-
blockBa.writeUint8(markerByte);
46-
blockBa.writeUint8(markerByte);
47-
const partialBlock = chunk.length() !== dataBytesPerBlock;
48-
const blockType = partialBlock ? blockTypePartial : blockTypeFull;
49-
blockBa.writeUint8(blockType);
50-
blockBa.writeBa(partialBlock ? chunk.chunksPadded(dataBytesPerBlock, 0x00)[0] : chunk);
51-
if (partialBlock) {
52-
blockBa.setUint8(130, chunk.length());
53-
}
54-
blockBa.setUint8(131, calculateChecksum8WithCarry(blockBa));
55-
56-
outBa.writeBa(blockBa);
64+
outBa.writeUint16Le(i === 0 ? pilotIrgLength : irgLength);
65+
outBa.writeBa(createDataBlock(chunk));
5766
}
5867

5968
// end of file block
6069
outBa.writeAsciiString('data');
6170
outBa.writeUint16Le(132);
62-
outBa.writeUint16Le(defaultIrgLength);
71+
outBa.writeUint16Le(irgLength);
72+
outBa.writeBa(createEndBlock());
73+
74+
return outBa;
75+
}
76+
77+
function createDataBlock(data: BufferAccess): BufferAccess {
78+
const dataBlock = BufferAccess.create(132);
79+
dataBlock.writeUint8(markerByte);
80+
dataBlock.writeUint8(markerByte);
81+
const partialBlock = data.length() !== dataBytesPerBlock;
82+
const blockType = partialBlock ? blockTypePartial : blockTypeFull;
83+
dataBlock.writeUint8(blockType);
84+
dataBlock.writeBa(partialBlock ? data.chunksPadded(dataBytesPerBlock, 0x00)[0] : data);
85+
if (partialBlock) {
86+
dataBlock.setUint8(130, data.length());
87+
}
88+
dataBlock.setUint8(131, calculateChecksum8WithCarry(dataBlock));
89+
90+
return dataBlock;
91+
}
92+
93+
function createEndBlock(): BufferAccess {
6394
const endBlock = BufferAccess.create(132);
6495
endBlock.writeUint8(markerByte);
6596
endBlock.writeUint8(markerByte);
6697
endBlock.writeUint8(blockTypeEndOfFile);
6798
endBlock.setUint8(131, calculateChecksum8WithCarry(endBlock));
68-
outBa.writeBa(endBlock);
6999

70-
return outBa;
100+
return endBlock;
71101
}

retroload-lib/src/conversion/converter/KcTapConverter.ts

Lines changed: 43 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,59 @@
11
import {BufferAccess} from '../../common/BufferAccess.js';
2-
import {type OptionContainer} from '../../encoding/Options.js';
2+
import {InvalidArgumentError} from '../../common/Exceptions.js';
3+
import {type ArgumentOptionDefinition, entryOption, loadOption, nameOption, type OptionContainer} from '../../encoding/Options.js';
34
import {type ConverterDefinition} from './ConverterDefinition.js';
45

6+
const kctypeOption: ArgumentOptionDefinition<string | undefined> = {
7+
name: 'kctype',
8+
label: 'File type',
9+
description: 'File type, 3 characters (default: COM)',
10+
common: false,
11+
required: false,
12+
type: 'text',
13+
parse(value: string) {
14+
if (value === '') {
15+
return undefined;
16+
}
17+
if (value.length !== 3) {
18+
throw new InvalidArgumentError(this.name, `Option ${this.name} is expected have exactly 3 characters (example: COM).`);
19+
}
20+
21+
return value;
22+
},
23+
};
24+
525
const definition: ConverterDefinition = {
626
name: 'KC .TAP-File',
727
identifier: 'kctap',
8-
options: [],
28+
options: [
29+
loadOption,
30+
entryOption,
31+
nameOption,
32+
],
933
convert,
1034
};
1135
export default definition;
1236

13-
function convert(data: BufferAccess, _options: OptionContainer): BufferAccess {
37+
const maxFileNameLength = 8;
38+
39+
function convert(data: BufferAccess, options: OptionContainer): BufferAccess {
1440
const blocks = data.chunksPadded(128, 0x00);
1541
const outBa = BufferAccess.create(16 + (1 + blocks.length) * 129);
1642

17-
// TODO: use options
43+
const loadAddress = options.getArgument(loadOption) ?? 0x0300;
44+
const entryAddress = options.getArgument(entryOption) ?? 0xffff; // default: no auto start
45+
const filename = options.getArgument(nameOption) ?? '';
46+
const filetype = options.getArgument(kctypeOption) ?? 'COM';
47+
if (filename.length > maxFileNameLength) {
48+
throw new InvalidArgumentError('name', `Maximum length of filename (${maxFileNameLength}) exceeded.`);
49+
}
50+
1851
const header = createHeader(
19-
'RL',
20-
'COM',
21-
0x0300,
22-
0x0300 + data.length() - 1,
23-
0x0300,
52+
filename,
53+
filetype,
54+
loadAddress,
55+
loadAddress + data.length() - 1,
56+
entryAddress,
2457
);
2558

2659
// TAP header
@@ -43,7 +76,7 @@ function convert(data: BufferAccess, _options: OptionContainer): BufferAccess {
4376
function createHeader(name: string, fileType: string, loadAddress: number, endAddress: number, startAddress: number): BufferAccess {
4477
const header = BufferAccess.create(128);
4578

46-
header.writeAsciiString(name, 8, 0x00);
79+
header.writeAsciiString(name, maxFileNameLength, 0x00);
4780
header.writeAsciiString(fileType);
4881
header.writeUint8(0x00); // reserved
4982
header.writeUint8(0x00); // reserved

0 commit comments

Comments
 (0)