Luigit
repositories / pi-ext

pi-ext

bugabingas pi extensions

owned by admin

extensions/git-safe/git-policy.ts

Raw
import { rmSync } from "node:fs";
import {
	chmod,
	mkdtemp,
	readFile,
	realpath,
	writeFile,
} from "node:fs/promises";
import { tmpdir } from "node:os";
import { delimiter, join } from "node:path";
import { findExecutable } from "./src/pi-ext-executable.ts";

const POLICY_MARKER = "# pi-git-safe-policy-v1";
const COMMAND_PREFIX_END = "# pi-command-prefix-end";

export interface GitPolicyRuntime {
	wrapperDir: string;
	realGit: string;
}

function pathKey(env: NodeJS.ProcessEnv): string {
	return Object.keys(env).find((key) => key.toLowerCase() === "path") ?? "PATH";
}

export async function findGitExecutable(
	env: NodeJS.ProcessEnv = process.env,
	platform = process.platform,
): Promise<string | undefined> {
	const path = env[pathKey(env)] ?? "";
	const names = platform === "win32" ? ["git.exe"] : ["git"];
	for (const dir of path.split(delimiter).filter(Boolean)) {
		const candidate = findExecutable(names, {
			path: dir.replace(/^"|"$/gu, ""),
			platform,
		});
		if (!candidate) continue;
		try {
			return await realpath(candidate);
		} catch {
			// Try the next PATH entry if canonicalization fails.
		}
	}
	return undefined;
}

const RUNTIME_CACHE = Symbol.for("@bugabinga/pi-ext-git-safe/runtime-cache-v1");
type RuntimeCache = {
	promises: Map<string, Promise<GitPolicyRuntime>>;
	wrapperDirs: Set<string>;
	cleanupRegistered: boolean;
};
type RuntimeGlobal = typeof globalThis & { [RUNTIME_CACHE]?: RuntimeCache };
const runtimeGlobal = globalThis as RuntimeGlobal;
const existingRuntimeCache = runtimeGlobal[RUNTIME_CACHE];
const runtimeCache: RuntimeCache = existingRuntimeCache ?? {
	promises: new Map(),
	wrapperDirs: new Set(),
	cleanupRegistered: false,
};
if (!existingRuntimeCache) runtimeGlobal[RUNTIME_CACHE] = runtimeCache;
if (!runtimeCache.cleanupRegistered) {
	runtimeCache.cleanupRegistered = true;
	process.once("exit", () => {
		for (const wrapperDir of runtimeCache.wrapperDirs) {
			try {
				rmSync(wrapperDir, { recursive: true, force: true });
			} catch {}
		}
	});
}

async function generateGitPolicyRuntime(
	env: NodeJS.ProcessEnv,
	platform: NodeJS.Platform,
): Promise<GitPolicyRuntime> {
	const realGit = await findGitExecutable(env, platform);
	if (!realGit) throw new Error("Git executable not found on PATH");
	const wrapperDir = await mkdtemp(join(tmpdir(), "pi-git-safe-policy-"));
	const wrapperScript = join(wrapperDir, "git-wrapper.cjs");
	const launcher = join(wrapperDir, "git");
	const wrapperScriptArgument =
		platform === "win32" ? wrapperScript : bashPath(wrapperScript);
	try {
		const template = await readFile(
			new URL("./git-wrapper.cjs", import.meta.url),
			"utf8",
		);
		await Promise.all([
			writeFile(
				wrapperScript,
				template.replace('"__PI_GIT_SAFE_REAL_GIT__"', JSON.stringify(realGit)),
				{ encoding: "utf8", mode: 0o600 },
			),
			writeFile(
				launcher,
				`#!/bin/sh
if [ ! -f ${shellQuote(bashPath(wrapperScript))} ]; then
	printf '%s\\n' 'git: command unavailable; ask the user to restart Pi' >&2
	exit 127
fi
exec ${shellQuote(bashPath(process.execPath))} ${shellQuote(wrapperScriptArgument)} "$@"
`,
				{ encoding: "utf8", mode: 0o500 },
			),
			writeFile(
				join(wrapperDir, "git.cmd"),
				`@echo off\r\n"${process.execPath}" "%~dp0git-wrapper.cjs" %*\r\n`,
				{ encoding: "utf8", mode: 0o500 },
			),
		]);
		await chmod(launcher, 0o500);
		runtimeCache.wrapperDirs.add(wrapperDir);
		return { wrapperDir, realGit };
	} catch (error) {
		rmSync(wrapperDir, { recursive: true, force: true });
		throw error;
	}
}

