use crate::platform::{self, PathStyle};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum SegmentPriority {
Sacred = 0,
Identity = 1,
Context = 2,
Expendable = 3,
}
#[derive(Debug, Clone)]
pub struct Segment {
pub text: String,
pub priority: SegmentPriority,
}
#[derive(Debug, Clone)]
pub struct PathInfo {
pub prefix: String,
pub segments: Vec<Segment>,
pub filename: String,
pub style: PathStyle,
}
impl PathInfo {
pub fn parse(path: &str, force_style: Option<PathStyle>) -> Self {
let style = force_style.unwrap_or_else(|| platform::detect_style(path));
let sep = platform::separator(style);
let normalized: String = match style {
PathStyle::Unix => path.replace('\\', "/"),
PathStyle::Windows => path.replace('/', "\\"),
};
let (prefix, remainder) = extract_prefix(&normalized, style);
let parts: Vec<&str> = remainder.split(sep).filter(|s| !s.is_empty()).collect();
if parts.is_empty() {
return PathInfo {
prefix,
segments: Vec::new(),
filename: String::new(),
style,
};
}
let filename = parts.last().unwrap().to_string();
let dir_parts = &parts[..parts.len() - 1];
let segments = classify_segments(dir_parts, &prefix, style);
PathInfo {
prefix,
segments,
filename,
style,
}
}
pub fn reassemble(&self, segment_texts: &[&str]) -> String {
let sep = platform::separator(self.style);
let mut result = self.prefix.clone();
for (i, text) in segment_texts.iter().enumerate() {
if i > 0 || (!result.is_empty() && !result.ends_with(sep)) {
result.push(sep);
}
result.push_str(text);
}
if !self.filename.is_empty() {
if !result.is_empty() && !result.ends_with(sep) {
result.push(sep);
}
result.push_str(&self.filename);
}
result
}
}
fn extract_prefix(path: &str, style: PathStyle) -> (String, &str) {
match style {
PathStyle::Windows => {
let bytes = path.as_bytes();
if bytes.starts_with(b"\\\\") {
let after_slashes = &path[2..];
if let Some(server_end) = after_slashes.find('\\') {
let after_server = &after_slashes[server_end + 1..];
let share_end = after_server.find('\\').unwrap_or(after_server.len());
let prefix_end = 2 + server_end + 1 + share_end;
let prefix = &path[..prefix_end];
let remainder = if prefix_end < path.len() {
&path[prefix_end + 1..]
} else {
""
};
return (prefix.to_string(), remainder);
}
return (path.to_string(), "");
}
if bytes.len() >= 3
&& bytes[0].is_ascii_alphabetic()
&& bytes[1] == b':'
&& bytes[2] == b'\\'
{
return (path[..3].to_string(), &path[3..]);
}
if bytes.starts_with(b".\\") {
return (".".to_string(), &path[2..]);
}
if bytes.starts_with(b"\\") {
return ("\\".to_string(), &path[1..]);
}
(String::new(), path)
}
PathStyle::Unix => {
let bytes = path.as_bytes();
if bytes.starts_with(b"/") {
return ("/".to_string(), &path[1..]);
}
if bytes.starts_with(b"~/") {
return ("~".to_string(), &path[2..]);
}
if path == "~" {
return ("~".to_string(), "");
}
(String::new(), path)
}
}
}
fn classify_segments(parts: &[&str], prefix: &str, style: PathStyle) -> Vec<Segment> {
let home_roots = match style {
PathStyle::Unix => platform::UNIX_HOME_ROOTS,
PathStyle::Windows => platform::WIN_HOME_ROOTS,
};
let identity_idx = parts.iter().enumerate().find_map(|(i, &part)| {
if i == 0
&& home_roots
.iter()
.any(|&root| part.eq_ignore_ascii_case(root))
{
if i + 1 < parts.len() {
return Some(i + 1);
}
}
None
});
parts
.iter()
.enumerate()
.map(|(i, &text)| {
let priority = if Some(i) == identity_idx {
SegmentPriority::Identity
} else if i == 0
&& home_roots
.iter()
.any(|&root| text.eq_ignore_ascii_case(root))
{
SegmentPriority::Context
} else if is_well_known_prefix(prefix) && i == 0 {
SegmentPriority::Context
} else {
SegmentPriority::Expendable
};
Segment {
text: text.to_string(),
priority,
}
})
.collect()
}
fn is_well_known_prefix(_prefix: &str) -> bool {
false }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_unix_home() {
let info = PathInfo::parse("/home/john/projects/rust/file.rs", None);
assert_eq!(info.prefix, "/");
assert_eq!(info.filename, "file.rs");
assert_eq!(info.segments.len(), 4);
assert_eq!(info.segments[0].text, "home");
assert_eq!(info.segments[0].priority, SegmentPriority::Context);
assert_eq!(info.segments[1].text, "john");
assert_eq!(info.segments[1].priority, SegmentPriority::Identity);
assert_eq!(info.segments[2].priority, SegmentPriority::Expendable);
}
#[test]
fn parse_windows_drive() {
let info = PathInfo::parse("C:\\Users\\Admin\\AppData\\Local\\file.txt", None);
assert_eq!(info.prefix, "C:\\");
assert_eq!(info.filename, "file.txt");
assert_eq!(info.segments[0].text, "Users");
assert_eq!(info.segments[0].priority, SegmentPriority::Context);
assert_eq!(info.segments[1].text, "Admin");
assert_eq!(info.segments[1].priority, SegmentPriority::Identity);
}
#[test]
fn parse_unc() {
let info = PathInfo::parse("\\\\server\\share\\dept\\file.xlsx", None);
assert_eq!(info.prefix, "\\\\server\\share");
assert_eq!(info.filename, "file.xlsx");
assert_eq!(info.segments.len(), 1);
assert_eq!(info.segments[0].text, "dept");
}
#[test]
fn parse_tilde() {
let info = PathInfo::parse("~/projects/rust/file.rs", None);
assert_eq!(info.prefix, "~");
assert_eq!(info.filename, "file.rs");
assert_eq!(info.segments.len(), 2);
}
#[test]
fn parse_dot_backslash() {
let info = PathInfo::parse(".\\Users\\Admin\\file.txt", None);
assert_eq!(info.prefix, ".");
assert_eq!(info.style, PathStyle::Windows);
assert_eq!(info.filename, "file.txt");
}
#[test]
fn parse_empty() {
let info = PathInfo::parse("", None);
assert_eq!(info.prefix, "");
assert_eq!(info.filename, "");
assert!(info.segments.is_empty());
}
#[test]
fn parse_filename_only() {
let info = PathInfo::parse("file.txt", None);
assert_eq!(info.filename, "file.txt");
assert!(info.segments.is_empty());
}
#[test]
fn reassemble() {
let info = PathInfo::parse("/home/john/projects/file.rs", None);
let texts: Vec<&str> = info.segments.iter().map(|s| s.text.as_str()).collect();
let result = info.reassemble(&texts);
assert_eq!(result, "/home/john/projects/file.rs");
}
#[test]
fn reassemble_windows() {
let info = PathInfo::parse("C:\\Users\\Admin\\Docs\\file.txt", None);
let texts: Vec<&str> = info.segments.iter().map(|s| s.text.as_str()).collect();
let result = info.reassemble(&texts);
assert_eq!(result, "C:\\Users\\Admin\\Docs\\file.txt");
}
}