1use prost::Message;
2
3use std::cmp::Ordering;
4
5mod grpc_blobservice_wrapper;
6mod grpc_directoryservice_wrapper;
7
8use crate::{B3Digest, DirectoryError, path::PathComponent};
9pub use grpc_blobservice_wrapper::GRPCBlobServiceWrapper;
10pub use grpc_directoryservice_wrapper::GRPCDirectoryServiceWrapper;
11
12tonic::include_proto!("snix.castore.v1");
13
14#[cfg(feature = "tonic-reflection")]
15pub const FILE_DESCRIPTOR_SET: &[u8] = tonic::include_file_descriptor_set!("snix.castore.v1");
19
20#[cfg(test)]
21mod tests;
22
23#[derive(Debug, PartialEq, Eq, thiserror::Error)]
25pub enum ValidateStatBlobResponseError {
26 #[error("Invalid digest length {0} for chunk #{1}")]
28 InvalidDigestLen(usize, usize),
29}
30
31fn checked_sum(iter: impl IntoIterator<Item = u64>) -> Option<u64> {
32 iter.into_iter().try_fold(0u64, |acc, i| acc.checked_add(i))
33}
34
35impl Directory {
36 pub fn size(&self) -> u64 {
39 if cfg!(debug_assertions) {
40 self.size_checked()
41 .expect("Directory::size exceeds u64::MAX")
42 } else {
43 self.size_checked().unwrap_or(u64::MAX)
44 }
45 }
46
47 fn size_checked(&self) -> Option<u64> {
48 checked_sum([
49 self.files.len().try_into().ok()?,
50 self.symlinks.len().try_into().ok()?,
51 self.directories.len().try_into().ok()?,
52 checked_sum(self.directories.iter().map(|e| e.size))?,
53 ])
54 }
55
56 pub fn digest(&self) -> B3Digest {
59 let mut hasher = blake3::Hasher::new();
60
61 hasher
62 .update(&self.encode_to_vec())
63 .finalize()
64 .as_bytes()
65 .into()
66 }
67}
68
69impl TryFrom<Directory> for crate::Directory {
70 type Error = DirectoryError;
71
72 fn try_from(value: Directory) -> Result<Self, Self::Error> {
73 value
78 .directories
79 .iter()
80 .try_fold(&b""[..], |prev_name, e| {
81 match e.name.as_ref().cmp(prev_name) {
82 Ordering::Less => Err(DirectoryError::WrongSorting(e.name.to_owned())),
83 Ordering::Equal => Err(DirectoryError::DuplicateName(
84 e.name
85 .to_owned()
86 .try_into()
87 .map_err(DirectoryError::InvalidName)?,
88 )),
89 Ordering::Greater => Ok(e.name.as_ref()),
90 }
91 })?;
92 value.files.iter().try_fold(&b""[..], |prev_name, e| {
93 match e.name.as_ref().cmp(prev_name) {
94 Ordering::Less => Err(DirectoryError::WrongSorting(e.name.to_owned())),
95 Ordering::Equal => Err(DirectoryError::DuplicateName(
96 e.name
97 .to_owned()
98 .try_into()
99 .map_err(DirectoryError::InvalidName)?,
100 )),
101 Ordering::Greater => Ok(e.name.as_ref()),
102 }
103 })?;
104 value.symlinks.iter().try_fold(&b""[..], |prev_name, e| {
105 match e.name.as_ref().cmp(prev_name) {
106 Ordering::Less => Err(DirectoryError::WrongSorting(e.name.to_owned())),
107 Ordering::Equal => Err(DirectoryError::DuplicateName(
108 e.name
109 .to_owned()
110 .try_into()
111 .map_err(DirectoryError::InvalidName)?,
112 )),
113 Ordering::Greater => Ok(e.name.as_ref()),
114 }
115 })?;
116
117 let mut elems: Vec<(PathComponent, crate::Node)> =
120 Vec::with_capacity(value.directories.len() + value.files.len() + value.symlinks.len());
121
122 for e in value.directories {
123 elems.push(
124 Entry {
125 entry: Some(entry::Entry::Directory(e)),
126 }
127 .try_into_name_and_node()?,
128 );
129 }
130
131 for e in value.files {
132 elems.push(
133 Entry {
134 entry: Some(entry::Entry::File(e)),
135 }
136 .try_into_name_and_node()?,
137 )
138 }
139
140 for e in value.symlinks {
141 elems.push(
142 Entry {
143 entry: Some(entry::Entry::Symlink(e)),
144 }
145 .try_into_name_and_node()?,
146 )
147 }
148
149 crate::Directory::try_from_iter(elems)
150 }
151}
152
153impl From<crate::Directory> for Directory {
154 fn from(value: crate::Directory) -> Self {
155 let mut directories = vec![];
156 let mut files = vec![];
157 let mut symlinks = vec![];
158
159 for (name, node) in value.into_nodes() {
160 match node {
161 crate::Node::File {
162 digest,
163 size,
164 executable,
165 } => files.push(FileEntry {
166 name: name.into(),
167 digest: digest.into(),
168 size,
169 executable,
170 }),
171 crate::Node::Directory { digest, size } => directories.push(DirectoryEntry {
172 name: name.into(),
173 digest: digest.into(),
174 size,
175 }),
176 crate::Node::Symlink { target } => {
177 symlinks.push(SymlinkEntry {
178 name: name.into(),
179 target: target.into(),
180 });
181 }
182 }
183 }
184
185 Directory {
186 directories,
187 files,
188 symlinks,
189 }
190 }
191}
192
193impl Entry {
194 pub fn try_into_name_and_node(self) -> Result<(PathComponent, crate::Node), DirectoryError> {
196 let (name_bytes, node) = self.try_into_unchecked_name_and_checked_node()?;
197 Ok((
198 name_bytes.try_into().map_err(DirectoryError::InvalidName)?,
199 node,
200 ))
201 }
202
203 fn try_into_unchecked_name_and_checked_node(
206 self,
207 ) -> Result<(bytes::Bytes, crate::Node), DirectoryError> {
208 match self.entry.ok_or_else(|| DirectoryError::NoEntrySet)? {
209 entry::Entry::Directory(n) => {
210 let digest = B3Digest::try_from(n.digest)
211 .map_err(|e| DirectoryError::InvalidNode(n.name.clone(), e.into()))?;
212
213 let node = crate::Node::Directory {
214 digest,
215 size: n.size,
216 };
217
218 Ok((n.name, node))
219 }
220 entry::Entry::File(n) => {
221 let digest = B3Digest::try_from(n.digest)
222 .map_err(|e| DirectoryError::InvalidNode(n.name.clone(), e.into()))?;
223
224 let node = crate::Node::File {
225 digest,
226 size: n.size,
227 executable: n.executable,
228 };
229
230 Ok((n.name, node))
231 }
232
233 entry::Entry::Symlink(n) => {
234 let node = crate::Node::Symlink {
235 target: n.target.try_into().map_err(|e| {
236 DirectoryError::InvalidNode(
237 n.name.clone(),
238 crate::ValidateNodeError::InvalidSymlinkTarget(e),
239 )
240 })?,
241 };
242
243 Ok((n.name, node))
244 }
245 }
246 }
247
248 pub fn try_into_anonymous_node(self) -> Result<crate::Node, DirectoryError> {
253 let (name, node) = Self::try_into_unchecked_name_and_checked_node(self)?;
254
255 if !name.is_empty() {
256 return Err(DirectoryError::NameInAnonymousNode);
257 }
258
259 Ok(node)
260 }
261
262 pub fn from_name_and_node(name: bytes::Bytes, n: crate::Node) -> Self {
266 match n {
267 crate::Node::Directory { digest, size } => Self {
268 entry: Some(entry::Entry::Directory(DirectoryEntry {
269 name,
270 digest: digest.into(),
271 size,
272 })),
273 },
274 crate::Node::File {
275 digest,
276 size,
277 executable,
278 } => Self {
279 entry: Some(entry::Entry::File(FileEntry {
280 name,
281 digest: digest.into(),
282 size,
283 executable,
284 })),
285 },
286 crate::Node::Symlink { target } => Self {
287 entry: Some(entry::Entry::Symlink(SymlinkEntry {
288 name,
289 target: target.into(),
290 })),
291 },
292 }
293 }
294}
295
296impl StatBlobResponse {
297 pub fn validate(&self) -> Result<(), ValidateStatBlobResponseError> {
301 for (i, chunk) in self.chunks.iter().enumerate() {
302 if chunk.digest.len() != blake3::KEY_LEN {
303 return Err(ValidateStatBlobResponseError::InvalidDigestLen(
304 chunk.digest.len(),
305 i,
306 ));
307 }
308 }
309 Ok(())
310 }
311}