Skip to content

Commit

Permalink
feat(parser): add range header parser
Browse files Browse the repository at this point in the history
  • Loading branch information
TomokiMiyauci committed Mar 7, 2023
0 parents commit 4d5a9ab
Show file tree
Hide file tree
Showing 2 changed files with 163 additions and 0 deletions.
69 changes: 69 additions & 0 deletions parser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { isNotEmpty, isString, isUndefined, trim } from "./deps.ts";
import type { IntRange, RangeSpec, SuffixRange } from "./types.ts";

const RangeSpecifierRe =
/^(?<rangeUnit>([!#$%&'*+-.^_`|~A-Za-z0-9])+)=(?<rangeSet>((([0-9])+-(([0-9])+)?)|(-([0-9])+))((\x20|\t)*,(\x20|\t)*((([0-9])+-(([0-9])+)?)|(-([0-9])+)))*)$/;

export interface RangesSpecifier {
readonly rangeUnit: string;
readonly rangeSet: string;
}

export function parseRangesSpecifier(input: string): RangesSpecifier {
const result = RangeSpecifierRe.exec(input);

if (!result || !result.groups) throw Error();

const rangeUnit = result.groups.rangeUnit;
const rangeSet = result.groups.rangeSet;

if (isUndefined(rangeUnit) || isUndefined(rangeSet)) {
throw SyntaxError();
}

return { rangeUnit, rangeSet };
}

const RangeSpecRe =
/^((?<firstPos>[0-9]+)-(?<lastPos>[0-9]+)?)$|^(-(?<suffixLength>[0-9]+))$/;

export function parseRangeSpec(input: string): IntRange | SuffixRange {
const result = RangeSpecRe.exec(input);

if (!result || !result.groups) {
throw SyntaxError("syntax error");
}

const firstPos = result.groups.firstPos;
const lastPos = result.groups.lastPos;
const suffixLength = result.groups.suffixLength;

if (isString(firstPos)) {
return {
firstPos: Number.parseInt(firstPos),
lastPos: lastPos ? Number.parseInt(lastPos) : undefined,
};
}

if (!isString(suffixLength)) {
throw SyntaxError("conflict syntax");
}

const suffix = Number.parseInt(suffixLength);

if (isNaN(suffix)) throw SyntaxError();

return { suffixLength: suffix };
}

export function parseRangeSet(input: string): [RangeSpec, ...RangeSpec[]] {
const result = input.split(",");

const ranges = result
.map(trim)
.map(parseRangeSpec);

if (!isNotEmpty(ranges)) throw SyntaxError();

return ranges;
}
94 changes: 94 additions & 0 deletions parser_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import {
parseRangeSpec,
parseRangesSpecifier,
RangesSpecifier,
} from "./parser.ts";
import { RangeSpec } from "./types.ts";
import { assertEquals, assertThrows, describe, it } from "./_dev_deps.ts";

describe("parseRangesSpecifier", () => {
it("should return parsed <ranges-specifier>", () => {
const table: [string, RangesSpecifier][] = [
["bytes=0-100", { rangeUnit: "bytes", rangeSet: "0-100" }],
["bytes=0-", { rangeUnit: "bytes", rangeSet: "0-" }],
["bytes=-100", { rangeUnit: "bytes", rangeSet: "-100" }],
["bytes=0-0,1-1", { rangeUnit: "bytes", rangeSet: "0-0,1-1" }],
["bytes=-100,0-100", { rangeUnit: "bytes", rangeSet: "-100,0-100" }],
["bytes=-100 , 0-100", { rangeUnit: "bytes", rangeSet: "-100 , 0-100" }],
["bytes=-100 , -200 , 300-400", {
rangeUnit: "bytes",
rangeSet: "-100 , -200 , 300-400",
}],
["unknown!=-1234567890", {
rangeUnit: "unknown!",
rangeSet: "-1234567890",
}],
];

table.forEach(([input, expected]) => {
assertEquals(parseRangesSpecifier(input), expected);
});
});

it("should throw error if the input is invalid syntax", () => {
const table: string[] = [
"",
"a",
"abc",
"a=b",
"=",
"a=1",
"<>=1-",
"a=1.1",
"a=1.0",
"a=120 ",
" a=120",
" a=120 ",
"a1=120",
];

table.forEach((input) => {
assertThrows(() => parseRangesSpecifier(input));
});
});
});

describe("parseRangeSpec", () => {
it("should return parsed <range-spec>", () => {
const table: [string, RangeSpec][] = [
["0-", { firstPos: 0, lastPos: undefined }],
["0-0", { firstPos: 0, lastPos: 0 }],
["100-100", { firstPos: 100, lastPos: 100 }],
["100-0", { firstPos: 100, lastPos: 0 }],
["100-0", { firstPos: 100, lastPos: 0 }],

["-0", { suffixLength: 0 }],
["-100", { suffixLength: 100 }],
];

table.forEach(([input, expected]) => {
assertEquals(parseRangeSpec(input), expected);
});
});

it("should throw error if the input is invalid syntax", () => {
const table: string[] = [
"",
"a",
"0",
"1",
"1.0-",
"0.1-",
"0-0.0",
"0-0.1",
"100-100,",
"100- ",
" 100-",
"-100,",
];

table.forEach((input) => {
assertThrows(() => parseRangeSpec(input));
});
});
});

0 comments on commit 4d5a9ab

Please sign in to comment.