qrloop
Version:
Envelop big blob of data into frames that can be displayed in series of QR Codes
137 lines (129 loc) • 3.88 kB
text/typescript
import md5 from "md5";
import { Buffer } from "buffer";
import { cutAndPad, xor } from "./Buffer";
import { MAX_NONCE, FOUNTAIN_V1 } from "./constants";
export function makeFountainFrame(
dataChunks: Buffer[],
selectedFrameIndexes: number[]
): string {
const k = selectedFrameIndexes.length;
const head = Buffer.alloc(3 + 2 * k);
head.writeUInt8(FOUNTAIN_V1, 0);
head.writeUInt16BE(k, 1);
const selectedFramesData = [];
for (let j = 0; j < k; j++) {
const frameIndex = selectedFrameIndexes[j];
selectedFramesData.push(dataChunks[frameIndex]);
head.writeUInt16BE(frameIndex, 3 + 2 * j);
}
const data = xor(selectedFramesData);
return Buffer.concat([head, data]).toString("base64");
}
export function makeDataFrame({
data,
nonce,
totalFrames,
frameIndex,
}: {
data: Buffer;
nonce: number;
totalFrames: number;
frameIndex: number;
}): string {
const head = Buffer.alloc(5);
head.writeUInt8(nonce, 0);
head.writeUInt16BE(totalFrames, 1);
head.writeUInt16BE(frameIndex, 3);
return Buffer.concat([head, data]).toString("base64");
}
export function wrapData(data: Buffer): Buffer {
const lengthBuffer = Buffer.alloc(4);
lengthBuffer.writeUInt32BE(data.length, 0);
const md5Buffer = Buffer.from(md5(data), "hex");
return Buffer.concat([lengthBuffer, md5Buffer, data]);
}
/**
* in one loop:
* the data is prepend in the frames with this head:
* 4 bytes: uint, data length
* 16 bytes: md5 of data
*
* each frame is a base64 of:
* 1 byte: nonce
* 2 bytes: uint, total number of frames
* 2 bytes: uint, index of frame
* variable data
*
* each "fountain" frame is base64 of:
* 1 byte: fountain version
* 2 bytes: number of K frames associated
* K times 2 bytes: the index of each frame
* variable data: the XOR of the frames data
*
* It inspires idea from https://en.wikipedia.org/wiki/Luby_transform_code
*/
function makeLoop(
wrappedData: Buffer,
dataSize: number,
index: number,
random: () => number
): string[] {
const nonce = index % MAX_NONCE;
const dataChunks = cutAndPad(wrappedData, dataSize);
const fountains = [];
if (dataChunks.length > 2) {
// TODO optimal number fcount and k still need to be determined
const fcount = Math.floor(dataChunks.length / 6);
const k = Math.ceil(dataChunks.length / 2);
for (let i = 0; i < fcount; i++) {
const distribution = Array(dataChunks.length)
.fill(null)
.map((_, i) => ({ i, n: random() }))
.sort((a, b) => a.n - b.n)
.slice(0, k)
.map((o) => o.i);
fountains.push(makeFountainFrame(dataChunks, distribution));
}
}
const result = [];
let j = 0;
const fountainEach = Math.floor(dataChunks.length / fountains.length);
for (let i = 0; i < dataChunks.length; i++) {
result.push(
makeDataFrame({
data: dataChunks[i],
nonce,
totalFrames: dataChunks.length,
frameIndex: i,
})
);
if (i % fountainEach === 0 && fountains[j]) {
result.push(fountains[j++]);
}
}
return result;
}
/**
* Export data into one series of chunk of string that you can generate a QR with
* @param dataOrStr the complete data to encode in a series of QR code frames
* @param dataSize the number of bytes to use from data for each frame
* @param loops number of loops to generate. more loops increase chance for readers to read frames
*/
export function dataToFrames(
dataOrStr: Buffer | string,
dataSize: number = 120,
loops: number = 1
): string[] {
// Simple deterministic RNG
let seed = 1;
function random() {
let x = Math.sin(seed++) * 10000;
return x - Math.floor(x);
}
const wrappedData = wrapData(Buffer.from(dataOrStr));
let r: string[] = [];
for (let i = 0; i < loops; i++) {
r = r.concat(makeLoop(wrappedData, dataSize, i, random));
}
return r;
}