Skip to content
Open
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
ede3e2c
windows: recognize reserved DOS device names and strip trailing dots/…
robobun Jul 20, 2026
d82ede4
fix test cleanup
robobun Jul 20, 2026
0aee1f5
[autofix.ci] apply automated fixes
autofix-ci[bot] Jul 20, 2026
4c9b3b2
exempt UNC, \\.\, \\?\UNC and \??\ paths from DOS device translation
robobun Jul 20, 2026
32c16fb
[autofix.ci] apply automated fixes
autofix-ci[bot] Jul 20, 2026
91e448c
emit \??\ for \\.\ inputs on the NT path; fix unit test assertions; r…
robobun Jul 20, 2026
a58ae23
[autofix.ci] apply automated fixes
autofix-ci[bot] Jul 20, 2026
30c0fd1
bundler: strip trailing dot from rendered output paths so an empty [e…
robobun Jul 20, 2026
abffb95
take FD_TEST_LOCK in the new normalize_path_windows unit tests
robobun Jul 20, 2026
30981ef
use comptime_string_map for the reserved DOS device name table
robobun Jul 20, 2026
4922b21
doc: cite RtlIsDosDeviceName_U / MS naming rules for the reserved-nam…
robobun Jul 20, 2026
95649a4
simplify the trailing-trim in windows_reserved_device_name_t
robobun Jul 20, 2026
768f14c
test.concurrent for the device-name suite; strip trailing space from …
robobun Jul 20, 2026
ff1f852
[autofix.ci] apply automated fixes
autofix-ci[bot] Jul 20, 2026
2b44001
remove now-dead \\.\ -> CreateFileW fallback from open_dir_at_windows…
robobun Jul 20, 2026
c485fe9
trim comments to three lines
robobun Jul 20, 2026
44a349e
drop the trailing-dot/space strip (regresses drive-absolute write/sta…
robobun Jul 20, 2026
7a6d51d
trim remaining comment blocks in normalize_path_windows_opts to three…
robobun Jul 20, 2026
f9472a9
drop the stale 'and our NT open path' clause from the bundler trim co…
robobun Jul 20, 2026
53a19fb
revert bundler trailing-dot trim (no longer needed now the normalize …
robobun Jul 20, 2026
de663fc
trim test file header comment to three lines
robobun Jul 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/bundler/bundle_v2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4207,6 +4207,12 @@
template
.print(&mut v, !self.transpiler.options.compile)
.expect("oom");
// An empty `[ext]` leaves a dangling `.`; Win32 and
// our NT open path strip trailing `.`/` `, so keep
// the written file and generated import agreeing.
while matches!(v.last(), Some(&b'.' | &b' ')) {
v.pop();
}

Check warning on line 4215 in src/bundler/bundle_v2.rs

View check run for this annotation

Claude / Claude Code Review

Stale bundler comments reference removed NT-open-path trailing-dot strip

The "and our NT open path" clause in these two comments (also at `src/bundler/linker_context/generateChunksInParallel.rs:372-374`) is stale — commit 44a349e8 dropped the trailing-`.`/` ` strip from `normalize_path_windows_opts`, so Bun's NT open path no longer strips these characters; only Win32/CreateFileW (for non-`\\?\` paths) does. The strip loop itself is still correct; just reword the rationale to cite Win32 semantics only (e.g. "Win32 strips trailing `.`/` ` at open time").
Comment thread
robobun marked this conversation as resolved.
Outdated
Comment thread
robobun marked this conversation as resolved.
Outdated
v.into_boxed_slice()
};

Expand Down
6 changes: 6 additions & 0 deletions src/bundler/linker_context/generateChunksInParallel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,12 @@ pub fn generate_chunks_in_parallel<const IS_DEV_SERVER: bool>(
.template
.print(&mut rel_path, !c.options.compile)
.expect("write to Vec<u8>");
// An empty `[ext]` leaves the template's literal `.` dangling;
// Win32 (and our NT open path) strip trailing `.`/` ` at open time,
// so keep the path in sync with what lands on disk.
while matches!(rel_path.last(), Some(&b'.' | &b' ')) {
rel_path.pop();
}
path::resolve_path::platform_to_posix_in_place::<u8>(&mut rel_path);

if path_names_map.get_or_put(&rel_path)?.found_existing {
Expand Down
123 changes: 123 additions & 0 deletions src/paths/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,129 @@ pub fn is_absolute_windows_wtf16(p: &[u16]) -> bool {
is_absolute_windows_t::<u16>(p)
}

// The set `RtlIsDosDeviceName_U` recognises (unchanged since DOS); see
// https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file.
// `COM0`/`LPT0` and superscript-digit variants are build-dependent and omitted.
bun_core::comptime_string_map! {
static WINDOWS_RESERVED_DEVICE_NAMES: &'static [u8] = {
b"nul" => b"NUL", b"con" => b"CON", b"prn" => b"PRN", b"aux" => b"AUX",
b"com1" => b"COM1", b"com2" => b"COM2", b"com3" => b"COM3",
b"com4" => b"COM4", b"com5" => b"COM5", b"com6" => b"COM6",
b"com7" => b"COM7", b"com8" => b"COM8", b"com9" => b"COM9",
b"lpt1" => b"LPT1", b"lpt2" => b"LPT2", b"lpt3" => b"LPT3",
b"lpt4" => b"LPT4", b"lpt5" => b"LPT5", b"lpt6" => b"LPT6",
b"lpt7" => b"LPT7", b"lpt8" => b"LPT8", b"lpt9" => b"LPT9",
};
}

/// Recognise a reserved Win32 DOS device name (`NUL`, `CON`, `PRN`, `AUX`,
/// `COM1`-`COM9`, `LPT1`-`LPT9`) in a single path component and return its
/// canonical uppercase spelling, or `None`. Mirrors `RtlIsDosDeviceName_U`
/// enough that every spelling Win32's `CreateFileW` would redirect is caught:
/// trailing `.`/` ` are ignored and the match is ASCII-case-insensitive.
/// Unlike the Win32 routine no extension/stream suffix is stripped, so
/// `nul.txt` is not a device; modern Windows and Node agree on that.
pub fn windows_reserved_device_name_t<T: PathChar>(component: &[T]) -> Option<&'static [u8]> {
let end = component
.iter()
.rposition(|c| !c.eq_ascii(b'.') && !c.eq_ascii(b' '))
.map_or(0, |i| i + 1);
if !(3..=4).contains(&end) {
return None;
}
let mut stem = [0u8; 4];
for (i, c) in component[..end].iter().enumerate() {
stem[i] = c.to_ascii()?;
}
WINDOWS_RESERVED_DEVICE_NAMES
.get_ascii_case_insensitive(&stem[..end])
.copied()
}

#[cfg(test)]
mod windows_reserved_device_name_tests {
use super::windows_reserved_device_name_t as check;

#[track_caller]
fn both(s: &str, want: Option<&[u8]>) {
assert_eq!(check(s.as_bytes()), want, "{s:?} (u8)");
let w: Vec<u16> = s.encode_utf16().collect();
assert_eq!(check::<u16>(&w), want, "{s:?} (u16)");
}

#[test]
fn three_char_devices() {
for (s, want) in [
("nul", b"NUL" as &[u8]),
("NUL", b"NUL"),
("Nul", b"NUL"),
("nUl", b"NUL"),
("con", b"CON"),
("CoN", b"CON"),
("prn", b"PRN"),
("aux", b"AUX"),
("AUX", b"AUX"),
] {
both(s, Some(want));
}
}

#[test]
fn numbered_devices() {
both("com1", Some(b"COM1"));
both("COM9", Some(b"COM9"));
both("Com5", Some(b"COM5"));
both("lpt1", Some(b"LPT1"));
both("LpT9", Some(b"LPT9"));
both("com0", None);
both("lpt0", None);
both("com10", None);
both("comA", None);
both("coma", None);
}

#[test]
fn trailing_dots_and_spaces_ignored() {
both("nul.", Some(b"NUL"));
both("nul ", Some(b"NUL"));
both("nul. ", Some(b"NUL"));
both("nul .", Some(b"NUL"));
both("Nul .. ", Some(b"NUL"));
both("com1 ", Some(b"COM1"));
both("aux.", Some(b"AUX"));
}

#[test]
fn near_misses() {
for s in [
"",
"n",
"nu",
"null",
"nul1",
"nu1",
"nula",
"anul",
" nul",
".nul",
"con1",
"conn",
"nul.txt",
"nul:stream",
"aux1",
"co",
"com",
"lpt",
"c:nul",
] {
both(s, None);
}
// Non-ASCII in a would-be match position.
let w: Vec<u16> = "n\u{00fc}l".encode_utf16().collect();
assert_eq!(check::<u16>(&w), None);
}
}

/// Returns the leading drive
/// designator (e.g. `C:` or `\\server\share`) or empty.
///
Expand Down
Loading
Loading