From 6b463c2b1a4e9b47b70d01c7b599afef2268cc49 Mon Sep 17 00:00:00 2001 From: hibna Date: Thu, 26 Feb 2026 22:36:57 +0000 Subject: [PATCH] fix(daemon): preserve file ownership on writes for cs2 addons --- apps/daemon/src/filesystem/operations.rs | 108 ++++++++++++++++++++++- 1 file changed, 106 insertions(+), 2 deletions(-) diff --git a/apps/daemon/src/filesystem/operations.rs b/apps/daemon/src/filesystem/operations.rs index 108f9dd..ade461a 100644 --- a/apps/daemon/src/filesystem/operations.rs +++ b/apps/daemon/src/filesystem/operations.rs @@ -1,4 +1,4 @@ -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use tokio::fs; use tracing::debug; @@ -94,14 +94,24 @@ impl FileSystem { /// Write file contents. pub async fn write_file(&self, path: &str, data: &[u8]) -> Result<(), DaemonError> { let resolved = self.resolve(path)?; + let owner = resolved + .parent() + .and_then(resolve_target_ownership); // Ensure parent directory exists if let Some(parent) = resolved.parent() { fs::create_dir_all(parent).await.map_err(DaemonError::Io)?; + if let Some(ref owner) = owner { + apply_ownership_to_path_chain(parent, &owner)?; + } } debug!(path = %resolved.display(), "Writing file"); - fs::write(&resolved, data).await.map_err(DaemonError::Io) + fs::write(&resolved, data).await.map_err(DaemonError::Io)?; + if let Some(ref owner) = owner { + apply_ownership(&resolved, owner.uid, owner.gid)?; + } + Ok(()) } /// Delete files or directories. @@ -119,6 +129,100 @@ impl FileSystem { } } +#[derive(Clone, Debug)] +struct OwnershipTarget { + anchor: PathBuf, + uid: u32, + gid: u32, +} + +#[cfg(unix)] +fn resolve_target_ownership(start: &Path) -> Option { + use std::os::unix::fs::MetadataExt; + + let mut cursor = Some(start); + let mut fallback: Option = None; + + while let Some(path) = cursor { + if let Ok(metadata) = std::fs::metadata(path) { + let candidate = OwnershipTarget { + anchor: path.to_path_buf(), + uid: metadata.uid(), + gid: metadata.gid(), + }; + + if fallback.is_none() { + fallback = Some(candidate.clone()); + } + + if candidate.uid != 0 || candidate.gid != 0 { + return Some(candidate); + } + } + cursor = path.parent(); + } + + fallback +} + +#[cfg(not(unix))] +fn resolve_target_ownership(_start: &Path) -> Option { + None +} + +#[cfg(unix)] +fn apply_ownership_to_path_chain(target: &Path, owner: &OwnershipTarget) -> Result<(), DaemonError> { + if !target.starts_with(&owner.anchor) { + return Ok(()); + } + + let mut current = owner.anchor.clone(); + apply_ownership(¤t, owner.uid, owner.gid)?; + + let remainder = match target.strip_prefix(&owner.anchor) { + Ok(path) => path, + Err(_) => return Ok(()), + }; + + for component in remainder.components() { + current.push(component.as_os_str()); + apply_ownership(¤t, owner.uid, owner.gid)?; + } + + Ok(()) +} + +#[cfg(not(unix))] +fn apply_ownership_to_path_chain(_target: &Path, _owner: &OwnershipTarget) -> Result<(), DaemonError> { + Ok(()) +} + +#[cfg(unix)] +fn apply_ownership(path: &Path, uid: u32, gid: u32) -> Result<(), DaemonError> { + use std::ffi::CString; + use std::os::unix::ffi::OsStrExt; + + let bytes = path.as_os_str().as_bytes(); + let c_path = CString::new(bytes).map_err(|err| { + DaemonError::Io(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + format!("invalid path for chown: {err}"), + )) + })?; + + let result = unsafe { libc::chown(c_path.as_ptr(), uid, gid) }; + if result != 0 { + return Err(DaemonError::Io(std::io::Error::last_os_error())); + } + + Ok(()) +} + +#[cfg(not(unix))] +fn apply_ownership(_path: &Path, _uid: u32, _gid: u32) -> Result<(), DaemonError> { + Ok(()) +} + #[derive(Debug, Clone)] pub struct FileEntry { pub name: String,