use std::path::{Path, PathBuf};
use prikk_object::{ObjectEnvelope, ObjectType};
use crate::file_codec::{decode_envelope_file, encode_envelope_file};
use crate::fsutil::{TestFailPoint, fail_once_for_test, publish_immutable_file};
use crate::test_support::{dummy_signature, unique_temp_dir};
use crate::{DoctorSeverity, RepositoryLayout, doctor_repository, verify_repository};
const TEST_OBJECT_RELATIVE: &str = "objects/g5-conformance-test.pobj";
fn signed_blob(payload: &[u8]) -> prikk_error::Result<ObjectEnvelope> {
let mut envelope = ObjectEnvelope::unsigned(ObjectType::Blob, 1, payload.to_vec());
envelope.add_signature(dummy_signature())?;
Ok(envelope)
}
fn mutate_first_signature_byte(envelope: &mut ObjectEnvelope) -> prikk_error::Result<()> {
let byte = envelope
.signatures
.first_mut()
.and_then(|signature| signature.signature_bytes.first_mut())
.ok_or_else(|| {
prikk_error::PrikkError::Integrity("missing test signature byte".to_string())
})?;
*byte ^= 0xff;
Ok(())
}
fn setup(name: &str) -> prikk_error::Result<(PathBuf, RepositoryLayout, ObjectEnvelope)> {
let root = unique_temp_dir(name);
let layout = RepositoryLayout::init(root.clone())?;
std::fs::create_dir_all(layout.prikk_dir().join("objects"))?;
Ok((root, layout, signed_blob(b"candidate")?))
}
fn absolute_test_object_path(layout: &RepositoryLayout) -> PathBuf {
layout.prikk_dir().join(TEST_OBJECT_RELATIVE)
}
fn validate_existing_test_object(
bytes: &[u8],
expected_type: ObjectType,
expected_id: prikk_object::ObjectId,
) -> prikk_error::Result<()> {
let envelope = decode_envelope_file(bytes).map_err(|error| {
prikk_error::PrikkError::Integrity(format!(
"existing immutable object is malformed: {error}"
))
})?;
if envelope.object_type != expected_type {
return Err(prikk_error::PrikkError::Integrity(format!(
"existing object type {} differs from path type {expected_type}",
envelope.object_type
)));
}
let actual_id = envelope.object_id();
if actual_id != expected_id {
return Err(prikk_error::PrikkError::Integrity(format!(
"existing object id {actual_id} differs from path id {expected_id}"
)));
}
Ok(())
}
fn publish(layout: &RepositoryLayout, envelope: &ObjectEnvelope) -> prikk_error::Result<()> {
let candidate = encode_envelope_file(envelope)?;
let object_type = envelope.object_type;
let object_id = envelope.object_id();
publish_immutable_file(
layout.repository_mutation_root(),
Path::new(TEST_OBJECT_RELATIVE),
&candidate,
move |existing| validate_existing_test_object(existing, object_type, object_id),
)
}
fn temp_paths(shard: &Path) -> prikk_error::Result<Vec<PathBuf>> {
let mut paths = Vec::new();
for entry in std::fs::read_dir(shard)? {
let path = entry?.path();
if path
.file_name()
.and_then(|name| name.to_str())
.is_some_and(|name| name.contains(".tmp."))
{
paths.push(path);
}
}
Ok(paths)
}
#[test]
fn exact_existing_bytes_are_synced_and_accepted() -> prikk_error::Result<()> {
let (root, layout, envelope) = setup("object-exact-existing")?;
publish(&layout, &envelope)?;
fail_once_for_test(TestFailPoint::ImmutableCleanupSync);
assert!(publish(&layout, &envelope).is_err());
assert!(publish(&layout, &envelope).is_ok());
let _ = std::fs::remove_dir_all(root);
Ok(())
}
#[test]
fn same_object_id_with_different_signature_transport_is_rejected() -> prikk_error::Result<()> {
let (root, layout, envelope) = setup("object-signature-mismatch")?;
let mut different = envelope.clone();
mutate_first_signature_byte(&mut different)?;
assert_eq!(different.object_id(), envelope.object_id());
publish(&layout, &envelope)?;
assert!(publish(&layout, &different).is_err());
assert_eq!(
std::fs::read(absolute_test_object_path(&layout))?,
encode_envelope_file(&envelope)?
);
let _ = std::fs::remove_dir_all(root);
Ok(())
}
#[test]
fn malformed_wrong_id_and_wrong_type_existing_files_are_rejected() -> prikk_error::Result<()> {
for (name, bytes) in [
("malformed", b"malformed".to_vec()),
("wrong-id", encode_envelope_file(&signed_blob(b"other")?)?),
(
"wrong-type",
encode_envelope_file(&ObjectEnvelope::unsigned(ObjectType::Patch, 1, vec![]))?,
),
] {
let (root, layout, envelope) = setup(&format!("object-{name}"))?;
std::fs::write(absolute_test_object_path(&layout), bytes)?;
assert!(publish(&layout, &envelope).is_err(), "case {name}");
let _ = std::fs::remove_dir_all(root);
}
Ok(())
}
#[cfg(target_family = "unix")]
#[test]
fn final_symlink_directory_and_symlinked_shard_are_rejected() -> prikk_error::Result<()> {
use std::os::unix::fs::symlink;
for kind in ["symlink", "directory", "shard-symlink"] {
let (root, layout, envelope) = setup(&format!("object-{kind}"))?;
let path = absolute_test_object_path(&layout);
let shard = path
.parent()
.ok_or_else(|| prikk_error::PrikkError::Io("object path has no parent".to_string()))?;
let external = root.join("external");
std::fs::create_dir_all(&external)?;
match kind {
"symlink" => {
std::fs::create_dir_all(shard)?;
let target = external.join("winner");
std::fs::write(&target, encode_envelope_file(&envelope)?)?;
symlink(target, &path)?;
}
"directory" => std::fs::create_dir_all(&path)?,
"shard-symlink" => {
std::fs::remove_dir_all(shard)?;
symlink(&external, shard)?;
}
_ => unreachable!(),
}
assert!(publish(&layout, &envelope).is_err(), "case {kind}");
let _ = std::fs::remove_dir_all(root);
}
Ok(())
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
#[test]
fn final_fifo_is_rejected_without_blocking() -> prikk_error::Result<()> {
use std::sync::mpsc;
use std::time::Duration;
use crate::test_support::create_fifo_for_test;
let (root, layout, envelope) = setup("object-fifo")?;
let path = absolute_test_object_path(&layout);
create_fifo_for_test(&path, 0o600)?;
let (sender, receiver) = mpsc::channel();
std::thread::spawn(move || {
let _ = sender.send(publish(&layout, &envelope));
});
let result = receiver.recv_timeout(Duration::from_secs(1));
let publication = result.map_err(|_| {
prikk_error::PrikkError::Io("FIFO rejection exceeded the bounded wait".to_string())
})?;
assert!(publication.is_err());
let _ = std::fs::remove_dir_all(root);
Ok(())
}
#[test]
fn immutable_failpoints_retain_required_artifacts_and_retry() -> prikk_error::Result<()> {
for point in [
TestFailPoint::ImmutableFileSync,
TestFailPoint::ImmutableInstall,
TestFailPoint::ImmutableInstallUnsupported,
TestFailPoint::ImmutableInstallNoSys,
TestFailPoint::ImmutableInstallPermission,
TestFailPoint::ImmutableInstallSync,
TestFailPoint::ImmutableTempUnlink,
TestFailPoint::ImmutableCleanupSync,
] {
let (root, layout, envelope) = setup(&format!("object-{point:?}"))?;
let path = absolute_test_object_path(&layout);
let shard = path
.parent()
.ok_or_else(|| prikk_error::PrikkError::Io("object path has no parent".to_string()))?
.to_path_buf();
fail_once_for_test(point);
let error = match publish(&layout, &envelope) {
Ok(()) => {
return Err(prikk_error::PrikkError::Integrity(
"injected immutable failure unexpectedly succeeded".to_string(),
));
}
Err(error) => error,
};
let installed = path.is_file();
let temps = temp_paths(&shard)?;
match point {
TestFailPoint::ImmutableFileSync | TestFailPoint::ImmutableInstall => {
assert!(!installed);
assert_eq!(temps.len(), 1);
}
TestFailPoint::ImmutableInstallUnsupported
| TestFailPoint::ImmutableInstallNoSys
| TestFailPoint::ImmutableInstallPermission => {
assert!(
error
.to_string()
.contains("unsupported by filesystem or policy")
);
assert!(!installed);
assert_eq!(temps.len(), 1);
}
TestFailPoint::ImmutableInstallSync | TestFailPoint::ImmutableTempUnlink => {
assert!(installed);
assert_eq!(temps.len(), 1);
}
TestFailPoint::ImmutableCleanupSync => {
assert!(installed);
assert!(temps.is_empty());
}
_ => unreachable!(),
}
assert!(publish(&layout, &envelope).is_ok());
assert!(path.is_file());
let _ = std::fs::remove_dir_all(root);
}
Ok(())
}
#[test]
fn crash_left_temp_is_ignored_by_reads_and_warned_without_cleanup() -> prikk_error::Result<()> {
let (root, layout, envelope) = setup("object-temp-diagnostics")?;
let path = layout.object_path(envelope.object_type, envelope.object_id());
let shard = path
.parent()
.ok_or_else(|| prikk_error::PrikkError::Io("object path has no parent".to_string()))?
.to_path_buf();
std::fs::create_dir_all(&shard)?;
let relative = layout.repository_relative(&path)?;
let candidate = encode_envelope_file(&envelope)?;
let object_type = envelope.object_type;
let object_id = envelope.object_id();
fail_once_for_test(TestFailPoint::ImmutableFileSync);
assert!(
publish_immutable_file(
layout.repository_mutation_root(),
&relative,
&candidate,
move |existing| validate_existing_test_object(existing, object_type, object_id),
)
.is_err()
);
let temps = temp_paths(&shard)?;
assert_eq!(temps.len(), 1);
let verification = verify_repository(&layout)?;
assert_eq!(verification.object_temp_paths, temps);
let doctor = doctor_repository(&layout);
assert!(doctor.is_healthy());
assert!(doctor.issues.iter().any(|issue| {
issue.code == "PRIKK-DOCTOR-OBJECT-TEMP-DEBRIS" && issue.severity == DoctorSeverity::Warning
}));
assert!(temps.first().is_some_and(|path| path.is_file()));
let _ = std::fs::remove_dir_all(root);
Ok(())
}