diff --git a/Cargo.lock b/Cargo.lock index 71926edf2a2c4..415af5e186c1f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -747,9 +747,9 @@ dependencies = [ [[package]] name = "bstr" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ffdb39cb703212f3c11973452c2861b972f757b021158f3516ba10f2fa8b2c1" +checksum = "c3d4260bcc2e8fc9df1eac4919a720effeb63a3f0952f5bf4944adfa18897f09" dependencies = [ "memchr", "once_cell", @@ -9017,6 +9017,7 @@ dependencies = [ name = "turbopath" version = "0.1.0" dependencies = [ + "bstr", "path-slash", "serde", "thiserror", @@ -9170,8 +9171,8 @@ version = "1.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97fee6b57c6a41524a810daee9286c02d7752c4253064d0b05472833a438f675" dependencies = [ - "cfg-if 0.1.10", - "rand 0.4.6", + "cfg-if 1.0.0", + "rand 0.8.5", "static_assertions", ] diff --git a/crates/turbopath/Cargo.toml b/crates/turbopath/Cargo.toml index b6fb4d764961d..df579a0bc71eb 100644 --- a/crates/turbopath/Cargo.toml +++ b/crates/turbopath/Cargo.toml @@ -7,6 +7,7 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +bstr = "1.4.0" path-slash = "0.2.1" # TODO: Make this a crate feature serde = { workspace = true } diff --git a/crates/turbopath/src/anchored_system_path_buf.rs b/crates/turbopath/src/anchored_system_path_buf.rs index fc8c7a3fd0970..c2eff48d26cc9 100644 --- a/crates/turbopath/src/anchored_system_path_buf.rs +++ b/crates/turbopath/src/anchored_system_path_buf.rs @@ -3,8 +3,7 @@ use std::path::{Path, PathBuf}; use serde::{Deserialize, Serialize}; use crate::{ - AbsoluteSystemPathBuf, IntoSystem, IntoUnix, PathError, PathValidationError, - RelativeUnixPathBuf, + AbsoluteSystemPathBuf, IntoSystem, PathError, PathValidationError, RelativeUnixPathBuf, }; #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Serialize, Deserialize)] @@ -15,7 +14,8 @@ impl TryFrom<&Path> for AnchoredSystemPathBuf { fn try_from(path: &Path) -> Result { if path.is_absolute() { - return Err(PathValidationError::NotRelative(path.to_path_buf()).into()); + let bad_path = path.display().to_string(); + return Err(PathValidationError::NotRelative(bad_path).into()); } Ok(AnchoredSystemPathBuf(path.into_system()?)) @@ -52,9 +52,17 @@ impl AnchoredSystemPathBuf { .ok_or_else(|| PathValidationError::InvalidUnicode(self.0.clone()).into()) } - pub fn to_unix(&self) -> Result { - let p = self.as_path().into_unix()?; - RelativeUnixPathBuf::new(p) + pub fn to_unix(&self) -> Result { + #[cfg(unix)] + { + use std::os::unix::ffi::OsStrExt; + let bytes = self.0.as_os_str().as_bytes(); + return RelativeUnixPathBuf::new(bytes); + } + #[cfg(not(unix))] + { + unimplemented!() + } } } diff --git a/crates/turbopath/src/lib.rs b/crates/turbopath/src/lib.rs index 4557cf1821970..dab88efc0a5b7 100644 --- a/crates/turbopath/src/lib.rs +++ b/crates/turbopath/src/lib.rs @@ -26,6 +26,8 @@ pub enum PathError { IO(#[from] io::Error), #[error("Path prefix error: {0}")] PrefixError(#[from] StripPrefixError), + #[error("Invalid UTF8: {0}")] + Utf8Error(#[from] bstr::Utf8Error), } impl PathError { @@ -45,13 +47,18 @@ pub enum PathValidationError { #[error("Path is not absolute: {0}")] NotAbsolute(PathBuf), #[error("Path is not relative: {0}")] - NotRelative(PathBuf), + NotRelative(String), #[error("Path {0} is not parent of {1}")] NotParent(String, String), #[error("Path {0} is not a unix path")] NotUnix(String), } +pub(crate) fn not_relative_error(bytes: &[u8]) -> PathValidationError { + let s = String::from_utf8_lossy(bytes).to_string(); + PathValidationError::NotRelative(s) +} + trait IntoSystem { fn into_system(self) -> Result; } diff --git a/crates/turbopath/src/relative_system_path_buf.rs b/crates/turbopath/src/relative_system_path_buf.rs index ef47cc6e24622..1d9fbcc9faef1 100644 --- a/crates/turbopath/src/relative_system_path_buf.rs +++ b/crates/turbopath/src/relative_system_path_buf.rs @@ -36,7 +36,8 @@ impl RelativeSystemPathBuf { pub fn new(unchecked_path: impl Into) -> Result { let unchecked_path = unchecked_path.into(); if unchecked_path.is_absolute() { - return Err(PathValidationError::NotRelative(unchecked_path)); + let bad_path = unchecked_path.display().to_string(); + return Err(PathValidationError::NotRelative(bad_path)); } let system_path = unchecked_path.into_system()?; diff --git a/crates/turbopath/src/relative_unix_path.rs b/crates/turbopath/src/relative_unix_path.rs index a8adfc244d016..ccf7f613821f8 100644 --- a/crates/turbopath/src/relative_unix_path.rs +++ b/crates/turbopath/src/relative_unix_path.rs @@ -1,25 +1,48 @@ -use std::path::Path; +use std::{ffi::OsString, path::PathBuf}; -use crate::{IntoSystem, PathError, PathValidationError, RelativeSystemPathBuf}; +use bstr::BStr; +use crate::{not_relative_error, PathError, RelativeSystemPathBuf}; + +#[repr(transparent)] pub struct RelativeUnixPath { - inner: Path, + inner: BStr, } impl RelativeUnixPath { - pub fn new>(value: &P) -> Result<&Self, PathError> { + pub fn new>(value: &P) -> Result<&Self, PathError> { let path = value.as_ref(); - if path.is_absolute() { - return Err(PathValidationError::NotRelative(path.to_owned()).into()); + if path[0] == b'/' { + return Err(not_relative_error(path).into()); } // copied from stdlib path.rs: relies on the representation of // RelativeUnixPath being just a Path, the same way Path relies on // just being an OsStr - Ok(unsafe { &*(path as *const Path as *const Self) }) + Ok(unsafe { &*(path as *const BStr as *const Self) }) } pub fn to_system_path(&self) -> Result { - let system_path = self.inner.into_system()?; - Ok(RelativeSystemPathBuf::new_unchecked(system_path)) + #[cfg(unix)] + { + // On unix, unix paths are already system paths. Copy the bytes + // but skip validation. + use std::os::unix::prelude::OsStringExt; + let path = PathBuf::from(OsString::from_vec(self.inner.to_vec())); + Ok(RelativeSystemPathBuf::new_unchecked(path)) + } + + #[cfg(windows)] + { + let system_path_bytes = self + .inner + .iter() + .map(|byte| if *byte == b'/' { b'\\' } else { *byte }) + .collect::>(); + // Is this safe to do? We think we have utf8 bytes or bytes that roundtrip + // through utf8 + let system_path_string = unsafe { String::from_utf8_unchecked(system_path_bytes) }; + let system_path_buf = PathBuf::from(system_path_string); + Ok(RelativeSystemPathBuf::new_unchecked(system_path_buf)) + } } } diff --git a/crates/turbopath/src/relative_unix_path_buf.rs b/crates/turbopath/src/relative_unix_path_buf.rs index 16b847356899c..22aa0feb9da3f 100644 --- a/crates/turbopath/src/relative_unix_path_buf.rs +++ b/crates/turbopath/src/relative_unix_path_buf.rs @@ -1,11 +1,9 @@ -use std::path::PathBuf; +use bstr::{BString, ByteSlice}; -use serde::Serialize; +use crate::{not_relative_error, PathError, PathValidationError}; -use crate::{IntoUnix, PathError, PathValidationError}; - -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Serialize)] -pub struct RelativeUnixPathBuf(PathBuf); +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] +pub struct RelativeUnixPathBuf(BString); impl RelativeUnixPathBuf { /// Create a new RelativeUnixPathBuf from a PathBuf by calling `into_unix()` @@ -24,32 +22,49 @@ impl RelativeUnixPathBuf { /// /// ``` /// ``` - pub fn new(path: impl Into) -> Result { - let path = path.into(); - if path.is_absolute() { - return Err(PathValidationError::NotRelative(path)); - } + // pub fn new(path: impl Into) -> Result { + // let path = path.into(); + // if path.is_absolute() { + // return Err(PathValidationError::NotRelative(path)); + // } - Ok(RelativeUnixPathBuf(path.into_unix()?)) + // Ok(RelativeUnixPathBuf(path.into_unix()?)) + // } + pub fn new(path: impl Into>) -> Result { + let bytes: Vec = path.into(); + if bytes[0] == b'/' { + return Err(not_relative_error(&bytes).into()); + } + Ok(Self(BString::new(bytes))) } - pub fn to_str(&self) -> Result<&str, PathValidationError> { - self.0 - .to_str() - .ok_or_else(|| PathValidationError::InvalidUnicode(self.0.clone())) + pub fn as_str(&self) -> Result<&str, PathError> { + let s = self.0.to_str()?; + Ok(s) } + // pub fn to_str(&self) -> Result<&str, PathValidationError> { + // self.0 + // .to_str() + // .ok_or_else(|| PathValidationError::InvalidUnicode(self.0.clone())) + // } + pub fn strip_prefix(&self, prefix: &RelativeUnixPathBuf) -> Result { - let stripped = self.0.strip_prefix(&prefix.0)?; - let path = Self::new(stripped)?; - Ok(path) + if !self.0.starts_with(&prefix.0) { + return Err(PathError::PathValidationError( + PathValidationError::NotParent(prefix.0.to_string(), self.0.to_string()), + )); + } + let tail_slice = &self.0[prefix.0.len()..]; + Self::new(tail_slice) } - pub fn join(&self, tail: &RelativeUnixPathBuf) -> Result { - // TODO: fix this to always be / - let path = self.0.join(&tail.0); - let path = path.as_path().into_unix()?; - Ok(RelativeUnixPathBuf(path)) + pub fn join(&self, tail: &RelativeUnixPathBuf) -> Self { + let buffer = Vec::with_capacity(self.0.len() + 1 + tail.0.len()); + let mut path = BString::new(buffer); + path.push(b'/'); + path.extend_from_slice(&tail.0); + Self(path) } } @@ -62,22 +77,23 @@ mod tests { #[test] fn test_relative_unix_path_buf() { - let path = RelativeUnixPathBuf::new(PathBuf::from("foo/bar")).unwrap(); - assert_eq!(path.to_str().unwrap(), "foo/bar"); + let path = RelativeUnixPathBuf::new("foo/bar").unwrap(); + assert_eq!(path.as_str().unwrap(), "foo/bar"); } #[test] fn test_relative_unix_path_buf_with_extension() { - let path = RelativeUnixPathBuf::new(PathBuf::from("foo/bar.txt")).unwrap(); - assert_eq!(path.to_str().unwrap(), "foo/bar.txt"); + let path = RelativeUnixPathBuf::new("foo/bar.txt").unwrap(); + assert_eq!(path.as_str().unwrap(), "foo/bar.txt"); } #[test] fn test_relative_unix_path_buf_errors() { - #[cfg(not(windows))] - assert!(RelativeUnixPathBuf::new(PathBuf::from("/foo/bar")).is_err()); - #[cfg(windows)] - assert!(RelativeUnixPathBuf::new(PathBuf::from("C:\\foo\\bar")).is_err()); + assert!(RelativeUnixPathBuf::new("/foo/bar").is_err()); + // Note: this shouldn't be an error, this is a valid relative unix path + // #[cfg(windows)] + // assert!(RelativeUnixPathBuf::new(PathBuf::from("C:\\foo\\bar")). + // is_err()); } #[cfg(windows)]