Skip to main content

nectar_primitives/file/
mod.rs

1//! File splitting and joining for arbitrary-size data.
2//!
3//! Async is the primary API. `SyncJoiner` implements `Read + Seek`, `SyncSplitter`
4//! implements `Write`.
5//!
6//! # Store-centric API (extension traits)
7//!
8//! ```
9//! use nectar_primitives::file::{SyncChunkGetExt, SyncChunkPutExt};
10//! use nectar_primitives::store::MemoryStore;
11//! use nectar_primitives::DEFAULT_BODY_SIZE;
12//!
13//! let store = MemoryStore::<DEFAULT_BODY_SIZE>::new();
14//! let addr = store.write_file(b"hello swarm").unwrap();
15//! let data = store.read_file(addr).unwrap();
16//! assert_eq!(data, b"hello swarm");
17//! ```
18//!
19//! # Free function API
20//!
21//! ```
22//! use nectar_primitives::file::{sync_split, sync_join};
23//! use nectar_primitives::DEFAULT_BODY_SIZE;
24//!
25//! let data = b"Hello, Swarm!";
26//! let (root, store) = sync_split::<DEFAULT_BODY_SIZE>(data).unwrap();
27//! let recovered = sync_join(&store, root).unwrap();
28//! assert_eq!(recovered, data);
29//! ```
30//!
31//! # Encrypted split and join
32//!
33//! ```
34//! # #[cfg(feature = "encryption")] {
35//! use nectar_primitives::file::{sync_split_encrypted, sync_join};
36//! use nectar_primitives::DEFAULT_BODY_SIZE;
37//!
38//! let data = b"secret data";
39//! let (root_ref, store) = sync_split_encrypted::<DEFAULT_BODY_SIZE>(data).unwrap();
40//! let recovered = sync_join(&store, root_ref).unwrap();
41//! assert_eq!(recovered, data);
42//! # }
43//! ```
44
45mod constants;
46pub mod entry_ref;
47pub mod error;
48mod frontier;
49mod helpers;
50#[cfg(test)]
51#[macro_use]
52mod joiner_tests;
53#[cfg(test)]
54#[macro_use]
55mod splitter_tests;
56mod joiner;
57pub mod mode;
58mod sync_joiner;
59mod sync_read_at;
60mod sync_splitter;
61mod sync_splitter_parallel;
62mod tree;
63
64use crate::chunk::ChunkAddress;
65#[cfg(feature = "encryption")]
66use crate::chunk::encryption::EncryptedChunkRef;
67use crate::store::{SyncChunkGet, SyncChunkPut};
68
69// Async (primary) re-exports
70#[cfg(feature = "encryption")]
71pub use joiner::EncryptedJoiner;
72#[cfg(feature = "tokio")]
73pub use joiner::JoinerReader;
74pub use joiner::{GenericJoiner, Joiner};
75// Sync (secondary) re-exports
76#[cfg(feature = "encryption")]
77pub use sync_joiner::EncryptedSyncJoiner;
78pub use sync_joiner::{GenericSyncJoiner, SyncJoiner};
79pub use sync_read_at::SyncReadAt;
80#[cfg(feature = "encryption")]
81pub use sync_splitter::EncryptedSyncSplitter;
82pub use sync_splitter::SyncSplitter;
83#[cfg(feature = "encryption")]
84pub use sync_splitter_parallel::EncryptedSyncParallelSplitter;
85pub use sync_splitter_parallel::SyncParallelSplitter;
86
87pub use entry_ref::EntryRef;
88pub use error::FileError;
89pub use tree::{ChunkRange, TreeParams};
90
91mod join_ref_sealed {
92    pub trait Sealed {}
93    impl Sealed for crate::ChunkAddress {}
94    #[cfg(feature = "encryption")]
95    impl Sealed for crate::EncryptedChunkRef {}
96}
97
98/// Maps a reference type to its join mode.
99/// Sealed — implemented for `ChunkAddress` and `EncryptedChunkRef`.
100pub trait JoinRef: join_ref_sealed::Sealed + Clone + Send + Sync + 'static {
101    /// The join mode associated with this reference type.
102    type Mode: mode::JoinMode + Send + Sync;
103
104    /// Convert into the root reference expected by the joiner.
105    fn into_root_ref(self) -> <Self::Mode as mode::JoinMode>::RootRef;
106}
107
108impl JoinRef for ChunkAddress {
109    type Mode = mode::PlainMode;
110
111    fn into_root_ref(self) -> Self {
112        self
113    }
114}
115
116#[cfg(feature = "encryption")]
117impl JoinRef for EncryptedChunkRef {
118    type Mode = mode::EncryptedMode;
119
120    fn into_root_ref(self) -> Self {
121        self
122    }
123}
124
125/// Resolve a `SeekFrom` position to an absolute byte offset.
126pub(crate) fn resolve_seek_position(
127    pos: std::io::SeekFrom,
128    current: u64,
129    span: u64,
130) -> std::io::Result<u64> {
131    use std::io::{Error, ErrorKind::InvalidInput, SeekFrom};
132    let to_i64 = |v: u64, msg: &str| i64::try_from(v).map_err(|_| Error::new(InvalidInput, msg));
133    let new_pos = match pos {
134        SeekFrom::Start(off) => to_i64(off, "seek offset exceeds i64::MAX")?,
135        SeekFrom::End(off) => to_i64(span, "file span exceeds i64::MAX")? + off,
136        SeekFrom::Current(off) => to_i64(current, "current position exceeds i64::MAX")? + off,
137    };
138    if new_pos < 0 {
139        return Err(Error::new(InvalidInput, "seek to negative position"));
140    }
141    Ok(new_pos as u64)
142}
143
144// ---- Primary async API ----
145
146/// Join chunks asynchronously. Dispatches plain/encrypted via [`JoinRef`].
147pub async fn join<R, G, const BODY_SIZE: usize>(getter: G, root: R) -> error::Result<Vec<u8>>
148where
149    R: JoinRef,
150    G: crate::store::ChunkGet<BODY_SIZE>,
151{
152    GenericJoiner::<G, R::Mode, BODY_SIZE>::new(getter, root.into_root_ref())
153        .await?
154        .read_all()
155        .await
156}
157
158// ---- Secondary sync API ----
159
160/// Split data into chunks synchronously, returning root address and chunk store.
161///
162/// Uses `SyncParallelSplitter` for best performance on in-memory data.
163pub fn sync_split<const BODY_SIZE: usize>(
164    data: &[u8],
165) -> error::Result<(ChunkAddress, crate::store::MemoryStore<BODY_SIZE>)> {
166    let store = crate::store::MemoryStore::<BODY_SIZE>::new();
167    let splitter = SyncParallelSplitter::new(store);
168    let root = splitter.split(&data)?;
169    Ok((root, splitter.into_store()))
170}
171
172/// Split data into encrypted chunks synchronously.
173#[cfg(feature = "encryption")]
174pub fn sync_split_encrypted<const BODY_SIZE: usize>(
175    data: &[u8],
176) -> error::Result<(EncryptedChunkRef, crate::store::MemoryStore<BODY_SIZE>)> {
177    let store = crate::store::MemoryStore::<BODY_SIZE>::new();
178    let splitter = EncryptedSyncParallelSplitter::new(store);
179    let root_ref = splitter.split(&data)?;
180    Ok((root_ref, splitter.into_store()))
181}
182
183/// Join chunks synchronously. Dispatches plain/encrypted via [`JoinRef`].
184pub fn sync_join<R, G, const BODY_SIZE: usize>(getter: G, root: R) -> error::Result<Vec<u8>>
185where
186    R: JoinRef,
187    G: SyncChunkGet<BODY_SIZE> + Clone + Send + Sync,
188{
189    GenericSyncJoiner::<G, R::Mode, BODY_SIZE>::new(getter, root.into_root_ref())?.read_all()
190}
191
192/// Calculate tree depth for a given file size (plain mode).
193#[cfg(test)]
194pub(crate) const fn levels(length: u64, chunk_size: usize) -> usize {
195    constants::tree_depth(length, chunk_size, constants::REF_SIZE)
196}
197
198// ---- Extension traits ----
199
200/// Extension methods for async chunk getters.
201///
202/// Uses [`JoinRef`] for unified plain/encrypted dispatch.
203pub trait ChunkGetExt<const BODY_SIZE: usize>: crate::store::ChunkGet<BODY_SIZE> {
204    /// Open a file for async reading.
205    fn joiner<R: JoinRef>(
206        self,
207        root: R,
208    ) -> impl std::future::Future<Output = error::Result<GenericJoiner<Self, R::Mode, BODY_SIZE>>> + Send
209    where
210        Self: Sized + Clone + Send + Sync + 'static,
211    {
212        GenericJoiner::new(self, root.into_root_ref())
213    }
214
215    /// Read entire file into memory asynchronously.
216    fn read_file<R: JoinRef>(
217        self,
218        root: R,
219    ) -> impl std::future::Future<Output = error::Result<Vec<u8>>> + Send
220    where
221        Self: Sized + Clone + Send + Sync + 'static,
222    {
223        join(self, root)
224    }
225}
226
227impl<T, const BODY_SIZE: usize> ChunkGetExt<BODY_SIZE> for T where
228    T: crate::store::ChunkGet<BODY_SIZE>
229{
230}
231
232/// Extension methods for sync chunk getters.
233///
234/// Automatically implemented for all types that implement [`SyncChunkGet`].
235/// Uses [`JoinRef`] for unified plain/encrypted dispatch.
236///
237/// ```
238/// use nectar_primitives::file::{SyncChunkPutExt, SyncChunkGetExt};
239/// use nectar_primitives::store::MemoryStore;
240/// use nectar_primitives::DEFAULT_BODY_SIZE;
241///
242/// let store = MemoryStore::<DEFAULT_BODY_SIZE>::new();
243/// let addr = store.write_file(b"hello swarm").unwrap();
244/// let recovered = store.read_file(addr).unwrap();
245/// assert_eq!(recovered, b"hello swarm");
246/// ```
247pub trait SyncChunkGetExt<const BODY_SIZE: usize>: SyncChunkGet<BODY_SIZE> {
248    /// Open a file for reading. Returns a joiner implementing `Read + Seek`.
249    fn joiner<R: JoinRef>(
250        self,
251        root: R,
252    ) -> error::Result<GenericSyncJoiner<Self, R::Mode, BODY_SIZE>>
253    where
254        Self: Clone + Send + Sync + Sized,
255    {
256        GenericSyncJoiner::new(self, root.into_root_ref())
257    }
258
259    /// Read entire file into memory (like `fs::read`).
260    fn read_file<R: JoinRef>(self, root: R) -> error::Result<Vec<u8>>
261    where
262        Self: Clone + Send + Sync + Sized,
263    {
264        sync_join(self, root)
265    }
266}
267
268impl<T, const BODY_SIZE: usize> SyncChunkGetExt<BODY_SIZE> for T where T: SyncChunkGet<BODY_SIZE> {}
269
270/// Extension methods for sync chunk putters.
271///
272/// Automatically implemented for all types that implement [`SyncChunkPut`].
273///
274/// ```
275/// use nectar_primitives::file::{SyncChunkPutExt, SyncChunkGetExt};
276/// use nectar_primitives::store::MemoryStore;
277/// use nectar_primitives::DEFAULT_BODY_SIZE;
278/// use std::io::Write;
279///
280/// // Filesystem-style write/read
281/// let store = MemoryStore::<DEFAULT_BODY_SIZE>::new();
282/// let addr = store.write_file(b"hello").unwrap();
283///
284/// // Streaming write via std::io::Write
285/// let mut writer = store.writer(6);
286/// writer.write_all(b"world!").unwrap();
287/// let (root, _) = writer.finish().unwrap();
288/// ```
289pub trait SyncChunkPutExt<const BODY_SIZE: usize>: SyncChunkPut<BODY_SIZE> {
290    /// Create a writer for streaming data. Returns a `SyncSplitter` implementing `Write`.
291    /// Call `.finish()` on the returned writer to get the root address.
292    fn writer(&self, size: u64) -> SyncSplitter<&Self, BODY_SIZE>
293    where
294        Self: Sized,
295    {
296        SyncSplitter::new(self, size)
297    }
298
299    /// Create an encrypted writer. Returns an `EncryptedSyncSplitter` implementing `Write`.
300    #[cfg(feature = "encryption")]
301    fn encrypted_writer(&self, size: u64) -> EncryptedSyncSplitter<&Self, BODY_SIZE>
302    where
303        Self: Sized,
304    {
305        EncryptedSyncSplitter::new(self, size)
306    }
307
308    /// Write file data into the store (like `fs::write`).
309    fn write_file(&self, data: &[u8]) -> error::Result<ChunkAddress>
310    where
311        Self: Send + Sync + Sized,
312    {
313        SyncParallelSplitter::<&Self, BODY_SIZE>::new(self).split(&data)
314    }
315
316    /// Write encrypted file data into the store.
317    #[cfg(feature = "encryption")]
318    fn write_encrypted_file(&self, data: &[u8]) -> error::Result<EncryptedChunkRef>
319    where
320        Self: Send + Sync + Sized,
321    {
322        EncryptedSyncParallelSplitter::<&Self, BODY_SIZE>::new(self).split(&data)
323    }
324}
325
326impl<T, const BODY_SIZE: usize> SyncChunkPutExt<BODY_SIZE> for T where T: SyncChunkPut<BODY_SIZE> {}
327
328#[cfg(test)]
329mod tests;