diff --git a/src/glob.zig b/src/glob.zig index 666df1ed94..14e5b9647c 100644 --- a/src/glob.zig +++ b/src/glob.zig @@ -1006,10 +1006,10 @@ pub fn GlobWalker_( } inline fn startsWithDot(filepath: []const u8) bool { - if (comptime !isWindows) { - return filepath[0] == '.'; + if (comptime isWindows) { + return filepath.len > 1 and filepath[1] == '.'; } else { - return filepath[1] == '.'; + return filepath.len > 0 and filepath[0] == '.'; } } diff --git a/src/resolver/resolve_path.zig b/src/resolver/resolve_path.zig index fabdb5e55d..130d6cf466 100644 --- a/src/resolver/resolve_path.zig +++ b/src/resolver/resolve_path.zig @@ -2120,7 +2120,7 @@ pub const PosixToWinNormalizer = struct { return maybe_posix_path; } - fn resolveCWDWithExternalBuf( + pub fn resolveCWDWithExternalBuf( buf: *Buf, maybe_posix_path: []const u8, ) ![]const u8 { diff --git a/src/sys.zig b/src/sys.zig index 111b6926bf..90553f8776 100644 --- a/src/sys.zig +++ b/src/sys.zig @@ -1715,18 +1715,24 @@ pub fn getMaxPipeSizeOnLinux() usize { ); } -pub fn existsOSPath(path: bun.OSPathSliceZ) bool { +pub fn existsOSPath(path: bun.OSPathSliceZ, file_only: bool) bool { if (comptime Environment.isPosix) { return system.access(path, 0) == 0; } if (comptime Environment.isWindows) { assertIsValidWindowsPath(bun.OSPathChar, path); - const result = kernel32.GetFileAttributesW(path.ptr); + const attributes = kernel32.GetFileAttributesW(path.ptr); if (Environment.isDebug) { - log("GetFileAttributesW({}) = {d}", .{ bun.fmt.fmtUTF16(path), result }); + log("GetFileAttributesW({}) = {d}", .{ bun.fmt.fmtUTF16(path), attributes }); } - return result != windows.INVALID_FILE_ATTRIBUTES; + if (attributes == windows.INVALID_FILE_ATTRIBUTES) { + return false; + } + if (file_only and attributes & windows.FILE_ATTRIBUTE_DIRECTORY != 0) { + return false; + } + return true; } @compileError("TODO: existsOSPath"); diff --git a/src/which.zig b/src/which.zig index 61ae241b53..a5802faf34 100644 --- a/src/which.zig +++ b/src/which.zig @@ -1,5 +1,6 @@ const std = @import("std"); const bun = @import("root").bun; +const PosixToWinNormalizer = bun.path.PosixToWinNormalizer; fn isValid(buf: *bun.PathBuffer, segment: []const u8, bin: []const u8) ?u16 { bun.copy(u8, buf, segment); @@ -70,66 +71,75 @@ pub fn endsWithExtension(str: []const u8) bool { return false; } +/// Check if the WPathBuffer holds a existing file path, checking also for windows extensions variants like .exe, .cmd and .bat (internally used by whichWin) +fn searchBin(buf: *bun.WPathBuffer, path_size: usize, check_windows_extensions: bool) ?[:0]const u16 { + if (bun.sys.existsOSPath(buf[0..path_size :0], true)) + return buf[0..path_size :0]; + + if (check_windows_extensions) { + buf[path_size] = '.'; + buf[path_size + 1 + 3] = 0; + inline for (win_extensionsW) |ext| { + @memcpy(buf[path_size + 1 .. path_size + 1 + 3], ext); + if (bun.sys.existsOSPath(buf[0 .. path_size + 1 + ext.len :0], true)) + return buf[0 .. path_size + 1 + ext.len :0]; + } + } + return null; +} + +/// Check if bin file exists in this path (internally used by whichWin) +fn searchBinInPath(buf: *bun.WPathBuffer, path_buf: *[bun.MAX_PATH_BYTES]u8, path: []const u8, bin: []const u8, check_windows_extensions: bool) ?[:0]const u16 { + if (path.len == 0) return null; + const segment = if (std.fs.path.isAbsolute(path)) (PosixToWinNormalizer.resolveCWDWithExternalBuf(path_buf, path) catch return null) else path; + const segment_utf16 = bun.strings.convertUTF8toUTF16InBuffer(buf, segment); + + var segment_len = segment.len; + var segment_utf16_len = segment_utf16.len; + if (buf[segment.len - 1] != std.fs.path.sep) { + buf[segment.len] = std.fs.path.sep; + segment_len += 1; + segment_utf16_len += 1; + } + + const bin_utf16 = bun.strings.convertUTF8toUTF16InBuffer(buf[segment_len..], bin); + const path_size = segment_utf16_len + bin_utf16.len; + buf[path_size] = 0; + + return searchBin(buf, path_size, check_windows_extensions); +} + /// This is the windows version of `which`. /// It operates on wide strings. /// It is similar to Get-Command in powershell. pub fn whichWin(buf: *bun.WPathBuffer, path: []const u8, cwd: []const u8, bin: []const u8) ?[:0]const u16 { - _ = cwd; if (bin.len == 0) return null; + var path_buf: [bun.MAX_PATH_BYTES]u8 = undefined; + + const check_windows_extensions = !endsWithExtension(bin); // handle absolute paths if (std.fs.path.isAbsolute(bin)) { - const bin_utf16 = bun.strings.convertUTF8toUTF16InBuffer(buf, bin); - if (endsWithExtension(bin)) { - buf[bin_utf16.len] = 0; - if (bun.sys.existsOSPath(buf[0..bin.len :0])) - return buf[0..bin.len :0]; - } - buf[bin_utf16.len] = '.'; - buf[bin_utf16.len + 1 + 3] = 0; - inline for (win_extensionsW) |ext| { - @memcpy(buf[bin.len + 1 .. bin_utf16.len + 1 + ext.len], ext); - if (bun.sys.existsOSPath(buf[0 .. bin.len + 1 + ext.len :0])) - return buf[0 .. bin.len + 1 + ext.len :0]; - } - return null; + const normalized_bin = PosixToWinNormalizer.resolveCWDWithExternalBuf(&path_buf, bin) catch return null; + const bin_utf16 = bun.strings.convertUTF8toUTF16InBuffer(buf, normalized_bin); + buf[bin_utf16.len] = 0; + return searchBin(buf, bin_utf16.len, check_windows_extensions); } - // TODO: cwd. This snippet does not work yet. - // if (cwd.len > 0) { - // const cwd_utf16 = bun.strings.convertUTF8toUTF16InBuffer(buf, cwd); - // const bin_utf16 = bun.strings.convertUTF8toUTF16InBuffer(buf[cwd_utf16.len + 1 ..], bin); - // if (endsWithExtension(bin)) { - // buf[cwd_utf16.len + 1 + bin_utf16.len] = 0; - // if (bun.sys.existsOSPath(buf[0 .. cwd_utf16.len + 1 + bin_utf16.len :0])) - // return buf[0 .. cwd_utf16.len + 1 + bin_utf16.len :0]; - // } - // buf[cwd_utf16.len + 1 + bin_utf16.len] = '.'; - // buf[cwd_utf16.len + 1 + bin_utf16.len + 1 + 3] = 0; - // inline for (win_extensionsW) |ext| { - // @memcpy(buf[cwd_utf16.len + 1 + bin_utf16.len + 1 .. cwd_utf16.len + 1 + bin_utf16.len + 1 + 3], ext); - // if (bun.sys.existsOSPath(buf[0 .. cwd_utf16.len + 1 + bin_utf16.len + 1 + ext.len :0])) - // return buf[0 .. cwd_utf16.len + 1 + bin_utf16.len + 1 + ext.len :0]; - // } - // } + // check if bin is in cwd + if (searchBinInPath(buf, &path_buf, cwd, bin, check_windows_extensions)) |bin_path| { + return bin_path; + } - const check_without_append_ext = endsWithExtension(bin); + // iterate over system path delimiter var path_iter = std.mem.tokenizeScalar(u8, path, std.fs.path.delimiter); - while (path_iter.next()) |segment| { - const segment_utf16 = bun.strings.convertUTF8toUTF16InBuffer(buf, segment); - buf[segment.len] = std.fs.path.sep; - const bin_utf16 = bun.strings.convertUTF8toUTF16InBuffer(buf[segment.len + 1 ..], bin); - if (check_without_append_ext) { - buf[segment_utf16.len + 1 + bin_utf16.len] = 0; - if (bun.sys.existsOSPath(buf[0 .. segment_utf16.len + 1 + bin_utf16.len :0])) - return buf[0 .. segment_utf16.len + 1 + bin_utf16.len :0]; - } - buf[segment_utf16.len + 1 + bin_utf16.len] = '.'; - buf[segment_utf16.len + 1 + bin_utf16.len + 1 + 3] = 0; - inline for (win_extensionsW) |ext| { - @memcpy(buf[segment_utf16.len + 1 + bin_utf16.len + 1 .. segment_utf16.len + 1 + bin_utf16.len + 1 + 3], ext); - if (bun.sys.existsOSPath(buf[0 .. segment_utf16.len + 1 + bin_utf16.len + 1 + ext.len :0])) - return buf[0 .. segment_utf16.len + 1 + bin_utf16.len + 1 + ext.len :0]; + while (path_iter.next()) |_segment| { + // we also iterate over ':' to match linux behavior + var segment_iter = std.mem.tokenizeScalar(u8, _segment, ':'); + while (segment_iter.next()) |segment_part| { + if (searchBinInPath(buf, &path_buf, segment_part, bin, check_windows_extensions)) |bin_path| { + return bin_path; + } } } diff --git a/test/js/bun/util/which.test.ts b/test/js/bun/util/which.test.ts index 248f9bdfcf..12696d82fa 100644 --- a/test/js/bun/util/which.test.ts +++ b/test/js/bun/util/which.test.ts @@ -5,6 +5,7 @@ import { which } from "bun"; import { mkdtempSync, rmSync, chmodSync, mkdirSync, unlinkSync, realpathSync } from "node:fs"; import { join } from "node:path"; import { tmpdir } from "node:os"; +import { rmdirSync } from "js/node/fs/export-star-from"; test("which", () => { { @@ -15,6 +16,7 @@ test("which", () => { } let basedir = join(tmpdir(), "which-test-" + Math.random().toString(36).slice(2)); + rmSync(basedir, { recursive: true, force: true }); mkdirSync(basedir, { recursive: true }); writeFixture(join(basedir, "myscript.sh")); @@ -30,6 +32,9 @@ test("which", () => { const orig = process.cwd(); process.chdir(tmpdir()); + try { + rmdirSync("myscript.sh"); + } catch {} // Our cwd is not /tmp expect(which("myscript.sh")).toBe(null);