pub const MEMBER_SEP: char = '/';
pub const DECOMPOSE_SEP: &str = "/>";
pub fn escape_segment(name: &str) -> String {
if name.starts_with(['>', '\\']) {
format!("\\{name}")
} else {
name.to_string()
}
}
pub fn member_child(parent: &str, name: &str) -> String {
let name = escape_segment(name);
if parent.is_empty() {
name
} else {
format!("{parent}{MEMBER_SEP}{name}")
}
}
pub fn decompose_child(parent: &str, name: &str) -> String {
let name = escape_segment(name);
format!("{parent}{DECOMPOSE_SEP}{name}")
}
pub fn file_name(path: &str) -> &str {
match path.rfind(MEMBER_SEP) {
Some(slash) if path[slash + 1..].starts_with('>') => &path[slash + 2..],
Some(slash) => &path[slash + 1..],
None => path,
}
}
pub fn segments(path: &str) -> Vec<(&str, &str)> {
let bytes = path.as_bytes();
let mut out = Vec::new();
let mut seg_start = 0;
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b'/' {
if i > seg_start {
out.push((&path[..i], &path[seg_start..i]));
}
if bytes.get(i + 1) == Some(&b'>') {
i += 2;
} else {
i += 1;
}
seg_start = i;
} else {
i += 1;
}
}
if seg_start < bytes.len() {
out.push((path, &path[seg_start..]));
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn member_and_decompose_construction() {
assert_eq!(member_child("", "a.txt"), "a.txt");
assert_eq!(member_child("dir", "a.txt"), "dir/a.txt");
assert_eq!(decompose_child("data.zip", "inner"), "data.zip/>inner");
assert_eq!(decompose_child("data.csv", "table_1"), "data.csv/>table_1");
}
#[test]
fn segment_escape_disambiguates_leading_decompose_marker() {
assert_eq!(escape_segment(">q1.csv"), r"\>q1.csv");
assert_eq!(escape_segment(r"\>q1.csv"), r"\\>q1.csv");
assert_eq!(member_child("dir", ">q1.csv"), r"dir/\>q1.csv");
assert_eq!(member_child("dir", r"\>q1.csv"), r"dir/\\>q1.csv");
assert_eq!(
decompose_child("book.xlsx", ">Summary"),
r"book.xlsx/>\>Summary"
);
}
#[test]
fn file_name_handles_both_separators() {
assert_eq!(file_name("a.txt"), "a.txt");
assert_eq!(file_name("dir/a.txt"), "a.txt");
assert_eq!(file_name("data.csv/>table_1"), "table_1");
assert_eq!(file_name(r"dir/\>q1.csv"), r"\>q1.csv");
assert_eq!(file_name("dir/data.zip/>reports/q1.csv"), "q1.csv");
assert_eq!(
file_name("dir/data.zip/>reports/q1.csv/>table_2"),
"table_2"
);
}
#[test]
fn segments_preserve_separators_in_cumulative_paths() {
assert_eq!(segments("a.txt"), vec![("a.txt", "a.txt")]);
assert_eq!(
segments("dir/a.txt"),
vec![("dir", "dir"), ("dir/a.txt", "a.txt")]
);
assert_eq!(
segments("data.csv/>table_1"),
vec![("data.csv", "data.csv"), ("data.csv/>table_1", "table_1")]
);
assert_eq!(
segments(r"dir/\>q1.csv"),
vec![("dir", "dir"), (r"dir/\>q1.csv", r"\>q1.csv")]
);
assert_eq!(
segments("dir/data.zip/>reports/q1.csv/>table_2"),
vec![
("dir", "dir"),
("dir/data.zip", "data.zip"),
("dir/data.zip/>reports", "reports"),
("dir/data.zip/>reports/q1.csv", "q1.csv"),
("dir/data.zip/>reports/q1.csv/>table_2", "table_2"),
]
);
}
#[test]
fn segments_skip_empty() {
assert_eq!(segments(""), Vec::<(&str, &str)>::new());
assert_eq!(segments("/"), Vec::<(&str, &str)>::new());
assert_eq!(segments("a//b"), vec![("a", "a"), ("a//b", "b")]);
}
}