mirror of
https://github.com/zed-industries/zed.git
synced 2026-10-19 02:54:11 +00:00
## 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
453 lines
14 KiB
Rust
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
|
|
}
|
|
}
|