use std::collections::BTreeMap;
use std::fmt;
use std::io;
use std::path::{Path, PathBuf};
use rucc_diag::SourceBytes;
pub trait FileSystem: fmt::Debug + Send + Sync {
fn read(&self, path: &Path) -> io::Result<SourceBytes>;
}
#[derive(Debug, Default)]
pub struct MemoryFileSystem {
files: BTreeMap<PathBuf, SourceBytes>,
}
impl MemoryFileSystem {
pub fn new() -> MemoryFileSystem {
MemoryFileSystem::default()
}
pub fn insert(
&mut self,
path: impl Into<PathBuf>,
contents: impl AsRef<[u8]> + Send + Sync + 'static,
) {
self.files.insert(path.into(), SourceBytes::new(contents));
}
pub fn len(&self) -> usize {
self.files.len()
}
pub fn is_empty(&self) -> bool {
self.files.is_empty()
}
}
impl FileSystem for MemoryFileSystem {
fn read(&self, path: &Path) -> io::Result<SourceBytes> {
self.files
.get(path)
.cloned()
.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "no such file"))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum IncludeForm {
Quoted,
Angled,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Dir {
pub path: PathBuf,
pub is_system: bool,
}
#[derive(Debug, Clone)]
pub struct Found {
pub path: PathBuf,
pub name: String,
pub is_system: bool,
pub next: usize,
pub bytes: SourceBytes,
}
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct SearchPath {
dirs: Vec<Dir>,
quote_end: usize,
bracket_end: usize,
system_end: usize,
}
impl SearchPath {
pub fn new() -> SearchPath {
SearchPath::default()
}
pub fn push_quote(&mut self, dir: impl Into<PathBuf>) {
let at = self.quote_end;
self.insert(at, dir.into(), false);
self.quote_end += 1;
self.bracket_end += 1;
self.system_end += 1;
}
pub fn push_bracket(&mut self, dir: impl Into<PathBuf>) {
let at = self.bracket_end;
self.insert(at, dir.into(), false);
self.bracket_end += 1;
self.system_end += 1;
}
pub fn push_system(&mut self, dir: impl Into<PathBuf>) {
let at = self.system_end;
self.insert(at, dir.into(), true);
self.system_end += 1;
}
pub fn push_after(&mut self, dir: impl Into<PathBuf>) {
let at = self.dirs.len();
self.insert(at, dir.into(), true);
}
fn insert(&mut self, at: usize, path: PathBuf, is_system: bool) {
self.dirs.insert(at, Dir { path, is_system });
}
pub fn dirs(&self) -> &[Dir] {
&self.dirs
}
pub fn start(&self, form: IncludeForm) -> usize {
match form {
IncludeForm::Quoted => 0,
IncludeForm::Angled => self.quote_end,
}
}
pub fn resolve(
&self,
fs: &dyn FileSystem,
name: &str,
form: IncludeForm,
relative_to: Option<&Path>,
from: usize,
) -> Option<Found> {
let as_path = Path::new(name);
if is_absolute(as_path) {
let bytes = fs.read(as_path).ok()?;
return Some(Found {
path: as_path.to_path_buf(),
name: name.to_owned(),
is_system: false,
next: 0,
bytes,
});
}
if form == IncludeForm::Quoted {
if let Some(dir) = relative_to {
let path = dir.join(as_path);
if let Ok(bytes) = fs.read(&path) {
return Some(Found {
name: display(&path),
path,
is_system: false,
next: 0,
bytes,
});
}
}
}
for (at, dir) in self.dirs.iter().enumerate().skip(from) {
let path = dir.path.join(as_path);
if let Ok(bytes) = fs.read(&path) {
return Some(Found {
name: display(&path),
path,
is_system: dir.is_system,
next: at + 1,
bytes,
});
}
}
None
}
pub fn tried(
&self,
name: &str,
form: IncludeForm,
relative_to: Option<&Path>,
from: usize,
) -> Vec<PathBuf> {
if is_absolute(Path::new(name)) {
return Vec::new();
}
let mut list = Vec::new();
if form == IncludeForm::Quoted {
if let Some(dir) = relative_to {
list.push(dir.to_path_buf());
}
}
list.extend(self.dirs.iter().skip(from).map(|d| d.path.clone()));
list
}
}
fn display(path: &Path) -> String {
path.to_string_lossy().into_owned()
}
fn is_absolute(path: &Path) -> bool {
path.is_absolute() || path.has_root()
}
#[cfg(test)]
mod tests {
use super::*;
fn fs_with(files: &[&str]) -> MemoryFileSystem {
let mut fs = MemoryFileSystem::new();
for f in files {
fs.insert(*f, format!("/* {f} */\n").into_bytes());
}
fs
}
fn text(found: &Found) -> String {
String::from_utf8_lossy(found.bytes.as_slice()).into_owned()
}
fn norm(path: &str) -> String {
path.replace('\\', "/")
}
#[test]
fn a_missing_file_is_not_found_rather_than_an_error() {
let fs = MemoryFileSystem::new();
let kind = fs.read(Path::new("/nope.h")).err().map(|e| e.kind());
assert_eq!(kind, Some(io::ErrorKind::NotFound));
assert!(fs.is_empty());
}
#[test]
fn quote_directories_are_invisible_to_an_angled_include() {
let fs = fs_with(&["/q/a.h", "/i/a.h"]);
let mut search = SearchPath::new();
search.push_quote("/q");
search.push_bracket("/i");
let quoted = search.resolve(&fs, "a.h", IncludeForm::Quoted, None, 0).unwrap();
assert_eq!(norm("ed.name), "/q/a.h");
let angled = search
.resolve(&fs, "a.h", IncludeForm::Angled, None, search.start(IncludeForm::Angled))
.unwrap();
assert_eq!(norm(&angled.name), "/i/a.h");
}
#[test]
fn the_including_files_own_directory_comes_first_for_a_quoted_include() {
let fs = fs_with(&["/src/a.h", "/i/a.h"]);
let mut search = SearchPath::new();
search.push_bracket("/i");
let here = Path::new("/src");
let quoted = search.resolve(&fs, "a.h", IncludeForm::Quoted, Some(here), 0).unwrap();
assert_eq!(norm("ed.name), "/src/a.h");
let angled = search.resolve(&fs, "a.h", IncludeForm::Angled, Some(here), 0).unwrap();
assert_eq!(norm(&angled.name), "/i/a.h");
}
#[test]
fn the_order_is_iquote_then_i_then_isystem_then_idirafter() {
let fs = fs_with(&["/after/a.h", "/sys/a.h", "/i/a.h", "/q/a.h"]);
let mut search = SearchPath::new();
search.push_after("/after");
search.push_system("/sys");
search.push_bracket("/i");
search.push_quote("/q");
let order: Vec<_> = search.dirs().iter().map(|d| norm(&d.path.to_string_lossy())).collect();
assert_eq!(order, ["/q", "/i", "/sys", "/after"]);
let found = search.resolve(&fs, "a.h", IncludeForm::Quoted, None, 0).unwrap();
assert_eq!(norm(&found.name), "/q/a.h");
assert_eq!(found.next, 1);
}
#[test]
fn a_system_directory_marks_what_it_holds_as_a_system_header() {
let fs = fs_with(&["/i/a.h", "/sys/b.h", "/after/c.h"]);
let mut search = SearchPath::new();
search.push_bracket("/i");
search.push_system("/sys");
search.push_after("/after");
let get = |n| search.resolve(&fs, n, IncludeForm::Angled, None, 0).unwrap();
assert!(!get("a.h").is_system);
assert!(get("b.h").is_system);
assert!(get("c.h").is_system);
}
#[test]
fn include_next_continues_past_the_directory_the_current_file_came_from() {
let fs = fs_with(&["/a/limits.h", "/b/limits.h", "/c/limits.h"]);
let mut search = SearchPath::new();
search.push_bracket("/a");
search.push_bracket("/b");
search.push_bracket("/c");
let first = search.resolve(&fs, "limits.h", IncludeForm::Angled, None, 0).unwrap();
assert_eq!(norm(&first.name), "/a/limits.h");
let second =
search.resolve(&fs, "limits.h", IncludeForm::Angled, None, first.next).unwrap();
assert_eq!(norm(&second.name), "/b/limits.h");
let third =
search.resolve(&fs, "limits.h", IncludeForm::Angled, None, second.next).unwrap();
assert_eq!(norm(&third.name), "/c/limits.h");
assert!(search.resolve(&fs, "limits.h", IncludeForm::Angled, None, third.next).is_none());
}
#[test]
fn a_name_with_a_directory_in_it_is_joined_onto_each_entry() {
let fs = fs_with(&["/i/sys/types.h"]);
let mut search = SearchPath::new();
search.push_bracket("/i");
let found = search.resolve(&fs, "sys/types.h", IncludeForm::Angled, None, 0).unwrap();
assert_eq!(norm(&found.name), "/i/sys/types.h");
assert_eq!(text(&found), "/* /i/sys/types.h */\n");
}
#[test]
fn an_absolute_name_ignores_the_search_path() {
let fs = fs_with(&["/gen/config.h", "/i/gen/config.h"]);
let mut search = SearchPath::new();
search.push_bracket("/i");
let found = search.resolve(&fs, "/gen/config.h", IncludeForm::Angled, None, 0).unwrap();
assert_eq!(norm(&found.name), "/gen/config.h");
assert!(search.tried("/gen/config.h", IncludeForm::Angled, None, 0).is_empty());
}
#[test]
fn the_list_of_places_tried_is_the_list_that_was_searched() {
let fs = MemoryFileSystem::new();
let mut search = SearchPath::new();
search.push_quote("/q");
search.push_bracket("/i");
search.push_system("/sys");
let here = Path::new("/src");
assert!(search.resolve(&fs, "a.h", IncludeForm::Quoted, Some(here), 0).is_none());
let tried = search.tried("a.h", IncludeForm::Quoted, Some(here), 0);
let tried: Vec<_> = tried.iter().map(|p| norm(&p.to_string_lossy())).collect();
assert_eq!(tried, ["/src", "/q", "/i", "/sys"]);
let start = search.start(IncludeForm::Angled);
let tried = search.tried("a.h", IncludeForm::Angled, Some(here), start);
let tried: Vec<_> = tried.iter().map(|p| norm(&p.to_string_lossy())).collect();
assert_eq!(tried, ["/i", "/sys"]);
}
#[test]
fn a_header_found_next_to_its_includer_does_not_skip_the_whole_path_afterwards() {
let fs = fs_with(&["/src/a.h", "/i/b.h"]);
let mut search = SearchPath::new();
search.push_bracket("/i");
let found =
search.resolve(&fs, "a.h", IncludeForm::Quoted, Some(Path::new("/src")), 0).unwrap();
assert_eq!(found.next, 0);
let next = search.resolve(&fs, "b.h", IncludeForm::Angled, None, found.next).unwrap();
assert_eq!(norm(&next.name), "/i/b.h");
}
}