Anforderungen  |   Konzepte  |   Entwurf  |   Entwicklung  |   Qualitätssicherung  |   Lebenszyklus  |   Steuerung
 
 
 
 


Quelle  fs.rs   Sprache: unbekannt

 
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

use crate::std::mock::{mock_key, try_hook, MockKey};
use std::collections::HashMap;
use std::ffi::OsString;
use std::io::{ErrorKind, Read, Result, Seek, SeekFrom, Write};
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
use std::time::SystemTime;

/// Mock filesystem file content.
#[derive(Debug, Default, Clone)]
pub struct MockFileContent(Arc<Mutex<Vec<u8>>>);

impl MockFileContent {
    pub fn empty() -> Self {
        Self::default()
    }

    pub fn new(data: String) -> Self {
        Self::new_bytes(data.into())
    }

    pub fn new_bytes(data: Vec<u8>) -> Self {
        MockFileContent(Arc::new(Mutex::new(data)))
    }

    pub fn make_copy(&self) -> Self {
        Self::new_bytes(self.0.lock().unwrap().clone())
    }
}

impl From<()> for MockFileContent {
    fn from(_: ()) -> Self {
        Self::empty()
    }
}

impl From<String> for MockFileContent {
    fn from(s: String) -> Self {
        Self::new(s)
    }
}

impl From<&str> for MockFileContent {
    fn from(s: &str) -> Self {
        Self::new(s.to_owned())
    }
}

impl From<Vec<u8>> for MockFileContent {
    fn from(bytes: Vec<u8>) -> Self {
        Self::new_bytes(bytes)
    }
}

impl From<&[u8]> for MockFileContent {
    fn from(bytes: &[u8]) -> Self {
        Self::new_bytes(bytes.to_owned())
    }
}

/// Mocked filesystem directory entries.
pub type MockDirEntries = HashMap<OsString, MockFSItem>;

/// The content of a mock filesystem item.
pub enum MockFSContent {
    /// File content.
    File(Result<MockFileContent>),
    /// A directory with the given entries.
    Dir(MockDirEntries),
}

impl std::fmt::Debug for MockFSContent {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        match self {
            Self::File(_) => f.debug_tuple("File").finish(),
            Self::Dir(e) => f.debug_tuple("Dir").field(e).finish(),
        }
    }
}

/// A mock filesystem item.
#[derive(Debug)]
pub struct MockFSItem {
    /// The content of the item (file/dir).
    pub content: MockFSContent,
    /// The modification time of the item.
    pub modified: SystemTime,
}

impl From<MockFSContent> for MockFSItem {
    fn from(content: MockFSContent) -> Self {
        MockFSItem {
            content,
            modified: SystemTime::UNIX_EPOCH,
        }
    }
}

/// A mock filesystem.
#[derive(Debug, Clone)]
pub struct MockFiles {
    root: Arc<Mutex<MockFSItem>>,
}

impl Default for MockFiles {
    fn default() -> Self {
        MockFiles {
            root: Arc::new(Mutex::new(MockFSContent::Dir(Default::default()).into())),
        }
    }
}

impl MockFiles {
    /// Create a new, empty filesystem.
    pub fn new() -> Self {
        Self::default()
    }

    /// Add a mocked file with the given content. The modification time will be the unix epoch.
    ///
    /// Pancis if the parent directory is not already mocked.
    pub fn add_file<P: AsRef<Path>, C: Into<MockFileContent>>(&self, path: P, content: C) -> &Self {
        self.add_file_result(path, Ok(content.into()), SystemTime::UNIX_EPOCH)
    }

    /// Add a mocked directory.
    pub fn add_dir<P: AsRef<Path>>(&self, path: P) -> &Self {
        self.path(path, true, |_| ()).unwrap();
        self
    }

    /// Add a mocked file that returns the given result and has the given modification time.
    ///
    /// Pancis if the parent directory is not already mocked.
    pub fn add_file_result<P: AsRef<Path>>(
        &self,
        path: P,
        result: Result<MockFileContent>,
        modified: SystemTime,
    ) -> &Self {
        let name = path.as_ref().file_name().expect("invalid path");
        self.parent_dir(path.as_ref(), move |dir| {
            if dir.contains_key(name) {
                Err(ErrorKind::AlreadyExists.into())
            } else {
                dir.insert(
                    name.to_owned(),
                    MockFSItem {
                        content: MockFSContent::File(result),
                        modified,
                    },
                );
                Ok(())
            }
        })
        .and_then(|r| r)
        .unwrap();
        self
    }

