///|
// Userland executable resolution for win32: `spawn` probes these candidates
// with `win32_file_exists` and hands the winner to `CreateProcessW`'s
// `lpApplicationName`, so that `cwd~` participates in the search and the
// parent's own cwd does not (see README "Executable resolution").
///|
#cfg(platform="windows")
#borrow(path)
extern "c" fn win32_get_full_path_name(path : String) -> String = "moonbit_pty_win32_get_full_path_name"
///|
#cfg(platform="windows")
fn get_full_path_name(path : String) -> String raise @os_error.OSError {
let full_path_name = win32_get_full_path_name(path)
if full_path_name.is_empty() {
raise @os_error.OSError(@os_error.get_errno(), context="@pty.spawn")
} else {
full_path_name
}
}
///|
#cfg(platform="windows")
#borrow(path)
extern "c" fn win32_file_exists(path : String) -> Bool = "moonbit_pty_win32_file_exists"
///|
#cfg(platform="windows")
fn split_path_list(path : StringView) -> FixedArray[String] {
let entries = []
let entry = StringBuilder()
fn flush() {
if !entry.is_empty() {
entries.push(entry.to_string())
entry.reset()
}
}
let mut quoted = false
for c in path {
match c {
'"' => quoted = !quoted
';' if !quoted => flush()
c => entry.write_char(c)
}
}
flush()
FixedArray::from_array(entries)
}
///|
#cfg(platform="windows")
fn join_path(dir : StringView, file : StringView) -> String {
if dir is [.., '/' | '\\'] {
"\{dir}\{file}"
} else {
"\{dir}\\\{file}"
}
}
///|
#cfg(platform="windows")
fn append_exe(file : StringView) -> String {
for view = file {
match view {
[.., '.'] => break file.to_owned()
[] | [.., '/' | '\\' | ':'] => break "\{file}.exe"
[.. rest, _] => continue rest
}
}
}
///|
#cfg(platform="windows")
fn get_path_candidates(
file~ : StringView,
path~ : StringView,
cwd~ : String,
) -> FixedArray[String] raise @os_error.OSError {
let file = append_exe(file)
match (file, cwd) {
(
['\\' | '/', '\\' | '/', ..]
| ['A'..='Z' | 'a'..='z', ':', '\\' | '/', ..],
_,
) => [file]
(['\\' | '/', ..], ['A'..='Z' | 'a'..='z' as drive, ..]) =>
["\{drive}:\{file}"]
(['\\' | '/', ..], _) => [file]
(
['A'..='Z' | 'a'..='z' as file_drive, ':', .. relative_path],
['A'..='Z' | 'a'..='z' as current_drive, ..],
) =>
if file_drive
.to_ascii_uppercase()
.equal(current_drive.to_ascii_uppercase()) {
[join_path(cwd, relative_path)]
} else {
[get_full_path_name(file)]
}
(['A'..='Z' | 'a'..='z', ':', ..], _) => [get_full_path_name(file)]
_ => {
if file.contains_any(chars="/\\") {
return [join_path(cwd, file)]
}
split_path_list(path).map(dir => join_path(dir, file))
}
}
}
///|
#cfg(platform="windows")
test "get_path_candidates" {
let cwd = get_full_path_name(".")
guard cwd is ['A'..='Z' | 'a'..='z' as drive, ..] else {
fail("get_path_candidates: unable to parse current drive")
}
// A drive letter that is NOT the parent's current drive, for cross-drive
// cases below.
let other = if drive is ('D' | 'd') { 'C' } else { 'D' }
// Bare name: joined to every PATH entry, ".exe" appended; empty entries
// are skipped, "." is kept.
get_path_candidates(file="echo", path=".\\test_data\\win32", cwd~)
|> assert_eq([".\\test_data\\win32\\echo.exe"])
get_path_candidates(
file="echo",
path=".\\test_data;.\\test_data\\win32",
cwd~,
)
|> assert_eq([".\\test_data\\echo.exe", ".\\test_data\\win32\\echo.exe"])
get_path_candidates(
file="echo",
path=";.\\test_data;.\\test_data\\win32",
cwd~,
)
|> assert_eq([".\\test_data\\echo.exe", ".\\test_data\\win32\\echo.exe"])
get_path_candidates(
file="echo",
path=".;.\\test_data;.\\test_data\\win32",
cwd~,
)
|> assert_eq([
".\\echo.exe", ".\\test_data\\echo.exe", ".\\test_data\\win32\\echo.exe",
])
// Empty PATH: a bare name gets no candidates.
get_path_candidates(file="echo", path="", cwd~) |> assert_eq([])
// PATH list parsing: quoted entries may contain a literal ';' and the
// quotes are stripped; a trailing separator is not doubled when joining.
get_path_candidates(file="echo", path="\"C:\\a;b\";C:\\c", cwd~)
|> assert_eq(["C:\\a;b\\echo.exe", "C:\\c\\echo.exe"])
get_path_candidates(file="echo", path="C:\\bin\\", cwd~)
|> assert_eq(["C:\\bin\\echo.exe"])
// UNC (and device) forms: absolute by construction, never searched in
// PATH nor joined to cwd.
get_path_candidates(
file="\\\\server\\share\\echo",
path=".\\test_data;.\\test_data\\win32",
cwd~,
)
|> assert_eq(["\\\\server\\share\\echo.exe"])
// Drive-absolute: as-is, regardless of cwd's drive.
get_path_candidates(
file="C:\\echo.exe",
path=".\\test_data;.\\test_data\\win32",
cwd="C:\\Users\\MoonBit",
)
|> assert_eq(["C:\\echo.exe"])
get_path_candidates(
file="C:\\echo.exe",
path=".\\test_data;.\\test_data\\win32",
cwd="D:\\Projects\\Pty",
)
|> assert_eq(["C:\\echo.exe"])
// Drive-relative (X:foo) on the same drive as cwd: joined to cwd.
get_path_candidates(
file="D:echo.exe",
path=".\\test_data;.\\test_data\\win32",
cwd="D:\\Projects\\Pty",
)
|> assert_eq(["D:\\Projects\\Pty\\echo.exe"])
// Drive-relative on another drive (or with a UNC cwd): expanded via
// GetFullPathNameW against the parent's ambient state. Using the
// parent's own drive as X makes the expansion deterministic on any
// machine: the parent cwd IS that drive's current directory.
get_path_candidates(
file="\{drive}:echo.exe",
path=".\\test_data;.\\test_data\\win32",
cwd="\{other}:\\Projects\\Pty",
)
|> assert_eq([join_path(cwd, "echo.exe")])
get_path_candidates(
file="\{drive}:echo.exe",
path=".\\test_data;.\\test_data\\win32",
cwd="\\\\server\\share",
)
|> assert_eq([join_path(cwd, "echo.exe")])
// An existing extension is preserved: .bat must not become .bat.exe.
get_path_candidates(
file="echo.bat",
path=".\\test_data;.\\test_data\\win32",
cwd~,
)
|> assert_eq([".\\test_data\\echo.bat", ".\\test_data\\win32\\echo.bat"])
get_path_candidates(
file="C:\\echo.bat",
path=".\\test_data;.\\test_data\\win32",
cwd~,
)
|> assert_eq(["C:\\echo.bat"])
// ".exe" appending looks at the last component only: a dot in a
// directory segment does not count, a trailing dot does.
get_path_candidates(file="test_data\\v1.2\\echo", path="", cwd="C:\\x")
|> assert_eq(["C:\\x\\test_data\\v1.2\\echo.exe"])
get_path_candidates(file="foo.", path="C:\\bin", cwd~)
|> assert_eq(["C:\\bin\\foo."])
// Relative path with a separator: joined to cwd, PATH ignored.
get_path_candidates(
file="test_data\\win32\\echo.bat",
path=".\\test_data;.\\test_data\\win32",
cwd~,
)
|> assert_eq([join_path(cwd, "test_data\\win32\\echo.bat")])
get_path_candidates(
file="test_data\\win32\\echo",
path=".\\test_data;.\\test_data\\win32",
cwd~,
)
|> assert_eq([join_path(cwd, "test_data\\win32\\echo.exe")])
get_path_candidates(file=".\\echo", path="ignored", cwd="C:\\proj")
|> assert_eq(["C:\\proj\\.\\echo.exe"])
// Root-relative (\foo): takes cwd's drive; a UNC cwd has none, so the
// path is left for the system to resolve.
get_path_candidates(file="\\echo", path=".\\test_data", cwd~)
|> assert_eq(["\{drive}:\\echo.exe"])
get_path_candidates(
file="\\echo",
path=".\\test_data",
cwd="\\\\server\\share",
)
|> assert_eq(["\\echo.exe"])
// Forward slashes are separators everywhere.
get_path_candidates(file="C:/echo.exe", path="ignored", cwd~)
|> assert_eq(["C:/echo.exe"])
get_path_candidates(file="//server/share/echo", path="ignored", cwd~)
|> assert_eq(["//server/share/echo.exe"])
get_path_candidates(file="./echo", path="ignored", cwd="C:\\proj")
|> assert_eq(["C:\\proj\\./echo.exe"])
// Drive letters compare case-insensitively.
get_path_candidates(file="d:echo.exe", path="", cwd="D:\\Projects")
|> assert_eq(["D:\\Projects\\echo.exe"])
get_path_candidates(file="c:\\echo.exe", path="", cwd~)
|> assert_eq(["c:\\echo.exe"])
}