// Light filesystem abstractions // // Copyright (C) 2014-2023 Ryan Specialty, LLC. // // This file is part of TAME. // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . //! Lightweight filesystem abstraction. //! //! This abstraction is intended to provide generics missing from Rust core, //! but makes no attempt to be comprehensive---it //! includes only what is needed for TAMER. //! //! - [`File`] provides a trait for operating on files; and //! - [`Filesystem`] provides a generic way to access files by path. //! //! This implements traits directly atop of Rust's core structs where //! possible. //! //! //! Visiting Files Once //! =================== //! [`VisitOnceFilesystem`] produces [`VisitOnceFile::FirstVisit`] the first //! time it encounters a given path, //! and [`VisitOnceFile::Visited`] every time thereafter. use std::collections::hash_map::RandomState; use std::collections::HashSet; use std::ffi::OsString; use std::fs; use std::hash::BuildHasher; use std::io::{BufReader, Read, Result}; use std::marker::PhantomData; use std::path::{Path, PathBuf}; use crate::span::{Context, UNKNOWN_CONTEXT}; use crate::sym::GlobalSymbolIntern; /// A file. pub trait File: Read where Self: Sized, { fn open>(path: P) -> Result; } impl File for fs::File { fn open>(path: P) -> Result { Self::open(path) } } impl File for BufReader { /// Open the file at `path` and construct a [`BufReader`] from it. fn open>(path: P) -> Result { Ok(BufReader::new(F::open(path)?)) } } #[derive(Debug, PartialEq)] pub struct PathFile(pub PathBuf, pub F, pub Context); impl File for PathFile { fn open>(path: P) -> Result { let buf = path.as_ref().to_path_buf(); let file = F::open(&buf)?; let ctx = buf .to_str() .map(|s| s.intern().into()) .unwrap_or(UNKNOWN_CONTEXT); Ok(Self(buf, file, ctx)) } } impl Read for PathFile { fn read(&mut self, buf: &mut [u8]) -> Result { self.1.read(buf) } } /// A filesystem. /// /// Opening a file (using [`open`](Filesystem::open)) proxies to `F::open`. /// The type of files opened by this abstraction can therefore be controlled /// via generics. pub trait Filesystem where Self: Sized, { fn open>(&mut self, path: P) -> Result { F::open(path) } } /// A potentially visited [`File`]. /// /// See [`VisitOnceFilesystem`] for more information. #[derive(Debug, PartialEq)] pub enum VisitOnceFile { /// First time visiting file at requested path. FirstVisit(F), /// Requested path has already been visited. Visited, } impl File for VisitOnceFile { fn open>(path: P) -> Result { F::open(path).map(|file| Self::FirstVisit(file)) } } impl Read for VisitOnceFile { fn read(&mut self, buf: &mut [u8]) -> Result { match self { Self::FirstVisit(file) => file.read(buf), Self::Visited => Ok(0), } } } /// Opens each path only once. /// /// When a [`File`] is first opened, /// it will be wrapped in [`VisitOnceFile::FirstVisit`] /// Subsequent calls to `open` will yield /// [`VisitOnceFile::Visited`] without attempting to open the file. /// /// A file will not be marked as visited if it fails to be opened. pub struct VisitOnceFilesystem where C: Canonicalizer, S: BuildHasher, { visited: HashSet, _c: PhantomData, } impl VisitOnceFilesystem where C: Canonicalizer, S: BuildHasher + Default, { /// New filesystem with no recorded paths. pub fn new() -> Self { Self { visited: Default::default(), _c: PhantomData, } } /// Number of visited paths. pub fn visit_len(&self) -> usize { self.visited.len() } } impl Filesystem> for VisitOnceFilesystem where C: Canonicalizer, S: BuildHasher, F: File, { /// Open a file, marking `path` as visited. /// /// The next time the same path is requested, /// [`VisitOnceFile::Visited`] will be returned. /// /// `path` will not be marked as visited if opening fails. fn open>(&mut self, path: P) -> Result> { let cpath = C::canonicalize(path)?; let ostr = cpath.as_os_str(); if self.visited.contains(ostr) { return Ok(VisitOnceFile::Visited); } VisitOnceFile::open(ostr).map(|file| { self.visited.insert(ostr.to_os_string()); file }) } } /// Vanilla filesystem access. /// /// This provides access to the filesystem as one would expect. /// The actual operations are delegated to `F`. #[derive(Debug)] pub struct VanillaFilesystem { _file: PhantomData, } impl Default for VanillaFilesystem { fn default() -> Self { Self { _file: Default::default(), } } } impl Filesystem for VanillaFilesystem { fn open>(&mut self, path: P) -> Result { F::open(path) } } pub trait Canonicalizer { fn canonicalize>(path: P) -> Result; } pub struct FsCanonicalizer; impl Canonicalizer for FsCanonicalizer { fn canonicalize>(path: P) -> Result { std::fs::canonicalize(path) } } #[cfg(test)] mod test { use super::*; use std::path::PathBuf; #[derive(Debug, PartialEq)] struct DummyFile(PathBuf); impl File for DummyFile { fn open>(path: P) -> Result { Ok(Self(path.as_ref().to_path_buf())) } } impl Read for DummyFile { fn read(&mut self, _buf: &mut [u8]) -> Result { Ok(0) } } #[test] fn buf_reader_file() { let path: PathBuf = "buf/path".into(); let result: BufReader = File::open(path.clone()).unwrap(); assert_eq!(DummyFile(path), result.into_inner()); } #[test] fn path_file() { let path: PathBuf = "buf/path".into(); let result: PathFile = File::open(path.clone()).unwrap(); assert_eq!( PathFile( path.clone(), DummyFile(path.clone()), "buf/path".intern().into() ), result ); } mod canonicalizer { use super::*; struct StubCanonicalizer; impl Canonicalizer for StubCanonicalizer { fn canonicalize>(path: P) -> Result { let mut buf = path.as_ref().to_path_buf(); buf.push("CANONICALIZED"); Ok(buf) } } #[test] fn vist_once() { let mut fs = VisitOnceFilesystem::::new(); let path: PathBuf = "foo/bar".into(); let result = fs.open(path.clone()).unwrap(); let mut expected_path = path.clone().to_path_buf(); expected_path.push("CANONICALIZED"); // First time, return file. assert_eq!( VisitOnceFile::FirstVisit(DummyFile(expected_path)), result ); // Second time, already visited. let result_2: VisitOnceFile = fs.open(path).unwrap(); assert_eq!(VisitOnceFile::Visited, result_2); } } }