1use std::collections::BTreeMap;
26use std::fmt;
27use std::io;
28use std::path::{Path, PathBuf};
29
30use rucc_diag::SourceBytes;
31
32pub trait FileSystem: fmt::Debug + Send + Sync {
38 fn read(&self, path: &Path) -> io::Result<SourceBytes>;
45}
46
47#[derive(Debug, Default)]
49pub struct MemoryFileSystem {
50 files: BTreeMap<PathBuf, SourceBytes>,
51}
52
53impl MemoryFileSystem {
54 pub fn new() -> MemoryFileSystem {
56 MemoryFileSystem::default()
57 }
58
59 pub fn insert(
61 &mut self,
62 path: impl Into<PathBuf>,
63 contents: impl AsRef<[u8]> + Send + Sync + 'static,
64 ) {
65 self.files.insert(path.into(), SourceBytes::new(contents));
66 }
67
68 pub fn len(&self) -> usize {
70 self.files.len()
71 }
72
73 pub fn is_empty(&self) -> bool {
75 self.files.is_empty()
76 }
77}
78
79impl FileSystem for MemoryFileSystem {
80 fn read(&self, path: &Path) -> io::Result<SourceBytes> {
81 self.files
82 .get(path)
83 .cloned()
84 .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "no such file"))
85 }
86}
87
88#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
90pub enum IncludeForm {
91 Quoted,
93 Angled,
95}
96
97#[derive(Debug, Clone, PartialEq, Eq)]
99pub struct Dir {
100 pub path: PathBuf,
103 pub is_system: bool,
106}
107
108#[derive(Debug, Clone)]
110pub struct Found {
111 pub path: PathBuf,
113 pub name: String,
115 pub is_system: bool,
117 pub next: usize,
124 pub bytes: SourceBytes,
126}
127
128#[derive(Debug, Default, Clone, PartialEq, Eq)]
136pub struct SearchPath {
137 dirs: Vec<Dir>,
138 quote_end: usize,
140 bracket_end: usize,
142 system_end: usize,
144}
145
146impl SearchPath {
147 pub fn new() -> SearchPath {
149 SearchPath::default()
150 }
151
152 pub fn push_quote(&mut self, dir: impl Into<PathBuf>) {
154 let at = self.quote_end;
155 self.insert(at, dir.into(), false);
156 self.quote_end += 1;
157 self.bracket_end += 1;
158 self.system_end += 1;
159 }
160
161 pub fn push_bracket(&mut self, dir: impl Into<PathBuf>) {
163 let at = self.bracket_end;
164 self.insert(at, dir.into(), false);
165 self.bracket_end += 1;
166 self.system_end += 1;
167 }
168
169 pub fn push_system(&mut self, dir: impl Into<PathBuf>) {
171 let at = self.system_end;
172 self.insert(at, dir.into(), true);
173 self.system_end += 1;
174 }
175
176 pub fn push_after(&mut self, dir: impl Into<PathBuf>) {
178 let at = self.dirs.len();
179 self.insert(at, dir.into(), true);
180 }
181
182 fn insert(&mut self, at: usize, path: PathBuf, is_system: bool) {
183 self.dirs.insert(at, Dir { path, is_system });
184 }
185
186 pub fn dirs(&self) -> &[Dir] {
188 &self.dirs
189 }
190
191 pub fn start(&self, form: IncludeForm) -> usize {
196 match form {
197 IncludeForm::Quoted => 0,
198 IncludeForm::Angled => self.quote_end,
199 }
200 }
201
202 pub fn resolve(
212 &self,
213 fs: &dyn FileSystem,
214 name: &str,
215 form: IncludeForm,
216 relative_to: Option<&Path>,
217 from: usize,
218 ) -> Option<Found> {
219 let as_path = Path::new(name);
220 if is_absolute(as_path) {
221 let bytes = fs.read(as_path).ok()?;
222 return Some(Found {
223 path: as_path.to_path_buf(),
224 name: name.to_owned(),
225 is_system: false,
226 next: 0,
227 bytes,
228 });
229 }
230 if form == IncludeForm::Quoted {
231 if let Some(dir) = relative_to {
232 let path = dir.join(as_path);
233 if let Ok(bytes) = fs.read(&path) {
234 return Some(Found {
235 name: display(&path),
236 path,
237 is_system: false,
238 next: 0,
242 bytes,
243 });
244 }
245 }
246 }
247 for (at, dir) in self.dirs.iter().enumerate().skip(from) {
248 let path = dir.path.join(as_path);
249 if let Ok(bytes) = fs.read(&path) {
250 return Some(Found {
251 name: display(&path),
252 path,
253 is_system: dir.is_system,
254 next: at + 1,
255 bytes,
256 });
257 }
258 }
259 None
260 }
261
262 pub fn tried(
268 &self,
269 name: &str,
270 form: IncludeForm,
271 relative_to: Option<&Path>,
272 from: usize,
273 ) -> Vec<PathBuf> {
274 if is_absolute(Path::new(name)) {
275 return Vec::new();
276 }
277 let mut list = Vec::new();
278 if form == IncludeForm::Quoted {
279 if let Some(dir) = relative_to {
280 list.push(dir.to_path_buf());
281 }
282 }
283 list.extend(self.dirs.iter().skip(from).map(|d| d.path.clone()));
284 list
285 }
286}
287
288fn display(path: &Path) -> String {
290 path.to_string_lossy().into_owned()
291}
292
293fn is_absolute(path: &Path) -> bool {
299 path.is_absolute() || path.has_root()
300}
301
302#[cfg(test)]
303mod tests {
304 use super::*;
305
306 fn fs_with(files: &[&str]) -> MemoryFileSystem {
307 let mut fs = MemoryFileSystem::new();
308 for f in files {
309 fs.insert(*f, format!("/* {f} */\n").into_bytes());
310 }
311 fs
312 }
313
314 fn text(found: &Found) -> String {
315 String::from_utf8_lossy(found.bytes.as_slice()).into_owned()
316 }
317
318 fn norm(path: &str) -> String {
321 path.replace('\\', "/")
322 }
323
324 #[test]
325 fn a_missing_file_is_not_found_rather_than_an_error() {
326 let fs = MemoryFileSystem::new();
327 let kind = fs.read(Path::new("/nope.h")).err().map(|e| e.kind());
328 assert_eq!(kind, Some(io::ErrorKind::NotFound));
329 assert!(fs.is_empty());
330 }
331
332 #[test]
333 fn quote_directories_are_invisible_to_an_angled_include() {
334 let fs = fs_with(&["/q/a.h", "/i/a.h"]);
335 let mut search = SearchPath::new();
336 search.push_quote("/q");
337 search.push_bracket("/i");
338
339 let quoted = search.resolve(&fs, "a.h", IncludeForm::Quoted, None, 0).unwrap();
340 assert_eq!(norm("ed.name), "/q/a.h");
341 let angled = search
342 .resolve(&fs, "a.h", IncludeForm::Angled, None, search.start(IncludeForm::Angled))
343 .unwrap();
344 assert_eq!(norm(&angled.name), "/i/a.h");
345 }
346
347 #[test]
348 fn the_including_files_own_directory_comes_first_for_a_quoted_include() {
349 let fs = fs_with(&["/src/a.h", "/i/a.h"]);
350 let mut search = SearchPath::new();
351 search.push_bracket("/i");
352 let here = Path::new("/src");
353
354 let quoted = search.resolve(&fs, "a.h", IncludeForm::Quoted, Some(here), 0).unwrap();
355 assert_eq!(norm("ed.name), "/src/a.h");
356 let angled = search.resolve(&fs, "a.h", IncludeForm::Angled, Some(here), 0).unwrap();
358 assert_eq!(norm(&angled.name), "/i/a.h");
359 }
360
361 #[test]
362 fn the_order_is_iquote_then_i_then_isystem_then_idirafter() {
363 let fs = fs_with(&["/after/a.h", "/sys/a.h", "/i/a.h", "/q/a.h"]);
364 let mut search = SearchPath::new();
365 search.push_after("/after");
368 search.push_system("/sys");
369 search.push_bracket("/i");
370 search.push_quote("/q");
371 let order: Vec<_> = search.dirs().iter().map(|d| norm(&d.path.to_string_lossy())).collect();
372 assert_eq!(order, ["/q", "/i", "/sys", "/after"]);
373
374 let found = search.resolve(&fs, "a.h", IncludeForm::Quoted, None, 0).unwrap();
375 assert_eq!(norm(&found.name), "/q/a.h");
376 assert_eq!(found.next, 1);
377 }
378
379 #[test]
380 fn a_system_directory_marks_what_it_holds_as_a_system_header() {
381 let fs = fs_with(&["/i/a.h", "/sys/b.h", "/after/c.h"]);
382 let mut search = SearchPath::new();
383 search.push_bracket("/i");
384 search.push_system("/sys");
385 search.push_after("/after");
386 let get = |n| search.resolve(&fs, n, IncludeForm::Angled, None, 0).unwrap();
387 assert!(!get("a.h").is_system);
388 assert!(get("b.h").is_system);
389 assert!(get("c.h").is_system);
390 }
391
392 #[test]
393 fn include_next_continues_past_the_directory_the_current_file_came_from() {
394 let fs = fs_with(&["/a/limits.h", "/b/limits.h", "/c/limits.h"]);
395 let mut search = SearchPath::new();
396 search.push_bracket("/a");
397 search.push_bracket("/b");
398 search.push_bracket("/c");
399
400 let first = search.resolve(&fs, "limits.h", IncludeForm::Angled, None, 0).unwrap();
401 assert_eq!(norm(&first.name), "/a/limits.h");
402 let second =
403 search.resolve(&fs, "limits.h", IncludeForm::Angled, None, first.next).unwrap();
404 assert_eq!(norm(&second.name), "/b/limits.h");
405 let third =
406 search.resolve(&fs, "limits.h", IncludeForm::Angled, None, second.next).unwrap();
407 assert_eq!(norm(&third.name), "/c/limits.h");
408 assert!(search.resolve(&fs, "limits.h", IncludeForm::Angled, None, third.next).is_none());
409 }
410
411 #[test]
412 fn a_name_with_a_directory_in_it_is_joined_onto_each_entry() {
413 let fs = fs_with(&["/i/sys/types.h"]);
414 let mut search = SearchPath::new();
415 search.push_bracket("/i");
416 let found = search.resolve(&fs, "sys/types.h", IncludeForm::Angled, None, 0).unwrap();
417 assert_eq!(norm(&found.name), "/i/sys/types.h");
418 assert_eq!(text(&found), "/* /i/sys/types.h */\n");
419 }
420
421 #[test]
422 fn an_absolute_name_ignores_the_search_path() {
423 let fs = fs_with(&["/gen/config.h", "/i/gen/config.h"]);
424 let mut search = SearchPath::new();
425 search.push_bracket("/i");
426 let found = search.resolve(&fs, "/gen/config.h", IncludeForm::Angled, None, 0).unwrap();
427 assert_eq!(norm(&found.name), "/gen/config.h");
428 assert!(search.tried("/gen/config.h", IncludeForm::Angled, None, 0).is_empty());
429 }
430
431 #[test]
432 fn the_list_of_places_tried_is_the_list_that_was_searched() {
433 let fs = MemoryFileSystem::new();
434 let mut search = SearchPath::new();
435 search.push_quote("/q");
436 search.push_bracket("/i");
437 search.push_system("/sys");
438 let here = Path::new("/src");
439
440 assert!(search.resolve(&fs, "a.h", IncludeForm::Quoted, Some(here), 0).is_none());
441 let tried = search.tried("a.h", IncludeForm::Quoted, Some(here), 0);
442 let tried: Vec<_> = tried.iter().map(|p| norm(&p.to_string_lossy())).collect();
443 assert_eq!(tried, ["/src", "/q", "/i", "/sys"]);
444
445 let start = search.start(IncludeForm::Angled);
446 let tried = search.tried("a.h", IncludeForm::Angled, Some(here), start);
447 let tried: Vec<_> = tried.iter().map(|p| norm(&p.to_string_lossy())).collect();
448 assert_eq!(tried, ["/i", "/sys"]);
449 }
450
451 #[test]
452 fn a_header_found_next_to_its_includer_does_not_skip_the_whole_path_afterwards() {
453 let fs = fs_with(&["/src/a.h", "/i/b.h"]);
456 let mut search = SearchPath::new();
457 search.push_bracket("/i");
458 let found =
459 search.resolve(&fs, "a.h", IncludeForm::Quoted, Some(Path::new("/src")), 0).unwrap();
460 assert_eq!(found.next, 0);
461 let next = search.resolve(&fs, "b.h", IncludeForm::Angled, None, found.next).unwrap();
462 assert_eq!(norm(&next.name), "/i/b.h");
463 }
464}