Skip to main content

ic_backup/model/command_custody/
mod.rs

1//! Validated v1 evidence binding one command sidecar to an exact journal operation.
2
3use serde::{Deserialize, Serialize};
4use std::path::{Path, PathBuf};
5use thiserror::Error;
6
7/// Maximum encoded bytes admitted when reading a standalone custody record.
8pub const MAX_COMMAND_CUSTODY_RECORD_BYTES: u64 = 32 * 1024;
9
10/// Retained local custody identity; this record alone grants no effect authority.
11#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
12#[serde(try_from = "CustodyFields")]
13pub struct CommandCustodyRecord {
14    version: u16,
15    journal: PathBuf,
16    operation_sequence: u64,
17    device: u64,
18    inode: u64,
19}
20
21#[derive(Deserialize)]
22#[serde(deny_unknown_fields)]
23struct CustodyFields {
24    version: u16,
25    journal: PathBuf,
26    operation_sequence: u64,
27    device: u64,
28    inode: u64,
29}
30
31impl TryFrom<CustodyFields> for CommandCustodyRecord {
32    type Error = CommandCustodyRecordError;
33    fn try_from(fields: CustodyFields) -> Result<Self, Self::Error> {
34        if fields.version != 1 {
35            return Err(CommandCustodyRecordError::UnsupportedVersion(
36                fields.version,
37            ));
38        }
39        Self::new(
40            fields.journal,
41            fields.operation_sequence,
42            fields.device,
43            fields.inode,
44        )
45    }
46}
47
48impl CommandCustodyRecord {
49    pub(crate) fn new(
50        journal: PathBuf,
51        operation_sequence: u64,
52        device: u64,
53        inode: u64,
54    ) -> Result<Self, CommandCustodyRecordError> {
55        if !super::journal_path::is_canonical(&journal, 4096) {
56            return Err(CommandCustodyRecordError::InvalidJournal { journal });
57        }
58        if inode == 0 {
59            return Err(CommandCustodyRecordError::UnknownFileIdentity);
60        }
61        Ok(Self {
62            version: 1,
63            journal,
64            operation_sequence,
65            device,
66            inode,
67        })
68    }
69
70    /// Return the resolved journal location bound to this custody identity.
71    #[must_use]
72    pub fn journal(&self) -> &Path {
73        &self.journal
74    }
75
76    /// Return the exact journal operation sequence.
77    #[must_use]
78    pub const fn operation_sequence(&self) -> u64 {
79        self.operation_sequence
80    }
81
82    /// Return the observed sidecar filesystem device identity.
83    #[must_use]
84    pub const fn device(&self) -> u64 {
85        self.device
86    }
87
88    /// Return the observed sidecar inode identity.
89    #[must_use]
90    pub const fn inode(&self) -> u64 {
91        self.inode
92    }
93
94    pub(crate) const fn matches_file(&self, device: u64, inode: u64) -> bool {
95        self.device == device && self.inode == inode
96    }
97}
98
99/// Invalid maintained-generation local custody evidence.
100#[derive(Debug, Error)]
101pub enum CommandCustodyRecordError {
102    /// The serialized generation is not maintained.
103    #[error("unsupported command custody version {0}")]
104    UnsupportedVersion(u16),
105    /// The journal location is not bounded, canonical, absolute UTF-8.
106    #[error("invalid command journal location: {journal:?}")]
107    InvalidJournal {
108        /// Rejected journal path.
109        journal: PathBuf,
110    },
111    /// No usable inode identity was observed.
112    #[error("command custody file identity is unknown")]
113    UnknownFileIdentity,
114}
115
116#[cfg(test)]
117mod tests;