diff --git a/Cargo.lock b/Cargo.lock index 06fe14f..834b811 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3480,6 +3480,17 @@ dependencies = [ "serde", ] +[[package]] +name = "quirks_path" +version = "0.1.0" +dependencies = [ + "nom", + "percent-encoding", + "serde", + "thiserror", + "url", +] + [[package]] name = "quote" version = "1.0.35" diff --git a/Cargo.toml b/Cargo.toml index 87d35d6..a68c36a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,3 @@ [workspace] -members = ["crates/recorder"] +members = ["crates/quirks_path", "crates/recorder"] resolver = "2" diff --git a/crates/quirks_path/Cargo.toml b/crates/quirks_path/Cargo.toml new file mode 100644 index 0000000..6e434d8 --- /dev/null +++ b/crates/quirks_path/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "quirks_path" +version = "0.1.0" +edition = "2021" + +[dependencies] +nom = "7.1.3" +percent-encoding = "2.3.1" +serde = { version = "1.0.197", features = ["derive"] } +thiserror = "1.0.57" +url = "2.5.0" diff --git a/crates/quirks_path/src/lib.rs b/crates/quirks_path/src/lib.rs new file mode 100644 index 0000000..f682157 --- /dev/null +++ b/crates/quirks_path/src/lib.rs @@ -0,0 +1,1687 @@ +#![feature(strict_provenance)] +#![feature(extend_one)] + +mod url; +pub mod windows; + +use std::{ + borrow::{Borrow, Cow}, + cmp, + collections::TryReserveError, + error::Error, + fmt, + hash::{Hash, Hasher}, + io, + iter::FusedIterator, + ops::{Deref, DerefMut}, + rc::Rc, + str::FromStr, + sync::Arc, +}; + +use ::url::Url; +pub use url::PathToUrlError; +use windows::is_windows_sep; + +use crate::{ + url::path_to_file_url_segments, + windows::{is_windows_verbatim_sep, parse_windows_path_prefix}, +}; + +#[derive(Debug, Clone, Copy, PartialOrd, Ord, PartialEq, Eq, Hash)] +pub enum Prefix<'a> { + Verbatim { prefix: &'a str }, + VerbatimUNC { server: &'a str, share: &'a str }, + VerbatimDisk { drive: char }, + DeviceNS { device: &'a str }, + UNC { server: &'a str, share: &'a str }, + Disk { drive: char }, +} + +impl<'a> Prefix<'a> { + #[inline] + pub fn is_verbatim(&self) -> bool { + use Prefix::*; + + matches!( + *self, + Verbatim { .. } | VerbatimDisk { .. } | VerbatimUNC { .. } + ) + } + + pub fn len(&self) -> usize { + use Prefix::*; + + match *self { + Verbatim { prefix } => 4 + prefix.len(), + VerbatimUNC { server, share } => { + 8 + server.len() + if share.len() > 0 { 1 + share.len() } else { 0 } + } + VerbatimDisk { .. } => 6, + UNC { server, share } => { + 2 + server.len() + if share.len() > 0 { 1 + share.len() } else { 0 } + } + DeviceNS { device } => 4 + device.len(), + Disk { .. } => 2, + } + } + + #[inline] + pub fn is_drive(&self) -> bool { + use Prefix::*; + + matches!(*self, Disk { .. } | VerbatimDisk { .. }) + } + + #[inline] + pub fn has_implicit_root(&self) -> bool { + !self.is_drive() + } +} + +fn iter_after<'a, 'b, I, J>(mut iter: I, mut prefix: J) -> Option +where + I: Iterator> + Clone, + J: Iterator>, +{ + 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; + } +} + +pub fn is_separator(c: char) -> bool { + is_windows_sep(c) +} + +pub fn is_sep_byte(c: char) -> bool { + is_windows_sep(c) +} + +pub fn is_verbatim_sep(c: char) -> bool { + is_windows_verbatim_sep(c) +} + +fn has_physical_root<'a>(s: &'a str, prefix: Option>) -> Option<&'a str> { + let (len, path) = if let Some(p) = prefix { + (p.len(), &s[p.len()..]) + } else { + (0, s) + }; + let path_bytes = path.as_bytes(); + if !path.is_empty() && is_separator(path_bytes[0] as char) { + Some(&s[len..len + 1]) + } else { + None + } +} + +fn rsplit_file_at_dot(file: &str) -> (Option<&str>, Option<&str>) { + if file == ".." { + return (Some(file), None); + } + + let mut iter = file.rsplitn(2, '.'); + let after = iter.next(); + let before = iter.next(); + if before == Some("") { + (Some(file), None) + } else { + (before, after) + } +} + +fn split_file_at_dot(file: &str) -> (&str, Option<&str>) { + if file == ".." { + return (file, None); + } + + let i = match file[1..].bytes().position(|b| b == b'.') { + Some(i) => i + 1, + None => return (file, None), + }; + let before = &file[..i]; + let after = &file[i + 1..]; + (before, Some(after)) +} + +#[derive(Debug, PartialEq, Clone, PartialOrd, Copy)] +enum State { + Prefix = 0, + StartDir = 1, + Body = 2, + Done = 3, +} + +#[derive(Debug, Eq, Clone, Copy)] +pub struct PrefixComponent<'a> { + raw: &'a str, + parsed: Prefix<'a>, +} + +impl<'a> PrefixComponent<'a> { + #[inline] + pub fn kind(&self) -> Prefix<'a> { + self.parsed + } + + #[inline] + pub fn as_str(&self) -> &'a str { + self.raw + } +} + +impl<'a> PartialOrd for PrefixComponent<'a> { + #[inline] + fn partial_cmp(&self, other: &Self) -> Option { + self.parsed.partial_cmp(&other.parsed) + } +} + +impl<'a> PartialEq for PrefixComponent<'a> { + #[inline] + fn eq(&self, other: &Self) -> bool { + self.parsed == other.parsed + } +} + +impl<'a> Ord for PrefixComponent<'a> { + #[inline] + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.parsed.cmp(&other.parsed) + } +} + +impl Hash for PrefixComponent<'_> { + fn hash(&self, h: &mut H) { + self.parsed.hash(h) + } +} + +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] +pub enum Component<'a> { + Prefix(PrefixComponent<'a>), + RootDir(Option<&'a str>), + CurDir, + ParentDir, + Normal(&'a str), +} + +impl<'a> Component<'a> { + pub fn as_str(self) -> &'a str { + match self { + Component::Prefix(p) => p.as_str(), + Component::RootDir(root) => root.unwrap_or("/"), + Component::CurDir => ".", + Component::ParentDir => "..", + Component::Normal(path) => path, + } + } +} + +impl AsRef for Component<'_> { + #[inline] + fn as_ref(&self) -> &str { + self.as_str() + } +} + +impl AsRef for Component<'_> { + #[inline] + fn as_ref(&self) -> &Path { + self.as_str().as_ref() + } +} + +#[derive(Clone)] +pub struct Components<'a> { + path: &'a str, + prefix: Option>, + has_physical_root: Option<&'a str>, + front: State, + back: State, +} + +#[derive(Clone)] +pub struct Iter<'a> { + inner: Components<'a>, +} + +impl fmt::Debug for Components<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + struct DebugHelper<'a>(&'a Path); + + impl fmt::Debug for DebugHelper<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_list().entries(self.0.components()).finish() + } + } + + f.debug_tuple("Components") + .field(&DebugHelper(self.as_path())) + .finish() + } +} + +impl<'a> Components<'a> { + #[inline] + fn prefix_len(&self) -> usize { + self.prefix.as_ref().map(Prefix::len).unwrap_or(0) + } + + #[inline] + fn prefix_verbatim(&self) -> bool { + self.prefix + .as_ref() + .map(Prefix::is_verbatim) + .unwrap_or(false) + } + + #[inline] + fn prefix_remaining(&self) -> usize { + if self.front == State::Prefix { + self.prefix_len() + } else { + 0 + } + } + + // Given the iteration so far, how much of the pre-State::Body path is left? + #[inline] + fn len_before_body(&self) -> usize { + let root = if self.front <= State::StartDir && matches!(self.has_physical_root, Some(..)) { + 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() { + is_verbatim_sep(b as char) + } else { + is_sep_byte(b as char) + } + } + + 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(); + } + Path::new(comps.path) + } + + fn has_root(&self) -> bool { + if matches!(self.has_physical_root, Some(..)) { + return true; + } + if 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 mut iter = self.path[self.prefix_remaining()..].as_bytes().iter(); + match (iter.next(), iter.next()) { + (Some(&b'.'), None) => true, + (Some(&b'.'), Some(&b)) => self.is_sep_byte(b), + _ => false, + } + } + + fn parse_single_component<'b>(&self, comp: &'b str) -> Option> { + match comp { + "." if self.prefix_verbatim() => Some(Component::CurDir), + "." => None, + ".." => Some(Component::ParentDir), + "" => None, + _ => Some(Component::Normal(comp)), + } + } + + fn parse_next_component(&self) -> (usize, Option>) { + debug_assert!(self.front == State::Body); + let (extra, comp) = match self + .path + .as_bytes() + .iter() + .position(|b| self.is_sep_byte(*b)) + { + None => (0, self.path), + Some(i) => (1, &self.path[..i]), + }; + (comp.len() + extra, self.parse_single_component(comp)) + } + + fn parse_next_component_back(&self) -> (usize, Option>) { + debug_assert!(self.back == State::Body); + let start = self.len_before_body(); + let (extra, comp) = match self.path[start..] + .as_bytes() + .iter() + .rposition(|b| self.is_sep_byte(*b)) + { + None => (0, &self.path[start..]), + Some(i) => (1, &self.path[start + i + 1..]), + }; + (comp.len() + extra, 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 AsRef for Components<'_> { + #[inline] + fn as_ref(&self) -> &Path { + self.as_path() + } +} + +impl AsRef for Components<'_> { + #[inline] + fn as_ref(&self) -> &str { + self.as_path().as_str() + } +} + +impl fmt::Debug for Iter<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + struct DebugHelper<'a>(&'a Path); + + impl fmt::Debug for DebugHelper<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_list().entries(self.0.iter()).finish() + } + } + + f.debug_tuple("Iter") + .field(&DebugHelper(self.as_path())) + .finish() + } +} + +impl<'a> Iter<'a> { + #[inline] + pub fn as_path(&self) -> &'a Path { + self.inner.as_path() + } +} + +impl AsRef for Iter<'_> { + #[inline] + fn as_ref(&self) -> &Path { + self.as_path() + } +} + +impl AsRef for Iter<'_> { + #[inline] + fn as_ref(&self) -> &str { + self.as_path().as_str() + } +} + +impl<'a> Iterator for Iter<'a> { + type Item = &'a str; + + #[inline] + fn next(&mut self) -> Option<&'a str> { + self.inner.next().map(Component::as_str) + } +} + +impl<'a> DoubleEndedIterator for Iter<'a> { + #[inline] + fn next_back(&mut self) -> Option<&'a str> { + self.inner.next_back().map(Component::as_str) + } +} + +impl FusedIterator for Iter<'_> {} + +impl<'a> Iterator for Components<'a> { + type Item = Component<'a>; + + fn next(&mut self) -> Option> { + while !self.finished() { + match self.front { + State::Prefix if self.prefix_len() > 0 => { + 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, + parsed: self.prefix.unwrap(), + })); + } + State::Prefix => { + self.front = State::StartDir; + } + State::StartDir => { + self.front = State::Body; + if let Some(root) = self.has_physical_root { + debug_assert!(!self.path.is_empty()); + self.path = &self.path[1..]; + return Some(Component::RootDir(Some(root))); + } else if let Some(p) = self.prefix { + if p.has_implicit_root() && !p.is_verbatim() { + return Some(Component::RootDir(None)); + } + } else if self.include_cur_dir() { + debug_assert!(!self.path.is_empty()); + self.path = &self.path[1..]; + return Some(Component::CurDir); + } + } + 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::Done => unreachable!(), + } + } + None + } +} + +impl<'a> DoubleEndedIterator for Components<'a> { + fn next_back(&mut self) -> Option> { + 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 = State::Prefix; + if let Some(root) = self.has_physical_root { + self.path = &self.path[..self.path.len() - 1]; + return Some(Component::RootDir(Some(root))); + } else if let Some(p) = self.prefix { + if p.has_implicit_root() && !p.is_verbatim() { + return Some(Component::RootDir(None)); + } + } else if self.include_cur_dir() { + self.path = &self.path[..self.path.len() - 1]; + return Some(Component::CurDir); + } + } + State::Prefix if self.prefix_len() > 0 => { + self.back = State::Done; + return Some(Component::Prefix(PrefixComponent { + raw: self.path, + parsed: self.prefix.unwrap(), + })); + } + State::Prefix => { + self.back = State::Done; + return None; + } + State::Done => unreachable!(), + } + } + None + } +} + +impl FusedIterator for Components<'_> {} + +impl<'a> PartialEq for Components<'a> { + #[inline] + fn eq(&self, other: &Components<'a>) -> bool { + let Components { + path: _, + front: _, + back: _, + has_physical_root: _, + prefix: _, + } = self; + + if self.path.len() == other.path.len() + && self.front == other.front + && self.back == State::Body + && other.back == State::Body + && self.prefix_verbatim() == other.prefix_verbatim() + { + if self.path == other.path { + return true; + } + } + + Iterator::eq(self.clone().rev(), other.clone().rev()) + } +} + +impl Eq for Components<'_> {} + +impl<'a> PartialOrd for Components<'a> { + #[inline] + fn partial_cmp(&self, other: &Components<'a>) -> Option { + Some(compare_components(self.clone(), other.clone())) + } +} + +impl Ord for Components<'_> { + #[inline] + fn cmp(&self, other: &Self) -> cmp::Ordering { + compare_components(self.clone(), other.clone()) + } +} + +fn compare_components(mut left: Components<'_>, mut right: Components<'_>) -> cmp::Ordering { + if left.prefix.is_none() && right.prefix.is_none() && left.front == right.front { + let first_difference = match left + .path + .as_bytes() + .iter() + .zip(right.path.as_bytes()) + .position(|(&a, &b)| a != b) + { + None if left.path.len() == right.path.len() => return cmp::Ordering::Equal, + None => left.path.len().min(right.path.len()), + Some(diff) => diff, + }; + + if let Some(previous_sep) = left.path[..first_difference] + .as_bytes() + .iter() + .rposition(|&b| left.is_sep_byte(b)) + { + let mismatched_component_start = previous_sep + 1; + left.path = &left.path[mismatched_component_start..]; + left.front = State::Body; + right.path = &right.path[mismatched_component_start..]; + right.front = State::Body; + } + } + + Iterator::cmp(left, right) +} + +#[derive(Copy, Clone, Debug)] +pub struct Ancestors<'a> { + next: Option<&'a Path>, +} + +impl<'a> Iterator for Ancestors<'a> { + type Item = &'a Path; + + #[inline] + fn next(&mut self) -> Option { + let next = self.next; + self.next = next.and_then(Path::parent); + next + } +} + +impl FusedIterator for Ancestors<'_> {} + +pub struct PathBuf { + inner: String, +} + +impl PathBuf { + #[inline] + fn as_mut_vec(&mut self) -> &mut Vec { + unsafe { self.inner.as_mut_vec() } + } + + #[inline] + pub fn new() -> PathBuf { + PathBuf { + inner: String::new(), + } + } + + #[inline] + pub fn with_capacity(capacity: usize) -> PathBuf { + PathBuf { + inner: String::with_capacity(capacity), + } + } + + #[inline] + pub fn as_path(&self) -> &Path { + self + } + + fn push>(&mut self, path: P) { + self._push(path.as_ref()) + } + + fn _push(&mut self, path: &Path) { + let main_sep_str = self.get_main_sep(); + let mut need_sep = self + .as_mut_vec() + .last() + .map_or(false, |c| !is_separator(*c as char)); + + let comps = self.components(); + + if comps.prefix_len() > 0 + && comps.prefix_len() == comps.path.len() + && comps.prefix.unwrap().is_drive() + { + need_sep = true; + } + + if path.is_absolute() || path.prefix().is_some() { + self.as_mut_vec().truncate(0); + } else if comps.prefix_verbatim() && !path.inner.is_empty() { + let mut buf: Vec<_> = comps.collect(); + for c in path.components() { + match c { + Component::RootDir { .. } => { + buf.truncate(1); + buf.push(c); + } + Component::CurDir => (), + Component::ParentDir => { + if let Some(Component::Normal(_)) = buf.last() { + buf.pop(); + } + } + _ => buf.push(c), + } + } + let mut res = String::new(); + let mut need_sep = false; + for c in buf { + if need_sep && !matches!(c, Component::RootDir { .. }) { + res.push(main_sep_str); + } + res.push_str(c.as_str()); + + need_sep = match c { + Component::RootDir { .. } => false, + Component::Prefix(prefix) => { + !prefix.parsed.is_drive() && prefix.parsed.len() > 0 + } + _ => true, + }; + } + self.inner = res; + return; + } else if path.has_root() { + let prefix_len = self.components().prefix_remaining(); + self.as_mut_vec().truncate(prefix_len); + } else if need_sep { + self.inner.push(main_sep_str) + } + + self.inner.extend(path) + } + + pub fn pop(&mut self) -> bool { + match self.parent().map(|p| p.inner.as_bytes().len()) { + Some(len) => { + self.as_mut_vec().truncate(len); + true + } + None => false, + } + } + + pub fn set_file_name>(&mut self, file_name: S) { + self._set_file_name(file_name.as_ref()) + } + + fn _set_file_name(&mut self, file_name: &str) { + if self.file_name().is_some() { + let popped = self.pop(); + debug_assert!(popped); + } + self.push(file_name); + } + + pub fn set_extension>(&mut self, extension: S) -> bool { + self._set_extension(extension.as_ref()) + } + + fn _set_extension(&mut self, extension: &str) -> bool { + let file_stem = match self.file_stem() { + None => return false, + Some(f) => f.as_bytes(), + }; + + // truncate until right after the file stem + let end_file_stem = file_stem[file_stem.len()..].as_ptr().addr(); + let start = self.inner.as_bytes().as_ptr().addr(); + let v = self.as_mut_vec(); + v.truncate(end_file_stem.wrapping_sub(start)); + + // add the new extension, if any + let new = extension.as_bytes(); + if !new.is_empty() { + v.reserve_exact(new.len() + 1); + v.push(b'.'); + v.extend_from_slice(new); + } + + true + } + + #[inline] + pub fn as_mut_string(&mut self) -> &mut String { + &mut self.inner + } + + #[inline] + pub fn into_string(self) -> String { + self.inner + } + + #[inline] + pub fn into_boxed_path(self) -> Box { + let rw = Box::into_raw(self.inner.into_boxed_str()) as *mut Path; + unsafe { Box::from_raw(rw) } + } + + #[inline] + pub fn capacity(&self) -> usize { + self.inner.capacity() + } + + #[inline] + pub fn clear(&mut self) { + self.inner.clear() + } + + #[inline] + pub fn reserve(&mut self, additional: usize) { + self.inner.reserve(additional) + } + + #[inline] + pub fn try_reserve(&mut self, additional: usize) -> Result<(), TryReserveError> { + self.inner.try_reserve(additional) + } + + #[inline] + pub fn reserve_exact(&mut self, additional: usize) { + self.inner.reserve_exact(additional) + } + + #[inline] + pub fn try_reserve_exact(&mut self, additional: usize) -> Result<(), TryReserveError> { + self.inner.try_reserve_exact(additional) + } + + #[inline] + pub fn shrink_to_fit(&mut self) { + self.inner.shrink_to_fit() + } + + #[inline] + pub fn shrink_to(&mut self, min_capacity: usize) { + self.inner.shrink_to(min_capacity) + } +} + +impl Clone for PathBuf { + #[inline] + fn clone(&self) -> Self { + PathBuf { + inner: self.inner.clone(), + } + } + + #[inline] + fn clone_from(&mut self, source: &Self) { + self.inner.clone_from(&source.inner) + } +} + +impl From<&Path> for Box { + /// Creates a boxed [`Path`] from a reference. + /// + /// This will allocate and clone `path` to it. + fn from(path: &Path) -> Box { + let boxed: Box = path.inner.into(); + let rw = Box::into_raw(boxed) as *mut Path; + unsafe { Box::from_raw(rw) } + } +} + +impl From> for Box { + #[inline] + fn from(cow: Cow<'_, Path>) -> Box { + match cow { + Cow::Borrowed(path) => Box::from(path), + Cow::Owned(path) => Box::from(path), + } + } +} + +impl From> for PathBuf { + #[inline] + fn from(boxed: Box) -> PathBuf { + boxed.into_path_buf() + } +} + +impl From for Box { + #[inline] + fn from(p: PathBuf) -> Box { + p.into_boxed_path() + } +} + +impl Clone for Box { + #[inline] + fn clone(&self) -> Self { + self.to_path_buf().into_boxed_path() + } +} + +impl> From<&T> for PathBuf { + #[inline] + fn from(s: &T) -> PathBuf { + PathBuf::from(s.as_ref().to_string()) + } +} + +impl From for PathBuf { + #[inline] + fn from(s: String) -> PathBuf { + PathBuf { inner: s } + } +} + +impl From for String { + #[inline] + fn from(path_buf: PathBuf) -> String { + path_buf.inner + } +} + +impl FromStr for PathBuf { + type Err = core::convert::Infallible; + + #[inline] + fn from_str(s: &str) -> Result { + Ok(PathBuf::from(s)) + } +} + +impl> FromIterator

