Skip to content

Commit

Permalink
feat(transform): change Range interface and transform response interface
Browse files Browse the repository at this point in the history
  • Loading branch information
TomokiMiyauci committed Mar 22, 2023
1 parent 909f355 commit ff78387
Show file tree
Hide file tree
Showing 4 changed files with 123 additions and 307 deletions.
85 changes: 29 additions & 56 deletions transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,13 @@
// This module is browser compatible.

import {
ConditionalHeader,
distinct,
isErr,
isNotEmpty,
isNull,
Method,
isString,
NoContentHeaders,
parse,
RangeHeader,
RangesSpecifier,
RepresentationHeader,
Status,
unsafe,
Expand All @@ -18,47 +17,47 @@ import {
RangeUnit as Unit,
RequestedRangeNotSatisfiableResponse,
shallowMergeHeaders,
toSpecifier,
} from "./utils.ts";
import type { Range, RangeUnit } from "./types.ts";
import { type Range, RangeUnit } from "./types.ts";

export type UnitLike =
| RangeUnit
| readonly [RangeUnit, ...readonly RangeUnit[]];

export function withAcceptRanges(
response: Response,
unit: RangeUnit,
unit: UnitLike,
): Response {
const units = isString(unit) ? [unit] : unit;
const unitValue = distinct(units).join(", ");

if (!response.headers.has(RangeHeader.AcceptRanges)) {
response.headers.set(RangeHeader.AcceptRanges, unit);
response.headers.set(RangeHeader.AcceptRanges, unitValue);
}

return response;
}

interface Context {
export interface Context {
readonly ranges: Iterable<Range>;
readonly rangeValue: string;
}

export async function withContentRange(
request: Request,
response: Response,
context: Context,
): Promise<Response> {
const rangeValue = request.headers.get(RangeHeader.Range);
const contentType = response.headers.get(RepresentationHeader.ContentType);

if (
// A server MUST ignore a Range header field received with a request method that is unrecognized or for which range handling is not defined. For this specification, GET is the only method for which range handling is defined.
// @see https://www.rfc-editor.org/rfc/rfc9110#section-14.2-4
request.method !== Method.Get ||
isNull(rangeValue) ||
request.headers.has(ConditionalHeader.IfRange) ||
response.status !== Status.OK ||
response.headers.has(RangeHeader.ContentRange) ||
response.headers.get(RangeHeader.AcceptRanges) === Unit.None ||
response.bodyUsed ||
isNull(contentType)
) return response;

const rangeContainer = unsafe(() => parse(rangeValue));
const rangeContainer = unsafe(() => parse(context.rangeValue));

if (isErr(rangeContainer)) {
// A server that supports range requests MAY ignore or reject a Range header field that contains an invalid ranges-specifier (Section 14.1.1), a ranges-specifier with more than two overlapping ranges, or a set of many small ranges that are not listed in ascending order, since these are indications of either a broken client or a deliberate denial-of-service attack (Section 17.15).
Expand All @@ -67,59 +66,33 @@ export async function withContentRange(
}

const parsedRange = rangeContainer.value;
const matchedRange = matchRange(parsedRange, context.ranges);
const matchedRange = Array.from(context.ranges).find(({ unit }) =>
unit === parsedRange.rangeUnit
);
const body = await response.clone().arrayBuffer();

if (!matchedRange) {
// @see https://www.rfc-editor.org/rfc/rfc9110#section-14.2-13
return new RequestedRangeNotSatisfiableResponse({
rangeUnit: parsedRange.rangeUnit,
completeLength: body.byteLength,
range: { completeLength: body.byteLength },
}, { headers: response.headers });
}

const targetRangeSet = matchedRange.getSatisfiable({
const partialResponse = await matchedRange.respond({
content: body,
contentType,
rangeUnit: parsedRange.rangeUnit,
rangeSet: parsedRange.rangeSet,
});

if (!isNotEmpty(targetRangeSet)) {
return new RequestedRangeNotSatisfiableResponse({
rangeUnit: matchedRange.unit,
completeLength: body.byteLength,
}, { headers: response.headers });
}

const partialContents = await matchedRange.getPartial({
rangeSet: targetRangeSet,
content: body,
contentType,
});
const headers = shallowMergeHeaders(
response.headers,
partialContents.headers,
);
const baseHeaders = !partialResponse.body
? new NoContentHeaders(response.headers)
: response.headers;
const headers = shallowMergeHeaders(baseHeaders, partialResponse.headers);

return new Response(partialContents.content, {
status: Status.PartialContent,
return new Response(partialResponse.body, {
headers,
status: partialResponse.status,
});
}

function matchRange(
range: RangesSpecifier,
ranges: Iterable<Range>,
): null | Range {
const maybeRange = Array.from(ranges).find(({ unit }) =>
unit === range.rangeUnit
);

if (!maybeRange) return null;

const result = range
.rangeSet
.map(toSpecifier)
.every((specifier) => maybeRange.specifiers.includes(specifier));

return result ? maybeRange : null;
}
Loading

0 comments on commit ff78387

Please sign in to comment.