    /// If create_dirs is true, all missing path components (_including the final component_) are
    /// created as directories. In this case `Err` is only returned if a file conflicts with
    /// a directory component.
    pub fn path<P: AsRef<Path>, F, R>(&self, path: P, create_dirs: bool, f: F) -> Result<R>
    where
        F: FnOnce(&mut MockFSItem) -> R,
    {
        let mut guard = self.root.lock().unwrap();
        let mut cur_entry = &mut *guard;
        for component in path.as_ref().components() {
            use std::path::Component::*;
            match component {
                CurDir | RootDir | Prefix(_) => continue,
                ParentDir => panic!("unsupported path: {}", path.as_ref().display()),
                Normal(name) => {
                    let cur_dir = match &mut cur_entry.content {
                        MockFSContent::File(_) => return Err(ErrorKind::NotFound.into()),
                        MockFSContent::Dir(d) => d,
                    };
                    cur_entry = if create_dirs {
                        cur_dir
                            .entry(name.to_owned())
                            .or_insert_with(|| MockFSContent::Dir(Default::default()).into())
                    } else {
                        cur_dir.get_mut(name).ok_or(ErrorKind::NotFound)?
                    };
                }
            }
        }
        Ok(f(cur_entry))
    }

    /// Get the mocked parent directory of the given path and call a callback on the mocked
    /// directory's entries.
    pub fn parent_dir<P: AsRef<Path>, F, R>(&self, path: P, f: F) -> Result<R>
    where
        F: FnOnce(&mut MockDirEntries) -> R,
    {
        self.path(
            path.as_ref().parent().unwrap_or(&Path::new("")),
            false,
            move |item| match &mut item.content {
                MockFSContent::File(_) => Err(ErrorKind::NotFound.into()),
                MockFSContent::Dir(d) => Ok(f(d)),
            },
        )
        .and_then(|r| r)
    }

    /// Return a file assertion helper for the mocked filesystem.
    pub fn assert_files(&self) -> AssertFiles {
        let mut files = HashMap::new();
        let root = self.root.lock().unwrap();

        fn dir(files: &mut HashMap<PathBuf, MockFileContent>, path: &Path, item: &MockFSItem) {
            match &item.content {
                MockFSContent::File(Ok(c)) => {
                    files.insert(path.to_owned(), c.clone());
                }
                MockFSContent::Dir(d) => {
                    for (component, item) in d {
                        dir(files, &path.join(component), item);
                    }
                }
                _ => (),
            }
        }
        dir(&mut files, Path::new(""), &*root);
        AssertFiles { files }
    }
}

/// A utility for asserting the state of the mocked filesystem.
///
/// All files must be accounted for; when dropped, a panic will occur if some files remain which
/// weren't checked.
#[derive(Debug)]
pub struct AssertFiles {
    files: HashMap<PathBuf, MockFileContent>,
}

// On windows we ignore drive prefixes. This is only relevant for real paths, which are only
// present for edge case situations in tests (where AssertFiles is used).
fn remove_prefix(p: &Path) -> &Path {
    let mut iter = p.components();
    if let Some(std::path::Component::Prefix(_)) = iter.next() {
        iter.next(); // Prefix is followed by RootDir
        iter.as_path()
    } else {
        p
    }
}

impl AssertFiles {
    /// Assert that the given path contains the given content (as a utf8 string).
    pub fn check<P: AsRef<Path>, S: AsRef<str>>(&mut self, path: P, content: S) -> &mut Self {
        let p = remove_prefix(path.as_ref());
        let Some(mfc) = self.files.remove(p) else {
            panic!("missing file: {}", p.display());
        };
        let guard = mfc.0.lock().unwrap();
        assert_eq!(
            std::str::from_utf8(&*guard).unwrap(),
            content.as_ref(),
            "file content mismatch: {}",
            p.display()
        );
        self
    }

    /// Assert that the given path contains the given byte content.
    pub fn check_bytes<P: AsRef<Path>, B: AsRef<[u8]>>(
        &mut self,
        path: P,
        content: B,
    ) -> &mut Self {
        let p = remove_prefix(path.as_ref());
        let Some(mfc) = self.files.remove(p) else {
            panic!("missing file: {}", p.display());
        };
        let guard = mfc.0.lock().unwrap();
        assert_eq!(
            &*guard,
            content.as_ref(),
            "file content mismatch: {}",
            p.display()
        );
        self
    }

    /// Ignore the given file (whether it exists or not).
    pub fn ignore<P: AsRef<Path>>(&mut self, path: P) -> &mut Self {
        self.files.remove(remove_prefix(path.as_ref()));
        self
    }

