use std::{
ffi::{OsStr, OsString},
path::Path,
time::SystemTime,
};
use objects::object::FileMode;
use crate::error::{MountError, Result};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct NodeId(pub u64);
impl NodeId {
pub const ROOT: NodeId = NodeId(1);
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum NodeKind {
Directory,
File,
Symlink,
}
#[derive(Clone, Debug)]
pub struct Entry {
pub node: NodeId,
pub name: OsString,
pub kind: NodeKind,
pub size: u64,
pub unix_mode: u32,
}
#[derive(Clone, Copy, Debug)]
pub struct Attrs {
pub node: NodeId,
pub kind: NodeKind,
pub size: u64,
pub unix_mode: u32,
pub nlink: u32,
pub mtime: SystemTime,
}
pub trait PlatformShell {
fn lookup(&self, parent: NodeId, name: &OsStr) -> Result<Option<Entry>>;
fn read(&self, node: NodeId, offset: u64, buf: &mut [u8]) -> Result<usize>;
fn write(&self, node: NodeId, offset: u64, data: &[u8]) -> Result<usize>;
fn enumerate(&self, dir: NodeId) -> Result<Vec<Entry>>;
fn attrs(&self, node: NodeId) -> Result<Attrs>;
fn invalidate(&self, node: NodeId) -> Result<()>;
fn flush(&self, _node: NodeId) -> Result<()> {
Ok(())
}
fn release(&self, node: NodeId) -> Result<()> {
self.flush(node)
}
fn on_open(&self, _node: NodeId) -> Result<()> {
Ok(())
}
fn create_file(
&self,
_parent: NodeId,
_name: &OsStr,
_mode: FileMode,
_exclusive: bool,
) -> Result<Entry> {
Err(MountError::ReadOnly)
}
fn make_dir(&self, _parent: NodeId, _name: &OsStr) -> Result<Entry> {
Err(MountError::ReadOnly)
}
fn unlink_entry(&self, _parent: NodeId, _name: &OsStr) -> Result<()> {
Err(MountError::ReadOnly)
}
fn rmdir_entry(&self, _parent: NodeId, _name: &OsStr) -> Result<()> {
Err(MountError::ReadOnly)
}
fn rename_entry(
&self,
_old_parent: NodeId,
_old_name: &OsStr,
_new_parent: NodeId,
_new_name: &OsStr,
) -> Result<()> {
Err(MountError::ReadOnly)
}
fn rename_entry_with_options(
&self,
old_parent: NodeId,
old_name: &OsStr,
new_parent: NodeId,
new_name: &OsStr,
_options: RenameOptions,
) -> Result<()> {
self.rename_entry(old_parent, old_name, new_parent, new_name)
}
fn set_attrs(&self, _node: NodeId, _update: AttrUpdate) -> Result<Attrs> {
Err(MountError::ReadOnly)
}
fn create_symlink(&self, _parent: NodeId, _name: &OsStr, _target: &Path) -> Result<Entry> {
Err(MountError::ReadOnly)
}
fn read_link(&self, _node: NodeId) -> Result<OsString> {
Err(MountError::ReadOnly)
}
}
#[derive(Clone, Copy, Debug, Default)]
pub struct AttrUpdate {
pub mode: Option<u32>,
pub uid: Option<u32>,
pub gid: Option<u32>,
pub size: Option<u64>,
pub mtime_sec: Option<i64>,
}
pub(crate) fn kind_for_mode(mode: FileMode) -> NodeKind {
match mode {
FileMode::Normal | FileMode::Executable | FileMode::Gitlink => NodeKind::File,
FileMode::Symlink => NodeKind::Symlink,
FileMode::Spoollink => NodeKind::File,
}
}
pub(crate) const DIR_UNIX_MODE: u32 = 0o040755;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct RenameOptions {
pub no_replace: bool,
}
#[cfg(test)]
mod tests {
use std::{cell::Cell, ffi::OsStr, time::UNIX_EPOCH};
use super::*;
#[derive(Default)]
struct StubShell {
flush_calls: Cell<u32>,
rename_calls: Cell<u32>,
}
impl PlatformShell for StubShell {
fn lookup(&self, _parent: NodeId, _name: &OsStr) -> Result<Option<Entry>> {
Ok(None)
}
fn read(&self, _node: NodeId, _offset: u64, _buf: &mut [u8]) -> Result<usize> {
Ok(0)
}
fn write(&self, _node: NodeId, _offset: u64, data: &[u8]) -> Result<usize> {
Ok(data.len())
}
fn enumerate(&self, _dir: NodeId) -> Result<Vec<Entry>> {
Ok(Vec::new())
}
fn attrs(&self, node: NodeId) -> Result<Attrs> {
Ok(Attrs {
node,
kind: NodeKind::File,
size: 0,
unix_mode: 0o100644,
nlink: 1,
mtime: UNIX_EPOCH,
})
}
fn invalidate(&self, _node: NodeId) -> Result<()> {
Ok(())
}
fn flush(&self, _node: NodeId) -> Result<()> {
self.flush_calls.set(self.flush_calls.get() + 1);
Ok(())
}
fn rename_entry(&self, _op: NodeId, _on: &OsStr, _np: NodeId, _nn: &OsStr) -> Result<()> {
self.rename_calls.set(self.rename_calls.get() + 1);
Ok(())
}
}
fn is_read_only<T>(r: Result<T>) -> bool {
matches!(r, Err(MountError::ReadOnly))
}
#[test]
fn write_side_defaults_return_read_only() {
let s = StubShell::default();
let p = NodeId::ROOT;
let name = OsStr::new("x");
assert!(is_read_only(s.create_file(
p,
name,
FileMode::Normal,
false
),));
assert!(is_read_only(s.make_dir(p, name)));
assert!(is_read_only(s.unlink_entry(p, name)));
assert!(is_read_only(s.rmdir_entry(p, name)));
assert!(is_read_only(s.set_attrs(NodeId(2), AttrUpdate::default())));
assert!(is_read_only(s.create_symlink(p, name, Path::new("target")),));
assert!(is_read_only(s.read_link(NodeId(2))));
}
#[test]
fn on_open_default_is_noop() {
let s = StubShell::default();
assert!(s.on_open(NodeId(7)).is_ok());
}
#[test]
fn release_default_delegates_to_flush() {
let s = StubShell::default();
assert_eq!(s.flush_calls.get(), 0);
s.release(NodeId(3)).expect("release");
assert_eq!(
s.flush_calls.get(),
1,
"release default must invoke flush exactly once",
);
}
#[test]
fn rename_with_options_default_delegates_to_rename_entry() {
let s = StubShell::default();
let opts = RenameOptions { no_replace: true };
s.rename_entry_with_options(NodeId(1), OsStr::new("a"), NodeId(1), OsStr::new("b"), opts)
.expect("rename");
assert_eq!(s.rename_calls.get(), 1);
assert!(opts.no_replace, "RenameOptions field survives copy");
assert_eq!(
RenameOptions::default(),
RenameOptions { no_replace: false }
);
}
#[test]
fn kind_for_mode_maps_each_file_mode() {
assert_eq!(kind_for_mode(FileMode::Normal), NodeKind::File);
assert_eq!(kind_for_mode(FileMode::Executable), NodeKind::File);
assert_eq!(kind_for_mode(FileMode::Symlink), NodeKind::Symlink);
assert_eq!(kind_for_mode(FileMode::Spoollink), NodeKind::File);
}
}