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
110#[derive(Debug, Clone)]
112pub struct Found {
113 pub path: PathBuf,
115 pub name: String,
117 pub is_system: bool,
119 pub next: usize,
126 pub bytes: SourceBytes,
128}
129
130#[derive(Debug, Default, Clone, PartialEq, Eq)]
138pub struct SearchPath {
139 dirs: Vec<Dir>,
140 quote_end: usize,
142 bracket_end: usize,
144 system_end: usize,
146}
147
148impl SearchPath {
149 pub fn new() -> SearchPath {
151 SearchPath::default()
152 }
153
154 pub fn push_quote(&mut self, dir: impl Into<PathBuf>) {
156 let at = self.quote_end;
157 self.insert(at, dir.into(), false);
158 self.quote_end += 1;
159 self.bracket_end += 1;
160 self.system_end += 1;
161 }
162
163 pub fn push_bracket(&mut self, dir: impl Into<PathBuf>) {
165 let at = self.bracket_end;
166 self.insert(at, dir.into(), false);
167 self.bracket_end += 1;
168 self.system_end += 1;
169 }
170
171 pub fn push_system(&mut self, dir: impl Into<PathBuf>) {
173 let at = self.system_end;
174 self.insert(at, dir.into(), true);
175 self.system_end += 1;
176 }
177
178 pub fn push_after(&mut self, dir: impl Into<PathBuf>) {
180 let at = self.dirs.len();
181 self.insert(at, dir.into(), true);
182 }
183
184 fn insert(&mut self, at: usize, path: PathBuf, is_system: bool) {
185 self.dirs.insert(at, Dir { path, is_system });
186 }
187
188 pub fn dirs(&self) -> &[Dir] {
190 &self.dirs
191 }
192
193 pub fn start(&self, form: IncludeForm) -> usize {
198 match form {
199 IncludeForm::Quoted => 0,
200 IncludeForm::Angled => self.quote_end,
201 }
202 }
203
204 pub fn resolve(
214 &self,
215 fs: &dyn FileSystem,
216 name: &str,
217 form: IncludeForm,
218 relative_to: Option<&Path>,
219 from: usize,
220 ) -> Option<Found> {
221 let as_path = Path::new(name);
222 if is_absolute(as_path) {
223 let bytes = open(fs, as_path).ok()?;
224 return Some(Found {
225 path: as_path.to_path_buf(),
226 name: name.to_owned(),
227 is_system: false,
228 next: 0,
229 bytes,
230 });
231 }
232 if form == IncludeForm::Quoted {
233 if let Some(dir) = relative_to {
234 let path = dir.join(as_path);
235 if let Ok(bytes) = open(fs, &path) {
236 return Some(Found {
237 name: display(&path),
238 path,
239 is_system: false,
240 next: 0,
244 bytes,
245 });
246 }
247 }
248 }
249 for (at, dir) in self.dirs.iter().enumerate().skip(from) {
250 let path = dir.path.join(as_path);
251 if let Ok(bytes) = open(fs, &path) {
252 return Some(Found {
253 name: display(&path),
254 path,
255 is_system: dir.is_system,
256 next: at + 1,
257 bytes,
258 });
259 }
260 }
261 None
262 }
263
264 pub fn tried(
270 &self,
271 name: &str,
272 form: IncludeForm,
273 relative_to: Option<&Path>,
274 from: usize,
275 ) -> Vec<PathBuf> {
276 if is_absolute(Path::new(name)) {
277 return Vec::new();
278 }
279 let mut list = Vec::new();
280 if form == IncludeForm::Quoted {
281 if let Some(dir) = relative_to {
282 list.push(dir.to_path_buf());
283 }
284 }
285 list.extend(self.dirs.iter().skip(from).map(|d| d.path.clone()));
286 list
287 }
288}
289
290fn open(fs: &dyn FileSystem, path: &Path) -> io::Result<SourceBytes> {
297 match runtime::read(path) {
298 Some(bytes) => Ok(bytes),
299 None => fs.read(path),
300 }
301}
302
303fn display(path: &Path) -> String {
305 path.to_string_lossy().into_owned()
306}
307
308fn is_absolute(path: &Path) -> bool {
314 path.is_absolute() || path.has_root()
315}
316
317#[cfg(test)]
318mod tests {
319 use super::*;
320
321 fn fs_with(files: &[&str]) -> MemoryFileSystem {
322 let mut fs = MemoryFileSystem::new();
323 for f in files {
324 fs.insert(*f, format!("/* {f} */\n").into_bytes());
325 }
326 fs
327 }
328
329 fn text(found: &Found) -> String {
330 String::from_utf8_lossy(found.bytes.as_slice()).into_owned()
331 }
332
333 fn norm(path: &str) -> String {
336 path.replace('\\', "/")
337 }
338
339 #[test]
340 fn a_missing_file_is_not_found_rather_than_an_error() {
341 let fs = MemoryFileSystem::new();
342 let kind = fs.read(Path::new("/nope.h")).err().map(|e| e.kind());
343 assert_eq!(kind, Some(io::ErrorKind::NotFound));
344 assert!(fs.is_empty());
345 }
346
347 #[test]
348 fn quote_directories_are_invisible_to_an_angled_include() {
349 let fs = fs_with(&["/q/a.h", "/i/a.h"]);
350 let mut search = SearchPath::new();
351 search.push_quote("/q");
352 search.push_bracket("/i");
353
354 let quoted = search.resolve(&fs, "a.h", IncludeForm::Quoted, None, 0).unwrap();
355 assert_eq!(norm("ed.name), "/q/a.h");
356 let angled = search
357 .resolve(&fs, "a.h", IncludeForm::Angled, None, search.start(IncludeForm::Angled))
358 .unwrap();
359 assert_eq!(norm(&angled.name), "/i/a.h");
360 }
361
362 #[test]
363 fn the_including_files_own_directory_comes_first_for_a_quoted_include() {
364 let fs = fs_with(&["/src/a.h", "/i/a.h"]);
365 let mut search = SearchPath::new();
366 search.push_bracket("/i");
367 let here = Path::new("/src");
368
369 let quoted = search.resolve(&fs, "a.h", IncludeForm::Quoted, Some(here), 0).unwrap();
370 assert_eq!(norm("ed.name), "/src/a.h");
371 let angled = search.resolve(&fs, "a.h", IncludeForm::Angled, Some(here), 0).unwrap();
373 assert_eq!(norm(&angled.name), "/i/a.h");
374 }
375
376 #[test]
377 fn the_order_is_iquote_then_i_then_isystem_then_idirafter() {
378 let fs = fs_with(&["/after/a.h", "/sys/a.h", "/i/a.h", "/q/a.h"]);
379 let mut search = SearchPath::new();
380 search.push_after("/after");
383 search.push_system("/sys");
384 search.push_bracket("/i");
385 search.push_quote("/q");
386 let order: Vec<_> = search.dirs().iter().map(|d| norm(&d.path.to_string_lossy())).collect();
387 assert_eq!(order, ["/q", "/i", "/sys", "/after"]);
388
389 let found = search.resolve(&fs, "a.h", IncludeForm::Quoted, None, 0).unwrap();
390 assert_eq!(norm(&found.name), "/q/a.h");
391 assert_eq!(found.next, 1);
392 }
393
394 #[test]
395 fn a_system_directory_marks_what_it_holds_as_a_system_header() {
396 let fs = fs_with(&["/i/a.h", "/sys/b.h", "/after/c.h"]);
397 let mut search = SearchPath::new();
398 search.push_bracket("/i");
399 search.push_system("/sys");
400 search.push_after("/after");
401 let get = |n| search.resolve(&fs, n, IncludeForm::Angled, None, 0).unwrap();
402 assert!(!get("a.h").is_system);
403 assert!(get("b.h").is_system);
404 assert!(get("c.h").is_system);
405 }
406
407 #[test]
408 fn include_next_continues_past_the_directory_the_current_file_came_from() {
409 let fs = fs_with(&["/a/limits.h", "/b/limits.h", "/c/limits.h"]);
410 let mut search = SearchPath::new();
411 search.push_bracket("/a");
412 search.push_bracket("/b");
413 search.push_bracket("/c");
414
415 let first = search.resolve(&fs, "limits.h", IncludeForm::Angled, None, 0).unwrap();
416 assert_eq!(norm(&first.name), "/a/limits.h");
417 let second =
418 search.resolve(&fs, "limits.h", IncludeForm::Angled, None, first.next).unwrap();
419 assert_eq!(norm(&second.name), "/b/limits.h");
420 let third =
421 search.resolve(&fs, "limits.h", IncludeForm::Angled, None, second.next).unwrap();
422 assert_eq!(norm(&third.name), "/c/limits.h");
423 assert!(search.resolve(&fs, "limits.h", IncludeForm::Angled, None, third.next).is_none());
424 }
425
426 #[test]
427 fn a_name_with_a_directory_in_it_is_joined_onto_each_entry() {
428 let fs = fs_with(&["/i/sys/types.h"]);
429 let mut search = SearchPath::new();
430 search.push_bracket("/i");
431 let found = search.resolve(&fs, "sys/types.h", IncludeForm::Angled, None, 0).unwrap();
432 assert_eq!(norm(&found.name), "/i/sys/types.h");
433 assert_eq!(text(&found), "/* /i/sys/types.h */\n");
434 }
435
436 #[test]
437 fn an_absolute_name_ignores_the_search_path() {
438 let fs = fs_with(&["/gen/config.h", "/i/gen/config.h"]);
439 let mut search = SearchPath::new();
440 search.push_bracket("/i");
441 let found = search.resolve(&fs, "/gen/config.h", IncludeForm::Angled, None, 0).unwrap();
442 assert_eq!(norm(&found.name), "/gen/config.h");
443 assert!(search.tried("/gen/config.h", IncludeForm::Angled, None, 0).is_empty());
444 }
445
446 #[test]
447 fn the_list_of_places_tried_is_the_list_that_was_searched() {
448 let fs = MemoryFileSystem::new();
449 let mut search = SearchPath::new();
450 search.push_quote("/q");
451 search.push_bracket("/i");
452 search.push_system("/sys");
453 let here = Path::new("/src");
454
455 assert!(search.resolve(&fs, "a.h", IncludeForm::Quoted, Some(here), 0).is_none());
456 let tried = search.tried("a.h", IncludeForm::Quoted, Some(here), 0);
457 let tried: Vec<_> = tried.iter().map(|p| norm(&p.to_string_lossy())).collect();
458 assert_eq!(tried, ["/src", "/q", "/i", "/sys"]);
459
460 let start = search.start(IncludeForm::Angled);
461 let tried = search.tried("a.h", IncludeForm::Angled, Some(here), start);
462 let tried: Vec<_> = tried.iter().map(|p| norm(&p.to_string_lossy())).collect();
463 assert_eq!(tried, ["/i", "/sys"]);
464 }
465
466 #[test]
467 fn a_header_found_next_to_its_includer_does_not_skip_the_whole_path_afterwards() {
468 let fs = fs_with(&["/src/a.h", "/i/b.h"]);
471 let mut search = SearchPath::new();
472 search.push_bracket("/i");
473 let found =
474 search.resolve(&fs, "a.h", IncludeForm::Quoted, Some(Path::new("/src")), 0).unwrap();
475 assert_eq!(found.next, 0);
476 let next = search.resolve(&fs, "b.h", IncludeForm::Angled, None, found.next).unwrap();
477 assert_eq!(norm(&next.name), "/i/b.h");
478 }
479}