SHA256
Доработать вложенные файлы и архив проекта
This commit is contained in:
@@ -0,0 +1,234 @@
|
||||
import { bytesToBase64Url, sha256Bytes } from './crypto-utils.js';
|
||||
|
||||
export const TORRENT_V2_BLOCK_BYTES = 16 * 1024;
|
||||
export const TORRENT_V2_PIECE_BYTES = 1024 * 1024;
|
||||
|
||||
const ZERO_HASH = new Uint8Array(32);
|
||||
const encoder = new TextEncoder();
|
||||
|
||||
function concatBytes(parts = []) {
|
||||
const arrays = parts.map((part) => part instanceof Uint8Array ? part : new Uint8Array(part || 0));
|
||||
const total = arrays.reduce((sum, part) => sum + part.byteLength, 0);
|
||||
const out = new Uint8Array(total);
|
||||
let offset = 0;
|
||||
for (const part of arrays) {
|
||||
out.set(part, offset);
|
||||
offset += part.byteLength;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
async function hashPair(left, right) {
|
||||
return sha256Bytes(concatBytes([left, right]));
|
||||
}
|
||||
|
||||
function nextPowerOfTwo(value) {
|
||||
let n = 1;
|
||||
while (n < value) n *= 2;
|
||||
return n;
|
||||
}
|
||||
|
||||
async function reduceMerkleLevel(nodes) {
|
||||
if (nodes.length <= 1) return nodes;
|
||||
const next = [];
|
||||
for (let i = 0; i < nodes.length; i += 2) {
|
||||
next.push(hashPair(nodes[i], nodes[i + 1]));
|
||||
}
|
||||
return Promise.all(next);
|
||||
}
|
||||
|
||||
/**
|
||||
* BEP 52 pieces root / piece-layer hash for one piece.
|
||||
* Leaves are SHA-256 hashes of 16 KiB blocks. Padding leaves are 32 zero bytes,
|
||||
* exactly as required by BitTorrent v2.
|
||||
*/
|
||||
export async function computeTorrentV2PieceRoot(pieceBytes, { padToPieceLength = false } = {}) {
|
||||
const bytes = pieceBytes instanceof Uint8Array ? pieceBytes : new Uint8Array(pieceBytes || 0);
|
||||
if (!bytes.byteLength) return null;
|
||||
|
||||
const blockCount = Math.ceil(bytes.byteLength / TORRENT_V2_BLOCK_BYTES);
|
||||
const leafPromises = [];
|
||||
for (let offset = 0; offset < bytes.byteLength; offset += TORRENT_V2_BLOCK_BYTES) {
|
||||
leafPromises.push(sha256Bytes(bytes.subarray(offset, Math.min(bytes.byteLength, offset + TORRENT_V2_BLOCK_BYTES))));
|
||||
}
|
||||
let level = await Promise.all(leafPromises);
|
||||
const targetLeaves = padToPieceLength
|
||||
? (TORRENT_V2_PIECE_BYTES / TORRENT_V2_BLOCK_BYTES)
|
||||
: nextPowerOfTwo(Math.max(1, blockCount));
|
||||
while (level.length < targetLeaves) level.push(ZERO_HASH.slice());
|
||||
while (level.length > 1) level = await reduceMerkleLevel(level);
|
||||
return level[0];
|
||||
}
|
||||
|
||||
let zeroPieceRootPromise = null;
|
||||
export function getTorrentV2ZeroPieceRoot() {
|
||||
if (!zeroPieceRootPromise) {
|
||||
zeroPieceRootPromise = (async () => {
|
||||
let level = Array.from(
|
||||
{ length: TORRENT_V2_PIECE_BYTES / TORRENT_V2_BLOCK_BYTES },
|
||||
() => ZERO_HASH.slice(),
|
||||
);
|
||||
while (level.length > 1) level = await reduceMerkleLevel(level);
|
||||
return level[0];
|
||||
})();
|
||||
}
|
||||
return zeroPieceRootPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Streaming Merkle accumulator for the BEP 52 piece layer. It keeps only O(log n)
|
||||
* hashes in memory and pads the right side with the standard zero-piece subtree.
|
||||
*/
|
||||
export class TorrentV2PieceAccumulator {
|
||||
constructor() {
|
||||
this.stack = [];
|
||||
this.count = 0;
|
||||
}
|
||||
|
||||
async #addSubtree(hash, level) {
|
||||
let current = hash;
|
||||
let currentLevel = level;
|
||||
while (this.stack[currentLevel]) {
|
||||
current = await hashPair(this.stack[currentLevel], current);
|
||||
this.stack[currentLevel] = null;
|
||||
currentLevel += 1;
|
||||
}
|
||||
this.stack[currentLevel] = current;
|
||||
}
|
||||
|
||||
async addPieceRoot(hash) {
|
||||
if (!(hash instanceof Uint8Array) || hash.byteLength !== 32) {
|
||||
throw new Error('Некорректный torrent-v2 piece hash');
|
||||
}
|
||||
await this.#addSubtree(hash, 0);
|
||||
this.count += 1;
|
||||
}
|
||||
|
||||
async finalize() {
|
||||
if (this.count <= 0) return null;
|
||||
let target = 1;
|
||||
while (target < this.count) target *= 2;
|
||||
let remaining = target - this.count;
|
||||
let count = this.count;
|
||||
const zeroRoots = [await getTorrentV2ZeroPieceRoot()];
|
||||
const ensureZeroLevel = async (level) => {
|
||||
while (zeroRoots.length <= level) {
|
||||
const previous = zeroRoots[zeroRoots.length - 1];
|
||||
zeroRoots.push(await hashPair(previous, previous));
|
||||
}
|
||||
return zeroRoots[level];
|
||||
};
|
||||
|
||||
while (remaining > 0) {
|
||||
let maxByRemaining = Math.floor(Math.log2(remaining));
|
||||
let alignmentLevel = 0;
|
||||
let aligned = count;
|
||||
while (aligned > 0 && aligned % 2 === 0) {
|
||||
alignmentLevel += 1;
|
||||
aligned /= 2;
|
||||
}
|
||||
const level = Math.min(maxByRemaining, alignmentLevel);
|
||||
const blockLeaves = 2 ** level;
|
||||
await this.#addSubtree(await ensureZeroLevel(level), level);
|
||||
count += blockLeaves;
|
||||
remaining -= blockLeaves;
|
||||
}
|
||||
|
||||
const root = this.stack.findLast?.((value) => value) || [...this.stack].reverse().find((value) => value) || null;
|
||||
return root ? root.slice() : null;
|
||||
}
|
||||
}
|
||||
|
||||
function encodeBString(bytes) {
|
||||
const data = bytes instanceof Uint8Array ? bytes : encoder.encode(String(bytes ?? ''));
|
||||
return concatBytes([encoder.encode(`${data.byteLength}:`), data]);
|
||||
}
|
||||
|
||||
function compareByteArrays(left, right) {
|
||||
const limit = Math.min(left.byteLength, right.byteLength);
|
||||
for (let i = 0; i < limit; i += 1) {
|
||||
if (left[i] !== right[i]) return left[i] - right[i];
|
||||
}
|
||||
return left.byteLength - right.byteLength;
|
||||
}
|
||||
|
||||
function encodeBValue(value) {
|
||||
if (value instanceof Uint8Array) return encodeBString(value);
|
||||
if (typeof value === 'string') return encodeBString(encoder.encode(value));
|
||||
if (typeof value === 'number' || typeof value === 'bigint') {
|
||||
const integer = typeof value === 'bigint' ? value : BigInt(Math.trunc(value));
|
||||
return encoder.encode(`i${integer.toString()}e`);
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
return concatBytes([encoder.encode('l'), ...value.map(encodeBValue), encoder.encode('e')]);
|
||||
}
|
||||
if (value && typeof value === 'object') {
|
||||
const entries = Object.entries(value).map(([key, item]) => ({
|
||||
keyBytes: encoder.encode(key),
|
||||
value: item,
|
||||
})).sort((a, b) => compareByteArrays(a.keyBytes, b.keyBytes));
|
||||
const parts = [encoder.encode('d')];
|
||||
for (const entry of entries) {
|
||||
parts.push(encodeBString(entry.keyBytes), encodeBValue(entry.value));
|
||||
}
|
||||
parts.push(encoder.encode('e'));
|
||||
return concatBytes(parts);
|
||||
}
|
||||
throw new Error('Неподдерживаемое значение bencode');
|
||||
}
|
||||
|
||||
export function buildTorrentV2InfoBytes({ name, size, piecesRoot, pieceLength = TORRENT_V2_PIECE_BYTES } = {}) {
|
||||
const cleanName = String(name || 'file');
|
||||
const fileData = { length: Math.max(0, Math.trunc(Number(size || 0))) };
|
||||
if (fileData.length > 0) {
|
||||
if (!(piecesRoot instanceof Uint8Array) || piecesRoot.byteLength !== 32) {
|
||||
throw new Error('Для непустого файла нужен 32-байтный pieces root');
|
||||
}
|
||||
fileData['pieces root'] = piecesRoot;
|
||||
}
|
||||
return encodeBValue({
|
||||
'file tree': {
|
||||
[cleanName]: {
|
||||
'': fileData,
|
||||
},
|
||||
},
|
||||
'meta version': 2,
|
||||
name: cleanName,
|
||||
'piece length': Math.trunc(pieceLength),
|
||||
});
|
||||
}
|
||||
|
||||
export async function computeTorrentV2InfoHash({ name, size, piecesRoot } = {}) {
|
||||
const infoBytes = buildTorrentV2InfoBytes({ name, size, piecesRoot });
|
||||
const hash = await sha256Bytes(infoBytes);
|
||||
return {
|
||||
infoBytes,
|
||||
infoHash: hash,
|
||||
infoHashB64Url: bytesToBase64Url(hash),
|
||||
};
|
||||
}
|
||||
|
||||
/** Builds a tracker-less BitTorrent v2 metainfo file. */
|
||||
export function buildTorrentV2MetainfoBytes({ name, size, piecesRoot, pieceLayer = [] } = {}) {
|
||||
const infoBytes = buildTorrentV2InfoBytes({ name, size, piecesRoot });
|
||||
const outer = [encoder.encode('d'), encodeBString(encoder.encode('info')), infoBytes];
|
||||
if (Number(size || 0) > TORRENT_V2_PIECE_BYTES) {
|
||||
if (!(piecesRoot instanceof Uint8Array) || piecesRoot.byteLength !== 32) {
|
||||
throw new Error('Некорректный pieces root');
|
||||
}
|
||||
const hashes = (Array.isArray(pieceLayer) ? pieceLayer : []).map((hash) => {
|
||||
if (!(hash instanceof Uint8Array) || hash.byteLength !== 32) throw new Error('Некорректный piece layer');
|
||||
return hash;
|
||||
});
|
||||
const layerBytes = concatBytes(hashes);
|
||||
outer.push(
|
||||
encodeBString(encoder.encode('piece layers')),
|
||||
encoder.encode('d'),
|
||||
encodeBString(piecesRoot),
|
||||
encodeBString(layerBytes),
|
||||
encoder.encode('e'),
|
||||
);
|
||||
}
|
||||
outer.push(encoder.encode('e'));
|
||||
return concatBytes(outer);
|
||||
}
|
||||
Reference in New Issue
Block a user