Skip to main content

nectar_primitives/file/
mode.rs

1//! Chunk mode traits for plain and encrypted file operations.
2
3use std::fmt::Debug;
4
5use bytes::Bytes;
6
7use crate::bmt::SPAN_SIZE;
8use crate::chunk::encryption::{EncryptedChunkRef, EncryptionKey, decrypt_chunk_data};
9use crate::chunk::{BmtChunk, Chunk, ChunkAddress, ContentChunk};
10use crate::store::{SyncChunkGet, SyncChunkPut};
11
12use super::constants::{ENCRYPTED_REF_SIZE, REF_SIZE, compute_spans_inline, subspan_for_spans};
13use super::error::{FileError, Result};
14
15/// Convert a `PrimitivesError` from chunk creation into a `FileError`.
16fn chunk_creation_error(e: crate::error::PrimitivesError) -> FileError {
17    match e {
18        crate::error::PrimitivesError::Chunk(c) => FileError::Chunk(c),
19        other => FileError::Store(Box::new(other)),
20    }
21}
22
23/// Create a `ContentChunk` from raw bytes.
24#[inline]
25fn create_chunk<const BS: usize>(data: Bytes) -> Result<ContentChunk<BS>> {
26    ContentChunk::<BS>::try_from(data).map_err(chunk_creation_error)
27}
28
29/// Store a chunk and return its address (derived from the chunk).
30fn store_chunk<const BS: usize, S: SyncChunkPut<BS>>(
31    chunk: ContentChunk<BS>,
32    store: &S,
33) -> Result<ChunkAddress> {
34    let address = *chunk.address();
35    store.put(chunk.into()).map_err(FileError::store)?;
36    Ok(address)
37}
38
39/// Joiner-side chunk mode operations.
40pub trait JoinMode: Sized + 'static {
41    /// Size of a single reference in bytes (32 plain, 64 encrypted).
42    const REF_SIZE: usize;
43
44    /// Root reference type: `ChunkAddress` (plain) or `EncryptedChunkRef` (encrypted).
45    type RootRef: Clone + Debug + Send + Sync;
46
47    /// Per-chunk context carried through tree traversal: `()` (plain) or `EncryptionKey`.
48    type JoinerContext: Clone + Debug + Send + Sync;
49
50    /// Number of child references per intermediate chunk.
51    #[inline]
52    fn refs_per_chunk(body_size: usize) -> usize {
53        body_size / Self::REF_SIZE
54    }
55
56    /// Tree depth for the given file length.
57    #[inline]
58    fn levels(length: u64, chunk_size: usize) -> usize {
59        super::constants::tree_depth(length, chunk_size, Self::REF_SIZE)
60    }
61
62    /// Subspan size for a given parent span.
63    #[inline]
64    fn subspan_size<const BS: usize>(span: u64) -> u64 {
65        let spans = compute_spans_inline(BS / Self::REF_SIZE);
66        subspan_for_spans::<BS>(span, &spans)
67    }
68
69    /// Compute the span covered by a child at `child_index` within a parent of `parent_span`.
70    #[inline]
71    fn child_span<const BS: usize>(parent_span: u64, subspan: u64, child_index: usize) -> u64 {
72        let branches = Self::refs_per_chunk(BS);
73        if child_index == branches - 1 {
74            let preceding = child_index as u64 * subspan;
75            parent_span.saturating_sub(preceding)
76        } else {
77            subspan.min(parent_span.saturating_sub(child_index as u64 * subspan))
78        }
79    }
80
81    /// Extract the chunk address from a root reference (for fetching).
82    fn root_address(input: &Self::RootRef) -> ChunkAddress;
83
84    /// Initialize joiner from a root ref and pre-fetched root chunk.
85    fn init_from_chunk<const BS: usize>(
86        input: Self::RootRef,
87        chunk: ContentChunk<BS>,
88    ) -> Result<(ChunkAddress, u64, Self::JoinerContext)>;
89
90    /// Decode a fetched chunk into body bytes (decrypting if needed).
91    fn decode_body<const BS: usize>(
92        chunk: ContentChunk<BS>,
93        context: &Self::JoinerContext,
94        span: u64,
95    ) -> Result<Bytes>;
96
97    /// Parse a child reference from body bytes at offset. Returns (address, child_context).
98    fn parse_child_ref(
99        body: &[u8],
100        ref_start: usize,
101    ) -> Result<(ChunkAddress, Self::JoinerContext)>;
102}
103
104/// Initialize joiner: fetch root chunk, extract span and context.
105pub(crate) fn joiner_init<M: JoinMode, G: SyncChunkGet<BS>, const BS: usize>(
106    getter: &G,
107    input: M::RootRef,
108) -> Result<(ChunkAddress, u64, M::JoinerContext)> {
109    let addr = M::root_address(&input);
110    let any = getter.get(&addr).map_err(FileError::getter)?;
111    let chunk = any.into_content().ok_or(FileError::InvalidChunkType {
112        type_name: "non-content",
113    })?;
114    M::init_from_chunk::<BS>(input, chunk)
115}
116
117/// Async variant of [`joiner_init`]: fetch root chunk, extract span and context.
118pub(crate) async fn joiner_init_async<
119    M: JoinMode + Send + Sync,
120    G: crate::store::ChunkGet<BS>,
121    const BS: usize,
122>(
123    getter: &G,
124    input: M::RootRef,
125) -> Result<(ChunkAddress, u64, M::JoinerContext)> {
126    let addr = M::root_address(&input);
127    let any = getter.get(&addr).await.map_err(FileError::getter)?;
128    let chunk = any.into_content().ok_or(FileError::InvalidChunkType {
129        type_name: "non-content",
130    })?;
131    M::init_from_chunk::<BS>(input, chunk)
132}
133
134/// Read chunk body at address with context. Returns body bytes (after decryption if needed).
135#[inline]
136pub(crate) fn read_chunk_body<M: JoinMode, G: SyncChunkGet<BS>, const BS: usize>(
137    getter: &G,
138    address: &ChunkAddress,
139    context: &M::JoinerContext,
140    span: u64,
141) -> Result<Bytes> {
142    let any = getter.get(address).map_err(FileError::getter)?;
143    let chunk = any.into_content().ok_or(FileError::InvalidChunkType {
144        type_name: "non-content",
145    })?;
146    M::decode_body::<BS>(chunk, context, span)
147}
148
149/// Async variant of [`read_chunk_body`].
150pub(crate) async fn read_chunk_body_async<
151    M: JoinMode + Send + Sync,
152    G: crate::store::ChunkGet<BS>,
153    const BS: usize,
154>(
155    getter: &G,
156    address: &ChunkAddress,
157    context: &M::JoinerContext,
158    span: u64,
159) -> Result<Bytes> {
160    let address = *address;
161    let context = context.clone();
162    let any = getter.get(&address).await.map_err(FileError::getter)?;
163    let chunk = any.into_content().ok_or(FileError::InvalidChunkType {
164        type_name: "non-content",
165    })?;
166    M::decode_body::<BS>(chunk, &context, span)
167}
168
169/// Splitter-side chunk mode operations (extends JoinMode).
170pub trait SplitMode: JoinMode {
171    /// Fixed-size byte array for a reference: `[u8; 32]` or `[u8; 64]`.
172    type RefBytes: AsRef<[u8]> + AsMut<[u8]> + Clone + Debug + Send + Sync;
173
174    /// Prepare chunk data (span + body) for storage, returning chunk and reference bytes.
175    /// Takes ownership of the payload to avoid an extra allocation.
176    fn prepare_chunk<const BS: usize>(data: Vec<u8>) -> Result<(ContentChunk<BS>, Self::RefBytes)>;
177
178    /// Process chunk data (span + body), store it, return reference bytes.
179    /// Takes ownership of the payload to avoid an extra allocation.
180    #[inline]
181    fn process_chunk<const BS: usize, S: SyncChunkPut<BS>>(
182        data: Vec<u8>,
183        store: &S,
184    ) -> Result<Self::RefBytes> {
185        let (chunk, ref_bytes) = Self::prepare_chunk::<BS>(data)?;
186        store.put(chunk.into()).map_err(FileError::store)?;
187        Ok(ref_bytes)
188    }
189
190    /// Process empty file, store chunk, return root ref.
191    fn process_empty<const BS: usize, S: SyncChunkPut<BS>>(store: &S) -> Result<Self::RootRef>;
192
193    /// Extract root reference from top of buffer.
194    fn extract_root(buffer: &[u8]) -> Result<Self::RootRef>;
195}
196
197/// Plain (unencrypted) chunk mode.
198#[derive(Debug)]
199pub struct PlainMode;
200
201impl JoinMode for PlainMode {
202    const REF_SIZE: usize = REF_SIZE;
203    type RootRef = ChunkAddress;
204    type JoinerContext = ();
205
206    #[inline]
207    fn root_address(input: &ChunkAddress) -> ChunkAddress {
208        *input
209    }
210
211    fn init_from_chunk<const BS: usize>(
212        root: ChunkAddress,
213        chunk: ContentChunk<BS>,
214    ) -> Result<(ChunkAddress, u64, ())> {
215        let span = chunk.span();
216        Ok((root, span, ()))
217    }
218
219    #[inline]
220    fn decode_body<const BS: usize>(
221        chunk: ContentChunk<BS>,
222        _context: &(),
223        _span: u64,
224    ) -> Result<Bytes> {
225        Ok(chunk.data().clone())
226    }
227
228    #[inline]
229    fn parse_child_ref(body: &[u8], ref_start: usize) -> Result<(ChunkAddress, ())> {
230        let ref_end = ref_start + REF_SIZE;
231        let child_addr_bytes: [u8; 32] = body[ref_start..ref_end]
232            .try_into()
233            .map_err(|_| FileError::InvalidReference { level: 0 })?;
234        Ok((ChunkAddress::from(child_addr_bytes), ()))
235    }
236}
237
238impl SplitMode for PlainMode {
239    type RefBytes = [u8; REF_SIZE];
240
241    #[inline]
242    fn prepare_chunk<const BS: usize>(data: Vec<u8>) -> Result<(ContentChunk<BS>, [u8; REF_SIZE])> {
243        let chunk = create_chunk::<BS>(Bytes::from(data))?;
244        let ref_bytes = (*chunk.address()).into();
245        Ok((chunk, ref_bytes))
246    }
247
248    fn process_empty<const BS: usize, S: SyncChunkPut<BS>>(store: &S) -> Result<ChunkAddress> {
249        // Use `new` (not `try_from`) because Bytes::new() is raw content,
250        // not pre-formatted span+body data.
251        let chunk = ContentChunk::<BS>::new(Bytes::new()).map_err(chunk_creation_error)?;
252        store_chunk::<BS, S>(chunk, store)
253    }
254
255    fn extract_root(buffer: &[u8]) -> Result<ChunkAddress> {
256        let root_bytes: [u8; 32] = buffer
257            .get(..REF_SIZE)
258            .and_then(|s| s.try_into().ok())
259            .ok_or(FileError::InvalidReference { level: 0 })?;
260        Ok(ChunkAddress::from(root_bytes))
261    }
262}
263
264/// Encrypted chunk mode.
265///
266/// `JoinMode` (decryption) is always available. `SplitMode` (encryption)
267/// requires the `encryption` feature because key generation depends on `rand`.
268#[derive(Debug)]
269pub struct EncryptedMode;
270
271impl EncryptedMode {
272    /// Calculate data length for decryption of a chunk with given span.
273    fn decrypt_data_length<const BS: usize>(span: u64) -> usize {
274        if span <= BS as u64 {
275            span as usize
276        } else {
277            let sub = Self::subspan_size::<BS>(span);
278            let num_children = span.div_ceil(sub) as usize;
279            let raw = num_children * ENCRYPTED_REF_SIZE;
280            raw.min(BS)
281        }
282    }
283}
284
285impl JoinMode for EncryptedMode {
286    const REF_SIZE: usize = ENCRYPTED_REF_SIZE;
287    type RootRef = EncryptedChunkRef;
288    type JoinerContext = EncryptionKey;
289
290    fn root_address(input: &EncryptedChunkRef) -> ChunkAddress {
291        *input.address()
292    }
293
294    fn init_from_chunk<const BS: usize>(
295        root_ref: EncryptedChunkRef,
296        chunk: ContentChunk<BS>,
297    ) -> Result<(ChunkAddress, u64, EncryptionKey)> {
298        let encrypted_data: Bytes = chunk.into();
299
300        let span_buf = decrypt_span::<BS>(&encrypted_data, root_ref.key())?;
301        let span = u64::from_le_bytes(span_buf);
302
303        let (address, key) = root_ref.into_parts();
304        Ok((address, span, key))
305    }
306
307    fn decode_body<const BS: usize>(
308        chunk: ContentChunk<BS>,
309        key: &EncryptionKey,
310        span: u64,
311    ) -> Result<Bytes> {
312        let encrypted_data: Bytes = chunk.into();
313
314        let data_length = Self::decrypt_data_length::<BS>(span);
315        let decrypted = decrypt_chunk_data::<BS>(&encrypted_data, key, data_length)?;
316        Ok(Bytes::from(decrypted).slice(SPAN_SIZE..))
317    }
318
319    fn parse_child_ref(body: &[u8], ref_start: usize) -> Result<(ChunkAddress, EncryptionKey)> {
320        let ref_end = ref_start + ENCRYPTED_REF_SIZE;
321        let child_addr_bytes: [u8; 32] = body[ref_start..ref_start + 32]
322            .try_into()
323            .map_err(|_| FileError::InvalidReference { level: 0 })?;
324        let child_key = EncryptionKey::try_from(&body[ref_start + 32..ref_end])?;
325        Ok((ChunkAddress::from(child_addr_bytes), child_key))
326    }
327}
328
329#[cfg(feature = "encryption")]
330impl SplitMode for EncryptedMode {
331    type RefBytes = [u8; ENCRYPTED_REF_SIZE];
332
333    fn prepare_chunk<const BS: usize>(
334        data: Vec<u8>,
335    ) -> Result<(ContentChunk<BS>, [u8; ENCRYPTED_REF_SIZE])> {
336        use crate::chunk::encryption::encrypt_chunk;
337
338        let key = EncryptionKey::generate();
339        let ciphertext = encrypt_chunk::<BS>(&data, &key)?;
340        let chunk = create_chunk::<BS>(Bytes::from(ciphertext))?;
341
342        let mut ref_bytes = [0u8; ENCRYPTED_REF_SIZE];
343        ref_bytes[..32].copy_from_slice(chunk.address().as_bytes());
344        ref_bytes[32..].copy_from_slice(key.as_bytes());
345        Ok((chunk, ref_bytes))
346    }
347
348    fn process_empty<const BS: usize, S: SyncChunkPut<BS>>(store: &S) -> Result<EncryptedChunkRef> {
349        use crate::chunk::encryption::encrypt_chunk;
350
351        let key = EncryptionKey::generate();
352        let chunk_bytes = 0u64.to_le_bytes().to_vec();
353        let ciphertext = encrypt_chunk::<BS>(&chunk_bytes, &key)?;
354        let chunk = create_chunk::<BS>(Bytes::from(ciphertext))?;
355        let address = store_chunk::<BS, S>(chunk, store)?;
356        Ok(EncryptedChunkRef::new(address, key))
357    }
358
359    fn extract_root(buffer: &[u8]) -> Result<EncryptedChunkRef> {
360        let root_ref_bytes = buffer
361            .get(..ENCRYPTED_REF_SIZE)
362            .ok_or(FileError::InvalidReference { level: 0 })?;
363        EncryptedChunkRef::try_from(root_ref_bytes)
364            .map_err(|_| FileError::InvalidReference { level: 0 })
365    }
366}
367
368/// Decrypt just the span (first 8 bytes) from encrypted chunk data.
369fn decrypt_span<const BODY_SIZE: usize>(
370    encrypted_data: &[u8],
371    key: &EncryptionKey,
372) -> Result<[u8; SPAN_SIZE]> {
373    use crate::chunk::encryption::transcrypt;
374
375    let expected_len = SPAN_SIZE + BODY_SIZE;
376    if encrypted_data.len() != expected_len {
377        return Err(FileError::Encryption(
378            crate::chunk::encryption::EncryptionError::DataTooShort {
379                len: encrypted_data.len(),
380                min: expected_len,
381            },
382        ));
383    }
384
385    let span_ctr = (BODY_SIZE / EncryptionKey::SIZE) as u32;
386    let mut span_buf = [0u8; SPAN_SIZE];
387    transcrypt(key, span_ctr, &encrypted_data[..SPAN_SIZE], &mut span_buf)
388        .map_err(FileError::Encryption)?;
389    Ok(span_buf)
390}