UNPKG

@apocentre/bc-ur

Version:

A JS implementation of the Uniform Resources (UR) specification from Blockchain Commons

209 lines 7.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.FountainDecoderPart = void 0; const utils_1 = require("./utils"); const fountainUtils_1 = require("./fountainUtils"); class FountainDecoderPart { constructor(_indexes, _fragment) { this._indexes = _indexes; this._fragment = _fragment; } get indexes() { return this._indexes; } get fragment() { return this._fragment; } static fromEncoderPart(encoderPart) { const indexes = fountainUtils_1.chooseFragments(encoderPart.seqNum, encoderPart.seqLength, encoderPart.checksum); const fragment = encoderPart.fragment; return new FountainDecoderPart(indexes, fragment); } isSimple() { return this.indexes.length === 1; } } exports.FountainDecoderPart = FountainDecoderPart; class FountainDecoder { constructor() { this.result = undefined; this.expectedMessageLength = 0; this.expectedChecksum = 0; this.expectedFragmentLength = 0; this.processedPartsCount = 0; this.expectedPartIndexes = []; this.lastPartIndexes = []; this.queuedParts = []; this.receivedPartIndexes = []; this.mixedParts = []; this.simpleParts = []; } validatePart(part) { // If this is the first part we've seen if (this.expectedPartIndexes.length === 0) { // Record the things that all the other parts we see will have to match to be valid. [...new Array(part.seqLength)] .forEach((_, index) => this.expectedPartIndexes.push(index)); this.expectedMessageLength = part.messageLength; this.expectedChecksum = part.checksum; this.expectedFragmentLength = part.fragment.length; } else { // If this part's values don't match the first part's values, throw away the part if (this.expectedPartIndexes.length !== part.seqLength) { return false; } if (this.expectedMessageLength !== part.messageLength) { return false; } if (this.expectedChecksum !== part.checksum) { return false; } if (this.expectedFragmentLength !== part.fragment.length) { return false; } } // This part should be processed return true; } reducePartByPart(a, b) { // If the fragments mixed into `b` are a strict (proper) subset of those in `a`... if (utils_1.arrayContains(a.indexes, b.indexes)) { const newIndexes = utils_1.setDifference(a.indexes, b.indexes); const newFragment = utils_1.bufferXOR(a.fragment, b.fragment); return new FountainDecoderPart(newIndexes, newFragment); } else { // `a` is not reducable by `b`, so return a return a; } } reduceMixedBy(part) { const newMixed = []; this.mixedParts .map(({ value: mixedPart }) => this.reducePartByPart(mixedPart, part)) .forEach(reducedPart => { if (reducedPart.isSimple()) { this.queuedParts.push(reducedPart); } else { newMixed.push({ key: reducedPart.indexes, value: reducedPart }); } }); this.mixedParts = newMixed; } processSimplePart(part) { // Don't process duplicate parts const fragmentIndex = part.indexes[0]; if (this.receivedPartIndexes.includes(fragmentIndex)) { return; } this.simpleParts.push({ key: part.indexes, value: part }); this.receivedPartIndexes.push(fragmentIndex); // If we've received all the parts if (utils_1.arraysEqual(this.receivedPartIndexes, this.expectedPartIndexes)) { // Reassemble the message from its fragments const sortedParts = this.simpleParts .map(({ value }) => value) .sort((a, b) => (a.indexes[0] - b.indexes[0])); const message = FountainDecoder.joinFragments(sortedParts.map(part => part.fragment), this.expectedMessageLength); const checksum = utils_1.getCRC(message); if (checksum === this.expectedChecksum) { this.result = message; } else { // result = InvalidChecksumError(); // TODO: what do } } else { this.reduceMixedBy(part); } } processMixedPart(part) { // Don't process duplicate parts if (this.mixedParts.some(({ key: indexes }) => utils_1.arraysEqual(indexes, part.indexes))) { return; } // Reduce this part by all the others let p2 = this.simpleParts.reduce((acc, { value: p }) => this.reducePartByPart(acc, p), part); p2 = this.mixedParts.reduce((acc, { value: p }) => this.reducePartByPart(acc, p), p2); // If the part is now simple if (p2.isSimple()) { // Add it to the queue this.queuedParts.push(p2); } else { this.reduceMixedBy(p2); this.mixedParts.push({ key: p2.indexes, value: p2 }); } } processQueuedItem() { if (this.queuedParts.length === 0) { return; } const part = this.queuedParts.shift(); if (part.isSimple()) { this.processSimplePart(part); } else { this.processMixedPart(part); } } receivePart(encoderPart) { if (this.isComplete()) { return false; } if (!this.validatePart(encoderPart)) { return false; } const decoderPart = FountainDecoderPart.fromEncoderPart(encoderPart); this.lastPartIndexes = decoderPart.indexes; this.queuedParts.push(decoderPart); while (!this.isComplete() && this.queuedParts.length > 0) { this.processQueuedItem(); } ; this.processedPartsCount += 1; return true; } isComplete() { return this.result !== undefined && this.result.length > 0; } isSuccess() { return this.error === undefined && this.isComplete(); } resultMessage() { return this.isSuccess() ? this.result : Buffer.from([]); } isFailure() { return this.error !== undefined; } resultError() { return this.error ? this.error.message : ''; } expectedPartCount() { return this.expectedPartIndexes.length; } estimatedPercentComplete() { if (this.isComplete()) { return 1; } const expectedPartCount = this.expectedPartCount(); if (expectedPartCount === 0) { return 0; } return Math.min(0.99, this.processedPartsCount / (expectedPartCount * 1.75)); } getProgress() { if (this.isComplete()) { return 1; } const expectedPartCount = this.expectedPartCount(); if (expectedPartCount === 0) { return 0; } return Math.min(0.99, this.receivedPartIndexes.length / expectedPartCount); } } exports.default = FountainDecoder; FountainDecoder.joinFragments = (fragments, messageLength) => { return Buffer.concat(fragments).slice(0, messageLength); }; //# sourceMappingURL=fountainDecoder.js.map