Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions src/decode/decode.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,27 @@ describe("decode", () => {
assertStrictEquals(info.scopes[0].kind, "");
});

it("throws in strict mode when the scopes string ends mid-VLQ", () => {
const encoder = new ItemEncoder();
encoder.addUnsignedVLQs(Tag.ORIGINAL_SCOPE_START, 0, 0, 0).finishItem();
// Replace the last digit with 'g', whose continuation bit is set. The
// string now ends while a VLQ still expects another digit.
const truncated = encoder.encode().slice(0, -1) + "g";
const map = createMap(truncated, []);

assertThrows(() => decode(map, { mode: DecodeMode.STRICT }));
});

it("tolerates a scopes string that ends mid-VLQ in lax mode", () => {
const encoder = new ItemEncoder();
encoder.addUnsignedVLQs(Tag.ORIGINAL_SCOPE_START, 0, 0, 0).finishItem();
const truncated = encoder.encode().slice(0, -1) + "g";
const map = createMap(truncated, []);

// Lax mode must not throw; it decodes best-effort.
decode(map, { mode: DecodeMode.LAX });
});

it("throws when encountering an ORIGINAL_SCOPE_END without start in strict mode", () => {
const encoder = new ItemEncoder();
encoder.addUnsignedVLQs(Tag.ORIGINAL_SCOPE_END, 0, 0).finishItem();
Expand Down
5 changes: 4 additions & 1 deletion src/decode/decode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,10 @@ class Decoder {
}

decode(): DecodedScopeInfo {
const iter = new TokenIterator(this.#encodedScopes);
const iter = new TokenIterator(
this.#encodedScopes,
this.#mode === DecodeMode.STRICT,
);

while (iter.hasNext()) {
const tag = iter.nextUnsignedVLQ();
Expand Down
16 changes: 15 additions & 1 deletion src/vlq.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import { describe, it } from "@std/testing/bdd";
import { TokenIterator } from "./vlq.ts";
import { assertFalse, assertStrictEquals } from "@std/assert";
import { assertFalse, assertStrictEquals, assertThrows } from "@std/assert";

describe("TokenIterator", () => {
describe("nextUnsignedVLQ", () => {
Expand Down Expand Up @@ -35,6 +35,20 @@ describe("TokenIterator", () => {
assertStrictEquals(iter.nextUnsignedVLQ(), 1);
assertStrictEquals(iter.peek(), "C");
});

it("throws in strict mode when the VLQ is truncated", () => {
// 'h' has the continuation bit set, but the string ends right after it.
const iter = new TokenIterator("h", /* strict */ true);

assertThrows(() => iter.nextUnsignedVLQ());
});

it("throws in strict mode when a continuation runs into an invalid digit", () => {
// 'h' has the continuation bit set, 'æ' is outside the base64 alphabet.
const iter = new TokenIterator("hæC", /* strict */ true);

assertThrows(() => iter.nextUnsignedVLQ());
});
});

describe("nextSignedVLQ", () => {
Expand Down
22 changes: 18 additions & 4 deletions src/vlq.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,12 @@ export function encodeUnsigned(n: number): string {

export class TokenIterator {
readonly #string: string;
readonly #strict: boolean;
#position: number;

constructor(string: string) {
constructor(string: string, strict = false) {
this.#string = string;
this.#strict = strict;
this.#position = 0;
}

Expand Down Expand Up @@ -83,13 +85,25 @@ export class TokenIterator {
nextUnsignedVLQ(): number {
let result = 0;
let shift = 0;
let digit = 0;
let continuation = 0;
do {
const charCode = this.nextCharCode();
digit = BASE64_CODES[charCode];
const digit = BASE64_CODES[charCode];
if (digit === undefined) {
// `charCode` is either `NaN` (we ran past the end of a truncated
// string) or points outside the base64 alphabet. Either way the
// previous digit's continuation bit promised another digit that isn't
// there, so the VLQ is truncated/malformed. In lax mode we stop and
// keep whatever we decoded so far; in strict mode this is an error.
if (this.#strict) {
throw new Error("Encountered truncated or malformed VLQ");
}
break;
}
result += (digit & VLQ_BASE_MASK) << shift;
shift += VLQ_BASE_SHIFT;
} while (digit & VLQ_CONTINUATION_MASK);
continuation = digit & VLQ_CONTINUATION_MASK;
} while (continuation);
return result;
}

Expand Down
Loading