12345678910111213141516import { ReedSolomonErasure } from "@digitaldefiance/reed-solomon-erasure.wasm";17import {18 CHUNK_SIZE,19 CodexError,20 computeParityShards,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 parityShards: number;93}949596979899100101export async function encodeWithErasure(data: Uint8Array): Promise<EncodeResult> {102 const rs = await freshReedSolomon();103104 const dataShards = Math.max(1, Math.ceil(data.length / CHUNK_SIZE));105 const parityShards = computeParityShards(dataShards);106 const totalShards = dataShards + parityShards;107 const buffer = new Uint8Array(totalShards * CHUNK_SIZE);108 buffer.set(data, 0); 109110 let code: number;111 try {112 code = rs.encode(buffer, dataShards, parityShards);113 } catch (err) {114 115 116 117 throw new CodexError(118 "ERASURE_CODING_VERIFICATION_FAILED",119 `Reed-Solomon encode trapped (data=${dataShards}, parity=${parityShards}): ${120 err instanceof Error ? err.message : String(err)121 }`,122 );123 }124 if (code !== ReedSolomonErasure.RESULT_OK) {125 throw new CodexError(126 "ERASURE_CODING_VERIFICATION_FAILED",127 `Reed-Solomon encode failed: ${ReedSolomonErasure.getResultMessage(code)}`,128 );129 }130131 132 133 const dataChunks: Uint8Array[] = [];134 for (let i = 0; i < dataShards; i++) {135 dataChunks.push(buffer.slice(i * CHUNK_SIZE, (i + 1) * CHUNK_SIZE));136 }137 const parityChunks: Uint8Array[] = [];138 for (let i = 0; i < parityShards; i++) {139 const base = (dataShards + i) * CHUNK_SIZE;140 parityChunks.push(buffer.slice(base, base + CHUNK_SIZE));141 }142143 return {144 dataChunks,145 parityChunks,146 totalChunks: totalShards,147 dataShards,148 parityShards,149 };150}151152153154155156157158159160161162163164export async function decodeWithErasure(165 chunks: Map<number, Uint8Array>,166 dataShards: number,167 totalShards: number,168): Promise<Uint8Array> {169 const parityShards = totalShards - dataShards;170 if (parityShards < 0) {171 throw new CodexError(172 "ERASURE_CODING_VERIFICATION_FAILED",173 `Invalid shard layout: dataShards=${dataShards} totalShards=${totalShards}`,174 );175 }176177 if (chunks.size < dataShards) {178 throw new CodexError(179 "INSUFFICIENT_CHUNKS",180 `Need at least ${dataShards} chunks, have ${chunks.size}`,181 );182 }183184 185 const firstChunk = chunks.values().next().value;186 if (!firstChunk || firstChunk.length === 0) {187 throw new CodexError(188 "ERASURE_CODING_VERIFICATION_FAILED",189 "First chunk is empty; cannot determine shard size",190 );191 }192 const shardSize = firstChunk.length;193194 const rs = await freshReedSolomon();195 const buffer = new Uint8Array(totalShards * shardSize);196 const available: boolean[] = new Array(totalShards).fill(false);197198 for (const [index, chunk] of chunks) {199 if (index < 0 || index >= totalShards) {200 throw new CodexError(201 "ERASURE_CODING_VERIFICATION_FAILED",202 `Chunk index ${index} out of range [0, ${totalShards})`,203 );204 }205 if (chunk.length !== shardSize) {206 throw new CodexError(207 "ERASURE_CODING_VERIFICATION_FAILED",208 `Chunk ${index} has length ${chunk.length}, expected ${shardSize}`,209 );210 }211 buffer.set(chunk, index * shardSize);212 available[index] = true;213 }214215 let code: number;216 try {217 code = rs.reconstruct(buffer, dataShards, parityShards, available);218 } catch (err) {219 throw new CodexError(220 "ERASURE_CODING_VERIFICATION_FAILED",221 `Reed-Solomon reconstruct trapped (data=${dataShards}, parity=${parityShards}, have=${chunks.size}): ${222 err instanceof Error ? err.message : String(err)223 }`,224 );225 }226 if (code !== ReedSolomonErasure.RESULT_OK) {227 228 229 230 if (code === ReedSolomonErasure.RESULT_ERROR_TOO_FEW_SHARDS_PRESENT) {231 throw new CodexError(232 "INSUFFICIENT_CHUNKS",233 `Need at least ${dataShards} valid chunks for reconstruction`,234 );235 }236 throw new CodexError(237 "ERASURE_CODING_VERIFICATION_FAILED",238 `Reed-Solomon reconstruct failed: ${ReedSolomonErasure.getResultMessage(code)}`,239 );240 }241242 return buffer.slice(0, dataShards * shardSize);243}