    /// Assert that the given path exists without checking its content.
    pub fn check_exists<P: AsRef<Path>>(&mut self, path: P) -> &mut Self {
        let p = remove_prefix(path.as_ref());
        if self.files.remove(p).is_none() {
            panic!("missing file: {}", p.display());
        }
        self
    }

    /// Finish checking files.
    ///
    /// This panics if all files were not checked.
    ///
    /// This is also called when the value is dropped.
    pub fn finish(&mut self) {
        let files = std::mem::take(&mut self.files);
        if !files.is_empty() {
            panic!("additional files not expected: {:?}", files.keys());
        }
    }
}

impl Drop for AssertFiles {
    fn drop(&mut self) {
        if !std::thread::panicking() {
            self.finish();
        }
    }
}

mock_key! {
    pub struct MockFS => MockFiles
}

pub struct File {
    content: MockFileContent,
    pos: usize,
}

impl File {
    pub fn open<P: AsRef<Path>>(path: P) -> Result<File> {
        MockFS.get(move |files| {
            files
                .path(path, false, |item| match &item.content {
                    MockFSContent::File(result) => result
                        .as_ref()
                        .map(|b| File {
                            content: b.clone(),
                            pos: 0,
                        })
                        .map_err(|e| e.kind().into()),
                    MockFSContent::Dir(_) => Err(ErrorKind::NotFound.into()),
                })
                .and_then(|r| r)
        })
    }

    pub fn create<P: AsRef<Path>>(path: P) -> Result<File> {
        let path = path.as_ref();
        MockFS.get(|files| {
            let name = path.file_name().expect("invalid path");
            files.parent_dir(path, move |d| {
                if !d.contains_key(name) {
                    d.insert(
                        name.to_owned(),
                        MockFSItem {
                            content: MockFSContent::File(Ok(Default::default())),
                            modified: super::time::SystemTime::now().0,
                        },
                    );
                }
            })
        })?;
        Self::open(path)
    }
}

impl Read for File {
    fn read(&mut self, buf: &mut [u8]) -> Result<usize> {
        let guard = self.content.0.lock().unwrap();
        if self.pos >= guard.len() {
            return Ok(0);
        }
        let to_read = std::cmp::min(buf.len(), guard.len() - self.pos);
        buf[..to_read].copy_from_slice(&guard[self.pos..self.pos + to_read]);
        self.pos += to_read;
        Ok(to_read)
    }
}

impl Seek for File {
    fn seek(&mut self, pos: SeekFrom) -> Result<u64> {
        let len = self.content.0.lock().unwrap().len();
        match pos {
            SeekFrom::Start(n) => self.pos = n as usize,
            SeekFrom::End(n) => {
                if n < 0 {
                    let offset = -n as usize;
                    if offset > len {
                        return Err(std::io::Error::new(
                            std::io::ErrorKind::InvalidInput,
                            "out of bounds",
                        ));
                    }
                    self.pos = len - offset;
                } else {
                    self.pos = len + n as usize
                }
            }
            SeekFrom::Current(n) => {
                if n < 0 {
                    let offset = -n as usize;
                    if offset > self.pos {
                        return Err(std::io::Error::new(
                            std::io::ErrorKind::InvalidInput,
                            "out of bounds",
                        ));
                    }
                    self.pos -= offset;
                } else {
                    self.pos += n as usize;
                }
            }
        }
        Ok(self.pos as u64)
    }
}

impl Write for File {
    fn write(&mut self, buf: &[u8]) -> Result<usize> {
        let mut guard = self.content.0.lock().unwrap();
        let end = self.pos + buf.len();
        if end > guard.len() {
            guard.resize(end, 0);
        }
        (&mut guard[self.pos..end]).copy_from_slice(buf);
        self.pos = end;
        Ok(buf.len())
    }

    fn flush(&mut self) -> Result<()> {
        Ok(())
    }
}

pub fn create_dir_all<P: AsRef<Path>>(path: P) -> Result<()> {
    MockFS.get(move |files| files.path(path, true, |_| ()))
}

pub fn rename<P: AsRef<Path>, Q: AsRef<Path>>(from: P, to: Q) -> Result<()> {
    if try_hook(false, "rename_fail") {
        log::debug!("intentionally failing std::fs::rename");
        return Err(ErrorKind::Unsupported.into());
    }

    MockFS.get(move |files| {
        let from_name = from.as_ref().file_name().expect("invalid path");
        let item = files
            .parent_dir(from.as_ref(), move |d| {
                d.remove(from_name).ok_or(ErrorKind::NotFound.into())
            })
            .and_then(|r| r)?;

        let to_name = to.as_ref().file_name().expect("invalid path");
        files
            .parent_dir(to.as_ref(), move |d| {
                // Just error if `to` exists, which doesn't quite follow `std::fs::rename` behavior.
                if d.contains_key(to_name) {
                    Err(ErrorKind::AlreadyExists.into())
                } else {
                    d.insert(to_name.to_owned(), item);
                    Ok(())
                }
            })
            .and_then(|r| r)
    })
}

