1use std::collections::BTreeMap;
26use std::fmt;
27use std::io;
28use std::path::{Path, PathBuf};
29
30use rucc_diag::SourceBytes;
31
32use crate::runtime;
33
34pub trait FileSystem: fmt::Debug + Send + Sync {
40 fn read(&self, path: &Path) -> io::Result<SourceBytes>;
47}
48
49#[derive(Debug, Default)]
51pub struct MemoryFileSystem {
52 files: BTreeMap<PathBuf, SourceBytes>,
53}
54
55impl MemoryFileSystem {
56 pub fn new() -> MemoryFileSystem {
58 MemoryFileSystem::default()
59 }
60
61 pub fn insert(
63 &mut self,
64 path: impl Into<PathBuf>,
65 contents: impl AsRef<[u8]> + Send + Sync + 'static,
66 ) {
67 self.files.insert(path.into(), SourceBytes::new(contents));
68 }
69
70 pub fn len(&self) -> usize {
72 self.files.len()
73 }
74
75 pub fn is_empty(&self) -> bool {
77 self.files.is_empty()
78 }
79}
80
81impl FileSystem for MemoryFileSystem {
82 fn read(&self, path: &Path) -> io::Result<SourceBytes> {
83 self.files
84 .get(path)
85 .cloned()
86 .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "no such file"))
87 }
88}
89
90#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
92pub enum IncludeForm {
93 Quoted,
95 Angled,
97}
98
99#[derive(Debug, Clone, PartialEq, Eq)]
101pub struct Dir {
102 pub path: PathBuf,
105 pub is_system: bool,
108}
109
110fn same_dir(a: &Path, b: &Path) -> bool {
116 let normal = |p: &Path| -> PathBuf {
117 p.components().filter(|c| !matches!(c, std::path::Component::CurDir)).collect()
118 };
119 normal(a) == normal(b)
120}
121
122#[derive(Debug, Clone)]
124pub struct Found {
125 pub path: PathBuf,
127 pub name: String,
129 pub is_system: bool,
131 pub next: usize,
138 pub bytes: SourceBytes,
140}
141
142#[derive(Debug, Default, Clone, PartialEq, Eq)]
150pub struct SearchPath {
151 dirs: Vec<Dir>,
152 quote_end: usize,
154 bracket_end: usize,
156 system_end: usize,
158}
159
160impl SearchPath {
161 pub fn new() -> SearchPath {
163 SearchPath::default()
164 }
165
166 pub fn push_quote(&mut self, dir: impl Into<PathBuf>) {
168 let at = self.quote_end;
169 self.insert(at, dir.into(), false);
170 self.quote_end += 1;
171 self.bracket_end += 1;
172 self.system_end += 1;
173 }
174
175 pub fn push_bracket(&mut self, dir: impl Into<PathBuf>) {
177 let at = self.bracket_end;
178 self.insert(at, dir.into(), false);
179 self.bracket_end += 1;
180 self.system_end += 1;
181 }
182
183 pub fn push_system(&mut self, dir: impl Into<PathBuf>) {
185 let at = self.system_end;
186 self.insert(at, dir.into(), true);
187 self.system_end += 1;
188 }
189
190 pub fn push_after(&mut self, dir: impl Into<PathBuf>) {
192 let at = self.dirs.len();
193 self.insert(at, dir.into(), true);
194 }
195
196 fn insert(&mut self, at: usize, path: PathBuf, is_system: bool) {
197 self.dirs.insert(at, Dir { path, is_system });
198 }
199
200 pub fn remove_duplicates(&mut self) {
220 let mut keep = vec![true; self.dirs.len()];
221 for i in 0..self.dirs.len() {
222 for j in i + 1..self.dirs.len() {
223 if !keep[i] {
224 break;
225 }
226 if !keep[j] || !same_dir(&self.dirs[i].path, &self.dirs[j].path) {
227 continue;
228 }
229 if self.dirs[j].is_system && !self.dirs[i].is_system {
230 keep[i] = false;
231 } else {
232 keep[j] = false;
233 }
234 }
235 }
236 let (quote, bracket, system) = (self.quote_end, self.bracket_end, self.system_end);
237 let mut at = 0;
238 self.dirs.retain(|_| {
239 let kept = keep[at];
240 if !kept {
241 self.quote_end -= usize::from(at < quote);
242 self.bracket_end -= usize::from(at < bracket);
243 self.system_end -= usize::from(at < system);
244 }
245 at += 1;
246 kept
247 });
248 }
249
250 pub fn dirs(&self) -> &[Dir] {
252 &self.dirs
253 }
254
255 pub fn start(&self, form: IncludeForm) -> usize {
260 match form {
261 IncludeForm::Quoted => 0,
262 IncludeForm::Angled => self.quote_end,
263 }
264 }
265
266 pub fn resolve(
276 &self,
277 fs: &dyn FileSystem,
278 name: &str,
279 form: IncludeForm,
280 relative_to: Option<&Path>,
281 from: usize,
282 ) -> Option<Found> {
283 let as_path = Path::new(name);
284 if is_absolute(as_path) {
285 let bytes = open(fs, as_path).ok()?;
286 return Some(Found {
287 path: as_path.to_path_buf(),
288 name: name.to_owned(),
289 is_system: false,
290 next: 0,
291 bytes,
292 });
293 }
294 if form == IncludeForm::Quoted {
295 if let Some(dir) = relative_to {
296 let path = dir.join(as_path);
297 if let Ok(bytes) = open(fs, &path) {
298 return Some(Found {
299 name: display(&path),
300 path,
301 is_system: false,
302 next: 0,
306 bytes,
307 });
308 }
309 }
310 }
311 for (at, dir) in self.dirs.iter().enumerate().skip(from) {
312 let path = dir.path.join(as_path);
313 if let Ok(bytes) = open(fs, &path) {
314 return Some(Found {
315 name: display(&path),
316 path,
317 is_system: dir.is_system,
318 next: at + 1,
319 bytes,
320 });
321 }
322 }
323 None
324 }
325
326 pub fn tried(
332 &self,
333 name: &str,
334 form: IncludeForm,
335 relative_to: Option<&Path>,
336 from: usize,
337 ) -> Vec<PathBuf> {
338 if is_absolute(Path::new(name)) {
339 return Vec::new();
340 }
341 let mut list = Vec::new();
342 if form == IncludeForm::Quoted {
343 if let Some(dir) = relative_to {
344 list.push(dir.to_path_buf());
345 }
346 }
347 list.extend(self.dirs.iter().skip(from).map(|d| d.path.clone()));
348 list
349 }
350}
351
352fn open(fs: &dyn FileSystem, path: &Path) -> io::Result<SourceBytes> {
359 match runtime::read(path) {
360 Some(bytes) => Ok(bytes),
361 None => fs.read(path),
362 }
363}
364
365fn display(path: &Path) -> String {
367 path.to_string_lossy().into_owned()
368}
369
370fn is_absolute(path: &Path) -> bool {
376 path.is_absolute() || path.has_root()
377}
378
379#[cfg(test)]
380mod tests {
381 use super::*;
382
383 fn fs_with(files: &[&str]) -> MemoryFileSystem {
384 let mut fs = MemoryFileSystem::new();
385 for f in files {
386 fs.insert(*f, format!("/* {f} */\n").into_bytes());
387 }
388 fs
389 }
390
391 fn text(found: &Found) -> String {
392 String::from_utf8_lossy(found.bytes.as_slice()).into_owned()
393 }
394
395 fn norm(path: &str) -> String {
398 path.replace('\\', "/")
399 }
400
401 #[test]
402 fn a_missing_file_is_not_found_rather_than_an_error() {
403 let fs = MemoryFileSystem::new();
404 let kind = fs.read(Path::new("/nope.h")).err().map(|e| e.kind());
405 assert_eq!(kind, Some(io::ErrorKind::NotFound));
406 assert!(fs.is_empty());
407 }
408
409 #[test]
410 fn quote_directories_are_invisible_to_an_angled_include() {
411 let fs = fs_with(&["/q/a.h", "/i/a.h"]);
412 let mut search = SearchPath::new();
413 search.push_quote("/q");
414 search.push_bracket("/i");
415
416 let quoted = search.resolve(&fs, "a.h", IncludeForm::Quoted, None, 0).unwrap();
417 assert_eq!(norm("ed.name), "/q/a.h");
418 let angled = search
419 .resolve(&fs, "a.h", IncludeForm::Angled, None, search.start(IncludeForm::Angled))
420 .unwrap();
421 assert_eq!(norm(&angled.name), "/i/a.h");
422 }
423
424 #[test]
425 fn the_including_files_own_directory_comes_first_for_a_quoted_include() {
426 let fs = fs_with(&["/src/a.h", "/i/a.h"]);
427 let mut search = SearchPath::new();
428 search.push_bracket("/i");
429 let here = Path::new("/src");
430
431 let quoted = search.resolve(&fs, "a.h", IncludeForm::Quoted, Some(here), 0).unwrap();
432 assert_eq!(norm("ed.name), "/src/a.h");
433 let angled = search.resolve(&fs, "a.h", IncludeForm::Angled, Some(here), 0).unwrap();
435 assert_eq!(norm(&angled.name), "/i/a.h");
436 }
437
438 #[test]
439 fn the_order_is_iquote_then_i_then_isystem_then_idirafter() {
440 let fs = fs_with(&["/after/a.h", "/sys/a.h", "/i/a.h", "/q/a.h"]);
441 let mut search = SearchPath::new();
442 search.push_after("/after");
445 search.push_system("/sys");
446 search.push_bracket("/i");
447 search.push_quote("/q");
448 let order: Vec<_> = search.dirs().iter().map(|d| norm(&d.path.to_string_lossy())).collect();
449 assert_eq!(order, ["/q", "/i", "/sys", "/after"]);
450
451 let found = search.resolve(&fs, "a.h", IncludeForm::Quoted, None, 0).unwrap();
452 assert_eq!(norm(&found.name), "/q/a.h");
453 assert_eq!(found.next, 1);
454 }
455
456 #[test]
457 fn a_directory_already_on_the_path_is_dropped_rather_than_searched_twice() {
458 let mut search = SearchPath::new();
459 search.push_system("/usr/local/include");
460 search.push_system("/usr/include");
461 search.push_system("/usr/local/include");
462 search.push_system("/usr/include/");
463 search.remove_duplicates();
464 let order: Vec<_> = search.dirs().iter().map(|d| norm(&d.path.to_string_lossy())).collect();
465 assert_eq!(order, ["/usr/local/include", "/usr/include"]);
468 }
469
470 #[test]
471 fn a_duplicate_is_what_makes_include_next_find_the_file_it_is_standing_in() {
472 let fs = fs_with(&["/usr/include/a.h"]);
476 let mut search = SearchPath::new();
477 search.push_system("/usr/include");
478 search.push_system("/usr/include");
479 let first = search.resolve(&fs, "a.h", IncludeForm::Angled, None, 0).unwrap();
480 assert!(search.resolve(&fs, "a.h", IncludeForm::Angled, None, first.next).is_some());
481 search.remove_duplicates();
482 let first = search.resolve(&fs, "a.h", IncludeForm::Angled, None, 0).unwrap();
483 assert!(search.resolve(&fs, "a.h", IncludeForm::Angled, None, first.next).is_none());
484 }
485
486 #[test]
487 fn a_bracket_directory_that_names_a_system_one_is_the_entry_that_goes() {
488 let fs = fs_with(&["/usr/include/a.h"]);
491 let mut search = SearchPath::new();
492 search.push_bracket("/usr/include");
493 search.push_bracket("/i");
494 search.push_system("/usr/include");
495 search.remove_duplicates();
496 let order: Vec<_> = search.dirs().iter().map(|d| norm(&d.path.to_string_lossy())).collect();
497 assert_eq!(order, ["/i", "/usr/include"]);
498 assert!(search.resolve(&fs, "a.h", IncludeForm::Angled, None, 0).unwrap().is_system);
499 }
500
501 #[test]
502 fn dropping_an_entry_keeps_the_group_boundaries_where_the_groups_are() {
503 let fs = fs_with(&["/q/a.h", "/i/a.h"]);
504 let mut search = SearchPath::new();
505 search.push_quote("/q");
506 search.push_quote("/q");
507 search.push_bracket("/i");
508 search.push_bracket("/i");
509 search.remove_duplicates();
510 let at = search.start(IncludeForm::Angled);
512 let found = search.resolve(&fs, "a.h", IncludeForm::Angled, None, at).unwrap();
513 assert_eq!(norm(&found.name), "/i/a.h");
514 }
515
516 #[test]
517 fn a_system_directory_marks_what_it_holds_as_a_system_header() {
518 let fs = fs_with(&["/i/a.h", "/sys/b.h", "/after/c.h"]);
519 let mut search = SearchPath::new();
520 search.push_bracket("/i");
521 search.push_system("/sys");
522 search.push_after("/after");
523 let get = |n| search.resolve(&fs, n, IncludeForm::Angled, None, 0).unwrap();
524 assert!(!get("a.h").is_system);
525 assert!(get("b.h").is_system);
526 assert!(get("c.h").is_system);
527 }
528
529 #[test]
530 fn include_next_continues_past_the_directory_the_current_file_came_from() {
531 let fs = fs_with(&["/a/limits.h", "/b/limits.h", "/c/limits.h"]);
532 let mut search = SearchPath::new();
533 search.push_bracket("/a");
534 search.push_bracket("/b");
535 search.push_bracket("/c");
536
537 let first = search.resolve(&fs, "limits.h", IncludeForm::Angled, None, 0).unwrap();
538 assert_eq!(norm(&first.name), "/a/limits.h");
539 let second =
540 search.resolve(&fs, "limits.h", IncludeForm::Angled, None, first.next).unwrap();
541 assert_eq!(norm(&second.name), "/b/limits.h");
542 let third =
543 search.resolve(&fs, "limits.h", IncludeForm::Angled, None, second.next).unwrap();
544 assert_eq!(norm(&third.name), "/c/limits.h");
545 assert!(search.resolve(&fs, "limits.h", IncludeForm::Angled, None, third.next).is_none());
546 }
547
548 #[test]
549 fn a_name_with_a_directory_in_it_is_joined_onto_each_entry() {
550 let fs = fs_with(&["/i/sys/types.h"]);
551 let mut search = SearchPath::new();
552 search.push_bracket("/i");
553 let found = search.resolve(&fs, "sys/types.h", IncludeForm::Angled, None, 0).unwrap();
554 assert_eq!(norm(&found.name), "/i/sys/types.h");
555 assert_eq!(text(&found), "/* /i/sys/types.h */\n");
556 }
557
558 #[test]
559 fn an_absolute_name_ignores_the_search_path() {
560 let fs = fs_with(&["/gen/config.h", "/i/gen/config.h"]);
561 let mut search = SearchPath::new();
562 search.push_bracket("/i");
563 let found = search.resolve(&fs, "/gen/config.h", IncludeForm::Angled, None, 0).unwrap();
564 assert_eq!(norm(&found.name), "/gen/config.h");
565 assert!(search.tried("/gen/config.h", IncludeForm::Angled, None, 0).is_empty());
566 }
567
568 #[test]
569 fn the_list_of_places_tried_is_the_list_that_was_searched() {
570 let fs = MemoryFileSystem::new();
571 let mut search = SearchPath::new();
572 search.push_quote("/q");
573 search.push_bracket("/i");
574 search.push_system("/sys");
575 let here = Path::new("/src");
576
577 assert!(search.resolve(&fs, "a.h", IncludeForm::Quoted, Some(here), 0).is_none());
578 let tried = search.tried("a.h", IncludeForm::Quoted, Some(here), 0);
579 let tried: Vec<_> = tried.iter().map(|p| norm(&p.to_string_lossy())).collect();
580 assert_eq!(tried, ["/src", "/q", "/i", "/sys"]);
581
582 let start = search.start(IncludeForm::Angled);
583 let tried = search.tried("a.h", IncludeForm::Angled, Some(here), start);
584 let tried: Vec<_> = tried.iter().map(|p| norm(&p.to_string_lossy())).collect();
585 assert_eq!(tried, ["/i", "/sys"]);
586 }
587
588 #[test]
589 fn a_header_found_next_to_its_includer_does_not_skip_the_whole_path_afterwards() {
590 let fs = fs_with(&["/src/a.h", "/i/b.h"]);
593 let mut search = SearchPath::new();
594 search.push_bracket("/i");
595 let found =
596 search.resolve(&fs, "a.h", IncludeForm::Quoted, Some(Path::new("/src")), 0).unwrap();
597 assert_eq!(found.next, 0);
598 let next = search.resolve(&fs, "b.h", IncludeForm::Angled, None, found.next).unwrap();
599 assert_eq!(norm(&next.name), "/i/b.h");
600 }
601}