use std::{
collections::{BTreeMap, BTreeSet},
fs::File,
io::Write,
path::Path,
};
use ic_host_fs::read::{open_regular_file, read_opened_file};
use serde::{Deserialize, Serialize};
const DIAGNOSTIC_ARTIFACT_FORMAT: &str = "icydb-diagnostic-schema";
const DIAGNOSTIC_ARTIFACT_VERSION: u8 = 1;
const MAX_DIAGNOSTIC_ARTIFACT_BYTES: usize = icydb_schema::MAX_SCHEMA_PROPOSAL_BYTES;
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub(crate) struct DiagnosticSchemaArtifact {
format: String,
version: u8,
provenance: DiagnosticArtifactOwner,
entities: Vec<DiagnosticArtifactEntity>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields, tag = "kind", rename_all = "snake_case")]
enum DiagnosticArtifactOwner {
Deployment {
environment: String,
canister: String,
},
Source {
source: String,
},
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
struct DiagnosticArtifactEntity {
fingerprint_method: u8,
fingerprint: [u8; 16],
entity_tag: u64,
entity_name: String,
entity_path: String,
fields: Vec<DiagnosticArtifactIdentity>,
constraints: Vec<DiagnosticArtifactConstraint>,
indexes: Vec<DiagnosticArtifactIdentity>,
relations: Vec<DiagnosticArtifactIdentity>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
struct DiagnosticArtifactIdentity {
id: u32,
name: String,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
struct DiagnosticArtifactConstraint {
id: u32,
name: String,
kind: String,
field_ids: Vec<u32>,
index_id: Option<u32>,
relation_id: Option<u32>,
}
pub(crate) struct ResolvedDiagnosticEntity<'a> {
entity: &'a DiagnosticArtifactEntity,
constraint: Option<&'a DiagnosticArtifactConstraint>,
}
impl DiagnosticSchemaArtifact {
pub(crate) fn from_report(
environment: &str,
canister: &str,
report: &[icydb::db::EntitySchemaDescription],
) -> Result<Self, String> {
let mut entities = report
.iter()
.map(DiagnosticArtifactEntity::from_description)
.collect::<Result<Vec<_>, _>>()?;
entities.sort_by_key(|entity| {
(
entity.fingerprint_method,
entity.fingerprint,
entity.entity_tag,
)
});
let artifact = Self {
format: DIAGNOSTIC_ARTIFACT_FORMAT.to_string(),
version: DIAGNOSTIC_ARTIFACT_VERSION,
provenance: DiagnosticArtifactOwner::Deployment {
environment: environment.to_string(),
canister: canister.to_string(),
},
entities,
};
artifact.validate()?;
Ok(artifact)
}
pub(crate) fn read_deployment(path: &Path) -> Result<Self, String> {
let artifact = Self::read(path)?;
if !matches!(
&artifact.provenance,
DiagnosticArtifactOwner::Deployment { .. }
) {
return Err(format!(
"diagnostic artifact '{}' is source metadata, not a deployment artifact",
path.display()
));
}
Ok(artifact)
}
pub(crate) fn read_source_metadata(path: &Path) -> Result<Self, String> {
let artifact = Self::read(path)?;
if !matches!(&artifact.provenance, DiagnosticArtifactOwner::Source { .. }) {
return Err(format!(
"diagnostic source metadata '{}' is a deployment artifact",
path.display()
));
}
Ok(artifact)
}
pub(crate) fn bind_to_source(mut self, source: &str) -> Result<Self, String> {
validate_text("source", source)?;
self.provenance = DiagnosticArtifactOwner::Source {
source: source.to_string(),
};
self.validate()?;
Ok(self)
}
fn read(path: &Path) -> Result<Self, String> {
let file = open_regular_file(path).map_err(|err| {
format!(
"failed to open diagnostic artifact '{}': {err}",
path.display()
)
})?;
let bytes = read_opened_file(file, MAX_DIAGNOSTIC_ARTIFACT_BYTES).map_err(|err| {
format!(
"failed to read diagnostic artifact '{}': {err}",
path.display()
)
})?;
let artifact = serde_json::from_slice::<Self>(bytes.as_slice()).map_err(|err| {
format!(
"failed to decode current diagnostic artifact '{}': {err}",
path.display()
)
})?;
artifact.validate()?;
Ok(artifact)
}
pub(crate) fn write_new(&self, path: &Path) -> Result<(), String> {
self.validate()?;
let bytes = serde_json::to_vec_pretty(self)
.map_err(|err| format!("failed to encode diagnostic artifact: {err}"))?;
if bytes.len() > MAX_DIAGNOSTIC_ARTIFACT_BYTES {
return Err(format!(
"encoded diagnostic artifact is {} bytes; maximum is {MAX_DIAGNOSTIC_ARTIFACT_BYTES}",
bytes.len()
));
}
#[cfg(unix)]
{
use ic_host_fs::durable::{PublicationMode, WriteOptions, write_at_with};
use std::os::fd::AsFd as _;
let raw_path = path.as_os_str().as_encoded_bytes();
if raw_path.ends_with(b"/") || raw_path.ends_with(b"/.") {
return Err(format!(
"diagnostic artifact target requires a directory: '{}'",
path.display()
));
}
let name = path.file_name().ok_or_else(|| {
format!(
"diagnostic artifact path has no filename: '{}'",
path.display()
)
})?;
let parent = path
.parent()
.filter(|parent| !parent.as_os_str().is_empty())
.unwrap_or_else(|| Path::new("."));
let directory = File::open(parent).map_err(|err| {
format!(
"failed to open diagnostic artifact parent '{}': {err}",
parent.display()
)
})?;
write_at_with(
directory.as_fd(),
name,
WriteOptions {
mode: PublicationMode::CreateNew,
permissions: 0o666,
},
|file| file.write_all(&bytes),
)
.map_err(|err| {
format!(
"failed to publish diagnostic artifact '{}': {err}",
path.display()
)
})
}
#[cfg(not(unix))]
{
let _ = bytes;
Err("durable diagnostic artifact publication is unsupported on this host".to_string())
}
}
pub(crate) fn provenance_matches(&self, environment: &str, canister: &str) -> bool {
matches!(
&self.provenance,
DiagnosticArtifactOwner::Deployment {
environment: artifact_environment,
canister: artifact_canister,
} if artifact_environment == environment && artifact_canister == canister
)
}
pub(crate) fn resolve(
&self,
fingerprint_method: u8,
fingerprint: [u8; 16],
entity_tag: u64,
constraint_id: Option<u32>,
) -> Option<ResolvedDiagnosticEntity<'_>> {
let entity = self.entities.iter().find(|entity| {
entity.fingerprint_method == fingerprint_method
&& entity.fingerprint == fingerprint
&& entity.entity_tag == entity_tag
})?;
let constraint = match constraint_id {
Some(constraint_id) => Some(entity.constraint(constraint_id)?),
None => None,
};
Some(ResolvedDiagnosticEntity { entity, constraint })
}
fn validate(&self) -> Result<(), String> {
if self.format != DIAGNOSTIC_ARTIFACT_FORMAT || self.version != DIAGNOSTIC_ARTIFACT_VERSION
{
return Err(format!(
"unsupported diagnostic artifact format/version '{}'/{}; expected '{}'/{}",
self.format, self.version, DIAGNOSTIC_ARTIFACT_FORMAT, DIAGNOSTIC_ARTIFACT_VERSION
));
}
match &self.provenance {
DiagnosticArtifactOwner::Deployment {
environment,
canister,
} => {
validate_text("environment", environment.as_str())?;
validate_text("canister", canister.as_str())?;
}
DiagnosticArtifactOwner::Source { source } => {
validate_text("source", source.as_str())?;
}
}
validate_count(
"entities",
self.entities.len(),
icydb_schema::MAX_FRAGMENT_ENTITIES,
)?;
let mut entity_keys = BTreeSet::new();
let mut entity_tags = BTreeSet::new();
for entity in &self.entities {
entity.validate()?;
if !entity_keys.insert((
entity.fingerprint_method,
entity.fingerprint,
entity.entity_tag,
)) {
return Err("diagnostic artifact contains duplicate entity identity".to_string());
}
if !entity_tags.insert(entity.entity_tag) {
return Err("diagnostic artifact contains a duplicate entity tag".to_string());
}
}
Ok(())
}
#[cfg(test)]
pub(super) fn test_fixture() -> Self {
Self {
format: DIAGNOSTIC_ARTIFACT_FORMAT.to_string(),
version: DIAGNOSTIC_ARTIFACT_VERSION,
provenance: DiagnosticArtifactOwner::Deployment {
environment: "demo".to_string(),
canister: "app".to_string(),
},
entities: vec![DiagnosticArtifactEntity {
fingerprint_method: 1,
fingerprint: [7; 16],
entity_tag: 42,
entity_name: "Account".to_string(),
entity_path: "schema::Account".to_string(),
fields: vec![DiagnosticArtifactIdentity {
id: 1,
name: "id".to_string(),
}],
constraints: vec![DiagnosticArtifactConstraint {
id: 3,
name: "account_name_unique".to_string(),
kind: "unique".to_string(),
field_ids: vec![],
index_id: None,
relation_id: None,
}],
indexes: vec![],
relations: vec![],
}],
}
}
#[cfg(test)]
pub(super) fn test_source_fixture() -> Self {
let mut artifact = Self::test_fixture();
artifact.provenance = DiagnosticArtifactOwner::Source {
source: "schema/account.rs".to_string(),
};
artifact.entities[0].entity_name = "SourceAccount".to_string();
artifact.entities[0].entity_path = "schema::source::Account".to_string();
artifact.entities[0].constraints[0].name = "source_account_name_unique".to_string();
artifact
}
}
impl DiagnosticArtifactEntity {
fn from_description(entity: &icydb::db::EntitySchemaDescription) -> Result<Self, String> {
let mut fields = BTreeMap::<u32, String>::new();
let mut indexes = BTreeMap::<u32, String>::new();
let mut relations = BTreeMap::<u32, String>::new();
let mut constraints = Vec::with_capacity(entity.constraints().len());
for constraint in entity.constraints() {
let mut field_ids = Vec::new();
if let Some(field_id) = constraint.field_id() {
let field_name = constraint.fields().first().ok_or_else(|| {
format!(
"constraint {} exposes field ID {field_id} without a field name",
constraint.id()
)
})?;
insert_exact_name(&mut fields, field_id, field_name, "field")?;
field_ids.push(field_id);
}
if let (Some(index_id), Some(index_name)) = (constraint.index_id(), constraint.index())
{
insert_exact_name(&mut indexes, index_id, index_name, "index")?;
}
if let (Some(relation_id), Some(relation_name)) =
(constraint.relation_id(), constraint.relation())
{
insert_exact_name(&mut relations, relation_id, relation_name, "relation")?;
}
constraints.push(DiagnosticArtifactConstraint {
id: constraint.id(),
name: constraint.name().to_string(),
kind: constraint.kind().to_string(),
field_ids,
index_id: constraint.index_id(),
relation_id: constraint.relation_id(),
});
}
constraints.sort_by_key(|constraint| constraint.id);
let entity = Self {
fingerprint_method: entity.accepted_schema_fingerprint_method(),
fingerprint: entity.accepted_schema_fingerprint(),
entity_tag: entity.entity_tag(),
entity_name: entity.entity_name().to_string(),
entity_path: entity.entity_path().to_string(),
fields: identities(fields),
constraints,
indexes: identities(indexes),
relations: identities(relations),
};
entity.validate()?;
Ok(entity)
}
fn constraint(&self, id: u32) -> Option<&DiagnosticArtifactConstraint> {
self.constraints
.iter()
.find(|constraint| constraint.id == id)
}
fn validate(&self) -> Result<(), String> {
if self.fingerprint_method == 0 {
return Err("diagnostic artifact fingerprint method must be non-zero".to_string());
}
validate_text("entity name", self.entity_name.as_str())?;
validate_text("entity path", self.entity_path.as_str())?;
validate_identities(
"fields",
self.fields.as_slice(),
icydb_schema::MAX_FRAGMENT_FIELDS,
)?;
validate_identities(
"indexes",
self.indexes.as_slice(),
icydb_schema::MAX_FRAGMENT_INDEXES,
)?;
validate_identities(
"relations",
self.relations.as_slice(),
icydb_schema::MAX_FRAGMENT_RELATIONS,
)?;
validate_count(
"constraints",
self.constraints.len(),
icydb_schema::MAX_FRAGMENT_CONSTRAINTS,
)?;
let mut constraint_ids = BTreeSet::new();
for constraint in &self.constraints {
if constraint.id == 0 || !constraint_ids.insert(constraint.id) {
return Err("diagnostic artifact contains an invalid constraint ID".to_string());
}
validate_text("constraint name", constraint.name.as_str())?;
validate_text("constraint kind", constraint.kind.as_str())?;
validate_count(
"constraint field IDs",
constraint.field_ids.len(),
icydb_schema::MAX_FRAGMENT_FIELDS,
)?;
if constraint
.field_ids
.iter()
.any(|id| !self.fields.iter().any(|field| field.id == *id))
{
return Err(
"diagnostic artifact constraint references an unknown field ID".to_string(),
);
}
if constraint
.index_id
.is_some_and(|id| !self.indexes.iter().any(|index| index.id == id))
{
return Err(
"diagnostic artifact constraint references an unknown index ID".to_string(),
);
}
if constraint
.relation_id
.is_some_and(|id| !self.relations.iter().any(|relation| relation.id == id))
{
return Err(
"diagnostic artifact constraint references an unknown relation ID".to_string(),
);
}
}
Ok(())
}
}
impl ResolvedDiagnosticEntity<'_> {
pub(crate) const fn entity_name(&self) -> &str {
self.entity.entity_name.as_str()
}
pub(crate) const fn entity_path(&self) -> &str {
self.entity.entity_path.as_str()
}
pub(crate) fn constraint_name(&self) -> Option<&str> {
self.constraint.map(|constraint| constraint.name.as_str())
}
pub(crate) fn constraint_kind(&self) -> Option<&str> {
self.constraint.map(|constraint| constraint.kind.as_str())
}
pub(crate) fn field_name(&self, id: u32) -> Option<&str> {
identity_name(self.entity.fields.as_slice(), id)
}
pub(crate) fn index_name(&self, id: u32) -> Option<&str> {
identity_name(self.entity.indexes.as_slice(), id)
}
pub(crate) fn relation_name(&self, id: u32) -> Option<&str> {
identity_name(self.entity.relations.as_slice(), id)
}
}
fn identities(names: BTreeMap<u32, String>) -> Vec<DiagnosticArtifactIdentity> {
names
.into_iter()
.map(|(id, name)| DiagnosticArtifactIdentity { id, name })
.collect()
}
fn identity_name(identities: &[DiagnosticArtifactIdentity], id: u32) -> Option<&str> {
identities
.iter()
.find(|identity| identity.id == id)
.map(|identity| identity.name.as_str())
}
fn insert_exact_name(
identities: &mut BTreeMap<u32, String>,
id: u32,
name: &str,
label: &str,
) -> Result<(), String> {
if let Some(existing) = identities.get(&id) {
if existing != name {
return Err(format!(
"diagnostic artifact {label} ID {id} has conflicting names"
));
}
return Ok(());
}
identities.insert(id, name.to_string());
Ok(())
}
fn validate_identities(
label: &str,
identities: &[DiagnosticArtifactIdentity],
maximum: usize,
) -> Result<(), String> {
validate_count(label, identities.len(), maximum)?;
let mut ids = BTreeSet::new();
for identity in identities {
if !ids.insert(identity.id) {
return Err(format!("diagnostic artifact contains duplicate {label} ID"));
}
validate_text(label, identity.name.as_str())?;
}
Ok(())
}
fn validate_count(label: &str, actual: usize, maximum: usize) -> Result<(), String> {
if actual > maximum {
return Err(format!(
"diagnostic artifact {label} count {actual} exceeds {maximum}"
));
}
Ok(())
}
fn validate_text(label: &str, value: &str) -> Result<(), String> {
if value.is_empty() || value.len() > icydb_schema::MAX_SOURCE_KEY_BYTES {
return Err(format!(
"diagnostic artifact {label} must contain 1..={} bytes",
icydb_schema::MAX_SOURCE_KEY_BYTES
));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn wrong_version_and_unknown_fields_fail_closed() {
let wrong_version = br#"{
"format":"icydb-diagnostic-schema",
"version":2,
"provenance":{"kind":"deployment","environment":"demo","canister":"app"},
"entities":[]
}"#;
let artifact: DiagnosticSchemaArtifact =
serde_json::from_slice(wrong_version).expect("shape should decode");
assert!(artifact.validate().is_err());
let unknown_field = br#"{
"format":"icydb-diagnostic-schema",
"version":1,
"provenance":{"kind":"deployment","environment":"demo","canister":"app"},
"entities":[],
"legacy":true
}"#;
assert!(serde_json::from_slice::<DiagnosticSchemaArtifact>(unknown_field).is_err());
}
#[test]
fn exact_fingerprint_and_entity_tag_are_required_for_resolution() {
let artifact = DiagnosticSchemaArtifact::test_fixture();
assert!(artifact.resolve(1, [7; 16], 42, Some(3)).is_some());
assert!(artifact.resolve(2, [7; 16], 42, Some(3)).is_none());
assert!(artifact.resolve(1, [8; 16], 42, Some(3)).is_none());
assert!(artifact.resolve(1, [7; 16], 43, Some(3)).is_none());
assert!(artifact.resolve(1, [7; 16], 42, Some(4)).is_none());
}
fn ordered_identity_fixture(order: [u32; 3]) -> DiagnosticSchemaArtifact {
let mut artifact = DiagnosticSchemaArtifact::test_fixture();
let entity = &mut artifact.entities[0];
for (label, identities) in [
("field", &mut entity.fields),
("index", &mut entity.indexes),
("relation", &mut entity.relations),
] {
*identities = order
.map(|id| DiagnosticArtifactIdentity {
id,
name: format!("{label}_{id}"),
})
.into();
}
entity.constraints = order
.map(|id| DiagnosticArtifactConstraint {
id,
name: format!("constraint_{id}"),
kind: "unique".to_string(),
field_ids: vec![id],
index_id: Some(id),
relation_id: Some(id),
})
.into();
artifact
}
#[test]
fn artifact_labels_resolve_independently_of_json_list_order() {
let root = ic_host_fs::testing::create_fixture_directory(
&std::env::temp_dir(),
"icydb-artifact-list-order",
)
.unwrap();
let path = root.join("artifact.json");
for order in [
[3, 5, 7],
[3, 7, 5],
[5, 3, 7],
[5, 7, 3],
[7, 3, 5],
[7, 5, 3],
] {
let deployment = ordered_identity_fixture(order);
let source = deployment
.clone()
.bind_to_source("schema/account.rs")
.unwrap();
for artifact in [deployment, source] {
std::fs::write(&path, serde_json::to_vec(&artifact).unwrap()).unwrap();
let decoded = match &artifact.provenance {
DiagnosticArtifactOwner::Deployment { .. } => {
DiagnosticSchemaArtifact::read_deployment(&path)
}
DiagnosticArtifactOwner::Source { .. } => {
DiagnosticSchemaArtifact::read_source_metadata(&path)
}
}
.unwrap();
assert_eq!(decoded, artifact);
let resolved = decoded.resolve(1, [7; 16], 42, None).unwrap();
for id in [3, 5, 7] {
assert_eq!(
resolved.field_name(id),
Some(format!("field_{id}").as_str())
);
assert_eq!(
resolved.index_name(id),
Some(format!("index_{id}").as_str())
);
assert_eq!(
resolved.relation_name(id),
Some(format!("relation_{id}").as_str())
);
let constraint = decoded.resolve(1, [7; 16], 42, Some(id)).unwrap();
assert_eq!(
constraint.constraint_name(),
Some(format!("constraint_{id}").as_str())
);
assert_eq!(constraint.constraint_kind(), Some("unique"));
}
for id in [0, 4, 8] {
assert_eq!(resolved.field_name(id), None);
assert_eq!(resolved.index_name(id), None);
assert_eq!(resolved.relation_name(id), None);
assert!(decoded.resolve(1, [7; 16], 42, Some(id)).is_none());
}
assert!(decoded.resolve(2, [7; 16], 42, Some(3)).is_none());
assert!(decoded.resolve(1, [8; 16], 42, Some(3)).is_none());
assert!(decoded.resolve(1, [7; 16], 43, Some(3)).is_none());
}
}
std::fs::remove_dir_all(root).unwrap();
}
#[test]
fn duplicate_artifact_ids_are_rejected_in_every_identity_list() {
for list in ["fields", "indexes", "relations", "constraints"] {
let mut json = serde_json::to_value(ordered_identity_fixture([7, 3, 5])).unwrap();
json["entities"][0][list][2]["id"] = serde_json::json!(3);
let artifact: DiagnosticSchemaArtifact = serde_json::from_value(json).unwrap();
assert!(artifact.validate().is_err(), "{list}");
}
}
#[test]
fn artifact_identity_is_projected_from_the_accepted_description() {
let report = [icydb::db::EntitySchemaDescription::new(
"schema::Account".to_string(),
"Account".to_string(),
42,
1,
[7; 16],
"id".to_string(),
vec!["id".to_string()],
Vec::new(),
Vec::new(),
Vec::new(),
Vec::new(),
1,
1,
)];
let artifact = DiagnosticSchemaArtifact::from_report("demo", "app", &report)
.expect("accepted description should export");
assert!(artifact.provenance_matches("demo", "app"));
let resolved = artifact
.resolve(1, [7; 16], 42, None)
.expect("exact accepted identity should resolve");
assert_eq!(resolved.entity_name(), "Account");
assert_eq!(resolved.entity_path(), "schema::Account");
}
#[test]
fn current_artifact_roundtrips_without_overwriting_existing_output() {
let artifact = DiagnosticSchemaArtifact::test_fixture();
let root = ic_host_fs::testing::create_fixture_directory(
&std::env::temp_dir(),
"icydb-diagnostic-artifact",
)
.unwrap();
let path = root.join("artifact.json");
artifact
.write_new(path.as_path())
.expect("current artifact should write");
assert_eq!(
DiagnosticSchemaArtifact::read_deployment(path.as_path())
.expect("current artifact should read"),
artifact
);
assert!(artifact.write_new(path.as_path()).is_err());
std::fs::remove_dir_all(root).expect("test fixture should be removable");
}
#[test]
#[cfg(unix)]
fn artifact_publication_preserves_parent_and_create_only_policy() {
use std::os::unix::fs::{PermissionsExt as _, symlink};
let root = ic_host_fs::testing::create_fixture_directory(
&std::env::temp_dir(),
"icydb-artifact-publication",
)
.unwrap();
let artifact = DiagnosticSchemaArtifact::test_fixture();
assert!(
artifact
.write_new(&root.join("missing/output.json"))
.is_err()
);
assert!(!root.join("missing").exists());
let parent = root.join("parent");
std::fs::create_dir(&parent).unwrap();
let linked_parent = root.join("linked-parent");
symlink(&parent, &linked_parent).unwrap();
let target = linked_parent.join("output.json");
artifact.write_new(&target).unwrap();
let expected = serde_json::to_vec_pretty(&artifact).unwrap();
assert_eq!(std::fs::read(&target).unwrap(), expected);
assert_eq!(std::fs::read_dir(&parent).unwrap().count(), 1);
let ordinary = parent.join("ordinary");
std::fs::write(&ordinary, b"preserved").unwrap();
assert_eq!(
std::fs::metadata(&target).unwrap().permissions().mode() & 0o777,
std::fs::metadata(&ordinary).unwrap().permissions().mode() & 0o777
);
let link = parent.join("link.json");
symlink(&ordinary, &link).unwrap();
assert!(artifact.write_new(&link).is_err());
assert_eq!(std::fs::read(&ordinary).unwrap(), b"preserved");
assert!(
std::fs::symlink_metadata(&link)
.unwrap()
.file_type()
.is_symlink()
);
assert!(artifact.write_new(&target).is_err());
assert_eq!(std::fs::read(&target).unwrap(), expected);
assert_eq!(std::fs::read_dir(&parent).unwrap().count(), 3);
std::fs::remove_dir_all(root).unwrap();
}
#[test]
#[cfg(unix)]
fn artifact_publication_rejects_directory_required_targets() {
let root = ic_host_fs::testing::create_fixture_directory(
&std::env::temp_dir(),
"icydb-artifact-target-suffix",
)
.unwrap();
let existing = root.join("existing");
std::fs::write(&existing, b"preserved").unwrap();
let artifact = DiagnosticSchemaArtifact::test_fixture();
for name in ["existing", "absent", "missing/child"] {
for suffix in ["/", "//", "/.", "/./.", "/.//"] {
let mut raw = root.join(name).into_os_string();
raw.push(suffix);
assert!(artifact.write_new(std::path::Path::new(&raw)).is_err());
assert_eq!(std::fs::read(&existing).unwrap(), b"preserved");
assert_eq!(std::fs::read_dir(&root).unwrap().count(), 1);
}
}
artifact.write_new(&root.join("./ordinary")).unwrap();
assert_eq!(std::fs::read_dir(&root).unwrap().count(), 2);
std::fs::remove_dir_all(root).unwrap();
}
#[test]
fn artifact_reads_preserve_exact_limits_and_reject_invalid_files() {
let root = ic_host_fs::testing::create_fixture_directory(
&std::env::temp_dir(),
"icydb-diagnostic-read-boundary",
)
.unwrap();
let path = root.join("artifact.json");
let artifact = DiagnosticSchemaArtifact::test_fixture();
let mut bytes = serde_json::to_vec(&artifact).unwrap();
bytes.resize(super::MAX_DIAGNOSTIC_ARTIFACT_BYTES, b' ');
std::fs::write(&path, &bytes).unwrap();
assert_eq!(
DiagnosticSchemaArtifact::read_deployment(&path).unwrap(),
artifact
);
bytes.push(b' ');
std::fs::write(&path, &bytes).unwrap();
assert!(DiagnosticSchemaArtifact::read_deployment(&path).is_err());
std::fs::write(&path, b"{invalid JSON").unwrap();
assert!(DiagnosticSchemaArtifact::read_deployment(&path).is_err());
std::fs::remove_file(&path).unwrap();
assert!(DiagnosticSchemaArtifact::read_deployment(&path).is_err());
std::fs::remove_dir_all(root).unwrap();
}
#[cfg(unix)]
#[test]
fn artifact_reads_preserve_symlink_selection_and_reject_special_files() {
let root = ic_host_fs::testing::create_fixture_directory(
&std::env::temp_dir(),
"icydb-diagnostic-read-link",
)
.unwrap();
let path = root.join("artifact.json");
let link = path.with_extension("link");
let artifact = DiagnosticSchemaArtifact::test_fixture();
std::fs::write(&path, serde_json::to_vec(&artifact).unwrap()).unwrap();
std::os::unix::fs::symlink(&path, &link).unwrap();
assert_eq!(
DiagnosticSchemaArtifact::read_deployment(&link).unwrap(),
artifact
);
assert!(DiagnosticSchemaArtifact::read_deployment(Path::new("/dev/null")).is_err());
std::fs::remove_dir_all(root).unwrap();
}
#[test]
fn deployment_and_source_provenance_are_not_interchangeable() {
let deployment = DiagnosticSchemaArtifact::test_fixture();
let source = DiagnosticSchemaArtifact::test_source_fixture();
let root = ic_host_fs::testing::create_fixture_directory(
&std::env::temp_dir(),
"icydb-diagnostic-provenance",
)
.unwrap();
let deployment_path = root.join("deployment.json");
let source_path = root.join("source.json");
deployment
.write_new(deployment_path.as_path())
.expect("deployment artifact should write");
source
.write_new(source_path.as_path())
.expect("source metadata should write");
assert!(
DiagnosticSchemaArtifact::read_deployment(source_path.as_path()).is_err(),
"source metadata must not impersonate a deployment artifact"
);
assert!(
DiagnosticSchemaArtifact::read_source_metadata(deployment_path.as_path()).is_err(),
"deployment provenance must not impersonate source metadata"
);
assert_eq!(
DiagnosticSchemaArtifact::read_source_metadata(source_path.as_path())
.expect("source metadata should read"),
source
);
std::fs::remove_dir_all(root).expect("test fixture should be removable");
}
#[test]
fn source_binding_preserves_the_exact_accepted_artifact_mapping() {
let deployment = DiagnosticSchemaArtifact::test_fixture();
let source = deployment
.clone()
.bind_to_source("schema/account.rs")
.expect("valid source identity should bind");
assert!(!source.provenance_matches("demo", "app"));
assert_eq!(source.entities, deployment.entities);
assert!(source.resolve(1, [7; 16], 42, Some(3)).is_some());
assert!(source.resolve(1, [7; 16], 42, Some(4)).is_none());
assert!(deployment.bind_to_source("").is_err());
}
}