12345678910111213141516import {17 CHUNK_SIZE,18 CodexError,19 MAX_DATA_SHARDS_PER_STRIPE,20 computeParityShards,21} from "../../shared/codex-types";22import { ReedSolomonErasure } from "./reed-solomon";23import { getReedSolomon } from "./rs-loader";242526272829303132333435let cachedWasmBytes: Uint8Array<ArrayBuffer> | null = null;3637async function freshReedSolomon(): Promise<ReedSolomonErasure> {38 if (cachedWasmBytes) {39 return ReedSolomonErasure.fromBytes(cachedWasmBytes);40 }41 42 43 44 45 await getReedSolomon();46 if (!cachedWasmBytes) {47 cachedWasmBytes = await loadWasmBytes();48 }49 return ReedSolomonErasure.fromBytes(cachedWasmBytes);50}515253545556575859async function loadWasmBytes(): Promise<Uint8Array<ArrayBuffer>> {60 const inPopup =61 typeof chrome !== "undefined" &&62 typeof chrome.runtime !== "undefined" &&63 typeof chrome.runtime.getURL === "function";64 if (inPopup) {65 const url = chrome.runtime.getURL("codex/reed_solomon_erasure_bg.wasm");66 const response = await fetch(url);67 if (!response.ok) {68 throw new CodexError(69 "ERASURE_CODING_VERIFICATION_FAILED",70 `Failed to fetch WASM: ${response.status}`,71 );72 }73 return new Uint8Array(await response.arrayBuffer());74 }75 return readTestWasmBytes();76}7778async function readTestWasmBytes(): Promise<Uint8Array<ArrayBuffer>> {79 if (import.meta.env.MODE !== "test") {80 throw new CodexError(81 "ERASURE_CODING_VERIFICATION_FAILED",82 "Reed-Solomon WASM can only be loaded through chrome.runtime.getURL outside tests",83 );84 }85 const { readNodeReedSolomonWasmBytes } = await import("./node-wasm");86 return readNodeReedSolomonWasmBytes();87}88899091929394function resultMessage(code: number): string {95 switch (code) {96 case ReedSolomonErasure.RESULT_OK: return "ok";97 case ReedSolomonErasure.RESULT_ERROR_TOO_FEW_SHARDS: return "too few shards";98 case ReedSolomonErasure.RESULT_ERROR_TOO_MANY_SHARDS: return "too many shards";99 case ReedSolomonErasure.RESULT_ERROR_TOO_FEW_DATA_SHARDS: return "too few data shards";100 case ReedSolomonErasure.RESULT_ERROR_TOO_MANY_DATA_SHARDS: return "too many data shards";101 case ReedSolomonErasure.RESULT_ERROR_TOO_FEW_PARITY_SHARDS: return "too few parity shards";102 case ReedSolomonErasure.RESULT_ERROR_TOO_MANY_PARITY_SHARDS: return "too many parity shards";103 case ReedSolomonErasure.RESULT_ERROR_TOO_FEW_BUFFER_SHARDS: return "too few buffer shards";104 case ReedSolomonErasure.RESULT_ERROR_TOO_MANY_BUFFER_SHARDS: return "too many buffer shards";105 case ReedSolomonErasure.RESULT_ERROR_INCORRECT_SHARD_SIZE: return "incorrect shard size";106 case ReedSolomonErasure.RESULT_ERROR_TOO_FEW_SHARDS_PRESENT: return "too few shards present";107 case ReedSolomonErasure.RESULT_ERROR_EMPTY_SHARD: return "empty shard";108 case ReedSolomonErasure.RESULT_ERROR_INVALID_SHARD_FLAGS: return "invalid shard flags";109 case ReedSolomonErasure.RESULT_ERROR_INVALID_INDEX: return "invalid index";110 default: return `unknown result code ${code}`;111 }112}113114export interface EncodeResult {115 dataChunks: Uint8Array[];116 parityChunks: Uint8Array[];117 totalChunks: number;118 dataShards: number;119 parityShards: number;120}121122export interface MultiStripeEncodeResult {123 stripes: EncodeResult[];124 125 totalDataChunks: number;126 127 totalParityChunks: number;128 129 totalChunks: number;130}131132133134135136137138export function planStripeShardCounts(totalDataShards: number): number[] {139 if (totalDataShards <= 0) return [];140 const numStripes = Math.max(141 1,142 Math.ceil(totalDataShards / MAX_DATA_SHARDS_PER_STRIPE),143 );144 const base = Math.floor(totalDataShards / numStripes);145 const remainder = totalDataShards % numStripes;146 const counts: number[] = [];147 for (let i = 0; i < numStripes; i++) {148 counts.push(base + (i < remainder ? 1 : 0));149 }150 return counts;151}152153154155156157158159160161162163export async function encodeWithErasure(data: Uint8Array): Promise<EncodeResult> {164 const rs = await freshReedSolomon();165166 const dataShards = Math.max(1, Math.ceil(data.length / CHUNK_SIZE));167 const parityShards = computeParityShards(dataShards);168 const totalShards = dataShards + parityShards;169 if (totalShards > 256) {170 throw new CodexError(171 "ERASURE_CODING_VERIFICATION_FAILED",172 `Single RS stripe cannot exceed 256 shards (got ${totalShards}; ${data.length} bytes / ${CHUNK_SIZE}-byte chunks). Use encodeIntoStripes for large payloads.`,173 );174 }175 const buffer = new Uint8Array(totalShards * CHUNK_SIZE);176 buffer.set(data, 0); 177178 let code: number;179 try {180 code = rs.encode(buffer, dataShards, parityShards);181 } catch (err) {182 183 184 185 throw new CodexError(186 "ERASURE_CODING_VERIFICATION_FAILED",187 `Reed-Solomon encode trapped (data=${dataShards}, parity=${parityShards}): ${188 err instanceof Error ? err.message : String(err)189 }`,190 );191 }192 if (code !== ReedSolomonErasure.RESULT_OK) {193 throw new CodexError(194 "ERASURE_CODING_VERIFICATION_FAILED",195 `Reed-Solomon encode failed: ${resultMessage(code)}`,196 );197 }198199 200 201 const dataChunks: Uint8Array[] = [];202 for (let i = 0; i < dataShards; i++) {203 dataChunks.push(buffer.slice(i * CHUNK_SIZE, (i + 1) * CHUNK_SIZE));204 }205 const parityChunks: Uint8Array[] = [];206 for (let i = 0; i < parityShards; i++) {207 const base = (dataShards + i) * CHUNK_SIZE;208 parityChunks.push(buffer.slice(base, base + CHUNK_SIZE));209 }210211 return {212 dataChunks,213 parityChunks,214 totalChunks: totalShards,215 dataShards,216 parityShards,217 };218}219220221222223224225226227228229230231232export async function decodeWithErasure(233 chunks: Map<number, Uint8Array>,234 dataShards: number,235 totalShards: number,236): Promise<Uint8Array> {237 const parityShards = totalShards - dataShards;238 if (parityShards < 0) {239 throw new CodexError(240 "ERASURE_CODING_VERIFICATION_FAILED",241 `Invalid shard layout: dataShards=${dataShards} totalShards=${totalShards}`,242 );243 }244245 if (chunks.size < dataShards) {246 throw new CodexError(247 "INSUFFICIENT_CHUNKS",248 `Need at least ${dataShards} chunks, have ${chunks.size}`,249 );250 }251252 253 const firstChunk = chunks.values().next().value;254 if (!firstChunk || firstChunk.length === 0) {255 throw new CodexError(256 "ERASURE_CODING_VERIFICATION_FAILED",257 "First chunk is empty; cannot determine shard size",258 );259 }260 const shardSize = firstChunk.length;261262 const rs = await freshReedSolomon();263 const buffer = new Uint8Array(totalShards * shardSize);264 const available: boolean[] = new Array(totalShards).fill(false);265266 for (const [index, chunk] of chunks) {267 if (index < 0 || index >= totalShards) {268 throw new CodexError(269 "ERASURE_CODING_VERIFICATION_FAILED",270 `Chunk index ${index} out of range [0, ${totalShards})`,271 );272 }273 if (chunk.length !== shardSize) {274 throw new CodexError(275 "ERASURE_CODING_VERIFICATION_FAILED",276 `Chunk ${index} has length ${chunk.length}, expected ${shardSize}`,277 );278 }279 buffer.set(chunk, index * shardSize);280 available[index] = true;281 }282283 let code: number;284 try {285 code = rs.reconstruct(buffer, dataShards, parityShards, available);286 } catch (err) {287 throw new CodexError(288 "ERASURE_CODING_VERIFICATION_FAILED",289 `Reed-Solomon reconstruct trapped (data=${dataShards}, parity=${parityShards}, have=${chunks.size}): ${290 err instanceof Error ? err.message : String(err)291 }`,292 );293 }294 if (code !== ReedSolomonErasure.RESULT_OK) {295 296 297 298 if (code === ReedSolomonErasure.RESULT_ERROR_TOO_FEW_SHARDS_PRESENT) {299 throw new CodexError(300 "INSUFFICIENT_CHUNKS",301 `Need at least ${dataShards} valid chunks for reconstruction`,302 );303 }304 throw new CodexError(305 "ERASURE_CODING_VERIFICATION_FAILED",306 `Reed-Solomon reconstruct failed: ${resultMessage(code)}`,307 );308 }309310 return buffer.slice(0, dataShards * shardSize);311}312313314315316317318319320321322export async function encodeIntoStripes(323 data: Uint8Array,324): Promise<MultiStripeEncodeResult> {325 const totalDataShards = Math.max(1, Math.ceil(data.length / CHUNK_SIZE));326 const stripeShardCounts = planStripeShardCounts(totalDataShards);327328 const stripes: EncodeResult[] = [];329 let byteCursor = 0;330 for (const shardCount of stripeShardCounts) {331 const stripeByteCap = shardCount * CHUNK_SIZE;332 const stripeEnd = Math.min(byteCursor + stripeByteCap, data.length);333 const stripeBytes = data.subarray(byteCursor, stripeEnd);334 const result = await encodeWithErasure(stripeBytes);335 336 337 338 339 340 341 if (result.dataShards !== shardCount) {342 throw new CodexError(343 "ERASURE_CODING_VERIFICATION_FAILED",344 `Stripe plan mismatch: planned ${shardCount} data shards, encoder produced ${result.dataShards}`,345 );346 }347 stripes.push(result);348 byteCursor += stripeByteCap;349 }350351 const totalDataChunks = stripes.reduce((s, x) => s + x.dataShards, 0);352 const totalParityChunks = stripes.reduce((s, x) => s + x.parityShards, 0);353 const totalChunks = totalDataChunks + totalParityChunks;354 return { stripes, totalDataChunks, totalParityChunks, totalChunks };355}