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