use std::path::{Path, PathBuf};
#[derive(Debug, thiserror::Error)]
pub enum ReleaseHandlesError {
#[error("--path must be non-empty")]
EmptyPath,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ReleaseHandlesAuthorization<'a> {
pub requester_account_id: &'a str,
pub daemon_owner_account_id: &'a str,
pub requester_can_write_target_path: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum ReleaseHandlesAuthorizationError {
#[error("release-handles requester identity must be non-empty")]
EmptyRequesterIdentity,
#[error("release-handles daemon owner identity must be non-empty")]
EmptyDaemonOwnerIdentity,
#[error("release-handles requester identity does not match daemon owner")]
OwnerMismatch,
#[error("release-handles requester lacks write access to target path")]
TargetPathWriteDenied,
}
pub fn authorize_release_handles_request(
authorization: ReleaseHandlesAuthorization<'_>,
) -> Result<(), ReleaseHandlesAuthorizationError> {
if authorization.requester_account_id.trim().is_empty() {
return Err(ReleaseHandlesAuthorizationError::EmptyRequesterIdentity);
}
if authorization.daemon_owner_account_id.trim().is_empty() {
return Err(ReleaseHandlesAuthorizationError::EmptyDaemonOwnerIdentity);
}
if authorization.requester_account_id != authorization.daemon_owner_account_id {
return Err(ReleaseHandlesAuthorizationError::OwnerMismatch);
}
if !authorization.requester_can_write_target_path {
return Err(ReleaseHandlesAuthorizationError::TargetPathWriteDenied);
}
Ok(())
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ReleaseHandlesOutcome {
pub path: PathBuf,
pub message: String,
pub manifests_scanned: u32,
pub handles_released: u32,
pub already_clean: bool,
}
impl ReleaseHandlesOutcome {
pub fn to_json(&self) -> String {
format!(
"{{\
\"path\":\"{path}\",\
\"manifests_scanned\":{manifests},\
\"handles_released\":{handles},\
\"already_clean\":{clean},\
\"message\":\"{message}\"\
}}",
path = json_escape(&self.path.to_string_lossy()),
manifests = self.manifests_scanned,
handles = self.handles_released,
clean = self.already_clean,
message = json_escape(&self.message),
)
}
}
pub fn run_release_handles(path: &Path) -> Result<ReleaseHandlesOutcome, ReleaseHandlesError> {
let path_str = path.to_string_lossy();
if path_str.trim().is_empty() {
return Err(ReleaseHandlesError::EmptyPath);
}
#[cfg(unix)]
{
Ok(ReleaseHandlesOutcome {
path: path.to_path_buf(),
message: format!(
"POSIX delete-on-close semantics make this a no-op; proceed with `rm -rf {path_str}`"
),
manifests_scanned: 0,
handles_released: 0,
already_clean: true,
})
}
#[cfg(windows)]
{
Ok(ReleaseHandlesOutcome {
path: path.to_path_buf(),
message: format!(
"Phase 2 manifest registry not yet shipped; no daemons to query for handles under \
{path_str}. Proceed with rm -rf and report soldr#710 reproductions if encountered."
),
manifests_scanned: 0,
handles_released: 0,
already_clean: true,
})
}
}
fn json_escape(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for c in s.chars() {
match c {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
c => out.push(c),
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_path_returns_error() {
let err = run_release_handles(Path::new("")).unwrap_err();
match err {
ReleaseHandlesError::EmptyPath => {}
}
}
#[test]
fn non_empty_path_returns_ok() {
let outcome = run_release_handles(Path::new("/tmp/example")).expect("ok");
assert_eq!(outcome.path, PathBuf::from("/tmp/example"));
assert!(outcome.already_clean);
assert_eq!(outcome.manifests_scanned, 0);
assert_eq!(outcome.handles_released, 0);
}
#[test]
fn json_output_has_stable_keys() {
let outcome = run_release_handles(Path::new("/tmp/example")).expect("ok");
let json = outcome.to_json();
assert!(json.contains("\"path\":"));
assert!(json.contains("\"manifests_scanned\":"));
assert!(json.contains("\"handles_released\":"));
assert!(json.contains("\"already_clean\":"));
assert!(json.contains("\"message\":"));
}
}