Skip to main content

prolly/prolly/store/
file.rs

1//! Filesystem-backed content-addressed node store.
2//!
3//! This backend is intentionally small and object-store-shaped: immutable node
4//! bytes live under a sharded CID namespace, while optional hints and named root
5//! manifests live under separate namespaces. It is useful as a local durable
6//! store and as a reference layout for S3/R2/GCS-style adapters.
7
8use 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/// Durable filesystem node store using content-addressed object layout.
27///
28/// Nodes are stored under `nodes/sha256/aa/bb/<cid-hex>` below the configured
29/// root. Hints are stored under `hints/<namespace-hex>/<key-hex>`, and named
30/// roots are stored under `roots/<name-hex>`. Node reads and writes verify that
31/// bytes hash to the CID key.
32#[derive(Clone, Debug)]
33pub struct FileNodeStore {
34    root: PathBuf,
35    manifest_lock: Arc<Mutex<()>>,
36}
37
38impl FileNodeStore {
39    /// Open or create a filesystem node store rooted at `root`.
40    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    /// Return the root directory for this store.
50    pub fn root(&self) -> &Path {
51        &self.root
52    }
53
54    /// Return the namespace directory containing immutable node objects.
55    pub fn node_namespace_dir(&self) -> PathBuf {
56        self.root.join("nodes").join("sha256")
57    }
58
59    /// Return the namespace directory containing optional performance hints.
60    pub fn hint_namespace_dir(&self) -> PathBuf {
61        self.root.join("hints")
62    }
63
64    /// Return the namespace directory containing named root manifests.
65    pub fn root_namespace_dir(&self) -> PathBuf {
66        self.root.join("roots")
67    }
68
69    /// Return the filesystem path for a content-addressed node.
70    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/// Error type for [`FileNodeStore`].
560#[derive(Debug)]
561pub enum FileNodeStoreError {
562    /// Filesystem I/O failed for the given path.
563    Io {
564        /// Path involved in the failed operation.
565        path: PathBuf,
566        /// Underlying I/O error.
567        source: std::io::Error,
568    },
569    /// A store key was not a 32-byte CID.
570    InvalidKey {
571        /// Validation failure description.
572        message: String,
573    },
574    /// A node object's bytes do not match its CID key.
575    CidMismatch {
576        /// Path involved in the validation failure.
577        path: PathBuf,
578        /// CID requested by the caller or encoded in the path.
579        expected: Cid,
580        /// CID computed from the stored bytes.
581        actual: Cid,
582    },
583    /// The store namespace contains an invalid path or filename.
584    InvalidPath {
585        /// Invalid path.
586        path: PathBuf,
587        /// Validation failure description.
588        message: String,
589    },
590    /// A named root manifest could not be encoded or decoded.
591    Manifest {
592        /// Manifest path involved in the failure.
593        path: PathBuf,
594        /// Validation failure description.
595        message: String,
596    },
597    /// A local manifest lock was poisoned.
598    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}