diskit 0.1.1

Utilities for intercepting disk requests.
Documentation
use std::{
    collections::HashMap,
    ffi::OsString,
    io::{Error, ErrorKind, SeekFrom},
    mem,
    path::{Component, Path, PathBuf},
};

use crate::{
    dir_entry::DirEntry,
    file::{File, FileInner},
    metadata::{FileType, Metadata},
    open_options::OpenOptions,
    virtual_diskit::VirtualDiskit,
    walkdir::{WalkDir, WalkdirIterator, WalkdirIteratorInner},
};

macro_rules! try_nested {
    ($val: expr, $self: expr, $inner: expr) => {
        loop
        {
            let error;
            match $val
            {
                Ok(x) => break x,
                Err(err) =>
                {
                    error = err;
                }
            }
            $self.walkdirs[$inner.val].fused = true;
            return Some(Err(error));
        }
    };
}

// The `Empty` variant is only used if something is deleted and since
// there is no deleting without the `trash` feature, it's dead code
// then.
#[cfg_attr(not(feature = "trash"), allow(dead_code))]
#[derive(Debug, PartialEq, Eq)]
pub enum InodeInner
{
    File(Vec<u8>),
    Dir(HashMap<OsString, usize>),
    Empty,
}

#[derive(Debug)]
pub struct Inode
{
    pub inner: InodeInner,
    pub id: usize,
}

// This pedantic lint is supposed to guard against implementing state
// machines as structs with a `bool` for every state instead of using
// enums.  This is not a state machine, so it's actually completely
// fine.
#[allow(clippy::struct_excessive_bools)]
#[derive(Debug)]
pub struct OpenedFile
{
    pub inode_id: usize,
    pub pos: usize,
    pub read: bool,
    pub write: bool,
    pub append: bool,
    pub closed: bool,
}

#[derive(Debug)]
pub struct WalkingDir
{
    pub pos: Vec<usize>,
    pub fused: bool,
}

#[derive(Debug)]
pub struct VirtualDiskitInner
{
    pub content: Vec<Inode>,
    pub files: Vec<OpenedFile>,
    pub walkdirs: Vec<WalkingDir>,
    pub pwd: PathBuf,
}

impl VirtualDiskitInner
{
    pub fn new() -> Self
    {
        Self {
            content: vec![Inode {
                inner: InodeInner::Dir(
                    [(OsString::from("."), 0), (OsString::from(".."), 0)].into(),
                ),
                id: 0,
            }],
            files: vec![],
            walkdirs: vec![],
            pwd: PathBuf::from("/"),
        }
    }

    pub fn open_with_options(
        &mut self,
        path: &Path,
        options: OpenOptions,
        diskit: &VirtualDiskit,
    ) -> Result<File<VirtualDiskit>, Error>
    {
        // Checks for disallowed combinations:
        // * truncating without writing
        // * create(_new)ing without also writing/appending
        if (options.truncate && !options.write)
            || ((options.create || options.create_new) && !(options.write || options.append))
        {
            return Err(From::from(ErrorKind::InvalidInput));
        }

        let inode = self.get_inode_by_full_path(path)?;

        if let Err(inode) = inode
        {
            if !(options.create || options.create_new)
            {
                return Err(From::from(ErrorKind::NotFound));
            }

            let len = self.content.len();

            match &mut self.get_mut_inode_by_id(inode.id)?.inner
            {
                InodeInner::Dir(dir) =>
                {
                    dir.insert(
                        path.file_name().ok_or(ErrorKind::InvalidInput)?.to_owned(),
                        len,
                    )
                    .ok_or(())
                    .expect_err("The directory shouldn't have this file already");

                    self.files.push(OpenedFile {
                        inode_id: len,
                        pos: 0,
                        read: options.read,
                        write: options.write,
                        append: options.append,
                        closed: false,
                    });

                    self.content.push(Inode {
                        inner: InodeInner::File(vec![]),
                        id: self.content.len(),
                    });

                    Ok(File {
                        inner: FileInner {
                            file: None,
                            val: self.files.len() - 1,
                        },
                        diskit: diskit.clone(),
                    })
                }
                InodeInner::File(_) => Err(From::from(ErrorKind::NotADirectory)),
                InodeInner::Empty => panic!("Empty inode found"),
            }
        }
        else if options.create_new
        // Implicitly: `&& ! let Err(_) = inode`
        {
            Err(From::from(ErrorKind::AlreadyExists))
        }
        else
        {
            let inode_id = inode.unwrap().id;
            let inode = self.get_mut_inode_by_id(inode_id)?;

            match &mut inode.inner
            {
                InodeInner::File(file) =>
                {
                    if options.truncate
                    {
                        file.truncate(0);
                    }

                    let pos = if options.append { file.len() } else { 0 };

                    self.files.push(OpenedFile {
                        inode_id,
                        pos,
                        read: options.read,
                        write: options.write,
                        append: options.append,
                        closed: false,
                    });

                    Ok(File {
                        inner: FileInner {
                            file: None,
                            val: self.files.len() - 1,
                        },
                        diskit: diskit.clone(),
                    })
                }
                InodeInner::Dir(_) => Err(From::from(ErrorKind::IsADirectory)),
                InodeInner::Empty => panic!("Empty inode found"),
            }
        }
    }

