mirror of
https://bitbucket.org/siakitem/my-pi.git
synced 2026-08-28 16:45:22 +00:00
147 lines
5.6 KiB
TypeScript
147 lines
5.6 KiB
TypeScript
import type { PathFlavor } from "#src/path/path-flavor";
|
||
|
||
import {
|
||
canonicalNormalizePathForComparison,
|
||
getPathPolicyValues,
|
||
normalizePathForComparison,
|
||
} from "./path-normalization";
|
||
|
||
/**
|
||
* A path's two representations held behind type-distinct accessors.
|
||
*
|
||
* A single `string` carrying both meanings was the root cause of [#418]:
|
||
* both external-directory gates matched config patterns against the
|
||
* symlink-resolved (canonical) path instead of the typed (lexical) path,
|
||
* defeating a configured `/tmp/*` allow.
|
||
*
|
||
* `AccessPath` makes the misuse a compile error:
|
||
* - {@link matchValues} returns `string[]` — the lexical alias union ∪ canonical,
|
||
* for `external_directory` pattern matching.
|
||
* - {@link boundaryValue} returns `string` — the canonical form, for
|
||
* outside-CWD containment and infra-read checks.
|
||
* - {@link value} returns `string` — the lexical absolute form, for display,
|
||
* approval patterns, decision values, and logs.
|
||
* - {@link resolvedAlias} returns `string | undefined` — the canonical form
|
||
* only when it names a location distinct from the lexical form, for
|
||
* disclosing a symlink target in a prompt or denial message.
|
||
*
|
||
* Construct via {@link forPath} (resolved, with optional cd-folded base) or
|
||
* {@link forLiteral} (literal-only, for an unknown base); the constructor is
|
||
* private.
|
||
*/
|
||
export class AccessPath {
|
||
private constructor(
|
||
private readonly lexical: string,
|
||
private readonly matchAliases: readonly string[],
|
||
private readonly canonical: string,
|
||
) {}
|
||
|
||
/**
|
||
* Pattern-match values for the `external_directory` surface: the lexical
|
||
* alias union plus the canonical alias, so a config pattern on either the
|
||
* typed form (`/tmp/*`) or the symlink-resolved form (`/private/tmp/*`)
|
||
* matches (#418).
|
||
*
|
||
* Collapses to the lexical aliases when the canonical equals one of them
|
||
* (e.g. when the path is not a symlink).
|
||
*/
|
||
matchValues(): string[] {
|
||
return this.canonical
|
||
? [...new Set([...this.matchAliases, this.canonical])]
|
||
: [...this.matchAliases];
|
||
}
|
||
|
||
/**
|
||
* Canonical (symlink-resolved, win32-lowercased) form, for the outside-CWD
|
||
* boundary decision and Pi infrastructure-read containment checks.
|
||
*
|
||
* Returns `""` when the path could not be resolved (empty input).
|
||
*/
|
||
boundaryValue(): string {
|
||
return this.canonical;
|
||
}
|
||
|
||
/**
|
||
* Lexical (as-typed, normalized but not symlink-resolved) form, for display,
|
||
* approval patterns, decision values, and log messages.
|
||
*
|
||
* Returns `""` for empty input.
|
||
*/
|
||
value(): string {
|
||
return this.lexical;
|
||
}
|
||
|
||
/**
|
||
* The canonical (symlink-resolved) form when it names a location distinct
|
||
* from the lexical form — for disclosing the resolved target in a prompt or
|
||
* denial message. `undefined` when the path is not a symlink (canonical
|
||
* equals lexical) or has no canonical (literal-only / empty input).
|
||
*/
|
||
resolvedAlias(): string | undefined {
|
||
if (!this.canonical || this.canonical === this.lexical) {
|
||
return undefined;
|
||
}
|
||
return this.canonical;
|
||
}
|
||
|
||
/**
|
||
* Build an `AccessPath` for a tool-input or bash-token path, resolved against
|
||
* `resolveBase` (the cd-folded effective directory; defaults to `cwd`).
|
||
*
|
||
* Serves every path surface: the tool path gate, the tool external-directory
|
||
* gate, and the bash path/external-directory gates (which pass a cd-resolved
|
||
* `resolveBase`).
|
||
*
|
||
* - `matchValues()` returns the lexical alias union from `getPathPolicyValues`
|
||
* plus the canonical alias from `canonicalNormalizePathForComparison`
|
||
* (#418), so a config pattern on either the typed or symlink-resolved form
|
||
* matches.
|
||
* - `boundaryValue()` returns
|
||
* `canonicalNormalizePathForComparison(pathValue, resolveBase)`, which is
|
||
* win32-lowercased (#382) — do not substitute a raw `canonicalizePath`
|
||
* output here.
|
||
* - `value()` returns `normalizePathForComparison(pathValue, resolveBase)`,
|
||
* the absolute lexical form.
|
||
*/
|
||
static forPath(
|
||
pathValue: string,
|
||
options: { cwd: string; resolveBase?: string; flavor: PathFlavor },
|
||
): AccessPath {
|
||
const { cwd, resolveBase = cwd, flavor } = options;
|
||
return new AccessPath(
|
||
normalizePathForComparison(pathValue, resolveBase, flavor),
|
||
getPathPolicyValues(pathValue, { cwd, resolveBase }, flavor),
|
||
canonicalNormalizePathForComparison(pathValue, resolveBase, flavor),
|
||
);
|
||
}
|
||
|
||
/**
|
||
* Build a literal-only `AccessPath` for a path whose effective base is
|
||
* unknown (a relative bash token after a non-literal `cd`).
|
||
*
|
||
* Carries no canonical alias and no absolute resolution — `matchValues()` is
|
||
* `[literal]` (or `[]` when empty) and `boundaryValue()` is `""` — so no
|
||
* spurious absolute or symlink-resolved rule can match (#393).
|
||
*/
|
||
static forLiteral(literal: string): AccessPath {
|
||
if (!literal) return new AccessPath("", [], "");
|
||
return new AccessPath(literal, [literal], "");
|
||
}
|
||
|
||
/**
|
||
* Build an `AccessPath` for a Git Bash/MSYS device path (`/dev/null`,
|
||
* `/dev/std{in,out,err}`) seen in a bash command on a win32 host.
|
||
*
|
||
* The token names an MSYS runtime device, not a filesystem path, so it is
|
||
* preserved verbatim across all three representations — `value()`,
|
||
* `boundaryValue()`, and `matchValues()` are the device path itself, never
|
||
* `win32.resolve`-mangled into `c:\dev\null`. The identical lexical and
|
||
* canonical forms let the boundary check reach `isSafeSystemPath` (so the
|
||
* device never triggers `external_directory`) while a config rule still
|
||
* matches the path as typed.
|
||
*/
|
||
static forDevice(devicePath: string): AccessPath {
|
||
return new AccessPath(devicePath, [devicePath], devicePath);
|
||
}
|
||
}
|