// file: crates/ksp-config-lib/src/persistence.rs // version: 4 static NEXT_TEMPORARY_FILE_ID: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1); /// Executes the crate-internal atomic write operation for the owning module. pub(crate) fn atomic_write(path: &std::path::Path, content: &[u8]) -> ksp_core_lib::Result<()> { return atomic_write_with_policy(path, content, false); } /// Executes the crate-internal atomic write private operation for the owning module. pub(crate) fn atomic_write_private(path: &std::path::Path, content: &[u8]) -> ksp_core_lib::Result<()> { return atomic_write_with_policy(path, content, true); } fn atomic_write_with_policy(path: &std::path::Path, content: &[u8], private_when_new: bool) -> ksp_core_lib::Result<()> { let parent = match path.parent() { std::option::Option::Some(value) if !value.as_os_str().is_empty() => value, _ => std::path::Path::new("."), }; let filename = match path.file_name().and_then(std::ffi::OsStr::to_str) { std::option::Option::Some(value) if !value.is_empty() => value, _ => return std::result::Result::Err(persistence_error(path, "managed Config path has no UTF-8 file name")), }; let existing_permissions = destination_permissions(path); let existing_permissions = match existing_permissions { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let temporary_id = NEXT_TEMPORARY_FILE_ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed); let temporary_name = format!(".{filename}.ksp-tmp-{}-{temporary_id}", std::process::id()); let temporary_path = parent.join(temporary_name); let opened = std::fs::OpenOptions::new().write(true).create_new(true).open(temporary_path.as_path()); let mut file = match opened { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(persistence_io_error(path, "temporary Config file cannot be created", error)), }; let permissions = apply_temporary_permissions(&file, existing_permissions, private_when_new); if let std::result::Result::Err(error) = permissions { cleanup_temporary_file(temporary_path.as_path()); return std::result::Result::Err(persistence_io_error(path, "temporary Config file permissions cannot be applied", error)); } let write = std::io::Write::write_all(&mut file, content); if let std::result::Result::Err(error) = write { cleanup_temporary_file(temporary_path.as_path()); return std::result::Result::Err(persistence_io_error(path, "temporary Config file cannot be written", error)); } let sync = file.sync_all(); if let std::result::Result::Err(error) = sync { cleanup_temporary_file(temporary_path.as_path()); return std::result::Result::Err(persistence_io_error(path, "temporary Config file cannot be synchronized", error)); } drop(file); let rename = std::fs::rename(temporary_path.as_path(), path); if let std::result::Result::Err(error) = rename { cleanup_temporary_file(temporary_path.as_path()); return std::result::Result::Err(persistence_io_error(path, "atomic Config file replacement failed", error)); } return std::result::Result::Ok(()); } fn destination_permissions(path: &std::path::Path) -> ksp_core_lib::Result> { let metadata = std::fs::metadata(path); return match metadata { std::result::Result::Ok(value) => std::result::Result::Ok(std::option::Option::Some(value.permissions())), std::result::Result::Err(error) if error.kind() == std::io::ErrorKind::NotFound => std::result::Result::Ok(std::option::Option::None), std::result::Result::Err(error) => { std::result::Result::Err(persistence_io_error(path, "managed Config file metadata cannot be read before replacement", error)) }, }; } fn apply_temporary_permissions( file: &std::fs::File, existing_permissions: std::option::Option, private_when_new: bool, ) -> std::io::Result<()> { if let std::option::Option::Some(permissions) = existing_permissions { return file.set_permissions(permissions); } return apply_new_file_permissions(file, private_when_new); } #[cfg(unix)] fn apply_new_file_permissions(file: &std::fs::File, private_when_new: bool) -> std::io::Result<()> { if private_when_new { let permissions = ::from_mode(0o600); return file.set_permissions(permissions); } return std::result::Result::Ok(()); } #[cfg(not(unix))] fn apply_new_file_permissions(_file: &std::fs::File, _private_when_new: bool) -> std::io::Result<()> { return std::result::Result::Ok(()); } fn cleanup_temporary_file(path: &std::path::Path) { let removal = std::fs::remove_file(path); if let std::result::Result::Err(error) = removal && error.kind() != std::io::ErrorKind::NotFound { ksp_logging_lib::warn!( target: crate::TRACING_TARGET, domain = "config.persistence", path = %path.to_string_lossy(), error = %error, "unable to cleanup temporary Config file" ); } } fn persistence_error(path: &std::path::Path, reason: &'static str) -> ksp_core_lib::Error { return ksp_core_lib::Error::new(crate::ERROR_CODE_PERSISTENCE_WRITE_FAILED, "Config persistence failed") .with_context("path", path.to_string_lossy().into_owned()) .with_context("reason", reason); } fn persistence_io_error(path: &std::path::Path, reason: &'static str, source: std::io::Error) -> ksp_core_lib::Error { return persistence_error(path, reason).with_source(source); }