use std::collections::{HashMap, VecDeque};
use std::ffi::{OsStr, OsString};
use std::os::unix::ffi::OsStrExt as _;
use std::path::Path;
use std::sync::{MutexGuard, PoisonError};
use std::time::{SystemTime, UNIX_EPOCH};
use nfs3_server::nfs3_types::nfs3::{
fattr3, ftype3, nfsstat3, nfstime3, sattr3, set_mode3, set_size3, specdata3,
};
use nfs3_server::vfs::FileHandleU64;
use super::super::attr::IdMap;
use super::write::DEFAULT_FILE_MODE;
use super::{MachineRoot, on_disk};
const CAPACITY: usize = 4096;
pub fn is_sidecar_name(name: &OsStr) -> bool {
let bytes = name.as_bytes();
bytes.len() > 2 && bytes.starts_with(b"._")
}
#[derive(Debug)]
struct Sidecar {
dir: u64,
name: OsString,
data: Vec<u8>,
mode: u32,
modified: SystemTime,
}
#[derive(Debug, Default)]
pub struct Sidecars {
by_id: HashMap<u64, Sidecar>,
by_name: HashMap<u64, HashMap<OsString, u64>>,
order: VecDeque<u64>,
}
impl Sidecars {
fn lookup(&self, dir: u64, name: &OsStr) -> Option<u64> {
self.by_name.get(&dir)?.get(name).copied()
}
fn contains(&self, id: u64) -> bool {
self.by_id.contains_key(&id)
}
fn insert(&mut self, id: u64, dir: u64, name: &OsStr) {
self.by_id.insert(
id,
Sidecar {
dir,
name: name.to_owned(),
data: Vec::new(),
mode: DEFAULT_FILE_MODE,
modified: SystemTime::now(),
},
);
self.by_name
.entry(dir)
.or_default()
.insert(name.to_owned(), id);
self.order.push_back(id);
while self.by_id.len() > CAPACITY {
let Some(oldest) = self.order.pop_front() else {
break;
};
self.remove(oldest);
}
}
fn remove(&mut self, id: u64) -> bool {
let Some(sidecar) = self.by_id.remove(&id) else {
return false;
};
if let Some(names) = self.by_name.get_mut(&sidecar.dir) {
names.remove(&sidecar.name);
if names.is_empty() {
self.by_name.remove(&sidecar.dir);
}
}
true
}
fn remove_named(&mut self, dir: u64, name: &OsStr) -> bool {
self.lookup(dir, name).is_some_and(|id| self.remove(id))
}
fn rename(&mut self, id: u64, to_dir: u64, to_name: &OsStr) {
let Some(sidecar) = self.by_id.get_mut(&id) else {
return;
};
let (from_dir, from_name) = (sidecar.dir, std::mem::take(&mut sidecar.name));
sidecar.dir = to_dir;
to_name.clone_into(&mut sidecar.name);
if let Some(names) = self.by_name.get_mut(&from_dir) {
names.remove(&from_name);
}
self.by_name
.entry(to_dir)
.or_default()
.insert(to_name.to_owned(), id);
}
fn read(&self, id: u64, offset: u64, count: u32) -> Option<(Vec<u8>, bool)> {
let data = &self.by_id.get(&id)?.data;
let start = usize::try_from(offset)
.unwrap_or(usize::MAX)
.min(data.len());
let end = start.saturating_add(count as usize).min(data.len());
Some((data[start..end].to_vec(), end >= data.len()))
}
fn write(&mut self, id: u64, offset: u64, bytes: &[u8], map: IdMap) -> Option<fattr3> {
let sidecar = self.by_id.get_mut(&id)?;
let start = usize::try_from(offset).ok()?;
let end = start.checked_add(bytes.len())?;
if sidecar.data.len() < end {
sidecar.data.resize(end, 0);
}
sidecar.data[start..end].copy_from_slice(bytes);
sidecar.modified = SystemTime::now();
self.attr(id, map)
}
fn setattr(&mut self, id: u64, attr: &sattr3, map: IdMap) -> Option<fattr3> {
let sidecar = self.by_id.get_mut(&id)?;
if let set_mode3::Some(mode) = attr.mode {
sidecar.mode = mode & 0o7777;
}
if let set_size3::Some(size) = attr.size {
sidecar
.data
.resize(usize::try_from(size).unwrap_or(usize::MAX), 0);
sidecar.modified = SystemTime::now();
}
self.attr(id, map)
}
fn attr(&self, id: u64, map: IdMap) -> Option<fattr3> {
let sidecar = self.by_id.get(&id)?;
let (guest_uid, guest_gid) = map.guest_owner();
let size = sidecar.data.len() as u64;
let elapsed = sidecar
.modified
.duration_since(UNIX_EPOCH)
.unwrap_or_default();
let time = nfstime3 {
seconds: u32::try_from(elapsed.as_secs()).unwrap_or(u32::MAX),
nseconds: elapsed.subsec_nanos(),
};
Some(fattr3 {
type_: ftype3::NF3REG,
mode: sidecar.mode,
nlink: 1,
uid: map.uid(guest_uid),
gid: map.gid(guest_gid),
size,
used: size,
rdev: specdata3::default(),
fsid: 0,
fileid: id,
atime: time,
mtime: time,
ctime: time,
})
}
fn remove_in_dir(&mut self, dir: u64) {
if let Some(names) = self.by_name.remove(&dir) {
for id in names.into_values() {
self.by_id.remove(&id);
}
}
}
}
impl MachineRoot {
fn sidecars(&self) -> MutexGuard<'_, Sidecars> {
self.sidecars.lock().unwrap_or_else(PoisonError::into_inner)
}
pub(super) async fn lookup_sidecar(
&self,
dirid: u64,
name: &OsStr,
path: &Path,
) -> Result<FileHandleU64, nfsstat3> {
if on_disk(path.to_path_buf()).await? {
self.sidecars().remove_named(dirid, name);
return Ok(self.handle_for(dirid, name));
}
self.sidecars()
.lookup(dirid, name)
.map(FileHandleU64::new)
.ok_or(nfsstat3::NFS3ERR_NOENT)
}
pub(super) fn is_sidecar(&self, id: u64) -> bool {
self.sidecars().contains(id)
}
pub(super) fn sidecar_attr(&self, id: u64) -> Option<fattr3> {
self.sidecars().attr(id, self.map)
}
pub(super) fn read_sidecar(&self, id: u64, offset: u64, count: u32) -> Option<(Vec<u8>, bool)> {
self.sidecars().read(id, offset, count)
}
pub(super) fn write_sidecar(&self, id: u64, offset: u64, data: &[u8]) -> Option<fattr3> {
self.sidecars().write(id, offset, data, self.map)
}
pub(super) fn setattr_sidecar(&self, id: u64, attr: &sattr3) -> Option<fattr3> {
self.sidecars().setattr(id, attr, self.map)
}
pub(super) async fn shadows(&self, name: &OsStr, path: &Path) -> Result<bool, nfsstat3> {
Ok(is_sidecar_name(name) && !on_disk(path.to_path_buf()).await?)
}
pub(super) fn create_sidecar(
&self,
dirid: u64,
name: &OsStr,
attr: &sattr3,
exclusive: bool,
) -> Result<(FileHandleU64, fattr3), nfsstat3> {
let id = self.ids().child(dirid, name);
let mut sidecars = self.sidecars();
if sidecars.contains(id) {
if exclusive {
return Err(nfsstat3::NFS3ERR_EXIST);
}
} else {
sidecars.insert(id, dirid, name);
}
let attr = sidecars
.setattr(id, attr, self.map)
.ok_or(nfsstat3::NFS3ERR_SERVERFAULT)?;
Ok((FileHandleU64::new(id), attr))
}
pub(super) fn remove_sidecar(&self, dirid: u64, name: &OsStr) -> bool {
if !self.sidecars().remove_named(dirid, name) {
return false;
}
self.ids().forget(dirid, name);
true
}
pub(super) fn rename_sidecar(
&self,
from_dirid: u64,
from_name: &OsStr,
to_dirid: u64,
to_name: &OsStr,
) -> Option<Result<(), nfsstat3>> {
let id = self.sidecars().lookup(from_dirid, from_name)?;
if !is_sidecar_name(to_name) {
return Some(Err(nfsstat3::NFS3ERR_XDEV));
}
self.remove_sidecar(to_dirid, to_name);
self.sidecars().rename(id, to_dirid, to_name);
self.ids().rename(from_dirid, from_name, to_dirid, to_name);
Some(Ok(()))
}
pub(super) fn drop_sidecars_in(&self, dirid: u64, name: &OsStr) {
let dir = self.ids().lookup(dirid, name);
if let Some(dir) = dir {
self.sidecars().remove_in_dir(dir);
}
}
}
#[cfg(test)]
mod tests {
use std::os::unix::fs::MetadataExt as _;
use nfs3_server::nfs3_types::nfs3::{filename3, stable_how};
use nfs3_server::vfs::{NextResult, NfsFileSystem, NfsReadFileSystem, ReadDirPlusIterator};
use super::*;
#[test]
fn only_a_dot_underscore_prefix_with_a_name_is_a_sidecar() {
assert!(is_sidecar_name(OsStr::new("._pack.idx")));
assert!(!is_sidecar_name(OsStr::new("._")));
assert!(!is_sidecar_name(OsStr::new(".hidden")));
assert!(!is_sidecar_name(OsStr::new("pack.idx")));
}
#[test]
fn the_oldest_sidecar_goes_when_the_table_is_full() {
let mut sidecars = Sidecars::default();
for i in 0..=CAPACITY as u64 {
sidecars.insert(i + 10, 1, OsStr::new(&format!("._{i}")));
}
assert_eq!(sidecars.by_id.len(), CAPACITY);
assert!(!sidecars.contains(10), "the first one inserted was evicted");
assert_eq!(sidecars.lookup(1, OsStr::new("._0")), None);
assert!(sidecars.contains(10 + CAPACITY as u64));
}
fn name(s: &str) -> filename3<'_> {
filename3::from(s.as_bytes())
}
#[tokio::test]
async fn the_macs_sidecars_stay_in_memory() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(dir.path().join("f"), b"file").unwrap();
std::fs::write(dir.path().join("._real"), b"machine").unwrap();
let me = std::fs::metadata(dir.path()).unwrap();
let map = IdMap::new(501, 20, me.uid(), me.gid());
let fs = MachineRoot::new(dir.path().to_path_buf(), map);
let root = fs.root_dir();
let (sc, attr) = fs
.create(&root, &name("._f"), sattr3::default())
.await
.unwrap();
assert_eq!(
(attr.size, attr.uid, attr.mode),
(0, 501, DEFAULT_FILE_MODE)
);
assert_eq!(
fs.create_exclusive(&root, &name("._f"), Default::default())
.await,
Err(nfsstat3::NFS3ERR_EXIST)
);
let (attr, stable) = fs
.write(&sc, 0, b"AppleDouble", stable_how::UNSTABLE)
.await
.unwrap();
assert_eq!((attr.size, stable), (11, stable_how::FILE_SYNC));
fs.commit(&sc, 0, 0).await.unwrap();
assert_eq!(
fs.read(&sc, 5, 100).await.unwrap(),
(b"Double".to_vec(), true)
);
assert_eq!(fs.lookup(&root, &name("._f")).await.unwrap(), sc);
assert_eq!(fs.getattr(&sc).await.unwrap().size, 11);
assert_eq!(fs.readlink(&sc).await, Err(nfsstat3::NFS3ERR_INVAL));
assert!(!dir.path().join("._f").exists());
let mut listing = fs.readdirplus(&root, 0).await.unwrap();
let mut names = Vec::new();
while let NextResult::Ok(entry) = listing.next().await {
names.push(String::from_utf8(entry.name.as_ref().to_vec()).unwrap());
}
names.sort();
assert_eq!(names, ["._real", "f"]);
let real = fs.lookup(&root, &name("._real")).await.unwrap();
assert_eq!(fs.read(&real, 0, 100).await.unwrap().0, b"machine");
fs.create(&root, &name("._real"), sattr3::default())
.await
.unwrap();
assert_eq!(
std::fs::read(dir.path().join("._real")).unwrap(),
b"machine"
);
assert_eq!(
fs.rename(&root, &name("._f"), &root, &name("g")).await,
Err(nfsstat3::NFS3ERR_XDEV)
);
fs.rename(&root, &name("._f"), &root, &name("._g"))
.await
.unwrap();
assert_eq!(
fs.lookup(&root, &name("._f")).await,
Err(nfsstat3::NFS3ERR_NOENT)
);
assert_eq!(fs.lookup(&root, &name("._g")).await.unwrap(), sc);
let attr = sattr3 {
size: set_size3::Some(5),
mode: set_mode3::Some(0o600),
..sattr3::default()
};
let attr = fs.setattr(&sc, attr).await.unwrap();
assert_eq!((attr.size, attr.mode), (5, 0o600));
assert_eq!(fs.read(&sc, 0, 100).await.unwrap().0, b"Apple");
std::fs::write(dir.path().join("._g"), b"disk").unwrap();
let g = fs.lookup(&root, &name("._g")).await.unwrap();
assert_eq!(fs.read(&g, 0, 100).await.unwrap().0, b"disk");
std::fs::remove_file(dir.path().join("._g")).unwrap();
assert_eq!(
fs.lookup(&root, &name("._g")).await,
Err(nfsstat3::NFS3ERR_NOENT)
);
let (sc, _) = fs
.create(&root, &name("._h"), sattr3::default())
.await
.unwrap();
fs.remove(&root, &name("._h")).await.unwrap();
assert!(matches!(
fs.getattr(&sc).await,
Err(nfsstat3::NFS3ERR_STALE)
));
assert_eq!(
fs.remove(&root, &name("._h")).await,
Err(nfsstat3::NFS3ERR_NOENT)
);
let (d, _) = fs.mkdir(&root, &name("d")).await.unwrap();
let (q, _) = fs
.create(&d, &name("._q"), sattr3::default())
.await
.unwrap();
fs.remove(&root, &name("d")).await.unwrap();
assert!(matches!(fs.getattr(&q).await, Err(nfsstat3::NFS3ERR_STALE)));
let mut on_disk: Vec<_> = std::fs::read_dir(dir.path())
.unwrap()
.map(|e| e.unwrap().file_name().into_string().unwrap())
.collect();
on_disk.sort();
assert_eq!(on_disk, ["._real", "f"]);
}
}