rucc_session/fs.rs
1//! The file system the compiler reads through, and the include search path.
2//!
3//! Design: `spec/04-driver-and-cli.md` section 4.4 for the search order, and
4//! `spec/05-preprocessor.md` section 5.4 for what `#include_next` means.
5//!
6//! Nothing below the driver calls `std::fs`. That is what makes the compiler usable as a
7//! library, and it is what makes a preprocessor test a value rather than a temporary
8//! directory: [`MemoryFileSystem`] is a map from path to bytes, and a test that needs a
9//! twelve deep header nest builds one in twelve lines with no clean up to forget.
10//!
11//! ```
12//! use rucc_session::{FileSystem, IncludeForm, MemoryFileSystem, SearchPath};
13//!
14//! let mut fs = MemoryFileSystem::new();
15//! fs.insert("/usr/include/stdio.h", *b"int puts(const char *);\n");
16//!
17//! let mut search = SearchPath::new();
18//! search.push_system("/usr/include");
19//!
20//! let found = search.resolve(&fs, "stdio.h", IncludeForm::Angled, None, 0).unwrap();
21//! assert_eq!(found.name.replace('\\', "/"), "/usr/include/stdio.h");
22//! assert!(found.is_system);
23//! ```
24
25use std::collections::BTreeMap;
26use std::fmt;
27use std::io;
28use std::path::{Path, PathBuf};
29
30use rucc_diag::SourceBytes;
31
32use crate::runtime;
33
34/// Where the compiler reads source from.
35///
36/// There is no `exists`, deliberately. Whether a file is there is answered by trying to read
37/// it, and an interface with a separate question invites the race where the answer changes
38/// between the two calls.
39pub trait FileSystem: fmt::Debug + Send + Sync {
40 /// Reads a file.
41 ///
42 /// # Errors
43 ///
44 /// Whatever the underlying file system says. [`io::ErrorKind::NotFound`] is the ordinary
45 /// case during an include search and is not by itself a problem.
46 fn read(&self, path: &Path) -> io::Result<SourceBytes>;
47
48 /// What this file system calls a file, for deciding that two names are one file.
49 ///
50 /// The multiple include optimization has to answer whether a header has been read
51 /// already, and the name in the directive is not that answer. One header found through
52 /// `-I .` and found again through `-I /tree` arrives under two names, and a project with
53 /// more than one include directory reaches the same header both ways all day. So does a
54 /// file that includes itself, which is the whole point of `#pragma once` in a main file.
55 ///
56 /// The default is [`path_key`], the text with the `.` components taken out, which is all
57 /// a map from name to bytes can say. An implementation backed by a real file system
58 /// resolves the name instead, so that a symlink, a `..` and a relative path all land on
59 /// the same answer. Only called for a file that has already been read, so it is a
60 /// question about a file that is there rather than a probe.
61 fn identity(&self, path: &Path) -> PathBuf {
62 path_key(path)
63 }
64}
65
66/// A file system held in memory, for tests and for embedding the compiler.
67#[derive(Debug, Default)]
68pub struct MemoryFileSystem {
69 files: BTreeMap<PathBuf, SourceBytes>,
70}
71
72impl MemoryFileSystem {
73 /// An empty file system.
74 pub fn new() -> MemoryFileSystem {
75 MemoryFileSystem::default()
76 }
77
78 /// Adds a file, replacing any file already at that path.
79 pub fn insert(
80 &mut self,
81 path: impl Into<PathBuf>,
82 contents: impl AsRef<[u8]> + Send + Sync + 'static,
83 ) {
84 self.files.insert(path_key(&path.into()), SourceBytes::new(contents));
85 }
86
87 /// How many files it holds.
88 pub fn len(&self) -> usize {
89 self.files.len()
90 }
91
92 /// Whether it holds nothing.
93 pub fn is_empty(&self) -> bool {
94 self.files.is_empty()
95 }
96}
97
98impl FileSystem for MemoryFileSystem {
99 fn read(&self, path: &Path) -> io::Result<SourceBytes> {
100 // Through [`path_key`], so that `./dir/x.h` and `dir/x.h` find one file here the way
101 // they do on a real file system. A test that behaves differently from the thing it
102 // stands in for is worse than no test.
103 self.files
104 .get(&path_key(path))
105 .cloned()
106 .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "no such file"))
107 }
108}
109
110/// Which spelling an `#include` used.
111#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
112pub enum IncludeForm {
113 /// `#include "local.h"`, which looks next to the including file first.
114 Quoted,
115 /// `#include <stdio.h>`, which does not.
116 Angled,
117}
118
119/// One directory on the search path.
120#[derive(Debug, Clone, PartialEq, Eq)]
121pub struct Dir {
122 /// The directory, as the user spelled it. Not canonicalised, because a diagnostic that
123 /// says `../include/foo.h` is more use than one naming a path the user never typed.
124 pub path: PathBuf,
125 /// Whether headers found here are system headers, which suppresses warnings in them and
126 /// sets the `3` flag on a `-E` line marker.
127 pub is_system: bool,
128}
129
130/// The key that every spelling of one path shares, as far as text can say.
131///
132/// `Path::components` is what does the work: it drops a trailing separator and the `.` inside
133/// a path, so `/usr/include/` and `/usr/include` are one directory, and `./dir/x.h` and
134/// `dir/x.h` are one file. `..` is left alone, since a component above a symlink does not go
135/// where reading the path suggests.
136///
137/// This is text, not identity. Two names for one file through a symlink or a hard link are two
138/// keys here, where a compiler that asked the file system would get one answer. Asking would
139/// mean a `stat` per include on a path where the whole point is not to open the file at all.
140pub fn path_key(path: &Path) -> PathBuf {
141 path.components().filter(|c| !matches!(c, std::path::Component::CurDir)).collect()
142}
143
144/// Whether two spellings name the same directory, as far as text can say.
145fn same_dir(a: &Path, b: &Path) -> bool {
146 path_key(a) == path_key(b)
147}
148
149/// A header that was found.
150#[derive(Debug, Clone)]
151pub struct Found {
152 /// The path to open, which is the directory joined with the name as written.
153 pub path: PathBuf,
154 /// That path as a string, for the source map and for diagnostics.
155 pub name: String,
156 /// Whether it came from a system directory.
157 pub is_system: bool,
158 /// Where an `#include_next` written in this file should start looking.
159 ///
160 /// One past the entry this header came from, or zero for a header found next to the file
161 /// that included it, because that directory is not on the path and there is nothing to
162 /// continue past. Carrying the answer rather than the position is what keeps the two
163 /// cases from being confused at the call site.
164 pub next: usize,
165 /// The contents.
166 pub bytes: SourceBytes,
167}
168
169/// The directories a header is looked for in, in order.
170///
171/// GCC's order, because a different one produces header shadowing bugs that are miserable to
172/// diagnose: `-iquote` first and only for a quoted include, then `-I`, then `-isystem`, then
173/// the configured system directories, then `-idirafter`. The directory of the including file
174/// comes before all of it for a quoted include, and it is not part of the numbered list
175/// because `#include_next` must not be able to land back on it.
176#[derive(Debug, Default, Clone, PartialEq, Eq)]
177pub struct SearchPath {
178 dirs: Vec<Dir>,
179 /// Where the `-I` directories begin, which is where an angled include starts looking.
180 quote_end: usize,
181 /// Where the `-isystem` and configured system directories begin.
182 bracket_end: usize,
183 /// Where the `-idirafter` directories begin.
184 system_end: usize,
185 /// Whether the directory of the including file has been taken off the front of the quoted
186 /// chain, which is half of what `-I-` does.
187 no_current_dir: bool,
188 /// Why the target's own system directories are not on here, when somebody knows.
189 missing_system: Option<String>,
190}
191
192impl SearchPath {
193 /// An empty search path.
194 pub fn new() -> SearchPath {
195 SearchPath::default()
196 }
197
198 /// Adds a `-iquote` directory, searched only for a quoted include.
199 pub fn push_quote(&mut self, dir: impl Into<PathBuf>) {
200 let at = self.quote_end;
201 self.insert(at, dir.into(), false);
202 self.quote_end += 1;
203 self.bracket_end += 1;
204 self.system_end += 1;
205 }
206
207 /// Adds a `-I` directory.
208 pub fn push_bracket(&mut self, dir: impl Into<PathBuf>) {
209 let at = self.bracket_end;
210 self.insert(at, dir.into(), false);
211 self.bracket_end += 1;
212 self.system_end += 1;
213 }
214
215 /// Adds a `-isystem` directory, or one of the target's configured system directories.
216 pub fn push_system(&mut self, dir: impl Into<PathBuf>) {
217 let at = self.system_end;
218 self.insert(at, dir.into(), true);
219 self.system_end += 1;
220 }
221
222 /// Adds a `-idirafter` directory, which is searched after everything else.
223 pub fn push_after(&mut self, dir: impl Into<PathBuf>) {
224 let at = self.dirs.len();
225 self.insert(at, dir.into(), true);
226 }
227
228 fn insert(&mut self, at: usize, path: PathBuf, is_system: bool) {
229 self.dirs.insert(at, Dir { path, is_system });
230 }
231
232 /// The search list in the shape `gcc -v` prints it to stderr.
233 ///
234 /// Build systems read this rather than asking, meson and cmake among them, to learn which
235 /// directories hold the system headers. The shape is GCC's to the character, a space in front
236 /// of each directory included, because what reads it is a parser written against GCC.
237 pub fn render_gcc(&self) -> String {
238 let mut out = String::from("#include \"...\" search starts here:\n");
239 let line = |out: &mut String, dir: &Dir| {
240 out.push(' ');
241 out.push_str(&dir.path.display().to_string());
242 out.push('\n');
243 };
244 for dir in &self.dirs[..self.quote_end] {
245 line(&mut out, dir);
246 }
247 out.push_str("#include <...> search starts here:\n");
248 for dir in &self.dirs[self.quote_end..] {
249 line(&mut out, dir);
250 }
251 out.push_str("End of search list.\n");
252 out
253 }
254
255 /// Makes every directory added so far reachable only by a quoted include, which is `-I-`.
256 ///
257 /// The flag GCC deprecated in favour of `-iquote` and still supports, because a build system
258 /// old enough to be worth compiling is old enough to pass it. It does two things at once. The
259 /// `-I` directories written before it move into the quoted chain, so `#include <x.h>` stops
260 /// seeing them, and the directory of the including file comes off the front of that chain, so
261 /// `#include "x.h"` stops looking next to the file that wrote it.
262 ///
263 /// The second half is the reason the flag was worth having and the reason it was worth
264 /// dropping. It is the only way to say that a quoted include means a directory the command
265 /// line named rather than whatever happens to sit beside the source, which is what a project
266 /// with two headers of the same name in two directories needs. It is also a global answer to
267 /// a question every include asks separately, which is why `-iquote` replaced it.
268 ///
269 /// A `-iquote` directory given before this stays in the quoted chain, and lands after the
270 /// `-I` directories that just joined it. That is GCC's order and not an accident of the
271 /// implementation: GCC holds `-iquote` back until every `-I` and `-I-` has been dealt with,
272 /// so a `-iquote` is always later in the chain than an `-I` whatever order they were written.
273 pub fn split_quote_chain(&mut self) {
274 let moved: Vec<Dir> = self.dirs.drain(self.quote_end..self.bracket_end).collect();
275 for (at, dir) in moved.into_iter().enumerate() {
276 self.dirs.insert(at, dir);
277 }
278 self.quote_end = self.bracket_end;
279 self.no_current_dir = true;
280 }
281
282 /// Records why this target has no system directories, for whoever has to report a header that
283 /// was not found.
284 ///
285 /// The driver is the only thing that knows the answer and the preprocessor is the only thing in
286 /// a position to use it, and a header that could not be found is the one moment where it helps.
287 /// Saying it earlier would mean refusing a program that includes none of the library, which for
288 /// a target whose headers nobody may redistribute is exactly the program that has to keep
289 /// working, so the reason travels with the path and waits.
290 pub fn explain_missing_system(&mut self, why: impl Into<String>) {
291 self.missing_system = Some(why.into());
292 }
293
294 /// What to add to a header that was not found, or [`None`] when nobody left a reason.
295 pub fn missing_system(&self) -> Option<&str> {
296 self.missing_system.as_deref()
297 }
298
299 /// Whether the directory of the including file is searched for a quoted include.
300 ///
301 /// False once `-I-` has been given. A caller that has a directory to offer still passes it,
302 /// and this is where it is refused, so that the rule lives with the search path rather than at
303 /// every call site that knows where a file came from.
304 pub fn searches_current_dir(&self) -> bool {
305 !self.no_current_dir
306 }
307
308 /// Drops the directories that are already on the path, the way GCC does.
309 ///
310 /// A duplicate is not a harmless extra entry that costs one failed open. It changes what
311 /// `#include_next` means, which is defined as continuing past the directory the current
312 /// file came from: a header found in the first `/usr/include` writes `#include_next
313 /// <stdint.h>` meaning "the one below me" and finds itself in the second, and a header set
314 /// that ends in a fixed point of its own is one that includes itself forever or answers
315 /// `__has_include_next` yes where the compiler it was written for said no. It shows up as
316 /// soon as somebody passes the system directories on the command line, which the compat
317 /// harness does deliberately and a build system does by accident.
318 ///
319 /// A `-I` that names a system directory loses to the system entry rather than the other way
320 /// round, and that is GCC's rule and is documented as one: keeping the earlier one would
321 /// move a system directory up the order and take the system treatment off the headers in
322 /// it, so the `-I` is the one that goes.
323 ///
324 /// Two names for one directory are two directories here, where GCC compares the device and
325 /// the inode and sees through a symlink. That wants a file system that can answer the
326 /// question and this one deliberately only reads.
327 pub fn remove_duplicates(&mut self) {
328 let mut keep = vec![true; self.dirs.len()];
329 for i in 0..self.dirs.len() {
330 for j in i + 1..self.dirs.len() {
331 if !keep[i] {
332 break;
333 }
334 if !keep[j] || !same_dir(&self.dirs[i].path, &self.dirs[j].path) {
335 continue;
336 }
337 if self.dirs[j].is_system && !self.dirs[i].is_system {
338 keep[i] = false;
339 } else {
340 keep[j] = false;
341 }
342 }
343 }
344 let (quote, bracket, system) = (self.quote_end, self.bracket_end, self.system_end);
345 let mut at = 0;
346 self.dirs.retain(|_| {
347 let kept = keep[at];
348 if !kept {
349 self.quote_end -= usize::from(at < quote);
350 self.bracket_end -= usize::from(at < bracket);
351 self.system_end -= usize::from(at < system);
352 }
353 at += 1;
354 kept
355 });
356 }
357
358 /// Every directory, in search order.
359 pub fn dirs(&self) -> &[Dir] {
360 &self.dirs
361 }
362
363 /// The first entry an include of this form looks at.
364 ///
365 /// An angled include skips the `-iquote` directories, which is the only difference
366 /// between the two chains once the including file's own directory is out of the way.
367 pub fn start(&self, form: IncludeForm) -> usize {
368 match form {
369 IncludeForm::Quoted => 0,
370 IncludeForm::Angled => self.quote_end,
371 }
372 }
373
374 /// Finds `name`, starting at entry `from` of the search path.
375 ///
376 /// `relative_to` is the directory of the file doing the including, tried first for a
377 /// quoted include and ignored otherwise. Pass `None` for an `#include_next`, which is
378 /// defined as continuing past the directory the current file was found in and so must not
379 /// look next to it again. It is also ignored after [`SearchPath::split_quote_chain`], which
380 /// is what `-I-` asks for.
381 ///
382 /// An absolute name is opened directly and the search path is not consulted, which is
383 /// what every C compiler does and what a generated header with an absolute path needs.
384 pub fn resolve(
385 &self,
386 fs: &dyn FileSystem,
387 name: &str,
388 form: IncludeForm,
389 relative_to: Option<&Path>,
390 from: usize,
391 ) -> Option<Found> {
392 let as_path = Path::new(name);
393 if is_absolute(as_path) {
394 let bytes = open(fs, as_path).ok()?;
395 return Some(Found {
396 path: as_path.to_path_buf(),
397 name: name.to_owned(),
398 is_system: false,
399 next: 0,
400 bytes,
401 });
402 }
403 if form == IncludeForm::Quoted && self.searches_current_dir() {
404 if let Some(dir) = relative_to {
405 let path = dir.join(as_path);
406 if let Ok(bytes) = open(fs, &path) {
407 return Some(Found {
408 name: display(&path),
409 path,
410 is_system: false,
411 // The including file's own directory is not an entry on the path, so
412 // an `#include_next` from a header found there starts at the top of
413 // the path rather than one past a position that does not exist.
414 next: 0,
415 bytes,
416 });
417 }
418 }
419 }
420 for (at, dir) in self.dirs.iter().enumerate().skip(from) {
421 let path = dir.path.join(as_path);
422 if let Ok(bytes) = open(fs, &path) {
423 return Some(Found {
424 name: display(&path),
425 path,
426 is_system: dir.is_system,
427 next: at + 1,
428 bytes,
429 });
430 }
431 }
432 None
433 }
434
435 /// The directories a failed [`SearchPath::resolve`] with the same arguments looked in.
436 ///
437 /// `spec/05-preprocessor.md` section 5.7 makes printing this the required behaviour for a
438 /// failed include, because "file not found" without the list of places that were tried is
439 /// the diagnostic that wastes the most time in this part of the compiler.
440 pub fn tried(
441 &self,
442 name: &str,
443 form: IncludeForm,
444 relative_to: Option<&Path>,
445 from: usize,
446 ) -> Vec<PathBuf> {
447 if is_absolute(Path::new(name)) {
448 return Vec::new();
449 }
450 let mut list = Vec::new();
451 if form == IncludeForm::Quoted && self.searches_current_dir() {
452 if let Some(dir) = relative_to {
453 list.push(dir.to_path_buf());
454 }
455 }
456 list.extend(self.dirs.iter().skip(from).map(|d| d.path.clone()));
457 list
458 }
459}
460
461/// Reads a path the search produced, from the shipped headers first and the disk after.
462///
463/// This is the one place the compiler's own headers are handed out, and it is here rather
464/// than in a [`FileSystem`] implementation on purpose. They are not files, they belong to
465/// every implementation of the trait equally, and the only way to reach them is through a
466/// search path entry spelled [`runtime::DIR`], which no real directory can be spelled as.
467fn open(fs: &dyn FileSystem, path: &Path) -> io::Result<SourceBytes> {
468 match runtime::read(path) {
469 Some(bytes) => Ok(bytes),
470 None => fs.read(path),
471 }
472}
473
474/// A path as a string, lossily, because a diagnostic has to say something.
475fn display(path: &Path) -> String {
476 path.to_string_lossy().into_owned()
477}
478
479/// Whether an include name names a file outright rather than one to be searched for.
480///
481/// `Path::is_absolute` is false for `/usr/include/stdio.h` on Windows, because it has no
482/// drive letter. A C file that says `#include "/usr/include/stdio.h"` means a path from the
483/// root whatever host is compiling it, so a leading separator counts here as well.
484fn is_absolute(path: &Path) -> bool {
485 path.is_absolute() || path.has_root()
486}
487
488#[cfg(test)]
489mod tests {
490
491 #[test]
492 fn the_search_list_is_printed_the_way_gcc_prints_it() {
493 let mut path = SearchPath::new();
494 path.push_quote("q");
495 path.push_bracket("i");
496 path.push_system("/usr/include");
497 assert_eq!(
498 path.render_gcc(),
499 "#include \"...\" search starts here:\n q\n#include <...> search starts here:\n i\n /usr/include\nEnd of search list.\n"
500 );
501 }
502 use super::*;
503
504 fn fs_with(files: &[&str]) -> MemoryFileSystem {
505 let mut fs = MemoryFileSystem::new();
506 for f in files {
507 fs.insert(*f, format!("/* {f} */\n").into_bytes());
508 }
509 fs
510 }
511
512 fn text(found: &Found) -> String {
513 String::from_utf8_lossy(found.bytes.as_slice()).into_owned()
514 }
515
516 /// A path with forward slashes, because `Path::join` uses a backslash on Windows and
517 /// these tests are about the search order rather than about separators.
518 fn norm(path: &str) -> String {
519 path.replace('\\', "/")
520 }
521
522 #[test]
523 fn a_missing_file_is_not_found_rather_than_an_error() {
524 let fs = MemoryFileSystem::new();
525 let kind = fs.read(Path::new("/nope.h")).err().map(|e| e.kind());
526 assert_eq!(kind, Some(io::ErrorKind::NotFound));
527 assert!(fs.is_empty());
528 }
529
530 #[test]
531 fn quote_directories_are_invisible_to_an_angled_include() {
532 let fs = fs_with(&["/q/a.h", "/i/a.h"]);
533 let mut search = SearchPath::new();
534 search.push_quote("/q");
535 search.push_bracket("/i");
536
537 let quoted = search.resolve(&fs, "a.h", IncludeForm::Quoted, None, 0).unwrap();
538 assert_eq!(norm("ed.name), "/q/a.h");
539 let angled = search
540 .resolve(&fs, "a.h", IncludeForm::Angled, None, search.start(IncludeForm::Angled))
541 .unwrap();
542 assert_eq!(norm(&angled.name), "/i/a.h");
543 }
544
545 #[test]
546 fn the_including_files_own_directory_comes_first_for_a_quoted_include() {
547 let fs = fs_with(&["/src/a.h", "/i/a.h"]);
548 let mut search = SearchPath::new();
549 search.push_bracket("/i");
550 let here = Path::new("/src");
551
552 let quoted = search.resolve(&fs, "a.h", IncludeForm::Quoted, Some(here), 0).unwrap();
553 assert_eq!(norm("ed.name), "/src/a.h");
554 // An angled include does not look there, even though it was passed.
555 let angled = search.resolve(&fs, "a.h", IncludeForm::Angled, Some(here), 0).unwrap();
556 assert_eq!(norm(&angled.name), "/i/a.h");
557 }
558
559 #[test]
560 fn the_order_is_iquote_then_i_then_isystem_then_idirafter() {
561 let fs = fs_with(&["/after/a.h", "/sys/a.h", "/i/a.h", "/q/a.h"]);
562 let mut search = SearchPath::new();
563 // Pushed in an order that is not the search order, because a driver reads the command
564 // line left to right and the groups interleave.
565 search.push_after("/after");
566 search.push_system("/sys");
567 search.push_bracket("/i");
568 search.push_quote("/q");
569 let order: Vec<_> = search.dirs().iter().map(|d| norm(&d.path.to_string_lossy())).collect();
570 assert_eq!(order, ["/q", "/i", "/sys", "/after"]);
571
572 let found = search.resolve(&fs, "a.h", IncludeForm::Quoted, None, 0).unwrap();
573 assert_eq!(norm(&found.name), "/q/a.h");
574 assert_eq!(found.next, 1);
575 }
576
577 #[test]
578 fn a_directory_already_on_the_path_is_dropped_rather_than_searched_twice() {
579 let mut search = SearchPath::new();
580 search.push_system("/usr/local/include");
581 search.push_system("/usr/include");
582 search.push_system("/usr/local/include");
583 search.push_system("/usr/include/");
584 search.remove_duplicates();
585 let order: Vec<_> = search.dirs().iter().map(|d| norm(&d.path.to_string_lossy())).collect();
586 // The first spelling is the one kept, trailing separator and all, because it is the one
587 // a diagnostic will name and the two are the same directory.
588 assert_eq!(order, ["/usr/local/include", "/usr/include"]);
589 }
590
591 #[test]
592 fn a_duplicate_is_what_makes_include_next_find_the_file_it_is_standing_in() {
593 // The bug this exists for. A header found in the first `/usr/include` writes
594 // `#include_next <a.h>` meaning the copy below it, and with the directory on the path
595 // twice the copy below it is itself.
596 let fs = fs_with(&["/usr/include/a.h"]);
597 let mut search = SearchPath::new();
598 search.push_system("/usr/include");
599 search.push_system("/usr/include");
600 let first = search.resolve(&fs, "a.h", IncludeForm::Angled, None, 0).unwrap();
601 assert!(search.resolve(&fs, "a.h", IncludeForm::Angled, None, first.next).is_some());
602 search.remove_duplicates();
603 let first = search.resolve(&fs, "a.h", IncludeForm::Angled, None, 0).unwrap();
604 assert!(search.resolve(&fs, "a.h", IncludeForm::Angled, None, first.next).is_none());
605 }
606
607 #[test]
608 fn a_bracket_directory_that_names_a_system_one_is_the_entry_that_goes() {
609 // GCC's documented rule. Keeping the `-I` would move a system directory up the order
610 // and take the system treatment off every header in it.
611 let fs = fs_with(&["/usr/include/a.h"]);
612 let mut search = SearchPath::new();
613 search.push_bracket("/usr/include");
614 search.push_bracket("/i");
615 search.push_system("/usr/include");
616 search.remove_duplicates();
617 let order: Vec<_> = search.dirs().iter().map(|d| norm(&d.path.to_string_lossy())).collect();
618 assert_eq!(order, ["/i", "/usr/include"]);
619 assert!(search.resolve(&fs, "a.h", IncludeForm::Angled, None, 0).unwrap().is_system);
620 }
621
622 #[test]
623 fn dropping_an_entry_keeps_the_group_boundaries_where_the_groups_are() {
624 let fs = fs_with(&["/q/a.h", "/i/a.h"]);
625 let mut search = SearchPath::new();
626 search.push_quote("/q");
627 search.push_quote("/q");
628 search.push_bracket("/i");
629 search.push_bracket("/i");
630 search.remove_duplicates();
631 // An angled include still skips the one `-iquote` entry left rather than a stale two.
632 let at = search.start(IncludeForm::Angled);
633 let found = search.resolve(&fs, "a.h", IncludeForm::Angled, None, at).unwrap();
634 assert_eq!(norm(&found.name), "/i/a.h");
635 }
636
637 #[test]
638 fn splitting_the_chain_takes_the_bracket_directories_out_of_an_angled_search() {
639 let fs = fs_with(&["/i/a.h", "/sys/a.h"]);
640 let mut search = SearchPath::new();
641 search.push_bracket("/i");
642 search.push_system("/sys");
643 search.split_quote_chain();
644
645 let quoted = search.resolve(&fs, "a.h", IncludeForm::Quoted, None, 0).unwrap();
646 assert_eq!(norm("ed.name), "/i/a.h");
647 let at = search.start(IncludeForm::Angled);
648 let angled = search.resolve(&fs, "a.h", IncludeForm::Angled, None, at).unwrap();
649 assert_eq!(norm(&angled.name), "/sys/a.h");
650 }
651
652 #[test]
653 fn a_quote_directory_given_before_the_split_lands_after_the_bracket_ones() {
654 // `-Iinc1 -iquote inc2 -I-`, which GCC answers with a quoted chain of `inc1` then
655 // `inc2`, because it holds `-iquote` back until every `-I` has been dealt with.
656 let mut search = SearchPath::new();
657 search.push_bracket("/inc1");
658 search.push_quote("/inc2");
659 search.push_system("/sys");
660 search.split_quote_chain();
661 let order: Vec<_> = search.dirs().iter().map(|d| norm(&d.path.to_string_lossy())).collect();
662 assert_eq!(order, ["/inc1", "/inc2", "/sys"]);
663 assert_eq!(search.start(IncludeForm::Angled), 2);
664 }
665
666 #[test]
667 fn a_bracket_directory_given_after_the_split_is_visible_to_both_chains() {
668 // `-Iinc1 -I- -Iinc2`. `inc1` is quoted only and `inc2` is an ordinary `-I`, which a
669 // quoted include reaches as well because the quoted chain runs on into the bracket one.
670 let fs = fs_with(&["/inc1/a.h", "/inc2/b.h"]);
671 let mut search = SearchPath::new();
672 search.push_bracket("/inc1");
673 search.split_quote_chain();
674 search.push_bracket("/inc2");
675
676 let at = search.start(IncludeForm::Angled);
677 assert!(search.resolve(&fs, "a.h", IncludeForm::Angled, None, at).is_none());
678 assert!(search.resolve(&fs, "b.h", IncludeForm::Angled, None, at).is_some());
679 assert!(search.resolve(&fs, "a.h", IncludeForm::Quoted, None, 0).is_some());
680 assert!(search.resolve(&fs, "b.h", IncludeForm::Quoted, None, 0).is_some());
681 }
682
683 #[test]
684 fn splitting_the_chain_stops_a_quoted_include_looking_next_to_the_file_that_wrote_it() {
685 let fs = fs_with(&["/src/a.h"]);
686 let mut search = SearchPath::new();
687 let here = Path::new("/src");
688 assert!(search.resolve(&fs, "a.h", IncludeForm::Quoted, Some(here), 0).is_some());
689 search.split_quote_chain();
690 assert!(search.resolve(&fs, "a.h", IncludeForm::Quoted, Some(here), 0).is_none());
691 // And the directory is not named among the places that were tried, since it was not one.
692 assert!(search.tried("a.h", IncludeForm::Quoted, Some(here), 0).is_empty());
693 }
694
695 #[test]
696 fn a_system_directory_marks_what_it_holds_as_a_system_header() {
697 let fs = fs_with(&["/i/a.h", "/sys/b.h", "/after/c.h"]);
698 let mut search = SearchPath::new();
699 search.push_bracket("/i");
700 search.push_system("/sys");
701 search.push_after("/after");
702 let get = |n| search.resolve(&fs, n, IncludeForm::Angled, None, 0).unwrap();
703 assert!(!get("a.h").is_system);
704 assert!(get("b.h").is_system);
705 assert!(get("c.h").is_system);
706 }
707
708 #[test]
709 fn include_next_continues_past_the_directory_the_current_file_came_from() {
710 let fs = fs_with(&["/a/limits.h", "/b/limits.h", "/c/limits.h"]);
711 let mut search = SearchPath::new();
712 search.push_bracket("/a");
713 search.push_bracket("/b");
714 search.push_bracket("/c");
715
716 let first = search.resolve(&fs, "limits.h", IncludeForm::Angled, None, 0).unwrap();
717 assert_eq!(norm(&first.name), "/a/limits.h");
718 let second =
719 search.resolve(&fs, "limits.h", IncludeForm::Angled, None, first.next).unwrap();
720 assert_eq!(norm(&second.name), "/b/limits.h");
721 let third =
722 search.resolve(&fs, "limits.h", IncludeForm::Angled, None, second.next).unwrap();
723 assert_eq!(norm(&third.name), "/c/limits.h");
724 assert!(search.resolve(&fs, "limits.h", IncludeForm::Angled, None, third.next).is_none());
725 }
726
727 #[test]
728 fn a_name_with_a_directory_in_it_is_joined_onto_each_entry() {
729 let fs = fs_with(&["/i/sys/types.h"]);
730 let mut search = SearchPath::new();
731 search.push_bracket("/i");
732 let found = search.resolve(&fs, "sys/types.h", IncludeForm::Angled, None, 0).unwrap();
733 assert_eq!(norm(&found.name), "/i/sys/types.h");
734 assert_eq!(text(&found), "/* /i/sys/types.h */\n");
735 }
736
737 #[test]
738 fn an_absolute_name_ignores_the_search_path() {
739 let fs = fs_with(&["/gen/config.h", "/i/gen/config.h"]);
740 let mut search = SearchPath::new();
741 search.push_bracket("/i");
742 let found = search.resolve(&fs, "/gen/config.h", IncludeForm::Angled, None, 0).unwrap();
743 assert_eq!(norm(&found.name), "/gen/config.h");
744 assert!(search.tried("/gen/config.h", IncludeForm::Angled, None, 0).is_empty());
745 }
746
747 #[test]
748 fn the_list_of_places_tried_is_the_list_that_was_searched() {
749 let fs = MemoryFileSystem::new();
750 let mut search = SearchPath::new();
751 search.push_quote("/q");
752 search.push_bracket("/i");
753 search.push_system("/sys");
754 let here = Path::new("/src");
755
756 assert!(search.resolve(&fs, "a.h", IncludeForm::Quoted, Some(here), 0).is_none());
757 let tried = search.tried("a.h", IncludeForm::Quoted, Some(here), 0);
758 let tried: Vec<_> = tried.iter().map(|p| norm(&p.to_string_lossy())).collect();
759 assert_eq!(tried, ["/src", "/q", "/i", "/sys"]);
760
761 let start = search.start(IncludeForm::Angled);
762 let tried = search.tried("a.h", IncludeForm::Angled, Some(here), start);
763 let tried: Vec<_> = tried.iter().map(|p| norm(&p.to_string_lossy())).collect();
764 assert_eq!(tried, ["/i", "/sys"]);
765 }
766
767 #[test]
768 fn a_header_found_next_to_its_includer_does_not_skip_the_whole_path_afterwards() {
769 // `at` for a file found beside its includer has to leave `at + 1` at the top of the
770 // path, because the directory it was found in is not on the path at all.
771 let fs = fs_with(&["/src/a.h", "/i/b.h"]);
772 let mut search = SearchPath::new();
773 search.push_bracket("/i");
774 let found =
775 search.resolve(&fs, "a.h", IncludeForm::Quoted, Some(Path::new("/src")), 0).unwrap();
776 assert_eq!(found.next, 0);
777 let next = search.resolve(&fs, "b.h", IncludeForm::Angled, None, found.next).unwrap();
778 assert_eq!(norm(&next.name), "/i/b.h");
779 }
780}