Skip to main content

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