@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
407 lines (406 loc) • 15.7 kB
JavaScript
"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.Deamp = exports.File = void 0;
const tslib_1 = require("tslib");
// tslint:disable:max-classes-per-file
const block_1 = require("./block");
const crc16_1 = require("./crc16");
const amp_1 = require("./amp");
const base91 = tslib_1.__importStar(require("./base91"));
const TypedEvent_1 = require("./TypedEvent");
const compressor_1 = require("./compressor");
let crc16 = crc16_1.crc16;
var ParserState;
(function (ParserState) {
ParserState[ParserState["LOOKFORBLOCK"] = 0] = "LOOKFORBLOCK";
ParserState[ParserState["BLOCKTAG"] = 1] = "BLOCKTAG";
ParserState[ParserState["DATA"] = 2] = "DATA";
})(ParserState || (ParserState = {}));
;
function bad(b) {
throw new Error("Invalid state!");
}
// Apply startsWith / endsWith polyfills if needed; this is a browser after all =]
(function (sp) {
if (!sp.startsWith)
sp.startsWith = function (str) {
return !!(str && this) && !this.lastIndexOf(str, 0);
};
if (!sp.endsWith)
sp.endsWith = function (str) {
var offset = str && this ? this.length - str.length : -1;
return offset >= 0 && this.lastIndexOf(str, offset) === offset;
};
})(String.prototype);
class File {
fromCallsign = null;
headerBlocks = [];
dataBlock = {};
description;
name;
size;
blockCount;
blockSize;
hash;
modified;
completeFired = false;
constructor(firstBlock) {
this.hash = firstBlock.hash;
this.addBlock(firstBlock);
}
getOrderedDataBlocks() {
if (!this.blockCount) {
throw new Error("Missing file header");
}
// Blocks are ordered 1 .. blockCount and we may be missing some
let orderedBlocks = [...Array(this.blockCount).keys()].map(i => this.dataBlock[i + 1]);
return orderedBlocks;
}
getNeededBlocks() {
let blocks = this.getOrderedDataBlocks();
return Object.keys(blocks).map(b => (blocks[Number(b)] ? 0 : Number(b) + 1)).filter(b => !!b);
}
getRawContent() {
if (!this.blockCount) {
throw new Error("Missing file header");
}
let blocks = this.getOrderedDataBlocks();
if (blocks.some(b => !b)) {
// We're missing one or more blocks!
throw new Error("Can't get content when we are missing blocks");
}
return blocks.map(b => b.data).join('');
}
getContent() {
let content = this.getRawContent();
if (content.startsWith('[b') && content.endsWith(':end]')) {
let endOfStart = content.indexOf(']') + 1;
let tag = content.substring(0, endOfStart);
content = content.substring(endOfStart, content.lastIndexOf('['));
let binaryString = '';
switch (tag) {
case '[b64:start]':
binaryString = atob(content);
break;
case '[b91:start]':
binaryString = base91.decode(content);
break;
}
if (binaryString) {
const c = compressor_1.Compressor.getDecompressor(binaryString);
if (c) {
const decompressedContent = c.decompress(binaryString);
content = new TextDecoder().decode(decompressedContent);
}
else {
const binArray = new Uint8Array(binaryString.length);
for (let i = 0; i < binaryString.length; i++) {
binArray[i] = binaryString.charCodeAt(i);
}
content = new TextDecoder().decode(binArray);
}
}
}
if (content.length !== this.size) {
console.error('File size is not correct', content.length, this.size);
return null;
}
return content;
}
toBlob() {
let content = this.getContent();
return new Blob([content], { type: 'text/plain' });
}
getUpdateRecord() {
return {
hash: this.hash,
filename: this.name,
blockCount: this.blockCount,
blockSize: this.blockSize,
blocksSeen: Object.keys(this.dataBlock).map(n => Number(n)),
blocksNeeded: this.blockSize ? this.getNeededBlocks() : void 0,
};
}
/**
* Adds a received block to the file; returns true if the block
* is new (has not been received already for this file)
* @param inBlock
*/
addBlock(inBlock) {
let isNew = false;
switch (inBlock.keyword) {
case amp_1.LTypes.FILE:
this.name = inBlock.data.substring(15);
this.modified = (0, amp_1.stringToDate)(inBlock.data.substr(0, 14));
break;
case amp_1.LTypes.DESC:
this.description = inBlock.data;
break;
case amp_1.LTypes.ID:
this.fromCallsign = inBlock.data;
break;
case amp_1.LTypes.SIZE:
let pieces = inBlock.data.split(' ').map(v => parseInt(v, 10));
this.size = pieces[0];
this.blockCount = pieces[1];
this.blockSize = pieces[2];
break;
case amp_1.LTypes.DATA:
return this.addDataBlock(inBlock);
default:
break;
}
if (!this.headerBlocks.find(b => b.keyword === inBlock.keyword && b.checksum === inBlock.checksum)) {
this.headerBlocks.push(inBlock);
return true;
}
return false;
}
addDataBlock(inBlock) {
let blockNum = inBlock.blockNum;
if (!this.dataBlock[blockNum]) {
this.dataBlock[blockNum] = inBlock;
return true;
}
return false;
}
isComplete() {
if (!this.name || !this.blockCount) {
return false;
}
try {
// Raw content length will be less than
// the size if we are compressed or encoded,
// so we don't use that check anymore.
// let rawContent = this.getRawContent();
// return rawContent.length === this.size;
//
// When we have no needed blocks and we know
// the name and the block count then WE ARE DONE!
let needed = this.getNeededBlocks();
return needed.length == 0;
}
catch (e) {
return false;
}
}
}
exports.File = File;
const BlockTagValidChars = /^[A-Za-z0-9 ]$/;
const BlockTagRegex = /^([A-Za-z]+) ([0-9]+) ([0-9A-Fa-f]+)$/;
class Deamp {
PROGRAM = "JSAMP";
VERSION = "0.0.1";
parserState = ParserState.LOOKFORBLOCK;
parserData = {
dataStart: 0,
dataLen: 0,
checksum: "",
tagname: amp_1.LTypes.ID,
};
// Fields used in receiving.
newFileEvent = new TypedEvent_1.TypedEvent();
fileUpdateEvent = new TypedEvent_1.TypedEvent();
fileCompleteEvent = new TypedEvent_1.TypedEvent();
receivedFiles = {};
inputBuffer = "";
constructor(opts) {
}
setCrc16(crc) {
crc16 = crc;
}
clearBuffer() {
this.inputBuffer = '';
}
pruneInputBuffer() {
let buffer = this.inputBuffer;
let lookAgain = true;
while (lookAgain) {
lookAgain = false;
let firstBracket = buffer.indexOf('<');
if (firstBracket > -1) {
// Discard everything up to the first bracket
buffer = buffer.substr(firstBracket);
let closeBracket = buffer.indexOf('>', 1);
if (closeBracket < 0 || closeBracket > 30) {
// This isn't a valid block
buffer = buffer.substr(1); // Drop the bracket, try again
lookAgain = true;
}
}
else {
buffer = '';
}
}
// If it isn't in the last 300 characters then it's not going to be used
this.inputBuffer = buffer.substr(-300);
}
/**
* This should only be called for unit tests
*/
__getInputBuffer() {
return this.inputBuffer;
}
ingestString(inString) {
for (let char of inString.split('')) {
let block = this._processInput(char);
if (block) {
this.addBlockToFiles(block);
}
}
return;
}
// Left intentionally public to allow us to test it separately
_processInput(oneChar) {
switch (this.parserState) {
case ParserState.LOOKFORBLOCK:
// Default state, we haven't found anything yet
if (oneChar == '<') {
// Hey, this could be the start of a beautiful tag!
this.inputBuffer = oneChar;
this.parserState = ParserState.BLOCKTAG;
}
break;
case ParserState.BLOCKTAG:
// We might be inside a block, so we're looking for
// useful things
if (BlockTagValidChars.test(oneChar)) {
// This is something which could very well go inside a block tag! Way cool!
this.inputBuffer += oneChar;
if (this.inputBuffer.length > 25) {
// Seriously, if we're 25 characters in with no > there is no way
// this is a real tag, so let's just drop out and try again
this.inputBuffer = "";
this.parserState = ParserState.LOOKFORBLOCK;
}
}
else if (oneChar == '>') {
// Hey, we've found the end of the block tag; wonder what it is and if it is valid?
let blockTagContent = this.inputBuffer.substring(1);
let search = BlockTagRegex.exec(blockTagContent);
if (search && Object.values(amp_1.LTypes).indexOf(search[1].toUpperCase()) > -1) {
// Hey, this might actually be a real tag!
this.parserData.tagname = search[1].toUpperCase();
let dataLen = this.parserData.dataLen = parseInt(search[2], 10);
let checksum = this.parserData.checksum = search[3];
if (isNaN(dataLen) || checksum.length != 4) {
// Oops, malformed!
this.parserState = ParserState.LOOKFORBLOCK;
this.inputBuffer = "";
return;
}
this.inputBuffer += '>';
this.parserData.dataStart = this.inputBuffer.length;
this.parserState = ParserState.DATA;
}
else {
// Not a valid tag! Discard and go back to the old state
this.inputBuffer = "";
this.parserState = ParserState.LOOKFORBLOCK;
}
}
else {
// This isn't a close tag and it's not valid for inside a block,
// so the block we're "in" is just a sad, sad illusion. Drop back
// but make sure we process this char in case it's the beginning of
// a real block
this.inputBuffer = "";
this.parserState = ParserState.LOOKFORBLOCK;
return this._processInput(oneChar);
}
break;
case ParserState.DATA:
// We've got a potentially valid block and we know how long it is,
// and we haven't reached the end yet
this.inputBuffer += oneChar;
// Check to see if that takes us to the end or not
let dataLen = this.inputBuffer.length - this.parserData.dataStart;
if (dataLen == this.parserData.dataLen) {
// We have received a full block! (well, maybe... let's double check)
let data = this.inputBuffer.substr(this.parserData.dataStart, dataLen);
let block = new block_1.Block(this.parserData.tagname, data);
if (block.checksum != this.parserData.checksum) {
// We got the block and checked it and .... it was bad.
// Oops.
// Well, toss this one and try again -- but keep in mind
// that we might actually need something from the data!
this.parserState = ParserState.LOOKFORBLOCK;
this.inputBuffer = "";
if (data.indexOf('<')) {
// There might be another block in there!
this.ingestString(data.substr(data.indexOf('<')));
}
}
else {
// Sweet! We found a complete block. Reset and continue!
this.inputBuffer = "";
this.parserState = ParserState.LOOKFORBLOCK;
return block;
}
}
break;
default:
return bad(this.parserState);
}
}
addBlockToFiles(inBlock) {
let isNewBlock = false;
let file = this.receivedFiles[inBlock.hash];
if (!file) {
file = this.receivedFiles[inBlock.hash] = new File(inBlock);
isNewBlock = true;
this.newFileEvent.emit({
hash: inBlock.hash
});
}
else {
isNewBlock = file.addBlock(inBlock);
}
if (isNewBlock) {
this.fileUpdateEvent.emit(file.getUpdateRecord());
if (file.isComplete() && !file.completeFired) {
this.fileCompleteEvent.emit({
hash: file.hash,
filename: file.name,
});
file.completeFired = true;
}
}
}
getFilesEntries() { return Object.keys(this.receivedFiles); }
/**
* Gets the file but leaves it in memory
* @param fileHash
*/
getFile(fileHash) {
return this.receivedFiles[fileHash];
}
/**
* Retrieves the file and frees all related memory
* @param fileHash
*/
popFile(fileHash) {
let file = this.receivedFiles[fileHash];
delete this.receivedFiles[fileHash];
return file;
}
/**
* Gets the file contents by hash
* @param fileHash
*/
getFileContents(fileHash) {
let file = this.getFile(fileHash);
let contents = file.getContent();
return contents;
}
}
exports.Deamp = Deamp;