Skip to main content

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/// The one method is deliberate. Everything the preprocessor wants to know about a file, up
37/// to and including whether it exists, is answered by trying to read it, and an interface
38/// with a separate `exists` invites the race where the answer changes 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
49/// A file system held in memory, for tests and for embedding the compiler.
50#[derive(Debug, Default)]
51pub struct MemoryFileSystem {
52    files: BTreeMap<PathBuf, SourceBytes>,
53}
54
55impl MemoryFileSystem {
56    /// An empty file system.
57    pub fn new() -> MemoryFileSystem {
58        MemoryFileSystem::default()
59    }
60
61    /// Adds a file, replacing any file already at that path.
62    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    /// How many files it holds.
71    pub fn len(&self) -> usize {
72        self.files.len()
73    }
74
75    /// Whether it holds nothing.
76    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/// Which spelling an `#include` used.
91#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
92pub enum IncludeForm {
93    /// `#include "local.h"`, which looks next to the including file first.
94    Quoted,
95    /// `#include <stdio.h>`, which does not.
96    Angled,
97}
98
99/// One directory on the search path.
100#[derive(Debug, Clone, PartialEq, Eq)]
101pub struct Dir {
102    /// The directory, as the user spelled it. Not canonicalised, because a diagnostic that
103    /// says `../include/foo.h` is more use than one naming a path the user never typed.
104    pub path: PathBuf,
105    /// Whether headers found here are system headers, which suppresses warnings in them and
106    /// sets the `3` flag on a `-E` line marker.
107    pub is_system: bool,
108}
109
110/// Whether two spellings name the same directory, as far as text can say.
111///
112/// `Path::components` is what does the work: it drops a trailing separator and the `.` inside
113/// a path, so `/usr/include/` and `/usr/include` are one directory. `..` is left alone, since
114/// a component above a symlink does not go where reading the path suggests.
115fn 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/// A header that was found.
123#[derive(Debug, Clone)]
124pub struct Found {
125    /// The path to open, which is the directory joined with the name as written.
126    pub path: PathBuf,
127    /// That path as a string, for the source map and for diagnostics.
128    pub name: String,
129    /// Whether it came from a system directory.
130    pub is_system: bool,
131    /// Where an `#include_next` written in this file should start looking.
132    ///
133    /// One past the entry this header came from, or zero for a header found next to the file
134    /// that included it, because that directory is not on the path and there is nothing to
135    /// continue past. Carrying the answer rather than the position is what keeps the two
136    /// cases from being confused at the call site.
137    pub next: usize,
138    /// The contents.
139    pub bytes: SourceBytes,
140}
141
142/// The directories a header is looked for in, in order.
143///
144/// GCC's order, because a different one produces header shadowing bugs that are miserable to
145/// diagnose: `-iquote` first and only for a quoted include, then `-I`, then `-isystem`, then
146/// the configured system directories, then `-idirafter`. The directory of the including file
147/// comes before all of it for a quoted include, and it is not part of the numbered list
148/// because `#include_next` must not be able to land back on it.
149#[derive(Debug, Default, Clone, PartialEq, Eq)]
150pub struct SearchPath {
151    dirs: Vec<Dir>,
152    /// Where the `-I` directories begin, which is where an angled include starts looking.
153    quote_end: usize,
154    /// Where the `-isystem` and configured system directories begin.
155    bracket_end: usize,
156    /// Where the `-idirafter` directories begin.
157    system_end: usize,
158}
159
160impl SearchPath {
161    /// An empty search path.
162    pub fn new() -> SearchPath {
163        SearchPath::default()
164    }
165
166    /// Adds a `-iquote` directory, searched only for a quoted include.
167    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    /// Adds a `-I` directory.
176    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    /// Adds a `-isystem` directory, or one of the target's configured system directories.
184    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    /// Adds a `-idirafter` directory, which is searched after everything else.
191    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    /// Drops the directories that are already on the path, the way GCC does.
201    ///
202    /// A duplicate is not a harmless extra entry that costs one failed open. It changes what
203    /// `#include_next` means, which is defined as continuing past the directory the current
204    /// file came from: a header found in the first `/usr/include` writes `#include_next
205    /// <stdint.h>` meaning "the one below me" and finds itself in the second, and a header set
206    /// that ends in a fixed point of its own is one that includes itself forever or answers
207    /// `__has_include_next` yes where the compiler it was written for said no. It shows up as
208    /// soon as somebody passes the system directories on the command line, which the compat
209    /// harness does deliberately and a build system does by accident.
210    ///
211    /// A `-I` that names a system directory loses to the system entry rather than the other way
212    /// round, and that is GCC's rule and is documented as one: keeping the earlier one would
213    /// move a system directory up the order and take the system treatment off the headers in
214    /// it, so the `-I` is the one that goes.
215    ///
216    /// Two names for one directory are two directories here, where GCC compares the device and
217    /// the inode and sees through a symlink. That wants a file system that can answer the
218    /// question and this one deliberately only reads.
219    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    /// Every directory, in search order.
251    pub fn dirs(&self) -> &[Dir] {
252        &self.dirs
253    }
254
255    /// The first entry an include of this form looks at.
256    ///
257    /// An angled include skips the `-iquote` directories, which is the only difference
258    /// between the two chains once the including file's own directory is out of the way.
259    pub fn start(&self, form: IncludeForm) -> usize {
260        match form {
261            IncludeForm::Quoted => 0,
262            IncludeForm::Angled => self.quote_end,
263        }
264    }
265
266    /// Finds `name`, starting at entry `from` of the search path.
267    ///
268    /// `relative_to` is the directory of the file doing the including, tried first for a
269    /// quoted include and ignored otherwise. Pass `None` for an `#include_next`, which is
270    /// defined as continuing past the directory the current file was found in and so must not
271    /// look next to it again.
272    ///
273    /// An absolute name is opened directly and the search path is not consulted, which is
274    /// what every C compiler does and what a generated header with an absolute path needs.
275    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                        // The including file's own directory is not an entry on the path, so
303                        // an `#include_next` from a header found there starts at the top of
304                        // the path rather than one past a position that does not exist.
305                        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    /// The directories a failed [`SearchPath::resolve`] with the same arguments looked in.
327    ///
328    /// `spec/05-preprocessor.md` section 5.7 makes printing this the required behaviour for a
329    /// failed include, because "file not found" without the list of places that were tried is
330    /// the diagnostic that wastes the most time in this part of the compiler.
331    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
352/// Reads a path the search produced, from the shipped headers first and the disk after.
353///
354/// This is the one place the compiler's own headers are handed out, and it is here rather
355/// than in a [`FileSystem`] implementation on purpose. They are not files, they belong to
356/// every implementation of the trait equally, and the only way to reach them is through a
357/// search path entry spelled [`runtime::DIR`], which no real directory can be spelled as.
358fn 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
365/// A path as a string, lossily, because a diagnostic has to say something.
366fn display(path: &Path) -> String {
367    path.to_string_lossy().into_owned()
368}
369
370/// Whether an include name names a file outright rather than one to be searched for.
371///
372/// `Path::is_absolute` is false for `/usr/include/stdio.h` on Windows, because it has no
373/// drive letter. A C file that says `#include "/usr/include/stdio.h"` means a path from the
374/// root whatever host is compiling it, so a leading separator counts here as well.
375fn 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    /// A path with forward slashes, because `Path::join` uses a backslash on Windows and
396    /// these tests are about the search order rather than about separators.
397    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(&quoted.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(&quoted.name), "/src/a.h");
433        // An angled include does not look there, even though it was passed.
434        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        // Pushed in an order that is not the search order, because a driver reads the command
443        // line left to right and the groups interleave.
444        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        // The first spelling is the one kept, trailing separator and all, because it is the one
466        // a diagnostic will name and the two are the same directory.
467        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        // The bug this exists for. A header found in the first `/usr/include` writes
473        // `#include_next <a.h>` meaning the copy below it, and with the directory on the path
474        // twice the copy below it is itself.
475        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        // GCC's documented rule. Keeping the `-I` would move a system directory up the order
489        // and take the system treatment off every header in it.
490        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        // An angled include still skips the one `-iquote` entry left rather than a stale two.
511        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        // `at` for a file found beside its includer has to leave `at + 1` at the top of the
591        // path, because the directory it was found in is not on the path at all.
592        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}