pub fn copy<P: AsRef<Path>, Q: AsRef<Path>>(from: P, to: Q) -> Result<()> {
    MockFS.get(move |files| {
        let from_name = from.as_ref().file_name().expect("invalid path");
        let item = files
            .parent_dir(from.as_ref(), move |d| {
                d.get(from_name)
                    .ok_or(ErrorKind::NotFound.into())
                    .and_then(|e| match &e.content {
                        MockFSContent::Dir(_) => Err(ErrorKind::NotFound.into()),
                        MockFSContent::File(f) => f
                            .as_ref()
                            .map(|f| MockFSItem {
                                content: MockFSContent::File(Ok(f.make_copy())),
                                modified: e.modified.clone(),
                            })
                            .map_err(|e| e.kind().into()),
                    })
            })
            .and_then(|r| r)?;

        let to_name = to.as_ref().file_name().expect("invalid path");
        files
            .parent_dir(to.as_ref(), move |d| {
                d.insert(to_name.to_owned(), item);
                Ok(())
            })
            .and_then(|r| r)
    })
}

pub fn remove_file<P: AsRef<Path>>(path: P) -> Result<()> {
    MockFS.get(move |files| {
        let name = path.as_ref().file_name().expect("invalid path");
        files
            .parent_dir(path.as_ref(), |d| {
                if let Some(MockFSItem {
                    content: MockFSContent::Dir(_),
                    ..
                }) = d.get(name)
                {
                    Err(ErrorKind::NotFound.into())
                } else {
                    d.remove(name).ok_or(ErrorKind::NotFound.into()).map(|_| ())
                }
            })
            .and_then(|r| r)
    })
}

pub fn write<P: AsRef<Path>, C: AsRef<[u8]>>(path: P, contents: C) -> Result<()> {
    File::create(path.as_ref())?.write_all(contents.as_ref())
}

pub struct ReadDir {
    base: PathBuf,
    children: Vec<OsString>,
}

impl ReadDir {
    pub fn new(path: &Path) -> Result<Self> {
        MockFS.get(move |files| {
            files
                .path(path, false, |item| match &item.content {
                    MockFSContent::Dir(d) => Ok(ReadDir {
                        base: path.to_owned(),
                        children: d.keys().cloned().collect(),
                    }),
                    MockFSContent::File(_) => Err(ErrorKind::NotFound.into()),
                })
                .and_then(|r| r)
        })
    }
}

impl Iterator for ReadDir {
    type Item = Result<DirEntry>;
    fn next(&mut self) -> Option<Self::Item> {
        let child = self.children.pop()?;
        Some(Ok(DirEntry(self.base.join(child))))
    }
}

pub struct DirEntry(PathBuf);

impl DirEntry {
    pub fn path(&self) -> super::path::PathBuf {
        super::path::PathBuf(self.0.clone())
    }

    pub fn metadata(&self) -> Result<Metadata> {
        MockFS.get(|files| {
            files.path(&self.0, false, |item| {
                let is_dir = matches!(&item.content, MockFSContent::Dir(_));
                Metadata {
                    is_dir,
                    modified: item.modified,
                }
            })
        })
    }
}

pub struct Metadata {
    is_dir: bool,
    modified: SystemTime,
}

impl Metadata {
    pub fn is_file(&self) -> bool {
        !self.is_dir
    }

    pub fn is_dir(&self) -> bool {
        self.is_dir
    }

    pub fn modified(&self) -> Result<super::time::SystemTime> {
        Ok(super::time::SystemTime(self.modified))
    }
}

[ Dauer der Verarbeitung: 0.36 Sekunden  (vorverarbeitet)  ]

                                                                                                                                                                                                                                                                                                                                                                                                     


Neuigkeiten

     Aktuelles
     Motto des Tages

Software

     Produkte
     Quellcodebibliothek

Aktivitäten

     Artikel über Sicherheit
     Anleitung zur Aktivierung von SSL

Muße

     Gedichte
     Musik
     Bilder

Jenseits des Üblichen ....
    

Besucherstatistik

Besucherstatistik

Monitoring

Montastic status badge