export function createGitPolicyRuntime(
	env: NodeJS.ProcessEnv = process.env,
	platform: NodeJS.Platform = process.platform,
): Promise<GitPolicyRuntime> {
	const key = `${platform}\0${process.execPath}\0${env[pathKey(env)] ?? ""}`;
	const existing = runtimeCache.promises.get(key);
	if (existing) return existing;
	const runtime = generateGitPolicyRuntime(env, platform).catch((error) => {
		runtimeCache.promises.delete(key);
		throw error;
	});
	runtimeCache.promises.set(key, runtime);
	return runtime;
}

function shellQuote(value: string): string {
	return `'${value.replaceAll("'", `'\\''`)}'`;
}

function bashPath(value: string): string {
	const normalized = value.replaceAll("\\", "/");
	const drive = normalized.match(/^([A-Za-z]):\/(.*)$/u);
	return drive ? `/${drive[1].toLowerCase()}/${drive[2]}` : normalized;
}

export function applyGitPolicy(
	command: string,
	runtime: GitPolicyRuntime,
): string {
	const wrapper = shellQuote(bashPath(join(runtime.wrapperDir, "git")));
	return `${POLICY_MARKER}
export PATH=${shellQuote(bashPath(runtime.wrapperDir))}:"$PATH"
git() {
	${wrapper} "$@"
}
git.exe() {
	${wrapper} "$@"
}
command() {
	local -a __pi_git_safe_args=("$@")
	local __pi_git_safe_index=0
	while [[ "\${__pi_git_safe_args[$__pi_git_safe_index]}" == "-p" || "\${__pi_git_safe_args[$__pi_git_safe_index]}" == "--" ]]; do
		((__pi_git_safe_index++))
	done
	if [[ "\${__pi_git_safe_args[$__pi_git_safe_index]}" == "git" || "\${__pi_git_safe_args[$__pi_git_safe_index]}" == "git.exe" ]]; then
		${wrapper} "\${__pi_git_safe_args[@]:$((__pi_git_safe_index + 1))}"
	else
		builtin command "$@"
	fi
}
env() {
	local -a __pi_git_safe_args=("$@")
	local __pi_git_safe_index=0
	while (( __pi_git_safe_index < \${#__pi_git_safe_args[@]} )); do
		case "\${__pi_git_safe_args[$__pi_git_safe_index]}" in
			--)
				((__pi_git_safe_index++))
				break
				;;
			--u*=*|--c*=*|--s*=*|--a*=*)
				((__pi_git_safe_index++))
				;;
			-u|--u*|-C|--c*|-S|--s*|--a*)
				((__pi_git_safe_index += 2))
				;;
			-*|*=*)
				((__pi_git_safe_index++))
				;;
			*)
				break
				;;
		esac
	done
	if [[ "\${__pi_git_safe_args[$__pi_git_safe_index]}" == "git" || "\${__pi_git_safe_args[$__pi_git_safe_index]}" == "git.exe" ]]; then
		__pi_git_safe_args[$__pi_git_safe_index]=${wrapper}
	fi
	builtin command env "\${__pi_git_safe_args[@]}"
}
xargs() {
	local -a __pi_git_safe_args=("$@")
	local __pi_git_safe_index=0
	while (( __pi_git_safe_index < \${#__pi_git_safe_args[@]} )); do
		case "\${__pi_git_safe_args[$__pi_git_safe_index]}" in
			--)
				((__pi_git_safe_index++))
				break
				;;
			--arg*=*|--e*=*|--r*=*|--m*=*|--d*=*)
				((__pi_git_safe_index++))
				;;
			-a|-E|-I|-L|-n|-P|-s|-d|--arg*|--e*|--r*|--m*|--d*)
				((__pi_git_safe_index += 2))
				;;
			-*)
				((__pi_git_safe_index++))
				;;
			*)
				break
				;;
		esac
	done
	if [[ "\${__pi_git_safe_args[$__pi_git_safe_index]}" == "git" || "\${__pi_git_safe_args[$__pi_git_safe_index]}" == "git.exe" ]]; then
		__pi_git_safe_args[$__pi_git_safe_index]=${wrapper}
	fi
	builtin command xargs "\${__pi_git_safe_args[@]}"
}
export -f git git.exe command env xargs
${COMMAND_PREFIX_END}
${command}`;
}