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
6 changes: 6 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ on:
branches: [main]
pull_request:

# Least privilege for the GITHUB_TOKEN: every job only reads the repository. Caches
# (setup-node, rust-cache, cache-cargo-install) use the Actions cache service, not this token;
# nothing here comments, uploads releases or writes checks.
permissions:
contents: read

concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/npm-smoke.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ on:
- "packages/create-cloudhal/**"
- "starters/**"

# Packs, installs and builds locally; the token only reads the repository.
permissions:
contents: read

jobs:
smoke:
runs-on: ubuntu-latest
Expand Down
42 changes: 42 additions & 0 deletions packages/analytics/test/fake-wae-check.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/** Alert 8: the fake WAE's type-error check answers as its regex did, in linear time. */
import { describe, expect, it } from "vitest";
import { weightsBesideLiteralZero } from "./helpers/fake-wae.ts";

const old = (q: string) => /quantileWeighted\([^;]*?if\([^,]+,\s*double\d+\s*,/.test(q);

describe("weightsBesideLiteralZero", () => {
it("answers as the regex did", () => {
for (const q of [
"SELECT quantileWeighted(0.5)(double1, if(blob2 = 'x', double3, 0)) FROM t",
"SELECT quantileWeighted(0.5)(double1, if(blob2 = 'x', 1, 0)) FROM t",
"SELECT quantileWeighted(0.5)(double1, _sample_interval) FROM t; SELECT if(a, double1, 0)",
"quantileWeighted(if(,double1,",
"quantileWeighted(if(a, double12 ,",
"if(a, double1, 0) quantileWeighted(",
"",
])
expect(weightsBesideLiteralZero(q), q).toBe(old(q));
let s = 8;
const next = () => {
s = (s * 1103515245 + 12345) & 0x7fffffff;
return s;
};
const tokens = ["quantileWeighted(", "if(", ",", " ", "double1", ";", "a", "double"];
for (let n = 0; n < 20_000; n++) {
let q = "";
for (let i = next() % 10; i > 0; i--) q += tokens[next() % tokens.length];
expect(weightsBesideLiteralZero(q), JSON.stringify(q)).toBe(old(q));
}
});

it("is linear on many quantileWeighted( and if(", () => {
const start = performance.now();
expect(weightsBesideLiteralZero("quantileWeighted(".repeat(10_000))).toBe(false);
expect(weightsBesideLiteralZero(`quantileWeighted(${"if(".repeat(50_000)}`)).toBe(false);
// every `if(` shares one comma followed by a long run the probe has to scan
const shared = `quantileWeighted(${"if(".repeat(50_000)}x,${" ".repeat(50_000)}y`;
expect(weightsBesideLiteralZero(shared)).toBe(false);
expect(weightsBesideLiteralZero(`${shared.slice(0, -1)}double1,`)).toBe(true);
expect(performance.now() - start).toBeLessThan(1500);
});
});
46 changes: 45 additions & 1 deletion packages/analytics/test/helpers/fake-wae.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ export class FakeWae implements WaeSqlBinding {
const alias = q.match(/\b(?:blob|double)\d+ AS (\w+)/);
if (alias) throw new SqlApiError(`Input was invalid: column \`${alias[1]}\` not found`);
}
if (/quantileWeighted\([^;]*?if\([^,]+,\s*double\d+\s*,/.test(q))
if (weightsBesideLiteralZero(q))
throw new SqlApiError(
"Input was invalid: Your query could not be executed due to a type error",
);
Expand All @@ -154,3 +154,47 @@ export class FakeWae implements WaeSqlBinding {
};
}
}

const isSpace = (c: string | undefined) => c !== undefined && /\s/.test(c);
const isDigit = (c: string | undefined) => c !== undefined && c >= "0" && c <= "9";

/** Whether `\s*double\d+\s*,` matches at `at`, scanned by hand (no regex to rescan). */
function weightArgAt(q: string, at: number): boolean {
let i = at;
while (isSpace(q[i])) i++;
if (!q.startsWith("double", i)) return false;
i += 6;
const digits = i;
while (isDigit(q[i])) i++;
if (i === digits) return false;
while (isSpace(q[i])) i++;
return q[i] === ",";
}

/**
* Whether `q` has `quantileWeighted(…if(<cond>, doubleN, …` before the next `;`: what
* `/quantileWeighted\([^;]*?if\([^,]+,\s*double\d+\s*,/` tested, in one pass (that regex rescanned
* from every `quantileWeighted(` and `if(`; CodeQL alert 8). A `quantileWeighted(` with no match
* before its `;` means none of the later ones before that `;` match either, and each comma is
* probed once however many `if(` share it.
*/
export function weightsBesideLiteralZero(q: string): boolean {
let comma = -1;
// The last comma probed: a probe that matched returned at once, so a repeat can only fail.
let probed = -1;
for (let at = q.indexOf("quantileWeighted("); at !== -1; ) {
const semi = q.indexOf(";", at);
const end = semi === -1 ? q.length : semi;
for (let k = q.indexOf("if(", at + 17); k !== -1 && k < end; k = q.indexOf("if(", k + 1)) {
// `[^,]+`: the condition runs to the first comma after `if(`, at least one character.
if (comma < k + 3) comma = q.indexOf(",", k + 3);
if (comma === -1) return false;
if (comma === k + 3 || comma === probed) continue;
probed = comma;
if (weightArgAt(q, comma + 1)) return true;
}
if (semi === -1) return false;
at = q.indexOf("quantileWeighted(", semi + 1);
}
return false;
}
2 changes: 1 addition & 1 deletion packages/astro-renderer/test/corrections.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ describe("corrections", () => {
const html = renderInlineMarkdown(
"<script>alert(1)</script> [x](javascript:alert(1)) ![pixel](https://evil.example/p.gif)\n\n# Not a heading",
);
expect(html).not.toMatch(/<script|<img|<h1|javascript:|<p>/);
expect(html).not.toMatch(/<script|<img|<h1|javascript:|<p>/i);
expect(html).toContain("&lt;script&gt;");
expect(html).toContain("pixel");
});
Expand Down
21 changes: 21 additions & 0 deletions packages/cli/test/i18n-catalog-entries.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/**
* Alert 37: the i18n codemod (scripts/i18n-codemod, tested here with the other scripts, whose
* files this package's tsconfig covers) decodes string literals of either quote without breaking
* escapes.
*/
import { describe, expect, it } from "vitest";
import { stringBodyAsJson } from "../../../scripts/i18n-codemod/catalog-entries.ts";

const decode = (body: string) => JSON.parse(`"${stringBodyAsJson(body)}"`) as string;

describe("stringBodyAsJson", () => {
it("decodes what the literal means", () => {
expect(decode("plain")).toBe("plain");
expect(decode("it\\'s")).toBe("it's");
expect(decode('say "hi"')).toBe('say "hi"');
expect(decode('a\\"b')).toBe('a"b');
expect(decode("back\\\\slash")).toBe("back\\slash");
expect(decode('back\\\\"')).toBe('back\\"');
expect(decode("line\\nbreak \\u00e9")).toBe("line\nbreak \u00e9");
});
});
24 changes: 21 additions & 3 deletions scripts/i18n-codemod/catalog-entries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,25 @@ export interface CatalogEntry {
comment?: string;
}

/**
* The body of a string literal (either quote) as the body of a JSON string: `\'` becomes `'`, a bare `"`
* is escaped, every other escape (`\\`, `\"`, `\n`, `\u…`) stays as written. One pass, so an
* escaped backslash before a quote is never taken apart (the replace chain it replaced turned
* `'a\\"b'`'s `\"` into `\\"`, which ends the JSON string early).
*/
export function stringBodyAsJson(body: string): string {
let out = "";
for (let k = 0; k < body.length; k++) {
const c = body[k] as string;
if (c === "\\") {
const next = body[k + 1] ?? "";
out += next === "'" ? "'" : `\\${next}`;
k++;
} else out += c === '"' ? '\\"' : c;
}
return out;
}

class Scanner {
i = 0;
readonly src: string;
Expand Down Expand Up @@ -53,9 +72,8 @@ class Scanner {
while (this.src[j] !== quote) j += this.src[j] === "\\" ? 2 : 1;
const body = this.src.slice(this.i + 1, j);
this.i = j + 1;
return quote === '"'
? (JSON.parse(`"${body}"`) as string)
: JSON.parse(`"${body.replace(/\\'/g, "'").replace(/"/g, '\\"')}"`);
// Either quote: `\'` is legal in a double-quoted literal too, and not in JSON.
return JSON.parse(`"${stringBodyAsJson(body)}"`) as string;
}

key(): string {
Expand Down
2 changes: 2 additions & 0 deletions scripts/mcp-tools-doc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@ function summary(description: string): string {
// A cut inside a code span would leave it open for the rest of the table.
if ((short.match(/`/g) ?? []).length % 2 === 1) short = `${short.slice(0, -1)}\`…`;
// `<` outside code spans would read as an HTML tag; inside them it shows as written.
// Backslashes first, so an escaped pipe in a description cannot end the cell.
return short
.replace(/\\/g, "\\\\")
.replace(/\|/g, "\\|")
.split("`")
.map((part, i) => (i % 2 === 0 ? part.replace(/</g, "&lt;") : part))
Expand Down
2 changes: 1 addition & 1 deletion scripts/release-plan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export function sharedVersion(pkgs: readonly Pkg[]): string {

/** Whether the npm registry serves `name@version`. */
export async function onNpm(name: string, version: string): Promise<boolean> {
const res = await fetch(`https://registry.npmjs.org/${name.replace("/", "%2F")}/${version}`);
const res = await fetch(`https://registry.npmjs.org/${name.replaceAll("/", "%2F")}/${version}`);
return res.ok;
}

Expand Down
Loading