#![cfg_attr(not(feature = "std"), no_std)]
mod slicevec;
mod util;
use slicevec::SliceVec;
use util::{ComponentIter, ComponentStack, SymlinkCounter};
#[cfg(target_family = "unix")]
const PATH_MAX: usize = libc::PATH_MAX as usize;
#[cfg(target_os = "wasi")]
const PATH_MAX: usize = 4096;
#[cfg(feature = "std")]
pub fn normpath<P: AsRef<std::path::Path>>(path: P) -> std::io::Result<std::path::PathBuf> {
#[cfg(target_family = "unix")]
use std::os::unix::prelude::*;
#[cfg(target_os = "wasi")]
use std::os::wasi::prelude::*;
let path = path.as_ref().as_os_str().as_bytes();
let mut buf = vec![0; path.len()];
let len = normpath_raw(path, &mut buf).map_err(std::io::Error::from_raw_os_error)?;
buf.truncate(len);
Ok(std::ffi::OsString::from_vec(buf).into())
}
pub fn normpath_raw(path: &[u8], buf: &mut [u8]) -> Result<usize, i32> {
let mut buf = SliceVec::empty(buf);
for component in ComponentIter::new(path)? {
if component == b"/" || component == b"//" {
buf.replace(component)?;
} else if component == b".." {
buf.make_parent_path()?;
} else {
if !matches!(buf.as_ref(), b"/" | b"//" | b"") {
buf.push(b'/')?;
}
buf.extend_from_slice(component)?;
}
}
if buf.is_empty() {
buf.push(b'.')?;
}
Ok(buf.len())
}
bitflags::bitflags! {
pub struct RealpathFlags: u32 {
const ALLOW_MISSING = 0x01;
const ALLOW_LAST_MISSING = 0x02;
const IGNORE_SYMLINKS = 0x04;
}
}
#[cfg(feature = "std")]
#[derive(Clone)]
pub struct RealpathBuilder {
max_len: usize,
flags: RealpathFlags,
}
#[cfg(feature = "std")]
impl RealpathBuilder {
#[inline]
pub fn new() -> Self {
Self {
max_len: if cfg!(target_os = "wasi") {
32768
} else {
PATH_MAX
},
flags: RealpathFlags::empty(),
}
}
#[inline]
pub fn max_len(&mut self, max_len: usize) -> &mut Self {
self.max_len = max_len;
self
}
#[inline]
pub fn flags(&mut self, flags: RealpathFlags) -> &mut Self {
self.flags = flags;
self
}
pub fn realpath<P: AsRef<std::path::Path>>(
&self,
path: P,
) -> std::io::Result<std::path::PathBuf> {
#[cfg(target_family = "unix")]
use std::os::unix::prelude::*;
#[cfg(target_os = "wasi")]
use std::os::wasi::prelude::*;
let len = PATH_MAX.min(self.max_len);
let mut buf = vec![0; len];
let mut tmp = vec![0; len + 100];
loop {
match realpath_raw_inner(
path.as_ref().as_os_str().as_bytes(),
&mut buf,
&mut tmp,
self.flags,
) {
Ok(len) => {
buf.truncate(len);
return Ok(std::ffi::OsString::from_vec(buf).into());
}
Err(libc::ENAMETOOLONG) if buf.len() < self.max_len => {
let new_len = buf.len().saturating_mul(2).min(self.max_len);
buf.resize(new_len, 0);
tmp.resize(new_len + 100, 0);
}
Err(eno) => return Err(std::io::Error::from_raw_os_error(eno)),
}
}
}
}
#[cfg(feature = "std")]
impl Default for RealpathBuilder {
#[inline]
fn default() -> Self {
Self::new()
}
}
#[cfg(feature = "std")]
pub fn realpath<P: AsRef<std::path::Path>>(
path: P,
flags: RealpathFlags,
) -> std::io::Result<std::path::PathBuf> {
RealpathBuilder::new().flags(flags).realpath(path)
}
pub struct RealpathRawBuilder<'a> {
flags: RealpathFlags,
tmp: Option<&'a mut [u8]>,
}
impl<'a> RealpathRawBuilder<'a> {
#[inline]
pub fn new() -> Self {
Self {
flags: RealpathFlags::empty(),
tmp: None,
}
}
#[inline]
pub fn flags(&mut self, flags: RealpathFlags) -> &mut Self {
self.flags = flags;
self
}
#[inline]
pub fn temp_buffer(&mut self, tmp: Option<&'a mut [u8]>) -> &mut Self {
self.tmp = tmp;
self
}
#[inline]
pub fn realpath_raw(&mut self, path: &[u8], buf: &mut [u8]) -> Result<usize, i32> {
if let Some(tmp) = self.tmp.as_mut() {
realpath_raw_inner(path, buf, tmp, self.flags)
} else {
realpath_raw(path, buf, self.flags)
}
}
}
impl Default for RealpathRawBuilder<'_> {
#[inline]
fn default() -> Self {
Self::new()
}
}
pub fn realpath_raw(path: &[u8], buf: &mut [u8], flags: RealpathFlags) -> Result<usize, i32> {
let mut tmp = [0u8; PATH_MAX + 100];
realpath_raw_inner(path, buf, &mut tmp, flags)
}
fn realpath_raw_inner(
path: &[u8],
buf: &mut [u8],
tmp: &mut [u8],
flags: RealpathFlags,
) -> Result<usize, i32> {
let mut stack = ComponentStack::new(tmp);
let mut path_it = ComponentIter::new(path)?;
let mut buf = SliceVec::empty(buf);
let mut links = SymlinkCounter::new();
while let Some(component) = stack.next().or_else(|| path_it.next()) {
debug_assert_ne!(buf.as_ref(), b".");
if component == b"/" || component == b"//" {
buf.replace(component)?;
} else if component == b".." {
buf.make_parent_path()?;
} else {
let oldlen = buf.len();
if !matches!(buf.as_ref(), b"/" | b"//" | b"") {
buf.push(b'/')?;
}
buf.extend_from_slice(component)?;
buf.push(b'\0')?;
let res = if flags.contains(RealpathFlags::IGNORE_SYMLINKS) {
Err(unsafe { util::readlink_empty(buf.as_ptr()) }
.err()
.unwrap_or(libc::EINVAL))
} else {
unsafe { stack.push_readlink(buf.as_ptr()) }
};
match res {
Ok(()) => {
links.advance()?;
debug_assert!(buf.len() > oldlen);
buf.truncate(oldlen);
}
Err(libc::EINVAL) => {
buf.pop();
}
Err(libc::ENOENT) | Err(libc::EACCES) | Err(libc::ENOTDIR)
if flags.contains(RealpathFlags::ALLOW_MISSING) =>
{
buf.pop();
}
Err(libc::ENOENT)
if flags.contains(RealpathFlags::ALLOW_LAST_MISSING)
&& stack.is_empty()
&& path_it.is_empty() =>
{
buf.pop();
}
Err(eno) => return Err(eno),
}
}
}
fn maybe_check_isdir(path: &[u8], buf: &mut SliceVec, flags: RealpathFlags) -> Result<(), i32> {
if (path.ends_with(b"/") || path.ends_with(b"/."))
&& !flags.contains(RealpathFlags::ALLOW_MISSING)
{
buf.push(b'\0')?;
match unsafe { util::check_isdir(buf.as_ptr()) } {
Ok(()) => (),
Err(libc::ENOENT) if flags.contains(RealpathFlags::ALLOW_LAST_MISSING) => (),
Err(eno) => return Err(eno),
}
buf.pop();
}
Ok(())
}
let mut tmp = SliceVec::empty(stack.clear());
if buf.as_ref() == b"" {
util::getcwd(&mut buf)?;
} else if buf.as_ref() == b".." {
util::getcwd(&mut buf)?;
buf.make_parent_path()?;
} else if buf.starts_with(b"../") {
let mut n = count_leading_dotdot(&buf);
if &buf[(n * 3)..] == b".." {
buf.clear();
n += 1;
} else {
maybe_check_isdir(path, &mut buf, flags)?;
buf.remove_range(0..(n * 3 - 1));
}
util::getcwd(&mut tmp)?;
for _ in 0..n {
tmp.make_parent_path()?;
}
buf.insert_from_slice(0, &tmp)?;
} else if !buf.starts_with(b"/") {
debug_assert!(!buf.starts_with(b"./"));
debug_assert_ne!(buf.as_ref(), b".");
maybe_check_isdir(path, &mut buf, flags)?;
tmp.clear();
util::getcwd(&mut tmp)?;
debug_assert!(tmp.len() > 0);
tmp.push(b'/')?;
buf.insert_from_slice(0, &tmp)?;
} else if !matches!(buf.as_ref(), b"/" | b"//") {
maybe_check_isdir(path, &mut buf, flags)?;
}
Ok(buf.len())
}
fn count_leading_dotdot(mut s: &[u8]) -> usize {
let mut n = 0;
while s.starts_with(b"../") {
n += 1;
s = &s[3..];
}
n
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_count_leading_dotdot() {
assert_eq!(count_leading_dotdot(b""), 0);
assert_eq!(count_leading_dotdot(b".."), 0);
assert_eq!(count_leading_dotdot(b"../a"), 1);
assert_eq!(count_leading_dotdot(b"../../a"), 2);
assert_eq!(count_leading_dotdot(b"../a/../b"), 1);
}
#[test]
fn test_normpath_raw() {
let mut buf = [0; 100];
let n = normpath_raw(b"/", &mut buf).unwrap();
assert_eq!(&buf[..n], b"/");
let n = normpath_raw(b".", &mut buf).unwrap();
assert_eq!(&buf[..n], b".");
let n = normpath_raw(b"a", &mut buf).unwrap();
assert_eq!(&buf[..n], b"a");
let n = normpath_raw(b"a/..", &mut buf).unwrap();
assert_eq!(&buf[..n], b".");
let n = normpath_raw(b"//a/./b/../c/", &mut buf).unwrap();
assert_eq!(&buf[..n], b"//a/c");
assert_eq!(normpath_raw(b"", &mut buf).unwrap_err(), libc::ENOENT);
assert_eq!(normpath_raw(b"\0", &mut buf).unwrap_err(), libc::EINVAL);
}
#[cfg(feature = "std")]
#[test]
fn test_normpath() {
assert_eq!(normpath("/").unwrap().as_os_str(), "/");
assert_eq!(normpath(".").unwrap().as_os_str(), ".");
assert_eq!(normpath("a/..").unwrap().as_os_str(), ".");
assert_eq!(normpath("//a/./b/../c/").unwrap().as_os_str(), "//a/c");
assert_eq!(normpath("").unwrap_err().raw_os_error(), Some(libc::ENOENT));
assert_eq!(
normpath("\0").unwrap_err().raw_os_error(),
Some(libc::EINVAL)
);
}
}