sugar_path 3.0.0

Host-native lexical path manipulation with borrowed and owned-buffer APIs
Documentation
use std::path::{Path, PathBuf};

use sugar_path::{SugarPath, SugarPathBuf};

fn buffer_identity(path: &PathBuf) -> (*const u8, usize) {
  (path.as_os_str().as_encoded_bytes().as_ptr(), path.capacity())
}

fn owned_path_with_capacity(path: &str) -> PathBuf {
  let mut owned = PathBuf::with_capacity(256);
  owned.push(path);
  owned
}

#[test]
fn into_normalized_reuses_clean_owned_buffers() {
  #[cfg(target_family = "unix")]
  let cases = [
    "/workspace/rolldown/crates/rolldown/src/module_loader/module_task.rs",
    "../../chunks/shared.js",
  ];
  #[cfg(target_family = "windows")]
  let cases = [
    r"C:\workspace\rolldown\crates\rolldown\src\module_loader\module_task.rs",
    r"..\..\chunks\shared.js",
  ];

  for input in cases {
    let path = owned_path_with_capacity(input);
    let identity = buffer_identity(&path);
    let normalized = path.into_normalized();

    assert_eq!(normalized.as_os_str(), Path::new(input).as_os_str());
    assert_eq!(buffer_identity(&normalized), identity);
  }
}

#[test]
fn into_normalized_reuses_current_directory_buffers_without_confusing_borrowed_dot() {
  #[cfg(target_family = "unix")]
  let cases = [("", "."), (".", "."), ("./", "./"), ("foo/..", ".")];
  #[cfg(target_family = "windows")]
  let cases = [("", "."), (".", "."), (r".\", r".\"), (r"foo\..", ".")];

  for (input, expected) in cases {
    let path = owned_path_with_capacity(input);
    let identity = buffer_identity(&path);
    let normalized = path.into_normalized();

    assert_eq!(normalized.as_os_str(), Path::new(expected).as_os_str(), "input {input:?}");
    assert_eq!(buffer_identity(&normalized), identity, "input {input:?}");
  }
}

#[test]
fn into_normalized_matches_borrowed_api_for_dirty_paths() {
  #[cfg(target_family = "unix")]
  let cases = ["foo/./bar/../baz", "../../foo/../bar", "foo//bar/"];
  #[cfg(target_family = "windows")]
  let cases = [r"foo\.\bar\..\baz", r"..\..\foo\..\bar", r"foo\\bar\"];

  for input in cases {
    let path = owned_path_with_capacity(input);
    let expected = path.normalize().into_owned();
    assert_eq!(path.into_normalized().as_os_str(), expected.as_os_str(), "input {input:?}");
  }
}

#[test]
fn owned_slash_apis_reuse_valid_unicode_buffers() {
  #[cfg(target_family = "unix")]
  let (input, expected) = (
    "/workspace/rolldown/crates/rolldown/src/module_loader/module_task.rs",
    "/workspace/rolldown/crates/rolldown/src/module_loader/module_task.rs",
  );
  #[cfg(target_family = "windows")]
  let (input, expected) = (
    r"C:\workspace\rolldown\crates\rolldown\src\module_loader\module_task.rs",
    "C:/workspace/rolldown/crates/rolldown/src/module_loader/module_task.rs",
  );

  let path = owned_path_with_capacity(input);
  let identity = buffer_identity(&path);
  let slash = path.into_slash();

  assert_eq!(slash, expected);
  assert_eq!((slash.as_ptr(), slash.capacity()), identity);

  let path = owned_path_with_capacity(input);
  let identity = buffer_identity(&path);
  let slash = path.try_into_slash().expect("the fixture is valid Unicode");
  assert_eq!(slash, expected);
  assert_eq!((slash.as_ptr(), slash.capacity()), identity);
}

#[cfg(target_family = "unix")]
#[test]
fn owned_apis_preserve_and_replace_invalid_unix_encoding() {
  use std::{
    ffi::OsString,
    os::unix::ffi::{OsStrExt, OsStringExt},
  };

  let input = PathBuf::from(OsString::from_vec(b"dir/invalid-\x80/./file".to_vec()));
  let normalized = input.clone().into_normalized();
  assert_eq!(normalized.as_os_str().as_bytes(), b"dir/invalid-\x80/file");
  let returned =
    input.clone().try_into_slash().expect_err("invalid Unix encoding must be returned unchanged");
  assert_eq!(returned.as_os_str().as_bytes(), input.as_os_str().as_bytes());
  assert_eq!(input.into_slash_lossy(), "dir/invalid-\u{fffd}/./file");
}

#[cfg(target_family = "windows")]
#[test]
fn owned_apis_preserve_and_replace_invalid_windows_encoding() {
  use std::{
    ffi::OsString,
    os::windows::ffi::{OsStrExt, OsStringExt},
  };

  let mut input_wide: Vec<u16> = r"C:\workspace\invalid-".encode_utf16().collect();
  input_wide.push(0xd800);
  input_wide.extend(r"\.\file".encode_utf16());
  let input = PathBuf::from(OsString::from_wide(&input_wide));

  let mut expected_wide: Vec<u16> = r"C:\workspace\invalid-".encode_utf16().collect();
  expected_wide.push(0xd800);
  expected_wide.extend(r"\file".encode_utf16());
  assert_eq!(
    input.clone().into_normalized().as_os_str().encode_wide().collect::<Vec<_>>(),
    expected_wide,
  );
  let returned = input
    .clone()
    .try_into_slash()
    .expect_err("invalid Windows encoding must be returned unchanged");
  assert_eq!(returned.as_os_str().encode_wide().collect::<Vec<_>>(), input_wide);
  assert_eq!(input.into_slash_lossy(), "C:/workspace/invalid-\u{fffd}/./file");
}