feat: add S3 byte range parser
This commit is contained in:
@@ -0,0 +1,46 @@
|
|||||||
|
export type RangeParseResult =
|
||||||
|
| { type: 'none' }
|
||||||
|
| { type: 'valid'; start: number; end: number }
|
||||||
|
| { type: 'invalid' };
|
||||||
|
|
||||||
|
const DECIMAL = /^\d+$/;
|
||||||
|
|
||||||
|
export const parseRangeHeader = (rangeHeader: string | null, size: number): RangeParseResult => {
|
||||||
|
if (!rangeHeader) return { type: 'none' };
|
||||||
|
if (!Number.isSafeInteger(size) || size < 0) return { type: 'invalid' };
|
||||||
|
if (!rangeHeader.startsWith('bytes=')) return { type: 'invalid' };
|
||||||
|
|
||||||
|
const spec = rangeHeader.slice('bytes='.length).trim();
|
||||||
|
if (spec.includes(',')) return { type: 'invalid' };
|
||||||
|
|
||||||
|
const dash = spec.indexOf('-');
|
||||||
|
if (dash === -1) return { type: 'invalid' };
|
||||||
|
|
||||||
|
const startText = spec.slice(0, dash).trim();
|
||||||
|
const endText = spec.slice(dash + 1).trim();
|
||||||
|
if (!startText && !endText) return { type: 'invalid' };
|
||||||
|
if (size === 0) return { type: 'invalid' };
|
||||||
|
|
||||||
|
if (!startText) {
|
||||||
|
if (!DECIMAL.test(endText)) return { type: 'invalid' };
|
||||||
|
const suffixLength = Number.parseInt(endText, 10);
|
||||||
|
if (suffixLength <= 0) return { type: 'invalid' };
|
||||||
|
return { type: 'valid', start: Math.max(size - suffixLength, 0), end: size - 1 };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!DECIMAL.test(startText)) return { type: 'invalid' };
|
||||||
|
const start = Number.parseInt(startText, 10);
|
||||||
|
if (start >= size) return { type: 'invalid' };
|
||||||
|
|
||||||
|
if (!endText) return { type: 'valid', start, end: size - 1 };
|
||||||
|
if (!DECIMAL.test(endText)) return { type: 'invalid' };
|
||||||
|
|
||||||
|
const requestedEnd = Number.parseInt(endText, 10);
|
||||||
|
if (requestedEnd < start) return { type: 'invalid' };
|
||||||
|
return { type: 'valid', start, end: Math.min(requestedEnd, size - 1) };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const contentRange = (start: number, end: number, size: number): string =>
|
||||||
|
`bytes ${start}-${end}/${size}`;
|
||||||
|
|
||||||
|
export const unsatisfiedContentRange = (size: number): string => `bytes */${size}`;
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { describe, expect, it } from 'bun:test';
|
||||||
|
import { contentRange, parseRangeHeader, unsatisfiedContentRange } from '../src/utils/s3/range';
|
||||||
|
|
||||||
|
describe('S3 HTTP range parser', () => {
|
||||||
|
it('returns none when Range is missing', () => {
|
||||||
|
expect(parseRangeHeader(null, 10)).toEqual({ type: 'none' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('parses explicit start/end ranges', () => {
|
||||||
|
expect(parseRangeHeader('bytes=2-5', 10)).toEqual({ type: 'valid', start: 2, end: 5 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('clamps open-ended ranges to object size', () => {
|
||||||
|
expect(parseRangeHeader('bytes=7-', 10)).toEqual({ type: 'valid', start: 7, end: 9 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('parses suffix ranges', () => {
|
||||||
|
expect(parseRangeHeader('bytes=-4', 10)).toEqual({ type: 'valid', start: 6, end: 9 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('clamps oversized suffix ranges to the whole object', () => {
|
||||||
|
expect(parseRangeHeader('bytes=-50', 10)).toEqual({ type: 'valid', start: 0, end: 9 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects multiple ranges', () => {
|
||||||
|
expect(parseRangeHeader('bytes=0-1,3-4', 10)).toEqual({ type: 'invalid' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects unsatisfiable ranges', () => {
|
||||||
|
expect(parseRangeHeader('bytes=10-12', 10)).toEqual({ type: 'invalid' });
|
||||||
|
expect(parseRangeHeader('bytes=6-3', 10)).toEqual({ type: 'invalid' });
|
||||||
|
expect(parseRangeHeader('bytes=-0', 10)).toEqual({ type: 'invalid' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('formats content-range headers', () => {
|
||||||
|
expect(contentRange(2, 5, 10)).toBe('bytes 2-5/10');
|
||||||
|
expect(unsatisfiedContentRange(10)).toBe('bytes */10');
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user