use std::ffi::OsStr;
use std::fs::Permissions;
use std::os::unix::fs::PermissionsExt as _;
use std::path::Path;
use std::time::Instant;
use nfs3_server::nfs3_types::nfs3::{fattr3, nfsstat3, sattr3, set_mode3, set_size3, stable_how};
use nfs3_server::vfs::FileHandleU64;
use super::appledouble::{AppleDouble, ParseError};
use super::images::Image;
use super::side_entry;
use super::xattrs::{Mode, Transfer};
use super::{FLUSH_IDLE, Sidecar, is_sidecar_name, sidecar_name_of};
use crate::machine_export::vfs::write::DEFAULT_FILE_MODE;
use crate::machine_export::vfs::{MachineRoot, blocking};
impl MachineRoot {
pub(in crate::machine_export::vfs) async fn create_sidecar(
&self,
sidecar: Sidecar,
attr: &sattr3,
exclusive: bool,
) -> Result<(FileHandleU64, fattr3), nfsstat3> {
if exclusive && self.image_facts(&sidecar).await?.is_some() {
return Err(nfsstat3::NFS3ERR_EXIST);
}
let id = sidecar.id(self);
self.start_image(id).await;
self.resize_and_chmod(id, attr)?;
let fattr = self.sidecar_attr(&sidecar).await?;
Ok((FileHandleU64::new(id), fattr))
}
pub(in crate::machine_export::vfs) async fn setattr_sidecar(
&self,
sidecar: &Sidecar,
attr: &sattr3,
) -> Result<fattr3, nfsstat3> {
if self.image_facts(sidecar).await?.is_none() {
return Err(nfsstat3::NFS3ERR_STALE);
}
self.resize_and_chmod(sidecar.id(self), attr)?;
self.sidecar_attr(sidecar).await
}
pub(in crate::machine_export::vfs) async fn write_sidecar(
&self,
sidecar: &Sidecar,
offset: u64,
data: &[u8],
stable: stable_how,
) -> Result<(fattr3, stable_how), nfsstat3> {
let id = sidecar.id(self);
if self.image_facts(sidecar).await?.is_none() {
self.start_image(id).await;
}
self.images().write(id, offset, data, Instant::now())?;
Ok((self.sidecar_attr(sidecar).await?, stable))
}
pub(in crate::machine_export::vfs) async fn commit_sidecar(
&self,
sidecar: &Sidecar,
) -> Result<(), nfsstat3> {
self.flush(sidecar).await
}
pub(in crate::machine_export::vfs) async fn remove_sidecar(
&self,
sidecar: Sidecar,
) -> Result<(), nfsstat3> {
let had_image = sidecar
.existing_id(self)
.is_some_and(|id| self.images().remove(id).is_some());
let (target, store) = (sidecar.target.clone(), self.store);
let had_attrs = match blocking(move || store.remove(&target)).await {
Ok(any) => any,
Err(nfsstat3::NFS3ERR_NOENT) => false,
Err(e) => return Err(e),
};
self.ids().forget(sidecar.dir, &sidecar.name);
if had_image || had_attrs {
Ok(())
} else {
Err(nfsstat3::NFS3ERR_NOENT)
}
}
pub(in crate::machine_export::vfs) async fn rename_sidecar(
&self,
from: Sidecar,
to_dirid: u64,
to_name: &OsStr,
to_path: &Path,
) -> Result<(), nfsstat3> {
let Some(to) = self.sidecar_at(to_dirid, to_name, to_path).await? else {
return Err(if is_sidecar_name(to_name) {
nfsstat3::NFS3ERR_EXIST
} else {
nfsstat3::NFS3ERR_XDEV
});
};
if let Some(replaced) = to.existing_id(self) {
self.images().remove(replaced);
}
let has_image = from
.existing_id(self)
.is_some_and(|id| self.images().contains(id));
let (from_target, to_target, store) = (from.target.clone(), to.target.clone(), self.store);
let transferred = blocking(move || store.transfer(&from_target, &to_target)).await?;
self.ids().rename(from.dir, &from.name, to.dir, &to.name);
if has_image {
self.flush(&to).await?;
}
match transferred {
Transfer::Parked(content) if !has_image => {
let now = Instant::now();
let id = to.id(self);
let evicted =
self.images()
.insert(id, Image::pending(content.to_bytes(), now), now);
self.settle_all(evicted).await;
Ok(())
}
Transfer::Parked(_) | Transfer::Moved => Ok(()),
Transfer::Nothing if has_image || self.image_facts(&to).await?.is_some() => Ok(()),
Transfer::Nothing => Err(nfsstat3::NFS3ERR_NOENT),
}
}
pub(in crate::machine_export::vfs) async fn target_created(
&self,
dirid: u64,
name: &OsStr,
path: &Path,
) {
let Some(sidecar) = self.pending_sidecar_of(dirid, name, path) else {
return;
};
if let Err(e) = self.flush(&sidecar).await {
tracing::warn!(target = %path.display(), error = ?e, "machine export: a sidecar written ahead of its file could not be applied");
}
}
pub(in crate::machine_export::vfs) async fn target_removed(
&self,
dirid: u64,
name: &OsStr,
path: &Path,
) {
self.forget_sidecar(dirid, name);
let path = path.to_path_buf();
if let Err(e) = blocking(move || side_entry::drop_for(&path)).await {
tracing::warn!(error = ?e, "machine export: a removed file's side entry stays behind");
}
}
pub(in crate::machine_export::vfs) async fn target_renaming(
&self,
dirid: u64,
name: &OsStr,
path: &Path,
) {
let Some(sidecar) = self.pending_sidecar_of(dirid, name, path) else {
return;
};
if let Err(e) = self.flush(&sidecar).await {
tracing::warn!(target = %path.display(), error = ?e, "machine export: a sidecar still being written was not applied before the rename");
}
}
pub(in crate::machine_export::vfs) async fn target_renamed(
&self,
(from_dirid, from_name, from_path): (u64, &OsStr, &Path),
(to_dirid, to_name, to_path): (u64, &OsStr, &Path),
) {
self.forget_sidecar(from_dirid, from_name);
let replaced = self.ids().lookup(to_dirid, to_name);
if let Some(replaced) = replaced {
self.images().remove(replaced);
}
if self
.pending_sidecar_of(to_dirid, to_name, to_path)
.is_none()
{
self.forget_sidecar(to_dirid, to_name);
}
let (from, to) = (from_path.to_path_buf(), to_path.to_path_buf());
if let Err(e) = blocking(move || side_entry::follow_rename(&from, &to)).await {
tracing::warn!(error = ?e, "machine export: a renamed file's side entry did not follow it");
}
self.target_created(to_dirid, to_name, to_path).await;
}
fn pending_sidecar_of(&self, dirid: u64, name: &OsStr, path: &Path) -> Option<Sidecar> {
let sidecar_name = sidecar_name_of(name);
let id = self.ids().lookup(dirid, &sidecar_name)?;
let dirty = self.images().peek(id).is_some_and(|image| image.dirty);
dirty.then(|| Sidecar {
dir: dirid,
path: path.with_file_name(&sidecar_name),
name: sidecar_name,
target: path.to_path_buf(),
})
}
fn forget_sidecar(&self, dirid: u64, name: &OsStr) {
let sidecar_name = sidecar_name_of(name);
let id = self.ids().lookup(dirid, &sidecar_name);
if let Some(id) = id {
self.images().remove(id);
self.ids().forget(dirid, &sidecar_name);
}
}
async fn start_image(&self, id: u64) {
let now = Instant::now();
let evicted = self.images().insert(id, Image::fresh(now), now);
self.settle_all(evicted).await;
}
fn resize_and_chmod(&self, id: u64, attr: &sattr3) -> Result<(), nfsstat3> {
let now = Instant::now();
let mut images = self.images();
if let set_size3::Some(size) = attr.size {
images.truncate(id, size, now)?;
}
if let set_mode3::Some(mode) = attr.mode
&& let Some(image) = images.get_mut(id, now)
{
image.mode = Some(mode & 0o7777);
}
Ok(())
}
async fn flush(&self, sidecar: &Sidecar) -> Result<(), nfsstat3> {
let Some(id) = sidecar.existing_id(self) else {
return Ok(());
};
let (bytes, generation, mode) = {
let images = self.images();
match images.peek(id) {
Some(image) if image.dirty => (
image.bytes.clone(),
image.generation,
if image.merge {
Mode::Merge
} else {
Mode::Replace
},
),
_ => return Ok(()),
}
};
match AppleDouble::parse(&bytes) {
Ok(content) => {
let (target, store) = (sidecar.target.clone(), self.store);
match blocking(move || store.store(&target, &content, mode)).await {
Ok(()) => {
let mut images = self.images();
match mode {
Mode::Merge => {
images.remove(id);
}
Mode::Replace => {
if let Some(image) = images.peek_mut(id)
&& image.generation == generation
{
image.dirty = false;
}
}
}
Ok(())
}
Err(nfsstat3::NFS3ERR_NOENT) => {
self.tried(id, generation);
Ok(())
}
Err(e) => Err(e),
}
}
Err(ParseError::Incomplete) => {
self.tried(id, generation);
Ok(())
}
Err(ParseError::Invalid(why)) => {
tracing::debug!(path = %sidecar.path.display(), why, "machine export: a `._` file that is not a sidecar goes to disk");
let Some(image) = self.images().remove(id) else {
return Ok(());
};
self.materialize(sidecar, image).await
}
}
}
fn tried(&self, id: u64, generation: u64) {
if let Some(image) = self.images().peek_mut(id) {
image.tried = Some(generation);
}
}
async fn materialize(&self, sidecar: &Sidecar, image: Image) -> Result<(), nfsstat3> {
let path = sidecar.path.clone();
let (uid, gid) = self.map.guest_owner();
let mode = image.mode.unwrap_or(DEFAULT_FILE_MODE);
blocking(move || {
std::fs::write(&path, &image.bytes)?;
std::os::unix::fs::lchown(&path, Some(uid), Some(gid))?;
std::fs::set_permissions(&path, Permissions::from_mode(mode))
})
.await
}
pub(super) async fn flush_idle(&self, now: Instant) {
let due = self.images().due(now, FLUSH_IDLE);
for id in due {
let Ok(Some(sidecar)) = self.sidecar_of(id).await else {
continue;
};
if let Err(e) = self.flush(&sidecar).await {
tracing::warn!(path = %sidecar.path.display(), error = ?e, "machine export: a sidecar could not be applied");
}
}
}
pub(super) async fn settle_all(&self, images: Vec<(u64, Image)>) {
for (id, image) in images {
if image.dirty {
self.settle(id, image).await;
}
}
}
async fn settle(&self, id: u64, image: Image) {
let sidecar = match self.sidecar_of(id).await {
Ok(Some(sidecar)) => sidecar,
_ => return,
};
let mode = if image.merge {
Mode::Merge
} else {
Mode::Replace
};
match AppleDouble::parse(&image.bytes) {
Ok(content) => {
let (target, store) = (sidecar.target.clone(), self.store);
if let Err(e) = blocking(move || store.store(&target, &content, mode)).await {
tracing::debug!(path = %sidecar.path.display(), error = ?e, "machine export: dropping a sidecar whose file never came or refused it");
}
}
Err(ParseError::Incomplete) if AppleDouble::has_magic(&image.bytes) => {
tracing::debug!(path = %sidecar.path.display(), "machine export: dropping a sidecar the Mac never finished");
}
Err(_) => {
if let Err(e) = self.materialize(&sidecar, image).await {
tracing::warn!(path = %sidecar.path.display(), error = ?e, "machine export: a `._` file could not be written to disk");
}
}
}
}
}