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
17pub const FILE_DESCRIPTOR_SET: &[u8] = tonic::include_file_descriptor_set!("snix.castore.v1");
21
22#[cfg(test)]
23mod tests;
24
25#[derive(Debug, PartialEq, Eq, thiserror::Error)]
27pub enum ValidateStatBlobResponseError {
28 #[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 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()?,
56 self.directories.len().try_into().ok()?,
57 checked_sum(self.directories.iter().map(|e| e.size))?,
58 ])
59 }
60
61 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 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 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 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 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 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 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 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}