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")]
18pub const FILE_DESCRIPTOR_SET: &[u8] = tonic::include_file_descriptor_set!("snix.castore.v1");
22
23#[cfg(test)]
24mod tests;
25
26#[derive(Debug, PartialEq, Eq, thiserror::Error)]
28pub enum ValidateStatBlobResponseError {
29 #[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 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 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 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 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 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 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 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 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 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}