Skip to main content

rustev_contract/
retention.rs

1//! Retention items and availability (spec 004, 3.1.2, 3.1.6 and 3.1.7).
2//!
3//! The host owns storage, access checks, expiry and erasure. A retention
4//! item declares what the host kept of one document and until when; Rustev
5//! validates the declaration and, at a caller-supplied time, the bytes. A
6//! digest proves integrity relative to expected bytes, never truth,
7//! authorization or producer identity.
8
9use std::fmt;
10
11use serde::{Deserialize, Serialize};
12
13use crate::ids::ContentDigest;
14use crate::time::Timestamp;
15
16/// A new bundle's metadata lifetime unless the host chooses otherwise.
17pub const DEFAULT_METADATA_LIFETIME_MS: u64 = 7 * DAY_MS;
18/// The hard maximum lifetime of a bundle, from its creation. Extending it
19/// requires an amendment; the host may enforce a shorter cap.
20pub const MAX_BUNDLE_LIFETIME_MS: u64 = 30 * DAY_MS;
21const DAY_MS: u64 = 24 * 60 * 60 * 1000;
22
23/// One retained document, as the host declares it.
24#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
25#[serde(deny_unknown_fields)]
26pub struct RetentionItem {
27    /// The schema string the document must carry.
28    pub document_schema: String,
29    /// The document's identity, where its schema defines one.
30    pub identity: Option<ContentDigest>,
31    pub retention: Retention,
32}
33
34/// How the bytes were kept. Every mode has an expiry.
35#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
36#[serde(rename_all = "snake_case", deny_unknown_fields)]
37pub enum Retention {
38    /// The record canonical document, embedded as a JSON UTF-8 string.
39    Retained {
40        bytes: String,
41        digest: ContentDigest,
42        expires_at_ms: Timestamp,
43    },
44    /// An opaque host reference; never an instruction to fetch a URL.
45    External {
46        reference: String,
47        digest: ContentDigest,
48        expires_at_ms: Timestamp,
49    },
50    /// Only the digest: always `missing` at replay.
51    DigestOnly {
52        digest: ContentDigest,
53        expires_at_ms: Timestamp,
54    },
55}
56
57impl Retention {
58    pub fn digest(&self) -> &ContentDigest {
59        match self {
60            Retention::Retained { digest, .. }
61            | Retention::External { digest, .. }
62            | Retention::DigestOnly { digest, .. } => digest,
63        }
64    }
65
66    pub fn expires_at(&self) -> Timestamp {
67        match self {
68            Retention::Retained { expires_at_ms, .. }
69            | Retention::External { expires_at_ms, .. }
70            | Retention::DigestOnly { expires_at_ms, .. } => *expires_at_ms,
71        }
72    }
73
74    /// The mode's name, for reports.
75    pub fn mode(&self) -> &'static str {
76        match self {
77            Retention::Retained { .. } => "retained",
78            Retention::External { .. } => "external",
79            Retention::DigestOnly { .. } => "digest_only",
80        }
81    }
82}
83
84/// What a retained input is at replay time. Only `available` contributes
85/// to reproduction; none of the others is a pass.
86#[derive(
87    Debug,
88    Clone,
89    Copy,
90    PartialEq,
91    Eq,
92    PartialOrd,
93    Ord,
94    Hash,
95    Serialize,
96    Deserialize,
97    schemars::JsonSchema,
98)]
99#[serde(rename_all = "snake_case")]
100pub enum Availability {
101    Available,
102    /// Digest-only, or the host has no such bytes.
103    Missing,
104    /// `now_ms >= expires_at_ms`; takes precedence over erasure at replay.
105    Expired,
106    /// The host reports the bytes erased before expiry.
107    Erased,
108    /// The bytes do not match the digest, are not valid JSON, or are not in
109    /// record canonical form.
110    Corrupt,
111    /// The host refused, failed or panicked while resolving.
112    Inaccessible,
113    /// Intact bytes with a wrong schema, identity, cross-reference or role.
114    Mismatched,
115}
116
117impl Availability {
118    pub fn name(self) -> &'static str {
119        match self {
120            Availability::Available => "available",
121            Availability::Missing => "missing",
122            Availability::Expired => "expired",
123            Availability::Erased => "erased",
124            Availability::Corrupt => "corrupt",
125            Availability::Inaccessible => "inaccessible",
126            Availability::Mismatched => "mismatched",
127        }
128    }
129}
130
131impl fmt::Display for Availability {
132    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
133        f.write_str(self.name())
134    }
135}