cross_path/
formatter.rs

1use crate::parser::ParsedPath;
2use crate::{PathConfig, PathResult, PathStyle};
3use std::fmt;
4use std::fmt::Write;
5
6/// Path formatter for generating styled path strings
7#[derive(Debug, Clone)]
8pub struct PathFormatter {
9    config: PathConfig,
10}
11
12impl PathFormatter {
13    /// Create new path formatter
14    #[must_use]
15    pub fn new(config: &PathConfig) -> Self {
16        Self {
17            config: config.clone(),
18        }
19    }
20
21    /// Format parsed path with specified style
22    ///
23    /// # Errors
24    ///
25    /// Returns `PathError` if formatting fails (e.g., invalid components).
26    pub fn format(&self, parsed: &ParsedPath, target_style: PathStyle) -> PathResult<String> {
27        match target_style {
28            PathStyle::Windows => Ok(self.format_windows(parsed)),
29            PathStyle::Unix => Ok(self.format_unix(parsed)),
30            PathStyle::Auto => {
31                let current_style = super::platform::current_style();
32                self.format(parsed, current_style)
33            }
34        }
35    }
36
37    /// Format as Windows path
38    fn format_windows(&self, parsed: &ParsedPath) -> String {
39        if parsed.is_unc {
40            return Self::format_unc_windows(parsed);
41        }
42
43        let mut result = String::new();
44
45        // Add drive letter
46        if let Some(drive) = parsed.drive_letter {
47            let _ = write!(result, "{drive}:");
48        } else if parsed.is_absolute {
49            // Default drive
50            result.push_str("C:");
51        }
52
53        // Add separator
54        if parsed.is_absolute && !parsed.is_unc {
55            result.push('\\');
56        }
57
58        // Add components
59        for (i, component) in parsed.components.iter().enumerate() {
60            if i > 0 {
61                result.push('\\');
62            }
63            result.push_str(component);
64        }
65
66        // Normalize if requested
67        if self.config.normalize {
68            result = Self::normalize_windows_path(&result);
69        }
70
71        result
72    }
73
74    /// Format as Unix path
75    fn format_unix(&self, parsed: &ParsedPath) -> String {
76        if parsed.is_unc {
77            return Self::format_unc_unix(parsed);
78        }
79
80        let mut result = String::new();
81
82        // UNC path handling
83        if parsed.is_unc {
84            if let (Some(server), Some(share)) = (&parsed.server, &parsed.share) {
85                let _ = write!(result, "//{server}/{share}");
86            }
87        } else if parsed.is_absolute {
88            if parsed.has_drive {
89                // Map drive letter to Unix mount point
90                if let Some(drive) = parsed.drive_letter {
91                    let drive_lower = drive.to_ascii_lowercase();
92                    result.push_str(&self.map_drive_to_unix(&format!("{drive_lower}:")));
93                }
94            } else {
95                result.push('/');
96            }
97        }
98
99        // Add components
100        for component in &parsed.components {
101            if !result.ends_with('/') && !result.is_empty() {
102                result.push('/');
103            }
104            result.push_str(component);
105        }
106
107        // Normalize if requested
108        if self.config.normalize {
109            result = Self::normalize_unix_path(&result);
110        }
111
112        result
113    }
114
115    /// Format UNC path as Windows format
116    fn format_unc_windows(parsed: &ParsedPath) -> String {
117        let mut result = String::from(r"\\");
118
119        if let Some(server) = &parsed.server {
120            result.push_str(server);
121        }
122
123        result.push('\\');
124
125        if let Some(share) = &parsed.share {
126            result.push_str(share);
127        }
128
129        for component in &parsed.components {
130            result.push('\\');
131            result.push_str(component);
132        }
133
134        result
135    }
136
137    /// Format UNC path as Unix format
138    fn format_unc_unix(parsed: &ParsedPath) -> String {
139        let mut result = String::from("//");
140
141        if let Some(server) = &parsed.server {
142            result.push_str(server);
143        }
144
145        result.push('/');
146
147        if let Some(share) = &parsed.share {
148            result.push_str(share);
149        }
150
151        for component in &parsed.components {
152            result.push('/');
153            result.push_str(component);
154        }
155
156        result
157    }
158
159    /// Map Windows drive letter to Unix path
160    fn map_drive_to_unix(&self, drive: &str) -> String {
161        for (windows_drive, unix_mount) in &self.config.drive_mappings {
162            if windows_drive == drive {
163                return unix_mount.clone();
164            }
165        }
166
167        // Default mapping
168        let drive_letter = drive.chars().next().unwrap().to_ascii_lowercase();
169        format!("/mnt/{drive_letter}")
170    }
171
172    /// Normalize Windows path string
173    fn normalize_windows_path(path: &str) -> String {
174        let mut result = path.to_string();
175
176        // Unify separators
177        result = result.replace('/', "\\");
178
179        // Remove duplicate separators
180        while result.contains("\\\\") && !result.starts_with(r"\\") {
181            result = result.replace("\\\\", "\\");
182        }
183
184        // Remove trailing separator (unless root path)
185        if result.ends_with('\\') && result.len() > 3 && !result.starts_with(r"\\") {
186            result.pop();
187        }
188
189        result
190    }
191
192    /// Normalize Unix path string
193    fn normalize_unix_path(path: &str) -> String {
194        let mut result = path.to_string();
195
196        // Unify separators
197        result = result.replace('\\', "/");
198
199        // Remove duplicate separators
200        while result.contains("//") && !result.starts_with("//") {
201            result = result.replace("//", "/");
202        }
203
204        // Remove trailing separator (unless root path)
205        if result.ends_with('/') && result != "/" && !result.starts_with("//") {
206            result.pop();
207        }
208
209        result
210    }
211}
212
213impl fmt::Display for PathFormatter {
214    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
215        write!(f, "PathFormatter(config: {:?})", self.config)
216    }
217}