1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
use std::{
    io,
    fs,
    cell::Cell,
    path::{Path, PathBuf},
    collections::hash_map::{HashMap, Entry},
};

use super::{Hook};


/// Hook for reading files from filesystem
pub struct FsHook {
    inc_dirs: Vec<PathBuf>,
    cache: Cell<Option<HashMap<PathBuf, String>>>,
}

impl FsHook {
    pub fn new() -> Self {
        FsHook {
            inc_dirs: Vec::new(),
            cache: Cell::new(Some(HashMap::new())),
        }
    }

    pub fn include_dir(mut self, dir: &Path) -> io::Result<Self> {
        self.check_dir(dir)?;
        self.inc_dirs.push(dir.to_path_buf());
        Ok(self)
    }

    fn check_dir(&self, dir: &Path) -> io::Result<()> {
        let meta = fs::metadata(dir)?;
        if !meta.is_dir() {
            Err(io::Error::new(
                io::ErrorKind::InvalidData,
                format!("'{}' is not a directory", dir.display()),
            ))
        } else {
            Ok(())
        }
    }

    fn check_file(&self, path: &Path) -> io::Result<()> {
        let map = self.cache.take().unwrap();
        let contains = map.contains_key(path);
        self.cache.set(Some(map));
        if contains {
            return Ok(());
        }

        match fs::metadata(path) {
            Ok(meta) => {
                if !meta.is_file() {
                    Err(io::Error::new(
                        io::ErrorKind::InvalidData,
                        format!("'{}' is not a file", path.display()),
                    ))
                } else {
                    Ok(())
                }
            }
            Err(e) => Err(e),
        }
    }

    fn find_in_dir(&self, dir: &Path, name: &Path) -> io::Result<Option<PathBuf>> {
        let path = dir.join(name);
        match self.check_file(&path) {
            Ok(()) => Ok(Some(path)),
            Err(e) => match e.kind() {
                io::ErrorKind::NotFound => Ok(None),
                _ => return Err(e),
            }
        }
    }

    fn find_file(&self, dir: Option<&Path>, name: &Path) -> io::Result<PathBuf> {
        if name.is_absolute() {
            return Ok(name.to_path_buf())
        }

        match dir {
            Some(dir) => match self.find_in_dir(dir, name)? {
                Some(path) => return Ok(path),
                None => (),
            },
            None => (),
        }

        for dir in self.inc_dirs.iter() {
            match self.find_in_dir(dir, name)? {
                Some(path) => return Ok(path),
                None => (),
            }
        }

        Err(io::Error::new(io::ErrorKind::NotFound, name.to_string_lossy()))
    }
}

impl Hook for FsHook {
    fn read(&self, path: &Path, dir: Option<&Path>) -> io::Result<(PathBuf, String)> {
        self.find_file(dir, path)
        .and_then(|path| {
            let mut map = self.cache.take().unwrap();
            
            let res = match map.entry(path.to_path_buf()) {
                Entry::Occupied(v) => Ok(v.get().clone()),
                Entry::Vacant(v) => {
                    fs::read_to_string(&path)
                    .and_then(|data| {
                        v.insert(data.clone());
                        Ok(data)
                    })
                }
            }
            .map(|data| (path, data));

            self.cache.set(Some(map));
            res
        })
    }
}