nu_path/
components.rs

1//! A wrapper around `Path::components()` that preserves trailing slashes.
2//!
3//! Trailing slashes are semantically important for us. For example, POSIX says
4//! that path resolution should always follow the final symlink if it has
5//! trailing slashes. Here's a demonstration:
6//!
7//! ```sh
8//! mkdir foo
9//! ln -s foo link
10//!
11//! cp -r link bar      # This copies the symlink, so bar is now a symlink to foo
12//! cp -r link/ baz     # This copies the directory, so baz is now a directory
13//! ```
14//!
15//! However, `Path::components()` normalizes trailing slashes away, and so does
16//! other APIs that uses `Path::components()` under the hood, such as
17//! `Path::parent()`. This is not ideal for path manipulation.
18//!
19//! This module provides a wrapper around `Path::components()` that produces an
20//! empty component when there's a trailing slash.
21//!
22//! You can reconstruct a path with a trailing slash by concatenating the
23//! components returned by this function using `PathBuf::push()` or
24//! `Path::join()`. It works because `PathBuf::push("")` will add a trailing
25//! slash when the original path doesn't have one.
26
27use std::{
28    ffi::OsStr,
29    path::{Component, Path},
30};
31
32use crate::trailing_slash::has_trailing_slash;
33
34/// Like `Path::components()`, but produces an extra empty component at the end
35/// when `path` contains a trailing slash.
36///
37/// Example:
38///
39/// ```
40/// # use std::path::{Path, Component};
41/// # use std::ffi::OsStr;
42/// use nu_path::components;
43///
44/// let path = Path::new("/foo/bar/");
45/// let mut components = components(path);
46///
47/// assert_eq!(components.next(), Some(Component::RootDir));
48/// assert_eq!(components.next(), Some(Component::Normal(OsStr::new("foo"))));
49/// assert_eq!(components.next(), Some(Component::Normal(OsStr::new("bar"))));
50/// assert_eq!(components.next(), Some(Component::Normal(OsStr::new(""))));
51/// assert_eq!(components.next(), None);
52/// ```
53pub fn components(path: &Path) -> impl Iterator<Item = Component> {
54    let mut final_component = Some(Component::Normal(OsStr::new("")));
55    path.components().chain(std::iter::from_fn(move || {
56        if has_trailing_slash(path) {
57            final_component.take()
58        } else {
59            None
60        }
61    }))
62}
63
64#[cfg(test)]
65mod test {
66    //! We'll go through every variant of Component, with or without trailing
67    //! slashes. Then we'll try reconstructing the path on some typical use cases.
68
69    use crate::assert_path_eq;
70    use std::{
71        ffi::OsStr,
72        path::{Component, Path, PathBuf},
73    };
74
75    #[test]
76    fn empty_path() {
77        let path = Path::new("");
78        let mut components = crate::components(path);
79
80        assert_eq!(components.next(), None);
81    }
82
83    #[test]
84    #[cfg(windows)]
85    fn prefix_only() {
86        let path = Path::new("C:");
87        let mut components = crate::components(path);
88
89        assert!(matches!(components.next(), Some(Component::Prefix(_))));
90        assert_eq!(components.next(), None);
91    }
92
93    #[test]
94    #[cfg(windows)]
95    fn prefix_with_trailing_slash() {
96        let path = Path::new("C:\\");
97        let mut components = crate::components(path);
98
99        assert!(matches!(components.next(), Some(Component::Prefix(_))));
100        assert!(matches!(components.next(), Some(Component::RootDir)));
101        assert_eq!(components.next(), Some(Component::Normal(OsStr::new(""))));
102        assert_eq!(components.next(), None);
103    }
104
105    #[test]
106    fn root() {
107        let path = Path::new("/");
108        let mut components = crate::components(path);
109
110        assert!(matches!(components.next(), Some(Component::RootDir)));
111        assert_eq!(components.next(), Some(Component::Normal(OsStr::new(""))));
112        assert_eq!(components.next(), None);
113    }
114
115    #[test]
116    fn cur_dir_only() {
117        let path = Path::new(".");
118        let mut components = crate::components(path);
119
120        assert!(matches!(components.next(), Some(Component::CurDir)));
121        assert_eq!(components.next(), None);
122    }
123
124    #[test]
125    fn cur_dir_with_trailing_slash() {
126        let path = Path::new("./");
127        let mut components = crate::components(path);
128
129        assert!(matches!(components.next(), Some(Component::CurDir)));
130        assert_eq!(components.next(), Some(Component::Normal(OsStr::new(""))));
131        assert_eq!(components.next(), None);
132    }
133
134    #[test]
135    fn parent_dir_only() {
136        let path = Path::new("..");
137        let mut components = crate::components(path);
138
139        assert!(matches!(components.next(), Some(Component::ParentDir)));
140        assert_eq!(components.next(), None);
141    }
142
143    #[test]
144    fn parent_dir_with_trailing_slash() {
145        let path = Path::new("../");
146        let mut components = crate::components(path);
147
148        assert!(matches!(components.next(), Some(Component::ParentDir)));
149        assert_eq!(components.next(), Some(Component::Normal(OsStr::new(""))));
150        assert_eq!(components.next(), None);
151    }
152
153    #[test]
154    fn normal_only() {
155        let path = Path::new("foo");
156        let mut components = crate::components(path);
157
158        assert_eq!(
159            components.next(),
160            Some(Component::Normal(OsStr::new("foo")))
161        );
162        assert_eq!(components.next(), None);
163    }
164
165    #[test]
166    fn normal_with_trailing_slash() {
167        let path = Path::new("foo/");
168        let mut components = crate::components(path);
169
170        assert_eq!(
171            components.next(),
172            Some(Component::Normal(OsStr::new("foo")))
173        );
174        assert_eq!(components.next(), Some(Component::Normal(OsStr::new(""))));
175        assert_eq!(components.next(), None);
176    }
177
178    #[test]
179    #[cfg(not(windows))]
180    fn reconstruct_unix_only() {
181        let path = Path::new("/home/Alice");
182
183        let mut buf = PathBuf::new();
184        for component in crate::components(path) {
185            buf.push(component);
186        }
187
188        assert_path_eq!(path, buf);
189    }
190
191    #[test]
192    #[cfg(not(windows))]
193    fn reconstruct_unix_with_trailing_slash() {
194        let path = Path::new("/home/Alice/");
195
196        let mut buf = PathBuf::new();
197        for component in crate::components(path) {
198            buf.push(component);
199        }
200
201        assert_path_eq!(path, buf);
202    }
203
204    #[test]
205    #[cfg(windows)]
206    fn reconstruct_windows_only() {
207        let path = Path::new("C:\\WINDOWS\\System32");
208
209        let mut buf = PathBuf::new();
210        for component in crate::components(path) {
211            buf.push(component);
212        }
213
214        assert_path_eq!(path, buf);
215    }
216
217    #[test]
218    #[cfg(windows)]
219    fn reconstruct_windows_with_trailing_slash() {
220        let path = Path::new("C:\\WINDOWS\\System32\\");
221
222        let mut buf = PathBuf::new();
223        for component in crate::components(path) {
224            buf.push(component);
225        }
226
227        assert_path_eq!(path, buf);
228    }
229}