// 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);
}
}
}