/** * The `Blob`/`File` adapter. * * Layer 5. The DOM `Blob` is never named: `BlobLike` is the structural shim from `types.ts`, so * a real `File` from an `` remains assignable while `lib: ["DOM"]` stays out * of the published `.d.ts`. * * A `Blob` read is the one place where the platform can legitimately hand back fewer bytes than * asked (a `File` whose backing file changed on disk since the picker ran), so the exact-length * contract is verified rather than assumed. */ import { EdfSourceError } from '../errors.js'; import { describeValue } from '../text/describe.js'; import type { BlobLike, ByteSource, ReadOptions } from '../types.js'; import { assertExactRead, assertReadRange, throwIfAborted } from './source.js'; /** * A `ByteSource` over a `Blob` or `File` — the browser entry point, and what an * `` hands you. Reads are ranged, so opening a file the user picked costs the * header rather than the recording. */ export function blobSource(blob: BlobLike): ByteSource { /* * Checked structurally, and at construction, for both of the reasons `byteSource` gives: a * `BlobLike` is an interface a caller may implement, and a source built over something that is * not a blob surfaces later as `[SOURCE_TOO_SMALL] the header is 0 bytes` — blaming the FILE for * a mistake in the argument. Until now it did not even get that far: `blob.size` on an omitted * argument was V8's `Cannot read properties of undefined (reading 'size')` (fixed in 0.6.102). */ const given = blob as BlobLike | null | undefined; if (typeof given?.size !== 'number' || typeof given.slice !== 'function') { throw new EdfSourceError( 'blobSource() needs a Blob or a File — an object with a size and a slice() — and received ' + `${describeValue(blob)}. Next: pass the File an or a drop event hands ` + 'you, or byteSource(bytes) if you already have the bytes in memory.', { offset: 0, requestedLength: 0 }, ); } /* * The SIZE, which the check above only established is a number. * * `fileHandleSource` refuses a `byteLength` that is not a byte count (0.6.85) and `fileSource` * refuses the one it reads off the handle; this is the third adapter that takes a size and the * one that took whatever arrived. `BlobLike` is a structural shim precisely so a caller can * IMPLEMENT it — a stream-backed file, a mocked blob in a test, a wrapper around a native file * picker — and an implementation is where a computed `size` comes from. * * A `NaN` did not fail; it disabled the range guard. `assertReadRange` compares every read * against `byteLength`, every comparison against `NaN` is false, so the check silently stopped * happening and the source advertised `byteLength: NaN` to everything downstream. A negative or * fractional size passed the same way. `options.ts` names this shape exactly: "a guard written * as `if (value < 1)` simply does not fire". */ if (!Number.isSafeInteger(blob.size) || blob.size < 0) { throw new EdfSourceError( `blobSource() was given ${describeValue(blob.size)} as the blob's size, which is not a ` + 'byte count edfcore can address, so no read could be bounded by it. Next: pass a real ' + 'Blob or File, whose size the platform sets, or byteSource(bytes) if you built the ' + 'bytes yourself.', { offset: 0, requestedLength: 0 }, ); } const byteLength = blob.size; return { byteLength, async read(offset: number, length: number, options?: ReadOptions): Promise { throwIfAborted(options); assertReadRange(offset, length, byteLength); if (length === 0) return new Uint8Array(0); // `Blob.slice` takes an EXCLUSIVE end, unlike an HTTP byte range. const buffer = await blob.slice(offset, offset + length).arrayBuffer(); throwIfAborted(options); /* * Diagnosed HERE rather than left to `assertExactRead`, which is the argument `node.ts` makes * for its own short read — and the case it names there is this one: "a picked `File`'s backing * file shrank". It was the only adapter of the three without the diagnosis. * * `assertExactRead` exists for a `ByteSource` the CALLER wrote. Its message says a source * "must resolve with exactly the requested number of bytes or reject", which accuses the * browser of breaking a contract it kept: the platform answered correctly for a file that is * now shorter than the one the picker measured. This module's own docblock calls that the one * legitimate short read there is. * * Same shape as the HTTP buffered-body path (0.3.75) and the file handle (0.3.93). */ if (buffer.byteLength < length) { throw new EdfSourceError( `Reading bytes ${offset}..${offset + length - 1}: the blob ended after ` + `${buffer.byteLength} of them. This source was built for ${byteLength} bytes, so the ` + 'range asked for is past the end of the blob as it is now. Next: a picked File is a ' + 'handle on a file that can still change — the backing file was truncated or replaced ' + 'since it was chosen, so ask for it again.', { offset, requestedLength: length, receivedLength: buffer.byteLength }, ); } return assertExactRead(new Uint8Array(buffer), offset, length); }, }; }