summaryrefslogtreecommitdiffstats
path: root/toolkit/crashreporter/client/app/src/std/fs.rs
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/crashreporter/client/app/src/std/fs.rs')
-rw-r--r--toolkit/crashreporter/client/app/src/std/fs.rs559
1 files changed, 559 insertions, 0 deletions
diff --git a/toolkit/crashreporter/client/app/src/std/fs.rs b/toolkit/crashreporter/client/app/src/std/fs.rs
new file mode 100644
index 0000000000..8ba2c572d5
--- /dev/null
+++ b/toolkit/crashreporter/client/app/src/std/fs.rs
@@ -0,0 +1,559 @@
+/* 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, 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)))
+ }
+}
+
+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<()> {
+ 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 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))
+ }
+}