    pub fn read(&mut self, file: &FileInner, buf: &mut [u8]) -> Result<usize, Error>
    {
        let opened_file = &self.files[file.val];

        debug_assert!(!opened_file.closed, "Attempted to use closed file");

        if !opened_file.read
        {
            return Err(From::from(ErrorKind::PermissionDenied));
        }

        let inode = &self.get_inode_by_id(opened_file.inode_id)?.inner;
        let pos = opened_file.pos;

        match inode
        {
            InodeInner::File(content) =>
            {
                if pos >= content.len()
                {
                    return Ok(0);
                }

                let mut amount = content.len() - pos;

                if amount > buf.len()
                {
                    amount = buf.len();
                }

                buf[0..amount].copy_from_slice(&content[pos..(pos + amount)]);

                self.files[file.val].pos += amount;

                Ok(amount)
            }
            InodeInner::Dir(_) => Err(From::from(ErrorKind::IsADirectory)),
            InodeInner::Empty => panic!("Empty inode found"),
        }
    }

    pub fn read_to_end(&mut self, file: &mut FileInner, buf: &mut Vec<u8>) -> Result<usize, Error>
    {
        let opened_file = &self.files[file.val];

        debug_assert!(!opened_file.closed, "Attempted to use closed file");

        if !opened_file.read
        {
            return Err(From::from(ErrorKind::PermissionDenied));
        }

        let inode = &self.get_inode_by_id(opened_file.inode_id)?.inner;
        let pos = opened_file.pos;

        match inode
        {
            InodeInner::File(content) =>
            {
                if pos >= content.len()
                {
                    return Ok(0);
                }

                buf.extend_from_slice(&content[pos..]);

                let amount = content.len() - pos;

                self.files[file.val].pos += content.len();

                Ok(amount)
            }
            InodeInner::Dir(_) => Err(From::from(ErrorKind::IsADirectory)),
            InodeInner::Empty => panic!("Empty inode found"),
        }
    }

    pub fn read_to_string(&mut self, file: &mut FileInner, buf: &mut String)
        -> Result<usize, Error>
    {
        let mut vec = vec![];

        let amount = self.read_to_end(file, &mut vec)?;

        buf.push_str(std::str::from_utf8(&vec).map_err(|_| ErrorKind::InvalidData)?);

        Ok(amount)
    }

    pub fn write(&mut self, file: &mut FileInner, buf: &[u8]) -> Result<usize, Error>
    {
        self.write_all(file, buf)?;

        Ok(buf.len())
    }

    pub fn write_all(&mut self, file: &mut FileInner, buf: &[u8]) -> Result<(), Error>
    {
        let opened_file = &self.files[file.val];
        let mut pos = opened_file.pos;
        let append = opened_file.append;

        debug_assert!(!opened_file.closed, "Attempted to use closed file");

        if !(opened_file.write || opened_file.append)
        {
            return Err(From::from(ErrorKind::PermissionDenied));
        }

        let inode = &mut self.get_mut_inode_by_id(opened_file.inode_id)?.inner;

        match inode
        {
            InodeInner::File(content) =>
            {
                if append
                {
                    pos = content.len();
                }

                if pos + buf.len() > content.len()
                {
                    content.append(&mut vec![0; pos + buf.len() - content.len()]);
                }

                content[pos..(pos + buf.len())].copy_from_slice(buf);

                self.files[file.val].pos = pos + buf.len();

                Ok(())
            }
            InodeInner::Dir(_) => Err(From::from(ErrorKind::IsADirectory)),
            InodeInner::Empty => panic!("Empty inode found"),
        }
    }

    pub fn metadata_inner(&self, file: &FileInner) -> Result<Metadata, Error>
    {
        match &self.get_inode_by_id(self.files[file.val].inode_id)?.inner
        {
            InodeInner::File(content) => Ok(Metadata {
                file_type: FileType::File,
                len: content.len() as _,
            }),
            InodeInner::Dir(_) => Ok(Metadata {
                file_type: FileType::Dir,
                len: 0,
            }),
            InodeInner::Empty => panic!("Empty inode found"),
        }
    }

