mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-09-30 20:49:41 +02:00
Two bugs in the File Viewer's media player, both reproduced in a real browser against an 18MB mp4 before and after the fix. 1. Closing the preview left the video playing. closeFilePreview() only dropped the overlay's `visible` class, which is display:none and nothing else, so the audio kept going with no visible player to pause. Detaching the element is not a fix either: a detached HTMLMediaElement plays on until it is garbage collected. _stopFilePreviewMedia() now pauses, drops src and load()s every media element (also on re-open, where overwriting innerHTML had the same effect), which additionally aborts the in-flight download. 2. The scrub bar was inert. file-raw read the whole file and answered 200 with no Accept-Ranges, so Chrome reported video.seekable as [0, 0] and silently reverted `currentTime = x`; Safari refuses to start such media at all. Raw bodies are now streamed and range-aware: Accept-Ranges: bytes on every response, 206 + Content-Range for a Range request, 416 for one past EOF, and a malformed spec ignored (200) per RFC 9110. Parsing is pure in src/web/http-range.ts. Measured on tmp/codeman-crt-v5-66s.mp4 (18MB, 66.6s): before seekable [0, 0] seek to 56.6s reverted to 3.9s close: still playing after seekable [0, 66.56] seek to 56.6s landed at 60.2s close: paused, NETWORK_EMPTY Range slices are byte-identical to `dd`, the full-file path is byte-identical to the file, and the SVG octet-stream/attachment hardening and the 50MB cap are unchanged (the cap is still checked before the range). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
102 lines
4.6 KiB
TypeScript
102 lines
4.6 KiB
TypeScript
/**
|
|
* @fileoverview Byte-range parsing for the raw file-serving routes.
|
|
*
|
|
* The file viewer's video player is only seekable when file-raw answers `Range`
|
|
* requests with 206 (measured before the fix: `video.seekable` was `[0, 0]` and
|
|
* `currentTime = x` silently reverted). What that correctness rests on is this
|
|
* parser, so the cases pinned here are the ones a media element actually emits
|
|
* plus the malformed input a browser never sends but a client can:
|
|
*
|
|
* - `bytes=0-` — how Chrome opens EVERY media element. Must be 206, not 200.
|
|
* - `bytes=-N` — the SUFFIX form (last N bytes), not "from N onwards"; mp4
|
|
* players use it to read a trailing moov atom.
|
|
* - out of bounds -> 416, malformed -> ignored (200), which are different
|
|
* answers for what looks like the same "bad range".
|
|
*/
|
|
|
|
import { describe, expect, it } from 'vitest';
|
|
import { parseByteRange } from '../src/web/http-range.js';
|
|
|
|
describe('parseByteRange', () => {
|
|
it('serves the full file when there is no Range header', () => {
|
|
expect(parseByteRange(undefined, 1000)).toEqual({ kind: 'full' });
|
|
expect(parseByteRange('', 1000)).toEqual({ kind: 'full' });
|
|
});
|
|
|
|
it('answers bytes=0- with a partial range (the form Chrome opens media with)', () => {
|
|
expect(parseByteRange('bytes=0-', 1000)).toEqual({ kind: 'partial', start: 0, end: 999 });
|
|
});
|
|
|
|
it('parses a closed range inclusive of both ends', () => {
|
|
expect(parseByteRange('bytes=100-199', 1000)).toEqual({ kind: 'partial', start: 100, end: 199 });
|
|
});
|
|
|
|
it('clamps an end past EOF instead of rejecting the range', () => {
|
|
expect(parseByteRange('bytes=900-5000', 1000)).toEqual({ kind: 'partial', start: 900, end: 999 });
|
|
});
|
|
|
|
it('reads bytes=-N as the LAST N bytes, not as an offset', () => {
|
|
expect(parseByteRange('bytes=-100', 1000)).toEqual({ kind: 'partial', start: 900, end: 999 });
|
|
});
|
|
|
|
it('clamps a suffix longer than the file to the whole file', () => {
|
|
expect(parseByteRange('bytes=-5000', 1000)).toEqual({ kind: 'partial', start: 0, end: 999 });
|
|
});
|
|
|
|
it('accepts a single-byte range', () => {
|
|
expect(parseByteRange('bytes=0-0', 1000)).toEqual({ kind: 'partial', start: 0, end: 0 });
|
|
});
|
|
|
|
it('tolerates whitespace and a capitalised unit', () => {
|
|
expect(parseByteRange(' BYTES = 10-20 ', 1000)).toEqual({ kind: 'partial', start: 10, end: 20 });
|
|
});
|
|
|
|
it('reports a start at or past EOF as unsatisfiable (416)', () => {
|
|
expect(parseByteRange('bytes=1000-', 1000)).toEqual({ kind: 'unsatisfiable' });
|
|
expect(parseByteRange('bytes=1500-1600', 1000)).toEqual({ kind: 'unsatisfiable' });
|
|
});
|
|
|
|
it('reports a zero-length suffix as unsatisfiable', () => {
|
|
expect(parseByteRange('bytes=-0', 1000)).toEqual({ kind: 'unsatisfiable' });
|
|
});
|
|
|
|
it('reports any range against an empty file as unsatisfiable', () => {
|
|
expect(parseByteRange('bytes=0-', 0)).toEqual({ kind: 'unsatisfiable' });
|
|
expect(parseByteRange('bytes=-10', 0)).toEqual({ kind: 'unsatisfiable' });
|
|
});
|
|
|
|
it('ignores an inverted range rather than 416-ing it (invalid spec, not unsatisfiable)', () => {
|
|
expect(parseByteRange('bytes=500-100', 1000)).toEqual({ kind: 'full' });
|
|
});
|
|
|
|
it('ignores units it does not implement', () => {
|
|
expect(parseByteRange('items=0-10', 1000)).toEqual({ kind: 'full' });
|
|
expect(parseByteRange('bytes 0-10', 1000)).toEqual({ kind: 'full' });
|
|
});
|
|
|
|
it('ignores multi-range requests instead of answering only the first range', () => {
|
|
// A multipart/byteranges body is the only correct answer to these, and no
|
|
// media element asks for one — serving the whole file is spec-legal.
|
|
expect(parseByteRange('bytes=0-99,200-299', 1000)).toEqual({ kind: 'full' });
|
|
});
|
|
|
|
it('ignores malformed specs', () => {
|
|
expect(parseByteRange('bytes=', 1000)).toEqual({ kind: 'full' });
|
|
expect(parseByteRange('bytes=-', 1000)).toEqual({ kind: 'full' });
|
|
expect(parseByteRange('bytes=abc-def', 1000)).toEqual({ kind: 'full' });
|
|
expect(parseByteRange('bytes=1.5-2', 1000)).toEqual({ kind: 'full' });
|
|
});
|
|
|
|
it('ignores a duplicated Range header rather than guessing which one won', () => {
|
|
expect(parseByteRange(['bytes=0-10', 'bytes=20-30'], 1000)).toEqual({ kind: 'full' });
|
|
});
|
|
|
|
it('bounds an absurdly long offset instead of producing Infinity', () => {
|
|
// A 100-digit first-byte-pos must not reach createReadStream as Infinity.
|
|
const huge = '9'.repeat(100);
|
|
expect(parseByteRange(`bytes=${huge}-`, 1000)).toEqual({ kind: 'unsatisfiable' });
|
|
const range = parseByteRange(`bytes=0-${huge}`, 1000);
|
|
expect(range).toEqual({ kind: 'partial', start: 0, end: 999 });
|
|
});
|
|
});
|