use std::path::PathBuf;
pub type VolumeId = String;
pub type SchemaRef = String;
pub type AppId = String;
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum HashAlgo {
Md5,
Sha1,
Sha256,
Sha512,
Blake3,
Other(String),
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum IdentityClaim {
CanonicalPath { volume: VolumeId, path: PathBuf },
InodeIdentity {
volume: VolumeId,
inode: u64,
generation: Option<u32>,
},
NtfsFileRef {
volume: VolumeId,
mft_record: u64,
sequence: u16,
},
ApfsFileId { volume_uuid: [u8; 16], file_id: u64 },
ContentHash { algo: HashAlgo, digest: Vec<u8> },
RecordIdentity {
schema: SchemaRef,
primary_key: Vec<u8>,
},
ApplicationGuid { app: AppId, guid: [u8; 16] },
SigningSubject { issuer: String, subject: String },
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum IdentityDiscipline {
PathStable,
ContentStable,
ObjectStable,
RecordStable,
LogicalStable,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct CohortKey([u8; 32]);
impl CohortKey {
#[must_use]
pub fn new(bytes: [u8; 32]) -> Self {
Self(bytes)
}
#[must_use]
pub fn as_bytes(&self) -> &[u8; 32] {
&self.0
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ArtifactRef {
pub claims: Vec<IdentityClaim>,
}
impl ArtifactRef {
#[must_use]
pub fn matches(&self, other: &Self, discipline: IdentityDiscipline) -> bool {
for a in &self.claims {
for b in &other.claims {
if claims_match_under(a, b, discipline) {
return true;
}
}
}
false
}
#[must_use]
pub fn cohort_key(&self, discipline: IdentityDiscipline) -> CohortKey {
let mut key = [0u8; 32];
key[0] = discipline as u8;
for (i, claim) in self.claims.iter().enumerate() {
if claim_matches_discipline(claim, discipline) {
let bytes = claim_fingerprint(claim);
for (j, b) in bytes.iter().enumerate() {
key[(i + j + 1) % 32] ^= b;
}
}
}
CohortKey(key)
}
}
fn claims_match_under(a: &IdentityClaim, b: &IdentityClaim, d: IdentityDiscipline) -> bool {
match d {
IdentityDiscipline::PathStable => match (a, b) {
(
IdentityClaim::CanonicalPath {
volume: va,
path: pa,
},
IdentityClaim::CanonicalPath {
volume: vb,
path: pb,
},
) => va == vb && pa == pb,
_ => false,
},
IdentityDiscipline::ContentStable => match (a, b) {
(
IdentityClaim::ContentHash {
algo: aa,
digest: da,
},
IdentityClaim::ContentHash {
algo: ab,
digest: db,
},
) => aa == ab && da == db,
_ => false,
},
IdentityDiscipline::ObjectStable => match (a, b) {
(
IdentityClaim::InodeIdentity {
volume: va,
inode: ia,
generation: ga,
},
IdentityClaim::InodeIdentity {
volume: vb,
inode: ib,
generation: gb,
},
) => va == vb && ia == ib && ga == gb,
(
IdentityClaim::NtfsFileRef {
volume: va,
mft_record: ma,
sequence: sa,
},
IdentityClaim::NtfsFileRef {
volume: vb,
mft_record: mb,
sequence: sb,
},
) => va == vb && ma == mb && sa == sb,
_ => false,
},
IdentityDiscipline::RecordStable => match (a, b) {
(
IdentityClaim::RecordIdentity {
schema: sa,
primary_key: ka,
},
IdentityClaim::RecordIdentity {
schema: sb,
primary_key: kb,
},
) => sa == sb && ka == kb,
_ => false,
},
IdentityDiscipline::LogicalStable => {
claims_match_under(a, b, IdentityDiscipline::PathStable)
|| claims_match_under(a, b, IdentityDiscipline::RecordStable)
}
}
}
fn claim_matches_discipline(claim: &IdentityClaim, d: IdentityDiscipline) -> bool {
matches!(
(claim, d),
(
IdentityClaim::CanonicalPath { .. },
IdentityDiscipline::PathStable
) | (
IdentityClaim::ContentHash { .. },
IdentityDiscipline::ContentStable
) | (
IdentityClaim::InodeIdentity { .. } | IdentityClaim::NtfsFileRef { .. },
IdentityDiscipline::ObjectStable
) | (
IdentityClaim::RecordIdentity { .. },
IdentityDiscipline::RecordStable
)
)
}
fn claim_fingerprint(claim: &IdentityClaim) -> Vec<u8> {
match claim {
IdentityClaim::CanonicalPath { volume, path } => {
let mut v = volume.as_bytes().to_vec();
v.extend_from_slice(path.to_string_lossy().as_bytes());
v
}
IdentityClaim::InodeIdentity {
volume,
inode,
generation,
} => {
let mut v = volume.as_bytes().to_vec();
v.extend_from_slice(&inode.to_le_bytes());
if let Some(g) = generation {
v.extend_from_slice(&g.to_le_bytes());
}
v
}
IdentityClaim::NtfsFileRef {
volume,
mft_record,
sequence,
} => {
let mut v = volume.as_bytes().to_vec();
v.extend_from_slice(&mft_record.to_le_bytes());
v.extend_from_slice(&sequence.to_le_bytes());
v
}
IdentityClaim::ApfsFileId {
volume_uuid,
file_id,
} => {
let mut v = volume_uuid.to_vec();
v.extend_from_slice(&file_id.to_le_bytes());
v
}
IdentityClaim::ContentHash { digest, .. } => digest.clone(),
IdentityClaim::RecordIdentity {
schema,
primary_key,
} => {
let mut v = schema.as_bytes().to_vec();
v.extend_from_slice(primary_key);
v
}
IdentityClaim::ApplicationGuid { guid, .. } => guid.to_vec(),
IdentityClaim::SigningSubject { issuer, subject } => {
let mut v = issuer.as_bytes().to_vec();
v.extend_from_slice(subject.as_bytes());
v
}
}
}