1use std::path::{Path, PathBuf};
7
8pub struct FindIter {
11 current: Option<PathBuf>,
12 file_name: String,
13}
14
15impl Iterator for FindIter {
16 type Item = PathBuf;
17
18 fn next(&mut self) -> Option<PathBuf> {
19 while let Some(ref dir) = self.current {
20 let candidate = dir.join(&self.file_name);
21 if candidate.is_file() {
22 let result = candidate;
23 self.current = dir.parent().map(PathBuf::from);
24 return Some(result);
25 }
26 self.current = dir.parent().map(PathBuf::from);
27 }
28 None
29 }
30}
31
32pub fn find<P, Q>(input: P, base: Option<Q>, file_name: Option<&str>) -> FindIter
58where
59 P: AsRef<Path>,
60 Q: AsRef<Path>,
61{
62 let file_name = file_name.unwrap_or("Cargo.toml").to_string();
63 let base: PathBuf = base
64 .map(|b| b.as_ref().to_path_buf())
65 .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
66 let start: PathBuf = if input.as_ref().is_absolute() {
67 input.as_ref().to_path_buf()
68 } else {
69 base.join(input.as_ref())
70 };
71 let start_normalized = normalize_path(&start);
72 let start_dir = if start_normalized.is_file() {
73 start_normalized
74 .parent()
75 .map(PathBuf::from)
76 .unwrap_or(start_normalized)
77 } else {
78 start_normalized
79 };
80
81 FindIter {
82 current: Some(start_dir),
83 file_name,
84 }
85}
86
87pub fn find_from_current_dir<P>(input: P, file_name: Option<&str>) -> FindIter
90where
91 P: AsRef<Path>,
92{
93 find(input, None::<PathBuf>, file_name)
94}
95
96fn normalize_path(path: &Path) -> PathBuf {
98 path.components().collect()
99}
100
101#[cfg(test)]
102mod tests {
103 use super::*;
104 use std::fs;
105 use std::io::Write;
106
107 #[test]
108 fn find_yields_nothing_when_no_cargo_toml() {
109 let tmp = std::env::temp_dir().join("find_cargo_toml_test_empty");
110 let _ = fs::create_dir_all(&tmp);
111 let count = find(&tmp, None::<PathBuf>, None).count();
112 let _ = fs::remove_dir_all(&tmp);
113 assert_eq!(count, 0);
114 }
115
116 #[test]
117 fn find_yields_path_when_cargo_toml_in_dir() {
118 let tmp = std::env::temp_dir().join("find_cargo_toml_test_with");
119 let _ = fs::create_dir_all(&tmp);
120 let manifest = tmp.join("Cargo.toml");
121 let _ = fs::File::create(&manifest).and_then(|mut f| f.write_all(b"[package]"));
122 let collected: Vec<_> = find(&tmp, None::<PathBuf>, None).collect();
123 let _ = fs::remove_file(manifest);
124 let _ = fs::remove_dir_all(&tmp);
125 assert_eq!(collected.len(), 1);
126 assert!(collected[0].ends_with("Cargo.toml"));
127 }
128
129 #[test]
130 fn find_respects_custom_file_name() {
131 let tmp = std::env::temp_dir().join("find_cargo_toml_test_custom");
132 let _ = fs::create_dir_all(&tmp);
133 let custom = tmp.join("MyManifest.toml");
134 let _ = fs::File::create(&custom).and_then(|mut f| f.write_all(b"[package]"));
135 let collected: Vec<_> = find(&tmp, None::<PathBuf>, Some("MyManifest.toml")).collect();
136 let _ = fs::remove_file(custom);
137 let _ = fs::remove_dir_all(&tmp);
138 assert_eq!(collected.len(), 1);
139 assert!(collected[0].ends_with("MyManifest.toml"));
140 }
141
142 #[test]
143 fn find_with_absolute_input() {
144 let tmp = std::env::temp_dir().join("find_cargo_toml_test_absolute");
145 let _ = fs::create_dir_all(&tmp);
146 let manifest = tmp.join("Cargo.toml");
147 let _ = fs::File::create(&manifest).and_then(|mut f| f.write_all(b"[package]"));
148 let abs = tmp.canonicalize().unwrap();
149 let collected: Vec<_> = find(&abs, None::<PathBuf>, None).collect();
150 let _ = fs::remove_file(manifest);
151 let _ = fs::remove_dir_all(&tmp);
152 assert_eq!(collected.len(), 1);
153 assert!(collected[0].ends_with("Cargo.toml"));
154 }
155
156 #[test]
157 fn find_when_input_is_file_uses_parent_dir() {
158 let tmp = std::env::temp_dir().join("find_cargo_toml_test_file_input");
159 let _ = fs::create_dir_all(&tmp);
160 let manifest = tmp.join("Cargo.toml");
161 let _ = fs::File::create(&manifest).and_then(|mut f| f.write_all(b"[package]"));
162 let some_file = tmp.join("foo.rs");
163 let _ = fs::File::create(&some_file);
164 let collected: Vec<_> = find(&some_file, None::<PathBuf>, None).collect();
165 let _ = fs::remove_file(some_file);
166 let _ = fs::remove_file(manifest);
167 let _ = fs::remove_dir_all(&tmp);
168 assert_eq!(collected.len(), 1);
169 assert!(collected[0].ends_with("Cargo.toml"));
170 }
171
172 #[test]
173 fn find_from_current_dir_delegates_to_find() {
174 let count = find_from_current_dir(".", None).count();
175 assert!(count >= 1, "project root has Cargo.toml");
176 }
177
178 #[test]
179 fn find_with_explicit_base() {
180 let tmp = std::env::temp_dir().join("find_cargo_toml_test_base");
181 let _ = fs::create_dir_all(&tmp);
182 let manifest = tmp.join("Cargo.toml");
183 let _ = fs::File::create(&manifest).and_then(|mut f| f.write_all(b"[package]"));
184 let collected: Vec<_> = find(".", Some(&tmp), None).collect();
185 let _ = fs::remove_file(manifest);
186 let _ = fs::remove_dir_all(&tmp);
187 assert_eq!(collected.len(), 1);
188 assert!(collected[0].ends_with("Cargo.toml"));
189 }
190
191 #[test]
192 fn find_normalizes_path_with_dot_dot() {
193 let tmp = std::env::temp_dir().join("find_cargo_toml_test_normalize");
194 let sub = tmp.join("sub");
195 let _ = fs::create_dir_all(&sub);
196 let manifest = tmp.join("Cargo.toml");
197 let _ = fs::File::create(&manifest).and_then(|mut f| f.write_all(b"[package]"));
198 let input = sub.join("..");
199 let collected: Vec<_> = find(&input, None::<PathBuf>, None).collect();
200 let _ = fs::remove_file(manifest);
201 let _ = fs::remove_dir_all(&tmp);
202 assert!(
203 collected
204 .iter()
205 .any(|p| p.parent().map(|d| d == tmp).unwrap_or(false)),
206 "should find Cargo.toml in tmp when input is sub/.."
207 );
208 }
209}