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/absolute_system_path_buf.rs b/crates/turbopath/src/absolute_system_path_buf.rs index 3cc18b348ef41..f0d6813f8b1af 100644 --- a/crates/turbopath/src/absolute_system_path_buf.rs +++ b/crates/turbopath/src/absolute_system_path_buf.rs @@ -163,6 +163,14 @@ impl AbsoluteSystemPathBuf { AbsoluteSystemPathBuf(self.0.join(Path::new(segment))) } + pub fn join_unix_path_literal>( + &self, + unix_path: S, + ) -> Result { + let tail = Path::new(unix_path.as_ref()).into_system()?; + Ok(AbsoluteSystemPathBuf(self.0.join(tail))) + } + pub fn ensure_dir(&self) -> Result<(), io::Error> { if let Some(parent) = self.0.parent() { fs::create_dir_all(parent) @@ -171,6 +179,10 @@ impl AbsoluteSystemPathBuf { } } + pub fn create_dir_all(&self) -> Result<(), io::Error> { + fs::create_dir_all(self.0.as_path()) + } + pub fn remove(&self) -> Result<(), io::Error> { fs::remove_file(self.0.as_path()) } @@ -245,6 +257,11 @@ impl AbsoluteSystemPathBuf { pub fn open(&self) -> Result { Ok(fs::File::open(&self.0)?) } + + pub fn to_realpath(&self) -> Result { + let realpath = fs::canonicalize(&self.0)?; + Ok(Self(realpath)) + } } impl From for PathBuf { diff --git a/crates/turbopath/src/anchored_system_path_buf.rs b/crates/turbopath/src/anchored_system_path_buf.rs index ab309a134f055..a3e81b59407c4 100644 --- a/crates/turbopath/src/anchored_system_path_buf.rs +++ b/crates/turbopath/src/anchored_system_path_buf.rs @@ -2,7 +2,9 @@ use std::path::{Path, PathBuf}; use serde::{Deserialize, Serialize}; -use crate::{AbsoluteSystemPathBuf, IntoSystem, PathError, PathValidationError}; +use crate::{ + AbsoluteSystemPathBuf, IntoSystem, PathError, PathValidationError, RelativeUnixPathBuf, +}; #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Serialize, Deserialize)] pub struct AnchoredSystemPathBuf(PathBuf); @@ -12,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()?)) @@ -33,6 +36,12 @@ impl AnchoredSystemPathBuf { Ok(AnchoredSystemPathBuf(stripped_path)) } + pub fn from_raw>(raw: P) -> Result { + let system_path = raw.as_ref(); + let system_path = system_path.into_system()?; + Ok(Self(system_path)) + } + pub fn as_path(&self) -> &Path { self.0.as_path() } @@ -42,6 +51,24 @@ impl AnchoredSystemPathBuf { .to_str() .ok_or_else(|| PathValidationError::InvalidUnicode(self.0.clone()).into()) } + + 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))] + { + use crate::IntoUnix; + let unix_buf = self.0.as_path().into_unix()?; + let unix_str = unix_buf + .to_str() + .ok_or_else(|| PathValidationError::InvalidUnicode(unix_buf.clone()))?; + return RelativeUnixPathBuf::new(unix_str.as_bytes()); + } + } } impl From for PathBuf { diff --git a/crates/turbopath/src/lib.rs b/crates/turbopath/src/lib.rs index c5bca98cd60a1..bd7273d524146 100644 --- a/crates/turbopath/src/lib.rs +++ b/crates/turbopath/src/lib.rs @@ -8,7 +8,7 @@ mod relative_unix_path_buf; use std::{ io, - path::{Path, PathBuf}, + path::{Path, PathBuf, StripPrefixError}, }; pub use absolute_system_path_buf::AbsoluteSystemPathBuf; @@ -24,6 +24,16 @@ pub enum PathError { PathValidationError(#[from] PathValidationError), #[error("IO Error {0}")] IO(#[from] io::Error), + #[error("Path prefix error: {0}")] + PrefixError(#[from] StripPrefixError), + #[error("Invalid UTF8: {0:?}")] + Utf8Error(Vec), +} + +impl From for PathError { + fn from(value: std::string::FromUtf8Error) -> Self { + PathError::Utf8Error(value.into_bytes()) + } } impl PathError { @@ -43,11 +53,20 @@ 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), + #[error("{0} is not a prefix for {1}")] + PrefixError(String, String), +} + +impl PathValidationError { + pub(crate) fn not_relative_error(bytes: &[u8]) -> PathValidationError { + let s = String::from_utf8_lossy(bytes).to_string(); + PathValidationError::NotRelative(s) + } } trait IntoSystem { 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..019609acc42be 100644 --- a/crates/turbopath/src/relative_unix_path.rs +++ b/crates/turbopath/src/relative_unix_path.rs @@ -1,25 +1,46 @@ -use std::path::Path; +use std::path::PathBuf; -use crate::{IntoSystem, PathError, PathValidationError, RelativeSystemPathBuf}; +use bstr::BStr; +use crate::{PathError, PathValidationError, 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.first() == Some(&b'/') { + return Err(PathValidationError::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 + // RelativeUnixPath being just a BStr, 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::{ffi::OsString, 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::>(); + let system_path_string = String::from_utf8(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 95306ee28b763..a3d8124595e7d 100644 --- a/crates/turbopath/src/relative_unix_path_buf.rs +++ b/crates/turbopath/src/relative_unix_path_buf.rs @@ -1,75 +1,182 @@ -use std::path::PathBuf; +use std::{fmt::Debug, io::Write}; -use serde::Serialize; +use bstr::{BString, ByteSlice}; -use crate::{IntoUnix, PathValidationError}; +use crate::{PathError, PathValidationError}; -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Serialize)] -pub struct RelativeUnixPathBuf(PathBuf); +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] +pub struct RelativeUnixPathBuf(BString); impl RelativeUnixPathBuf { - /// Create a new RelativeUnixPathBuf from a PathBuf by calling `into_unix()` - /// - /// NOTE: `into_unix` *only* converts Windows paths to Unix paths *on* a - /// Windows system. Do not pass a Windows path on a Unix system and - /// assume it'll be converted. - /// - /// # Arguments - /// - /// * `path`: - /// - /// returns: Result - /// - /// # Examples - /// - /// ``` - /// ``` - 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 bytes: Vec = path.into(); + if bytes.first() == Some(&b'/') { + return Err(PathValidationError::not_relative_error(&bytes).into()); } - - Ok(RelativeUnixPathBuf(path.into_unix()?)) + Ok(Self(BString::new(bytes))) } - pub fn to_str(&self) -> Result<&str, PathValidationError> { - self.0 + pub fn as_str(&self) -> Result<&str, PathError> { + let s = self + .0 .to_str() - .ok_or_else(|| PathValidationError::InvalidUnicode(self.0.clone())) + .or_else(|_| Err(PathError::Utf8Error(self.0.as_bytes().to_owned())))?; + Ok(s) + } + + // write_escaped_bytes writes this path to the given writer in the form + // "", where escaped_path is the path with '"' and '\n' + // characters escaped with '\'. + pub fn write_escaped_bytes(&self, writer: &mut W) -> Result<(), PathError> { + writer.write_all(&[b'\"'])?; + // i is our pointer into self.0, and to_escape_index is a pointer to the next + // byte to be escaped. Each time we find a byte to be escaped, we write + // out everything from i to to_escape_index, then the escape byte, '\\', + // then the byte-to-be-escaped. Finally we set i to 1 + to_escape_index + // to move our pointer past the byte we just escaped. + let mut i: usize = 0; + for (to_escaped_index, byte) in self + .0 + .iter() + .enumerate() + .filter(|(_, byte)| **byte == b'\"' || **byte == b'\n') + { + writer.write_all(&self.0[i..to_escaped_index])?; + writer.write_all(&[b'\\', *byte])?; + i = to_escaped_index + 1; + } + if i < self.0.len() { + writer.write_all(&self.0[i..])?; + } + writer.write_all(&[b'\"'])?; + Ok(()) + } + + pub fn strip_prefix(&self, prefix: &RelativeUnixPathBuf) -> Result { + let prefix_len = prefix.0.len(); + if prefix_len == 0 { + return Ok(self.clone()); + } + if !self.0.starts_with(&prefix.0) { + return Err(PathError::PathValidationError( + PathValidationError::NotParent(prefix.0.to_string(), self.0.to_string()), + )); + } + + // Handle the case where we are stripping the entire contents of this path + if self.0.len() == prefix.0.len() { + return Self::new(""); + } + + // We now know that this path starts with the prefix, and that this path's + // length is greater than the prefix's length + if self.0[prefix_len] != b'/' { + let prefix_str = prefix.0.to_str_lossy().into_owned(); + let this = self.0.to_str_lossy().into_owned(); + return Err(PathError::PathValidationError( + PathValidationError::PrefixError(prefix_str, this), + )); + } + + let tail_slice = &self.0[(prefix_len + 1)..]; + Self::new(tail_slice) + } + + 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); + if self.0.len() > 0 { + path.extend_from_slice(&self.0); + path.push(b'/'); + } + path.extend_from_slice(&tail.0); + Self(path) + } +} + +impl Debug for RelativeUnixPathBuf { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self.as_str() { + Ok(s) => write!(f, "{}", s), + Err(_) => write!(f, "Non-utf8 {:?}", self.0), + } } } #[cfg(test)] mod tests { + use std::io::BufWriter; + #[cfg(windows)] use std::path::Path; use super::*; #[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()); + fn test_join() { + let head = RelativeUnixPathBuf::new("some/path").unwrap(); + let tail = RelativeUnixPathBuf::new("child/leaf").unwrap(); + let combined = head.join(&tail); + assert_eq!(combined.as_str().unwrap(), "some/path/child/leaf"); } - #[cfg(windows)] #[test] - fn test_convert_from_windows_path() { - let path = RelativeUnixPathBuf::new(PathBuf::from("foo\\bar")).unwrap(); - assert_eq!(path.0.as_path(), Path::new("foo/bar")); + fn test_strip_prefix() { + let combined = RelativeUnixPathBuf::new("some/path/child/leaf").unwrap(); + let head = RelativeUnixPathBuf::new("some/path").unwrap(); + let expected = RelativeUnixPathBuf::new("child/leaf").unwrap(); + let tail = combined.strip_prefix(&head).unwrap(); + assert_eq!(tail, expected); + } + + #[test] + fn test_strip_entire_contents() { + let combined = RelativeUnixPathBuf::new("some/path").unwrap(); + let head = combined.clone(); + let expected = RelativeUnixPathBuf::new("").unwrap(); + let tail = combined.strip_prefix(&head).unwrap(); + assert_eq!(tail, expected); + } + + #[test] + fn test_strip_empty_prefix() { + let combined = RelativeUnixPathBuf::new("some/path").unwrap(); + let tail = combined + .strip_prefix(&RelativeUnixPathBuf::new("").unwrap()) + .unwrap(); + assert_eq!(tail, combined); + } + + #[test] + fn test_write_escaped() { + let input = "\"quote\"\nnewline\n".as_bytes(); + let expected = "\"\\\"quote\\\"\\\nnewline\\\n\"".as_bytes(); + let mut buffer = Vec::new(); + { + let mut writer = BufWriter::new(&mut buffer); + let path = RelativeUnixPathBuf::new(input).unwrap(); + path.write_escaped_bytes(&mut writer).unwrap(); + } + assert_eq!(buffer.as_slice(), expected); + } + + #[test] + fn test_relative_unix_path_buf_errors() { + 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()); } }