mod appledouble;
mod apply;
mod images;
mod side_entry;
#[cfg(test)]
mod tests;
mod xattrs;
use std::ffi::{OsStr, OsString};
use std::fs::{self, Metadata};
use std::os::unix::ffi::OsStrExt as _;
use std::os::unix::fs::MetadataExt as _;
use std::path::{Path, PathBuf};
use std::sync::{MutexGuard, PoisonError};
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
use nfs3_server::nfs3_types::nfs3::{fattr3, ftype3, nfsstat3, specdata3};
use nfs3_server::vfs::FileHandleU64;
pub(super) use self::images::Images;
use self::images::{Facts, Image};
pub(super) use self::side_entry::{SIDE_STORE_DIR, remove_dir};
pub(super) use self::xattrs::Store;
use super::super::attr::nfstime;
use super::write::DEFAULT_FILE_MODE;
use super::{MachineRoot, blocking, on_disk};
#[cfg(target_os = "linux")]
const RETIREMENT_INTERVAL: Duration = Duration::from_secs(1);
pub(super) const FLUSH_IDLE: Duration = Duration::from_secs(1);
pub fn is_sidecar_name(name: &OsStr) -> bool {
target_of(name).is_some()
}
fn target_of(name: &OsStr) -> Option<&OsStr> {
let rest = name.as_bytes().strip_prefix(b"._")?;
(!rest.is_empty() && rest != b"..").then(|| OsStr::from_bytes(rest))
}
fn sidecar_name_of(target: &OsStr) -> OsString {
let mut name = OsString::from("._");
name.push(target);
name
}
#[derive(Debug, Clone)]
pub(super) struct Sidecar {
dir: u64,
name: OsString,
path: PathBuf,
target: PathBuf,
}
impl Sidecar {
fn id(&self, fs: &MachineRoot) -> u64 {
fs.ids().child(self.dir, &self.name)
}
fn existing_id(&self, fs: &MachineRoot) -> Option<u64> {
fs.ids().lookup(self.dir, &self.name)
}
}
fn changed_at(meta: &Metadata) -> SystemTime {
let seconds = u64::try_from(meta.ctime()).unwrap_or(0);
let nanoseconds = u32::try_from(meta.ctime_nsec()).unwrap_or(0);
UNIX_EPOCH + Duration::new(seconds, nanoseconds)
}
fn slice(bytes: &[u8], offset: u64, count: u32) -> (Vec<u8>, bool) {
let start = usize::try_from(offset)
.unwrap_or(usize::MAX)
.min(bytes.len());
let end = start.saturating_add(count as usize).min(bytes.len());
(bytes[start..end].to_vec(), end >= bytes.len())
}
impl MachineRoot {
pub(super) fn images(&self) -> MutexGuard<'_, Images> {
self.images.lock().unwrap_or_else(PoisonError::into_inner)
}
pub(super) async fn sidecar_at(
&self,
dirid: u64,
name: &OsStr,
path: &Path,
) -> Result<Option<Sidecar>, nfsstat3> {
let Some(target) = target_of(name) else {
return Ok(None);
};
if on_disk(path.to_path_buf()).await? {
return Ok(None);
}
let dir_path = self.path_of(dirid)?;
let target = if target == "." {
dir_path
} else {
dir_path.join(target)
};
Ok(Some(Sidecar {
dir: dirid,
name: name.to_owned(),
path: path.to_path_buf(),
target,
}))
}
pub(super) async fn sidecar_of(&self, id: u64) -> Result<Option<Sidecar>, nfsstat3> {
let (dir, name) = {
let ids = self.ids();
match ids.entry(id) {
Some((dir, name)) if is_sidecar_name(name) => (dir, name.to_owned()),
_ => return Ok(None),
}
};
let path = self.path_of(id)?;
self.sidecar_at(dir, &name, &path).await
}
pub(super) async fn lookup_sidecar(
&self,
dirid: u64,
name: &OsStr,
path: &Path,
) -> Result<FileHandleU64, nfsstat3> {
let Some(sidecar) = self.sidecar_at(dirid, name, path).await? else {
return Ok(self.handle_for(dirid, name));
};
match self.image_facts(&sidecar).await? {
Some(_) => Ok(FileHandleU64::new(sidecar.id(self))),
None => Err(nfsstat3::NFS3ERR_NOENT),
}
}
async fn image_facts(&self, sidecar: &Sidecar) -> Result<Option<Facts>, nfsstat3> {
let now = Instant::now();
if let Some(id) = sidecar.existing_id(self)
&& let Some(image) = self.images().get(id, now)
{
return Ok(Some(image.facts()));
}
self.synthesize(sidecar).await
}
async fn synthesize(&self, sidecar: &Sidecar) -> Result<Option<Facts>, nfsstat3> {
let (target, store) = (sidecar.target.clone(), self.store);
let loaded = blocking(move || {
let content = store.load(&target)?;
let meta = fs::symlink_metadata(&target)?;
Ok(content.map(|content| (content, meta)))
})
.await;
let (content, meta) = match loaded {
Ok(Some(loaded)) => loaded,
Ok(None) | Err(nfsstat3::NFS3ERR_NOENT) => return Ok(None),
Err(e) => return Err(e),
};
let now = Instant::now();
let image = Image::synthesized(content.to_bytes(), changed_at(&meta), now);
let facts = image.facts();
let id = sidecar.id(self);
let evicted = self.images().insert(id, image, now);
self.settle_all(evicted).await;
Ok(Some(facts))
}
pub(super) async fn read_sidecar(
&self,
sidecar: &Sidecar,
offset: u64,
count: u32,
) -> Result<(Vec<u8>, bool), nfsstat3> {
if self.image_facts(sidecar).await?.is_none() {
return Err(nfsstat3::NFS3ERR_STALE);
}
let id = sidecar.id(self);
let mut images = self.images();
let image = images
.get(id, Instant::now())
.ok_or(nfsstat3::NFS3ERR_STALE)?;
Ok(slice(&image.bytes, offset, count))
}
pub(super) async fn sidecar_attr(&self, sidecar: &Sidecar) -> Result<fattr3, nfsstat3> {
let facts = self
.image_facts(sidecar)
.await?
.ok_or(nfsstat3::NFS3ERR_STALE)?;
let target = sidecar.target.clone();
let owner = match blocking(move || fs::symlink_metadata(&target)).await {
Ok(meta) => Some(meta),
Err(nfsstat3::NFS3ERR_NOENT) => None,
Err(e) => return Err(e),
};
let (uid, gid, mode) = match &owner {
Some(meta) => (meta.uid(), meta.gid(), meta.mode() & 0o666),
None => {
let (uid, gid) = self.map.guest_owner();
(uid, gid, DEFAULT_FILE_MODE)
}
};
let elapsed = facts
.modified
.duration_since(UNIX_EPOCH)
.unwrap_or_default();
let time = nfstime(
i64::try_from(elapsed.as_secs()).unwrap_or(i64::MAX),
i64::from(elapsed.subsec_nanos()),
);
Ok(fattr3 {
type_: ftype3::NF3REG,
mode: facts.mode.unwrap_or(mode),
nlink: 1,
uid: self.map.uid(uid),
gid: self.map.gid(gid),
size: facts.len,
used: facts.len,
rdev: specdata3::default(),
fsid: 0,
fileid: sidecar.id(self),
atime: time,
mtime: time,
ctime: time,
})
}
#[cfg(target_os = "linux")]
pub fn retire_sidecars_forever(&self) -> impl Future<Output = ()> + Send + 'static {
let fs = self.clone();
async move {
let mut tick = tokio::time::interval(RETIREMENT_INTERVAL);
loop {
tick.tick().await;
fs.retire_sidecars(Instant::now()).await;
}
}
}
pub(super) async fn retire_sidecars(&self, now: Instant) {
self.flush_idle(now).await;
let retired = self.images().retire(now);
self.settle_all(retired).await;
}
}