diff --git a/retroload-lib/src/Examples.ts b/retroload-lib/src/Examples.ts index 333e8c0..b2ee920 100644 --- a/retroload-lib/src/Examples.ts +++ b/retroload-lib/src/Examples.ts @@ -457,6 +457,10 @@ export function getLocalPath(example: ExampleDefinition): string { return `${__dirname}/../examples/formats/${example.dir}/${example.file}`; } +export function getLocalPathByDirAndFile(dir: string, file: string): string { + return `${__dirname}/../examples/formats/${dir}/${file}`; +} + export function getUrl(example: ExampleDefinition): string { return `https://github.com/stefanschramm/retroload/tree/main/retroload-lib/examples/formats/${example.dir}/${example.file}`; } diff --git a/retroload-lib/src/conversion/ConverterManager.test.ts b/retroload-lib/src/conversion/ConverterManager.test.ts new file mode 100644 index 0000000..292ef10 --- /dev/null +++ b/retroload-lib/src/conversion/ConverterManager.test.ts @@ -0,0 +1,13 @@ +import {getLocalPathByDirAndFile} from '../Examples.js'; +import {BufferAccess} from '../common/BufferAccess.js'; +import {convert} from './ConverterManager.js'; +import * as fs from 'fs'; + +test('ConverterManager calls convert function of ConverterDefinition', () => { + const data = BufferAccess.createFromNodeBuffer(fs.readFileSync(getLocalPathByDirAndFile('kc851_tap', 'rl.com'))); + + const result = convert(data, 'kctap', {}); + + const expectedData = BufferAccess.createFromNodeBuffer(fs.readFileSync(getLocalPathByDirAndFile('kc851_tap', 'rl.tap'))); + expect(result.asHexDump()).toBe(expectedData.asHexDump()); +}); diff --git a/retroload-lib/src/conversion/ConverterManager.ts b/retroload-lib/src/conversion/ConverterManager.ts new file mode 100644 index 0000000..8395c5c --- /dev/null +++ b/retroload-lib/src/conversion/ConverterManager.ts @@ -0,0 +1,17 @@ +import {type BufferAccess} from '../common/BufferAccess.js'; +import {FormatNotFoundError} from '../common/Exceptions.js'; +import {OptionContainer, type OptionValues} from '../encoding/Options.js'; +import {converters} from './ConverterProvider.js'; +import {type ConverterDefinition} from './converter/ConverterDefinition.js'; + +export function convert(data: BufferAccess, identifier: string, options: OptionValues): BufferAccess { + const chosenConverters = converters.filter((c: ConverterDefinition) => c.identifier === identifier); + + if (chosenConverters.length === 0) { + throw new FormatNotFoundError(identifier); + } + + const converter = chosenConverters[0]; + + return converter.convert(data, new OptionContainer(options)); +} diff --git a/retroload-lib/src/conversion/ConverterProvider.ts b/retroload-lib/src/conversion/ConverterProvider.ts new file mode 100644 index 0000000..829f566 --- /dev/null +++ b/retroload-lib/src/conversion/ConverterProvider.ts @@ -0,0 +1,6 @@ +import {type ConverterDefinition} from './converter/ConverterDefinition.js'; +import KcTapConverter from './converter/KcTapConverter.js'; + +export const converters: ConverterDefinition[] = [ + KcTapConverter, +]; diff --git a/retroload-lib/src/conversion/converter/ConverterDefinition.ts b/retroload-lib/src/conversion/converter/ConverterDefinition.ts new file mode 100644 index 0000000..de9921e --- /dev/null +++ b/retroload-lib/src/conversion/converter/ConverterDefinition.ts @@ -0,0 +1,9 @@ +import {type BufferAccess} from '../../common/BufferAccess.js'; +import {type OptionContainer, type PublicOptionDefinition} from '../../encoding/Options.js'; + +export type ConverterDefinition = { + readonly name: string; + readonly identifier: string; + readonly options: PublicOptionDefinition[]; + readonly convert: (ba: BufferAccess, options: OptionContainer) => BufferAccess; +}; diff --git a/retroload-lib/src/conversion/converter/KcTapConverter.ts b/retroload-lib/src/conversion/converter/KcTapConverter.ts new file mode 100644 index 0000000..3724514 --- /dev/null +++ b/retroload-lib/src/conversion/converter/KcTapConverter.ts @@ -0,0 +1,59 @@ +import {BufferAccess} from '../../common/BufferAccess.js'; +import {type OptionContainer} from '../../encoding/Options.js'; +import {type ConverterDefinition} from './ConverterDefinition.js'; + +const definition: ConverterDefinition = { + name: 'KC .TAP-File', + identifier: 'kctap', + options: [], + convert, +}; +export default definition; + +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 header = createHeader( + 'RL', + 'COM', + 0x0300, + 0x0300 + data.length() - 1, + 0x0300, + ); + + // TAP header + outBa.writeUint8(0xc3); + outBa.writeAsciiString('KC-TAPE by AF. '); + + // header block (FCB) + outBa.writeUint8(0); + outBa.writeBa(header); + + // data blocks + for (let i = 0; i < blocks.length; i++) { + outBa.writeUint8(i === blocks.length - 1 ? 0xff : i + 1); + outBa.writeBa(blocks[i]); + } + + return outBa; +} + +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(fileType); + header.writeUint8(0x00); // reserved + header.writeUint8(0x00); // reserved + header.writeUint16Le(0x0000); // PSUM (block checksum(?)) + header.writeUint8(0x00); // ARB (internal working cell(?)) + header.writeUint8(0x03); // BLNR (block number(?)) (count?) + header.writeUint16Le(loadAddress); // AADR + header.writeUint16Le(endAddress); // EADR + header.writeUint16Le(startAddress); // SADR + header.writeUint8(0x00); // SBY (protection byte) + + return header; +}