Skip to main content

ic_backup/model/restore_references/
mod.rs

1//! Maintained v1 restore dependencies and model-owned retention transitions.
2
3use crate::model::artifacts::{ChecksumError, canonical_hash};
4use serde::{Deserialize, Deserializer, Serialize, de};
5use std::{
6    collections::BTreeSet,
7    fmt,
8    path::{Path, PathBuf},
9};
10use thiserror::Error;
11
12/// Maximum references admitted into one layout's retained dependency record.
13pub const MAX_RESTORE_REFERENCES: usize = 1024;
14/// Maximum UTF-8 bytes in a normalized absolute journal location.
15pub const MAX_JOURNAL_PATH_BYTES: usize = 4096;
16
17/// One exact journal location bound to immutable restore-intent digest bytes.
18#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
19#[serde(try_from = "ReferenceFields")]
20pub struct RestoreReferenceRecord {
21    journal: PathBuf,
22    authority: String,
23}
24
25#[derive(Deserialize)]
26#[serde(deny_unknown_fields)]
27struct ReferenceFields {
28    journal: PathBuf,
29    authority: String,
30}
31
32impl TryFrom<ReferenceFields> for RestoreReferenceRecord {
33    type Error = RestoreReferenceError;
34    fn try_from(fields: ReferenceFields) -> Result<Self, Self::Error> {
35        Self::new(fields.journal, &fields.authority)
36    }
37}
38
39impl RestoreReferenceRecord {
40    /// Bind a normalized absolute journal location to a SHA-256 intent digest.
41    ///
42    /// # Errors
43    /// Rejects relative, noncanonical, non-UTF-8 or excessive paths and invalid hashes.
44    pub fn new(journal: PathBuf, authority: &str) -> Result<Self, RestoreReferenceError> {
45        if !super::journal_path::is_canonical(&journal, MAX_JOURNAL_PATH_BYTES) {
46            return Err(RestoreReferenceError::InvalidJournal { journal });
47        }
48        let authority = canonical_hash(authority)?;
49        Ok(Self { journal, authority })
50    }
51
52    /// Return the resolved journal location, which need not currently exist.
53    #[must_use]
54    pub fn journal(&self) -> &Path {
55        &self.journal
56    }
57
58    /// Return the canonical immutable restore-intent digest.
59    #[must_use]
60    pub fn authority(&self) -> &str {
61        &self.authority
62    }
63}
64
65/// Durable unfinished restore dependencies for one backup layout.
66#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
67#[serde(try_from = "ReferencesFields")]
68pub struct RestoreReferencesRecord {
69    version: u16,
70    restores: Vec<RestoreReferenceRecord>,
71}
72
73#[derive(Deserialize)]
74#[serde(deny_unknown_fields)]
75struct ReferencesFields {
76    version: u16,
77    #[serde(deserialize_with = "read_bounded_references")]
78    restores: Vec<RestoreReferenceRecord>,
79}
80
81impl TryFrom<ReferencesFields> for RestoreReferencesRecord {
82    type Error = RestoreReferenceError;
83    fn try_from(mut fields: ReferencesFields) -> Result<Self, Self::Error> {
84        if fields.version != 1 {
85            return Err(RestoreReferenceError::UnsupportedVersion(fields.version));
86        }
87        let mut journals = BTreeSet::new();
88        for entry in &fields.restores {
89            if !journals.insert(entry.journal()) {
90                return Err(RestoreReferenceError::DuplicateJournal {
91                    journal: entry.journal.clone(),
92                });
93            }
94        }
95        fields
96            .restores
97            .sort_by(|left, right| left.journal.cmp(&right.journal));
98        Ok(Self {
99            version: 1,
100            restores: fields.restores,
101        })
102    }
103}
104
105impl RestoreReferencesRecord {
106    /// Construct an empty maintained-generation dependency record.
107    #[must_use]
108    pub const fn empty() -> Self {
109        Self {
110            version: 1,
111            restores: Vec::new(),
112        }
113    }
114
115    /// Read retained dependencies in canonical journal-path order.
116    #[must_use]
117    pub fn entries(&self) -> &[RestoreReferenceRecord] {
118        &self.restores
119    }
120
121    /// Report whether there are no retained restore dependencies.
122    #[must_use]
123    pub fn is_empty(&self) -> bool {
124        self.restores.is_empty()
125    }
126
127    pub(crate) fn release(
128        &mut self,
129        reference: &RestoreReferenceRecord,
130    ) -> Result<bool, RestoreReferenceError> {
131        let Some(index) = self
132            .restores
133            .iter()
134            .position(|entry| entry.journal == reference.journal)
135        else {
136            return Ok(false);
137        };
138        if self.restores[index] != *reference {
139            return Err(RestoreReferenceError::AuthorityConflict {
140                journal: reference.journal.clone(),
141            });
142        }
143        self.restores.remove(index);
144        Ok(true)
145    }
146
147    pub(crate) fn retain(
148        &mut self,
149        reference: RestoreReferenceRecord,
150    ) -> Result<bool, RestoreReferenceError> {
151        if let Some(existing) = self
152            .restores
153            .iter()
154            .find(|entry| entry.journal == reference.journal)
155        {
156            if existing != &reference {
157                return Err(RestoreReferenceError::AuthorityConflict {
158                    journal: reference.journal,
159                });
160            }
161            return Ok(false);
162        }
163        if self.restores.len() == MAX_RESTORE_REFERENCES {
164            return Err(RestoreReferenceError::TooManyReferences {
165                limit: MAX_RESTORE_REFERENCES,
166            });
167        }
168        self.restores.push(reference);
169        self.restores
170            .sort_by(|left, right| left.journal.cmp(&right.journal));
171        Ok(true)
172    }
173}
174
175fn read_bounded_references<'de, D: Deserializer<'de>>(
176    deserializer: D,
177) -> Result<Vec<RestoreReferenceRecord>, D::Error> {
178    struct ReferencesVisitor;
179    impl<'de> de::Visitor<'de> for ReferencesVisitor {
180        type Value = Vec<RestoreReferenceRecord>;
181        fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
182            formatter.write_str("a bounded list of restore references")
183        }
184        fn visit_seq<A: de::SeqAccess<'de>>(
185            self,
186            mut sequence: A,
187        ) -> Result<Self::Value, A::Error> {
188            let mut entries = Vec::new();
189            while let Some(entry) = sequence.next_element()? {
190                if entries.len() == MAX_RESTORE_REFERENCES {
191                    return Err(de::Error::custom("restore reference count exceeds limit"));
192                }
193                entries.push(entry);
194            }
195            Ok(entries)
196        }
197    }
198    deserializer.deserialize_seq(ReferencesVisitor)
199}
200
201/// Typed schema or immutable-reference transition failure.
202#[derive(Debug, Error)]
203pub enum RestoreReferenceError {
204    /// The journal path is not an exact bounded absolute UTF-8 location.
205    #[error("invalid restore journal location: {journal:?}")]
206    InvalidJournal {
207        /// Rejected journal location.
208        journal: PathBuf,
209    },
210    /// An intent digest is malformed.
211    #[error(transparent)]
212    Checksum(#[from] ChecksumError),
213    /// The persisted reference generation is not maintained.
214    #[error("unsupported restore references version {0}")]
215    UnsupportedVersion(u16),
216    /// A decoded record repeats one journal location.
217    #[error("duplicate restore journal location: {journal:?}")]
218    DuplicateJournal {
219        /// Repeated journal location.
220        journal: PathBuf,
221    },
222    /// An existing journal location is bound to different intent bytes.
223    #[error("restore authority conflict at {journal:?}")]
224    AuthorityConflict {
225        /// Retained journal location.
226        journal: PathBuf,
227    },
228    /// Retaining another dependency would exceed the maintained count bound.
229    #[error("restore reference count exceeds {limit}")]
230    TooManyReferences {
231        /// Maximum admitted entries.
232        limit: usize,
233    },
234}
235
236#[cfg(test)]
237mod tests;