signal-desktop/ts/util/logPadding.ts
2024-04-15 22:54:21 +02:00

88 lines
2.2 KiB
TypeScript

// Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { Transform } from 'stream';
import type { Duplex, Readable } from 'stream';
const PADDING_CHUNK_SIZE = 64 * 1024;
export function logPadSize(size: number): number {
return Math.max(
541,
Math.floor(1.05 ** Math.ceil(Math.log(size) / Math.log(1.05)))
);
}
/**
* Creates iterator that yields zero-filled padding chunks.
*/
function* generatePadding(size: number) {
const targetLength = logPadSize(size);
const paddingSize = targetLength - size;
const paddingChunks = Math.floor(paddingSize / PADDING_CHUNK_SIZE);
const paddingChunk = new Uint8Array(PADDING_CHUNK_SIZE); // zero-filled
for (let i = 0; i < paddingChunks; i += 1) {
yield paddingChunk;
}
const paddingRemainder = new Uint8Array(paddingSize % PADDING_CHUNK_SIZE);
if (paddingRemainder.byteLength > 0) {
yield paddingRemainder;
}
}
// Push as much padding as we can. If we reach the end
// of the padding, return true.
function pushPadding(
paddingIterator: Iterator<Uint8Array>,
readable: Readable
): boolean {
// eslint-disable-next-line no-constant-condition
while (true) {
const result = paddingIterator.next();
if (result.done) {
break;
}
const keepGoing = readable.push(result.value);
if (!keepGoing) {
return false;
}
}
return true;
}
/**
* Appends zero-padding to the stream to a target bucket size.
*/
export function appendPaddingStream(): Duplex {
let onReadableDrained: undefined | (() => void);
let fileSize = 0;
return new Transform({
read(size) {
// When in the process of pushing padding, we pause and wait for
// read to be called again.
if (onReadableDrained != null) {
onReadableDrained();
}
// Always call _read, even if we're done.
Transform.prototype._read.call(this, size);
},
transform(chunk, _encoding, callback) {
fileSize += chunk.byteLength;
callback(null, chunk);
},
flush(callback) {
const iterator = generatePadding(fileSize);
onReadableDrained = () => {
if (!pushPadding(iterator, this)) {
return;
}
callback();
};
onReadableDrained();
},
});
}