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 const code = rs.encode(buffer, dataShards, parityShards);111 if (code !== ReedSolomonErasure.RESULT_OK) {112 throw new CodexError(113 "ERASURE_CODING_VERIFICATION_FAILED",114 `Reed-Solomon encode failed: ${ReedSolomonErasure.getResultMessage(code)}`,115 );116 }117118 119 120 const dataChunks: Uint8Array[] = [];121 for (let i = 0; i < dataShards; i++) {122 dataChunks.push(buffer.slice(i * CHUNK_SIZE, (i + 1) * CHUNK_SIZE));123 }124 const parityChunks: Uint8Array[] = [];125 for (let i = 0; i < parityShards; i++) {126 const base = (dataShards + i) * CHUNK_SIZE;127 parityChunks.push(buffer.slice(base, base + CHUNK_SIZE));128 }129130 return {131 dataChunks,132 parityChunks,133 totalChunks: totalShards,134 dataShards,135 parityShards,136 };137}138139140141142143144145146147148149150151export async function decodeWithErasure(152 chunks: Map<number, Uint8Array>,153 dataShards: number,154 totalShards: number,155): Promise<Uint8Array> {156 const parityShards = totalShards - dataShards;157 if (parityShards < 0) {158 throw new CodexError(159 "ERASURE_CODING_VERIFICATION_FAILED",160 `Invalid shard layout: dataShards=${dataShards} totalShards=${totalShards}`,161 );162 }163164 if (chunks.size < dataShards) {165 throw new CodexError(166 "INSUFFICIENT_CHUNKS",167 `Need at least ${dataShards} chunks, have ${chunks.size}`,168 );169 }170171 172 const firstChunk = chunks.values().next().value;173 if (!firstChunk || firstChunk.length === 0) {174 throw new CodexError(175 "ERASURE_CODING_VERIFICATION_FAILED",176 "First chunk is empty; cannot determine shard size",177 );178 }179 const shardSize = firstChunk.length;180181 const rs = await freshReedSolomon();182 const buffer = new Uint8Array(totalShards * shardSize);183 const available: boolean[] = new Array(totalShards).fill(false);184185 for (const [index, chunk] of chunks) {186 if (index < 0 || index >= totalShards) {187 throw new CodexError(188 "ERASURE_CODING_VERIFICATION_FAILED",189 `Chunk index ${index} out of range [0, ${totalShards})`,190 );191 }192 if (chunk.length !== shardSize) {193 throw new CodexError(194 "ERASURE_CODING_VERIFICATION_FAILED",195 `Chunk ${index} has length ${chunk.length}, expected ${shardSize}`,196 );197 }198 buffer.set(chunk, index * shardSize);199 available[index] = true;200 }201202 const code = rs.reconstruct(buffer, dataShards, parityShards, available);203 if (code !== ReedSolomonErasure.RESULT_OK) {204 205 206 207 if (code === ReedSolomonErasure.RESULT_ERROR_TOO_FEW_SHARDS_PRESENT) {208 throw new CodexError(209 "INSUFFICIENT_CHUNKS",210 `Need at least ${dataShards} valid chunks for reconstruction`,211 );212 }213 throw new CodexError(214 "ERASURE_CODING_VERIFICATION_FAILED",215 `Reed-Solomon reconstruct failed: ${ReedSolomonErasure.getResultMessage(code)}`,216 );217 }218219 return buffer.slice(0, dataShards * shardSize);220}