snix_castore/proto/
mod.rs

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