1use std::fs::{self, DirEntry, OpenOptions};
9use std::io::Write;
10use std::path::{Path, PathBuf};
11use std::sync::{Arc, Mutex};
12use std::time::{SystemTime, UNIX_EPOCH};
13
14use super::super::cid::Cid;
15use super::super::error::Error;
16use super::super::manifest::{
17 sort_named_root_manifests, ManifestStore, ManifestStoreScan, ManifestUpdate, NamedRootManifest,
18 RootManifest,
19};
20use super::super::transaction::{
21 RootCondition, RootWrite, TransactionConflict, TransactionNodeWrite, TransactionUpdate,
22 TransactionalStore,
23};
24use super::{sort_cids, BatchOp, NodeStoreScan, Store};
25
26#[derive(Clone, Debug)]
33pub struct FileNodeStore {
34 root: PathBuf,
35 manifest_lock: Arc<Mutex<()>>,
36}
37
38impl FileNodeStore {
39 pub fn open(root: impl Into<PathBuf>) -> Result<Self, FileNodeStoreError> {
41 let store = Self {
42 root: root.into(),
43 manifest_lock: Arc::new(Mutex::new(())),
44 };
45 store.ensure_namespace_dirs()?;
46 Ok(store)
47 }
48
49 pub fn root(&self) -> &Path {
51 &self.root
52 }
53
54 pub fn node_namespace_dir(&self) -> PathBuf {
56 self.root.join("nodes").join("sha256")
57 }
58
59 pub fn hint_namespace_dir(&self) -> PathBuf {
61 self.root.join("hints")
62 }
63
64 pub fn root_namespace_dir(&self) -> PathBuf {
66 self.root.join("roots")
67 }
68
69 pub fn path_for_cid(&self, cid: &Cid) -> PathBuf {
71 self.node_path(cid)
72 }
73
74 fn ensure_namespace_dirs(&self) -> Result<(), FileNodeStoreError> {
75 for path in [
76 self.node_namespace_dir(),
77 self.hint_namespace_dir(),
78 self.root_namespace_dir(),
79 ] {
80 fs::create_dir_all(&path).map_err(|source| FileNodeStoreError::Io { path, source })?;
81 }
82 Ok(())
83 }
84
85 fn node_path(&self, cid: &Cid) -> PathBuf {
86 let hex = cid_hex(cid);
87 self.node_namespace_dir()
88 .join(&hex[0..2])
89 .join(&hex[2..4])
90 .join(hex)
91 }
92
93 fn hint_path(&self, namespace: &[u8], key: &[u8]) -> PathBuf {
94 self.hint_namespace_dir()
95 .join(hex_label(namespace))
96 .join(hex_label(key))
97 }
98
99 fn root_path(&self, name: &[u8]) -> PathBuf {
100 self.root_namespace_dir().join(hex_label(name))
101 }
102
103 fn read_node_path(
104 &self,
105 cid: &Cid,
106 path: &Path,
107 ) -> Result<Option<Vec<u8>>, FileNodeStoreError> {
108 match fs::read(path) {
109 Ok(bytes) => {
110 validate_node_bytes(cid, path, &bytes)?;
111 Ok(Some(bytes))
112 }
113 Err(source) if source.kind() == std::io::ErrorKind::NotFound => Ok(None),
114 Err(source) => Err(FileNodeStoreError::Io {
115 path: path.to_path_buf(),
116 source,
117 }),
118 }
119 }
120
121 fn write_node(&self, cid: &Cid, bytes: &[u8]) -> Result<(), FileNodeStoreError> {
122 validate_node_bytes(cid, &self.node_path(cid), bytes)?;
123 let path = self.node_path(cid);
124 if self.read_node_path(cid, &path)?.is_some() {
125 return Ok(());
126 }
127 write_file_atomically(&path, bytes)
128 }
129
130 fn read_root_path(&self, path: &Path) -> Result<Option<RootManifest>, FileNodeStoreError> {
131 match fs::read(path) {
132 Ok(bytes) => RootManifest::from_bytes(&bytes).map(Some).map_err(|err| {
133 FileNodeStoreError::Manifest {
134 path: path.to_path_buf(),
135 message: err.to_string(),
136 }
137 }),
138 Err(source) if source.kind() == std::io::ErrorKind::NotFound => Ok(None),
139 Err(source) => Err(FileNodeStoreError::Io {
140 path: path.to_path_buf(),
141 source,
142 }),
143 }
144 }
145
146 fn write_root_path(
147 &self,
148 path: &Path,
149 manifest: &RootManifest,
150 ) -> Result<(), FileNodeStoreError> {
151 let bytes = manifest
152 .to_bytes()
153 .map_err(|err| FileNodeStoreError::Manifest {
154 path: path.to_path_buf(),
155 message: err.to_string(),
156 })?;
157 write_file_atomically(path, &bytes)
158 }
159}
160
161impl Store for FileNodeStore {
162 type Error = FileNodeStoreError;
163
164 fn get(&self, key: &[u8]) -> Result<Option<Vec<u8>>, Self::Error> {
165 let cid = cid_from_node_key(key)?;
166 self.read_node_path(&cid, &self.node_path(&cid))
167 }
168
169 fn put(&self, key: &[u8], value: &[u8]) -> Result<(), Self::Error> {
170 let cid = cid_from_node_key(key)?;
171 self.write_node(&cid, value)
172 }
173
174 fn delete(&self, key: &[u8]) -> Result<(), Self::Error> {
175 let cid = cid_from_node_key(key)?;
176 let path = self.node_path(&cid);
177 match fs::remove_file(&path) {
178 Ok(()) => {
179 remove_empty_dirs(&path, &self.node_namespace_dir());
180 Ok(())
181 }
182 Err(source) if source.kind() == std::io::ErrorKind::NotFound => Ok(()),
183 Err(source) => Err(FileNodeStoreError::Io { path, source }),
184 }
185 }
186
187 fn batch(&self, ops: &[BatchOp<'_>]) -> Result<(), Self::Error> {
188 let mut validated = Vec::with_capacity(ops.len());
189 for op in ops {
190 match op {
191 BatchOp::Upsert { key, value } => {
192 let cid = cid_from_node_key(key)?;
193 validate_node_bytes(&cid, &self.node_path(&cid), value)?;
194 validated.push(BatchOpOwned::Upsert {
195 cid,
196 value: (*value).to_vec(),
197 });
198 }
199 BatchOp::Delete { key } => {
200 validated.push(BatchOpOwned::Delete {
201 cid: cid_from_node_key(key)?,
202 });
203 }
204 }
205 }
206
207 for op in validated {
208 match op {
209 BatchOpOwned::Upsert { cid, value } => self.write_node(&cid, &value)?,
210 BatchOpOwned::Delete { cid } => self.delete(cid.as_bytes())?,
211 }
212 }
213 Ok(())
214 }
215
216 fn batch_get(
217 &self,
218 keys: &[&[u8]],
219 ) -> Result<std::collections::HashMap<Vec<u8>, Vec<u8>>, Self::Error> {
220 let mut results = std::collections::HashMap::with_capacity(keys.len());
221 for key in keys {
222 if results.contains_key(*key) {
223 continue;
224 }
225 if let Some(value) = self.get(key)? {
226 results.insert((*key).to_vec(), value);
227 }
228 }
229 Ok(results)
230 }
231
232 fn batch_get_ordered(&self, keys: &[&[u8]]) -> Result<Vec<Option<Vec<u8>>>, Self::Error> {
233 keys.iter().map(|key| self.get(key)).collect()
234 }
235
236 fn batch_get_ordered_unique(
237 &self,
238 keys: &[&[u8]],
239 ) -> Result<Vec<Option<Vec<u8>>>, Self::Error> {
240 keys.iter().map(|key| self.get(key)).collect()
241 }
242
243 fn batch_put(&self, entries: &[(&[u8], &[u8])]) -> Result<(), Self::Error> {
244 let mut validated = Vec::with_capacity(entries.len());
245 for (key, value) in entries {
246 let cid = cid_from_node_key(key)?;
247 validate_node_bytes(&cid, &self.node_path(&cid), value)?;
248 validated.push((cid, (*value).to_vec()));
249 }
250
251 for (cid, value) in validated {
252 self.write_node(&cid, &value)?;
253 }
254 Ok(())
255 }
256
257 fn supports_hints(&self) -> bool {
258 true
259 }
260
261 fn get_hint(&self, namespace: &[u8], key: &[u8]) -> Result<Option<Vec<u8>>, Self::Error> {
262 let path = self.hint_path(namespace, key);
263 match fs::read(&path) {
264 Ok(bytes) => Ok(Some(bytes)),
265 Err(source) if source.kind() == std::io::ErrorKind::NotFound => Ok(None),
266 Err(source) => Err(FileNodeStoreError::Io { path, source }),
267 }
268 }
269
270 fn put_hint(&self, namespace: &[u8], key: &[u8], value: &[u8]) -> Result<(), Self::Error> {
271 write_file_atomically(&self.hint_path(namespace, key), value)
272 }
273
274 fn batch_put_with_hint(
275 &self,
276 entries: &[(&[u8], &[u8])],
277 namespace: &[u8],
278 key: &[u8],
279 value: &[u8],
280 ) -> Result<(), Self::Error> {
281 self.batch_put(entries)?;
282 self.put_hint(namespace, key, value)
283 }
284}
285
286impl NodeStoreScan for FileNodeStore {
287 type Error = FileNodeStoreError;
288
289 fn list_node_cids(&self) -> Result<Vec<Cid>, Self::Error> {
290 let namespace = self.node_namespace_dir();
291 if !namespace.exists() {
292 return Ok(Vec::new());
293 }
294
295 let mut cids = Vec::new();
296 for first in read_visible_dir_entries(&namespace)? {
297 ensure_shard_dir(&first, 2)?;
298 for second in read_visible_dir_entries(&first.path())? {
299 ensure_shard_dir(&second, 2)?;
300 for node in read_visible_dir_entries(&second.path())? {
301 let path = node.path();
302 let file_type = node.file_type().map_err(|source| FileNodeStoreError::Io {
303 path: path.clone(),
304 source,
305 })?;
306 if !file_type.is_file() {
307 return Err(FileNodeStoreError::InvalidPath {
308 path,
309 message: "node namespace entry is not a file".to_string(),
310 });
311 }
312
313 let name = entry_name(&node)?;
314 let cid =
315 parse_cid_hex(&name).ok_or_else(|| FileNodeStoreError::InvalidPath {
316 path: path.clone(),
317 message: "node filename is not a 64-byte hex CID".to_string(),
318 })?;
319 if name[0..2] != entry_name(&first)? || name[2..4] != entry_name(&second)? {
320 return Err(FileNodeStoreError::InvalidPath {
321 path,
322 message: "node CID does not match shard directories".to_string(),
323 });
324 }
325 cids.push(cid);
326 }
327 }
328 }
329
330 sort_cids(&mut cids);
331 Ok(cids)
332 }
333}
334
335impl ManifestStore for FileNodeStore {
336 type Error = FileNodeStoreError;
337
338 fn get_root(&self, name: &[u8]) -> Result<Option<RootManifest>, Self::Error> {
339 self.read_root_path(&self.root_path(name))
340 }
341
342 fn put_root(&self, name: &[u8], manifest: &RootManifest) -> Result<(), Self::Error> {
343 let _guard = self
344 .manifest_lock
345 .lock()
346 .map_err(|err| FileNodeStoreError::LockPoisoned(err.to_string()))?;
347 self.write_root_path(&self.root_path(name), manifest)
348 }
349
350 fn delete_root(&self, name: &[u8]) -> Result<(), Self::Error> {
351 let _guard = self
352 .manifest_lock
353 .lock()
354 .map_err(|err| FileNodeStoreError::LockPoisoned(err.to_string()))?;
355 let path = self.root_path(name);
356 match fs::remove_file(&path) {
357 Ok(()) => Ok(()),
358 Err(source) if source.kind() == std::io::ErrorKind::NotFound => Ok(()),
359 Err(source) => Err(FileNodeStoreError::Io { path, source }),
360 }
361 }
362
363 fn compare_and_swap_root(
364 &self,
365 name: &[u8],
366 expected: Option<&RootManifest>,
367 new: Option<&RootManifest>,
368 ) -> Result<ManifestUpdate, Self::Error> {
369 let _guard = self
370 .manifest_lock
371 .lock()
372 .map_err(|err| FileNodeStoreError::LockPoisoned(err.to_string()))?;
373 let path = self.root_path(name);
374 let current = self.read_root_path(&path)?;
375 if current.as_ref() != expected {
376 return Ok(ManifestUpdate::Conflict { current });
377 }
378
379 match new {
380 Some(manifest) => self.write_root_path(&path, manifest)?,
381 None => match fs::remove_file(&path) {
382 Ok(()) => {}
383 Err(source) if source.kind() == std::io::ErrorKind::NotFound => {}
384 Err(source) => return Err(FileNodeStoreError::Io { path, source }),
385 },
386 }
387
388 Ok(ManifestUpdate::Applied)
389 }
390}
391
392impl ManifestStoreScan for FileNodeStore {
393 fn list_roots(&self) -> Result<Vec<NamedRootManifest>, Self::Error> {
394 let namespace = self.root_namespace_dir();
395 if !namespace.exists() {
396 return Ok(Vec::new());
397 }
398
399 let mut roots = Vec::new();
400 for entry in read_visible_dir_entries(&namespace)? {
401 let path = entry.path();
402 let file_type = entry.file_type().map_err(|source| FileNodeStoreError::Io {
403 path: path.clone(),
404 source,
405 })?;
406 if !file_type.is_file() {
407 return Err(FileNodeStoreError::InvalidPath {
408 path,
409 message: "root namespace entry is not a file".to_string(),
410 });
411 }
412
413 let name_hex = entry_name(&entry)?;
414 let name =
415 decode_hex_label(&name_hex).ok_or_else(|| FileNodeStoreError::InvalidPath {
416 path: entry.path(),
417 message: "root filename is not an encoded name".to_string(),
418 })?;
419 let Some(manifest) = self.read_root_path(&entry.path())? else {
420 continue;
421 };
422 roots.push(NamedRootManifest::new(name, manifest));
423 }
424
425 sort_named_root_manifests(&mut roots);
426 Ok(roots)
427 }
428}
429
430impl TransactionalStore for FileNodeStore {
431 fn supports_transactions(&self) -> bool {
432 true
433 }
434
435 fn commit_transaction(
436 &self,
437 node_writes: &[TransactionNodeWrite],
438 root_conditions: &[RootCondition],
439 root_writes: &[RootWrite],
440 ) -> Result<TransactionUpdate, Error> {
441 let mut validated_nodes = Vec::with_capacity(node_writes.len());
442 for write in node_writes {
443 match write {
444 TransactionNodeWrite::Upsert { key, value } => {
445 let cid = cid_from_node_key(key).map_err(|err| Error::Store(Box::new(err)))?;
446 validate_node_bytes(&cid, &self.node_path(&cid), value)
447 .map_err(|err| Error::Store(Box::new(err)))?;
448 validated_nodes.push(BatchOpOwned::Upsert {
449 cid,
450 value: value.clone(),
451 });
452 }
453 TransactionNodeWrite::Delete { key } => {
454 validated_nodes.push(BatchOpOwned::Delete {
455 cid: cid_from_node_key(key).map_err(|err| Error::Store(Box::new(err)))?,
456 });
457 }
458 }
459 }
460
461 let _guard = self.manifest_lock.lock().map_err(|err| {
462 Error::Store(Box::new(FileNodeStoreError::LockPoisoned(err.to_string())))
463 })?;
464
465 for condition in root_conditions {
466 let current = self
467 .read_root_path(&self.root_path(&condition.name))
468 .map_err(|err| Error::Store(Box::new(err)))?;
469 if current != condition.expected {
470 return Ok(TransactionUpdate::Conflict(Box::new(
471 TransactionConflict::new(
472 condition.name.clone(),
473 condition.expected.clone(),
474 current,
475 ),
476 )));
477 }
478 }
479
480 for op in validated_nodes {
481 match op {
482 BatchOpOwned::Upsert { cid, value } => {
483 self.write_node(&cid, &value)
484 .map_err(|err| Error::Store(Box::new(err)))?;
485 }
486 BatchOpOwned::Delete { cid } => self
487 .delete(cid.as_bytes())
488 .map_err(|err| Error::Store(Box::new(err)))?,
489 }
490 }
491
492 let snapshots = root_writes
493 .iter()
494 .map(|write| {
495 let name = write.name().to_vec();
496 self.read_root_path(&self.root_path(&name))
497 .map(|previous| (name, previous))
498 })
499 .collect::<Result<Vec<_>, _>>()
500 .map_err(|err| Error::Store(Box::new(err)))?;
501
502 let mut applied: Vec<(Vec<u8>, Option<RootManifest>)> = Vec::new();
503 for write in root_writes {
504 if let Err(err) = self.apply_root_write_unlocked(write) {
505 for (name, previous) in applied.into_iter().rev() {
506 let _ = self.restore_root_unlocked(&name, previous.as_ref());
507 }
508 return Err(Error::Store(Box::new(err)));
509 }
510 applied.push((
511 write.name().to_vec(),
512 snapshots
513 .iter()
514 .find(|(name, _)| name.as_slice() == write.name())
515 .and_then(|(_, previous)| previous.clone()),
516 ));
517 }
518
519 Ok(TransactionUpdate::Applied {
520 nodes_written: node_writes.len(),
521 roots_written: root_writes.len(),
522 })
523 }
524}
525
526impl FileNodeStore {
527 fn apply_root_write_unlocked(&self, write: &RootWrite) -> Result<(), FileNodeStoreError> {
528 match write {
529 RootWrite::Put { name, manifest } => {
530 self.write_root_path(&self.root_path(name), manifest)
531 }
532 RootWrite::Delete { name } => self.restore_root_unlocked(name, None),
533 }
534 }
535
536 fn restore_root_unlocked(
537 &self,
538 name: &[u8],
539 manifest: Option<&RootManifest>,
540 ) -> Result<(), FileNodeStoreError> {
541 let path = self.root_path(name);
542 match manifest {
543 Some(manifest) => self.write_root_path(&path, manifest),
544 None => match fs::remove_file(&path) {
545 Ok(()) => Ok(()),
546 Err(source) if source.kind() == std::io::ErrorKind::NotFound => Ok(()),
547 Err(source) => Err(FileNodeStoreError::Io { path, source }),
548 },
549 }
550 }
551}
552
553#[derive(Debug)]
554enum BatchOpOwned {
555 Upsert { cid: Cid, value: Vec<u8> },
556 Delete { cid: Cid },
557}
558
559#[derive(Debug)]
561pub enum FileNodeStoreError {
562 Io {
564 path: PathBuf,
566 source: std::io::Error,
568 },
569 InvalidKey {
571 message: String,
573 },
574 CidMismatch {
576 path: PathBuf,
578 expected: Cid,
580 actual: Cid,
582 },
583 InvalidPath {
585 path: PathBuf,
587 message: String,
589 },
590 Manifest {
592 path: PathBuf,
594 message: String,
596 },
597 LockPoisoned(String),
599}
600
601impl std::fmt::Display for FileNodeStoreError {
602 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
603 match self {
604 Self::Io { path, source } => {
605 write!(
606 f,
607 "file node store I/O error at {}: {source}",
608 path.display()
609 )
610 }
611 Self::InvalidKey { message } => write!(f, "invalid file node key: {message}"),
612 Self::CidMismatch {
613 path,
614 expected,
615 actual,
616 } => write!(
617 f,
618 "file node CID mismatch at {}: expected {:?}, got {:?}",
619 path.display(),
620 expected,
621 actual
622 ),
623 Self::InvalidPath { path, message } => {
624 write!(f, "invalid file node path {}: {message}", path.display())
625 }
626 Self::Manifest { path, message } => {
627 write!(f, "invalid file node root {}: {message}", path.display())
628 }
629 Self::LockPoisoned(message) => write!(f, "file node manifest lock poisoned: {message}"),
630 }
631 }
632}
633
634impl std::error::Error for FileNodeStoreError {
635 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
636 match self {
637 Self::Io { source, .. } => Some(source),
638 Self::InvalidKey { .. }
639 | Self::CidMismatch { .. }
640 | Self::InvalidPath { .. }
641 | Self::Manifest { .. }
642 | Self::LockPoisoned(_) => None,
643 }
644 }
645}
646
647fn cid_from_node_key(key: &[u8]) -> Result<Cid, FileNodeStoreError> {
648 let bytes: [u8; 32] = key.try_into().map_err(|_| FileNodeStoreError::InvalidKey {
649 message: format!("node key has length {}, expected 32", key.len()),
650 })?;
651 Ok(Cid(bytes))
652}
653
654fn validate_node_bytes(
655 expected: &Cid,
656 path: &Path,
657 bytes: &[u8],
658) -> Result<(), FileNodeStoreError> {
659 let actual = Cid::from_bytes(bytes);
660 if &actual == expected {
661 Ok(())
662 } else {
663 Err(FileNodeStoreError::CidMismatch {
664 path: path.to_path_buf(),
665 expected: expected.clone(),
666 actual,
667 })
668 }
669}
670
671fn write_file_atomically(path: &Path, bytes: &[u8]) -> Result<(), FileNodeStoreError> {
672 let Some(parent) = path.parent() else {
673 return Err(FileNodeStoreError::InvalidPath {
674 path: path.to_path_buf(),
675 message: "path has no parent directory".to_string(),
676 });
677 };
678 fs::create_dir_all(parent).map_err(|source| FileNodeStoreError::Io {
679 path: parent.to_path_buf(),
680 source,
681 })?;
682
683 let mut temp_path = temp_path_for(path);
684 let mut file = match OpenOptions::new()
685 .write(true)
686 .create_new(true)
687 .open(&temp_path)
688 {
689 Ok(file) => file,
690 Err(source) if source.kind() == std::io::ErrorKind::AlreadyExists => {
691 temp_path = temp_path_for(path);
692 OpenOptions::new()
693 .write(true)
694 .create_new(true)
695 .open(&temp_path)
696 .map_err(|source| FileNodeStoreError::Io {
697 path: temp_path.clone(),
698 source,
699 })?
700 }
701 Err(source) => {
702 return Err(FileNodeStoreError::Io {
703 path: temp_path,
704 source,
705 });
706 }
707 };
708
709 if let Err(source) = file.write_all(bytes) {
710 let _ = fs::remove_file(&temp_path);
711 return Err(FileNodeStoreError::Io {
712 path: temp_path,
713 source,
714 });
715 }
716 if let Err(source) = file.sync_all() {
717 let _ = fs::remove_file(&temp_path);
718 return Err(FileNodeStoreError::Io {
719 path: temp_path,
720 source,
721 });
722 }
723 drop(file);
724
725 if let Err(source) = fs::rename(&temp_path, path) {
726 let _ = fs::remove_file(&temp_path);
727 return Err(FileNodeStoreError::Io {
728 path: path.to_path_buf(),
729 source,
730 });
731 }
732 Ok(())
733}
734
735fn temp_path_for(path: &Path) -> PathBuf {
736 let file_name = path
737 .file_name()
738 .and_then(|name| name.to_str())
739 .unwrap_or("object");
740 let nanos = SystemTime::now()
741 .duration_since(UNIX_EPOCH)
742 .map(|duration| duration.as_nanos())
743 .unwrap_or_default();
744 path.with_file_name(format!(".{file_name}.{}.{}.tmp", std::process::id(), nanos))
745}
746
747fn read_visible_dir_entries(path: &Path) -> Result<Vec<DirEntry>, FileNodeStoreError> {
748 let mut entries = Vec::new();
749 let dir = fs::read_dir(path).map_err(|source| FileNodeStoreError::Io {
750 path: path.to_path_buf(),
751 source,
752 })?;
753
754 for entry in dir {
755 let entry = entry.map_err(|source| FileNodeStoreError::Io {
756 path: path.to_path_buf(),
757 source,
758 })?;
759 let name = entry_name(&entry)?;
760 if name.starts_with('.') {
761 continue;
762 }
763 entries.push(entry);
764 }
765
766 entries.sort_by_key(|entry| entry.file_name());
767 Ok(entries)
768}
769
770fn ensure_shard_dir(entry: &DirEntry, len: usize) -> Result<(), FileNodeStoreError> {
771 let path = entry.path();
772 let file_type = entry.file_type().map_err(|source| FileNodeStoreError::Io {
773 path: path.clone(),
774 source,
775 })?;
776 if !file_type.is_dir() {
777 return Err(FileNodeStoreError::InvalidPath {
778 path,
779 message: "node shard entry is not a directory".to_string(),
780 });
781 }
782
783 let name = entry_name(entry)?;
784 if name.len() != len || !name.as_bytes().iter().all(|byte| is_lower_hex_byte(*byte)) {
785 return Err(FileNodeStoreError::InvalidPath {
786 path,
787 message: "node shard directory is not lowercase hex".to_string(),
788 });
789 }
790 Ok(())
791}
792
793fn entry_name(entry: &DirEntry) -> Result<String, FileNodeStoreError> {
794 entry
795 .file_name()
796 .into_string()
797 .map_err(|name| FileNodeStoreError::InvalidPath {
798 path: entry.path(),
799 message: format!("path is not valid UTF-8: {name:?}"),
800 })
801}
802
803fn remove_empty_dirs(path: &Path, namespace: &Path) {
804 let Some(second) = path.parent() else {
805 return;
806 };
807 let _ = fs::remove_dir(second);
808 let Some(first) = second.parent() else {
809 return;
810 };
811 if first != namespace {
812 let _ = fs::remove_dir(first);
813 }
814}
815
816fn cid_hex(cid: &Cid) -> String {
817 bytes_hex(cid.as_bytes())
818}
819
820fn parse_cid_hex(input: &str) -> Option<Cid> {
821 if input.len() != 64 || !input.as_bytes().iter().all(|byte| is_lower_hex_byte(*byte)) {
822 return None;
823 }
824 let bytes = decode_hex(input)?;
825 let bytes: [u8; 32] = bytes.try_into().ok()?;
826 Some(Cid(bytes))
827}
828
829fn hex_label(bytes: &[u8]) -> String {
830 if bytes.is_empty() {
831 "_".to_string()
832 } else {
833 bytes_hex(bytes)
834 }
835}
836
837fn decode_hex_label(input: &str) -> Option<Vec<u8>> {
838 if input == "_" {
839 return Some(Vec::new());
840 }
841 if input.is_empty() {
842 return None;
843 }
844 decode_hex(input)
845}
846
847fn bytes_hex(bytes: &[u8]) -> String {
848 const HEX: &[u8; 16] = b"0123456789abcdef";
849 let mut out = String::with_capacity(bytes.len() * 2);
850 for byte in bytes {
851 out.push(HEX[(byte >> 4) as usize] as char);
852 out.push(HEX[(byte & 0x0f) as usize] as char);
853 }
854 out
855}
856
857fn decode_hex(input: &str) -> Option<Vec<u8>> {
858 if input.len() % 2 != 0 || !input.as_bytes().iter().all(|byte| is_lower_hex_byte(*byte)) {
859 return None;
860 }
861 input
862 .as_bytes()
863 .chunks_exact(2)
864 .map(|pair| Some((hex_value(pair[0])? << 4) | hex_value(pair[1])?))
865 .collect()
866}
867
868fn is_lower_hex_byte(byte: u8) -> bool {
869 byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte)
870}
871
872fn hex_value(byte: u8) -> Option<u8> {
873 match byte {
874 b'0'..=b'9' => Some(byte - b'0'),
875 b'a'..=b'f' => Some(byte - b'a' + 10),
876 _ => None,
877 }
878}