use crate::error::{Result, VfsError};
pub fn normalize(path: &str) -> Result<String> {
if path.is_empty() {
return Ok("/".to_string());
}
let path = if path.starts_with('/') {
path.to_string()
} else {
format!("/{}", path)
};
let mut components: Vec<&str> = Vec::new();
for part in path.split('/') {
match part {
"" | "." => continue,
".." => {
if components.is_empty() {
return Err(VfsError::InvalidPath(
"path escapes root directory".to_string(),
));
}
components.pop();
}
_ => {
validate_component(part)?;
components.push(part);
}
}
}
if components.is_empty() {
Ok("/".to_string())
} else {
Ok(format!("/{}", components.join("/")))
}
}
fn validate_component(name: &str) -> Result<()> {
if name.is_empty() {
return Err(VfsError::InvalidPath("empty component".to_string()));
}
if name.len() > 255 {
return Err(VfsError::InvalidPath(format!(
"component too long: {} chars (max 255)",
name.len()
)));
}
for c in name.chars() {
if c == '\0' || c == '/' {
return Err(VfsError::InvalidPath(format!(
"invalid character in path: {:?}",
c
)));
}
}
Ok(())
}
pub fn split(path: &str) -> Result<(Option<String>, String)> {
let normalized = normalize(path)?;
if normalized == "/" {
return Ok((None, String::new()));
}
match normalized.rfind('/') {
Some(0) => {
Ok((Some("/".to_string()), normalized[1..].to_string()))
}
Some(pos) => {
let parent = normalized[..pos].to_string();
let name = normalized[pos + 1..].to_string();
Ok((Some(parent), name))
}
None => {
Err(VfsError::InvalidPath(normalized))
}
}
}
pub fn join(parent: &str, name: &str) -> Result<String> {
if name.contains('/') {
return Err(VfsError::InvalidPath(
"child name cannot contain /".to_string(),
));
}
let parent = normalize(parent)?;
if parent == "/" {
normalize(&format!("/{}", name))
} else {
normalize(&format!("{}/{}", parent, name))
}
}
pub fn components(path: &str) -> Result<Vec<String>> {
let normalized = normalize(path)?;
if normalized == "/" {
return Ok(Vec::new());
}
Ok(normalized[1..]
.split('/')
.map(|s| s.to_string())
.collect())
}
pub fn filename(path: &str) -> Result<String> {
let (_, name) = split(path)?;
Ok(name)
}
pub fn parent(path: &str) -> Result<Option<String>> {
let (parent, _) = split(path)?;
Ok(parent)
}
pub fn is_root(path: &str) -> bool {
matches!(normalize(path), Ok(p) if p == "/")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_normalize() {
assert_eq!(normalize("/").unwrap(), "/");
assert_eq!(normalize("").unwrap(), "/");
assert_eq!(normalize("/a/b/c").unwrap(), "/a/b/c");
assert_eq!(normalize("a/b/c").unwrap(), "/a/b/c");
assert_eq!(normalize("/a//b///c").unwrap(), "/a/b/c");
assert_eq!(normalize("/a/b/c/").unwrap(), "/a/b/c");
assert_eq!(normalize("/a/./b/c").unwrap(), "/a/b/c");
assert_eq!(normalize("/a/b/../c").unwrap(), "/a/c");
assert_eq!(normalize("/a/b/c/..").unwrap(), "/a/b");
}
#[test]
fn test_normalize_errors() {
assert!(normalize("/..").is_err());
assert!(normalize("/a/../..").is_err());
}
#[test]
fn test_split() {
assert_eq!(split("/").unwrap(), (None, "".to_string()));
assert_eq!(split("/a").unwrap(), (Some("/".to_string()), "a".to_string()));
assert_eq!(
split("/a/b").unwrap(),
(Some("/a".to_string()), "b".to_string())
);
assert_eq!(
split("/a/b/c").unwrap(),
(Some("/a/b".to_string()), "c".to_string())
);
}
#[test]
fn test_join() {
assert_eq!(join("/", "a").unwrap(), "/a");
assert_eq!(join("/a", "b").unwrap(), "/a/b");
assert_eq!(join("/a/b", "c").unwrap(), "/a/b/c");
}
#[test]
fn test_components() {
assert_eq!(components("/").unwrap(), Vec::<String>::new());
assert_eq!(components("/a").unwrap(), vec!["a"]);
assert_eq!(components("/a/b/c").unwrap(), vec!["a", "b", "c"]);
}
}