import { assertEquals, assertRejects } from '@std/assert'; import { ReaderError, TruncatedArchiveError } from '../lib/errors.ts'; import { concatReaders } from '../lib/reader/concat.ts'; import { fromFetch, fromFile, fromUint8Array } from '../lib/reader/common.ts'; import { concat, readBytes } from '../lib/utils/buffer.ts'; const sample = new Uint8Array(256); for (let i = 0; i < sample.length; i++) { sample[i] = i; } Deno.test('fromUint8Array: length matches input', () => { const reader = fromUint8Array(sample); assertEquals(reader.length, 256); }); Deno.test('fromUint8Array: full read returns identical bytes', async () => { const reader = fromUint8Array(sample); const got = await readBytes(reader, 0, sample.length); assertEquals(got, sample); }); Deno.test('fromUint8Array: ranged read returns the requested slice', async () => { const reader = fromUint8Array(sample); const got = await readBytes(reader, 100, 32); assertEquals(got, sample.subarray(100, 132)); }); Deno.test('fromUint8Array: read past end throws TruncatedArchiveError', async () => { const reader = fromUint8Array(sample); await assertRejects( () => readBytes(reader, 200, 100), TruncatedArchiveError, ); }); Deno.test('fromUint8Array: read with omitted length reads to end', async () => { const reader = fromUint8Array(sample); const stream = await reader.read(250); const chunks: Uint8Array[] = []; for await (const chunk of stream) { chunks.push(chunk); } const merged = new Uint8Array(chunks.reduce((n, c) => n + c.length, 0)); let pos = 0; for (const chunk of chunks) { merged.set(chunk, pos); pos += chunk.length; } assertEquals(merged, sample.subarray(250)); }); Deno.test('fromFile: matches fromUint8Array for the same payload', async () => { const blob = new Blob([sample]); const reader = fromFile(blob); assertEquals(reader.length, 256); const got = await readBytes(reader, 64, 32); assertEquals(got, sample.subarray(64, 96)); }); Deno.test('readBytes: BYOB path used when fromFile delivers a byte stream and size >= 4096', async () => { // fromFile re-emits Blob.stream() through a byte-typed stream, so it still // supports BYOB readers. readBytes prefers BYOB at size>=4096, so this // exercises the branch fromUint8Array can't reach. const big = new Uint8Array(8192); for (let i = 0; i < big.length; i++) { big[i] = (i * 7) & 0xff; } const reader = fromFile(new Blob([big])); const got = await readBytes(reader, 0, big.length); assertEquals(got, big); }); // a Blob whose slice streams deliver their bytes but never emit a final // `{ done: true }` — the Bun behaviour fromFile has to defend against. const neverClosingBlob = (payload: Uint8Array): Blob => ({ size: payload.byteLength, slice(start = 0, end = payload.byteLength) { const part = payload.subarray(start, end); return { size: part.byteLength, stream: () => new ReadableStream>({ start(controller) { if (part.byteLength > 0) { // fresh buffer per chunk, like a real Blob.stream(); a byte // stream detaches whatever it enqueues. controller.enqueue(part.slice() as Uint8Array); } // deliberately never close() }, }), }; }, }) as unknown as Blob; // drains a stream to completion, failing fast instead of hanging if it never // closes (the symptom this guards against). const collectBounded = async (stream: ReadableStream): Promise> => { let timer: ReturnType | undefined; const timeout = new Promise((_, reject) => { timer = setTimeout(() => reject(new Error('stream did not close within 5s')), 5000); }); const drain = (async () => { const chunks: Uint8Array[] = []; for await (const chunk of stream) { chunks.push(chunk); } return concat(chunks); })(); try { return await Promise.race([drain, timeout]); } finally { clearTimeout(timer); } }; Deno.test('fromFile: a non-empty range ends even when the source never closes', async () => { const payload = new Uint8Array(1000); for (let i = 0; i < payload.length; i++) { payload[i] = (i * 3) & 0xff; } const reader = fromFile(neverClosingBlob(payload)); const got = await collectBounded(await reader.read(100, 300)); assertEquals(got, payload.subarray(100, 400)); }); Deno.test('fromFile: an empty range ends with no bytes', async () => { const payload = new Uint8Array(1000); const reader = fromFile(neverClosingBlob(payload)); const got = await collectBounded(await reader.read(100, 0)); assertEquals(got.byteLength, 0); }); Deno.test('concatReaders: single reader passes through unchanged', () => { const inner = fromUint8Array(sample); const out = concatReaders([inner]); assertEquals(out, inner); }); Deno.test('concatReaders: rejects empty reader list', () => { let threw = false; try { concatReaders([]); } catch { threw = true; } assertEquals(threw, true); }); Deno.test('concatReaders: ranged read spanning a volume boundary', async () => { const left = fromUint8Array(sample.slice(0, 100) as Uint8Array); const right = fromUint8Array(sample.slice(100) as Uint8Array); const combined = concatReaders([left, right]); assertEquals(combined.length, 256); const got = await readBytes(combined, 80, 100); // crosses the 100-byte boundary assertEquals(got, sample.subarray(80, 180)); }); // #region fromFetch const serveFixture = ( bytes: Uint8Array, overrides: { headStatus?: number; headHeaders?: Record; rangeStatus?: number; rangeBody?: BodyInit | null; } = {}, ): { url: string; shutdown: () => Promise } => { const ac = new AbortController(); const server = Deno.serve( { hostname: '127.0.0.1', onListen: () => {}, port: 0, signal: ac.signal }, (req) => { if (req.method === 'HEAD') { const headers = new Headers(); const baseLength = overrides.headHeaders?.['content-length']; if (baseLength !== null) { headers.set('content-length', baseLength ?? String(bytes.byteLength)); } return new Response(null, { headers, status: overrides.headStatus ?? 200 }); } if (req.method === 'GET') { const range = req.headers.get('range'); if (range !== null) { const m = /^bytes=(\d+)-(\d+)$/.exec(range); if (m === null) { return new Response('bad range', { status: 416 }); } const start = parseInt(m[1], 10); const end = parseInt(m[2], 10); return new Response( overrides.rangeBody === undefined ? bytes.subarray(start, end + 1) as Uint8Array : overrides.rangeBody, { status: overrides.rangeStatus ?? 206 }, ); } return new Response(bytes, { status: 200 }); } return new Response(null, { status: 405 }); }, ); return { shutdown: async () => { ac.abort(); await server.finished; }, url: `http://127.0.0.1:${server.addr.port}`, }; }; Deno.test('fromFetch: happy path resolves content-length and serves ranges', async () => { const { url, shutdown } = serveFixture(sample); try { const reader = await fromFetch({ input: url }); assertEquals(reader.length, sample.length); const got = await readBytes(reader, 32, 64); assertEquals(got, sample.subarray(32, 96)); } finally { await shutdown(); } }); Deno.test('fromFetch: HEAD non-2xx throws ReaderError', async () => { const { url, shutdown } = serveFixture(sample, { headStatus: 500 }); try { await assertRejects(() => fromFetch({ input: url }), ReaderError); } finally { await shutdown(); } }); Deno.test('fromFetch: missing content-length throws ReaderError', async () => { const { url, shutdown } = serveFixture(sample, { headHeaders: { 'content-length': null } }); try { await assertRejects(() => fromFetch({ input: url }), ReaderError); } finally { await shutdown(); } }); Deno.test('fromFetch: invalid content-length throws ReaderError', async () => { const { url, shutdown } = serveFixture(sample, { headHeaders: { 'content-length': 'nope' } }); try { await assertRejects(() => fromFetch({ input: url }), ReaderError); } finally { await shutdown(); } }); Deno.test('fromFetch: range request returning 200 (no range support) throws ReaderError', async () => { const { url, shutdown } = serveFixture(sample, { rangeStatus: 200 }); try { const reader = await fromFetch({ input: url }); await assertRejects(() => readBytes(reader, 0, 16), ReaderError); } finally { await shutdown(); } }); Deno.test('fromFetch: range request non-success throws ReaderError', async () => { const { url, shutdown } = serveFixture(sample, { rangeStatus: 500 }); try { const reader = await fromFetch({ input: url }); await assertRejects(() => readBytes(reader, 0, 16), ReaderError); } finally { await shutdown(); } }); Deno.test('fromFetch: caller-supplied fetch override is honoured', async () => { let called = 0; const customFetch: typeof fetch = (input, init) => { called++; return fetch(input, init); }; const { url, shutdown } = serveFixture(sample); try { const reader = await fromFetch({ fetch: customFetch, input: url }); await readBytes(reader, 0, 16); assertEquals(called >= 2, true); // HEAD + at least one range GET } finally { await shutdown(); } }); // #endregion