zed/crates/path/src/path_component.rs
Xin Zhao 51a3a0e971
path: Distinguish is_absolute and has_root in PathStyle (#65468)
## Summary

Follow-up of #64149,

Zed currently has three handcrafted, identical `is_absolute()`
functions, living in `crates/path/src/path.rs` and
`crates/util/src/paths.rs`. They read as follows:

93d7456688/crates/path/src/path.rs (L409-L420)
In fact, these handcrafted methods behave like `Path::has_root()` rather
than `Path::is_absolute()`. The difference between the two is that
`Path::has_root()` only checks whether a path has a root, whereas
`Path::is_absolute()` checks whether the path is truly absolute. On
Unix, which has a single root, `has_root()` and `is_absolute()`
coincide. Windows, however, is multi-rooted (`C:\` and `D:\` are
different roots), so a rooted path such as `\temp\path` is not absolute:
depending on the working directory, it may resolve to `C:\temp\path` or
`D:\temp\path`, which are different paths. This misleading naming can
lead to misuse of the API.

This PR does three things:
1. Deduplicates the methods, keeping only `is_absolute()` in the
`PathStyle` body.
2. Reimplements `PathStyle::has_root()` and `PathStyle::is_absolute()`
based on the standard library’s implementations; the former replaces the
old `is_absolute()`.
3. Updates some callers of the old `is_absolute()` (now `has_root()`,
per step 2) to the new `is_absolute()`, based on each call site’s
semantics and the actual behavior of the methods.

Steps 1 and 2 are pure refactors; the only functional change is in step
3. I believe this fixes some edge cases — or at least surfaces things
that need fixing — though I am not entirely certain that every
replacement is correct.

## Testing

No new tests are introduced. The refactored parts pass the existing
tests in the affected crates.

## Self-review

- [x] I've reviewed my diff for quality, security, reliability, and
performance.
- [x] UI changes follow the
[checklist](https://zed.dev/docs/development/ui-checklist).
- [ ] Tests cover the new or changed behavior.

Release Notes:

- N/A
2026-10-12 12:08:43 +00:00

453 lines
14 KiB
Rust

//! Path component parsing adapted from the Rust standard library
//! (`std::path::Components` in rust-lang/rust's `library/std/src/path.rs`,
//! revision 315ecf4a9).
//!
//! The code is largely a copy of upstream; the only notable changes are
//! `PathStyle`-related: the `cfg(windows)`/`cfg(unix)` branches are replaced
//! with runtime checks on [`PathStyle`], and helpers that `std` defines as
//! private inherent methods on `Prefix` live in the local [`PrefixExt`] trait.
use crate::PathStyle;
use std::ffi::OsStr;
use std::path::Path;
pub use std::path::Prefix;
pub(crate) trait PrefixExt {
fn len(&self) -> usize;
fn is_drive(&self) -> bool;
#[inline]
fn has_implicit_root(&self) -> bool {
!self.is_drive()
}
}
impl PrefixExt for Prefix<'_> {
#[inline]
fn len(&self) -> usize {
use self::Prefix::*;
fn os_str_len(s: &OsStr) -> usize {
s.as_encoded_bytes().len()
}
match *self {
Verbatim(x) => 4 + os_str_len(x),
VerbatimUNC(x, y) => {
8 + os_str_len(x)
+ if os_str_len(y) > 0 {
1 + os_str_len(y)
} else {
0
}
}
VerbatimDisk(_) => 6,
UNC(x, y) => {
2 + os_str_len(x)
+ if os_str_len(y) > 0 {
1 + os_str_len(y)
} else {
0
}
}
DeviceNS(x) => 4 + os_str_len(x),
Disk(_) => 2,
}
}
#[inline]
fn is_drive(&self) -> bool {
matches!(*self, Prefix::Disk(_))
}
}
// Iterate through `iter` while it matches `prefix`; return `None` if `prefix`
// is not a prefix of `iter`, otherwise return `Some(iter_after_prefix)` giving
// `iter` after having exhausted `prefix`.
pub(crate) fn iter_after<'a, 'b, I, J>(mut iter: I, mut prefix: J) -> Option<I>
where
I: Iterator<Item = Component<'a>> + Clone,
J: Iterator<Item = Component<'b>>,
{
loop {
let mut iter_next = iter.clone();
match (iter_next.next(), prefix.next()) {
(Some(ref x), Some(ref y)) if x == y => (),
(Some(_), Some(_)) => return None,
(Some(_), None) => return Some(iter),
(None, None) => return Some(iter),
(None, Some(_)) => return None,
}
iter = iter_next;
}
}
#[derive(PartialEq, PartialOrd, Clone)]
enum State {
Prefix = 0, // c:
StartDir = 1, // / or . or nothing
Body = 2, // foo/bar/baz
Done = 3,
}
#[allow(dead_code)]
pub(crate) struct PrefixComponent<'a> {
/// The prefix as an unparsed `OsStr` slice.
raw: &'a OsStr,
/// The parsed prefix data.
parsed: Prefix<'a>,
}
impl<'a> PartialEq for PrefixComponent<'a> {
#[inline]
fn eq(&self, other: &PrefixComponent<'a>) -> bool {
self.parsed == other.parsed
}
}
#[derive(PartialEq)]
pub(crate) enum Component<'a> {
/// A Windows path prefix, e.g., `C:` or `\\server\share`.
///
/// There is a large variety of prefix types, see [`Prefix`]'s documentation
/// for more.
///
/// Does not occur on Unix.
Prefix(PrefixComponent<'a>),
/// The root directory component, appears after any prefix and before anything else.
///
/// It represents a separator that designates that a path starts from root.
RootDir,
/// A reference to the current directory, i.e., `.`.
CurDir,
/// A reference to the parent directory, i.e., `..`.
ParentDir,
/// A normal component, e.g., `a` and `b` in `a/b`.
///
/// This variant is the most common one, it represents references to files
/// or directories.
Normal(&'a OsStr),
}
#[derive(Clone)]
pub(crate) struct Components<'a> {
path_style: PathStyle,
// The path left to parse components from
path: &'a [u8],
// The prefix as it was originally parsed, if any
prefix: Option<Prefix<'a>>,
// true if path *physically* has a root separator; for most Windows
// prefixes, it may have a "logical" root separator for the purposes of
// normalization, e.g., \\server\share == \\server\share\.
has_physical_root: bool,
// The iterator is double-ended, and these two states keep track of what has
// been produced from either end
front: State,
back: State,
}
impl<'a> Components<'a> {
pub fn new(path_style: PathStyle, path: &'a Path) -> Self {
let prefix = path_style.parse_prefix(path.as_os_str());
Components {
path_style,
path: path.as_os_str().as_encoded_bytes(),
prefix,
has_physical_root: path_style
.has_physical_root(path.as_os_str().as_encoded_bytes(), prefix),
// use a platform-specific initial state to avoid one turn of
// the state-machine when the platform doesn't have a Prefix.
front: if path_style.has_prefixes() {
State::Prefix
} else {
State::StartDir
},
back: State::Body,
}
}
pub(crate) fn prefix(&self) -> Option<Prefix<'_>> {
self.prefix
}
fn has_prefixes(&self) -> bool {
self.path_style.has_prefixes()
}
#[inline]
fn prefix_len(&self) -> usize {
if !self.has_prefixes() {
return 0;
}
self.prefix.as_ref().map(Prefix::len).unwrap_or(0)
}
#[inline]
fn prefix_verbatim(&self) -> bool {
if !self.has_prefixes() {
return false;
}
self.prefix
.as_ref()
.map(Prefix::is_verbatim)
.unwrap_or(false)
}
#[inline]
fn prefix_remaining(&self) -> usize {
if !self.has_prefixes() {
return 0;
}
if self.front == State::Prefix {
self.prefix_len()
} else {
0
}
}
#[inline]
fn len_before_body(&self) -> usize {
let root = if self.front <= State::StartDir && self.has_physical_root {
1
} else {
0
};
let cur_dir = if self.front <= State::StartDir && self.include_cur_dir() {
1
} else {
0
};
self.prefix_remaining() + root + cur_dir
}
#[inline]
fn finished(&self) -> bool {
self.front == State::Done || self.back == State::Done || self.front > self.back
}
#[inline]
fn is_sep_byte(&self, b: u8) -> bool {
if self.prefix_verbatim() {
self.path_style.is_verbatim_sep(b)
} else {
self.path_style.is_sep_byte(b)
}
}
pub fn as_path(&self) -> &'a Path {
let mut comps = self.clone();
if comps.front == State::Body {
comps.trim_left();
}
if comps.back == State::Body {
comps.trim_right();
}
unsafe { Path::new(OsStr::from_encoded_bytes_unchecked(comps.path)) }
}
pub(crate) fn has_root(&self) -> bool {
if self.has_physical_root {
return true;
}
if self.has_prefixes()
&& let Some(p) = self.prefix
{
if p.has_implicit_root() {
return true;
}
}
false
}
fn include_cur_dir(&self) -> bool {
if self.has_root() {
return false;
}
let slice = &self.path[self.prefix_remaining()..];
match slice {
[b'.'] => true,
[b'.', b, ..] => self.is_sep_byte(*b),
_ => false,
}
}
unsafe fn parse_single_component<'b>(&self, comp: &'b [u8]) -> Option<Component<'b>> {
match comp {
b"." if self.has_prefixes() && self.prefix_verbatim() => Some(Component::CurDir),
b"." => None, // . components are normalized away, except at
// the beginning of a path, which is treated
// separately via `include_cur_dir`
b".." => Some(Component::ParentDir),
b"" => None,
_ => Some(Component::Normal(unsafe {
OsStr::from_encoded_bytes_unchecked(comp)
})),
}
}
fn parse_next_component(&self) -> (usize, Option<Component<'a>>) {
debug_assert!(self.front == State::Body);
let (extra, comp) = match self.path.iter().position(|b| self.is_sep_byte(*b)) {
None => (0, self.path),
Some(i) => (1, &self.path[..i]),
};
// SAFETY: `comp` is a valid substring, since it is split on a separator.
(comp.len() + extra, unsafe {
self.parse_single_component(comp)
})
}
fn parse_next_component_back(&self) -> (usize, Option<Component<'a>>) {
debug_assert!(self.back == State::Body);
let start = self.len_before_body();
let (extra, comp) = match self.path[start..]
.iter()
.rposition(|b| self.is_sep_byte(*b))
{
None => (0, &self.path[start..]),
Some(i) => (1, &self.path[start + i + 1..]),
};
// SAFETY: `comp` is a valid substring, since it is split on a separator.
(comp.len() + extra, unsafe {
self.parse_single_component(comp)
})
}
fn trim_left(&mut self) {
while !self.path.is_empty() {
let (size, comp) = self.parse_next_component();
if comp.is_some() {
return;
} else {
self.path = &self.path[size..];
}
}
}
fn trim_right(&mut self) {
while self.path.len() > self.len_before_body() {
let (size, comp) = self.parse_next_component_back();
if comp.is_some() {
return;
} else {
self.path = &self.path[..self.path.len() - size];
}
}
}
}
impl<'a> Iterator for Components<'a> {
type Item = Component<'a>;
fn next(&mut self) -> Option<Component<'a>> {
while !self.finished() {
match self.front {
// most likely case first
State::Body if !self.path.is_empty() => {
let (size, comp) = self.parse_next_component();
self.path = &self.path[size..];
if comp.is_some() {
return comp;
}
}
State::Body => {
self.front = State::Done;
}
State::StartDir => {
self.front = State::Body;
if self.has_physical_root {
debug_assert!(!self.path.is_empty());
self.path = &self.path[1..];
return Some(Component::RootDir);
} else if self.has_prefixes()
&& let Some(p) = self.prefix
{
if p.has_implicit_root() && !p.is_verbatim() {
return Some(Component::RootDir);
}
} else if self.include_cur_dir() {
debug_assert!(!self.path.is_empty());
self.path = &self.path[1..];
return Some(Component::CurDir);
}
}
_ if !self.has_prefixes() => unreachable!(),
State::Prefix if self.prefix_len() == 0 => {
self.front = State::StartDir;
}
State::Prefix => {
self.front = State::StartDir;
debug_assert!(self.prefix_len() <= self.path.len());
let raw = &self.path[..self.prefix_len()];
self.path = &self.path[self.prefix_len()..];
return Some(Component::Prefix(PrefixComponent {
raw: unsafe { OsStr::from_encoded_bytes_unchecked(raw) },
parsed: self.prefix.unwrap(),
}));
}
State::Done => unreachable!(),
}
}
None
}
}
impl<'a> DoubleEndedIterator for Components<'a> {
fn next_back(&mut self) -> Option<Component<'a>> {
while !self.finished() {
match self.back {
State::Body if self.path.len() > self.len_before_body() => {
let (size, comp) = self.parse_next_component_back();
self.path = &self.path[..self.path.len() - size];
if comp.is_some() {
return comp;
}
}
State::Body => {
self.back = State::StartDir;
}
State::StartDir => {
self.back = if self.has_prefixes() {
State::Prefix
} else {
State::Done
};
if self.has_physical_root {
self.path = &self.path[..self.path.len() - 1];
return Some(Component::RootDir);
} else if self.has_prefixes()
&& let Some(p) = self.prefix
{
if p.has_implicit_root() && !p.is_verbatim() {
return Some(Component::RootDir);
}
} else if self.include_cur_dir() {
self.path = &self.path[..self.path.len() - 1];
return Some(Component::CurDir);
}
}
_ if !self.has_prefixes() => unreachable!(),
State::Prefix if self.prefix_len() > 0 => {
self.back = State::Done;
return Some(Component::Prefix(PrefixComponent {
raw: unsafe { OsStr::from_encoded_bytes_unchecked(self.path) },
parsed: self.prefix.unwrap(),
}));
}
State::Prefix => {
self.back = State::Done;
return None;
}
State::Done => unreachable!(),
}
}
None
}
}