12345678910111213141516import { ReedSolomonErasure } from "@digitaldefiance/reed-solomon-erasure.wasm";17import {18 CHUNK_SIZE,19 CodexError,20 PARITY_SHARDS,21} from "../../shared/codex-types";22import { getReedSolomon } from "./rs-loader";232425262728293031323334353637let cachedWasmBytes: Uint8Array | null = null;3839async function freshReedSolomon(): Promise<ReedSolomonErasure> {40 if (cachedWasmBytes) {41 return ReedSolomonErasure.fromBytes(cachedWasmBytes);42 }43 44 45 46 47 await getReedSolomon();48 if (!cachedWasmBytes) {49 cachedWasmBytes = await loadWasmBytes();50 }51 return ReedSolomonErasure.fromBytes(cachedWasmBytes);52}5354555657585960616263async function loadWasmBytes(): Promise<Uint8Array> {64 const inPopup =65 typeof chrome !== "undefined" &&66 typeof chrome.runtime !== "undefined" &&67 typeof chrome.runtime.getURL === "function";68 if (inPopup) {69 const url = chrome.runtime.getURL("codex/reed_solomon_erasure_bg.wasm");70 const response = await fetch(url);71 if (!response.ok) {72 throw new CodexError(73 "ERASURE_CODING_VERIFICATION_FAILED",74 `Failed to fetch WASM: ${response.status}`,75 );76 }77 return new Uint8Array(await response.arrayBuffer());78 }79 const fs = await import("node:fs");80 const path = await import("node:path");81 const wasmPath = path.resolve(82 "node_modules/@digitaldefiance/reed-solomon-erasure.wasm/dist/reed_solomon_erasure_bg.wasm",83 );84 return new Uint8Array(fs.readFileSync(wasmPath));85}8687export interface EncodeResult {88 dataChunks: Uint8Array[];89 parityChunks: Uint8Array[];90 totalChunks: number;91 dataShards: number;92}93949596979899100export async function encodeWithErasure(data: Uint8Array): Promise<EncodeResult> {101 const rs = await freshReedSolomon();102103 const dataShards = Math.max(1, Math.ceil(data.length / CHUNK_SIZE));104 const totalShards = dataShards + PARITY_SHARDS;105 const buffer = new Uint8Array(totalShards * CHUNK_SIZE);106 buffer.set(data, 0); 107108 const code = rs.encode(buffer, dataShards, PARITY_SHARDS);109 if (code !== ReedSolomonErasure.RESULT_OK) {110 throw new CodexError(111 "ERASURE_CODING_VERIFICATION_FAILED",112 `Reed-Solomon encode failed: ${ReedSolomonErasure.getResultMessage(code)}`,113 );114 }115116 117 118 const dataChunks: Uint8Array[] = [];119 for (let i = 0; i < dataShards; i++) {120 dataChunks.push(buffer.slice(i * CHUNK_SIZE, (i + 1) * CHUNK_SIZE));121 }122 const parityChunks: Uint8Array[] = [];123 for (let i = 0; i < PARITY_SHARDS; i++) {124 const base = (dataShards + i) * CHUNK_SIZE;125 parityChunks.push(buffer.slice(base, base + CHUNK_SIZE));126 }127128 return { dataChunks, parityChunks, totalChunks: totalShards, dataShards };129}130131132133134135136137138139140141142143export async function decodeWithErasure(144 chunks: Map<number, Uint8Array>,145 dataShards: number,146 totalShards: number,147): Promise<Uint8Array> {148 const parityShards = totalShards - dataShards;149 if (parityShards < 0) {150 throw new CodexError(151 "ERASURE_CODING_VERIFICATION_FAILED",152 `Invalid shard layout: dataShards=${dataShards} totalShards=${totalShards}`,153 );154 }155156 if (chunks.size < dataShards) {157 throw new CodexError(158 "INSUFFICIENT_CHUNKS",159 `Need at least ${dataShards} chunks, have ${chunks.size}`,160 );161 }162163 164 const firstChunk = chunks.values().next().value;165 if (!firstChunk || firstChunk.length === 0) {166 throw new CodexError(167 "ERASURE_CODING_VERIFICATION_FAILED",168 "First chunk is empty; cannot determine shard size",169 );170 }171 const shardSize = firstChunk.length;172173 const rs = await freshReedSolomon();174 const buffer = new Uint8Array(totalShards * shardSize);175 const available: boolean[] = new Array(totalShards).fill(false);176177 for (const [index, chunk] of chunks) {178 if (index < 0 || index >= totalShards) {179 throw new CodexError(180 "ERASURE_CODING_VERIFICATION_FAILED",181 `Chunk index ${index} out of range [0, ${totalShards})`,182 );183 }184 if (chunk.length !== shardSize) {185 throw new CodexError(186 "ERASURE_CODING_VERIFICATION_FAILED",187 `Chunk ${index} has length ${chunk.length}, expected ${shardSize}`,188 );189 }190 buffer.set(chunk, index * shardSize);191 available[index] = true;192 }193194 const code = rs.reconstruct(buffer, dataShards, parityShards, available);195 if (code !== ReedSolomonErasure.RESULT_OK) {196 197 198 199 if (code === ReedSolomonErasure.RESULT_ERROR_TOO_FEW_SHARDS_PRESENT) {200 throw new CodexError(201 "INSUFFICIENT_CHUNKS",202 `Need at least ${dataShards} valid chunks for reconstruction`,203 );204 }205 throw new CodexError(206 "ERASURE_CODING_VERIFICATION_FAILED",207 `Reed-Solomon reconstruct failed: ${ReedSolomonErasure.getResultMessage(code)}`,208 );209 }210211 return buffer.slice(0, dataShards * shardSize);212}