for PathBuf { + fn from_iter>(iter: I) -> PathBuf { + let mut buf = PathBuf::new(); + buf.extend(iter); + buf + } +} + +impl> Extend

for PathBuf { + fn extend>(&mut self, iter: I) { + iter.into_iter().for_each(move |p| self.push(p.as_ref())); + } + + #[inline] + fn extend_one(&mut self, p: P) { + self.push(p.as_ref()); + } +} + +impl fmt::Debug for PathBuf { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Debug::fmt(&**self, formatter) + } +} + +impl Deref for PathBuf { + type Target = Path; + #[inline] + fn deref(&self) -> &Path { + Path::new(&self.inner) + } +} + +impl DerefMut for PathBuf { + #[inline] + fn deref_mut(&mut self) -> &mut Path { + Path::from_inner_mut(&mut self.inner) + } +} + +impl Borrow for PathBuf { + #[inline] + fn borrow(&self) -> &Path { + self.deref() + } +} + +impl Default for PathBuf { + #[inline] + fn default() -> Self { + PathBuf::new() + } +} + +impl<'a> From<&'a Path> for Cow<'a, Path> { + #[inline] + fn from(s: &'a Path) -> Cow<'a, Path> { + Cow::Borrowed(s) + } +} + +impl<'a> From for Cow<'a, Path> { + #[inline] + fn from(s: PathBuf) -> Cow<'a, Path> { + Cow::Owned(s) + } +} + +impl<'a> From<&'a PathBuf> for Cow<'a, Path> { + #[inline] + fn from(p: &'a PathBuf) -> Cow<'a, Path> { + Cow::Borrowed(p.as_path()) + } +} + +impl<'a> From> for PathBuf { + #[inline] + fn from(p: Cow<'a, Path>) -> Self { + p.into_owned() + } +} + +impl From for Arc { + #[inline] + fn from(s: PathBuf) -> Arc { + let arc: Arc = Arc::from(s.into_string()); + unsafe { Arc::from_raw(Arc::into_raw(arc) as *const Path) } + } +} + +impl From<&Path> for Arc { + /// Converts a [`Path`] into an [`Arc`] by copying the [`Path`] data into a + /// new [`Arc`] buffer. + #[inline] + fn from(s: &Path) -> Arc { + let arc: Arc = Arc::from(s.as_str()); + unsafe { Arc::from_raw(Arc::into_raw(arc) as *const Path) } + } +} + +impl From for Rc { + /// Converts a [`PathBuf`] into an [Rc]<[Path]> by moving the + /// [`PathBuf`] data into a new [`Rc`] buffer. + #[inline] + fn from(s: PathBuf) -> Rc { + let rc: Rc = Rc::from(s.into_string()); + unsafe { Rc::from_raw(Rc::into_raw(rc) as *const Path) } + } +} + +impl From<&Path> for Rc { + #[inline] + fn from(s: &Path) -> Rc { + let rc: Rc = Rc::from(s.as_str()); + unsafe { Rc::from_raw(Rc::into_raw(rc) as *const Path) } + } +} + +impl ToOwned for Path { + type Owned = PathBuf; + #[inline] + fn to_owned(&self) -> PathBuf { + self.to_path_buf() + } + #[inline] + fn clone_into(&self, target: &mut PathBuf) { + self.inner.clone_into(&mut target.inner); + } +} + +impl PartialEq for PathBuf { + #[inline] + fn eq(&self, other: &PathBuf) -> bool { + self.components() == other.components() + } +} + +impl Hash for PathBuf { + fn hash(&self, h: &mut H) { + self.as_path().hash(h) + } +} + +impl Eq for PathBuf {} + +impl PartialOrd for PathBuf { + #[inline] + fn partial_cmp(&self, other: &PathBuf) -> Option { + Some(compare_components(self.components(), other.components())) + } +} + +impl Ord for PathBuf { + #[inline] + fn cmp(&self, other: &PathBuf) -> cmp::Ordering { + compare_components(self.components(), other.components()) + } +} + +impl AsRef for PathBuf { + #[inline] + fn as_ref(&self) -> &str { + &self.inner[..] + } +} + +pub struct Path { + inner: str, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct StripPrefixError(()); + +impl Path { + pub fn new + ?Sized>(s: &S) -> &Path { + unsafe { &*(s.as_ref() as *const str as *const Path) } + } + + pub fn from_inner_mut(s: &mut str) -> &mut Path { + unsafe { &mut *(s as *mut str as *mut Path) } + } + + #[inline] + pub fn as_str(&self) -> &str { + &self.inner + } + + #[inline] + pub fn as_mut_str(&mut self) -> &mut str { + &mut self.inner + } + + pub fn to_path_buf(&self) -> PathBuf { + PathBuf { + inner: self.inner.to_owned(), + } + } + + pub fn is_absolute(&self) -> bool { + self.has_root() + } + + pub fn is_relative(&self) -> bool { + !self.is_absolute() + } + + fn prefix(&self) -> Option> { + self.components().prefix + } + + pub fn has_root(&self) -> bool { + self.components().has_root() + } + + pub fn parent(&self) -> Option<&Path> { + let mut comps = self.components(); + let comp = comps.next_back(); + comp.and_then(|p| match p { + Component::Normal(_) | Component::CurDir | Component::ParentDir => { + Some(comps.as_path()) + } + _ => None, + }) + } + + pub fn ancestors(&self) -> Ancestors<'_> { + Ancestors { next: Some(&self) } + } + + pub fn file_name(&self) -> Option<&str> { + self.components().next_back().and_then(|p| match p { + Component::Normal(p) => Some(p), + _ => None, + }) + } + + pub fn strip_prefix

(&self, base: P) -> Result<&Path, StripPrefixError> + where + P: AsRef, + { + self._strip_prefix(base.as_ref()) + } + + fn _strip_prefix(&self, base: &Path) -> Result<&Path, StripPrefixError> { + iter_after(self.components(), base.components()) + .map(|c| c.as_path()) + .ok_or(StripPrefixError(())) + } + + pub fn starts_with>(&self, base: P) -> bool { + self._starts_with(base.as_ref()) + } + + fn _starts_with(&self, base: &Path) -> bool { + iter_after(self.components(), base.components()).is_some() + } + + pub fn ends_with>(&self, child: P) -> bool { + self._ends_with(child.as_ref()) + } + + fn _ends_with(&self, child: &Path) -> bool { + iter_after(self.components().rev(), child.components().rev()).is_some() + } + + pub fn file_stem(&self) -> Option<&str> { + self.file_name() + .map(rsplit_file_at_dot) + .and_then(|(before, after)| before.or(after)) + } + + pub fn file_prefix(&self) -> Option<&str> { + self.file_name() + .map(split_file_at_dot) + .and_then(|(before, _after)| Some(before)) + } + + pub fn extension(&self) -> Option<&str> { + self.file_name() + .map(rsplit_file_at_dot) + .and_then(|(before, after)| before.and(after)) + } + + pub fn join>(&self, path: P) -> PathBuf { + self._join(path.as_ref()) + } + + fn _join(&self, path: &Path) -> PathBuf { + let mut buf = self.to_path_buf(); + buf.push(path); + buf + } + + pub fn with_file_name>(&self, file_name: S) -> PathBuf { + self._with_file_name(file_name.as_ref()) + } + + fn _with_file_name(&self, file_name: &str) -> PathBuf { + let mut buf = self.to_path_buf(); + buf.set_file_name(file_name); + buf + } + + pub fn with_extension>(&self, extension: S) -> PathBuf { + self._with_extension(extension.as_ref()) + } + + fn _with_extension(&self, extension: &str) -> PathBuf { + let self_len = self.as_str().len(); + let self_bytes = self.as_str().as_bytes(); + + let (new_capacity, slice_to_copy) = match self.extension() { + None => { + // Enough capacity for the extension and the dot + let capacity = self_len + extension.len() + 1; + let whole_path = self_bytes.iter(); + (capacity, whole_path) + } + Some(previous_extension) => { + let capacity = self_len + extension.len() - previous_extension.len(); + let path_till_dot = self_bytes[..self_len - previous_extension.len()].iter(); + (capacity, path_till_dot) + } + }; + + let mut new_path = PathBuf::with_capacity(new_capacity); + new_path.as_mut_vec().extend(slice_to_copy); + new_path.set_extension(extension); + new_path + } + + pub fn components(&self) -> Components<'_> { + let prefix = parse_windows_path_prefix(self.as_str()); + let prefix = prefix.ok().map(|(_, prefix)| prefix); + Components { + path: self.as_str(), + has_physical_root: has_physical_root(self.as_str(), prefix.clone()), + prefix, + front: State::Prefix, + back: State::Body, + } + } + + #[inline] + pub fn iter(&self) -> Iter<'_> { + Iter { + inner: self.components(), + } + } + + pub fn into_path_buf(self: Box) -> PathBuf { + let rw = Box::into_raw(self) as *mut str; + let inner = unsafe { Box::from_raw(rw) }; + PathBuf { + inner: String::from(inner), + } + } + + pub fn get_main_sep(&self) -> char { + if let Ok((_, prefix)) = parse_windows_path_prefix(self.as_str()) { + if prefix.is_verbatim() { + return '\\'; + } + } + '/' + } + + pub fn get_main_sep_str(&self) -> &'static str { + if let Ok((_, prefix)) = parse_windows_path_prefix(self.as_str()) { + if prefix.is_verbatim() { + return r"\\"; + } + } + "/" + } + + pub fn to_file_url(&self) -> Result { + let mut serialization = "file://".to_owned(); + let _host_start = serialization.len() as u32; + let (_host_end, _host) = path_to_file_url_segments(self, &mut serialization)?; + let u = Url::parse(&serialization)?; + Ok(u) + } +} + +impl AsRef for Path { + #[inline] + fn as_ref(&self) -> &str { + &self.inner + } +} + +impl fmt::Debug for Path { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Debug::fmt(&self.inner, formatter) + } +} + +impl PartialEq for Path { + #[inline] + fn eq(&self, other: &Path) -> bool { + self.components() == other.components() + } +} + +impl Hash for Path { + fn hash(&self, h: &mut H) { + let path_str = self.as_str(); + let path_bytes = path_str.as_bytes(); + let (prefix_len, verbatim) = match parse_windows_path_prefix(path_str) { + Ok((_, prefix)) => { + prefix.hash(h); + (prefix.len(), prefix.is_verbatim()) + } + _ => (0, false), + }; + let bytes = &path_bytes[prefix_len..]; + + let mut component_start = 0; + let mut bytes_hashed = 0; + + for i in 0..bytes.len() { + let is_sep = if verbatim { + is_verbatim_sep(bytes[i] as char) + } else { + is_sep_byte(bytes[i] as char) + }; + if is_sep { + if i > component_start { + let to_hash = &bytes[component_start..i]; + h.write(to_hash); + bytes_hashed += to_hash.len(); + } + + // skip over separator and optionally a following CurDir item + // since components() would normalize these away. + component_start = i + 1; + + let tail = &bytes[component_start..]; + + if !verbatim { + component_start += match tail { + [b'.'] => 1, + [b'.', sep @ _, ..] if is_sep_byte(*sep as char) => 1, + _ => 0, + }; + } + } + } + + if component_start < bytes.len() { + let to_hash = &bytes[component_start..]; + h.write(to_hash); + bytes_hashed += to_hash.len(); + } + + h.write_usize(bytes_hashed); + } +} + +impl Eq for Path {} + +impl PartialOrd for Path { + #[inline] + fn partial_cmp(&self, other: &Path) -> Option { + Some(compare_components(self.components(), other.components())) + } +} + +impl Ord for Path { + #[inline] + fn cmp(&self, other: &Path) -> cmp::Ordering { + compare_components(self.components(), other.components()) + } +} + +impl AsRef for Path { + #[inline] + fn as_ref(&self) -> &Path { + self + } +} + +impl AsRef for str { + #[inline] + fn as_ref(&self) -> &Path { + Path::new(self) + } +} + +impl AsRef for Cow<'_, str> { + #[inline] + fn as_ref(&self) -> &Path { + Path::new(self) + } +} + +impl AsRef for String { + #[inline] + fn as_ref(&self) -> &Path { + Path::new(self) + } +} + +impl AsRef for PathBuf { + #[inline] + fn as_ref(&self) -> &Path { + self + } +} + +impl<'a> IntoIterator for &'a PathBuf { + type Item = &'a str; + type IntoIter = Iter<'a>; + #[inline] + fn into_iter(self) -> Iter<'a> { + self.iter() + } +} + +impl<'a> IntoIterator for &'a Path { + type Item = &'a str; + type IntoIter = Iter<'a>; + #[inline] + fn into_iter(self) -> Iter<'a> { + self.iter() + } +} + +macro_rules! impl_cmp { + (<$($life:lifetime),*> $lhs:ty, $rhs: ty) => { + impl<$($life),*> PartialEq<$rhs> for $lhs { + #[inline] + fn eq(&self, other: &$rhs) -> bool { + ::eq(self, other) + } + } + + impl<$($life),*> PartialEq<$lhs> for $rhs { + #[inline] + fn eq(&self, other: &$lhs) -> bool { + ::eq(self, other) + } + } + + impl<$($life),*> PartialOrd<$rhs> for $lhs { + #[inline] + fn partial_cmp(&self, other: &$rhs) -> Option { + ::partial_cmp(self, other) + } + } + + impl<$($life),*> PartialOrd<$lhs> for $rhs { + #[inline] + fn partial_cmp(&self, other: &$lhs) -> Option { + ::partial_cmp(self, other) + } + } + }; +} + +impl_cmp!(<> PathBuf, Path); +impl_cmp!(<'a> PathBuf, &'a Path); +impl_cmp!(<'a> Cow<'a, Path>, Path); +impl_cmp!(<'a, 'b> Cow<'a, Path>, &'b Path); +impl_cmp!(<'a> Cow<'a, Path>, PathBuf); + +macro_rules! impl_cmp_str { + (<$($life:lifetime),*> $lhs:ty, $rhs: ty) => { + impl<$($life),*> PartialEq<$rhs> for $lhs { + #[inline] + fn eq(&self, other: &$rhs) -> bool { + ::eq(self, other.as_ref()) + } + } + + impl<$($life),*> PartialEq<$lhs> for $rhs { + #[inline] + fn eq(&self, other: &$lhs) -> bool { + ::eq(self.as_ref(), other) + } + } + + impl<$($life),*> PartialOrd<$rhs> for $lhs { + #[inline] + fn partial_cmp(&self, other: &$rhs) -> Option { + ::partial_cmp(self, other.as_ref()) + } + } + + impl<$($life),*> PartialOrd<$lhs> for $rhs { + #[inline] + fn partial_cmp(&self, other: &$lhs) -> Option { + ::partial_cmp(self.as_ref(), other) + } + } + }; +} + +impl_cmp_str!(<> PathBuf, str); +impl_cmp_str!(<'a> PathBuf, &'a str); +impl_cmp_str!(<'a> PathBuf, Cow<'a, str>); +impl_cmp_str!(<> PathBuf, String); +impl_cmp_str!(<> Path, str); +impl_cmp_str!(<'a> Path, &'a str); +impl_cmp_str!(<'a> Path, Cow<'a, str>); +impl_cmp_str!(<> Path, String); +impl_cmp_str!(<'a> &'a Path, str); +impl_cmp_str!(<'a, 'b> &'a Path, Cow<'b, str>); +impl_cmp_str!(<'a> &'a Path, String); + +impl fmt::Display for StripPrefixError { + #[allow(deprecated, deprecated_in_future)] + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.description().fmt(f) + } +} + +impl Error for StripPrefixError { + #[allow(deprecated)] + fn description(&self) -> &str { + "prefix not found" + } +} + +pub fn absolute>(path: P) -> io::Result { + let path = path.as_ref(); + let path_str = path.as_str(); + if path_str.is_empty() { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "cannot make an empty path absolute", + )); + } + let prefix = parse_windows_path_prefix(path_str); + + if prefix.map(|(_, p)| p.is_verbatim()).unwrap_or(false) { + if path_str.contains('\0') { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "strings passed to WinAPI cannot contain NULs", + )); + } + return Ok(PathBuf::from(path_str.to_owned())); + } + + let mut components = path.strip_prefix(".").unwrap_or(path).components(); + + let path_str = path.as_str(); + let mut normalized = if path.is_absolute() { + if path_str.starts_with("//") && !path_str.starts_with("///") { + components.next(); + PathBuf::from("//") + } else { + PathBuf::new() + } + } else { + PathBuf::new() + }; + normalized.extend(components); + + if path_str.ends_with('/') || path_str.ends_with('\\') { + normalized.push(""); + } + + Ok(normalized) +} + +#[cfg(test)] +mod tests { + + use url::Url; + + use crate::{url::PathToUrlError, Path, PathBuf}; + + #[test] + fn test_black_slash_and_slash_mix_path() { + let mut path = PathBuf::from(r"\\?\C:\Users\test\Desktop/1.txt"); + path.push("abc"); + assert_eq!(path.as_str(), r"\\?\C:\Users\test\Desktop/1.txt\abc"); + } + + #[test] + fn test_to_file_url() { + let test_fn = |source: &str| -> Result { + let p = Path::new(source); + let u: Url = p.to_file_url()?; + Ok(u.to_string()) + }; + assert_eq!( + test_fn(r"\\?\UNC\server\share\path").expect("parse success"), + "file://server/share/path" + ); + assert_eq!( + test_fn(r"\\?\C:\path").expect("parse success"), + "file:///C:/path" + ); + assert_eq!( + test_fn(r"\\server\share\path").expect("parse success"), + "file://server/share/path" + ); + assert_eq!( + test_fn(r"C:\path").expect("parse success"), + "file:///C:/path" + ); + assert!(matches!( + test_fn(r"\\?\abc\path"), + Err(PathToUrlError::UrlNotSupportedPrefix { .. }) + )); + assert!(matches!( + test_fn(r"\\.\device\path"), + Err(PathToUrlError::UrlNotSupportedPrefix { .. }) + )); + assert!(matches!( + test_fn(r"~/a"), + Err(PathToUrlError::PathNotAbsoluteError { .. }) + )); + } +} diff --git a/crates/quirks_path/src/url.rs b/crates/quirks_path/src/url.rs new file mode 100644 index 0000000..74fa94b --- /dev/null +++ b/crates/quirks_path/src/url.rs @@ -0,0 +1,97 @@ +use std::{borrow::Cow, fmt::Write}; + +use percent_encoding::{percent_encode, AsciiSet, CONTROLS}; + +use crate::{windows::parse_drive, Component, Path, Prefix}; + +const URL_FRAGMENT: &AsciiSet = &CONTROLS.add(b' ').add(b'"').add(b'<').add(b'>').add(b'`'); +const URL_PATH: &AsciiSet = &URL_FRAGMENT.add(b'#').add(b'?').add(b'{').add(b'}'); +const URL_PATH_SEGMENT: &AsciiSet = &URL_PATH.add(b'/').add(b'%'); + +#[derive(thiserror::Error, Debug)] +pub enum PathToUrlError { + #[error("Path not absolute: {path}")] + PathNotAbsoluteError { path: Cow<'static, str> }, + #[error("Invalid UNC path")] + ParseUrlError(#[from] ::url::ParseError), + #[error("Path prefix can not be a url: {path}")] + UrlNotSupportedPrefix { path: Cow<'static, str> }, +} + +#[inline] +pub fn to_u32(i: usize) -> Result { + if i <= ::std::u32::MAX as usize { + Ok(i as u32) + } else { + Err(url::ParseError::Overflow) + } +} + +pub(crate) fn path_to_file_url_segments( + path: &Path, + serialization: &mut String, +) -> Result<(u32, Option<::url::Host>), PathToUrlError> { + if !path.is_absolute() { + return Err(PathToUrlError::PathNotAbsoluteError { + path: Cow::Owned(path.as_str().to_string()), + }); + } + let mut components = path.components(); + + let host_start = serialization.len() + 1; + let host_end; + let host_internal: Option>; + + match components.next() { + Some(Component::Prefix(ref p)) => match p.kind() { + Prefix::Disk { drive } | Prefix::VerbatimDisk { drive } => { + host_end = to_u32(serialization.len()).unwrap(); + host_internal = None; + serialization.push('/'); + serialization.push(drive); + serialization.push(':'); + } + Prefix::UNC { server, share } | Prefix::VerbatimUNC { server, share } => { + let host = url::Host::parse(server)?; + write!(serialization, "{}", host).unwrap(); + host_end = to_u32(serialization.len()).unwrap(); + host_internal = Some(host); + serialization.push('/'); + serialization.extend(percent_encode(share.as_bytes(), URL_PATH_SEGMENT)); + } + _ => { + return Err(PathToUrlError::UrlNotSupportedPrefix { + path: Cow::Owned(path.as_str().to_string()), + }) + } + }, + _ => { + return Err(PathToUrlError::UrlNotSupportedPrefix { + path: Cow::Owned(path.as_str().to_string()), + }) + } + } + + let mut path_only_has_prefix = true; + for component in components { + if matches!(component, Component::RootDir(..)) { + continue; + } + + path_only_has_prefix = false; + let component = component.as_str(); + + serialization.push('/'); + serialization.extend(percent_encode(component.as_bytes(), URL_PATH_SEGMENT)); + } + + // A windows drive letter must end with a slash. + if serialization.len() > host_start + && matches!(parse_drive(&serialization[host_start..]), Ok(..)) + && path_only_has_prefix + { + serialization.push('/'); + } + + Ok((host_end, host_internal)) +} diff --git a/crates/quirks_path/src/windows.rs b/crates/quirks_path/src/windows.rs new file mode 100644 index 0000000..48354ab --- /dev/null +++ b/crates/quirks_path/src/windows.rs @@ -0,0 +1,138 @@ +use nom::{ + bytes::complete::tag, + character::complete::{self, satisfy}, + combinator::peek, + error::{context, ContextError, Error, ErrorKind, ParseError}, + sequence::pair, + AsChar, IResult, InputIter, InputTakeAtPosition, +}; + +use crate::Prefix; + +fn non_slash(input: &str) -> IResult<&str, &str> { + input.split_at_position_complete(|item| item != '/') +} + +pub fn parse_drive(path: &str) -> IResult<&str, char> { + context("drive", satisfy(char::is_alpha))(path).map(|a: (&str, char)| a) +} + +pub fn parse_drive_exact(path: &str) -> IResult<&str, char> { + context("drive_exact", pair(parse_drive, complete::char(':')))(path) + .map(|(path, (drive, _))| (path, drive)) +} + +pub fn is_windows_verbatim_sep(c: char) -> bool { + c == '\\' +} + +pub fn is_windows_sep(c: char) -> bool { + c == '\\' || c == '/' +} + +pub fn parse_windows_next_component(path: &str, verbatim: bool) -> (&str, &str, &str) { + let separator = if verbatim { + is_windows_verbatim_sep + } else { + is_windows_sep + }; + let p = path.as_bytes(); + match p.position(|x| separator(x as char)) { + Some(separator_start) => { + let separator_end = separator_start + 1; + let component = &path[0..separator_start]; + let path_with_sep = &path[separator_start..]; + let path_without_sep = &path[separator_end..]; + + (component, path_with_sep, path_without_sep) + } + None => (path, "", ""), + } +} + +fn context_verify_error<'a>(input: &'a str, context: &'static str) -> nom::Err> { + nom::Err::Error(Error::add_context( + input, + context, + Error::from_error_kind(input, ErrorKind::Verify), + )) +} + +pub fn parse_windows_path_prefix(raw_path: &str) -> IResult<&str, Prefix<'_>> { + if let Ok((path, _)) = tag(r"\\")(raw_path) as IResult<&str, &str> { + if let Ok((path, _)) = tag(r"?\")(path) as IResult<&str, &str> { + if let Ok((path, _)) = peek(non_slash)(path) as IResult<&str, &str> { + if let Ok((path, _)) = tag(r"UNC\")(path) as IResult<&str, &str> { + let (server, _, other) = parse_windows_next_component(path, true); + let (share, next_input, _) = parse_windows_next_component(other, true); + + return Ok((next_input, Prefix::VerbatimUNC { server, share })); + } else if let Ok((path, drive)) = parse_drive_exact(path) { + return Ok((path, Prefix::VerbatimDisk { drive })); + } else { + let (prefix, next_input, _) = parse_windows_next_component(path, true); + return Ok((next_input, Prefix::Verbatim { prefix })); + } + } + } + if let Ok((path, _)) = tag(r".\")(path) as IResult<&str, &str> { + let (prefix, next_input, _) = parse_windows_next_component(path, false); + return Ok((next_input, Prefix::DeviceNS { device: prefix })); + } + let (server, _, other) = parse_windows_next_component(path, false); + let (share, next_input, _) = parse_windows_next_component(other, false); + + if !server.is_empty() && !share.is_empty() { + return Ok((next_input, Prefix::UNC { server, share })); + } + return Err(context_verify_error(raw_path, "windows path prefix")); + } else if let Ok((path, drive)) = parse_drive_exact(raw_path) { + return Ok((path, Prefix::Disk { drive })); + } else { + return Err(context_verify_error(raw_path, "windows path prefix")); + } +} + +#[cfg(test)] +mod tests { + #[test] + fn test_parse_windows_path_prefix() { + use super::*; + assert_eq!( + parse_windows_path_prefix(r"\\?\UNC\server\share\path"), + Ok(( + r"\path", + Prefix::VerbatimUNC { + server: "server", + share: "share" + } + )) + ); + assert_eq!( + parse_windows_path_prefix(r"\\?\C:\path"), + Ok((r"\path", Prefix::VerbatimDisk { drive: 'C' })) + ); + assert_eq!( + parse_windows_path_prefix(r"\\server\share\path"), + Ok(( + r"\path", + Prefix::UNC { + server: "server", + share: "share" + } + )) + ); + assert_eq!( + parse_windows_path_prefix(r"C:\path"), + Ok((r"\path", Prefix::Disk { drive: 'C' })) + ); + assert_eq!( + parse_windows_path_prefix(r"\\.\device\path"), + Ok((r"\path", Prefix::DeviceNS { device: "device" })) + ); + assert_eq!( + parse_windows_path_prefix(r"\\?\abc\path"), + Ok((r"\path", Prefix::Verbatim { prefix: "abc" })) + ) + } +} diff --git a/crates/recorder/src/downloaders/qbitorrent.rs b/crates/recorder/src/downloaders/qbitorrent.rs index 52a0270..92632df 100644 --- a/crates/recorder/src/downloaders/qbitorrent.rs +++ b/crates/recorder/src/downloaders/qbitorrent.rs @@ -457,11 +457,7 @@ pub mod tests { let docker = testcontainers::clients::Cli::default(); let image = create_qbit_testcontainer(); - let container = docker.run(image); - - let mut exec = ExecCommand::default(); - - container.exec(exec); + let _container = docker.run(image); test_qbittorrent_downloader_impl().await; } diff --git a/crates/recorder/src/path/vfs_path.rs b/crates/recorder/src/path/vfs_path.rs index 5acbebd..0b991b8 100644 --- a/crates/recorder/src/path/vfs_path.rs +++ b/crates/recorder/src/path/vfs_path.rs @@ -1,4 +1,4 @@ -use std::path::PathBuf; +use std::{borrow::Cow, collections::VecDeque, path::PathBuf}; use lazy_static::lazy_static; pub use uni_path::{Path as VFSSubPath, PathBuf as VFSSubPathBuf}; @@ -77,8 +77,8 @@ impl<'a> VFSPath<'a> { #[derive(Clone, Debug)] pub struct VFSPathBuf { - pub root: String, - pub sub: VFSSubPathBuf, + root: String, + sub: VFSSubPathBuf, } impl VFSPathBuf {