snix_castore/proto/
mod.rs

1use prost::Message;
2
3use std::cmp::Ordering;
4
5mod grpc_blobservice_wrapper;
6mod grpc_directoryservice_wrapper;
7
8use crate::{B3Digest, DirectoryError, path::PathComponent};
9pub use grpc_blobservice_wrapper::GRPCBlobServiceWrapper;
10pub use grpc_directoryservice_wrapper::GRPCDirectoryServiceWrapper;
11
12tonic::include_proto!("snix.castore.v1");
13
14#[cfg(feature = "tonic-reflection")]
15/// Compiled file descriptors for implementing [gRPC
16/// reflection](https://github.com/grpc/grpc/blob/master/doc/server-reflection.md) with e.g.
17/// [`tonic_reflection`](https://docs.rs/tonic-reflection).
18pub const FILE_DESCRIPTOR_SET: &[u8] = tonic::include_file_descriptor_set!("snix.castore.v1");
19
20#[cfg(test)]
21mod tests;
22
23/// Errors that occur during StatBlobResponse validation
24#[derive(Debug, PartialEq, Eq, thiserror::Error)]
25pub enum ValidateStatBlobResponseError {
26    /// Invalid digest length encountered
27    #[error("Invalid digest length {0} for chunk #{1}")]
28    InvalidDigestLen(usize, usize),
29}
30
31fn checked_sum(iter: impl IntoIterator<Item = u64>) -> Option<u64> {
32    iter.into_iter().try_fold(0u64, |acc, i| acc.checked_add(i))
33}
34
35impl Directory {
36    /// The size of a directory is the number of all regular and symlink elements,
37    /// the number of directory elements, and their size fields.
38    pub fn size(&self) -> u64 {
39        if cfg!(debug_assertions) {
40            self.size_checked()
41                .expect("Directory::size exceeds u64::MAX")
42        } else {
43            self.size_checked().unwrap_or(u64::MAX)
44        }
45    }
46
47    fn size_checked(&self) -> Option<u64> {
48        checked_sum([
49            self.files.len().try_into().ok()?,
50            self.symlinks.len().try_into().ok()?,
51            self.directories.len().try_into().ok()?,
52            checked_sum(self.directories.iter().map(|e| e.size))?,
53        ])
54    }
55
56    /// Calculates the digest of a Directory, which is the blake3 hash of a
57    /// Directory protobuf message, serialized in protobuf canonical form.
58    pub fn digest(&self) -> B3Digest {
59        let mut hasher = blake3::Hasher::new();
60
61        hasher
62            .update(&self.encode_to_vec())
63            .finalize()
64            .as_bytes()
65            .into()
66    }
67}
68
69impl TryFrom<Directory> for crate::Directory {
70    type Error = DirectoryError;
71
72    fn try_from(value: Directory) -> Result<Self, Self::Error> {
73        // Check directories, files and symlinks are sorted
74        // We'll notice duplicates across all three fields when constructing the Directory.
75        // FUTUREWORK: use is_sorted() once stable, and/or implement the producer for
76        // [crate::Directory::try_from_iter] iterating over all three and doing all checks inline.
77        value
78            .directories
79            .iter()
80            .try_fold(&b""[..], |prev_name, e| {
81                match e.name.as_ref().cmp(prev_name) {
82                    Ordering::Less => Err(DirectoryError::WrongSorting(e.name.to_owned())),
83                    Ordering::Equal => Err(DirectoryError::DuplicateName(
84                        e.name
85                            .to_owned()
86                            .try_into()
87                            .map_err(DirectoryError::InvalidName)?,
88                    )),
89                    Ordering::Greater => Ok(e.name.as_ref()),
90                }
91            })?;
92        value.files.iter().try_fold(&b""[..], |prev_name, e| {
93            match e.name.as_ref().cmp(prev_name) {
94                Ordering::Less => Err(DirectoryError::WrongSorting(e.name.to_owned())),
95                Ordering::Equal => Err(DirectoryError::DuplicateName(
96                    e.name
97                        .to_owned()
98                        .try_into()
99                        .map_err(DirectoryError::InvalidName)?,
100                )),
101                Ordering::Greater => Ok(e.name.as_ref()),
102            }
103        })?;
104        value.symlinks.iter().try_fold(&b""[..], |prev_name, e| {
105            match e.name.as_ref().cmp(prev_name) {
106                Ordering::Less => Err(DirectoryError::WrongSorting(e.name.to_owned())),
107                Ordering::Equal => Err(DirectoryError::DuplicateName(
108                    e.name
109                        .to_owned()
110                        .try_into()
111                        .map_err(DirectoryError::InvalidName)?,
112                )),
113                Ordering::Greater => Ok(e.name.as_ref()),
114            }
115        })?;
116
117        // FUTUREWORK: use is_sorted() once stable, and/or implement the producer for
118        // [crate::Directory::try_from_iter] iterating over all three and doing all checks inline.
119        let mut elems: Vec<(PathComponent, crate::Node)> =
120            Vec::with_capacity(value.directories.len() + value.files.len() + value.symlinks.len());
121
122        for e in value.directories {
123            elems.push(
124                Entry {
125                    entry: Some(entry::Entry::Directory(e)),
126                }
127                .try_into_name_and_node()?,
128            );
129        }
130
131        for e in value.files {
132            elems.push(
133                Entry {
134                    entry: Some(entry::Entry::File(e)),
135                }
136                .try_into_name_and_node()?,
137            )
138        }
139
140        for e in value.symlinks {
141            elems.push(
142                Entry {
143                    entry: Some(entry::Entry::Symlink(e)),
144                }
145                .try_into_name_and_node()?,
146            )
147        }
148
149        crate::Directory::try_from_iter(elems)
150    }
151}
152
153impl From<crate::Directory> for Directory {
154    fn from(value: crate::Directory) -> Self {
155        let mut directories = vec![];
156        let mut files = vec![];
157        let mut symlinks = vec![];
158
159        for (name, node) in value.into_nodes() {
160            match node {
161                crate::Node::File {
162                    digest,
163                    size,
164                    executable,
165                } => files.push(FileEntry {
166                    name: name.into(),
167                    digest: digest.into(),
168                    size,
169                    executable,
170                }),
171                crate::Node::Directory { digest, size } => directories.push(DirectoryEntry {
172                    name: name.into(),
173                    digest: digest.into(),
174                    size,
175                }),
176                crate::Node::Symlink { target } => {
177                    symlinks.push(SymlinkEntry {
178                        name: name.into(),
179                        target: target.into(),
180                    });
181                }
182            }
183        }
184
185        Directory {
186            directories,
187            files,
188            symlinks,
189        }
190    }
191}
192
193impl Entry {
194    /// Converts a proto [Entry] to a [crate::Node], and splits off the name as a [PathComponent].
195    pub fn try_into_name_and_node(self) -> Result<(PathComponent, crate::Node), DirectoryError> {
196        let (name_bytes, node) = self.try_into_unchecked_name_and_checked_node()?;
197        Ok((
198            name_bytes.try_into().map_err(DirectoryError::InvalidName)?,
199            node,
200        ))
201    }
202
203    /// Converts a proto [Entry] to a [crate::Node], and splits off the name as a
204    /// [bytes::Bytes] without doing any checking of it.
205    fn try_into_unchecked_name_and_checked_node(
206        self,
207    ) -> Result<(bytes::Bytes, crate::Node), DirectoryError> {
208        match self.entry.ok_or_else(|| DirectoryError::NoEntrySet)? {
209            entry::Entry::Directory(n) => {
210                let digest = B3Digest::try_from(n.digest)
211                    .map_err(|e| DirectoryError::InvalidNode(n.name.clone(), e.into()))?;
212
213                let node = crate::Node::Directory {
214                    digest,
215                    size: n.size,
216                };
217
218                Ok((n.name, node))
219            }
220            entry::Entry::File(n) => {
221                let digest = B3Digest::try_from(n.digest)
222                    .map_err(|e| DirectoryError::InvalidNode(n.name.clone(), e.into()))?;
223
224                let node = crate::Node::File {
225                    digest,
226                    size: n.size,
227                    executable: n.executable,
228                };
229
230                Ok((n.name, node))
231            }
232
233            entry::Entry::Symlink(n) => {
234                let node = crate::Node::Symlink {
235                    target: n.target.try_into().map_err(|e| {
236                        DirectoryError::InvalidNode(
237                            n.name.clone(),
238                            crate::ValidateNodeError::InvalidSymlinkTarget(e),
239                        )
240                    })?,
241                };
242
243                Ok((n.name, node))
244            }
245        }
246    }
247
248    /// Converts a proto [Entry] to a [crate::Node], and splits off the name and returns it as a
249    /// [bytes::Bytes].
250    ///
251    /// The name must be empty.
252    pub fn try_into_anonymous_node(self) -> Result<crate::Node, DirectoryError> {
253        let (name, node) = Self::try_into_unchecked_name_and_checked_node(self)?;
254
255        if !name.is_empty() {
256            return Err(DirectoryError::NameInAnonymousNode);
257        }
258
259        Ok(node)
260    }
261
262    /// Constructs an [Entry] from a name and [crate::Node].
263    /// The name is a [bytes::Bytes], not a [PathComponent], as we have use an
264    /// empty name in some places.
265    pub fn from_name_and_node(name: bytes::Bytes, n: crate::Node) -> Self {
266        match n {
267            crate::Node::Directory { digest, size } => Self {
268                entry: Some(entry::Entry::Directory(DirectoryEntry {
269                    name,
270                    digest: digest.into(),
271                    size,
272                })),
273            },
274            crate::Node::File {
275                digest,
276                size,
277                executable,
278            } => Self {
279                entry: Some(entry::Entry::File(FileEntry {
280                    name,
281                    digest: digest.into(),
282                    size,
283                    executable,
284                })),
285            },
286            crate::Node::Symlink { target } => Self {
287                entry: Some(entry::Entry::Symlink(SymlinkEntry {
288                    name,
289                    target: target.into(),
290                })),
291            },
292        }
293    }
294}
295
296impl StatBlobResponse {
297    /// Validates a StatBlobResponse. All chunks must have valid blake3 digests.
298    /// It is allowed to send an empty list, if no more granular chunking is
299    /// available.
300    pub fn validate(&self) -> Result<(), ValidateStatBlobResponseError> {
301        for (i, chunk) in self.chunks.iter().enumerate() {
302            if chunk.digest.len() != blake3::KEY_LEN {
303                return Err(ValidateStatBlobResponseError::InvalidDigestLen(
304                    chunk.digest.len(),
305                    i,
306                ));
307            }
308        }
309        Ok(())
310    }
311}