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 {
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 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 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 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 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 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 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 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 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}