diff --git a/install-scripts/install-safe-chain.ps1 b/install-scripts/install-safe-chain.ps1 index 0d7b745..a11edf6 100644 --- a/install-scripts/install-safe-chain.ps1 +++ b/install-scripts/install-safe-chain.ps1 @@ -25,17 +25,17 @@ function Test-InstallDir { return @{ Ok = $false; Reason = "-InstallDir must not contain the PATH separator ($([System.IO.Path]::PathSeparator))" } } + $inputSegments = $Dir.Split([char[]]@('\', '/'), [System.StringSplitOptions]::RemoveEmptyEntries) + if ($inputSegments -contains "..") { + return @{ Ok = $false; Reason = "-InstallDir must not contain path traversal segments" } + } + $normalized = [System.IO.Path]::GetFullPath($Dir) $root = [System.IO.Path]::GetPathRoot($normalized) if ($normalized.TrimEnd('\', '/') -eq $root.TrimEnd('\', '/')) { return @{ Ok = $false; Reason = "-InstallDir cannot be a root or drive-root directory" } } - $segments = $normalized.Substring($root.Length).Split([char[]]@('\', '/'), [System.StringSplitOptions]::RemoveEmptyEntries) - if ($segments -contains "..") { - return @{ Ok = $false; Reason = "-InstallDir must not contain path traversal segments" } - } - return @{ Ok = $true; Normalized = $normalized } } diff --git a/packages/safe-chain/src/config/safeChainDir.js b/packages/safe-chain/src/config/safeChainDir.js index 595300a..6762d0b 100644 --- a/packages/safe-chain/src/config/safeChainDir.js +++ b/packages/safe-chain/src/config/safeChainDir.js @@ -1,5 +1,6 @@ import os from "os"; import path from "path"; +import { fileURLToPath } from "url"; import { getInstalledSafeChainDir } from "../installLocation.js"; /** @@ -8,3 +9,49 @@ import { getInstalledSafeChainDir } from "../installLocation.js"; export function getSafeChainBaseDir() { return getInstalledSafeChainDir() ?? path.join(os.homedir(), ".safe-chain"); } + +/** + * @returns {string} + */ +export function getBinDir() { + return path.join(getSafeChainBaseDir(), "bin"); +} + +/** + * @returns {string} + */ +export function getShimsDir() { + return path.join(getSafeChainBaseDir(), "shims"); +} + +/** + * @returns {string} + */ +export function getScriptsDir() { + return path.join(getSafeChainBaseDir(), "scripts"); +} + +/** + * @returns {string} + */ +export function getCertsDir() { + return path.join(getSafeChainBaseDir(), "certs"); +} + +/** + * @param {string} moduleUrl + * @param {string} fileName + * @returns {string} + */ +export function getStartupScriptSourcePath(moduleUrl, fileName) { + return path.join(path.dirname(fileURLToPath(moduleUrl)), "startup-scripts", fileName); +} + +/** + * @param {string} moduleUrl + * @param {string} fileName + * @returns {string} + */ +export function getPathWrapperTemplatePath(moduleUrl, fileName) { + return path.join(path.dirname(fileURLToPath(moduleUrl)), "path-wrappers", "templates", fileName); +} diff --git a/packages/safe-chain/src/registryProxy/certUtils.js b/packages/safe-chain/src/registryProxy/certUtils.js index 50fad7b..3918177 100644 --- a/packages/safe-chain/src/registryProxy/certUtils.js +++ b/packages/safe-chain/src/registryProxy/certUtils.js @@ -1,16 +1,12 @@ import forge from "node-forge"; import path from "path"; import fs from "fs"; -import { getSafeChainBaseDir } from "../config/safeChainDir.js"; +import { getCertsDir } from "../config/safeChainDir.js"; const ca = loadCa(); const certCache = new Map(); -function getCertFolder() { - return path.join(getSafeChainBaseDir(), "certs"); -} - /** * @param {forge.pki.PublicKey} publicKey * @returns {string} @@ -23,7 +19,7 @@ function createKeyIdentifier(publicKey) { } export function getCaCertPath() { - return path.join(getCertFolder(), "ca-cert.pem"); + return path.join(getCertsDir(), "ca-cert.pem"); } /** @@ -115,7 +111,7 @@ export function generateCertForHost(hostname) { } function loadCa() { - const certFolder = getCertFolder(); + const certFolder = getCertsDir(); const keyPath = path.join(certFolder, "ca-key.pem"); const certPath = path.join(certFolder, "ca-cert.pem"); diff --git a/packages/safe-chain/src/registryProxy/certUtils.spec.js b/packages/safe-chain/src/registryProxy/certUtils.spec.js index c715c8c..4bf8c95 100644 --- a/packages/safe-chain/src/registryProxy/certUtils.spec.js +++ b/packages/safe-chain/src/registryProxy/certUtils.spec.js @@ -9,6 +9,7 @@ describe("certUtils", () => { mock.module("../config/safeChainDir.js", { namedExports: { getSafeChainBaseDir: () => installedSafeChainDir ?? "/home/test/.safe-chain", + getCertsDir: () => `${installedSafeChainDir ?? "/home/test/.safe-chain"}/certs`, }, }); }); diff --git a/packages/safe-chain/src/shell-integration/helpers.js b/packages/safe-chain/src/shell-integration/helpers.js index 3dd73aa..e763a5f 100644 --- a/packages/safe-chain/src/shell-integration/helpers.js +++ b/packages/safe-chain/src/shell-integration/helpers.js @@ -3,7 +3,6 @@ import * as os from "os"; import fs from "fs"; import path from "path"; import { ECOSYSTEM_JS, ECOSYSTEM_PY } from "../config/settings.js"; -import { getSafeChainBaseDir } from "../config/safeChainDir.js"; import { safeSpawn } from "../utils/safeSpawn.js"; import { ui } from "../environment/userInteraction.js"; @@ -122,34 +121,6 @@ export function getPackageManagerList() { return `${tools.join(", ")}, and ${lastTool} commands`; } -/** - * Returns the safe-chain base directory. - * Uses the packaged binary location when available, otherwise defaults to ~/.safe-chain. - * @returns {string} - */ -export { getSafeChainBaseDir }; - -/** - * @returns {string} - */ -export function getBinDir() { - return path.join(getSafeChainBaseDir(), "bin"); -} - -/** - * @returns {string} - */ -export function getShimsDir() { - return path.join(getSafeChainBaseDir(), "shims"); -} - -/** - * @returns {string} - */ -export function getScriptsDir() { - return path.join(getSafeChainBaseDir(), "scripts"); -} - /** * @param {string} executableName * diff --git a/packages/safe-chain/src/shell-integration/helpers.spec.js b/packages/safe-chain/src/shell-integration/helpers.spec.js index 8870451..e93a690 100644 --- a/packages/safe-chain/src/shell-integration/helpers.spec.js +++ b/packages/safe-chain/src/shell-integration/helpers.spec.js @@ -186,22 +186,27 @@ describe("removeLinesMatchingPatternTests", () => { describe("getSafeChainBaseDir / getBinDir / getShimsDir / getScriptsDir", () => { it("defaults base dir to ~/.safe-chain when no packaged install dir is available", async () => { - const { getSafeChainBaseDir } = await import("./helpers.js"); + const { getSafeChainBaseDir } = await import("../config/safeChainDir.js"); assert.strictEqual(getSafeChainBaseDir(), path.join(homedir(), ".safe-chain")); }); it("getBinDir returns ~/.safe-chain/bin by default", async () => { - const { getBinDir } = await import("./helpers.js"); + const { getBinDir } = await import("../config/safeChainDir.js"); assert.strictEqual(getBinDir(), path.join(homedir(), ".safe-chain", "bin")); }); it("getShimsDir returns ~/.safe-chain/shims by default", async () => { - const { getShimsDir } = await import("./helpers.js"); + const { getShimsDir } = await import("../config/safeChainDir.js"); assert.strictEqual(getShimsDir(), path.join(homedir(), ".safe-chain", "shims")); }); it("getScriptsDir returns ~/.safe-chain/scripts by default", async () => { - const { getScriptsDir } = await import("./helpers.js"); + const { getScriptsDir } = await import("../config/safeChainDir.js"); assert.strictEqual(getScriptsDir(), path.join(homedir(), ".safe-chain", "scripts")); }); + + it("getCertsDir returns ~/.safe-chain/certs by default", async () => { + const { getCertsDir } = await import("../config/safeChainDir.js"); + assert.strictEqual(getCertsDir(), path.join(homedir(), ".safe-chain", "certs")); + }); }); diff --git a/packages/safe-chain/src/shell-integration/setup-ci.js b/packages/safe-chain/src/shell-integration/setup-ci.js index 1986bba..f9e6767 100644 --- a/packages/safe-chain/src/shell-integration/setup-ci.js +++ b/packages/safe-chain/src/shell-integration/setup-ci.js @@ -1,24 +1,14 @@ import chalk from "chalk"; import { ui } from "../environment/userInteraction.js"; -import { getPackageManagerList, knownAikidoTools, getShimsDir, getBinDir } from "./helpers.js"; +import { getPackageManagerList, knownAikidoTools } from "./helpers.js"; +import { + getShimsDir, + getBinDir, + getPathWrapperTemplatePath, +} from "../config/safeChainDir.js"; import fs from "fs"; import os from "os"; import path from "path"; -import { fileURLToPath } from "url"; - -/** @type {string} */ -// This checks the current file's dirname in a way that's compatible with: -// - Modulejs (import.meta.url) -// - ES modules (__dirname) -// This is needed because safe-chain's npm package is built using ES modules, -// but building the binaries requires commonjs. -let dirname; -if (import.meta.url) { - const filename = fileURLToPath(import.meta.url); - dirname = path.dirname(filename); -} else { - dirname = __dirname; -} /** * Loops over the detected shells and calls the setup function for each. @@ -50,12 +40,7 @@ export async function setupCi() { */ function createUnixShims(shimsDir) { // Read the template file - const templatePath = path.resolve( - dirname, - "path-wrappers", - "templates", - "unix-wrapper.template.sh" - ); + const templatePath = getPathWrapperTemplatePath(import.meta.url, "unix-wrapper.template.sh"); if (!fs.existsSync(templatePath)) { ui.writeError(`Template file not found: ${templatePath}`); @@ -89,12 +74,7 @@ function createUnixShims(shimsDir) { */ function createWindowsShims(shimsDir) { // Read the template file - const templatePath = path.resolve( - dirname, - "path-wrappers", - "templates", - "windows-wrapper.template.cmd" - ); + const templatePath = getPathWrapperTemplatePath(import.meta.url, "windows-wrapper.template.cmd"); if (!fs.existsSync(templatePath)) { ui.writeError(`Windows template file not found: ${templatePath}`); diff --git a/packages/safe-chain/src/shell-integration/setup-ci.spec.js b/packages/safe-chain/src/shell-integration/setup-ci.spec.js index de570f5..7af41d6 100644 --- a/packages/safe-chain/src/shell-integration/setup-ci.spec.js +++ b/packages/safe-chain/src/shell-integration/setup-ci.spec.js @@ -50,8 +50,15 @@ describe("Setup CI shell integration", () => { { tool: "yarn", aikidoCommand: "aikido-yarn" }, ], getPackageManagerList: () => "npm, yarn", + }, + }); + + mock.module("../config/safeChainDir.js", { + namedExports: { getShimsDir: () => mockShimsDir, getBinDir: () => path.join(mockHomeDir, ".safe-chain", "bin"), + getPathWrapperTemplatePath: (_moduleUrl, fileName) => + path.join(mockTemplateDir, "path-wrappers", "templates", fileName), }, }); @@ -64,22 +71,6 @@ describe("Setup CI shell integration", () => { }, }); - // Mock path module to resolve templates correctly - mock.module("path", { - namedExports: { - join: path.join, - dirname: () => mockTemplateDir, - resolve: (...args) => path.resolve(mockTemplateDir, ...args.slice(1)), - }, - }); - - // Mock fileURLToPath - mock.module("url", { - namedExports: { - fileURLToPath: () => path.join(mockTemplateDir, "setup-ci.js"), - }, - }); - // Import setupCi module after mocking setupCi = (await import("./setup-ci.js")).setupCi; }); diff --git a/packages/safe-chain/src/shell-integration/setup.js b/packages/safe-chain/src/shell-integration/setup.js index 120723a..04534df 100644 --- a/packages/safe-chain/src/shell-integration/setup.js +++ b/packages/safe-chain/src/shell-integration/setup.js @@ -1,28 +1,10 @@ import chalk from "chalk"; import { ui } from "../environment/userInteraction.js"; import { detectShells } from "./shellDetection.js"; -import { - knownAikidoTools, - getPackageManagerList, - getScriptsDir, -} from "./helpers.js"; +import { knownAikidoTools, getPackageManagerList } from "./helpers.js"; +import { getScriptsDir, getStartupScriptSourcePath } from "../config/safeChainDir.js"; import fs from "fs"; import path from "path"; -import { fileURLToPath } from "url"; - -/** @type {string} */ -// This checks the current file's dirname in a way that's compatible with: -// - Modulejs (import.meta.url) -// - ES modules (__dirname) -// This is needed because safe-chain's npm package is built using ES modules, -// but building the binaries requires commonjs. -let dirname; -if (import.meta.url) { - const filename = fileURLToPath(import.meta.url); - dirname = path.dirname(filename); -} else { - dirname = __dirname; -} /** * Loops over the detected shells and calls the setup function for each. @@ -122,7 +104,7 @@ function copyStartupFiles() { fs.mkdirSync(targetDir, { recursive: true }); } - const sourcePath = path.join(dirname, "startup-scripts", file); + const sourcePath = getStartupScriptSourcePath(import.meta.url, file); fs.copyFileSync(sourcePath, targetPath); } } diff --git a/packages/safe-chain/src/shell-integration/supported-shells/bash.js b/packages/safe-chain/src/shell-integration/supported-shells/bash.js index 4c3334c..e106928 100644 --- a/packages/safe-chain/src/shell-integration/supported-shells/bash.js +++ b/packages/safe-chain/src/shell-integration/supported-shells/bash.js @@ -2,8 +2,8 @@ import { addLineToFile, doesExecutableExistOnSystem, removeLinesMatchingPattern, - getScriptsDir, } from "../helpers.js"; +import { getScriptsDir } from "../../config/safeChainDir.js"; import { execSync, spawnSync } from "child_process"; import * as os from "os"; import path from "path"; diff --git a/packages/safe-chain/src/shell-integration/supported-shells/bash.spec.js b/packages/safe-chain/src/shell-integration/supported-shells/bash.spec.js index f0a56d2..a6b09a0 100644 --- a/packages/safe-chain/src/shell-integration/supported-shells/bash.spec.js +++ b/packages/safe-chain/src/shell-integration/supported-shells/bash.spec.js @@ -19,7 +19,6 @@ describe("Bash shell integration", () => { mock.module("../helpers.js", { namedExports: { doesExecutableExistOnSystem: () => true, - getScriptsDir: () => "/test-home/.safe-chain/scripts", addLineToFile: (filePath, line) => { if (!fs.existsSync(filePath)) { fs.writeFileSync(filePath, "", "utf-8"); @@ -36,6 +35,12 @@ describe("Bash shell integration", () => { }, }); + mock.module("../../config/safeChainDir.js", { + namedExports: { + getScriptsDir: () => "/test-home/.safe-chain/scripts", + }, + }); + // Mock child_process execSync mock.module("child_process", { namedExports: { diff --git a/packages/safe-chain/src/shell-integration/supported-shells/fish.js b/packages/safe-chain/src/shell-integration/supported-shells/fish.js index 29bc485..95c867b 100644 --- a/packages/safe-chain/src/shell-integration/supported-shells/fish.js +++ b/packages/safe-chain/src/shell-integration/supported-shells/fish.js @@ -2,8 +2,8 @@ import { addLineToFile, doesExecutableExistOnSystem, removeLinesMatchingPattern, - getScriptsDir, } from "../helpers.js"; +import { getScriptsDir } from "../../config/safeChainDir.js"; import { execSync } from "child_process"; import path from "path"; diff --git a/packages/safe-chain/src/shell-integration/supported-shells/fish.spec.js b/packages/safe-chain/src/shell-integration/supported-shells/fish.spec.js index 0933b6e..c1c5715 100644 --- a/packages/safe-chain/src/shell-integration/supported-shells/fish.spec.js +++ b/packages/safe-chain/src/shell-integration/supported-shells/fish.spec.js @@ -17,7 +17,6 @@ describe("Fish shell integration", () => { mock.module("../helpers.js", { namedExports: { doesExecutableExistOnSystem: () => true, - getScriptsDir: () => "/test-home/.safe-chain/scripts", addLineToFile: (filePath, line) => { if (!fs.existsSync(filePath)) { fs.writeFileSync(filePath, "", "utf-8"); @@ -34,6 +33,12 @@ describe("Fish shell integration", () => { }, }); + mock.module("../../config/safeChainDir.js", { + namedExports: { + getScriptsDir: () => "/test-home/.safe-chain/scripts", + }, + }); + // Mock child_process execSync mock.module("child_process", { namedExports: { diff --git a/packages/safe-chain/src/shell-integration/supported-shells/powershell.js b/packages/safe-chain/src/shell-integration/supported-shells/powershell.js index 3340bb4..2717e36 100644 --- a/packages/safe-chain/src/shell-integration/supported-shells/powershell.js +++ b/packages/safe-chain/src/shell-integration/supported-shells/powershell.js @@ -3,8 +3,8 @@ import { doesExecutableExistOnSystem, removeLinesMatchingPattern, validatePowerShellExecutionPolicy, - getScriptsDir, } from "../helpers.js"; +import { getScriptsDir } from "../../config/safeChainDir.js"; import { execSync } from "child_process"; import path from "path"; diff --git a/packages/safe-chain/src/shell-integration/supported-shells/powershell.spec.js b/packages/safe-chain/src/shell-integration/supported-shells/powershell.spec.js index 1d9f65c..b14c73f 100644 --- a/packages/safe-chain/src/shell-integration/supported-shells/powershell.spec.js +++ b/packages/safe-chain/src/shell-integration/supported-shells/powershell.spec.js @@ -40,6 +40,11 @@ describe("PowerShell Core shell integration", () => { fs.writeFileSync(filePath, filteredLines.join("\n"), "utf-8"); }, validatePowerShellExecutionPolicy: () => executionPolicyResult, + }, + }); + + mock.module("../../config/safeChainDir.js", { + namedExports: { getScriptsDir: () => "/test-home/.safe-chain/scripts", }, }); diff --git a/packages/safe-chain/src/shell-integration/supported-shells/windowsPowershell.js b/packages/safe-chain/src/shell-integration/supported-shells/windowsPowershell.js index d458027..7213d38 100644 --- a/packages/safe-chain/src/shell-integration/supported-shells/windowsPowershell.js +++ b/packages/safe-chain/src/shell-integration/supported-shells/windowsPowershell.js @@ -3,8 +3,8 @@ import { doesExecutableExistOnSystem, removeLinesMatchingPattern, validatePowerShellExecutionPolicy, - getScriptsDir, } from "../helpers.js"; +import { getScriptsDir } from "../../config/safeChainDir.js"; import { execSync } from "child_process"; import path from "path"; diff --git a/packages/safe-chain/src/shell-integration/supported-shells/windowsPowershell.spec.js b/packages/safe-chain/src/shell-integration/supported-shells/windowsPowershell.spec.js index 621b380..277a3f7 100644 --- a/packages/safe-chain/src/shell-integration/supported-shells/windowsPowershell.spec.js +++ b/packages/safe-chain/src/shell-integration/supported-shells/windowsPowershell.spec.js @@ -40,6 +40,11 @@ describe("Windows PowerShell shell integration", () => { fs.writeFileSync(filePath, filteredLines.join("\n"), "utf-8"); }, validatePowerShellExecutionPolicy: () => executionPolicyResult, + }, + }); + + mock.module("../../config/safeChainDir.js", { + namedExports: { getScriptsDir: () => "/test-home/.safe-chain/scripts", }, }); diff --git a/packages/safe-chain/src/shell-integration/supported-shells/zsh.js b/packages/safe-chain/src/shell-integration/supported-shells/zsh.js index 18917fd..c3e8d73 100644 --- a/packages/safe-chain/src/shell-integration/supported-shells/zsh.js +++ b/packages/safe-chain/src/shell-integration/supported-shells/zsh.js @@ -2,8 +2,8 @@ import { addLineToFile, doesExecutableExistOnSystem, removeLinesMatchingPattern, - getScriptsDir, } from "../helpers.js"; +import { getScriptsDir } from "../../config/safeChainDir.js"; import { execSync } from "child_process"; import path from "path"; diff --git a/packages/safe-chain/src/shell-integration/supported-shells/zsh.spec.js b/packages/safe-chain/src/shell-integration/supported-shells/zsh.spec.js index 41e1bd1..50af5ca 100644 --- a/packages/safe-chain/src/shell-integration/supported-shells/zsh.spec.js +++ b/packages/safe-chain/src/shell-integration/supported-shells/zsh.spec.js @@ -17,7 +17,6 @@ describe("Zsh shell integration", () => { mock.module("../helpers.js", { namedExports: { doesExecutableExistOnSystem: () => true, - getScriptsDir: () => "/test-home/.safe-chain/scripts", addLineToFile: (filePath, line) => { if (!fs.existsSync(filePath)) { fs.writeFileSync(filePath, "", "utf-8"); @@ -34,6 +33,12 @@ describe("Zsh shell integration", () => { }, }); + mock.module("../../config/safeChainDir.js", { + namedExports: { + getScriptsDir: () => "/test-home/.safe-chain/scripts", + }, + }); + // Mock child_process execSync mock.module("child_process", { namedExports: { diff --git a/packages/safe-chain/src/shell-integration/teardown.js b/packages/safe-chain/src/shell-integration/teardown.js index e5f149d..cdeeae2 100644 --- a/packages/safe-chain/src/shell-integration/teardown.js +++ b/packages/safe-chain/src/shell-integration/teardown.js @@ -1,7 +1,8 @@ import chalk from "chalk"; import { ui } from "../environment/userInteraction.js"; import { detectShells } from "./shellDetection.js"; -import { knownAikidoTools, getPackageManagerList, getShimsDir, getScriptsDir } from "./helpers.js"; +import { knownAikidoTools, getPackageManagerList } from "./helpers.js"; +import { getShimsDir, getScriptsDir } from "../config/safeChainDir.js"; import fs from "fs"; /**