2020-04-06 16:13:32 -04:00
|
|
|
// Light filesystem abstractions
|
|
|
|
//
|
2021-07-22 15:00:15 -04:00
|
|
|
// Copyright (C) 2014-2021 Ryan Specialty Group, LLC.
|
2020-04-06 16:13:32 -04:00
|
|
|
//
|
|
|
|
// 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 <http://www.gnu.org/licenses/>.
|
|
|
|
|
|
|
|
//! 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;
|
2020-04-06 22:38:44 -04:00
|
|
|
use std::io::{BufReader, Read, Result};
|
2020-04-07 11:40:19 -04:00
|
|
|
use std::marker::PhantomData;
|
2020-04-06 22:07:39 -04:00
|
|
|
use std::path::{Path, PathBuf};
|
2020-04-06 16:13:32 -04:00
|
|
|
|
|
|
|
/// A file.
|
|
|
|
pub trait File
|
|
|
|
where
|
|
|
|
Self: Sized,
|
|
|
|
{
|
|
|
|
fn open<P: AsRef<Path>>(path: P) -> Result<Self>;
|
|
|
|
}
|
|
|
|
|
|
|
|
impl File for fs::File {
|
|
|
|
fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
|
|
|
|
Self::open(path)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-04-06 22:38:44 -04:00
|
|
|
impl<F: File + Read> File for BufReader<F> {
|
|
|
|
/// Open the file at `path` and construct a [`BufReader`] from it.
|
|
|
|
fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
|
|
|
|
Ok(BufReader::new(F::open(path)?))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-04-07 11:40:19 -04:00
|
|
|
#[derive(Debug, PartialEq)]
|
|
|
|
pub struct PathFile<F: File>(PathBuf, F);
|
2020-04-06 22:07:39 -04:00
|
|
|
|
2020-04-07 11:40:19 -04:00
|
|
|
impl<F: File> Into<(PathBuf, F)> for PathFile<F> {
|
2020-04-06 22:07:39 -04:00
|
|
|
fn into(self) -> (PathBuf, F) {
|
|
|
|
(self.0, self.1)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-04-07 11:40:19 -04:00
|
|
|
impl<F: File> File for PathFile<F> {
|
2020-04-06 22:07:39 -04:00
|
|
|
fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
|
2020-04-07 11:40:19 -04:00
|
|
|
let buf = path.as_ref().to_path_buf();
|
|
|
|
let file = F::open(&buf)?;
|
2020-04-06 22:07:39 -04:00
|
|
|
|
2020-04-07 11:40:19 -04:00
|
|
|
Ok(Self(buf, file))
|
2020-04-06 22:07:39 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-04-06 16:13:32 -04:00
|
|
|
/// 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<F: File>
|
|
|
|
where
|
|
|
|
Self: Sized,
|
|
|
|
{
|
|
|
|
fn open<P: AsRef<Path>>(&mut self, path: P) -> Result<F> {
|
|
|
|
F::open(path)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// A potentially visited [`File`].
|
|
|
|
///
|
|
|
|
/// See [`VisitOnceFilesystem`] for more information.
|
|
|
|
#[derive(Debug, PartialEq)]
|
|
|
|
pub enum VisitOnceFile<F: File> {
|
|
|
|
/// First time visiting file at requested path.
|
|
|
|
FirstVisit(F),
|
|
|
|
|
|
|
|
/// Requested path has already been visited.
|
|
|
|
Visited,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl<F: File> File for VisitOnceFile<F> {
|
|
|
|
fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
|
|
|
|
F::open(path).map(|file| Self::FirstVisit(file))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// 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.
|
2020-04-07 11:40:19 -04:00
|
|
|
pub struct VisitOnceFilesystem<C, S = RandomState>
|
2020-04-06 16:13:32 -04:00
|
|
|
where
|
2020-04-07 11:40:19 -04:00
|
|
|
C: Canonicalizer,
|
2020-04-06 16:13:32 -04:00
|
|
|
S: BuildHasher,
|
|
|
|
{
|
|
|
|
visited: HashSet<OsString, S>,
|
2020-04-07 11:40:19 -04:00
|
|
|
_c: PhantomData<C>,
|
2020-04-06 16:13:32 -04:00
|
|
|
}
|
|
|
|
|
2020-04-07 11:40:19 -04:00
|
|
|
impl<C, S> VisitOnceFilesystem<C, S>
|
2020-04-06 16:13:32 -04:00
|
|
|
where
|
2020-04-07 11:40:19 -04:00
|
|
|
C: Canonicalizer,
|
2020-04-06 16:13:32 -04:00
|
|
|
S: BuildHasher + Default,
|
|
|
|
{
|
|
|
|
/// New filesystem with no recorded paths.
|
|
|
|
pub fn new() -> Self {
|
|
|
|
Self {
|
|
|
|
visited: Default::default(),
|
2020-04-07 11:40:19 -04:00
|
|
|
_c: PhantomData,
|
2020-04-06 16:13:32 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Number of visited paths.
|
|
|
|
pub fn visit_len(&self) -> usize {
|
|
|
|
self.visited.len()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-04-07 11:40:19 -04:00
|
|
|
impl<C, S, F> Filesystem<VisitOnceFile<F>> for VisitOnceFilesystem<C, S>
|
2020-04-06 16:13:32 -04:00
|
|
|
where
|
2020-04-07 11:40:19 -04:00
|
|
|
C: Canonicalizer,
|
2020-04-06 16:13:32 -04:00
|
|
|
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<P: AsRef<Path>>(&mut self, path: P) -> Result<VisitOnceFile<F>> {
|
2020-04-07 11:40:19 -04:00
|
|
|
let cpath = C::canonicalize(path)?;
|
|
|
|
let ostr = cpath.as_os_str();
|
2020-04-06 16:13:32 -04:00
|
|
|
|
|
|
|
if self.visited.contains(ostr) {
|
|
|
|
return Ok(VisitOnceFile::Visited);
|
|
|
|
}
|
|
|
|
|
|
|
|
VisitOnceFile::open(ostr).and_then(|file| {
|
|
|
|
self.visited.insert(ostr.to_os_string());
|
|
|
|
Ok(file)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-04-07 11:40:19 -04:00
|
|
|
pub trait Canonicalizer {
|
|
|
|
fn canonicalize<P: AsRef<Path>>(path: P) -> Result<PathBuf>;
|
|
|
|
}
|
|
|
|
|
|
|
|
pub struct FsCanonicalizer;
|
|
|
|
|
|
|
|
impl Canonicalizer for FsCanonicalizer {
|
|
|
|
fn canonicalize<P: AsRef<Path>>(path: P) -> Result<PathBuf> {
|
|
|
|
std::fs::canonicalize(path)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-04-06 16:13:32 -04:00
|
|
|
#[cfg(test)]
|
|
|
|
mod test {
|
|
|
|
use super::*;
|
|
|
|
use std::path::PathBuf;
|
|
|
|
|
|
|
|
#[derive(Debug, PartialEq)]
|
|
|
|
struct DummyFile(PathBuf);
|
|
|
|
|
|
|
|
impl File for DummyFile {
|
|
|
|
fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
|
|
|
|
Ok(Self(path.as_ref().to_path_buf()))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-04-06 22:38:44 -04:00
|
|
|
impl Read for DummyFile {
|
|
|
|
fn read(&mut self, _buf: &mut [u8]) -> Result<usize> {
|
|
|
|
Ok(0)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn buf_reader_file() {
|
|
|
|
let path: PathBuf = "buf/path".into();
|
|
|
|
let result: BufReader<DummyFile> = File::open(path.clone()).unwrap();
|
|
|
|
|
|
|
|
assert_eq!(DummyFile(path), result.into_inner());
|
|
|
|
}
|
|
|
|
|
2020-04-06 16:13:32 -04:00
|
|
|
#[test]
|
2020-04-07 11:40:19 -04:00
|
|
|
fn path_file() {
|
|
|
|
let path: PathBuf = "buf/path".into();
|
|
|
|
let result: PathFile<DummyFile> = File::open(path.clone()).unwrap();
|
2020-04-06 16:13:32 -04:00
|
|
|
|
2020-04-07 11:40:19 -04:00
|
|
|
assert_eq!(PathFile(path.clone(), DummyFile(path.clone())), result);
|
|
|
|
|
|
|
|
// Tuple conversion.
|
|
|
|
assert_eq!((path.clone(), DummyFile(path.clone())), result.into());
|
|
|
|
}
|
2020-04-06 16:13:32 -04:00
|
|
|
|
2020-04-07 11:40:19 -04:00
|
|
|
mod canonicalizer {
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
struct StubCanonicalizer;
|
|
|
|
|
|
|
|
impl Canonicalizer for StubCanonicalizer {
|
|
|
|
fn canonicalize<P: AsRef<Path>>(path: P) -> Result<PathBuf> {
|
|
|
|
let mut buf = path.as_ref().to_path_buf();
|
|
|
|
buf.push("CANONICALIZED");
|
|
|
|
|
|
|
|
Ok(buf)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn vist_once() {
|
|
|
|
let mut fs =
|
|
|
|
VisitOnceFilesystem::<StubCanonicalizer, RandomState>::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<DummyFile> = fs.open(path).unwrap();
|
|
|
|
assert_eq!(VisitOnceFile::Visited, result_2);
|
|
|
|
}
|
2020-04-06 16:13:32 -04:00
|
|
|
}
|
|
|
|
}
|