diff --git a/src/index.ts b/src/index.ts index 92e2a59..a335b88 100644 --- a/src/index.ts +++ b/src/index.ts @@ -469,7 +469,19 @@ function generateShShim (src: string, to: string, opts: InternalOptions): string let progArgs = opts.progArgs ? `${opts.progArgs.join(` `)} ` : '' // #!/bin/sh - // basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')") + // # Resolve $0 through symlinks so basedir is the shim's real directory. + // # Cap hops at the kernel's ELOOP limit so a cycle cannot hang the shim. + // link="$0" + // hops=0 + // while [ -L "$link" ] && [ "$hops" -lt 40 ]; do + // hops=$((hops+1)) + // target=$(readlink "$link") + // case "$target" in + // /*) link="$target" ;; + // *) link="$(dirname "$link")/$target" ;; + // esac + // done + // basedir=$(dirname "$(echo "$link" | sed -e 's,\\,/,g')") // basedir_win="$basedir" // exe="" // msys="" @@ -510,7 +522,19 @@ function generateShShim (src: string, to: string, opts: InternalOptions): string let sh = `\ #!/bin/sh -basedir=$(dirname "$(echo "$0" | sed -e 's,\\\\,/,g')") +# Resolve $0 through symlinks so basedir is the shim's real directory. +# Cap hops at the kernel's ELOOP limit so a cycle cannot hang the shim. +link="$0" +hops=0 +while [ -L "$link" ] && [ "$hops" -lt 40 ]; do + hops=$((hops+1)) + target=$(readlink "$link") + case "$target" in + /*) link="$target" ;; + *) link="$(dirname "$link")/$target" ;; + esac +done +basedir=$(dirname "$(echo "$link" | sed -e 's,\\\\,/,g')") basedir_win="$basedir" exe="" msys="" diff --git a/test/e2e.test.js b/test/e2e.test.js index 514fdbb..1fd97f4 100644 --- a/test/e2e.test.js +++ b/test/e2e.test.js @@ -108,3 +108,91 @@ describeOnPosix('sh shim uses POSIX runtime from PATH', () => { assert.doesNotMatch(r.stderr, /node\.exe/) }) }) + +describeOnPosix('sh shim invoked through a chain of external symlinks', () => { + // POSIX shells set $0 to the invoked symlink, not the shim it points at, + // so the shim must follow the chain before deriving basedir + // (https://github.com/pnpm/pnpm/issues/13405). + const makeShimmedTool = async (tempDir) => { + const binDir = path.join(tempDir, 'node_modules', '.bin') + const targetDir = path.join(tempDir, 'node_modules', 'typescript', 'bin') + fs.mkdirSync(binDir, { recursive: true }) + fs.mkdirSync(targetDir, { recursive: true }) + + const target = path.join(targetDir, 'tsc') + fs.writeFileSync(target, '#!/bin/sh\necho "tsc-output"\n', 'utf8') + fs.chmodSync(target, 0o755) + + const shim = path.join(binDir, 'tsc') + await cmdShim(target, shim) + return shim + } + + const runShim = (cmd) => { + const r = spawnSync(cmd, { + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'pipe'], + }) + assert.equal(r.status, 0, `shim exited ${r.status}\nstdout: ${r.stdout}\nstderr: ${r.stderr}`) + return r.stdout.trim() + } + + test('follows a chain of absolute symlinks', async () => { + const tempDir = tempy.directory() + const shim = await makeShimmedTool(tempDir) + + const hop1 = path.join(tempDir, 'symlink_hop_1') + const hop2 = path.join(tempDir, 'symlink_hop_2') + fs.symlinkSync(shim, hop1) + fs.symlinkSync(hop1, hop2) + + assert.equal(runShim(hop2), 'tsc-output') + }) + + test('follows relative symlinks from another directory', async () => { + const tempDir = tempy.directory() + await makeShimmedTool(tempDir) + + // Both hops use relative targets, exercising the shim's + // dirname-composition branch rather than the absolute one. + const localBin = path.join(tempDir, 'usr', 'local', 'bin') + fs.mkdirSync(localBin, { recursive: true }) + const hop1 = path.join(tempDir, 'usr', 'tsc') + fs.symlinkSync(path.join('..', 'node_modules', '.bin', 'tsc'), hop1) + const hop2 = path.join(localBin, 'tsc') + fs.symlinkSync(path.join('..', '..', 'tsc'), hop2) + + assert.equal(runShim(hop2), 'tsc-output') + }) + + test('resolves symlinks when directories contain spaces', async () => { + const tempDir = path.join(tempy.directory(), 'dir with spaces') + fs.mkdirSync(tempDir, { recursive: true }) + await makeShimmedTool(tempDir) + + const hopDir = path.join(tempDir, 'link dir') + fs.mkdirSync(hopDir) + const hop = path.join(hopDir, 'tsc') + fs.symlinkSync(path.join('..', 'node_modules', '.bin', 'tsc'), hop) + + assert.equal(runShim(hop), 'tsc-output') + }) + + test('runs a node-shebang bin through a symlink chain', async () => { + const tempDir = tempy.directory() + const binDir = path.join(tempDir, 'node_modules', '.bin') + const pkgDir = path.join(tempDir, 'node_modules', 'tool') + fs.mkdirSync(binDir, { recursive: true }) + fs.mkdirSync(pkgDir, { recursive: true }) + + const target = path.join(pkgDir, 'cli.js') + fs.writeFileSync(target, '#!/usr/bin/env node\nconsole.log("NODE_BIN_OK")\n', 'utf8') + const shim = path.join(binDir, 'tool') + await cmdShim(target, shim, { createCmdFile: false }) + + const hop = path.join(tempDir, 'tool') + fs.symlinkSync(path.join('node_modules', '.bin', 'tool'), hop) + + assert.equal(runShim(hop), 'NODE_BIN_OK') + }) +}) diff --git a/test/e2e.test.js.snapshot b/test/e2e.test.js.snapshot index 6490475..20db918 100644 --- a/test/e2e.test.js.snapshot +++ b/test/e2e.test.js.snapshot @@ -1,7 +1,19 @@ exports[`create a command shim for a .exe file > shim files 1`] = ` #!/bin/sh -basedir=$(dirname "$(echo "$0" | sed -e 's,\\\\,/,g')") +# Resolve $0 through symlinks so basedir is the shim's real directory. +# Cap hops at the kernel's ELOOP limit so a cycle cannot hang the shim. +link="$0" +hops=0 +while [ -L "$link" ] && [ "$hops" -lt 40 ]; do + hops=$((hops+1)) + target=$(readlink "$link") + case "$target" in + /*) link="$target" ;; + *) link="$(dirname "$link")/$target" ;; + esac +done +basedir=$(dirname "$(echo "$link" | sed -e 's,\\\\,/,g')") basedir_win="$basedir" exe="" msys="" diff --git a/test/test.js.snapshot b/test/test.js.snapshot index 0dfcfff..9b29f19 100644 --- a/test/test.js.snapshot +++ b/test/test.js.snapshot @@ -1,7 +1,19 @@ exports[`batch script > shim files > bat.shim 1`] = ` #!/bin/sh -basedir=$(dirname "$(echo "$0" | sed -e 's,\\\\,/,g')") +# Resolve $0 through symlinks so basedir is the shim's real directory. +# Cap hops at the kernel's ELOOP limit so a cycle cannot hang the shim. +link="$0" +hops=0 +while [ -L "$link" ] && [ "$hops" -lt 40 ]; do + hops=$((hops+1)) + target=$(readlink "$link") + case "$target" in + /*) link="$target" ;; + *) link="$(dirname "$link")/$target" ;; + esac +done +basedir=$(dirname "$(echo "$link" | sed -e 's,\\\\,/,g')") basedir_win="$basedir" exe="" msys="" @@ -104,7 +116,19 @@ exit $ret exports[`custom node executable > shim files > env.shim 1`] = ` #!/bin/sh -basedir=$(dirname "$(echo "$0" | sed -e 's,\\\\,/,g')") +# Resolve $0 through symlinks so basedir is the shim's real directory. +# Cap hops at the kernel's ELOOP limit so a cycle cannot hang the shim. +link="$0" +hops=0 +while [ -L "$link" ] && [ "$hops" -lt 40 ]; do + hops=$((hops+1)) + target=$(readlink "$link") + case "$target" in + /*) link="$target" ;; + *) link="$(dirname "$link")/$target" ;; + esac +done +basedir=$(dirname "$(echo "$link" | sed -e 's,\\\\,/,g')") basedir_win="$basedir" exe="" msys="" @@ -165,7 +189,19 @@ exit $LASTEXITCODE exports[`env shebang > shim files > env.shim 1`] = ` #!/bin/sh -basedir=$(dirname "$(echo "$0" | sed -e 's,\\\\,/,g')") +# Resolve $0 through symlinks so basedir is the shim's real directory. +# Cap hops at the kernel's ELOOP limit so a cycle cannot hang the shim. +link="$0" +hops=0 +while [ -L "$link" ] && [ "$hops" -lt 40 ]; do + hops=$((hops+1)) + target=$(readlink "$link") + case "$target" in + /*) link="$target" ;; + *) link="$(dirname "$link")/$target" ;; + esac +done +basedir=$(dirname "$(echo "$link" | sed -e 's,\\\\,/,g')") basedir_win="$basedir" exe="" msys="" @@ -252,7 +288,19 @@ exit $ret exports[`env shebang with NODE_PATH > shim files > env.shim 1`] = ` #!/bin/sh -basedir=$(dirname "$(echo "$0" | sed -e 's,\\\\,/,g')") +# Resolve $0 through symlinks so basedir is the shim's real directory. +# Cap hops at the kernel's ELOOP limit so a cycle cannot hang the shim. +link="$0" +hops=0 +while [ -L "$link" ] && [ "$hops" -lt 40 ]; do + hops=$((hops+1)) + target=$(readlink "$link") + case "$target" in + /*) link="$target" ;; + *) link="$(dirname "$link")/$target" ;; + esac +done +basedir=$(dirname "$(echo "$link" | sed -e 's,\\\\,/,g')") basedir_win="$basedir" exe="" msys="" @@ -362,7 +410,19 @@ exit $ret exports[`env shebang with PATH extending > shim files > env.shim 1`] = ` #!/bin/sh -basedir=$(dirname "$(echo "$0" | sed -e 's,\\\\,/,g')") +# Resolve $0 through symlinks so basedir is the shim's real directory. +# Cap hops at the kernel's ELOOP limit so a cycle cannot hang the shim. +link="$0" +hops=0 +while [ -L "$link" ] && [ "$hops" -lt 40 ]; do + hops=$((hops+1)) + target=$(readlink "$link") + case "$target" in + /*) link="$target" ;; + *) link="$(dirname "$link")/$target" ;; + esac +done +basedir=$(dirname "$(echo "$link" | sed -e 's,\\\\,/,g')") basedir_win="$basedir" exe="" msys="" @@ -461,7 +521,19 @@ exit $ret exports[`env shebang with args > shim files > env.args.shim 1`] = ` #!/bin/sh -basedir=$(dirname "$(echo "$0" | sed -e 's,\\\\,/,g')") +# Resolve $0 through symlinks so basedir is the shim's real directory. +# Cap hops at the kernel's ELOOP limit so a cycle cannot hang the shim. +link="$0" +hops=0 +while [ -L "$link" ] && [ "$hops" -lt 40 ]; do + hops=$((hops+1)) + target=$(readlink "$link") + case "$target" in + /*) link="$target" ;; + *) link="$(dirname "$link")/$target" ;; + esac +done +basedir=$(dirname "$(echo "$link" | sed -e 's,\\\\,/,g')") basedir_win="$basedir" exe="" msys="" @@ -548,7 +620,19 @@ exit $ret exports[`env shebang with default args > shim files > env.shim 1`] = ` #!/bin/sh -basedir=$(dirname "$(echo "$0" | sed -e 's,\\\\,/,g')") +# Resolve $0 through symlinks so basedir is the shim's real directory. +# Cap hops at the kernel's ELOOP limit so a cycle cannot hang the shim. +link="$0" +hops=0 +while [ -L "$link" ] && [ "$hops" -lt 40 ]; do + hops=$((hops+1)) + target=$(readlink "$link") + case "$target" in + /*) link="$target" ;; + *) link="$(dirname "$link")/$target" ;; + esac +done +basedir=$(dirname "$(echo "$link" | sed -e 's,\\\\,/,g')") basedir_win="$basedir" exe="" msys="" @@ -635,7 +719,19 @@ exit $ret exports[`env shebang with no NODE_PATH > shim files > env.shim 1`] = ` #!/bin/sh -basedir=$(dirname "$(echo "$0" | sed -e 's,\\\\,/,g')") +# Resolve $0 through symlinks so basedir is the shim's real directory. +# Cap hops at the kernel's ELOOP limit so a cycle cannot hang the shim. +link="$0" +hops=0 +while [ -L "$link" ] && [ "$hops" -lt 40 ]; do + hops=$((hops+1)) + target=$(readlink "$link") + case "$target" in + /*) link="$target" ;; + *) link="$(dirname "$link")/$target" ;; + esac +done +basedir=$(dirname "$(echo "$link" | sed -e 's,\\\\,/,g')") basedir_win="$basedir" exe="" msys="" @@ -722,7 +818,19 @@ exit $ret exports[`explicit shebang > shim files > sh.shim 1`] = ` #!/bin/sh -basedir=$(dirname "$(echo "$0" | sed -e 's,\\\\,/,g')") +# Resolve $0 through symlinks so basedir is the shim's real directory. +# Cap hops at the kernel's ELOOP limit so a cycle cannot hang the shim. +link="$0" +hops=0 +while [ -L "$link" ] && [ "$hops" -lt 40 ]; do + hops=$((hops+1)) + target=$(readlink "$link") + case "$target" in + /*) link="$target" ;; + *) link="$(dirname "$link")/$target" ;; + esac +done +basedir=$(dirname "$(echo "$link" | sed -e 's,\\\\,/,g')") basedir_win="$basedir" exe="" msys="" @@ -809,7 +917,19 @@ exit $ret exports[`explicit shebang with args > shim files > sh.args.shim 1`] = ` #!/bin/sh -basedir=$(dirname "$(echo "$0" | sed -e 's,\\\\,/,g')") +# Resolve $0 through symlinks so basedir is the shim's real directory. +# Cap hops at the kernel's ELOOP limit so a cycle cannot hang the shim. +link="$0" +hops=0 +while [ -L "$link" ] && [ "$hops" -lt 40 ]; do + hops=$((hops+1)) + target=$(readlink "$link") + case "$target" in + /*) link="$target" ;; + *) link="$(dirname "$link")/$target" ;; + esac +done +basedir=$(dirname "$(echo "$link" | sed -e 's,\\\\,/,g')") basedir_win="$basedir" exe="" msys="" @@ -896,7 +1016,19 @@ exit $ret exports[`explicit shebang with args, linking to another drive on Windows > shim files > sh.args.shim 1`] = ` #!/bin/sh -basedir=$(dirname "$(echo "$0" | sed -e 's,\\\\,/,g')") +# Resolve $0 through symlinks so basedir is the shim's real directory. +# Cap hops at the kernel's ELOOP limit so a cycle cannot hang the shim. +link="$0" +hops=0 +while [ -L "$link" ] && [ "$hops" -lt 40 ]; do + hops=$((hops+1)) + target=$(readlink "$link") + case "$target" in + /*) link="$target" ;; + *) link="$(dirname "$link")/$target" ;; + esac +done +basedir=$(dirname "$(echo "$link" | sed -e 's,\\\\,/,g')") basedir_win="$basedir" exe="" msys="" @@ -983,7 +1115,19 @@ exit $ret exports[`explicit shebang with prog args > shim files > sh.args.shim 1`] = ` #!/bin/sh -basedir=$(dirname "$(echo "$0" | sed -e 's,\\\\,/,g')") +# Resolve $0 through symlinks so basedir is the shim's real directory. +# Cap hops at the kernel's ELOOP limit so a cycle cannot hang the shim. +link="$0" +hops=0 +while [ -L "$link" ] && [ "$hops" -lt 40 ]; do + hops=$((hops+1)) + target=$(readlink "$link") + case "$target" in + /*) link="$target" ;; + *) link="$(dirname "$link")/$target" ;; + esac +done +basedir=$(dirname "$(echo "$link" | sed -e 's,\\\\,/,g')") basedir_win="$basedir" exe="" msys="" @@ -1070,7 +1214,19 @@ exit $ret exports[`no cmd file > shim files > exe.shim 1`] = ` #!/bin/sh -basedir=$(dirname "$(echo "$0" | sed -e 's,\\\\,/,g')") +# Resolve $0 through symlinks so basedir is the shim's real directory. +# Cap hops at the kernel's ELOOP limit so a cycle cannot hang the shim. +link="$0" +hops=0 +while [ -L "$link" ] && [ "$hops" -lt 40 ]; do + hops=$((hops+1)) + target=$(readlink "$link") + case "$target" in + /*) link="$target" ;; + *) link="$(dirname "$link")/$target" ;; + esac +done +basedir=$(dirname "$(echo "$link" | sed -e 's,\\\\,/,g')") basedir_win="$basedir" exe="" msys="" @@ -1124,7 +1280,19 @@ exit $LASTEXITCODE exports[`no shebang > shim files > exe.shim 1`] = ` #!/bin/sh -basedir=$(dirname "$(echo "$0" | sed -e 's,\\\\,/,g')") +# Resolve $0 through symlinks so basedir is the shim's real directory. +# Cap hops at the kernel's ELOOP limit so a cycle cannot hang the shim. +link="$0" +hops=0 +while [ -L "$link" ] && [ "$hops" -lt 40 ]; do + hops=$((hops+1)) + target=$(readlink "$link") + case "$target" in + /*) link="$target" ;; + *) link="$(dirname "$link")/$target" ;; + esac +done +basedir=$(dirname "$(echo "$link" | sed -e 's,\\\\,/,g')") basedir_win="$basedir" exe="" msys="" @@ -1185,7 +1353,19 @@ exit $LASTEXITCODE exports[`shebang with -S > shim files > from.env.s.shim 1`] = ` #!/bin/sh -basedir=$(dirname "$(echo "$0" | sed -e 's,\\\\,/,g')") +# Resolve $0 through symlinks so basedir is the shim's real directory. +# Cap hops at the kernel's ELOOP limit so a cycle cannot hang the shim. +link="$0" +hops=0 +while [ -L "$link" ] && [ "$hops" -lt 40 ]; do + hops=$((hops+1)) + target=$(readlink "$link") + case "$target" in + /*) link="$target" ;; + *) link="$(dirname "$link")/$target" ;; + esac +done +basedir=$(dirname "$(echo "$link" | sed -e 's,\\\\,/,g')") basedir_win="$basedir" exe="" msys=""