Skip to main content

rns_core/resource/
receiver.rs

1use alloc::boxed::Box;
2use alloc::vec;
3use alloc::vec::Vec;
4
5use super::advertisement::ResourceAdvertisement;
6use super::assembly::{AssemblyData, AssemblyId, AssemblyResult, ResourceAssembly};
7use super::parts::map_hash;
8use super::types::*;
9use super::window::WindowState;
10use crate::buffer::types::Compressor;
11use crate::constants::*;
12use rns_crypto::token::TokenDecryptStream;
13
14/// Parts' worth of storage a receiver reserves ahead at most.
15const PART_RESERVE_PARTS: usize = 64;
16
17/// Decrypts a resource's ciphertext held as its parts, in order, dropping the
18/// first `skip` plaintext bytes (the random prefix). It must authenticate the
19/// whole ciphertext before returning any plaintext.
20pub type PartsDecryptFn<'a> = dyn Fn(&[&[u8]], usize) -> Result<Vec<u8>, ()> + 'a;
21
22/// Resource receiver state machine.
23///
24/// Unpacks advertisements, requests parts, receives parts, assembles data.
25/// Returns `Vec<ResourceAction>` — no I/O, no callbacks.
26pub struct ResourceReceiver {
27    assembly_id: Option<AssemblyId>,
28    /// Decryption of the parts held in order from the first, ahead of
29    /// assembly; see [`Self::decrypt_next_part`].
30    decrypt: Option<Box<TokenDecryptStream>>,
31    decrypted_parts: usize,
32    /// Current status
33    pub status: ResourceStatus,
34    /// Resource hash (from advertisement, 32 bytes)
35    pub resource_hash: Vec<u8>,
36    /// Random hash (from advertisement)
37    pub random_hash: Vec<u8>,
38    /// Original hash
39    pub original_hash: Vec<u8>,
40    /// Flags
41    pub flags: AdvFlags,
42    /// Transfer size (encrypted)
43    pub transfer_size: u64,
44    /// Total uncompressed data size
45    pub data_size: u64,
46    /// Total parts
47    pub total_parts: usize,
48    /// Received parts, back to back in arrival order.
49    part_data: Vec<u8>,
50    /// Part index -> (start, length) in `part_data` (None = not yet received)
51    part_spans: Vec<Option<(usize, usize)>>,
52    /// Hashmap: part index -> map_hash (None if not yet known)
53    hashmap: Vec<Option<[u8; RESOURCE_MAPHASH_LEN]>>,
54    /// Number of hashmap entries populated
55    hashmap_height: usize,
56    /// Whether we're waiting for a hashmap update
57    pub waiting_for_hmu: bool,
58    /// Number of parts received
59    pub received_count: usize,
60    /// Outstanding parts in current window request
61    pub outstanding_parts: usize,
62    /// Consecutive completed height (-1 means none)
63    consecutive_completed_height: isize,
64    /// SDU size
65    sdu: usize,
66    /// Link RTT estimate (from link establishment)
67    link_rtt: f64,
68    /// Retries left
69    pub retries_left: usize,
70    /// Max retries
71    max_retries: usize,
72    /// RTT estimate
73    pub rtt: Option<f64>,
74    /// Part timeout factor
75    part_timeout_factor: f64,
76    /// Last activity timestamp
77    pub last_activity: f64,
78    /// Request sent timestamp
79    pub req_sent: f64,
80    /// Request sent bytes
81    req_sent_bytes: usize,
82    /// Request response timestamp
83    req_resp: Option<f64>,
84    /// RTT received bytes
85    rtt_rxd_bytes: usize,
86    /// RTT received bytes at part request
87    rtt_rxd_bytes_at_part_req: usize,
88    /// Request response RTT rate
89    req_resp_rtt_rate: f64,
90    /// Request data RTT rate
91    req_data_rtt_rate: f64,
92    /// EIFR
93    pub eifr: Option<f64>,
94    /// Previous EIFR from prior transfer
95    previous_eifr: Option<f64>,
96    /// Segment index
97    pub segment_index: u64,
98    /// Total segments
99    pub total_segments: u64,
100    /// Has metadata
101    pub has_metadata: bool,
102    /// Request ID
103    pub request_id: Option<Vec<u8>>,
104    /// Window state
105    pub window: WindowState,
106    /// Original advertisement bytes for this incoming transfer.
107    pub advertisement_packet: Vec<u8>,
108    /// Maximum allowed decompressed size for compressed resource payloads.
109    pub max_decompressed_size: usize,
110}
111
112impl ResourceReceiver {
113    /// Create a receiver from an advertisement packet.
114    pub fn from_advertisement(
115        adv_data: &[u8],
116        sdu: usize,
117        link_rtt: f64,
118        now: f64,
119        previous_window: Option<usize>,
120        previous_eifr: Option<f64>,
121    ) -> Result<Self, ResourceError> {
122        let adv = ResourceAdvertisement::unpack(adv_data)?;
123
124        // Validate resource_hash is 32 bytes
125        if adv.resource_hash.len() != 32 {
126            return Err(ResourceError::InvalidAdvertisement);
127        }
128        if adv.random_hash.len() != RESOURCE_RANDOM_HASH_SIZE || adv.original_hash.len() != 32 {
129            return Err(ResourceError::InvalidAdvertisement);
130        }
131        // Split advertisements carry the logical size of the complete
132        // Resource in data_size, while each receiver only assembles one
133        // bounded segment. Aggregate receive policy is enforced by rns-net.
134        if adv.transfer_size > RESOURCE_AUTO_COMPRESS_MAX_SIZE as u64
135            || (!adv.flags.split && adv.data_size > RESOURCE_AUTO_COMPRESS_MAX_SIZE as u64)
136        {
137            return Err(ResourceError::TooLarge);
138        }
139
140        // Python derives the part count of the transfer it receives from the
141        // advertised (encrypted, compressed) transfer size of this segment:
142        // ceil(t / sdu). Senders compute `n` the same way over the same SDU,
143        // so a matching advertisement carries exactly that count. No tolerance
144        // applies: Python's receiver ignores `n` and expects exactly this many
145        // parts, so a transfer advertising any other count cannot complete.
146        let total_parts = usize::try_from(adv.num_parts).map_err(|_| ResourceError::TooLarge)?;
147        if total_parts == 0 {
148            return Err(ResourceError::InvalidAdvertisement);
149        }
150        let transfer_size =
151            usize::try_from(adv.transfer_size).map_err(|_| ResourceError::TooLarge)?;
152        let expected_parts = transfer_size.div_ceil(sdu.max(1));
153        if total_parts != expected_parts {
154            return Err(ResourceError::PartCountMismatch);
155        }
156
157        // Part storage grows as parts arrive, so an accepted advertisement
158        // that is never followed by data holds no per-part allocation. The
159        // hashmap holds only the entries the advertisement carries.
160        let initial_hashes = core::cmp::min(adv.hashmap.len() / RESOURCE_MAPHASH_LEN, total_parts);
161        let mut hashmap_vec: Vec<Option<[u8; RESOURCE_MAPHASH_LEN]>> =
162            Vec::with_capacity(initial_hashes);
163
164        // Populate initial hashmap from advertisement
165        for chunk in adv
166            .hashmap
167            .chunks_exact(RESOURCE_MAPHASH_LEN)
168            .take(initial_hashes)
169        {
170            let mut h = [0u8; RESOURCE_MAPHASH_LEN];
171            h.copy_from_slice(chunk);
172            hashmap_vec.push(Some(h));
173        }
174        let hashmap_height = hashmap_vec.len();
175
176        let mut window_state = WindowState::new();
177        if let Some(prev_w) = previous_window {
178            window_state.restore(prev_w);
179        }
180
181        Ok(ResourceReceiver {
182            assembly_id: None,
183            decrypt: None,
184            decrypted_parts: 0,
185            status: ResourceStatus::None,
186            resource_hash: adv.resource_hash,
187            random_hash: adv.random_hash,
188            original_hash: adv.original_hash,
189            flags: adv.flags,
190            transfer_size: adv.transfer_size,
191            data_size: adv.data_size,
192            total_parts,
193            part_data: Vec::new(),
194            part_spans: Vec::new(),
195            hashmap: hashmap_vec,
196            hashmap_height,
197            waiting_for_hmu: false,
198            received_count: 0,
199            outstanding_parts: 0,
200            consecutive_completed_height: -1,
201            sdu,
202            link_rtt,
203            retries_left: RESOURCE_MAX_RETRIES,
204            max_retries: RESOURCE_MAX_RETRIES,
205            rtt: None,
206            part_timeout_factor: RESOURCE_PART_TIMEOUT_FACTOR,
207            last_activity: now,
208            req_sent: 0.0,
209            req_sent_bytes: 0,
210            req_resp: None,
211            rtt_rxd_bytes: 0,
212            rtt_rxd_bytes_at_part_req: 0,
213            req_resp_rtt_rate: 0.0,
214            req_data_rtt_rate: 0.0,
215            eifr: None,
216            previous_eifr,
217            segment_index: adv.segment_index,
218            total_segments: adv.total_segments,
219            has_metadata: adv.flags.has_metadata,
220            request_id: adv.request_id,
221            window: window_state,
222            advertisement_packet: adv_data.to_vec(),
223            max_decompressed_size: RESOURCE_AUTO_COMPRESS_MAX_SIZE,
224        })
225    }
226
227    fn has_part(&self, index: usize) -> bool {
228        self.part_spans.get(index).is_some_and(Option::is_some)
229    }
230
231    fn store_part(&mut self, index: usize, data: &[u8]) {
232        if self.part_spans.len() <= index {
233            // Reserve for the rest of a small transfer at once, and in bounded
234            // steps for a large one, so storage still grows only with what
235            // arrives.
236            let wanted = (index + 1).max(self.total_parts.min(PART_RESERVE_PARTS));
237            self.part_spans.reserve(wanted - self.part_spans.len());
238            self.part_spans.resize(index + 1, None);
239        }
240        if self.part_data.capacity() - self.part_data.len() < data.len() {
241            let remaining = self.total_parts - self.received_count;
242            let wanted = remaining.min(PART_RESERVE_PARTS).saturating_mul(self.sdu);
243            self.part_data.reserve(wanted.max(data.len()));
244        }
245        self.part_spans[index] = Some((self.part_data.len(), data.len()));
246        self.part_data.extend_from_slice(data);
247    }
248
249    fn hash_at(&self, index: usize) -> Option<&[u8; RESOURCE_MAPHASH_LEN]> {
250        self.hashmap.get(index).and_then(Option::as_ref)
251    }
252
253    fn store_hash(&mut self, index: usize, hash: [u8; RESOURCE_MAPHASH_LEN]) {
254        if self.hashmap.len() <= index {
255            self.hashmap.resize(index + 1, None);
256        }
257        self.hashmap[index] = Some(hash);
258    }
259
260    /// Accept the advertised resource. Begins transfer.
261    pub fn accept(&mut self, now: f64) -> Vec<ResourceAction> {
262        if self.status >= ResourceStatus::Assembling {
263            return vec![];
264        }
265        self.status = ResourceStatus::Transferring;
266        self.last_activity = now;
267        self.request_next(now)
268    }
269
270    /// Reject the advertised resource.
271    pub fn reject(&mut self) -> Vec<ResourceAction> {
272        self.status = ResourceStatus::Rejected;
273        vec![ResourceAction::SendCancelReceiver(
274            self.resource_hash.clone(),
275        )]
276    }
277
278    /// Cancel an accepted incoming transfer.
279    pub fn cancel(&mut self) -> Vec<ResourceAction> {
280        if self.status < ResourceStatus::Complete {
281            self.status = ResourceStatus::Failed;
282            vec![ResourceAction::SendCancelReceiver(
283                self.resource_hash.clone(),
284            )]
285        } else {
286            Vec::new()
287        }
288    }
289
290    fn corrupt_actions(&mut self, error: ResourceError) -> Vec<ResourceAction> {
291        self.status = ResourceStatus::Corrupt;
292        vec![
293            ResourceAction::SendCancelReceiver(self.resource_hash.clone()),
294            ResourceAction::Failed(error),
295            ResourceAction::TeardownLink,
296        ]
297    }
298
299    /// Continue the window and rate estimate the previous segment of the
300    /// same split transfer ended with, instead of starting slow again.
301    pub fn continue_from(&mut self, window: WindowState, eifr: Option<f64>) {
302        self.window = window;
303        if eifr.is_some() {
304            self.previous_eifr = eifr;
305        }
306    }
307
308    /// Whether held parts wait for [`Self::decrypt_next_part`].
309    pub fn has_parts_to_decrypt(&self) -> bool {
310        self.flags.encrypted
311            && self.status < ResourceStatus::Assembling
312            && (self.decrypted_parts as isize) <= self.consecutive_completed_height
313    }
314
315    /// Authenticate and decrypt the next held part ahead of assembly, so
316    /// that assembly only has the parts after it left to do. Parts are taken
317    /// in order from the first; `start` begins the decryption at the first
318    /// call. Returns whether more held parts wait. Nothing decrypted here is
319    /// used before assembly has checked the tag.
320    pub fn decrypt_next_part(
321        &mut self,
322        start: impl FnOnce() -> Option<TokenDecryptStream>,
323    ) -> bool {
324        if !self.has_parts_to_decrypt() {
325            return false;
326        }
327        if self.decrypt.is_none() {
328            let Some(mut stream) = start() else {
329                return false;
330            };
331            // The plaintext of an uncompressed resource is the data its hash
332            // covers, hashed alongside the tag's MAC.
333            if !self.flags.compressed {
334                stream.hash_plaintext();
335            }
336            self.decrypt = Some(Box::new(stream));
337        }
338        let Some(stream) = self.decrypt.as_mut() else {
339            return false;
340        };
341        let Some(Some((at, len))) = self.part_spans.get(self.decrypted_parts).copied() else {
342            return false;
343        };
344        stream.update(&self.part_data[at..at + len]);
345        self.decrypted_parts += 1;
346        self.has_parts_to_decrypt()
347    }
348
349    /// Whether this receiver's parts are decrypted at assembly, and so could
350    /// be fed to a decryption elsewhere ahead of it.
351    pub fn decrypts(&self) -> bool {
352        self.flags.encrypted && self.status < ResourceStatus::Assembling
353    }
354
355    /// Set up a fresh decryption as [`Self::decrypt_next_part`] would, for
356    /// parts fed to it elsewhere, in order from the first.
357    pub fn prepare_decrypt_stream(&self, stream: &mut TokenDecryptStream) {
358        // The plaintext of an uncompressed resource is the data its hash
359        // covers, hashed alongside the tag's MAC.
360        if !self.flags.compressed {
361            stream.hash_plaintext();
362        }
363    }
364
365    /// The held parts from index `from` up to the first one missing: what a
366    /// decryption fed elsewhere takes next, in order.
367    pub fn parts_in_order_from(&self, from: usize) -> impl Iterator<Item = &[u8]> + '_ {
368        let end = (self.consecutive_completed_height + 1).max(0) as usize;
369        self.part_spans
370            .get(from..end.max(from))
371            .unwrap_or(&[])
372            .iter()
373            .map_while(|span| span.map(|(at, len)| &self.part_data[at..at + len]))
374    }
375
376    /// Hand back a decryption fed the first `parts` held parts elsewhere
377    /// ([`Self::prepare_decrypt_stream`], [`Self::parts_in_order_from`]):
378    /// assembly feeds it only the rest. Ignored if this receiver already
379    /// decrypted ahead itself.
380    pub fn resume_decrypt(&mut self, stream: Box<TokenDecryptStream>, parts: usize) {
381        if self.decrypt.is_none() && self.decrypted_parts == 0 && parts <= self.part_spans.len() {
382            self.decrypt = Some(stream);
383            self.decrypted_parts = parts;
384        }
385    }
386
387    /// [`Self::decrypt_next_part`] for held parts until about `budget` bytes
388    /// are done; at least one part is. Returns whether more held parts wait.
389    pub fn decrypt_held_parts(
390        &mut self,
391        start: impl FnOnce() -> Option<TokenDecryptStream>,
392        budget: usize,
393    ) -> bool {
394        let mut done = self.next_part_len();
395        if !self.decrypt_next_part(start) {
396            return false;
397        }
398        while done < budget {
399            done += self.next_part_len();
400            // The decryption has started: `start` is not called again.
401            if !self.decrypt_next_part(|| None) {
402                return false;
403            }
404        }
405        self.has_parts_to_decrypt()
406    }
407
408    fn next_part_len(&self) -> usize {
409        match self.part_spans.get(self.decrypted_parts) {
410            Some(Some((_, len))) => *len,
411            _ => 0,
412        }
413    }
414
415    /// Receive a part. Matches by map hash and stores it.
416    pub fn receive_part(&mut self, part_data: &[u8], now: f64) -> Vec<ResourceAction> {
417        let mut actions = Vec::new();
418        self.receive_part_into(part_data, now, &mut actions);
419        actions
420    }
421
422    /// [`Self::receive_part`], appending the actions to `out`.
423    pub fn receive_part_into(&mut self, part_data: &[u8], now: f64, out: &mut Vec<ResourceAction>) {
424        if self.status >= ResourceStatus::Assembling {
425            return;
426        }
427
428        self.last_activity = now;
429        self.retries_left = self.max_retries;
430
431        // Update RTT on first part of window
432        if self.req_resp.is_none() {
433            self.req_resp = Some(now);
434            let rtt = now - self.req_sent;
435            self.part_timeout_factor = RESOURCE_PART_TIMEOUT_FACTOR_AFTER_RTT;
436
437            if self.rtt.is_none() {
438                self.rtt = Some(rtt);
439            } else if let Some(current_rtt) = self.rtt {
440                if rtt < current_rtt {
441                    self.rtt = Some(f64::max(current_rtt - current_rtt * 0.05, rtt));
442                } else {
443                    self.rtt = Some(f64::min(current_rtt + current_rtt * 0.05, rtt));
444                }
445            }
446
447            if rtt > 0.0 {
448                let req_resp_cost = part_data.len() + self.req_sent_bytes;
449                self.req_resp_rtt_rate = req_resp_cost as f64 / rtt;
450                self.window.update_req_resp_rate(self.req_resp_rtt_rate);
451            }
452        }
453
454        self.status = ResourceStatus::Transferring;
455
456        // Compute map hash for this part
457        let part_hash = map_hash(part_data, &self.random_hash);
458
459        // Align with request_next(): the completed height itself is not part
460        // of the outstanding window, which starts at the following index.
461        let search_start = (self.consecutive_completed_height + 1) as usize;
462        let mut matched = false;
463        let search_end = core::cmp::min(search_start + self.window.window, self.total_parts);
464        for i in search_start..search_end {
465            if let Some(h) = self.hash_at(i) {
466                if *h == part_hash {
467                    if !self.has_part(i) {
468                        self.store_part(i, part_data);
469                        self.rtt_rxd_bytes += part_data.len();
470                        self.received_count += 1;
471                        self.outstanding_parts = self.outstanding_parts.saturating_sub(1);
472
473                        // Walk forward to extend consecutive height
474                        let mut cp = (self.consecutive_completed_height + 1) as usize;
475                        while cp < self.total_parts && self.has_part(cp) {
476                            self.consecutive_completed_height = cp as isize;
477                            cp += 1;
478                        }
479
480                        matched = true;
481                    }
482                    break;
483                }
484            }
485        }
486
487        // Check if all parts received
488        if self.received_count == self.total_parts {
489            out.push(ResourceAction::ProgressUpdate {
490                received: self.received_count,
491                total: self.total_parts,
492            });
493            // Assembly will be triggered by caller
494            return;
495        }
496
497        if matched {
498            out.push(ResourceAction::ProgressUpdate {
499                received: self.received_count,
500                total: self.total_parts,
501            });
502        }
503
504        // Request next window when outstanding is 0
505        if self.outstanding_parts == 0 && self.received_count < self.total_parts {
506            // Window complete — grow
507            self.window.on_window_complete();
508
509            // Update data rate
510            if self.req_sent > 0.0 {
511                let rtt = now - self.req_sent;
512                let req_transferred = self.rtt_rxd_bytes - self.rtt_rxd_bytes_at_part_req;
513                if rtt > 0.0 {
514                    self.req_data_rtt_rate = req_transferred as f64 / rtt;
515                    self.rtt_rxd_bytes_at_part_req = self.rtt_rxd_bytes;
516                    self.window.update_data_rate(self.req_data_rtt_rate);
517                }
518            }
519
520            self.request_next_into(now, out);
521        }
522    }
523
524    /// Handle a hashmap update packet.
525    ///
526    /// HMU format: [resource_hash: 32 bytes][msgpack([segment, hashmap])]
527    pub fn handle_hashmap_update(&mut self, hmu_data: &[u8], now: f64) -> Vec<ResourceAction> {
528        if self.status >= ResourceStatus::Assembling || !self.waiting_for_hmu {
529            return vec![];
530        }
531
532        if hmu_data.len() < 32 || hmu_data[..32] != self.resource_hash {
533            return vec![];
534        }
535        if hmu_data.len() == 32 {
536            return self.cancel();
537        }
538
539        let payload = &hmu_data[32..];
540        let value = match crate::msgpack::unpack_exact(payload) {
541            Ok(v) => v,
542            Err(_) => return self.cancel(),
543        };
544
545        let arr = match value.as_array() {
546            Some(a) if a.len() == 2 => a,
547            _ => return self.cancel(),
548        };
549
550        let segment = match arr[0].as_uint() {
551            Some(s) => match usize::try_from(s) {
552                Ok(segment) => segment,
553                Err(_) => return self.cancel(),
554            },
555            None => return self.cancel(),
556        };
557
558        let hashmap_bytes = match arr[1].as_bin() {
559            Some(b) => b,
560            None => return self.cancel(),
561        };
562
563        if hashmap_bytes.is_empty() || !hashmap_bytes.len().is_multiple_of(RESOURCE_MAPHASH_LEN) {
564            return self.cancel();
565        }
566
567        let seg_len = RESOURCE_HASHMAP_MAX_LEN;
568        let segment_start = match segment.checked_mul(seg_len) {
569            Some(start) if start < self.total_parts => start,
570            _ => return self.cancel(),
571        };
572        let num_hashes = hashmap_bytes.len() / RESOURCE_MAPHASH_LEN;
573        if num_hashes > seg_len || segment_start.saturating_add(num_hashes) > self.total_parts {
574            return self.cancel();
575        }
576
577        self.last_activity = now;
578        self.retries_left = self.max_retries;
579        self.status = ResourceStatus::Transferring;
580
581        // Populate hashmap slots
582        for i in 0..num_hashes {
583            let idx = segment_start + i;
584            let start = i * RESOURCE_MAPHASH_LEN;
585            let end = start + RESOURCE_MAPHASH_LEN;
586            if self.hash_at(idx).is_none() {
587                self.hashmap_height += 1;
588            }
589            let mut h = [0u8; RESOURCE_MAPHASH_LEN];
590            h.copy_from_slice(&hashmap_bytes[start..end]);
591            self.store_hash(idx, h);
592        }
593
594        self.waiting_for_hmu = false;
595        self.request_next(now)
596    }
597
598    /// Build and return request for next window of parts.
599    pub fn request_next(&mut self, now: f64) -> Vec<ResourceAction> {
600        let mut actions = Vec::new();
601        self.request_next_into(now, &mut actions);
602        actions
603    }
604
605    /// [`Self::request_next`], appending the request to `out`. The request is
606    /// written into one buffer sized for the window.
607    pub fn request_next_into(&mut self, now: f64, out: &mut Vec<ResourceAction>) {
608        if self.status >= ResourceStatus::Assembling {
609            return;
610        }
611        if self.status == ResourceStatus::Failed || self.waiting_for_hmu {
612            return;
613        }
614
615        self.outstanding_parts = 0;
616        let mut hashmap_exhausted = RESOURCE_HASHMAP_IS_NOT_EXHAUSTED;
617        // [exhausted flag][last map hash, if exhausted][resource hash][requested hashes]
618        let mut request_data = Vec::with_capacity(
619            1 + RESOURCE_MAPHASH_LEN
620                + self.resource_hash.len()
621                + self.window.window * RESOURCE_MAPHASH_LEN,
622        );
623        request_data.push(hashmap_exhausted);
624        let hashes_at = request_data.len();
625
626        if self.hash_at(0).is_none() {
627            // The advertisement carried no hashmap because it would not fit
628            // the sender's link MDU. No part can be requested before the
629            // sender delivers the first segment.
630            hashmap_exhausted = RESOURCE_HASHMAP_IS_EMPTY;
631        } else {
632            let pn_start = (self.consecutive_completed_height + 1) as usize;
633            let search_end = core::cmp::min(pn_start + self.window.window, self.total_parts);
634            let mut i = 0;
635
636            for pn in pn_start..search_end {
637                if !self.has_part(pn) {
638                    match self.hash_at(pn) {
639                        Some(h) => {
640                            request_data.extend_from_slice(h);
641                            self.outstanding_parts += 1;
642                            i += 1;
643                        }
644                        None => {
645                            hashmap_exhausted = RESOURCE_HASHMAP_IS_EXHAUSTED;
646                        }
647                    }
648                }
649                if i >= self.window.window || hashmap_exhausted == RESOURCE_HASHMAP_IS_EXHAUSTED {
650                    break;
651                }
652            }
653        }
654
655        request_data[0] = hashmap_exhausted;
656        // The requested hashes were written first; the fields that precede
657        // them on the wire go in front.
658        let mut prefix = [0u8; RESOURCE_MAPHASH_LEN + 32];
659        let mut prefix_len = 0;
660        if hashmap_exhausted == RESOURCE_HASHMAP_IS_EXHAUSTED {
661            // Append last known map hash
662            let last_hash = if self.hashmap_height > 0 {
663                self.hash_at(self.hashmap_height - 1).copied()
664            } else {
665                None
666            };
667            prefix[..RESOURCE_MAPHASH_LEN]
668                .copy_from_slice(&last_hash.unwrap_or([0u8; RESOURCE_MAPHASH_LEN]));
669            prefix_len = RESOURCE_MAPHASH_LEN;
670            self.waiting_for_hmu = true;
671        }
672        if hashmap_exhausted == RESOURCE_HASHMAP_IS_EMPTY {
673            self.waiting_for_hmu = true;
674        }
675        request_data.splice(
676            hashes_at..hashes_at,
677            prefix[..prefix_len]
678                .iter()
679                .chain(self.resource_hash.iter())
680                .copied(),
681        );
682
683        self.last_activity = now;
684        self.req_sent = now;
685        self.req_sent_bytes = request_data.len();
686        self.req_resp = None;
687
688        out.push(ResourceAction::SendRequest(request_data));
689    }
690
691    /// Assemble received parts, decrypt, decompress, verify hash.
692    pub fn assemble(
693        &mut self,
694        decrypt_fn: &PartsDecryptFn<'_>,
695        compressor: &dyn Compressor,
696    ) -> Vec<ResourceAction> {
697        match self.prepare_data(decrypt_fn) {
698            Ok(data) => self.finish_data(data.run(compressor)),
699            Err(actions) => actions,
700        }
701    }
702
703    /// Authenticate and decrypt the parts, then transfer owned plaintext to an assembly job.
704    /// Keep this receiver alive until completion or cancellation. A pending attempt
705    /// ignores duplicate parts and ordinary part-receive timeouts.
706    pub fn prepare_assembly(
707        &mut self,
708        decrypt_fn: &PartsDecryptFn<'_>,
709    ) -> Result<ResourceAssembly, Vec<ResourceAction>> {
710        let data = self.prepare_data(decrypt_fn)?;
711        let id = AssemblyId::new();
712        self.assembly_id = Some(id.clone());
713        Ok(ResourceAssembly { id, data })
714    }
715
716    pub fn assembly_id(&self) -> Option<&AssemblyId> {
717        (self.status == ResourceStatus::Assembling)
718            .then_some(self.assembly_id.as_ref())
719            .flatten()
720    }
721
722    /// Publish a result only for the still-pending attempt that produced it.
723    /// Results from another receiver or a cancelled attempt are discarded.
724    pub fn complete_assembly(&mut self, result: AssemblyResult) -> Vec<ResourceAction> {
725        if self.assembly_id() != Some(&result.id) {
726            return Vec::new();
727        }
728        self.assembly_id = None;
729        self.finish_data(result.result)
730    }
731
732    /// Fail a pending worker attempt without blaming the peer for a local failure.
733    pub fn fail_assembly(&mut self, id: &AssemblyId) -> Vec<ResourceAction> {
734        if self.assembly_id() != Some(id) {
735            return Vec::new();
736        }
737        self.assembly_id = None;
738        let mut actions = self.cancel();
739        actions.push(ResourceAction::Failed(ResourceError::InvalidState));
740        actions
741    }
742
743    fn finish_data(
744        &mut self,
745        result: Result<Vec<ResourceAction>, ResourceError>,
746    ) -> Vec<ResourceAction> {
747        match result {
748            Ok(actions) => {
749                self.status = ResourceStatus::Complete;
750                actions
751            }
752            Err(error) => self.corrupt_actions(error),
753        }
754    }
755
756    fn prepare_data(
757        &mut self,
758        decrypt_fn: &PartsDecryptFn<'_>,
759    ) -> Result<AssemblyData, Vec<ResourceAction>> {
760        if self.status >= ResourceStatus::Assembling || self.received_count != self.total_parts {
761            return Err(vec![ResourceAction::Failed(ResourceError::InvalidState)]);
762        }
763
764        self.status = ResourceStatus::Assembling;
765
766        // Part storage grows as parts arrive; a short vector means a part is missing.
767        if self.part_spans.len() != self.total_parts {
768            self.status = ResourceStatus::Failed;
769            return Err(vec![ResourceAction::Failed(ResourceError::InvalidState)]);
770        }
771
772        // Every part must be held; size from the parts actually held, not the
773        // advertised transfer size.
774        let mut stream_len = 0usize;
775        for span in &self.part_spans {
776            match span {
777                Some((_, len)) => match stream_len.checked_add(*len) {
778                    Some(len) => stream_len = len,
779                    None => {
780                        self.status = ResourceStatus::Failed;
781                        return Err(vec![ResourceAction::Failed(ResourceError::TooLarge)]);
782                    }
783                },
784                None => {
785                    self.status = ResourceStatus::Failed;
786                    return Err(vec![ResourceAction::Failed(ResourceError::InvalidState)]);
787                }
788            }
789        }
790        // Assembly is terminal for part reception. The parts are released once
791        // authentication has produced the plaintext, including on failure.
792        let part_data = core::mem::take(&mut self.part_data);
793        let part_spans = core::mem::take(&mut self.part_spans);
794        let parts = || {
795            part_spans
796                .iter()
797                .flatten()
798                .map(|&(start, len)| &part_data[start..start + len])
799        };
800        self.hashmap = Vec::new();
801        let mut data_state = None;
802
803        // The plaintext starts with the random prefix, which is dropped here.
804        let plaintext = if self.flags.encrypted {
805            let decrypted = if let Some(mut stream) = self.decrypt.take() {
806                // Parts decrypted ahead are not fed again.
807                for data in parts().skip(self.decrypted_parts) {
808                    stream.update(data);
809                }
810                stream
811                    .finish_hashed()
812                    .map_err(|_| ())
813                    .map(|(plaintext, hash)| {
814                        data_state = hash.map(Box::new);
815                        plaintext
816                    })
817            } else {
818                // Each part is authenticated and decrypted in turn, without
819                // joining the ciphertext first.
820                let chunks: Vec<&[u8]> = parts().collect();
821                decrypt_fn(&chunks, RESOURCE_RANDOM_HASH_SIZE)
822            };
823            drop(part_spans);
824            drop(part_data);
825            match decrypted {
826                Ok(d) => d,
827                Err(_) => {
828                    self.status = ResourceStatus::Failed;
829                    return Err(vec![ResourceAction::Failed(
830                        ResourceError::DecryptionFailed,
831                    )]);
832                }
833            }
834        } else {
835            let Some(len) = stream_len.checked_sub(RESOURCE_RANDOM_HASH_SIZE) else {
836                return Err(self.corrupt_actions(ResourceError::InvalidPart));
837            };
838            let mut plaintext = Vec::with_capacity(len);
839            let mut skip = RESOURCE_RANDOM_HASH_SIZE;
840            for data in parts() {
841                let from = skip.min(data.len());
842                plaintext.extend_from_slice(&data[from..]);
843                skip -= from;
844            }
845            plaintext
846        };
847
848        let (Ok(resource_hash), Ok(random_hash)) = (
849            self.resource_hash.as_slice().try_into(),
850            self.random_hash.as_slice().try_into(),
851        ) else {
852            return Err(self.corrupt_actions(ResourceError::InvalidAdvertisement));
853        };
854        Ok(AssemblyData {
855            plaintext,
856            data_state,
857            resource_hash,
858            random_hash,
859            compressed: self.flags.compressed,
860            metadata: self.has_metadata && self.segment_index == 1,
861            app_flag: self.flags.app_flag,
862            limit: self.max_decompressed_size,
863        })
864    }
865
866    /// Handle cancel from sender (RESOURCE_ICL).
867    pub fn handle_cancel(&mut self) -> Vec<ResourceAction> {
868        if self.status < ResourceStatus::Complete {
869            self.status = ResourceStatus::Failed;
870            return vec![ResourceAction::Failed(ResourceError::Rejected)];
871        }
872        vec![]
873    }
874
875    /// Periodic tick. Checks for timeouts.
876    pub fn tick(
877        &mut self,
878        now: f64,
879        decrypt_fn: &PartsDecryptFn<'_>,
880        compressor: &dyn Compressor,
881    ) -> Vec<ResourceAction> {
882        if self.status >= ResourceStatus::Assembling {
883            return vec![];
884        }
885
886        if self.status == ResourceStatus::Transferring {
887            // Check if all parts received — trigger assembly
888            if self.received_count == self.total_parts {
889                return self.assemble(decrypt_fn, compressor);
890            }
891
892            // Compute timeout
893            let eifr = self.compute_eifr();
894            let retries_used = self.max_retries - self.retries_left;
895            let extra_wait = retries_used as f64 * RESOURCE_PER_RETRY_DELAY;
896            let expected_hmu_wait =
897                if eifr > 0.0 && (self.waiting_for_hmu || self.outstanding_parts == 0) {
898                    (self.sdu as f64 * 8.0 * RESOURCE_HMU_WAIT_FACTOR) / eifr
899                } else {
900                    0.0
901                };
902            let expected_tof = if self.outstanding_parts > 0 && eifr > 0.0 {
903                (self.outstanding_parts as f64 * self.sdu as f64 * 8.0) / eifr
904            } else if eifr > 0.0 {
905                (3.0 * self.sdu as f64) / eifr
906            } else {
907                10.0 // fallback
908            };
909
910            let sleep_time = self.last_activity
911                + self.part_timeout_factor * expected_tof
912                + expected_hmu_wait
913                + RESOURCE_RETRY_GRACE_TIME
914                + extra_wait;
915
916            if now > sleep_time {
917                if self.retries_left > 0 {
918                    // Timeout — shrink window, retry
919                    self.window.on_timeout();
920                    self.retries_left -= 1;
921                    self.waiting_for_hmu = false;
922                    return self.request_next(now);
923                } else {
924                    self.status = ResourceStatus::Failed;
925                    return vec![ResourceAction::Failed(ResourceError::MaxRetriesExceeded)];
926                }
927            }
928        }
929
930        vec![]
931    }
932
933    /// Compute EIFR (expected inflight rate) and update self.eifr.
934    fn compute_eifr(&mut self) -> f64 {
935        let eifr = if self.req_data_rtt_rate > 0.0 {
936            self.req_data_rtt_rate * 8.0
937        } else if let Some(prev) = self.previous_eifr {
938            prev
939        } else {
940            // Fallback: use link_rtt as establishment cost estimate
941            let rtt = self.rtt.unwrap_or(self.link_rtt);
942            if rtt > 0.0 {
943                (self.sdu as f64 * 8.0) / rtt
944            } else {
945                10000.0
946            }
947        };
948        self.eifr = Some(eifr);
949        eifr
950    }
951
952    /// Get current progress as (received, total).
953    pub fn progress(&self) -> (usize, usize) {
954        (self.received_count, self.total_parts)
955    }
956
957    /// Get window and EIFR for passing to next transfer.
958    pub fn get_transfer_state(&self) -> (usize, Option<f64>) {
959        (self.window.window, self.eifr)
960    }
961}
962
963#[cfg(test)]
964mod tests {
965    use super::*;
966    use crate::buffer::types::{Compressor, DecompressError, NoopCompressor};
967    use crate::resource::advertisement::ResourceAdvertisement;
968    use crate::resource::sender::ResourceSender;
969
970    fn identity_encrypt(data: &[u8]) -> Vec<u8> {
971        data.to_vec()
972    }
973
974    fn identity_decrypt(parts: &[&[u8]], skip: usize) -> Result<Vec<u8>, ()> {
975        let joined = parts.concat();
976        joined.get(skip..).map(<[u8]>::to_vec).ok_or(())
977    }
978
979    struct ExpandingCompressor;
980
981    impl Compressor for ExpandingCompressor {
982        fn compress(&self, data: &[u8]) -> Option<Vec<u8>> {
983            Some(data[..data.len() / 2].to_vec())
984        }
985
986        fn decompress(&self, data: &[u8]) -> Option<Vec<u8>> {
987            self.decompress_bounded(data, usize::MAX).ok()
988        }
989
990        fn decompress_bounded(
991            &self,
992            data: &[u8],
993            max_output_size: usize,
994        ) -> Result<Vec<u8>, DecompressError> {
995            let mut out = data.to_vec();
996            out.extend_from_slice(data);
997            if out.len() > max_output_size {
998                return Err(DecompressError::TooLarge);
999            }
1000            Ok(out)
1001        }
1002    }
1003
1004    fn base_timeout(receiver: &ResourceReceiver, eifr: f64) -> f64 {
1005        let expected_tof = if receiver.outstanding_parts > 0 {
1006            (receiver.outstanding_parts as f64 * receiver.sdu as f64 * 8.0) / eifr
1007        } else {
1008            (3.0 * receiver.sdu as f64) / eifr
1009        };
1010
1011        receiver.last_activity
1012            + receiver.part_timeout_factor * expected_tof
1013            + RESOURCE_RETRY_GRACE_TIME
1014    }
1015
1016    fn hmu_timeout(receiver: &ResourceReceiver, eifr: f64) -> f64 {
1017        let expected_hmu_wait = (receiver.sdu as f64 * 8.0 * RESOURCE_HMU_WAIT_FACTOR) / eifr;
1018        base_timeout(receiver, eifr) + expected_hmu_wait
1019    }
1020
1021    fn make_sender_receiver() -> (ResourceSender, ResourceReceiver) {
1022        let mut rng = rns_crypto::FixedRng::new(&[0x42; 64]);
1023        let data = b"Hello, Resource Transfer!";
1024
1025        let sender = ResourceSender::new(
1026            data,
1027            None,
1028            RESOURCE_SDU,
1029            &identity_encrypt,
1030            &NoopCompressor,
1031            &mut rng,
1032            1000.0,
1033            false,
1034            false,
1035            None,
1036            1,
1037            1,
1038            None,
1039            0.5,
1040            6.0,
1041        )
1042        .unwrap();
1043
1044        let adv_data = sender.get_advertisement(0);
1045        let receiver =
1046            ResourceReceiver::from_advertisement(&adv_data, RESOURCE_SDU, 0.5, 1000.0, None, None)
1047                .unwrap();
1048
1049        (sender, receiver)
1050    }
1051
1052    fn ready_receiver() -> ResourceReceiver {
1053        let (mut sender, mut receiver) = make_sender_receiver();
1054        for action in receiver.accept(1001.0) {
1055            if let ResourceAction::SendRequest(request) = action {
1056                for action in sender.handle_request(&request, 1002.0) {
1057                    if let ResourceAction::SendPart(part) = action {
1058                        receiver.receive_part(&part, 1003.0);
1059                    }
1060                }
1061            }
1062        }
1063        assert_eq!(receiver.received_count, receiver.total_parts);
1064        receiver
1065    }
1066
1067    /// A receiver holding every part of a `len`-byte resource encrypted
1068    /// with `token`, sent in parts of `sdu` bytes.
1069    fn encrypted_receiver(
1070        token: &rns_crypto::token::Token,
1071        len: usize,
1072        sdu: usize,
1073    ) -> ResourceReceiver {
1074        let mut rng = rns_crypto::FixedRng::new(&[0x42; 64]);
1075        let data: Vec<u8> = (0..len).map(|i| (i * 31 + 7) as u8).collect();
1076        let encrypt = |plaintext: &[u8]| token.encrypt_with_iv(plaintext, &[0x17; 16]);
1077        let mut sender = ResourceSender::new(
1078            &data,
1079            None,
1080            sdu,
1081            &encrypt,
1082            &NoopCompressor,
1083            &mut rng,
1084            1000.0,
1085            false,
1086            false,
1087            None,
1088            1,
1089            1,
1090            None,
1091            0.5,
1092            6.0,
1093        )
1094        .unwrap();
1095        let adv = sender.get_advertisement(0);
1096        let mut receiver =
1097            ResourceReceiver::from_advertisement(&adv, sdu, 0.5, 1000.0, None, None).unwrap();
1098        let mut actions = receiver.accept(1001.0);
1099        while let Some(action) = actions.pop() {
1100            if let ResourceAction::SendRequest(request) = action {
1101                for action in sender.handle_request(&request, 1002.0) {
1102                    if let ResourceAction::SendPart(part) = action {
1103                        actions.extend(receiver.receive_part(&part, 1003.0));
1104                    }
1105                }
1106            }
1107        }
1108        assert_eq!(receiver.received_count, receiver.total_parts);
1109        receiver
1110    }
1111
1112    #[test]
1113    fn parts_decrypted_ahead_assemble_as_the_parts_decrypted_at_once() {
1114        let token = rns_crypto::token::Token::new(&[0x5A; 64]).unwrap();
1115        let decrypt =
1116            |parts: &[&[u8]], skip: usize| token.decrypt_chunks(parts, skip).map_err(|_| ());
1117        for (len, sdu) in [(10usize, 100usize), (5000, 100), (5000, 97), (20_000, 464)] {
1118            let expected = encrypted_receiver(&token, len, sdu).assemble(&decrypt, &NoopCompressor);
1119            assert!(matches!(expected[1], ResourceAction::DataReceived { .. }));
1120            let total = encrypted_receiver(&token, len, sdu).total_parts;
1121            for ahead in [1, total / 2, total.saturating_sub(1), total] {
1122                if ahead == 0 {
1123                    continue;
1124                }
1125                let mut receiver = encrypted_receiver(&token, len, sdu);
1126                for _ in 0..ahead {
1127                    receiver.decrypt_next_part(|| {
1128                        Some(token.decrypt_stream(RESOURCE_RANDOM_HASH_SIZE))
1129                    });
1130                }
1131                assert_eq!(receiver.has_parts_to_decrypt(), ahead < total);
1132                let actions = receiver.assemble(
1133                    &|_, _| panic!("parts decrypted ahead were decrypted again"),
1134                    &NoopCompressor,
1135                );
1136                assert_eq!(actions, expected, "len {len} sdu {sdu} ahead {ahead}");
1137            }
1138            // In budgeted batches, as the driver's idle work does.
1139            for budget in [1, 250, 1000, usize::MAX] {
1140                let mut receiver = encrypted_receiver(&token, len, sdu);
1141                let mut jobs = 0;
1142                while receiver.decrypt_held_parts(
1143                    || Some(token.decrypt_stream(RESOURCE_RANDOM_HASH_SIZE)),
1144                    budget,
1145                ) {
1146                    jobs += 1;
1147                }
1148                assert!(!receiver.has_parts_to_decrypt());
1149                assert!(jobs < total.max(1));
1150                let actions = receiver.assemble(
1151                    &|_, _| panic!("parts decrypted ahead were decrypted again"),
1152                    &NoopCompressor,
1153                );
1154                assert_eq!(actions, expected, "len {len} sdu {sdu} budget {budget}");
1155            }
1156        }
1157    }
1158
1159    #[test]
1160    fn parts_fed_to_a_decryption_elsewhere_assemble_as_at_once() {
1161        let token = rns_crypto::token::Token::new(&[0x5A; 64]).unwrap();
1162        let decrypt =
1163            |parts: &[&[u8]], skip: usize| token.decrypt_chunks(parts, skip).map_err(|_| ());
1164        for (len, sdu) in [(10usize, 100usize), (5000, 100), (5000, 97), (20_000, 464)] {
1165            let expected = encrypted_receiver(&token, len, sdu).assemble(&decrypt, &NoopCompressor);
1166            let total = encrypted_receiver(&token, len, sdu).total_parts;
1167            for fed in [0, 1, total / 2, total] {
1168                let mut receiver = encrypted_receiver(&token, len, sdu);
1169                assert!(receiver.decrypts());
1170                let mut stream = token.decrypt_stream(RESOURCE_RANDOM_HASH_SIZE);
1171                receiver.prepare_decrypt_stream(&mut stream);
1172                // Fed in order from the first, as a decrypting thread is.
1173                let parts: Vec<Vec<u8>> = receiver
1174                    .parts_in_order_from(0)
1175                    .map(<[u8]>::to_vec)
1176                    .collect();
1177                assert_eq!(parts.len(), total);
1178                assert_eq!(receiver.parts_in_order_from(total).count(), 0);
1179                for part in &parts[..fed] {
1180                    stream.update(part);
1181                }
1182                receiver.resume_decrypt(Box::new(stream), fed);
1183                let actions = receiver.assemble(
1184                    &|_, _| panic!("a decryption handed back is finished, not redone"),
1185                    &NoopCompressor,
1186                );
1187                assert_eq!(actions, expected, "len {len} sdu {sdu} fed {fed}");
1188            }
1189        }
1190    }
1191
1192    #[test]
1193    fn parts_decrypted_ahead_under_another_key_fail_at_assembly() {
1194        let token = rns_crypto::token::Token::new(&[0x5A; 64]).unwrap();
1195        let other = rns_crypto::token::Token::new(&[0x5B; 64]).unwrap();
1196        let mut receiver = encrypted_receiver(&token, 5000, 100);
1197        receiver.decrypt_next_part(|| Some(other.decrypt_stream(RESOURCE_RANDOM_HASH_SIZE)));
1198        let actions = receiver.assemble(&|_, _| panic!("decrypted again"), &NoopCompressor);
1199        assert!(actions.contains(&ResourceAction::Failed(ResourceError::DecryptionFailed)));
1200        assert!(!actions
1201            .iter()
1202            .any(|a| matches!(a, ResourceAction::DataReceived { .. })));
1203    }
1204
1205    #[test]
1206    fn assembly_handoff_matches_synchronous_actions_and_releases_parts() {
1207        let mut receiver = ready_receiver();
1208        let job = receiver.prepare_assembly(&identity_decrypt).unwrap();
1209        assert!(receiver.part_spans.is_empty() && receiver.hashmap.is_empty());
1210        receiver.waiting_for_hmu = true;
1211        assert!(receiver
1212            .handle_hashmap_update(&receiver.resource_hash.clone(), 1004.0)
1213            .is_empty());
1214        assert!(receiver.accept(1004.0).is_empty());
1215        assert!(receiver.request_next(1004.0).is_empty());
1216        assert_eq!(receiver.assembly_id(), Some(job.id()));
1217        assert!(receiver.receive_part(b"duplicate", 1004.0).is_empty());
1218        assert!(receiver
1219            .tick(
1220                100000.0,
1221                &|_, _| panic!("pending assembly decrypted again"),
1222                &NoopCompressor
1223            )
1224            .is_empty());
1225        assert!(receiver.prepare_assembly(&identity_decrypt).is_err());
1226        let actions = receiver.complete_assembly(job.run(&NoopCompressor));
1227        let expected = ready_receiver().assemble(&identity_decrypt, &NoopCompressor);
1228        assert_eq!(format!("{actions:?}"), format!("{expected:?}"));
1229        assert_eq!(receiver.status, ResourceStatus::Complete);
1230        assert!(receiver.assembly_id().is_none());
1231    }
1232
1233    #[test]
1234    fn assembly_result_requires_original_live_attempt_even_for_same_hash() {
1235        let mut first = ready_receiver();
1236        let mut second = ready_receiver();
1237        assert_eq!(first.resource_hash, second.resource_hash);
1238        let first_job = first.prepare_assembly(&identity_decrypt).unwrap();
1239        let second_job = second.prepare_assembly(&identity_decrypt).unwrap();
1240        assert_ne!(first_job.id(), second_job.id());
1241        assert!(first
1242            .complete_assembly(second_job.run(&NoopCompressor))
1243            .is_empty());
1244        first.cancel();
1245        assert!(first
1246            .complete_assembly(first_job.run(&NoopCompressor))
1247            .is_empty());
1248        assert_eq!(first.status, ResourceStatus::Failed);
1249    }
1250
1251    #[test]
1252    fn assembly_local_failure_and_authentication_failure_cannot_publish_success() {
1253        let mut receiver = ready_receiver();
1254        assert!(receiver.prepare_assembly(&|_, _| Err(())).is_err());
1255        assert!(receiver.assembly_id().is_none());
1256        assert!(receiver.part_spans.is_empty() && receiver.hashmap.is_empty());
1257        assert!(receiver
1258            .prepare_assembly(&|_, _| panic!("failed assembly retried"))
1259            .is_err());
1260        let mut receiver = ready_receiver();
1261        let job = receiver.prepare_assembly(&identity_decrypt).unwrap();
1262        let actions = receiver.fail_assembly(job.id());
1263        assert!(matches!(
1264            actions.as_slice(),
1265            [
1266                ResourceAction::SendCancelReceiver(_),
1267                ResourceAction::Failed(ResourceError::InvalidState)
1268            ]
1269        ));
1270        assert!(receiver.fail_assembly(job.id()).is_empty());
1271        assert!(receiver
1272            .complete_assembly(job.run(&NoopCompressor))
1273            .is_empty());
1274    }
1275
1276    #[test]
1277    fn test_assembly_joins_actual_parts_independent_of_advertised_size() {
1278        let (mut sender, mut receiver) = make_sender_receiver();
1279        let mut stream = Vec::new();
1280        for action in receiver.accept(1001.0) {
1281            if let ResourceAction::SendRequest(request) = action {
1282                for response in sender.handle_request(&request, 1002.0) {
1283                    if let ResourceAction::SendPart(part) = response {
1284                        stream.extend_from_slice(&part);
1285                    }
1286                }
1287            }
1288        }
1289        // Exercise uneven parts and a misleading size without allocating from it.
1290        receiver.part_data = stream.to_vec();
1291        receiver.part_spans = (0..stream.len())
1292            .step_by(7)
1293            .map(|start| Some((start, (stream.len() - start).min(7))))
1294            .collect();
1295        receiver.total_parts = receiver.part_spans.len();
1296        receiver.received_count = receiver.total_parts;
1297        receiver.transfer_size = u64::MAX;
1298        let actions = receiver.assemble(
1299            &|parts, skip| {
1300                // The parts reach decryption as held, without being joined.
1301                assert_eq!(parts.len(), stream.len().div_ceil(7));
1302                assert!(parts.iter().all(|part| part.len() <= 7));
1303                assert_eq!(parts.concat(), stream);
1304                identity_decrypt(parts, skip)
1305            },
1306            &NoopCompressor,
1307        );
1308        assert!(actions.iter().any(|action| matches!(action,
1309            ResourceAction::DataReceived { data, .. } if data == b"Hello, Resource Transfer!")));
1310        assert_eq!(receiver.status, ResourceStatus::Complete);
1311    }
1312
1313    #[test]
1314    fn test_assembly_rejects_missing_part_before_decryption() {
1315        let (_, mut receiver) = make_sender_receiver();
1316        receiver.received_count = receiver.total_parts;
1317        let actions = receiver.assemble(
1318            &|_, _| panic!("missing part reached decryption"),
1319            &NoopCompressor,
1320        );
1321        assert!(matches!(
1322            actions.as_slice(),
1323            [ResourceAction::Failed(ResourceError::InvalidState)]
1324        ));
1325        assert_eq!(receiver.status, ResourceStatus::Failed);
1326    }
1327
1328    #[test]
1329    fn test_from_advertisement() {
1330        let (sender, receiver) = make_sender_receiver();
1331        assert_eq!(receiver.total_parts, sender.total_parts());
1332        assert_eq!(receiver.transfer_size, sender.transfer_size as u64);
1333        assert_eq!(receiver.resource_hash, sender.resource_hash.to_vec());
1334        assert!(!receiver.advertisement_packet.is_empty());
1335        assert_eq!(
1336            receiver.max_decompressed_size,
1337            RESOURCE_AUTO_COMPRESS_MAX_SIZE
1338        );
1339    }
1340
1341    #[test]
1342    fn test_from_advertisement_rejects_huge_part_count() {
1343        let adv = ResourceAdvertisement {
1344            transfer_size: 1024,
1345            data_size: 1024,
1346            num_parts: u64::MAX,
1347            resource_hash: vec![0x11; 32],
1348            random_hash: vec![0x22; RESOURCE_RANDOM_HASH_SIZE],
1349            original_hash: vec![0x33; 32],
1350            hashmap: vec![0x44; RESOURCE_MAPHASH_LEN],
1351            flags: AdvFlags {
1352                encrypted: false,
1353                compressed: false,
1354                split: false,
1355                is_request: false,
1356                is_response: false,
1357                has_metadata: false,
1358                app_flag: false,
1359            },
1360            segment_index: 1,
1361            total_segments: 1,
1362            request_id: None,
1363        };
1364
1365        let err = match ResourceReceiver::from_advertisement(
1366            &adv.pack(0),
1367            RESOURCE_SDU,
1368            0.5,
1369            1000.0,
1370            None,
1371            None,
1372        ) {
1373            Ok(_) => panic!("huge advertisement should be rejected"),
1374            Err(err) => err,
1375        };
1376
1377        assert_eq!(err, ResourceError::PartCountMismatch);
1378    }
1379
1380    fn sized_advertisement(transfer_size: u64, num_parts: u64) -> ResourceAdvertisement {
1381        ResourceAdvertisement {
1382            transfer_size,
1383            data_size: transfer_size,
1384            num_parts,
1385            resource_hash: vec![0x11; 32],
1386            random_hash: vec![0x22; RESOURCE_RANDOM_HASH_SIZE],
1387            original_hash: vec![0x33; 32],
1388            hashmap: vec![0x44; RESOURCE_MAPHASH_LEN * num_parts.min(512) as usize],
1389            flags: AdvFlags {
1390                encrypted: true,
1391                compressed: false,
1392                split: false,
1393                is_request: false,
1394                is_response: false,
1395                has_metadata: false,
1396                app_flag: false,
1397            },
1398            segment_index: 1,
1399            total_segments: 1,
1400            request_id: None,
1401        }
1402    }
1403
1404    fn receive_advertisement(
1405        adv: &ResourceAdvertisement,
1406        sdu: usize,
1407    ) -> Result<ResourceReceiver, ResourceError> {
1408        ResourceReceiver::from_advertisement(&adv.pack(0), sdu, 0.5, 1000.0, None, None)
1409    }
1410
1411    #[test]
1412    fn part_count_above_transfer_size_is_refused() {
1413        let adv = sized_advertisement(1, 140_000);
1414        assert_eq!(
1415            receive_advertisement(&adv, RESOURCE_SDU).err(),
1416            Some(ResourceError::PartCountMismatch)
1417        );
1418        assert_eq!(
1419            receive_advertisement(&adv, 1).err(),
1420            Some(ResourceError::PartCountMismatch)
1421        );
1422    }
1423
1424    #[test]
1425    fn part_count_below_transfer_size_is_refused() {
1426        let sdu = RESOURCE_SDU;
1427        let adv = sized_advertisement(10 * sdu as u64 + 1, 10);
1428        assert_eq!(
1429            receive_advertisement(&adv, sdu).err(),
1430            Some(ResourceError::PartCountMismatch)
1431        );
1432    }
1433
1434    #[test]
1435    fn part_count_matching_transfer_size_is_accepted() {
1436        let sdu = RESOURCE_SDU;
1437        let receiver = receive_advertisement(&sized_advertisement(1, 1), sdu).unwrap();
1438        assert_eq!(receiver.total_parts, 1);
1439
1440        let receiver = receive_advertisement(&sized_advertisement(sdu as u64 + 1, 2), sdu).unwrap();
1441        assert_eq!(receiver.total_parts, 2);
1442    }
1443
1444    #[test]
1445    fn transfer_size_on_sdu_boundary_needs_no_extra_part() {
1446        let sdu = RESOURCE_SDU;
1447        let exact = 3 * sdu as u64;
1448        let receiver = receive_advertisement(&sized_advertisement(exact, 3), sdu).unwrap();
1449        assert_eq!(receiver.total_parts, 3);
1450        assert_eq!(
1451            receive_advertisement(&sized_advertisement(exact, 4), sdu).err(),
1452            Some(ResourceError::PartCountMismatch)
1453        );
1454        assert_eq!(
1455            receive_advertisement(&sized_advertisement(exact, 2), sdu).err(),
1456            Some(ResourceError::PartCountMismatch)
1457        );
1458    }
1459
1460    #[test]
1461    fn split_segment_part_count_follows_segment_transfer_size() {
1462        let sdu = RESOURCE_SDU;
1463        let segment_size = RESOURCE_MAX_EFFICIENT_SIZE as u64 + 61;
1464        let segment_parts = segment_size.div_ceil(sdu as u64);
1465        let mut adv = sized_advertisement(segment_size, segment_parts);
1466        adv.data_size = 3 * RESOURCE_MAX_EFFICIENT_SIZE as u64;
1467        adv.flags.split = true;
1468        adv.segment_index = 2;
1469        adv.total_segments = 3;
1470
1471        let receiver = receive_advertisement(&adv, sdu).unwrap();
1472        assert_eq!(receiver.total_parts as u64, segment_parts);
1473        assert_eq!(receiver.segment_index, 2);
1474
1475        // The logical size of the whole split resource does not set the count.
1476        adv.num_parts = adv.data_size.div_ceil(sdu as u64);
1477        assert_eq!(
1478            receive_advertisement(&adv, sdu).err(),
1479            Some(ResourceError::PartCountMismatch)
1480        );
1481    }
1482
1483    #[test]
1484    fn transfer_size_above_segment_limit_is_refused_up_front() {
1485        let limit = 3 * RESOURCE_MAX_EFFICIENT_SIZE as u64;
1486        let adv = sized_advertisement(limit + 1, (limit + 1).div_ceil(RESOURCE_SDU as u64));
1487        assert_eq!(
1488            receive_advertisement(&adv, RESOURCE_SDU).err(),
1489            Some(ResourceError::InvalidAdvertisement)
1490        );
1491    }
1492
1493    #[test]
1494    fn idle_accepted_advertisement_holds_no_part_storage() {
1495        let sdu = 48;
1496        let limit = 3 * RESOURCE_MAX_EFFICIENT_SIZE as u64;
1497        let parts = limit.div_ceil(sdu as u64);
1498        let mut receiver = receive_advertisement(&sized_advertisement(limit, parts), sdu).unwrap();
1499        assert_eq!(receiver.total_parts as u64, parts);
1500
1501        let actions = receiver.accept(1000.0);
1502        assert!(actions
1503            .iter()
1504            .any(|action| matches!(action, ResourceAction::SendRequest(_))));
1505        assert_eq!(receiver.part_spans.capacity(), 0);
1506        assert_eq!(receiver.part_data.capacity(), 0);
1507        assert!(receiver.hashmap.capacity() <= RESOURCE_HASHMAP_MAX_LEN);
1508        assert_eq!(receiver.hashmap.len(), receiver.hashmap_height);
1509    }
1510
1511    #[test]
1512    fn part_storage_grows_with_received_parts() {
1513        let mut rng = rns_crypto::FixedRng::new(&[0x42; 64]);
1514        let sdu = 8;
1515        let data: Vec<u8> = (0..100u8).collect();
1516        let mut sender = ResourceSender::new(
1517            &data,
1518            None,
1519            sdu,
1520            &identity_encrypt,
1521            &NoopCompressor,
1522            &mut rng,
1523            1000.0,
1524            false,
1525            false,
1526            None,
1527            1,
1528            1,
1529            None,
1530            0.5,
1531            6.0,
1532        )
1533        .unwrap();
1534        let mut receiver = ResourceReceiver::from_advertisement(
1535            &sender.get_advertisement(0),
1536            sdu,
1537            0.5,
1538            1000.0,
1539            None,
1540            None,
1541        )
1542        .unwrap();
1543        assert!(receiver.total_parts > 1);
1544        assert!(receiver.part_spans.is_empty());
1545
1546        let request = receiver
1547            .accept(1000.0)
1548            .into_iter()
1549            .find_map(|action| match action {
1550                ResourceAction::SendRequest(request) => Some(request),
1551                _ => None,
1552            })
1553            .unwrap();
1554        let first_part = sender
1555            .handle_request(&request, 1001.0)
1556            .into_iter()
1557            .find_map(|action| match action {
1558                ResourceAction::SendPart(part) => Some(part),
1559                _ => None,
1560            })
1561            .unwrap();
1562        receiver.receive_part(&first_part, 1002.0);
1563        assert_eq!(receiver.received_count, 1);
1564        assert_eq!(receiver.part_spans.len(), 1);
1565    }
1566
1567    #[test]
1568    fn test_from_advertisement_rejects_oversized_transfer() {
1569        let adv = ResourceAdvertisement {
1570            transfer_size: RESOURCE_AUTO_COMPRESS_MAX_SIZE as u64 + 1,
1571            data_size: 1024,
1572            num_parts: 1,
1573            resource_hash: vec![0x11; 32],
1574            random_hash: vec![0x22; RESOURCE_RANDOM_HASH_SIZE],
1575            original_hash: vec![0x33; 32],
1576            hashmap: vec![0x44; RESOURCE_MAPHASH_LEN],
1577            flags: AdvFlags {
1578                encrypted: false,
1579                compressed: false,
1580                split: false,
1581                is_request: false,
1582                is_response: false,
1583                has_metadata: false,
1584                app_flag: false,
1585            },
1586            segment_index: 1,
1587            total_segments: 1,
1588            request_id: None,
1589        };
1590
1591        let err = match ResourceReceiver::from_advertisement(
1592            &adv.pack(0),
1593            RESOURCE_SDU,
1594            0.5,
1595            1000.0,
1596            None,
1597            None,
1598        ) {
1599            Ok(_) => panic!("oversized advertisement should be rejected"),
1600            Err(err) => err,
1601        };
1602
1603        assert_eq!(err, ResourceError::InvalidAdvertisement);
1604    }
1605
1606    #[test]
1607    fn test_accept() {
1608        let (_, mut receiver) = make_sender_receiver();
1609        let actions = receiver.accept(1000.0);
1610        assert_eq!(receiver.status, ResourceStatus::Transferring);
1611        assert!(!actions.is_empty());
1612        assert!(actions
1613            .iter()
1614            .any(|a| matches!(a, ResourceAction::SendRequest(_))));
1615    }
1616
1617    #[test]
1618    fn test_reject() {
1619        let (_, mut receiver) = make_sender_receiver();
1620        let actions = receiver.reject();
1621        assert_eq!(receiver.status, ResourceStatus::Rejected);
1622        assert!(actions
1623            .iter()
1624            .any(|a| matches!(a, ResourceAction::SendCancelReceiver(_))));
1625    }
1626
1627    #[test]
1628    fn test_cancel_is_distinct_from_advertisement_rejection() {
1629        let (_, mut receiver) = make_sender_receiver();
1630        receiver.accept(1000.0);
1631        let actions = receiver.cancel();
1632        assert_eq!(receiver.status, ResourceStatus::Failed);
1633        assert!(actions
1634            .iter()
1635            .any(|a| matches!(a, ResourceAction::SendCancelReceiver(_))));
1636        assert!(receiver.cancel().is_empty());
1637    }
1638
1639    fn hmu(resource_hash: &[u8], segment: u64, hashmap: Vec<u8>) -> Vec<u8> {
1640        let payload = crate::msgpack::pack(&crate::msgpack::Value::Array(vec![
1641            crate::msgpack::Value::UInt(segment),
1642            crate::msgpack::Value::Bin(hashmap),
1643        ]));
1644        let mut data = resource_hash.to_vec();
1645        data.extend_from_slice(&payload);
1646        data
1647    }
1648
1649    #[test]
1650    fn test_unsolicited_hmu_is_ignored_without_refreshing_timeout_state() {
1651        let (_, mut receiver) = make_sender_receiver();
1652        receiver.status = ResourceStatus::Transferring;
1653        receiver.waiting_for_hmu = false;
1654        receiver.last_activity = 1000.0;
1655        receiver.retries_left = 1;
1656        let update = hmu(&receiver.resource_hash, 0, vec![0x99; RESOURCE_MAPHASH_LEN]);
1657
1658        let actions = receiver.handle_hashmap_update(&update, 2000.0);
1659
1660        assert!(actions.is_empty());
1661        assert_eq!(receiver.status, ResourceStatus::Transferring);
1662        assert!(!receiver.waiting_for_hmu);
1663        assert_eq!(receiver.last_activity, 1000.0);
1664        assert_eq!(receiver.retries_left, 1);
1665    }
1666
1667    #[test]
1668    fn test_empty_hmu_cancels_waiting_receiver() {
1669        let (_, mut receiver) = make_sender_receiver();
1670        receiver.status = ResourceStatus::Transferring;
1671        receiver.waiting_for_hmu = true;
1672        let update = hmu(&receiver.resource_hash, 1, Vec::new());
1673
1674        let actions = receiver.handle_hashmap_update(&update, 1001.0);
1675
1676        assert_eq!(receiver.status, ResourceStatus::Failed);
1677        assert!(actions
1678            .iter()
1679            .any(|action| matches!(action, ResourceAction::SendCancelReceiver(_))));
1680        assert!(!actions
1681            .iter()
1682            .any(|action| matches!(action, ResourceAction::SendRequest(_))));
1683    }
1684
1685    #[test]
1686    fn test_malformed_hmu_cancels_waiting_receiver() {
1687        let (_, mut receiver) = make_sender_receiver();
1688        receiver.status = ResourceStatus::Transferring;
1689        receiver.waiting_for_hmu = true;
1690        let mut malformed = receiver.resource_hash.clone();
1691        malformed.extend_from_slice(&[0xc1]);
1692
1693        let actions = receiver.handle_hashmap_update(&malformed, 1001.0);
1694
1695        assert_eq!(receiver.status, ResourceStatus::Failed);
1696        assert!(actions
1697            .iter()
1698            .any(|action| matches!(action, ResourceAction::SendCancelReceiver(_))));
1699    }
1700
1701    #[test]
1702    fn test_hmu_without_payload_cancels_waiting_receiver() {
1703        let (_, mut receiver) = make_sender_receiver();
1704        receiver.status = ResourceStatus::Transferring;
1705        receiver.waiting_for_hmu = true;
1706        let update = receiver.resource_hash.clone();
1707
1708        let actions = receiver.handle_hashmap_update(&update, 1001.0);
1709
1710        assert_eq!(receiver.status, ResourceStatus::Failed);
1711        assert!(actions
1712            .iter()
1713            .any(|action| matches!(action, ResourceAction::SendCancelReceiver(_))));
1714    }
1715
1716    #[test]
1717    fn test_hmu_for_another_resource_is_ignored() {
1718        let (_, mut receiver) = make_sender_receiver();
1719        receiver.status = ResourceStatus::Transferring;
1720        receiver.waiting_for_hmu = true;
1721        receiver.last_activity = 1000.0;
1722        let update = hmu(&[0xEE; 32], 0, vec![0x99; RESOURCE_MAPHASH_LEN]);
1723
1724        let actions = receiver.handle_hashmap_update(&update, 2000.0);
1725
1726        assert!(actions.is_empty());
1727        assert!(receiver.waiting_for_hmu);
1728        assert_eq!(receiver.last_activity, 1000.0);
1729    }
1730
1731    #[test]
1732    fn test_misaligned_hmu_hashmap_cancels_waiting_receiver() {
1733        let (_, mut receiver) = make_sender_receiver();
1734        receiver.status = ResourceStatus::Transferring;
1735        receiver.waiting_for_hmu = true;
1736        let update = hmu(
1737            &receiver.resource_hash,
1738            0,
1739            vec![0x99; RESOURCE_MAPHASH_LEN + 1],
1740        );
1741
1742        let actions = receiver.handle_hashmap_update(&update, 1001.0);
1743
1744        assert_eq!(receiver.status, ResourceStatus::Failed);
1745        assert!(actions
1746            .iter()
1747            .any(|action| matches!(action, ResourceAction::SendCancelReceiver(_))));
1748    }
1749
1750    #[test]
1751    fn test_out_of_range_hmu_segment_cancels_waiting_receiver() {
1752        let (_, mut receiver) = make_sender_receiver();
1753        receiver.status = ResourceStatus::Transferring;
1754        receiver.waiting_for_hmu = true;
1755        let update = hmu(
1756            &receiver.resource_hash,
1757            u64::MAX,
1758            vec![0x99; RESOURCE_MAPHASH_LEN],
1759        );
1760
1761        let actions = receiver.handle_hashmap_update(&update, 1001.0);
1762
1763        assert_eq!(receiver.status, ResourceStatus::Failed);
1764        assert!(actions
1765            .iter()
1766            .any(|action| matches!(action, ResourceAction::SendCancelReceiver(_))));
1767    }
1768
1769    #[test]
1770    fn test_tick_is_inert_once_receiver_starts_assembling() {
1771        for status in [
1772            ResourceStatus::Assembling,
1773            ResourceStatus::Complete,
1774            ResourceStatus::Failed,
1775            ResourceStatus::Corrupt,
1776            ResourceStatus::Rejected,
1777        ] {
1778            let (_, mut receiver) = make_sender_receiver();
1779            receiver.status = status;
1780            receiver.last_activity = 1.0;
1781            receiver.retries_left = 0;
1782
1783            let actions = receiver.tick(10_000.0, &identity_decrypt, &NoopCompressor);
1784
1785            assert!(actions.is_empty(), "tick emitted an action for {status:?}");
1786            assert_eq!(receiver.status, status);
1787            assert_eq!(receiver.last_activity, 1.0);
1788        }
1789    }
1790
1791    #[test]
1792    fn test_receive_part_stores() {
1793        let (mut sender, mut receiver) = make_sender_receiver();
1794        receiver.accept(1000.0);
1795
1796        // Get part data from sender (we use identity encryption so parts ARE the raw data)
1797        // Request first part
1798        let mut request = Vec::new();
1799        request.push(RESOURCE_HASHMAP_IS_NOT_EXHAUSTED);
1800        request.extend_from_slice(&sender.resource_hash);
1801        request.extend_from_slice(&sender.part_hashes[0]);
1802
1803        let send_actions = sender.handle_request(&request, 1001.0);
1804        let part_data = send_actions
1805            .iter()
1806            .find_map(|a| match a {
1807                ResourceAction::SendPart(d) => Some(d.clone()),
1808                _ => None,
1809            })
1810            .unwrap();
1811
1812        // Give it to receiver
1813        receiver.req_sent = 1000.5;
1814        let _actions = receiver.receive_part(&part_data, 1001.0);
1815        assert_eq!(receiver.received_count, 1);
1816    }
1817
1818    #[test]
1819    fn test_consecutive_completed_height() {
1820        let (sender, mut receiver) = make_sender_receiver();
1821        receiver.accept(1000.0);
1822
1823        // Simulate receiving parts in order for a multi-part resource
1824        if sender.total_parts() > 1 {
1825            // This only applies to multi-part transfers
1826            assert_eq!(receiver.consecutive_completed_height, -1);
1827        }
1828    }
1829
1830    #[test]
1831    fn receive_part_search_window_matches_request_after_completed_height() {
1832        let mut rng = rns_crypto::FixedRng::new(&[0x5a; 64]);
1833        let data: Vec<u8> = (0..48).collect();
1834        let mut sender = ResourceSender::new(
1835            &data,
1836            None,
1837            8,
1838            &identity_encrypt,
1839            &NoopCompressor,
1840            &mut rng,
1841            1000.0,
1842            false,
1843            false,
1844            None,
1845            1,
1846            1,
1847            None,
1848            0.5,
1849            6.0,
1850        )
1851        .unwrap();
1852        assert!(sender.total_parts() > 5);
1853
1854        let advertisement = sender.get_advertisement(0);
1855        let mut receiver =
1856            ResourceReceiver::from_advertisement(&advertisement, 8, 0.5, 1000.0, None, None)
1857                .unwrap();
1858        let first_request = receiver
1859            .accept(1000.0)
1860            .into_iter()
1861            .find_map(|action| match action {
1862                ResourceAction::SendRequest(data) => Some(data),
1863                _ => None,
1864            })
1865            .unwrap();
1866        let first_parts = sender.handle_request(&first_request, 1001.0);
1867        let first_part = first_parts
1868            .iter()
1869            .find_map(|action| match action {
1870                ResourceAction::SendPart(data)
1871                    if map_hash(data, &receiver.random_hash) == sender.part_hashes[0] =>
1872                {
1873                    Some(data.clone())
1874                }
1875                _ => None,
1876            })
1877            .unwrap();
1878        receiver.receive_part(&first_part, 1002.0);
1879        assert_eq!(receiver.consecutive_completed_height, 0);
1880
1881        let next_request = receiver
1882            .request_next(1003.0)
1883            .into_iter()
1884            .find_map(|action| match action {
1885                ResourceAction::SendRequest(data) => Some(data),
1886                _ => None,
1887            })
1888            .unwrap();
1889        let next_parts = sender.handle_request(&next_request, 1004.0);
1890        let window_tail_index = receiver.window.window;
1891        let window_tail = next_parts
1892            .iter()
1893            .find_map(|action| match action {
1894                ResourceAction::SendPart(data)
1895                    if map_hash(data, &receiver.random_hash)
1896                        == sender.part_hashes[window_tail_index] =>
1897                {
1898                    Some(data.clone())
1899                }
1900                _ => None,
1901            })
1902            .expect("request must include the inclusive tail of the next window");
1903
1904        let actions = receiver.receive_part(&window_tail, 1005.0);
1905        assert!(receiver.part_spans[window_tail_index].is_some());
1906        assert_eq!(receiver.received_count, 2);
1907        assert_eq!(receiver.consecutive_completed_height, 0);
1908        assert!(actions.iter().any(|action| matches!(
1909            action,
1910            ResourceAction::ProgressUpdate {
1911                received: 2,
1912                total,
1913            } if *total == receiver.total_parts
1914        )));
1915    }
1916
1917    #[test]
1918    fn test_handle_cancel() {
1919        let (_, mut receiver) = make_sender_receiver();
1920        receiver.accept(1000.0);
1921        let _actions = receiver.handle_cancel();
1922        assert_eq!(receiver.status, ResourceStatus::Failed);
1923    }
1924
1925    #[test]
1926    fn test_assemble_compressed_resource_rejects_oversized_decompression() {
1927        let data = b"oversized!";
1928        let mut rng = rns_crypto::FixedRng::new(&[0x93; 64]);
1929
1930        let mut sender = ResourceSender::new(
1931            data,
1932            None,
1933            RESOURCE_SDU,
1934            &identity_encrypt,
1935            &ExpandingCompressor,
1936            &mut rng,
1937            1000.0,
1938            true,
1939            false,
1940            None,
1941            1,
1942            1,
1943            None,
1944            0.5,
1945            6.0,
1946        )
1947        .unwrap();
1948
1949        let adv = sender.get_advertisement(0);
1950        let mut receiver =
1951            ResourceReceiver::from_advertisement(&adv, RESOURCE_SDU, 0.5, 1000.0, None, None)
1952                .unwrap();
1953        receiver.max_decompressed_size = data.len() - 1;
1954
1955        let request_data = receiver
1956            .accept(1001.0)
1957            .into_iter()
1958            .find_map(|a| match a {
1959                ResourceAction::SendRequest(d) => Some(d),
1960                _ => None,
1961            })
1962            .unwrap();
1963
1964        let send_actions = sender.handle_request(&request_data, 1002.0);
1965        receiver.req_sent = 1001.0;
1966        for action in &send_actions {
1967            if let ResourceAction::SendPart(part_data) = action {
1968                receiver.receive_part(part_data, 1003.0);
1969            }
1970        }
1971
1972        let assemble_actions = receiver.assemble(&identity_decrypt, &ExpandingCompressor);
1973        assert_eq!(receiver.status, ResourceStatus::Corrupt);
1974        assert!(assemble_actions
1975            .iter()
1976            .any(|a| matches!(a, ResourceAction::SendCancelReceiver(_))));
1977        assert!(assemble_actions
1978            .iter()
1979            .any(|a| matches!(a, ResourceAction::Failed(ResourceError::TooLarge))));
1980        assert!(assemble_actions
1981            .iter()
1982            .any(|a| matches!(a, ResourceAction::TeardownLink)));
1983    }
1984
1985    #[test]
1986    fn test_full_transfer_small_data() {
1987        // End-to-end: sender creates, receiver accepts, parts flow, assembly completes
1988        let data = b"small data";
1989        let mut rng = rns_crypto::FixedRng::new(&[0x77; 64]);
1990
1991        let mut sender = ResourceSender::new(
1992            data,
1993            None,
1994            RESOURCE_SDU,
1995            &identity_encrypt,
1996            &NoopCompressor,
1997            &mut rng,
1998            1000.0,
1999            false,
2000            false,
2001            None,
2002            1,
2003            1,
2004            None,
2005            0.5,
2006            6.0,
2007        )
2008        .unwrap();
2009
2010        let adv = sender.get_advertisement(0);
2011        let mut receiver =
2012            ResourceReceiver::from_advertisement(&adv, RESOURCE_SDU, 0.5, 1000.0, None, None)
2013                .unwrap();
2014
2015        // Accept
2016        let req_actions = receiver.accept(1001.0);
2017        assert_eq!(receiver.status, ResourceStatus::Transferring);
2018
2019        // Get request data
2020        let request_data = req_actions
2021            .iter()
2022            .find_map(|a| match a {
2023                ResourceAction::SendRequest(d) => Some(d.clone()),
2024                _ => None,
2025            })
2026            .unwrap();
2027
2028        // Sender handles request
2029        let send_actions = sender.handle_request(&request_data, 1002.0);
2030
2031        // Feed all parts to receiver
2032        receiver.req_sent = 1001.0;
2033        for action in &send_actions {
2034            if let ResourceAction::SendPart(part_data) = action {
2035                receiver.receive_part(part_data, 1003.0);
2036            }
2037        }
2038
2039        assert_eq!(receiver.received_count, receiver.total_parts);
2040
2041        // Assemble
2042        let assemble_actions = receiver.assemble(&identity_decrypt, &NoopCompressor);
2043
2044        // Check for proof and data
2045        let has_proof = assemble_actions
2046            .iter()
2047            .any(|a| matches!(a, ResourceAction::SendProof(_)));
2048        let has_data = assemble_actions
2049            .iter()
2050            .any(|a| matches!(a, ResourceAction::DataReceived { .. }));
2051        let has_complete = assemble_actions
2052            .iter()
2053            .any(|a| matches!(a, ResourceAction::Completed));
2054
2055        assert!(has_proof, "Should send proof");
2056        assert!(has_data, "Should return data");
2057        assert!(has_complete, "Should be completed");
2058
2059        // Verify data matches
2060        let received_data = assemble_actions
2061            .iter()
2062            .find_map(|a| match a {
2063                ResourceAction::DataReceived { data, .. } => Some(data.clone()),
2064                _ => None,
2065            })
2066            .unwrap();
2067        assert_eq!(received_data, data);
2068
2069        // Verify proof validates on sender side
2070        let proof_data = assemble_actions
2071            .iter()
2072            .find_map(|a| match a {
2073                ResourceAction::SendProof(d) => Some(d.clone()),
2074                _ => None,
2075            })
2076            .unwrap();
2077
2078        let _proof_actions = sender.handle_proof(&proof_data, 1004.0);
2079        assert_eq!(sender.status, ResourceStatus::Complete);
2080    }
2081
2082    /// Drive an accepted transfer to completion from the receiver's first
2083    /// actions, returning the request flags it sent and the assembly actions.
2084    fn run_transfer(
2085        sender: &mut ResourceSender,
2086        receiver: &mut ResourceReceiver,
2087        mut pending: Vec<ResourceAction>,
2088    ) -> (Vec<u8>, Vec<ResourceAction>) {
2089        let mut now = 1001.0;
2090        let mut request_flags = Vec::new();
2091        for _ in 0..1000 {
2092            if receiver.received_count == receiver.total_parts {
2093                break;
2094            }
2095            now += 0.01;
2096            let mut next = Vec::new();
2097            for action in pending.drain(..) {
2098                if let ResourceAction::SendRequest(request) = action {
2099                    request_flags.push(request[0]);
2100                    for reply in sender.handle_request(&request, now) {
2101                        match reply {
2102                            ResourceAction::SendPart(part) => {
2103                                next.extend(receiver.receive_part(&part, now))
2104                            }
2105                            ResourceAction::SendHmu(hmu) => {
2106                                next.extend(receiver.handle_hashmap_update(&hmu, now))
2107                            }
2108                            other => panic!("unexpected sender action {other:?}"),
2109                        }
2110                    }
2111                }
2112            }
2113            pending = next;
2114        }
2115        assert_eq!(receiver.received_count, receiver.total_parts);
2116        (
2117            request_flags,
2118            receiver.assemble(&identity_decrypt, &NoopCompressor),
2119        )
2120    }
2121
2122    fn multi_hashmap_sender(advertisement_mdu: usize) -> (Vec<u8>, ResourceSender) {
2123        let mut rng = rns_crypto::FixedRng::new(&[0x5a; 64]);
2124        // More parts than one advertisement hashmap segment holds.
2125        let data: Vec<u8> = (0..(RESOURCE_HASHMAP_MAX_LEN + 26) * RESOURCE_SDU)
2126            .map(|i| (i * 7 + i / 251) as u8)
2127            .collect();
2128        let mut sender = ResourceSender::new(
2129            &data,
2130            None,
2131            RESOURCE_SDU,
2132            &identity_encrypt,
2133            &NoopCompressor,
2134            &mut rng,
2135            1000.0,
2136            false,
2137            false,
2138            None,
2139            1,
2140            1,
2141            None,
2142            0.5,
2143            6.0,
2144        )
2145        .unwrap();
2146        assert!(sender.total_parts() > RESOURCE_HASHMAP_MAX_LEN);
2147        sender.advertisement_mdu = advertisement_mdu;
2148        (data, sender)
2149    }
2150
2151    #[test]
2152    fn advertisement_too_large_for_link_mdu_defers_the_initial_hashmap() {
2153        // Python packs an advertisement exceeding the link MDU with an empty
2154        // hashmap; the receiver asks for it with HASHMAP_IS_EMPTY and the
2155        // sender answers with segment 0 before any part is requested.
2156        let (data, mut sender) = multi_hashmap_sender(200);
2157        let adv = sender.advertise(1000.0);
2158        let ResourceAction::SendAdvertisement(adv_data) = &adv[0] else {
2159            panic!("expected an advertisement");
2160        };
2161        assert!(adv_data.len() <= 200);
2162        assert!(ResourceAdvertisement::unpack(adv_data)
2163            .unwrap()
2164            .hashmap
2165            .is_empty());
2166
2167        let mut receiver =
2168            ResourceReceiver::from_advertisement(adv_data, RESOURCE_SDU, 0.5, 1000.0, None, None)
2169                .unwrap();
2170        let first = receiver.accept(1000.5);
2171        let [ResourceAction::SendRequest(request)] = first.as_slice() else {
2172            panic!("expected one request, got {first:?}");
2173        };
2174        // Flag, resource hash, no last map hash and no part hashes.
2175        assert_eq!(request[0], RESOURCE_HASHMAP_IS_EMPTY);
2176        assert_eq!(request.len(), 1 + 32);
2177        assert!(receiver.waiting_for_hmu);
2178
2179        let (flags, actions) = run_transfer(&mut sender, &mut receiver, first);
2180        assert_eq!(flags[0], RESOURCE_HASHMAP_IS_EMPTY);
2181        assert!(
2182            flags.contains(&RESOURCE_HASHMAP_IS_EXHAUSTED),
2183            "the second hashmap segment still uses the exhausted path: {flags:?}"
2184        );
2185        let received = actions.iter().find_map(|a| match a {
2186            ResourceAction::DataReceived { data, .. } => Some(data.clone()),
2187            _ => None,
2188        });
2189        assert_eq!(received.as_deref(), Some(data.as_slice()));
2190        let proof = actions
2191            .iter()
2192            .find_map(|a| match a {
2193                ResourceAction::SendProof(proof) => Some(proof.clone()),
2194                _ => None,
2195            })
2196            .unwrap();
2197        sender.handle_proof(&proof, 1100.0);
2198        assert_eq!(sender.status, ResourceStatus::Complete);
2199    }
2200
2201    #[test]
2202    fn advertisement_within_link_mdu_keeps_its_hashmap() {
2203        let (data, mut sender) = multi_hashmap_sender(LINK_MDU);
2204        let adv = sender.get_advertisement(0);
2205        assert!(adv.len() <= LINK_MDU);
2206        assert_eq!(
2207            ResourceAdvertisement::unpack(&adv).unwrap().hashmap.len(),
2208            RESOURCE_HASHMAP_MAX_LEN * RESOURCE_MAPHASH_LEN
2209        );
2210        let mut receiver =
2211            ResourceReceiver::from_advertisement(&adv, RESOURCE_SDU, 0.5, 1000.0, None, None)
2212                .unwrap();
2213        let first = receiver.accept(1001.0);
2214        let (flags, actions) = run_transfer(&mut sender, &mut receiver, first);
2215        assert_eq!(flags[0], RESOURCE_HASHMAP_IS_NOT_EXHAUSTED);
2216        assert!(!flags.contains(&RESOURCE_HASHMAP_IS_EMPTY));
2217        assert!(actions.iter().any(
2218            |a| matches!(a, ResourceAction::DataReceived { data: received, .. } if *received == data)
2219        ));
2220    }
2221
2222    #[test]
2223    fn app_flag_reaches_the_receiver() {
2224        let (mut sender, _) = make_sender_receiver();
2225        sender.flags.app_flag = true;
2226        let adv = sender.get_advertisement(0);
2227        let unpacked = ResourceAdvertisement::unpack(&adv).unwrap();
2228        assert_eq!(unpacked.flags.to_byte() & 0x40, 0x40);
2229        let mut receiver =
2230            ResourceReceiver::from_advertisement(&adv, RESOURCE_SDU, 0.5, 1000.0, None, None)
2231                .unwrap();
2232        assert!(receiver.flags.app_flag);
2233        let first = receiver.accept(1001.0);
2234        let (_, actions) = run_transfer(&mut sender, &mut receiver, first);
2235        assert!(actions
2236            .iter()
2237            .any(|a| matches!(a, ResourceAction::DataReceived { app_flag: true, .. })));
2238
2239        let (mut sender, mut receiver) = make_sender_receiver();
2240        let first = receiver.accept(1001.0);
2241        let (_, actions) = run_transfer(&mut sender, &mut receiver, first);
2242        assert!(actions.iter().any(|a| matches!(
2243            a,
2244            ResourceAction::DataReceived {
2245                app_flag: false,
2246                ..
2247            }
2248        )));
2249    }
2250
2251    #[test]
2252    fn test_full_transfer_with_metadata() {
2253        let data = b"data with metadata";
2254        let metadata = b"some metadata";
2255        let mut rng = rns_crypto::FixedRng::new(&[0x88; 64]);
2256
2257        let mut sender = ResourceSender::new(
2258            data,
2259            Some(metadata),
2260            RESOURCE_SDU,
2261            &identity_encrypt,
2262            &NoopCompressor,
2263            &mut rng,
2264            1000.0,
2265            false,
2266            false,
2267            None,
2268            1,
2269            1,
2270            None,
2271            0.5,
2272            6.0,
2273        )
2274        .unwrap();
2275
2276        assert!(sender.flags.has_metadata);
2277
2278        let adv = sender.get_advertisement(0);
2279        let mut receiver =
2280            ResourceReceiver::from_advertisement(&adv, RESOURCE_SDU, 0.5, 1000.0, None, None)
2281                .unwrap();
2282
2283        assert!(receiver.has_metadata);
2284
2285        // Transfer all parts
2286        let req_actions = receiver.accept(1001.0);
2287        let request_data = req_actions
2288            .iter()
2289            .find_map(|a| match a {
2290                ResourceAction::SendRequest(d) => Some(d.clone()),
2291                _ => None,
2292            })
2293            .unwrap();
2294
2295        let send_actions = sender.handle_request(&request_data, 1002.0);
2296        receiver.req_sent = 1001.0;
2297        for action in &send_actions {
2298            if let ResourceAction::SendPart(part_data) = action {
2299                receiver.receive_part(part_data, 1003.0);
2300            }
2301        }
2302
2303        let assemble_actions = receiver.assemble(&identity_decrypt, &NoopCompressor);
2304
2305        let (recv_data, recv_meta) = assemble_actions
2306            .iter()
2307            .find_map(|a| match a {
2308                ResourceAction::DataReceived { data, metadata, .. } => {
2309                    Some((data.clone(), metadata.clone()))
2310                }
2311                _ => None,
2312            })
2313            .unwrap();
2314
2315        assert_eq!(recv_data, data);
2316        assert_eq!(recv_meta.unwrap(), metadata);
2317    }
2318
2319    #[test]
2320    fn test_previous_window_restore() {
2321        let (_, _receiver) = make_sender_receiver();
2322        // Create with previous window
2323        let adv_data = {
2324            let mut rng = rns_crypto::FixedRng::new(&[0x42; 64]);
2325            let sender = ResourceSender::new(
2326                b"test",
2327                None,
2328                RESOURCE_SDU,
2329                &identity_encrypt,
2330                &NoopCompressor,
2331                &mut rng,
2332                1000.0,
2333                false,
2334                false,
2335                None,
2336                1,
2337                1,
2338                None,
2339                0.5,
2340                6.0,
2341            )
2342            .unwrap();
2343            sender.get_advertisement(0)
2344        };
2345
2346        let receiver = ResourceReceiver::from_advertisement(
2347            &adv_data,
2348            RESOURCE_SDU,
2349            0.5,
2350            1000.0,
2351            Some(8),
2352            Some(50000.0),
2353        )
2354        .unwrap();
2355        assert_eq!(receiver.window.window, 8);
2356    }
2357
2358    #[test]
2359    fn test_tick_timeout_retry() {
2360        let (_, mut receiver) = make_sender_receiver();
2361        receiver.accept(1000.0);
2362        receiver.rtt = Some(0.1);
2363
2364        // Way past timeout
2365        let actions = receiver.tick(9999.0, &identity_decrypt, &NoopCompressor);
2366        // Should have retried (window decreased, request_next called)
2367        assert!(!actions.is_empty() || receiver.retries_left < RESOURCE_MAX_RETRIES);
2368    }
2369
2370    #[test]
2371    fn test_tick_waiting_for_hmu_gets_extra_timeout() {
2372        let (_, mut receiver) = make_sender_receiver();
2373        receiver.accept(1000.0);
2374        receiver.waiting_for_hmu = true;
2375        receiver.outstanding_parts = 0;
2376        let eifr = 10_000.0;
2377        receiver.previous_eifr = Some(eifr);
2378        receiver.last_activity = 1000.0;
2379
2380        let old_timeout = base_timeout(&receiver, eifr);
2381        let now = old_timeout + 0.01;
2382
2383        let actions = receiver.tick(now, &identity_decrypt, &NoopCompressor);
2384
2385        assert!(actions.is_empty(), "receiver should keep waiting for HMU");
2386        assert_eq!(receiver.retries_left, RESOURCE_MAX_RETRIES);
2387        assert_eq!(receiver.status, ResourceStatus::Transferring);
2388    }
2389
2390    #[test]
2391    fn test_tick_zero_outstanding_parts_gets_extra_timeout_without_hmu_flag() {
2392        let (_, mut receiver) = make_sender_receiver();
2393        receiver.accept(1000.0);
2394        receiver.waiting_for_hmu = false;
2395        receiver.outstanding_parts = 0;
2396        let eifr = 10_000.0;
2397        receiver.previous_eifr = Some(eifr);
2398        receiver.last_activity = 1000.0;
2399
2400        let old_timeout = base_timeout(&receiver, eifr);
2401        let now = old_timeout + 0.01;
2402
2403        let actions = receiver.tick(now, &identity_decrypt, &NoopCompressor);
2404
2405        assert!(
2406            actions.is_empty(),
2407            "receiver should keep waiting for follow-up hashmap data"
2408        );
2409        assert_eq!(receiver.retries_left, RESOURCE_MAX_RETRIES);
2410        assert_eq!(receiver.status, ResourceStatus::Transferring);
2411    }
2412
2413    #[test]
2414    fn test_tick_waiting_for_hmu_retries_after_extended_timeout() {
2415        let (_, mut receiver) = make_sender_receiver();
2416        receiver.accept(1000.0);
2417        receiver.waiting_for_hmu = true;
2418        receiver.outstanding_parts = 0;
2419        let eifr = 10_000.0;
2420        receiver.previous_eifr = Some(eifr);
2421        receiver.last_activity = 1000.0;
2422
2423        let now = hmu_timeout(&receiver, eifr) + 0.01;
2424        let _actions = receiver.tick(now, &identity_decrypt, &NoopCompressor);
2425
2426        assert_eq!(receiver.retries_left, RESOURCE_MAX_RETRIES - 1);
2427        assert!(!receiver.waiting_for_hmu);
2428    }
2429
2430    #[test]
2431    fn test_tick_inflight_parts_do_not_get_hmu_timeout_extension() {
2432        let (_, mut receiver) = make_sender_receiver();
2433        receiver.accept(1000.0);
2434        receiver.waiting_for_hmu = false;
2435        receiver.outstanding_parts = 2;
2436        let eifr = 10_000.0;
2437        receiver.previous_eifr = Some(eifr);
2438        receiver.last_activity = 1000.0;
2439
2440        let now = base_timeout(&receiver, eifr) + 0.01;
2441        let _actions = receiver.tick(now, &identity_decrypt, &NoopCompressor);
2442
2443        assert_eq!(receiver.retries_left, RESOURCE_MAX_RETRIES - 1);
2444    }
2445
2446    #[test]
2447    fn test_tick_max_retries_exceeded() {
2448        let (_, mut receiver) = make_sender_receiver();
2449        receiver.accept(1000.0);
2450        receiver.retries_left = 0;
2451        receiver.rtt = Some(0.001);
2452        receiver.eifr = Some(100000.0);
2453
2454        let _actions = receiver.tick(9999.0, &identity_decrypt, &NoopCompressor);
2455        assert_eq!(receiver.status, ResourceStatus::Failed);
2456    }
2457}