UNPKG

@hamstudy/flamp

Version:

JavaScript Amateur Multicast Protocol AMP-2 Version 3 Implemented from specification document http://www.w1hkj.com/files/flamp/Amp-2.V3.0.Protocol.pdf • Version 1.0.0 - W5ALT, Walt Fair, Jr. (Derived From) • Version 2.0.0 - W1HKJ, Dave Freese, w

316 lines (315 loc) 12.3 kB
"use strict"; // JavaScript Amateur Multicast Protocol AMP-2 Version 3 // Implemented from specification document // http://www.w1hkj.com/files/flamp/Amp-2.V3.0.Protocol.pdf // • Version 1.0.0 - W5ALT, Walt Fair, Jr. (Derived From) // • Version 2.0.0 - W1HKJ, Dave Freese, w1hkj@w1hkj.com // • Version 2.0.1 - W1HKJ, Dave Freese, w1hkj@w1hkj.com, 5 Oct 2012 // • Version 3.0.0 - KK5VD, Robert Stiles, kk5vd@yahoo.com, 21 April 2013 // • Javascript Implementation by KV9G, Michael Stufflebeam cpuchip@gmail.com, 29 June 2018 // Object.defineProperty(exports, "__esModule", { value: true }); exports.Amp = exports.CompressionType = exports.BaseEncode = exports.ControlWord = exports.LTypes = exports.MODIFIED_TIME_REGEX = exports.lzmaCompressedPrefix = void 0; exports.dateToString = dateToString; exports.stringToDate = stringToDate; exports.uint8ToBinaryString = uint8ToBinaryString; const tslib_1 = require("tslib"); const block_1 = require("./block"); const crc16_1 = require("./crc16"); const compressor_1 = require("./compressor"); const base91 = tslib_1.__importStar(require("./base91")); exports.lzmaCompressedPrefix = '\u0001LZMA'; exports.MODIFIED_TIME_REGEX = /(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})/; /** * Takes a date and returns a string in format YYYYMMDDhhmmss */ function dateToString(d) { const year = d.getFullYear(); const month = (`0${d.getMonth() + 1}`).slice(-2); const day = (`0${d.getDate()}`).slice(-2); const hours = (`0${d.getHours()}`).slice(-2); const minutes = (`0${d.getMinutes()}`).slice(-2); const seconds = (`0${d.getSeconds()}`).slice(-2); return `${year}${month}${day}${hours}${minutes}${seconds}`; } /** * Takes a string in format YYYYMMDDhhmmss and returns a date */ function stringToDate(str) { return new Date(str.replace(exports.MODIFIED_TIME_REGEX, '$1-$2-$3T$4:$5:$6')); } function uint8ToBinaryString(bytes) { let s = ""; const CHUNK = 0x8000; // 32768 for (let i = 0; i < bytes.length; i += CHUNK) { s += String.fromCharCode(...bytes.subarray(i, i + CHUNK)); } return s; } const unprintableRegex = /[^ -~\n\r]+/; function hasNotPrintable(c) { return unprintableRegex.test(c); } function assertUnreachable(x) { throw new Error("Invalid case"); } var LTypes; (function (LTypes) { LTypes["FILE"] = "FILE"; LTypes["ID"] = "ID"; LTypes["SIZE"] = "SIZE"; LTypes["DESC"] = "DESC"; LTypes["DATA"] = "DATA"; LTypes["PROG"] = "PROG"; LTypes["CNTL"] = "CNTL"; })(LTypes || (exports.LTypes = LTypes = {})); var ControlWord; (function (ControlWord) { ControlWord["EOF"] = "EOF"; ControlWord["EOT"] = "EOT"; })(ControlWord || (exports.ControlWord = ControlWord = {})); var BaseEncode; (function (BaseEncode) { BaseEncode["b64"] = "base64"; BaseEncode["b91"] = "base91"; // b128 = 'base128', // b256 = 'base256', })(BaseEncode || (exports.BaseEncode = BaseEncode = {})); var CompressionType; (function (CompressionType) { CompressionType["LZMA"] = "LZMA"; })(CompressionType || (exports.CompressionType = CompressionType = {})); class Amp { static toString(blocks, fromCallsign = '', toCallsign = '') { if (!Object.keys(blocks || {}).length) { return ''; } let preProtocolHeaders; if (toCallsign && fromCallsign) { preProtocolHeaders = `${toCallsign} DE ${fromCallsign}\n\n`; } else if (toCallsign) { preProtocolHeaders = `${toCallsign} DE ME\n\n`; } else if (fromCallsign) { preProtocolHeaders = `QST DE ${fromCallsign}\n\n`; } else { preProtocolHeaders = "QST\n\n"; } const blockStrings = []; if (preProtocolHeaders) { blockStrings.push(preProtocolHeaders); } // Looping through the keywords in a specific order to build the output string for (let key of [ LTypes.PROG, LTypes.FILE, LTypes.ID, LTypes.SIZE, LTypes.DESC, LTypes.DATA, ControlWord.EOF, ControlWord.EOT ]) { if (key === LTypes.DATA) { for (const k of Object.keys(blocks)) { if (isNaN(Number(k))) { continue; } const block = blocks[k]; blockStrings.push(block.toString()); } continue; } let block = blocks[key]; if (key === LTypes.ID) { if (!fromCallsign) { continue; } if (!block || fromCallsign !== block.data) { let hash = ''; if (block) { hash = block.hash; } else { let k = Object.keys(blocks)[0]; if (k) { hash = blocks[k].hash; } } if (hash) { block = block_1.Block.MakeBlock({ keyword: LTypes.ID, hash, data: fromCallsign }); } } } if (!block) { continue; } blockStrings.push(block.toString()); } if (fromCallsign) { blockStrings.push(fromCallsign); } return blockStrings.join('\n'); } static getHash(filename, modified, compressed, baseConversion, blockSize) { // | DTS : FN |C| B |BS| // DTS = Date/Time Stamp // FN = File Name // C = Compression 1=ON,0=OFF // B = Base Conversion (base64, base128, or base256) // BS = Block Size, 1 or more characters // | = Field separator. let DTS = dateToString(modified); return (0, crc16_1.crc16)(`${DTS}:${filename}${compressed ? '1' : '0'}${baseConversion}${blockSize}`); } fromCallsign; toCallsign; filename; fileDescription; fileModifiedTime; inputBuffer; blkSize; PROGRAM = "JSAMP"; VERSION = "1.1.8"; base = ""; compression = false; forceCompress = false; blocks = {}; hash = ''; dataBlockCount = 0; skipProgram = false; useEOF = true; useEOT = true; // Fields used in receiving. receivedFiles = {}; constructor(opts) { this.fromCallsign = opts.fromCallsign || null; this.toCallsign = opts.toCallsign || null; this.filename = opts.filename; this.fileDescription = opts.fileDescription || ''; this.fileModifiedTime = opts.fileModifiedTime; this.blkSize = opts.blkSize; this.compression = opts.compression || false; this.forceCompress = !!opts.forceCompress; this.skipProgram = !!opts.skipProgram; this.useEOF = opts.useEOF !== false; this.useEOT = opts.useEOT !== false; if (opts.base) { this.setBase(opts.base); } this.inputBuffer = opts.inputBuffer; this.hash = Amp.getHash(this.filename, this.fileModifiedTime, !!this.compression, this.base, this.blkSize); this.makeBlocks(); } makeBlocks() { this.blocks = {}; this.dataBlockCount = this.quantizeMessage(); if (!this.skipProgram) { this.blocks[LTypes.PROG] = block_1.Block.MakeBlock({ keyword: LTypes.PROG, hash: this.hash, data: `${this.PROGRAM} ${this.VERSION}` }); } this.blocks[LTypes.FILE] = block_1.Block.MakeBlock({ keyword: LTypes.FILE, hash: this.hash, data: `${dateToString(this.fileModifiedTime)}:${this.filename}` }); if (this.fromCallsign) { this.blocks[LTypes.ID] = block_1.Block.MakeBlock({ keyword: LTypes.ID, hash: this.hash, data: this.fromCallsign }); } if (this.fileDescription) { this.blocks[LTypes.DESC] = block_1.Block.MakeBlock({ keyword: LTypes.DESC, hash: this.hash, data: this.fileDescription }); } this.blocks[LTypes.SIZE] = block_1.Block.MakeBlock({ keyword: LTypes.SIZE, hash: this.hash, data: `${this.inputBuffer.length} ${this.dataBlockCount} ${this.blkSize}` }); if (this.useEOF) { this.blocks[ControlWord.EOF] = block_1.Block.MakeBlock({ keyword: LTypes.CNTL, hash: this.hash, controlWord: ControlWord.EOF }); } if (this.useEOT) { this.blocks[ControlWord.EOT] = block_1.Block.MakeBlock({ keyword: LTypes.CNTL, hash: this.hash, controlWord: ControlWord.EOT }); } } toString(dataBlockList, includeHeaders = true) { const blocks = {}; for (const key of Object.keys(this.blocks)) { if ((isNaN(Number(key)) ? !includeHeaders : (!dataBlockList || dataBlockList.indexOf(Number(key)) === -1)) || (key === LTypes.PROG && this.skipProgram) || (key === LTypes.ID && !this.fromCallsign) || (key === LTypes.DESC && !this.fileDescription) || (key === ControlWord.EOF && !this.useEOF) || (key === ControlWord.EOT && !this.useEOT)) { continue; } blocks[key] = this.blocks[key]; } return Amp.toString(blocks, this.fromCallsign || '', this.toCallsign || ''); } getDataBlockCount() { return this.dataBlockCount; } /** * The base to use for transmitting the data, if any * @param base base64 or base91 */ setBase(base) { this.base = base; } baseEncode(base, data) { switch (base) { case BaseEncode.b64: return `[b64:start]${btoa(uint8ToBinaryString(data))}[b64:end]`; case BaseEncode.b91: return `[b91:start]${base91.encode(data)}[b91:end]`; default: return assertUnreachable(base); } } quantizeMessage() { let actualBuffer = this.inputBuffer; let needsBase = hasNotPrintable(actualBuffer); if (needsBase && !this.base) { // For our purposes we're forcing some base conversion if there are unprintable characters this.base = BaseEncode.b91; } let baseApplied = false; // Apply compression if any if (this.compression === CompressionType.LZMA) { let c = compressor_1.Compressor.getCompressor(); // get the default compressor try { const compressedBuffer = c.compress(actualBuffer); const newBuffer = this.baseEncode(this.base || BaseEncode.b91, compressedBuffer); if (newBuffer.length < actualBuffer.length - 200 || this.forceCompress) { // If compression doesn't save us at least 200 bytes it's not worth while actualBuffer = newBuffer; baseApplied = true; } } catch (e) { console.error('Compression failed, continuing without compression', e); } } // Apply base64 or base91 encoding here if (this.base && !baseApplied) { actualBuffer = this.baseEncode(this.base, new TextEncoder().encode(actualBuffer)); baseApplied = true; } if (actualBuffer.length > this.inputBuffer.length && !needsBase && !this.forceCompress) { // If all characters were printable and it's shorter without the conversion // then let's just send it without actualBuffer = this.inputBuffer; } let numbOfBlocks = Math.floor(actualBuffer.length / this.blkSize); if (actualBuffer.length % this.blkSize > 0) { numbOfBlocks++; } let blockNum = 1; let start = 0; while (start < actualBuffer.length) { let block = block_1.Block.MakeBlock({ keyword: LTypes.DATA, hash: this.hash, data: actualBuffer.slice(start, start + this.blkSize), blockNum }); this.blocks[blockNum] = block; start += this.blkSize; blockNum++; } return numbOfBlocks; } } exports.Amp = Amp;