    pub fn seek(&mut self, file: &mut FileInner, pos: SeekFrom) -> Result<u64, Error>
    {
        let len = self.metadata_inner(file)?.len;

        let file = &mut self.files[file.val];

        debug_assert!(!file.closed, "Attempted to use closed file");

        match pos
        {
            SeekFrom::Start(x) => file.pos = x as _,
            SeekFrom::End(x) =>
            {
                file.pos = ((len as i64) + x) as _;
            }
            SeekFrom::Current(x) => file.pos = ((file.pos as i64) + x) as _,
        }

        Ok(file.pos as _)
    }

    pub fn create_dir(&mut self, path: &Path) -> Result<(), Error>
    {
        let filename = if let Component::Normal(filename) =
            path.components().last().ok_or(ErrorKind::InvalidInput)?
        {
            filename
        }
        else
        {
            return Err(From::from(ErrorKind::InvalidInput));
        };

        let old_content_len = self.content.len();

        let inode = self.get_mut_inode_by_id(
            self.get_inode_by_full_path(path)?
                .err()
                .ok_or(ErrorKind::AlreadyExists)?
                .id,
        )?;

        let old_inode_id = inode.id;

        match &mut inode.inner
        {
            InodeInner::Dir(dir) =>
            {
                dir.insert(filename.to_owned(), old_content_len)
                    .ok_or(())
                    .expect_err("The directory shouldn't have this file already");

                self.content.push(Inode {
                    inner: InodeInner::Dir(
                        [
                            (OsString::from("."), old_content_len),
                            (OsString::from(".."), old_inode_id),
                        ]
                        .into(),
                    ),
                    id: old_content_len,
                });

                Ok(())
            }
            InodeInner::File(_) => Err(From::from(ErrorKind::NotADirectory)),
            InodeInner::Empty => panic!("Empty inode found"),
        }
    }

    pub fn create_dir_all(&mut self, path: &Path) -> Result<(), Error>
    {
        let mut full_path = PathBuf::from(".");

        for component in path.components()
        {
            full_path.push(component);

            self.create_dir(&full_path).or_else(|err| {
                if err.kind() == ErrorKind::AlreadyExists
                {
                    Ok(())
                }
                else
                {
                    Err(err)
                }
            })?;
        }

        Ok(())
    }

    pub fn close(&mut self, file: &mut FileInner)
    {
        let files = &mut self.files;
        files[file.val].closed = true;

        while !files.is_empty() && files[files.len() - 1].closed
        {
            files.pop();
        }
    }

    // This is a false positive, because the value that gets
    // "`into`-ed" is not `self`, but `walkdir` and that is actually
    // taken by value.
    #[allow(clippy::wrong_self_convention)]
    pub fn into_walkdir_iterator(
        &mut self,
        walkdir: WalkDir<VirtualDiskit>,
    ) -> WalkdirIterator<VirtualDiskit>
    {
        let pos = match self
            .get_first_walkdir_pos(&walkdir.options.path, walkdir.options.contents_first)
        {
            Ok(pos) => pos,
            Err(err) => return WalkdirIterator { inner: Err(err) },
        };
        self.walkdirs.push(WalkingDir { pos, fused: false });

        WalkdirIterator {
            inner: Ok((
                WalkdirIteratorInner {
                    walkdir: None,
                    val: self.walkdirs.len() - 1,
                    original: walkdir.options,
                },
                walkdir.diskit,
            )),
        }
    }

