/* jslint node: true */ 'use strict'; const Errors = require('./enig_error.js').Errors; // deps const iconv = require('iconv-lite'); const { Parser } = require('binary-parser'); exports.readSAUCE = readSAUCE; const SAUCE_SIZE = 128; const SAUCE_ID = Buffer.from([0x53, 0x41, 0x55, 0x43, 0x45]); // 'SAUCE' // :TODO read comments //const COMNT_ID = Buffer.from([0x43, 0x4f, 0x4d, 0x4e, 0x54]); // 'COMNT' exports.SAUCE_SIZE = SAUCE_SIZE; // :TODO: SAUCE should be a class // - with getFontName() // - ...other methods // // See // http://www.acid.org/info/sauce/sauce.htm // const SAUCE_VALID_DATA_TYPES = [0, 1, 2, 3, 4, 5, 6, 7, 8]; const SAUCEParser = new Parser() .buffer('id', { length: 5 }) .buffer('version', { length: 2 }) .buffer('title', { length: 35 }) .buffer('author', { length: 20 }) .buffer('group', { length: 20 }) .buffer('date', { length: 8 }) .uint32le('fileSize') .int8('dataType') .int8('fileType') .uint16le('tinfo1') .uint16le('tinfo2') .uint16le('tinfo3') .uint16le('tinfo4') .int8('numComments') .int8('flags') // :TODO: does this need to be optional? .buffer('tinfos', { length: 22 }); // SAUCE 00.5 function readSAUCE(data, cb) { if (data.length < SAUCE_SIZE) { return cb(Errors.DoesNotExist('No SAUCE record present')); } let sauceRec; try { sauceRec = SAUCEParser.parse(data.slice(data.length - SAUCE_SIZE)); } catch (e) { return cb(Errors.Invalid('Invalid SAUCE record')); } if (!SAUCE_ID.equals(sauceRec.id)) { return cb(Errors.DoesNotExist('No SAUCE record present')); } const ver = iconv.decode(sauceRec.version, 'cp437'); if ('00' !== ver) { return cb(Errors.Invalid(`Unsupported SAUCE version: ${ver}`)); } if (-1 === SAUCE_VALID_DATA_TYPES.indexOf(sauceRec.dataType)) { return cb(Errors.Invalid(`Unsupported SAUCE DataType: ${sauceRec.dataType}`)); } const sauce = { id: iconv.decode(sauceRec.id, 'cp437'), version: iconv.decode(sauceRec.version, 'cp437').trim(), title: iconv.decode(sauceRec.title, 'cp437').trim(), author: iconv.decode(sauceRec.author, 'cp437').trim(), group: iconv.decode(sauceRec.group, 'cp437').trim(), date: iconv.decode(sauceRec.date, 'cp437').trim(), fileSize: sauceRec.fileSize, dataType: sauceRec.dataType, fileType: sauceRec.fileType, tinfo1: sauceRec.tinfo1, tinfo2: sauceRec.tinfo2, tinfo3: sauceRec.tinfo3, tinfo4: sauceRec.tinfo4, numComments: sauceRec.numComments, flags: sauceRec.flags, tinfos: sauceRec.tinfos, }; const dt = SAUCE_DATA_TYPES[sauce.dataType]; if (dt && dt.parser) { sauce[dt.name] = dt.parser(sauce); } return cb(null, sauce); } // :TODO: These need completed: const SAUCE_DATA_TYPES = { 0: { name: 'None' }, 1: { name: 'Character', parser: parseCharacterSAUCE }, 2: 'Bitmap', 3: 'Vector', 4: 'Audio', 5: 'BinaryText', 6: 'XBin', 7: 'Archive', 8: 'Executable', }; const SAUCE_CHARACTER_FILE_TYPES = { 0: 'ASCII', 1: 'ANSi', 2: 'ANSiMation', 3: 'RIP script', 4: 'PCBoard', 5: 'Avatar', 6: 'HTML', 7: 'Source', 8: 'TundraDraw', }; // // Map of SAUCE font -> encoding hint // // Note that this is the same mapping that x84 uses. Be compatible! // const SAUCE_FONT_TO_ENCODING_HINT = { 'Amiga MicroKnight': 'amiga', 'Amiga MicroKnight+': 'amiga', 'Amiga mOsOul': 'amiga', 'Amiga P0T-NOoDLE': 'amiga', 'Amiga Topaz 1': 'amiga', 'Amiga Topaz 1+': 'amiga', 'Amiga Topaz 2': 'amiga', 'Amiga Topaz 2+': 'amiga', 'Atari ATASCII': 'atari', 'IBM EGA43': 'cp437', 'IBM EGA': 'cp437', 'IBM VGA25G': 'cp437', 'IBM VGA50': 'cp437', 'IBM VGA': 'cp437', }; [ '437', '720', '737', '775', '819', '850', '852', '855', '857', '858', '860', '861', '862', '863', '864', '865', '866', '869', '872', ].forEach(page => { const codec = 'cp' + page; SAUCE_FONT_TO_ENCODING_HINT['IBM EGA43 ' + page] = codec; SAUCE_FONT_TO_ENCODING_HINT['IBM EGA ' + page] = codec; SAUCE_FONT_TO_ENCODING_HINT['IBM VGA25g ' + page] = codec; SAUCE_FONT_TO_ENCODING_HINT['IBM VGA50 ' + page] = codec; SAUCE_FONT_TO_ENCODING_HINT['IBM VGA ' + page] = codec; }); function parseCharacterSAUCE(sauce) { const result = {}; result.fileType = SAUCE_CHARACTER_FILE_TYPES[sauce.fileType] || 'Unknown'; if (sauce.fileType === 0 || sauce.fileType === 1 || sauce.fileType === 2) { // convenience: create ansiFlags sauce.ansiFlags = sauce.flags; let i = 0; while (i < sauce.tinfos.length && sauce.tinfos[i] !== 0x00) { ++i; } const fontName = iconv.decode(sauce.tinfos.slice(0, i), 'cp437'); if (fontName.length > 0) { result.fontName = fontName; } const setDimen = (v, field) => { const i = parseInt(v, 10); if (!isNaN(i)) { result[field] = i; } }; setDimen(sauce.tinfo1, 'characterWidth'); setDimen(sauce.tinfo2, 'characterHeight'); } return result; }