Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
126 changes: 126 additions & 0 deletions src/paths/classify.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
//! Single source of truth for one-pass relative-path classification.
//! Callers OR `has_sep`/`has_dot` for "needs resolution" routing and gate
//! `..`-clamp work on `climbs_above_start`.

use crate::PathChar;
use crate::component_iterator::PathFormat;

/// Facts about a relative path segment, computed in one pass.
#[derive(Clone, Copy, Debug)]
pub struct RelPathFacts {
/// Any separator for the given format.
pub has_sep: bool,
/// Any `.` anywhere (including inside names like `a.b`).
pub has_dot: bool,
/// `..` resolution climbs above the segment's own start: the running
/// component depth (name +1, `..` −1, `.`/empty 0) ever goes negative.
pub climbs_above_start: bool,
}

/// Classify `rel` in a single pass. Exits early only once every field is
/// final: depth gone negative at a separator (bare trailing `..` still walks
/// to the end so `has_sep` stays honest).
pub fn classify_rel_t<T: PathChar>(rel: &[T], fmt: PathFormat) -> RelPathFacts {
let mut facts = RelPathFacts {
has_sep: false,
has_dot: false,
climbs_above_start: false,
};
let mut depth = 0isize; // net component depth so far
let mut dots = 0usize; // `.` count in the current component
let mut other = false; // non-dot chars in the current component
for &c in rel {
if fmt.is_sep(c) {
facts.has_sep = true;
if other || dots > 2 {
depth += 1;
} else if dots == 2 {
depth -= 1;
}
dots = 0;
other = false;
if depth < 0 {
break; // only `..` decrements, so every field is final here
}
} else if c.eq_ascii(b'.') {
facts.has_dot = true;
dots += 1;
} else {
other = true;
}
}
if dots == 2 && !other {
depth -= 1; // trailing `..` component applies its −1 too
}
facts.climbs_above_start = depth < 0;
facts
}

#[cfg(test)]
mod tests {
use super::*;

#[track_caller]
fn check(rel: &str, fmt: PathFormat, want: (bool, bool, bool)) {
let f = classify_rel_t(rel.as_bytes(), fmt);
assert_eq!(
(f.has_sep, f.has_dot, f.climbs_above_start),
want,
"{rel:?} {fmt:?}"
);
// Same answers for u16 input.
let wide: Vec<u16> = rel.encode_utf16().collect();
let f = classify_rel_t(&wide, fmt);
assert_eq!(
(f.has_sep, f.has_dot, f.climbs_above_start),
want,
"{rel:?} {fmt:?} (u16)"
);
}

#[test]
fn windows_cases() {
use PathFormat::Windows as W;
check("", W, (false, false, false));
check("a", W, (false, false, false));
check("a.b", W, (false, true, false));
check("a\\b", W, (true, false, false));
check("a/b", W, (true, false, false));
// Bare `..`: no separator, but it climbs.
check("..", W, (false, true, true));
check("..\\x", W, (true, true, true));
check("a/../b", W, (true, true, false));
check("a\\..", W, (true, true, false));
check("..a", W, (false, true, false));
check("a..", W, (false, true, false));
check("...", W, (false, true, false));
check("a\\...\\b", W, (true, true, false));
// Doubled separator: the empty component costs no depth.
check("a\\\\..", W, (true, true, false));
check("a\\", W, (true, false, false));
// The `!other` guard: dots mixed with other chars never form `..`.
check("..a\\x", W, (true, true, false));
check("a..\\x", W, (true, true, false));
check(".a.\\x", W, (true, true, false));
// Field-exactness: the closing separator flips has_sep, nothing else.
check("..\\", W, (true, true, true));
// Within-tree `..` never climbs; net-negative walks do.
check("a\\..\\b", W, (true, true, false));
check("a\\..\\..", W, (true, true, true));
check("..a\\..", W, (true, true, false));
check(".\\..", W, (true, true, true));
check("a\\..\\b\\..\\..", W, (true, true, true));
}

#[test]
fn posix_cases() {
use PathFormat::Posix as P;
// Backslash is not a separator: one component `a\b`.
check("a\\b", P, (false, false, false));
// Component is `a\..`, not `..`.
check("a\\..", P, (false, true, false));
check("..", P, (false, true, true));
check("a/../b", P, (true, true, false));
check("a/..", P, (true, true, false));
}
}
2 changes: 2 additions & 0 deletions src/paths/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,8 @@ pub mod component_iterator;
pub use component_iterator::{
Component, ComponentIterator, MakePathStep, PathFormat, component_iterator, make_path_with,
};
pub mod classify;
pub use classify::{RelPathFacts, classify_rel_t};
// Crate-root re-exports for the path-mutation helpers so `#[cfg(windows)]`
// install paths can call
// `bun_paths::dangerously_convert_path_to_posix_in_place(..)` directly.
Expand Down
4 changes: 0 additions & 4 deletions src/paths/resolve_path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1145,11 +1145,7 @@

let result = &mut buf[0..buf_i];

if cfg!(debug_assertions) && is_windows {
debug_assert!(!strings::has_prefix_t::<T>(result, T::lit(b"\\:\\")));
}

result

Check failure on line 1148 in src/paths/resolve_path.rs

View workflow job for this annotation

GitHub Actions / cargo clippy

returning the result of a `let` binding from a block
}

#[derive(Copy, Clone, PartialEq, Eq, Debug, core::marker::ConstParamTy)]
Expand Down
Loading
Loading