Skip to main content

xet_data/file_reconstruction/reconstruction_terms/
file_term.rs

1use std::collections::HashMap;
2use std::collections::hash_map::Entry;
3use std::sync::Arc;
4
5use bytes::Bytes;
6use tokio::sync::OnceCell;
7#[cfg(target_family = "wasm")]
8use tokio_with_wasm::alias as tokio;
9use xet_client::cas_client::Client;
10use xet_client::cas_types::{ChunkRange, FileRange, HttpRange};
11use xet_client::chunk_cache::ChunkCache;
12use xet_core_structures::merklehash::MerkleHash;
13use xet_runtime::core::XetContext;
14use xet_runtime::utils::UniqueId;
15
16use super::super::FileReconstructionError;
17use super::super::data_writer::DataFuture;
18use super::super::error::Result;
19use super::retrieval_urls::TermBlockRetrievalURLs;
20use super::xorb_block::{XorbBlock, XorbBlockData, XorbReference};
21use crate::progress_tracking::ItemProgressUpdater;
22/// A single term in a file reconstruction, representing a contiguous byte range
23/// in the output file that maps to a chunk range within a xorb block.
24#[derive(Clone)]
25pub struct FileTerm {
26    // The byte range in the file of this term.
27    pub byte_range: FileRange,
28
29    // Absolute chunk range within the full xorb.  Doesn't account for only a partial xorb being downloaded.
30    pub xorb_chunk_range: ChunkRange,
31
32    // The index of the (chunk index, byte offset) pair in the xorb block that starts this file term.
33    pub xorb_block_start_index: usize,
34
35    // The byte offset into the first range of the xorb block should this term not start on a chunk boundary.
36    pub offset_into_first_range: u64,
37
38    // The xorb block that sourced this file term.
39    pub xorb_block: Arc<XorbBlock>,
40
41    // The retrieval URL information for this file term.
42    pub url_info: Arc<TermBlockRetrievalURLs>,
43}
44
45impl FileTerm {
46    pub fn extract_bytes(&self, xorb_block_data: &XorbBlockData) -> Bytes {
47        let (_, start_byte_offset) = xorb_block_data.chunk_offsets[self.xorb_block_start_index];
48        let start_byte_offset = start_byte_offset + self.offset_into_first_range as usize;
49        let expected_size = (self.byte_range.end - self.byte_range.start) as usize;
50        let end_byte_offset = start_byte_offset + expected_size;
51
52        xorb_block_data.data.slice(start_byte_offset..end_byte_offset)
53    }
54
55    /// Get a future that will retrieve and extract the data bytes for this file term.
56    ///
57    /// If the xorb data is already cached, returns a future that immediately resolves (no progress
58    /// report, since the block was already reported by the term that triggered the download).
59    /// Otherwise, spawns a task that calls retrieve_data, which uses single-flight to ensure
60    /// only one download per xorb block (other callers wait without acquiring CAS permits).
61    pub async fn get_data_task(
62        &self,
63        ctx: XetContext,
64        client: Arc<dyn Client>,
65        progress_updater: Option<Arc<ItemProgressUpdater>>,
66        chunk_cache: Option<Arc<dyn ChunkCache>>,
67    ) -> Result<DataFuture> {
68        // Fast path: data already cached, no need to spawn a task.
69        if let Some(xorb_block_data) = self.xorb_block.data.get() {
70            let bytes = self.extract_bytes(xorb_block_data);
71            return Ok(Box::pin(async move { Ok(bytes) }));
72        }
73
74        let file_term = self.clone();
75        let url_info = self.url_info.clone();
76        let xorb_block = self.xorb_block.clone();
77
78        let task = tokio::task::spawn(async move {
79            let xorb_block_data = xorb_block
80                .retrieve_data(ctx, client, url_info, progress_updater, chunk_cache)
81                .await?;
82            Ok(file_term.extract_bytes(&xorb_block_data))
83        });
84
85        Ok(Box::pin(async move { task.await? }))
86    }
87}
88
89/// Intermediate data for a single file term, collected during the first pass of
90/// `retrieve_file_term_block` before the final `FileTerm` structs are built.
91///
92/// We need this because `FileTerm` requires `Arc<XorbBlock>` and `Arc<TermBlockRetrievalURLs>`,
93/// which can't be constructed until all terms have been processed.
94struct FileTermEntry {
95    /// The byte range in the output file that this term covers.
96    byte_range: FileRange,
97    /// The chunk range within the xorb that sources this term's data.
98    xorb_chunk_range: ChunkRange,
99    /// Byte offset into the first chunk's data, non-zero only for the first term
100    /// when the query range starts mid-chunk.
101    offset_into_first_range: u64,
102    /// Index into the `xorb_blocks` / `xorb_block_retrieval_urls` vectors.
103    xorb_block_index: usize,
104    /// Flattened index into the xorb block's `chunk_offsets` for this term's start chunk.
105    xorb_block_start_index: usize,
106}
107
108/// Retrieve file terms from the client for a given file hash and byte range.
109/// Returns None if the requested byte range is past the end of the file.
110/// Returns the actual retrieved range and the number of bytes required for the
111/// download (with dedup and compression enabled)
112/// along with the Vec<FileTerm>.
113pub async fn retrieve_file_term_block(
114    ctx: &XetContext,
115    client: Arc<dyn Client>,
116    file_hash: MerkleHash,
117    query_file_byte_range: FileRange,
118) -> Result<Option<(FileRange, u64, Vec<FileTerm>)>> {
119    // get_reconstruction always returns V2 format (the client converts V1 internally).
120    let Some(raw_reconstruction) = client.get_reconstruction(&file_hash, Some(query_file_byte_range)).await? else {
121        // None means we've requested a byte range beyond the end of the file.
122        return Ok(None);
123    };
124
125    // Each acquisition gets a unique ID used for single-flight URL refresh dedup.
126    let acquisition_id = UniqueId::new();
127
128    // First pass: iterate through the reconstruction terms and build up intermediate
129    // FileTermEntry data, XorbBlock objects, and retrieval URL info.  We can't construct
130    // the final FileTerm structs yet because they need Arc<XorbBlock> and Arc<TermBlockRetrievalURLs>,
131    // which require all terms to be processed first.
132    let mut file_term_data = Vec::<FileTermEntry>::with_capacity(raw_reconstruction.terms.len());
133
134    // Parallel vectors indexed by xorb_block_index:
135    // - xorb_blocks: the block metadata (hash, chunk ranges, references)
136    // - xorb_block_retrieval_urls: the download URL and byte ranges for each block
137    let mut xorb_blocks: Vec<XorbBlock> = Vec::new();
138    let mut xorb_block_retrieval_urls = Vec::<(String, Vec<HttpRange>)>::new();
139
140    // Dedup map: (xorb_hash, first_range_chunk_start) -> xorb_block_index.
141    // Multiple terms may reference the same xorb block; this ensures we create
142    // each block only once and share it across terms.
143    let mut xorb_index_lookup = HashMap::<(MerkleHash, u32), usize>::new();
144
145    // Track the current byte offset in the output file as we process terms sequentially.
146    let mut cur_file_byte_offset = query_file_byte_range.start;
147
148    let enable_multirange = ctx.config.client.enable_multirange_fetching;
149
150    for (local_term_index, term) in raw_reconstruction.terms.iter().enumerate() {
151        let xorb_hash: MerkleHash = term.hash.into();
152
153        let Some(xorb_descriptor) = raw_reconstruction.xorbs.get(&term.hash) else {
154            return Err(FileReconstructionError::CorruptedReconstruction(format!(
155                "Xorb info not found for xorb hash {xorb_hash:?}"
156            )));
157        };
158
159        // Find the XorbBlock for this term's chunk range. The behavior depends on the
160        // enable_multirange_fetching config:
161        //
162        // - When true: one XorbBlock per XorbMultiRangeFetch entry, preserving all ranges in a single block
163        //   (multi-range HTTP request).
164        // - When false (default): one XorbBlock per individual XorbRangeDescriptor, so each range is fetched as a
165        //   separate single-range HTTP request in parallel.
166        let xorb_block_index = 'find_xorb_block: {
167            for fetch_entry in xorb_descriptor.iter() {
168                if enable_multirange {
169                    let term_contained = fetch_entry
170                        .ranges
171                        .iter()
172                        .any(|r| r.chunks.start <= term.range.start && term.range.end <= r.chunks.end);
173
174                    if !term_contained {
175                        continue;
176                    }
177
178                    let first_chunk_start = fetch_entry.ranges[0].chunks.start;
179
180                    let index = match xorb_index_lookup.entry((xorb_hash, first_chunk_start)) {
181                        Entry::Occupied(entry) => *entry.get(),
182                        Entry::Vacant(entry) => {
183                            let new_index = xorb_blocks.len();
184
185                            let chunk_ranges: Vec<ChunkRange> = fetch_entry.ranges.iter().map(|r| r.chunks).collect();
186                            let http_ranges: Vec<HttpRange> = fetch_entry.ranges.iter().map(|r| r.bytes).collect();
187
188                            xorb_blocks.push(XorbBlock {
189                                xorb_hash,
190                                chunk_ranges,
191                                xorb_block_index: new_index,
192                                references: vec![],
193                                uncompressed_size_if_known: None,
194                                data: OnceCell::new(),
195                            });
196
197                            xorb_block_retrieval_urls.push((fetch_entry.url.clone(), http_ranges));
198
199                            entry.insert(new_index);
200                            new_index
201                        },
202                    };
203
204                    break 'find_xorb_block index;
205                } else {
206                    for range in &fetch_entry.ranges {
207                        if range.chunks.start <= term.range.start && term.range.end <= range.chunks.end {
208                            let index = match xorb_index_lookup.entry((xorb_hash, range.chunks.start)) {
209                                Entry::Occupied(entry) => *entry.get(),
210                                Entry::Vacant(entry) => {
211                                    let new_index = xorb_blocks.len();
212
213                                    xorb_blocks.push(XorbBlock {
214                                        xorb_hash,
215                                        chunk_ranges: vec![range.chunks],
216                                        xorb_block_index: new_index,
217                                        references: vec![],
218                                        uncompressed_size_if_known: None,
219                                        data: OnceCell::new(),
220                                    });
221
222                                    xorb_block_retrieval_urls.push((fetch_entry.url.clone(), vec![range.bytes]));
223
224                                    entry.insert(new_index);
225                                    new_index
226                                },
227                            };
228
229                            break 'find_xorb_block index;
230                        }
231                    }
232                }
233            }
234            return Err(FileReconstructionError::CorruptedReconstruction(format!(
235                "No xorb fetch entry found for file term {local_term_index:?} in xorb info for xorb hash {xorb_hash:?}"
236            )));
237        };
238
239        // Only the first term can have a non-zero offset into its first chunk,
240        // which happens when the query byte range starts mid-chunk.
241        let offset_into_first_range = if local_term_index == 0 {
242            raw_reconstruction.offset_into_first_range
243        } else {
244            0
245        };
246
247        // The term's contribution to the output file is its full uncompressed size
248        // minus any offset into the first chunk.
249        let term_byte_size = term.unpacked_length as u64 - offset_into_first_range;
250
251        // Record this term as a reference on its xorb block (used later to determine
252        // whether the block's total uncompressed size can be inferred).
253        xorb_blocks[xorb_block_index].references.push(XorbReference {
254            term_chunks: term.range,
255            uncompressed_size: term.unpacked_length as usize,
256        });
257
258        // Compute the flattened index into the block's chunk_offsets for this term's
259        // starting chunk. This accounts for disjoint chunk ranges in multi-range blocks.
260        //
261        // The term_contained check above guarantees term.range.start falls within one of
262        // the block's chunk_ranges, so this loop always finds a match.
263        let xorb_block_start_index = {
264            let chunk_start = term.range.start;
265            let chunk_ranges = &xorb_blocks[xorb_block_index].chunk_ranges;
266            let mut idx = 0;
267            let mut found = false;
268            for range in chunk_ranges {
269                if chunk_start >= range.start && chunk_start < range.end {
270                    idx += (chunk_start - range.start) as usize;
271                    found = true;
272                    break;
273                }
274                idx += (range.end - range.start) as usize;
275            }
276            if !found {
277                return Err(FileReconstructionError::CorruptedReconstruction(format!(
278                    "chunk_start {chunk_start} not found in chunk_ranges {chunk_ranges:?} for file term {local_term_index}"
279                )));
280            }
281            idx
282        };
283
284        file_term_data.push(FileTermEntry {
285            byte_range: FileRange::new(cur_file_byte_offset, cur_file_byte_offset + term_byte_size),
286            xorb_chunk_range: term.range,
287            offset_into_first_range,
288            xorb_block_index,
289            xorb_block_start_index,
290        });
291
292        cur_file_byte_offset += term_byte_size;
293    }
294
295    // Sort each block's references by chunk start so that determine_size_if_possible
296    // can use its forward-chaining DP to check coverage.
297    for block in &mut xorb_blocks {
298        block.references.sort_by_key(|r| r.term_chunks.start);
299        block.uncompressed_size_if_known =
300            XorbBlock::determine_size_if_possible(&block.chunk_ranges, &block.references);
301    }
302
303    // The last term in the reconstruction may extend beyond the requested range
304    // (e.g. when the query ends mid-chunk). Trim it to the query boundary.
305    if cur_file_byte_offset > query_file_byte_range.end {
306        let last_term_shrinkage = cur_file_byte_offset - query_file_byte_range.end;
307
308        debug_assert!(!file_term_data.is_empty());
309
310        if let Some(entry) = file_term_data.last_mut() {
311            entry.byte_range.end -= last_term_shrinkage;
312        }
313    }
314
315    // The actual range covered, which may be smaller than requested if the file
316    // ends before the requested range.
317    let actual_range = FileRange::new(
318        file_term_data.first().map(|e| e.byte_range.start).unwrap_or(0),
319        file_term_data.last().map(|e| e.byte_range.end).unwrap_or(0),
320    );
321
322    // Total compressed bytes that will be transferred across all xorb block downloads.
323    let total_transfer_bytes: u64 = xorb_block_retrieval_urls
324        .iter()
325        .flat_map(|(_, ranges)| ranges)
326        .map(|r| r.length())
327        .sum();
328
329    // Wrap the retrieval URLs in a shared struct so all file terms can share them
330    // and coordinate URL refreshes through a single lock.
331    let url_info =
332        Arc::new(TermBlockRetrievalURLs::new(file_hash, actual_range, acquisition_id, xorb_block_retrieval_urls));
333
334    // Second pass: convert the intermediate FileTermEntry data into final FileTerm
335    // structs, now that we can wrap xorb blocks in Arc and share the url_info.
336    let xorb_blocks_arc: Vec<Arc<XorbBlock>> = xorb_blocks.into_iter().map(Arc::new).collect();
337
338    let file_terms: Vec<FileTerm> = file_term_data
339        .into_iter()
340        .map(|entry| FileTerm {
341            byte_range: entry.byte_range,
342            xorb_chunk_range: entry.xorb_chunk_range,
343            xorb_block_start_index: entry.xorb_block_start_index,
344            offset_into_first_range: entry.offset_into_first_range,
345            xorb_block: xorb_blocks_arc[entry.xorb_block_index].clone(),
346            url_info: url_info.clone(),
347        })
348        .collect();
349
350    Ok(Some((actual_range, total_transfer_bytes, file_terms)))
351}
352
353#[cfg(test)]
354mod tests {
355    use std::sync::Arc;
356
357    use more_asserts::assert_le;
358    use xet_client::cas_client::{ClientTestingUtils, LocalClient, RandomFileContents};
359    use xet_client::cas_types::{ChunkRange, FileRange};
360    use xet_runtime::core::XetContext;
361    use xet_runtime::utils::UniqueId;
362
363    use super::*;
364
365    const TEST_CHUNK_SIZE: usize = 101;
366
367    fn verify_xorb_block_references(file_terms: &[FileTerm]) {
368        for file_term in file_terms {
369            let refs = &file_term.xorb_block.references;
370            assert!(
371                refs.iter().any(|r| r.term_chunks == file_term.xorb_chunk_range),
372                "xorb_chunk_range {:?} must be in block references {:?}",
373                file_term.xorb_chunk_range,
374                refs.as_slice()
375            );
376        }
377        let mut seen_blocks = std::collections::HashSet::new();
378        for file_term in file_terms {
379            if seen_blocks.insert(file_term.xorb_block.xorb_block_index) {
380                let refs = &file_term.xorb_block.references;
381                for w in refs.windows(2) {
382                    assert_le!(w[0].term_chunks.start, w[1].term_chunks.start);
383                }
384            }
385        }
386    }
387
388    /// Creates a test client and uploads a random file with the given term specification.
389    /// Returns the client and file contents for verification.
390    async fn setup_test_file(term_spec: &[(u64, (u64, u64))]) -> (XetContext, Arc<LocalClient>, RandomFileContents) {
391        let ctx = XetContext::default().unwrap();
392        let client = LocalClient::temporary(ctx.clone()).await.unwrap();
393        let file_contents = client.upload_random_file(term_spec, TEST_CHUNK_SIZE).await.unwrap();
394        (ctx, client, file_contents)
395    }
396
397    /// Retrieves file terms and thoroughly verifies their correctness.
398    ///
399    /// If `requested_range` is None, retrieves the full file range.
400    ///
401    /// This function:
402    /// - Retrieves file terms from the client for the given range
403    /// - Verifies file terms are contiguous and cover the range
404    /// - Verifies each file term's xorb block references are valid
405    /// - Verifies chunk ranges are within xorb block boundaries
406    /// - Cross-references with the known file contents for correctness
407    /// - Verifies number of file terms matches expected from term_spec
408    async fn retrieve_and_verify(
409        ctx: &XetContext,
410        client: &Arc<LocalClient>,
411        file_contents: &RandomFileContents,
412        requested_range: Option<FileRange>,
413    ) {
414        let requested_range = requested_range.unwrap_or_else(|| FileRange::new(0, file_contents.data.len() as u64));
415        let dyn_client: Arc<dyn Client> = client.clone();
416
417        let (returned_range, _, file_terms) =
418            retrieve_file_term_block(ctx, dyn_client.clone(), file_contents.file_hash, requested_range)
419                .await
420                .expect("retrieve_file_term_block should succeed")
421                .expect("file_terms should not be None for valid range");
422
423        // Verify the returned range matches the requested range.
424        assert_eq!(returned_range, requested_range);
425
426        // Track position within the requested range.
427        let mut current_pos = requested_range.start;
428        let mut file_term_data_offset = 0usize;
429
430        // Find the starting term index in file_contents based on requested_range.start.
431        let mut expected_term_idx = 0;
432        let mut byte_offset = 0u64;
433        for (idx, term) in file_contents.terms.iter().enumerate() {
434            let term_end = byte_offset + term.data.len() as u64;
435            if term_end > requested_range.start {
436                expected_term_idx = idx;
437                file_term_data_offset = (requested_range.start - byte_offset) as usize;
438                break;
439            }
440            byte_offset = term_end;
441        }
442
443        // Collect unique xorb block indices to verify count
444        let mut seen_xorb_indices = std::collections::HashSet::new();
445
446        // Now verify actual data reconstruction by fetching all file terms.
447        let mut reconstructed_data = Vec::with_capacity((requested_range.end - requested_range.start) as usize);
448        let mut term_count = 0;
449
450        for file_term in &file_terms {
451            // Verify byte range is contiguous.
452            assert_eq!(file_term.byte_range.start, current_pos);
453            assert!(file_term.byte_range.end > file_term.byte_range.start);
454            assert_le!(file_term.byte_range.end, requested_range.end);
455
456            // Track xorb block index
457            seen_xorb_indices.insert(file_term.xorb_block.xorb_block_index);
458
459            // Verify chunk range is within xorb block boundaries: the term's chunk range
460            // must be contained within at least one of the block's chunk ranges.
461            let xorb_block = &file_term.xorb_block;
462            let term_in_some_range = xorb_block
463                .chunk_ranges
464                .iter()
465                .any(|cr| file_term.xorb_chunk_range.start >= cr.start && file_term.xorb_chunk_range.end <= cr.end);
466            assert!(
467                term_in_some_range,
468                "term chunk range {:?} not within any block chunk range {:?}",
469                file_term.xorb_chunk_range, xorb_block.chunk_ranges
470            );
471
472            // Cross-reference with known file contents.
473            if expected_term_idx < file_contents.terms.len() {
474                let expected_term = &file_contents.terms[expected_term_idx];
475
476                // Verify xorb hash matches.
477                assert_eq!(xorb_block.xorb_hash, expected_term.xorb_hash);
478
479                // Verify chunk range matches (accounting for partial first term).
480                if file_term_data_offset == 0 {
481                    assert_eq!(file_term.xorb_chunk_range.start, expected_term.chunk_start);
482                }
483            }
484
485            // Verify all xorb blocks referenced have valid hashes.
486            assert!(file_contents.xorbs.contains_key(&file_term.xorb_block.xorb_hash));
487
488            // Get the data task and await it.
489            let data_future = file_term
490                .get_data_task(ctx.clone(), dyn_client.clone(), None, None)
491                .await
492                .unwrap();
493            let data = data_future.await.unwrap();
494
495            // Verify the data size matches the byte range.
496            let expected_size = (file_term.byte_range.end - file_term.byte_range.start) as usize;
497            assert_eq!(data.len(), expected_size, "Term {term_count} data size mismatch");
498
499            reconstructed_data.extend_from_slice(&data);
500
501            current_pos = file_term.byte_range.end;
502            expected_term_idx += 1;
503            file_term_data_offset = 0;
504            term_count += 1;
505        }
506
507        // Verify we covered the entire requested range.
508        assert_eq!(current_pos, requested_range.end);
509
510        // For full file range, verify we have the expected number of file terms.
511        if requested_range.start == 0 && requested_range.end == file_contents.data.len() as u64 {
512            assert_eq!(term_count, file_contents.terms.len());
513        }
514
515        // Compare reconstructed data with expected file contents.
516        let expected_data = &file_contents.data[requested_range.start as usize..requested_range.end as usize];
517        assert_eq!(reconstructed_data.len(), expected_data.len());
518        assert_eq!(reconstructed_data, expected_data);
519
520        verify_xorb_block_references(&file_terms);
521    }
522
523    // ==================== Test Cases ====================
524
525    #[tokio::test]
526    async fn test_xorb_block_references_exact() {
527        let (runtime, client, file_contents) = setup_test_file(&[(1, (0, 2)), (1, (2, 4)), (1, (4, 6))]).await;
528        let file_range = FileRange::new(0, file_contents.data.len() as u64);
529        let dyn_client: Arc<dyn Client> = client.clone();
530        let (_, _, file_terms) = retrieve_file_term_block(&runtime, dyn_client, file_contents.file_hash, file_range)
531            .await
532            .unwrap()
533            .unwrap();
534        verify_xorb_block_references(&file_terms);
535        assert_eq!(file_terms.len(), 3);
536        let block = &file_terms[0].xorb_block;
537        let ref_ranges: Vec<ChunkRange> = block.references.iter().map(|r| r.term_chunks).collect();
538        let expected = vec![ChunkRange::new(0, 2), ChunkRange::new(2, 4), ChunkRange::new(4, 6)];
539        assert_eq!(ref_ranges, expected);
540
541        let (runtime2, client2, file_contents2) = setup_test_file(&[(1, (0, 5)), (1, (0, 5))]).await;
542        let file_range2 = FileRange::new(0, file_contents2.data.len() as u64);
543        let dyn_client2: Arc<dyn Client> = client2.clone();
544        let (_, _, file_terms2) =
545            retrieve_file_term_block(&runtime2, dyn_client2, file_contents2.file_hash, file_range2)
546                .await
547                .unwrap()
548                .unwrap();
549        verify_xorb_block_references(&file_terms2);
550        let block2 = &file_terms2[0].xorb_block;
551        let ref_ranges2: Vec<ChunkRange> = block2.references.iter().map(|r| r.term_chunks).collect();
552        let expected2 = vec![ChunkRange::new(0, 5), ChunkRange::new(0, 5)];
553        assert_eq!(ref_ranges2, expected2);
554    }
555
556    #[tokio::test]
557    async fn test_single_xorb_full_range() {
558        let (runtime, client, file_contents) = setup_test_file(&[(1, (0, 5))]).await;
559        retrieve_and_verify(&runtime, &client, &file_contents, None).await;
560    }
561
562    #[tokio::test]
563    async fn test_multiple_terms_same_xorb() {
564        let (runtime, client, file_contents) = setup_test_file(&[(1, (0, 2)), (1, (2, 4)), (1, (4, 6))]).await;
565        retrieve_and_verify(&runtime, &client, &file_contents, None).await;
566    }
567
568    #[tokio::test]
569    async fn test_multiple_xorbs() {
570        let (runtime, client, file_contents) = setup_test_file(&[(1, (0, 3)), (2, (0, 2)), (3, (0, 4))]).await;
571        retrieve_and_verify(&runtime, &client, &file_contents, None).await;
572    }
573
574    #[tokio::test]
575    async fn test_overlapping_chunk_ranges() {
576        let (runtime, client, file_contents) = setup_test_file(&[(1, (0, 5)), (1, (1, 3)), (1, (2, 4))]).await;
577        retrieve_and_verify(&runtime, &client, &file_contents, None).await;
578    }
579
580    #[tokio::test]
581    async fn test_partial_range_middle() {
582        let (runtime, client, file_contents) = setup_test_file(&[(1, (0, 10))]).await;
583        let file_len = file_contents.data.len() as u64;
584        retrieve_and_verify(&runtime, &client, &file_contents, Some(FileRange::new(file_len / 4, file_len * 3 / 4)))
585            .await;
586    }
587
588    #[tokio::test]
589    async fn test_partial_range_start() {
590        let (runtime, client, file_contents) = setup_test_file(&[(1, (0, 10))]).await;
591        let file_len = file_contents.data.len() as u64;
592        retrieve_and_verify(&runtime, &client, &file_contents, Some(FileRange::new(0, file_len / 2))).await;
593    }
594
595    #[tokio::test]
596    async fn test_partial_range_end() {
597        let (runtime, client, file_contents) = setup_test_file(&[(1, (0, 10))]).await;
598        let file_len = file_contents.data.len() as u64;
599        retrieve_and_verify(&runtime, &client, &file_contents, Some(FileRange::new(file_len / 2, file_len))).await;
600    }
601
602    #[tokio::test]
603    async fn test_beyond_file_end() {
604        let (runtime, client, file_contents) = setup_test_file(&[(1, (0, 3))]).await;
605        let file_len = file_contents.data.len() as u64;
606        let beyond_range = FileRange::new(file_len + 1000, file_len + 2000);
607
608        let dyn_client: Arc<dyn Client> = client.clone();
609        let result = retrieve_file_term_block(&runtime, dyn_client, file_contents.file_hash, beyond_range).await;
610
611        match result {
612            Ok(None) => {},
613            Ok(Some((_, _, file_terms))) => assert!(file_terms.is_empty()),
614            Err(_) => {},
615        }
616    }
617
618    #[tokio::test]
619    async fn test_interleaved_xorbs() {
620        let (runtime, client, file_contents) =
621            setup_test_file(&[(1, (0, 2)), (2, (0, 2)), (1, (2, 4)), (2, (2, 4))]).await;
622        retrieve_and_verify(&runtime, &client, &file_contents, None).await;
623    }
624
625    #[tokio::test]
626    async fn test_non_contiguous_chunks() {
627        let (runtime, client, file_contents) = setup_test_file(&[(1, (0, 2)), (1, (4, 6))]).await;
628        retrieve_and_verify(&runtime, &client, &file_contents, None).await;
629    }
630
631    #[tokio::test]
632    async fn test_adjacent_chunks() {
633        let (runtime, client, file_contents) = setup_test_file(&[(1, (0, 3)), (1, (3, 5))]).await;
634        retrieve_and_verify(&runtime, &client, &file_contents, None).await;
635    }
636
637    #[tokio::test]
638    async fn test_single_chunk_terms() {
639        let (runtime, client, file_contents) =
640            setup_test_file(&[(1, (0, 1)), (1, (1, 2)), (1, (2, 3)), (2, (0, 1)), (2, (1, 2))]).await;
641        retrieve_and_verify(&runtime, &client, &file_contents, None).await;
642    }
643
644    #[tokio::test]
645    async fn test_large_file_many_xorbs() {
646        let term_spec: Vec<(u64, (u64, u64))> = (1..=10).map(|i| (i, (0, 3))).collect();
647        let (runtime, client, file_contents) = setup_test_file(&term_spec).await;
648        retrieve_and_verify(&runtime, &client, &file_contents, None).await;
649    }
650
651    #[tokio::test]
652    async fn test_xorb_block_deduplication() {
653        let (runtime, client, file_contents) = setup_test_file(&[(1, (0, 5)), (1, (0, 5))]).await;
654        retrieve_and_verify(&runtime, &client, &file_contents, None).await;
655    }
656
657    #[tokio::test]
658    async fn test_retrieval_url_acquisition() {
659        let (runtime, client, file_contents) = setup_test_file(&[(1, (0, 5))]).await;
660        let file_range = FileRange::new(0, file_contents.data.len() as u64);
661        let dyn_client: Arc<dyn Client> = client.clone();
662
663        let (_, _, file_terms) = retrieve_file_term_block(&runtime, dyn_client, file_contents.file_hash, file_range)
664            .await
665            .unwrap()
666            .unwrap();
667
668        // Get the first file term's xorb block to test URL retrieval
669        let file_term = &file_terms[0];
670        let xorb_block_index = file_term.xorb_block.xorb_block_index;
671        let (unique_id, url, http_ranges) = file_term.url_info.get_retrieval_url(xorb_block_index).await;
672
673        assert!(!url.is_empty());
674        assert!(!http_ranges.is_empty());
675        assert!(http_ranges[0].start <= http_ranges[0].end);
676        assert!(unique_id != UniqueId::null());
677    }
678
679    #[tokio::test]
680    async fn test_complex_mixed_pattern() {
681        let term_spec = &[
682            (1, (0, 3)),
683            (2, (0, 2)),
684            (1, (3, 5)),
685            (3, (1, 4)),
686            (2, (4, 6)),
687            (1, (0, 2)),
688        ];
689        let (runtime, client, file_contents) = setup_test_file(term_spec).await;
690        retrieve_and_verify(&runtime, &client, &file_contents, None).await;
691    }
692
693    #[tokio::test]
694    async fn test_repeated_xorb_different_ranges() {
695        let (runtime, client, file_contents) =
696            setup_test_file(&[(1, (0, 2)), (1, (3, 5)), (1, (1, 3)), (1, (4, 6))]).await;
697        retrieve_and_verify(&runtime, &client, &file_contents, None).await;
698    }
699
700    #[tokio::test]
701    async fn test_single_chunk_file() {
702        let (runtime, client, file_contents) = setup_test_file(&[(1, (0, 1))]).await;
703        retrieve_and_verify(&runtime, &client, &file_contents, None).await;
704    }
705
706    #[tokio::test]
707    async fn test_many_small_terms_from_different_xorbs() {
708        let term_spec: Vec<(u64, (u64, u64))> = (1..=20).map(|i| (i, (0, 1))).collect();
709        let (runtime, client, file_contents) = setup_test_file(&term_spec).await;
710        retrieve_and_verify(&runtime, &client, &file_contents, None).await;
711    }
712
713    #[tokio::test]
714    async fn test_range_few_bytes_before_end() {
715        let (runtime, client, file_contents) = setup_test_file(&[(1, (0, 5))]).await;
716        let file_len = file_contents.data.len() as u64;
717
718        let range = FileRange::new(0, file_len - 3);
719        retrieve_and_verify(&runtime, &client, &file_contents, Some(range)).await;
720
721        let range = FileRange::new(0, file_len - 1);
722        retrieve_and_verify(&runtime, &client, &file_contents, Some(range)).await;
723    }
724
725    #[tokio::test]
726    async fn test_range_few_bytes_after_start() {
727        let (runtime, client, file_contents) = setup_test_file(&[(1, (0, 5))]).await;
728        let file_len = file_contents.data.len() as u64;
729
730        let range = FileRange::new(3, file_len);
731        retrieve_and_verify(&runtime, &client, &file_contents, Some(range)).await;
732
733        let range = FileRange::new(1, file_len);
734        retrieve_and_verify(&runtime, &client, &file_contents, Some(range)).await;
735    }
736
737    #[tokio::test]
738    async fn test_range_few_bytes_offset_both_ends() {
739        let (runtime, client, file_contents) = setup_test_file(&[(1, (0, 5))]).await;
740        let file_len = file_contents.data.len() as u64;
741
742        let range = FileRange::new(2, file_len - 2);
743        retrieve_and_verify(&runtime, &client, &file_contents, Some(range)).await;
744
745        let range = FileRange::new(file_len / 2 - 1, file_len / 2 + 1);
746        retrieve_and_verify(&runtime, &client, &file_contents, Some(range)).await;
747    }
748
749    #[tokio::test]
750    async fn test_range_single_byte_at_various_positions() {
751        let (runtime, client, file_contents) = setup_test_file(&[(1, (0, 5))]).await;
752        let file_len = file_contents.data.len() as u64;
753
754        retrieve_and_verify(&runtime, &client, &file_contents, Some(FileRange::new(0, 1))).await;
755
756        retrieve_and_verify(&runtime, &client, &file_contents, Some(FileRange::new(file_len - 1, file_len))).await;
757
758        let mid = file_len / 2;
759        retrieve_and_verify(&runtime, &client, &file_contents, Some(FileRange::new(mid, mid + 1))).await;
760    }
761
762    #[tokio::test]
763    async fn test_multi_term_range_ends_mid_chunk() {
764        let (runtime, client, file_contents) = setup_test_file(&[(1, (0, 3)), (2, (0, 3)), (3, (0, 3))]).await;
765        let file_len = file_contents.data.len() as u64;
766
767        let range = FileRange::new(0, file_len - 5);
768        retrieve_and_verify(&runtime, &client, &file_contents, Some(range)).await;
769    }
770
771    #[tokio::test]
772    async fn test_multi_term_range_starts_mid_chunk() {
773        let (runtime, client, file_contents) = setup_test_file(&[(1, (0, 3)), (2, (0, 3)), (3, (0, 3))]).await;
774        let file_len = file_contents.data.len() as u64;
775
776        let range = FileRange::new(5, file_len);
777        retrieve_and_verify(&runtime, &client, &file_contents, Some(range)).await;
778    }
779
780    // ==================== Multi-Disjoint Range Edge Cases ====================
781
782    /// Single xorb with three disjoint chunk ranges.
783    /// This creates one XorbBlock with chunk_ranges = [(0,2), (4,6), (8,10)].
784    #[tokio::test]
785    async fn test_triple_disjoint_same_xorb() {
786        let (runtime, client, file_contents) = setup_test_file(&[(1, (0, 2)), (1, (4, 6)), (1, (8, 10))]).await;
787        retrieve_and_verify(&runtime, &client, &file_contents, None).await;
788    }
789
790    /// Triple disjoint ranges with a partial byte range spanning the gap.
791    #[tokio::test]
792    async fn test_triple_disjoint_partial_range_across_gap() {
793        let (runtime, client, file_contents) = setup_test_file(&[(1, (0, 2)), (1, (4, 6)), (1, (8, 10))]).await;
794        let file_len = file_contents.data.len() as u64;
795        let range = FileRange::new(file_len / 4, file_len * 3 / 4);
796        retrieve_and_verify(&runtime, &client, &file_contents, Some(range)).await;
797    }
798
799    /// Two xorbs, each with two disjoint ranges, interleaved in file order.
800    #[tokio::test]
801    async fn test_two_xorbs_interleaved_disjoint() {
802        let term_spec = &[(1, (0, 2)), (2, (0, 2)), (1, (4, 6)), (2, (4, 6))];
803        let (runtime, client, file_contents) = setup_test_file(term_spec).await;
804        retrieve_and_verify(&runtime, &client, &file_contents, None).await;
805    }
806
807    /// Two xorbs interleaved with disjoint ranges, partial byte range.
808    #[tokio::test]
809    async fn test_two_xorbs_interleaved_disjoint_partial() {
810        let term_spec = &[(1, (0, 2)), (2, (0, 2)), (1, (4, 6)), (2, (4, 6))];
811        let (runtime, client, file_contents) = setup_test_file(term_spec).await;
812        let file_len = file_contents.data.len() as u64;
813        retrieve_and_verify(&runtime, &client, &file_contents, Some(FileRange::new(file_len / 3, file_len * 2 / 3)))
814            .await;
815    }
816
817    /// Single xorb with four disjoint ranges, each a single chunk wide.
818    #[tokio::test]
819    async fn test_four_single_chunk_disjoint() {
820        let term_spec = &[(1, (0, 1)), (1, (3, 4)), (1, (6, 7)), (1, (9, 10))];
821        let (runtime, client, file_contents) = setup_test_file(term_spec).await;
822        retrieve_and_verify(&runtime, &client, &file_contents, None).await;
823    }
824
825    /// Mix of contiguous and disjoint ranges from the same xorb.
826    /// Chunks 0-4 are contiguous, then a gap, then chunk 8-10.
827    #[tokio::test]
828    async fn test_contiguous_then_disjoint() {
829        let term_spec = &[(1, (0, 2)), (1, (2, 4)), (1, (8, 10))];
830        let (runtime, client, file_contents) = setup_test_file(term_spec).await;
831        retrieve_and_verify(&runtime, &client, &file_contents, None).await;
832    }
833
834    /// Three xorbs with complex disjoint access patterns.
835    #[tokio::test]
836    async fn test_three_xorbs_complex_disjoint() {
837        let term_spec = &[
838            (1, (0, 2)),
839            (2, (0, 3)),
840            (3, (2, 5)),
841            (1, (5, 8)),
842            (2, (6, 8)),
843            (3, (0, 2)),
844        ];
845        let (runtime, client, file_contents) = setup_test_file(term_spec).await;
846        retrieve_and_verify(&runtime, &client, &file_contents, None).await;
847    }
848}