mirror of
https://bitbucket.org/siakitem/my-pi.git
synced 2026-10-26 18:55:22 +00:00
289 lines
11 KiB
TypeScript
289 lines
11 KiB
TypeScript
import { homedir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
// Mock node:fs so realpathSync (used by canonicalizePath) is controllable.
|
|
// Default is identity so all existing lexical tests are unaffected.
|
|
// Every other fs binding passes through to the real module, so filesystem-
|
|
// backed helpers (lstatSync, mkdtempSync, symlinkSync, …) stay usable here.
|
|
const realpathSync = vi.hoisted(() =>
|
|
vi.fn<(path: string) => string>((p) => p),
|
|
);
|
|
vi.mock("node:fs", async () => {
|
|
const actual = await vi.importActual<typeof import("node:fs")>("node:fs");
|
|
return {
|
|
...actual,
|
|
realpathSync,
|
|
default: { ...actual, realpathSync },
|
|
};
|
|
});
|
|
|
|
import { BashProgram } from "#src/access-intent/bash/program";
|
|
import { pathFlavorForPlatform } from "#src/path/path-flavor";
|
|
import { PathNormalizer } from "#src/path-normalizer";
|
|
import { createTmpFixture } from "#test/helpers/tmp-fixture";
|
|
|
|
describe("BashProgram", () => {
|
|
describe("pathRuleCandidates", () => {
|
|
const cwd = "/projects/my-app";
|
|
const normalizer = new PathNormalizer(
|
|
pathFlavorForPlatform(process.platform),
|
|
cwd,
|
|
);
|
|
|
|
beforeEach(() => {
|
|
realpathSync.mockReset();
|
|
realpathSync.mockImplementation((p: string) => p);
|
|
});
|
|
|
|
describe("operands of nested commands hosted in a redirect (#741)", () => {
|
|
it("projects the operand of a redirect-hosted command", async () => {
|
|
const program = await BashProgram.parse(
|
|
"echo hi > $(cat /etc/shadow)",
|
|
normalizer,
|
|
);
|
|
expect(program.pathRuleCandidates().map(({ token }) => token)).toEqual([
|
|
"/etc/shadow",
|
|
]);
|
|
});
|
|
|
|
it("does not promote a bare inner token that names nothing", async () => {
|
|
const program = await BashProgram.parse(
|
|
"echo hi > $(rm nonexistent-file)",
|
|
normalizer,
|
|
);
|
|
expect(program.pathRuleCandidates()).toEqual([]);
|
|
});
|
|
});
|
|
|
|
it("adds absolute and relative policy values for relative tokens", async () => {
|
|
const program = await BashProgram.parse("cat src/foo.ts", normalizer);
|
|
const candidates = program.pathRuleCandidates();
|
|
expect(candidates.map(({ token }) => token)).toEqual(["src/foo.ts"]);
|
|
expect(candidates[0].path.matchValues()).toEqual([
|
|
"/projects/my-app/src/foo.ts",
|
|
"src/foo.ts",
|
|
]);
|
|
expect(candidates[0].path.value()).toBe("/projects/my-app/src/foo.ts");
|
|
});
|
|
|
|
it("resolves tokens after literal cd against the effective directory", async () => {
|
|
const program = await BashProgram.parse(
|
|
"cd nested && cat src/file.txt",
|
|
normalizer,
|
|
);
|
|
const fileCandidate = program
|
|
.pathRuleCandidates()
|
|
.find((candidate) => candidate.token === "src/file.txt");
|
|
expect(fileCandidate?.path.matchValues()).toEqual([
|
|
"/projects/my-app/nested/src/file.txt",
|
|
"nested/src/file.txt",
|
|
"src/file.txt",
|
|
]);
|
|
expect(fileCandidate?.path.value()).toBe(
|
|
"/projects/my-app/nested/src/file.txt",
|
|
);
|
|
});
|
|
|
|
it("adds the canonical alias for a symlinked token (#486)", async () => {
|
|
// /projects/my-app/src/foo.ts is a symlink to /vault/foo.ts.
|
|
realpathSync.mockImplementation((p: string) =>
|
|
p === "/projects/my-app/src/foo.ts" ? "/vault/foo.ts" : p,
|
|
);
|
|
const program = await BashProgram.parse("cat src/foo.ts", normalizer);
|
|
const candidate = program.pathRuleCandidates()[0];
|
|
expect(candidate.path.matchValues()).toEqual([
|
|
"/projects/my-app/src/foo.ts",
|
|
"src/foo.ts",
|
|
"/vault/foo.ts",
|
|
]);
|
|
});
|
|
|
|
it("does not absolute-allow relative tokens after unknown cd", async () => {
|
|
const program = await BashProgram.parse(
|
|
'cd "$DIR" && cat src/foo.ts',
|
|
normalizer,
|
|
);
|
|
const fileCandidate = program
|
|
.pathRuleCandidates()
|
|
.find((candidate) => candidate.token === "src/foo.ts");
|
|
expect(fileCandidate?.path.matchValues()).toEqual(["src/foo.ts"]);
|
|
expect(fileCandidate?.path.value()).toBe("src/foo.ts");
|
|
});
|
|
|
|
it("keeps an unknown-cd token literal-only even when it would resolve a symlink (#393)", async () => {
|
|
// A canonical alias here would resolve against the wrong (unknown) base.
|
|
realpathSync.mockImplementation(() => "/somewhere/else");
|
|
const program = await BashProgram.parse(
|
|
'cd "$DIR" && cat src/foo.ts',
|
|
normalizer,
|
|
);
|
|
const fileCandidate = program
|
|
.pathRuleCandidates()
|
|
.find((candidate) => candidate.token === "src/foo.ts");
|
|
expect(fileCandidate?.path.matchValues()).toEqual(["src/foo.ts"]);
|
|
expect(fileCandidate?.path.boundaryValue()).toBe("");
|
|
});
|
|
|
|
describe("existence-probe bare-token promotion (#645)", () => {
|
|
// Candidacy comes from the filesystem, so these run against a real
|
|
// tmpdir cwd with real lstat/realpath rather than the fake cwd above.
|
|
const tmp = createTmpFixture();
|
|
let root: string;
|
|
let probeNormalizer: PathNormalizer;
|
|
|
|
beforeEach(async () => {
|
|
const actual =
|
|
await vi.importActual<typeof import("node:fs")>("node:fs");
|
|
realpathSync.mockImplementation(actual.realpathSync);
|
|
// Canonicalize the root: on macOS the tmpdir is itself a symlink, so a
|
|
// lexical root would disagree with every canonical form derived below.
|
|
root = actual.realpathSync(tmp.dir("pi-perm-bash-"));
|
|
probeNormalizer = new PathNormalizer(
|
|
pathFlavorForPlatform(process.platform),
|
|
root,
|
|
);
|
|
});
|
|
|
|
afterEach(() => {
|
|
tmp.cleanup();
|
|
});
|
|
|
|
it("promotes a bare token naming an existing file", async () => {
|
|
tmp.file(root, "id_rsa", "key");
|
|
const program = await BashProgram.parse("cat id_rsa", probeNormalizer);
|
|
const candidates = program.pathRuleCandidates();
|
|
expect(candidates.map(({ token }) => token)).toEqual(["id_rsa"]);
|
|
expect(candidates[0].path.matchValues()).toEqual([
|
|
join(root, "id_rsa"),
|
|
"id_rsa",
|
|
]);
|
|
});
|
|
|
|
it("drops a bare token naming nothing — `git status` stays silent (#509)", async () => {
|
|
const program = await BashProgram.parse("git status", probeNormalizer);
|
|
expect(program.pathRuleCandidates()).toHaveLength(0);
|
|
});
|
|
|
|
it("drops every bare word of a command referencing no real file", async () => {
|
|
const program = await BashProgram.parse(
|
|
"npm run build && git checkout main",
|
|
probeNormalizer,
|
|
);
|
|
expect(program.pathRuleCandidates()).toHaveLength(0);
|
|
});
|
|
|
|
it("promotes a bare symlink and carries its target as a match value", async () => {
|
|
// The issue's second repro shape: a_sym -> .some.secret, where the rule
|
|
// names the target. Raw-token matching could never see this.
|
|
const secret = tmp.file(root, ".some.secret", "s3cret");
|
|
tmp.symlink(root, "a_sym", secret);
|
|
const program = await BashProgram.parse("cat a_sym", probeNormalizer);
|
|
const candidate = program
|
|
.pathRuleCandidates()
|
|
.find((c) => c.token === "a_sym");
|
|
expect(candidate?.path.matchValues()).toContain(
|
|
join(root, ".some.secret"),
|
|
);
|
|
});
|
|
|
|
it("promotes a bare token naming a directory", async () => {
|
|
tmp.subdir(root, "vault");
|
|
const program = await BashProgram.parse("ls vault", probeNormalizer);
|
|
expect(program.pathRuleCandidates().map(({ token }) => token)).toEqual([
|
|
"vault",
|
|
]);
|
|
});
|
|
|
|
it("promotes a dangling symlink — the link is the named operand", async () => {
|
|
tmp.symlink(root, "dangling", join(root, "gone"));
|
|
const program = await BashProgram.parse(
|
|
"cat dangling",
|
|
probeNormalizer,
|
|
);
|
|
expect(program.pathRuleCandidates().map(({ token }) => token)).toEqual([
|
|
"dangling",
|
|
]);
|
|
});
|
|
|
|
it("keeps a promoted token literal-only after an unknown cd (#393)", async () => {
|
|
tmp.file(root, "id_rsa", "key");
|
|
const program = await BashProgram.parse(
|
|
'cd "$DIR" && cat id_rsa',
|
|
probeNormalizer,
|
|
);
|
|
// An unknown base cannot be probed against a known directory, so the
|
|
// token stays unpromoted rather than resolving against the wrong cwd.
|
|
expect(program.pathRuleCandidates()).toHaveLength(0);
|
|
});
|
|
|
|
it("does not double-promote a token the shape gate already accepts", async () => {
|
|
tmp.file(root, "id_rsa", "key");
|
|
const program = await BashProgram.parse(
|
|
"cat ./id_rsa",
|
|
probeNormalizer,
|
|
);
|
|
expect(program.pathRuleCandidates()).toHaveLength(1);
|
|
});
|
|
|
|
it("probes a bare token against the effective directory after a literal cd", async () => {
|
|
const nested = tmp.subdir(root, "nested");
|
|
tmp.file(nested, "inner.txt", "x");
|
|
const program = await BashProgram.parse(
|
|
"cd nested && cat inner.txt",
|
|
probeNormalizer,
|
|
);
|
|
const candidate = program
|
|
.pathRuleCandidates()
|
|
.find((c) => c.token === "inner.txt");
|
|
expect(candidate?.path.matchValues()).toContain(
|
|
join(root, "nested", "inner.txt"),
|
|
);
|
|
});
|
|
|
|
it("consults no policy — promotion needs no matcher argument", async () => {
|
|
tmp.file(root, "id_rsa", "key");
|
|
const program = await BashProgram.parse("cat id_rsa", probeNormalizer);
|
|
expect(program.pathRuleCandidates().map(({ token }) => token)).toEqual([
|
|
"id_rsa",
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe("resolved shell expansions (#694)", () => {
|
|
it("resolves ${HOME}/… instead of fabricating a cwd-relative path", async () => {
|
|
const program = await BashProgram.parse(
|
|
'ls "${HOME}/somewhere"',
|
|
normalizer,
|
|
);
|
|
expect(program.pathRuleCandidates().map(({ token }) => token)).toEqual([
|
|
join(homedir(), "somewhere"),
|
|
]);
|
|
});
|
|
|
|
it("keeps a $PWD token literal-only after a non-literal cd", async () => {
|
|
// `$PWD` becomes the base-relative `.`, so it inherits the #393
|
|
// unknown-base treatment rather than resolving against the wrong
|
|
// directory — and never fabricates `<cwd>/$PWD/x`.
|
|
const program = await BashProgram.parse(
|
|
'cd "$DIR" && ls "$PWD/x"',
|
|
normalizer,
|
|
);
|
|
const candidate = program
|
|
.pathRuleCandidates()
|
|
.find(({ token }) => token === "./x");
|
|
expect(candidate?.path.matchValues()).toEqual(["./x"]);
|
|
expect(candidate?.path.boundaryValue()).toBe("");
|
|
});
|
|
|
|
it("leaves a variable outside the resolvable set unresolved", async () => {
|
|
const program = await BashProgram.parse('ls "$CONFIG/x"', normalizer);
|
|
expect(program.pathRuleCandidates().map(({ token }) => token)).toEqual([
|
|
"$CONFIG/x",
|
|
]);
|
|
});
|
|
});
|
|
});
|
|
|
|
});
|