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