From 0b382bf596e1d52c20861cc1dad7054710cea274 Mon Sep 17 00:00:00 2001 From: Stefan Schramm Date: Tue, 19 Dec 2023 22:15:35 +0100 Subject: [PATCH] Restructure tests and add options --- .../src/conversion/ConverterManager.test.ts | 50 +++++++++---- .../conversion/converter/AtariCasConverter.ts | 72 +++++++++++++------ .../conversion/converter/KcTapConverter.ts | 53 +++++++++++--- 3 files changed, 131 insertions(+), 44 deletions(-) diff --git a/retroload-lib/src/conversion/ConverterManager.test.ts b/retroload-lib/src/conversion/ConverterManager.test.ts index e5bd7a3..326b871 100644 --- a/retroload-lib/src/conversion/ConverterManager.test.ts +++ b/retroload-lib/src/conversion/ConverterManager.test.ts @@ -1,22 +1,46 @@ import {getLocalPathByDirAndFile} from '../Examples.js'; import {BufferAccess} from '../common/BufferAccess.js'; +import {type OptionValues} from '../encoding/Options.js'; import {convert} from './ConverterManager.js'; import * as fs from 'fs'; -test('Format kctap is converted correctly', () => { - const data = BufferAccess.createFromNodeBuffer(fs.readFileSync(getLocalPathByDirAndFile('kc851_tap', 'rl.com'))); +type TestDefinition = { + name: string; + dir: string; + input: string; + expected: string; + options: OptionValues; +}; - const result = convert(data, 'kctap', {}); +const formatTests: TestDefinition[] = [ + { + name: 'ataricas', + dir: 'atari_bin', + input: 'rl.bin', + expected: 'rl.cas', + options: {}, + }, + { + name: 'kctap', + dir: 'kc851_tap', + input: 'rl.com', + expected: 'rl.tap', + options: {load: '0300', entry: '0300', name: 'RL', kctype: 'COM'}, + }, +]; - const expectedData = BufferAccess.createFromNodeBuffer(fs.readFileSync(getLocalPathByDirAndFile('kc851_tap', 'rl.tap'))); - expect(result.asHexDump()).toBe(expectedData.asHexDump()); +describe('Formats are converted correctly', () => { + it.each(formatTests.map((t: TestDefinition) => ({label: getTestLabel(t), definition: t})))( + '$label', + (test) => { + const data = BufferAccess.createFromNodeBuffer(fs.readFileSync(getLocalPathByDirAndFile(test.definition.dir, test.definition.input))); + const result = convert(data, test.definition.name, test.definition.options); + const expectedData = BufferAccess.createFromNodeBuffer(fs.readFileSync(getLocalPathByDirAndFile(test.definition.dir, test.definition.expected))); + expect(result.asHexDump()).toBe(expectedData.asHexDump()); + }, + ); }); -test('Format ataricas is converted correctly', () => { - const data = BufferAccess.createFromNodeBuffer(fs.readFileSync(getLocalPathByDirAndFile('atari_bin', 'rl.bin'))); - - const result = convert(data, 'ataricas', {}); - - const expectedData = BufferAccess.createFromNodeBuffer(fs.readFileSync(getLocalPathByDirAndFile('atari_bin', 'rl.cas'))); - expect(result.asHexDump()).toBe(expectedData.asHexDump()); -}); +function getTestLabel(test: TestDefinition): string { + return `${test.name}: ${test.dir}/${test.input} --> ${test.dir}/${test.expected}, options: ${JSON.stringify(test.options)}`; +} diff --git a/retroload-lib/src/conversion/converter/AtariCasConverter.ts b/retroload-lib/src/conversion/converter/AtariCasConverter.ts index 9a851d2..45f75b6 100644 --- a/retroload-lib/src/conversion/converter/AtariCasConverter.ts +++ b/retroload-lib/src/conversion/converter/AtariCasConverter.ts @@ -1,8 +1,29 @@ import {BufferAccess} from '../../common/BufferAccess.js'; +import {InvalidArgumentError} from '../../common/Exceptions.js'; import {calculateChecksum8WithCarry} from '../../common/Utils.js'; -import {type OptionContainer} from '../../encoding/Options.js'; +import {type OptionContainer, type ArgumentOptionDefinition} from '../../encoding/Options.js'; import {type ConverterDefinition} from './ConverterDefinition.js'; +const irgLengthOption: ArgumentOptionDefinition = { + name: 'irglength', + label: 'Intra record gap length', + description: 'Gap between blocks in ms (default: 250)', + common: false, + required: false, + type: 'text', + parse(value: string) { + if (value === '') { + return undefined; + } + const casted = Number(value); + if (isNaN(casted)) { + throw new InvalidArgumentError(this.name, `Option ${this.name} is expected to be a number in decimal or hexadecimal notation.`); + } + + return casted; + }, +}; + const definition: ConverterDefinition = { name: 'Atari .CAS-File', identifier: 'ataricas', @@ -20,7 +41,9 @@ const dataBytesPerBlock = 128; const pilotIrgLength = 20000; const defaultIrgLength = 250; // TODO: longer for 'ENTER'-loading -function convert(data: BufferAccess, _options: OptionContainer): BufferAccess { +function convert(data: BufferAccess, options: OptionContainer): BufferAccess { + const irgLength = options.getArgument(irgLengthOption) ?? defaultIrgLength; + const chunks = data.chunks(dataBytesPerBlock); // FUJI-Header, baud-block, data blocks, end of file block @@ -36,36 +59,43 @@ function convert(data: BufferAccess, _options: OptionContainer): BufferAccess { for (let i = 0; i < chunks.length; i++) { const chunk = chunks[i]; - outBa.writeAsciiString('data'); outBa.writeUint16Le(132); - outBa.writeUint16Le(i === 0 ? pilotIrgLength : defaultIrgLength); - - const blockBa = BufferAccess.create(132); - blockBa.writeUint8(markerByte); - blockBa.writeUint8(markerByte); - const partialBlock = chunk.length() !== dataBytesPerBlock; - const blockType = partialBlock ? blockTypePartial : blockTypeFull; - blockBa.writeUint8(blockType); - blockBa.writeBa(partialBlock ? chunk.chunksPadded(dataBytesPerBlock, 0x00)[0] : chunk); - if (partialBlock) { - blockBa.setUint8(130, chunk.length()); - } - blockBa.setUint8(131, calculateChecksum8WithCarry(blockBa)); - - outBa.writeBa(blockBa); + outBa.writeUint16Le(i === 0 ? pilotIrgLength : irgLength); + outBa.writeBa(createDataBlock(chunk)); } // end of file block outBa.writeAsciiString('data'); outBa.writeUint16Le(132); - outBa.writeUint16Le(defaultIrgLength); + outBa.writeUint16Le(irgLength); + outBa.writeBa(createEndBlock()); + + return outBa; +} + +function createDataBlock(data: BufferAccess): BufferAccess { + const dataBlock = BufferAccess.create(132); + dataBlock.writeUint8(markerByte); + dataBlock.writeUint8(markerByte); + const partialBlock = data.length() !== dataBytesPerBlock; + const blockType = partialBlock ? blockTypePartial : blockTypeFull; + dataBlock.writeUint8(blockType); + dataBlock.writeBa(partialBlock ? data.chunksPadded(dataBytesPerBlock, 0x00)[0] : data); + if (partialBlock) { + dataBlock.setUint8(130, data.length()); + } + dataBlock.setUint8(131, calculateChecksum8WithCarry(dataBlock)); + + return dataBlock; +} + +function createEndBlock(): BufferAccess { const endBlock = BufferAccess.create(132); endBlock.writeUint8(markerByte); endBlock.writeUint8(markerByte); endBlock.writeUint8(blockTypeEndOfFile); endBlock.setUint8(131, calculateChecksum8WithCarry(endBlock)); - outBa.writeBa(endBlock); - return outBa; + return endBlock; } diff --git a/retroload-lib/src/conversion/converter/KcTapConverter.ts b/retroload-lib/src/conversion/converter/KcTapConverter.ts index 3724514..714445a 100644 --- a/retroload-lib/src/conversion/converter/KcTapConverter.ts +++ b/retroload-lib/src/conversion/converter/KcTapConverter.ts @@ -1,26 +1,59 @@ import {BufferAccess} from '../../common/BufferAccess.js'; -import {type OptionContainer} from '../../encoding/Options.js'; +import {InvalidArgumentError} from '../../common/Exceptions.js'; +import {type ArgumentOptionDefinition, entryOption, loadOption, nameOption, type OptionContainer} from '../../encoding/Options.js'; import {type ConverterDefinition} from './ConverterDefinition.js'; +const kctypeOption: ArgumentOptionDefinition = { + name: 'kctype', + label: 'File type', + description: 'File type, 3 characters (default: COM)', + common: false, + required: false, + type: 'text', + parse(value: string) { + if (value === '') { + return undefined; + } + if (value.length !== 3) { + throw new InvalidArgumentError(this.name, `Option ${this.name} is expected have exactly 3 characters (example: COM).`); + } + + return value; + }, +}; + const definition: ConverterDefinition = { name: 'KC .TAP-File', identifier: 'kctap', - options: [], + options: [ + loadOption, + entryOption, + nameOption, + ], convert, }; export default definition; -function convert(data: BufferAccess, _options: OptionContainer): BufferAccess { +const maxFileNameLength = 8; + +function convert(data: BufferAccess, options: OptionContainer): BufferAccess { const blocks = data.chunksPadded(128, 0x00); const outBa = BufferAccess.create(16 + (1 + blocks.length) * 129); - // TODO: use options + const loadAddress = options.getArgument(loadOption) ?? 0x0300; + const entryAddress = options.getArgument(entryOption) ?? 0xffff; // default: no auto start + const filename = options.getArgument(nameOption) ?? ''; + const filetype = options.getArgument(kctypeOption) ?? 'COM'; + if (filename.length > maxFileNameLength) { + throw new InvalidArgumentError('name', `Maximum length of filename (${maxFileNameLength}) exceeded.`); + } + const header = createHeader( - 'RL', - 'COM', - 0x0300, - 0x0300 + data.length() - 1, - 0x0300, + filename, + filetype, + loadAddress, + loadAddress + data.length() - 1, + entryAddress, ); // TAP header @@ -43,7 +76,7 @@ function convert(data: BufferAccess, _options: OptionContainer): BufferAccess { function createHeader(name: string, fileType: string, loadAddress: number, endAddress: number, startAddress: number): BufferAccess { const header = BufferAccess.create(128); - header.writeAsciiString(name, 8, 0x00); + header.writeAsciiString(name, maxFileNameLength, 0x00); header.writeAsciiString(fileType); header.writeUint8(0x00); // reserved header.writeUint8(0x00); // reserved