use std::path::Path;
use crate::patch::apply::DirWriteGuard;
use super::{
SidecarAdvisory, SidecarAdvisoryCode, SidecarError, SidecarFile, SidecarFileAction,
SidecarPayload, SidecarSeverity,
};
const METADATA_FILE: &str = ".nupkg.metadata";
pub(crate) async fn fixup(pkg_path: &Path) -> Result<Option<SidecarPayload>, SidecarError> {
let mut files = Vec::new();
let metadata_path = pkg_path.join(METADATA_FILE);
let dir_guard = DirWriteGuard::acquire(Some(pkg_path)).await;
let remove_result = tokio::fs::remove_file(&metadata_path).await;
dir_guard.restore().await;
match remove_result {
Ok(()) => files.push(SidecarFile {
path: METADATA_FILE.to_string(),
action: SidecarFileAction::Deleted,
}),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => { }
Err(source) => {
return Err(SidecarError::Io {
path: metadata_path.display().to_string(),
source,
});
}
}
let advisory = if has_signed_marker(pkg_path).await {
Some(SidecarAdvisory {
code: SidecarAdvisoryCode::NugetSignedPackageTampered,
severity: SidecarSeverity::Warning,
message: "NuGet: package has a .nupkg.sha512 signature sidecar — \
NuGet may flag this install as tampered. No safe recovery."
.to_string(),
})
} else {
None
};
if files.is_empty() && advisory.is_none() {
return Ok(None);
}
Ok(Some(SidecarPayload { files, advisory }))
}
async fn has_signed_marker(pkg_path: &Path) -> bool {
let mut entries = match tokio::fs::read_dir(pkg_path).await {
Ok(rd) => rd,
Err(_) => return false,
};
while let Ok(Some(entry)) = entries.next_entry().await {
if entry
.file_name()
.as_encoded_bytes()
.ends_with(b".nupkg.sha512")
{
return true;
}
}
false
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn deletes_metadata_when_present() {
let d = tempfile::tempdir().unwrap();
tokio::fs::write(d.path().join(METADATA_FILE), b"{}")
.await
.unwrap();
let out = fixup(d.path()).await.unwrap();
let payload = out.expect("metadata existed, expect a payload");
assert_eq!(payload.files.len(), 1);
assert_eq!(payload.files[0].path, METADATA_FILE);
assert_eq!(payload.files[0].action, SidecarFileAction::Deleted);
assert!(payload.advisory.is_none());
assert!(tokio::fs::metadata(d.path().join(METADATA_FILE))
.await
.is_err());
}
#[tokio::test]
async fn no_metadata_yields_none() {
let d = tempfile::tempdir().unwrap();
let out = fixup(d.path()).await.unwrap();
assert!(out.is_none());
}
#[tokio::test]
async fn signed_without_metadata_returns_advisory_only() {
let d = tempfile::tempdir().unwrap();
tokio::fs::write(d.path().join("pkg.1.0.0.nupkg.sha512"), b"hash")
.await
.unwrap();
let out = fixup(d.path()).await.unwrap();
let payload = out.expect("signed package expects a payload");
assert!(payload.files.is_empty());
let adv = payload.advisory.expect("expected advisory");
assert_eq!(adv.code, SidecarAdvisoryCode::NugetSignedPackageTampered);
assert_eq!(adv.severity, SidecarSeverity::Warning);
}
#[cfg(unix)]
#[tokio::test]
async fn deletes_metadata_inside_readonly_dir() {
use std::os::unix::fs::PermissionsExt;
let d = tempfile::tempdir().unwrap();
let pkg = d.path();
tokio::fs::write(pkg.join(METADATA_FILE), b"{}")
.await
.unwrap();
tokio::fs::set_permissions(pkg, std::fs::Permissions::from_mode(0o555))
.await
.unwrap();
let out = fixup(pkg).await;
let mode = tokio::fs::metadata(pkg).await.unwrap().permissions().mode() & 0o7777;
tokio::fs::set_permissions(pkg, std::fs::Permissions::from_mode(0o755))
.await
.unwrap();
let payload = out
.expect("delete inside a read-only dir must not error")
.expect("metadata existed, expect a payload");
assert_eq!(payload.files.len(), 1);
assert_eq!(payload.files[0].action, SidecarFileAction::Deleted);
assert!(tokio::fs::metadata(pkg.join(METADATA_FILE)).await.is_err());
assert_eq!(
mode, 0o555,
"package dir mode must be restored after the unlink"
);
}
#[tokio::test]
async fn signed_with_metadata_carries_files_and_advisory() {
let d = tempfile::tempdir().unwrap();
tokio::fs::write(d.path().join(METADATA_FILE), b"{}")
.await
.unwrap();
tokio::fs::write(d.path().join("pkg.1.0.0.nupkg.sha512"), b"hash")
.await
.unwrap();
let out = fixup(d.path()).await.unwrap();
let payload = out.expect("expect a payload");
assert_eq!(payload.files.len(), 1);
assert_eq!(payload.files[0].action, SidecarFileAction::Deleted);
let adv = payload
.advisory
.expect("signed-package case must surface advisory alongside the file entry");
assert_eq!(adv.code, SidecarAdvisoryCode::NugetSignedPackageTampered);
assert!(tokio::fs::metadata(d.path().join(METADATA_FILE))
.await
.is_err());
}
}