    // I don't see any way to shorten it yet.  TODO: Fix!
    #[allow(clippy::too_many_lines)]
    fn walkdir_next_helper(
        &mut self,
        inner: &mut WalkdirIteratorInner,
    ) -> Option<Result<DirEntry, Error>>
    {
        let walkdir = &self.walkdirs[inner.val];

        if walkdir.fused
        {
            return None;
        }

        let root_inode = try_nested!(
            try_nested!(
                self.get_inode_by_full_path(&inner.original.path),
                self,
                inner
            )
            .map_err(|_| ErrorKind::NotFound.into()),
            self,
            inner
        );

        let root_dir = if let InodeInner::Dir(dir) = &root_inode.inner
        {
            dir
        }
        else
        {
            self.walkdirs[inner.val].fused = true;
            return Some(Err(ErrorKind::NotADirectory.into()));
        };

        if walkdir.pos.is_empty()
        {
            let ino = root_inode.id as _;

            if inner.original.contents_first
            {
                let len = root_dir.len();
                self.walkdirs[inner.val].pos.push(len);
            }
            else
            {
                self.walkdirs[inner.val].pos.push(0);
            }

            return Some(Ok(DirEntry {
                path: inner.original.path.clone(),
                metadata: Metadata {
                    file_type: FileType::Dir,
                    len: 0,
                },
                follow_link: false,
                depth: 0,
                ino,
            }));
        }

        let mut inode_path = vec![0];
        let mut path = inner.original.path.clone();
        let mut inode = root_inode;

        for index in &walkdir.pos
        {
            let (content_path, &content_inode) =
                try_nested!(Self::get_dir_as_iterator(inode), self, inner).nth(*index)?;
            path.push(content_path);
            inode_path.push(content_inode);

            inode = try_nested!(self.get_inode_by_id(content_inode), self, inner);
        }

        let metadata = match &inode.inner
        {
            InodeInner::File(file) => Metadata {
                file_type: FileType::File,
                len: file.len() as _,
            },
            InodeInner::Dir(_) => Metadata {
                file_type: FileType::Dir,
                len: 0,
            },
            InodeInner::Empty => panic!("Empty inode found"),
        };

        let rv = DirEntry {
            path,
            metadata,
            follow_link: false,
            depth: walkdir.pos.len(),
            ino: inode.id as _,
        };

        let mut pos = self.walkdirs[inner.val].pos.clone();

        if inner.original.contents_first
        {
            let len = pos.len();

            pos[len - 1] += 1;
            while self.check_pos_path(root_inode, &pos).is_some()
            {
                pos.push(0);
            }
            pos.pop();
        }
        else
        {
            pos.push(0);

            if self.check_pos_path(root_inode, &pos).is_none()
            {
                pos.pop();
                loop
                {
                    let len = pos.len();

                    if len == 0
                    {
                        self.walkdirs[inner.val].fused = true;
                        break;
                    }

                    pos[len - 1] += 1;
                    if self.check_pos_path(root_inode, &pos).is_none()
                    {
                        pos.pop();
                        continue;
                    }
                    break;
                }
            }
        }

        drop(mem::replace(&mut self.walkdirs[inner.val].pos, pos));

        Some(Ok(rv))
    }

    pub fn walkdir_next_inner(
        &mut self,
        inner: &mut WalkdirIteratorInner,
    ) -> Option<Result<DirEntry, Error>>
    {
        let options = inner.original.clone();

        loop
        {
            match self.walkdir_next_helper(inner)
            {
                Some(Ok(dir_entry)) =>
                {
                    if dir_entry.depth() >= options.min_depth
                        && dir_entry.depth() <= options.max_depth
                    {
                        return Some(Ok(dir_entry));
                    }
                }
                other => return other,
            }
        }
    }

    #[cfg(feature = "trash")]
    pub fn trash_delete(&mut self, path: &Path) -> Result<(), Error>
    {
        fn get_parent_dir<'a>(
            self_: &'a mut VirtualDiskitInner,
            parent_path: &Path,
        ) -> Result<&'a mut HashMap<OsString, usize>, Error>
        {
            VirtualDiskitInner::get_mut_dir_from_inode(
                self_.get_mut_inode_by_full_path(parent_path)?,
            )
        }

        let path = self.pwd.join(path);
        let parent_path = path.parent().ok_or(ErrorKind::PermissionDenied)?.to_owned();

        if path.ends_with("..") || path.ends_with(".")
        {
            return self.trash_delete(&parent_path);
        }

        let file_inode_id = *get_parent_dir(self, &parent_path)?
            .get(path.file_name().expect("Path should have a file name"))
            .ok_or(ErrorKind::NotFound)?;

        let mut all_inodes = vec![file_inode_id];
        if self.get_dir_by_id(file_inode_id).is_ok()
        {
            self.collect_inodes(self.get_inode_by_id(file_inode_id)?, &mut all_inodes)?;
        }
        if all_inodes
            .iter()
            .any(|&id| self.files.iter().any(|file| id == file.inode_id))
        {
            return Err(ErrorKind::ResourceBusy.into());
        }

        get_parent_dir(self, &parent_path)?
            .remove(path.file_name().expect("Path should have a file name"))
            .expect("Value was proven to exist, but doesn't");

        for inode_id in all_inodes
        {
            drop(mem::replace(
                self.get_mut_inode_by_id(inode_id)?,
                Inode {
                    inner: InodeInner::Empty,
                    id: inode_id,
                },
            ));
        }

        while !self.content.is_empty()
            && self.content[self.content.len() - 1].inner == InodeInner::Empty
        {
            self.content.pop();
        }

        Ok(())
    }
}