Skip to main content

rns_core/resource/
advertisement.rs

1use alloc::vec::Vec;
2
3use super::types::{AdvFlags, ResourceError};
4use crate::constants::{RESOURCE_HASHMAP_MAX_LEN, RESOURCE_MAPHASH_LEN};
5use crate::msgpack;
6
7/// Resource advertisement data, corresponding to Python's ResourceAdvertisement.
8#[derive(Debug, Clone)]
9pub struct ResourceAdvertisement {
10    /// Transfer size (encrypted data size)
11    pub transfer_size: u64,
12    /// Total uncompressed data size (including metadata overhead)
13    pub data_size: u64,
14    /// Number of parts
15    pub num_parts: u64,
16    /// Resource hash (full 32 bytes)
17    pub resource_hash: Vec<u8>,
18    /// Random hash (4 bytes)
19    pub random_hash: Vec<u8>,
20    /// Original hash (first segment, 32 bytes)
21    pub original_hash: Vec<u8>,
22    /// Hashmap segment (concatenated 4-byte part hashes)
23    pub hashmap: Vec<u8>,
24    /// Flags byte
25    pub flags: AdvFlags,
26    /// Segment index (1-based)
27    pub segment_index: u64,
28    /// Total segments
29    pub total_segments: u64,
30    /// Request ID (optional)
31    pub request_id: Option<Vec<u8>>,
32}
33
34/// An advertisement's fields, borrowed, for packing without copying them.
35#[derive(Clone, Copy)]
36pub(crate) struct AdvertisementFields<'a> {
37    pub transfer_size: u64,
38    pub data_size: u64,
39    pub num_parts: u64,
40    pub resource_hash: &'a [u8],
41    pub random_hash: &'a [u8],
42    pub original_hash: &'a [u8],
43    pub hashmap: &'a [u8],
44    pub flags: AdvFlags,
45    pub segment_index: u64,
46    pub total_segments: u64,
47    pub request_id: Option<&'a [u8]>,
48}
49
50impl AdvertisementFields<'_> {
51    /// [`ResourceAdvertisement::pack`].
52    pub fn pack(&self, segment: usize) -> Vec<u8> {
53        let hashmap_start = segment * RESOURCE_HASHMAP_MAX_LEN * RESOURCE_MAPHASH_LEN;
54        let max_end = (segment + 1) * RESOURCE_HASHMAP_MAX_LEN * RESOURCE_MAPHASH_LEN;
55        let hashmap_end = core::cmp::min(max_end, self.hashmap.len());
56        let hashmap_segment = if hashmap_start < self.hashmap.len() {
57            &self.hashmap[hashmap_start..hashmap_end]
58        } else {
59            &[]
60        };
61
62        // Written directly, in Python's key order (t, d, n, h, r, o, i, l, q,
63        // f, m): building a msgpack value first cloned every field. Sized for
64        // the worst case so the buffer is allocated once.
65        let bins = self.resource_hash.len()
66            + self.random_hash.len()
67            + self.original_hash.len()
68            + self.request_id.map_or(0, <[u8]>::len)
69            + hashmap_segment.len();
70        let mut out = Vec::with_capacity(1 + 11 * 2 + 6 * 9 + 5 * 5 + bins);
71        out.push(0x80 | 11);
72        let uint = |out: &mut Vec<u8>, key: &str, value: u64| {
73            msgpack::pack_str(key, out);
74            msgpack::pack_uint(value, out);
75        };
76        uint(&mut out, "t", self.transfer_size);
77        uint(&mut out, "d", self.data_size);
78        uint(&mut out, "n", self.num_parts);
79        for (key, value) in [
80            ("h", self.resource_hash),
81            ("r", self.random_hash),
82            ("o", self.original_hash),
83        ] {
84            msgpack::pack_str(key, &mut out);
85            msgpack::pack_bin(value, &mut out);
86        }
87        uint(&mut out, "i", self.segment_index);
88        uint(&mut out, "l", self.total_segments);
89        msgpack::pack_str("q", &mut out);
90        match self.request_id {
91            Some(id) => msgpack::pack_bin(id, &mut out),
92            None => out.push(0xc0),
93        }
94        uint(&mut out, "f", self.flags.to_byte() as u64);
95        msgpack::pack_str("m", &mut out);
96        msgpack::pack_bin(hashmap_segment, &mut out);
97        out
98    }
99
100    /// [`ResourceAdvertisement::pack_for_mdu`].
101    pub fn pack_for_mdu(&self, segment: usize, mdu: usize) -> Vec<u8> {
102        let packed = self.pack(segment);
103        if packed.len() <= mdu {
104            return packed;
105        }
106        AdvertisementFields {
107            hashmap: &[],
108            ..*self
109        }
110        .pack(segment)
111    }
112}
113
114impl ResourceAdvertisement {
115    pub(crate) fn fields(&self) -> AdvertisementFields<'_> {
116        AdvertisementFields {
117            transfer_size: self.transfer_size,
118            data_size: self.data_size,
119            num_parts: self.num_parts,
120            resource_hash: &self.resource_hash,
121            random_hash: &self.random_hash,
122            original_hash: &self.original_hash,
123            hashmap: &self.hashmap,
124            flags: self.flags,
125            segment_index: self.segment_index,
126            total_segments: self.total_segments,
127            request_id: self.request_id.as_deref(),
128        }
129    }
130
131    /// Pack the advertisement to msgpack bytes.
132    /// `segment` controls which hashmap segment to include (0-based).
133    pub fn pack(&self, segment: usize) -> Vec<u8> {
134        self.fields().pack(segment)
135    }
136
137    /// Pack the advertisement for a link with the given MDU.
138    ///
139    /// Like Python, an advertisement that would exceed the MDU is sent with
140    /// an empty hashmap, and the receiver requests the first hashmap segment
141    /// with `RESOURCE_HASHMAP_IS_EMPTY` before any part.
142    pub fn pack_for_mdu(&self, segment: usize, mdu: usize) -> Vec<u8> {
143        self.fields().pack_for_mdu(segment, mdu)
144    }
145
146    /// Unpack an advertisement from msgpack bytes.
147    pub fn unpack(data: &[u8]) -> Result<Self, ResourceError> {
148        // Read in place: decoding to a msgpack value first allocated every
149        // key and field, and each field was then copied again.
150        let invalid = ResourceError::InvalidAdvertisement;
151        let value = msgpack::unpack_exact_ref(data).map_err(|_| invalid.clone())?;
152        let mut fields: [Option<msgpack::ValueRef<'_>>; 11] = [None; 11];
153        for (key, field) in value.map_entries().ok_or(invalid.clone())? {
154            let Some(key) = key.as_str() else { continue };
155            let Some(index) = ["t", "d", "n", "h", "r", "o", "i", "l", "q", "f", "m"]
156                .iter()
157                .position(|k| *k == key)
158            else {
159                continue;
160            };
161            // The first entry for a key wins, as in `Value::map_get`.
162            fields[index].get_or_insert(field);
163        }
164        let [t, d, n, h, r, o, i, l, q, f, m] = fields;
165        let uint =
166            |v: Option<msgpack::ValueRef<'_>>| v.and_then(|v| v.as_uint()).ok_or(invalid.clone());
167        let bin = |v: Option<msgpack::ValueRef<'_>>| {
168            v.and_then(|v| v.as_bin())
169                .map(<[u8]>::to_vec)
170                .ok_or(invalid.clone())
171        };
172        let (t, d, n) = (uint(t)?, uint(d)?, uint(n)?);
173        let (h, r, o, m) = (bin(h)?, bin(r)?, bin(o)?, bin(m)?);
174        let f = uint(f)? as u8;
175        let (i, l) = (uint(i)?, uint(l)?);
176        let q = q.ok_or(invalid.clone())?;
177        let request_id = if q.is_nil() {
178            None
179        } else {
180            Some(q.as_bin().ok_or(invalid.clone())?.to_vec())
181        };
182
183        if t > (crate::constants::RESOURCE_MAX_EFFICIENT_SIZE * 3) as u64 {
184            return Err(ResourceError::InvalidAdvertisement);
185        }
186
187        Ok(ResourceAdvertisement {
188            transfer_size: t,
189            data_size: d,
190            num_parts: n,
191            resource_hash: h,
192            random_hash: r,
193            original_hash: o,
194            hashmap: m,
195            flags: AdvFlags::from_byte(f),
196            segment_index: i,
197            total_segments: l,
198            request_id,
199        })
200    }
201
202    /// Check if this advertisement is a request.
203    pub fn is_request(&self) -> bool {
204        self.request_id.is_some() && self.flags.is_request
205    }
206
207    /// Check if this advertisement is a response.
208    pub fn is_response(&self) -> bool {
209        self.request_id.is_some() && self.flags.is_response
210    }
211
212    /// Whether the sender set the application flag (Python `app_flag_set`).
213    pub fn app_flag_set(&self) -> bool {
214        self.flags.app_flag
215    }
216
217    /// Get the number of hashmap segments needed.
218    pub fn hashmap_segments(&self) -> usize {
219        let total_hashes = self.num_parts as usize;
220        if total_hashes == 0 {
221            return 1;
222        }
223        total_hashes.div_ceil(RESOURCE_HASHMAP_MAX_LEN)
224    }
225}
226
227#[cfg(test)]
228mod tests {
229    use super::*;
230    use crate::msgpack::Value;
231    use alloc::vec;
232
233    /// The encoder before direct writing, kept as the reference.
234    fn pack_reference(adv: &ResourceAdvertisement, segment: usize) -> Vec<u8> {
235        let hashmap_start = segment * RESOURCE_HASHMAP_MAX_LEN * RESOURCE_MAPHASH_LEN;
236        let max_end = (segment + 1) * RESOURCE_HASHMAP_MAX_LEN * RESOURCE_MAPHASH_LEN;
237        let hashmap_end = core::cmp::min(max_end, adv.hashmap.len());
238        let hashmap_segment = if hashmap_start < adv.hashmap.len() {
239            &adv.hashmap[hashmap_start..hashmap_end]
240        } else {
241            &[]
242        };
243        let q_value = match &adv.request_id {
244            Some(id) => Value::Bin(id.clone()),
245            None => Value::Nil,
246        };
247        let entries: Vec<(&str, Value)> = vec![
248            ("t", Value::UInt(adv.transfer_size)),
249            ("d", Value::UInt(adv.data_size)),
250            ("n", Value::UInt(adv.num_parts)),
251            ("h", Value::Bin(adv.resource_hash.clone())),
252            ("r", Value::Bin(adv.random_hash.clone())),
253            ("o", Value::Bin(adv.original_hash.clone())),
254            ("i", Value::UInt(adv.segment_index)),
255            ("l", Value::UInt(adv.total_segments)),
256            ("q", q_value),
257            ("f", Value::UInt(adv.flags.to_byte() as u64)),
258            ("m", Value::Bin(hashmap_segment.to_vec())),
259        ];
260        msgpack::pack_str_map(&entries)
261    }
262
263    /// The decoder before reading in place, kept as the reference.
264    fn unpack_reference(data: &[u8]) -> Option<ResourceAdvertisement> {
265        let value = msgpack::unpack_exact(data).ok()?;
266        let uint = |k| value.map_get(k).and_then(|v| v.as_uint());
267        let bin = |k| {
268            value
269                .map_get(k)
270                .and_then(|v| v.as_bin())
271                .map(<[u8]>::to_vec)
272        };
273        let (t, d, n) = (uint("t")?, uint("d")?, uint("n")?);
274        let (h, r, o, m) = (bin("h")?, bin("r")?, bin("o")?, bin("m")?);
275        let f = uint("f")? as u8;
276        let (i, l) = (uint("i")?, uint("l")?);
277        let q = value.map_get("q")?;
278        let request_id = if q.is_nil() {
279            None
280        } else {
281            Some(q.as_bin()?.to_vec())
282        };
283        if t > (crate::constants::RESOURCE_MAX_EFFICIENT_SIZE * 3) as u64 {
284            return None;
285        }
286        Some(ResourceAdvertisement {
287            transfer_size: t,
288            data_size: d,
289            num_parts: n,
290            resource_hash: h,
291            random_hash: r,
292            original_hash: o,
293            hashmap: m,
294            flags: AdvFlags::from_byte(f),
295            segment_index: i,
296            total_segments: l,
297            request_id,
298        })
299    }
300
301    struct XorShift(u64);
302    impl XorShift {
303        fn next(&mut self) -> u64 {
304            self.0 ^= self.0 << 13;
305            self.0 ^= self.0 >> 7;
306            self.0 ^= self.0 << 17;
307            self.0
308        }
309        fn bytes(&mut self, len: usize) -> Vec<u8> {
310            (0..len).map(|_| self.next() as u8).collect()
311        }
312    }
313
314    fn random_adv(rng: &mut XorShift) -> ResourceAdvertisement {
315        // Sizes cross every msgpack integer width.
316        let widths = [
317            0u64,
318            1,
319            127,
320            128,
321            255,
322            256,
323            65_535,
324            65_536,
325            u32::MAX as u64,
326            u64::MAX,
327        ];
328        let pick = |rng: &mut XorShift| {
329            let w = widths[(rng.next() % widths.len() as u64) as usize];
330            w.saturating_sub(rng.next() % 3)
331        };
332        let hashmap_len = [0usize, 4, 300, 74 * 4 * 2][(rng.next() % 4) as usize];
333        let original_len = (rng.next() % 40) as usize;
334        ResourceAdvertisement {
335            transfer_size: pick(rng) % 1_000_000,
336            data_size: pick(rng),
337            num_parts: pick(rng),
338            resource_hash: rng.bytes(32),
339            random_hash: rng.bytes(4),
340            original_hash: rng.bytes(original_len),
341            hashmap: rng.bytes(hashmap_len),
342            flags: AdvFlags::from_byte(rng.next() as u8),
343            segment_index: pick(rng),
344            total_segments: pick(rng),
345            request_id: rng.next().is_multiple_of(2).then(|| rng.bytes(16)),
346        }
347    }
348
349    fn same(a: &Option<ResourceAdvertisement>, b: &Option<ResourceAdvertisement>) -> bool {
350        match (a, b) {
351            (None, None) => true,
352            (Some(a), Some(b)) => {
353                a.transfer_size == b.transfer_size
354                    && a.data_size == b.data_size
355                    && a.num_parts == b.num_parts
356                    && a.resource_hash == b.resource_hash
357                    && a.random_hash == b.random_hash
358                    && a.original_hash == b.original_hash
359                    && a.hashmap == b.hashmap
360                    && a.flags.to_byte() == b.flags.to_byte()
361                    && a.segment_index == b.segment_index
362                    && a.total_segments == b.total_segments
363                    && a.request_id == b.request_id
364            }
365            _ => false,
366        }
367    }
368
369    #[test]
370    fn direct_pack_matches_the_value_encoder_byte_for_byte() {
371        let mut rng = XorShift(0x5EED_0001);
372        for _ in 0..2_000 {
373            let adv = random_adv(&mut rng);
374            for segment in 0..3 {
375                assert_eq!(adv.pack(segment), pack_reference(&adv, segment));
376            }
377        }
378    }
379
380    #[test]
381    fn in_place_unpack_matches_the_value_decoder_on_valid_and_corrupt_input() {
382        let mut rng = XorShift(0x5EED_0002);
383        let mut cases = 0;
384        for round in 0..400u32 {
385            let adv = random_adv(&mut rng);
386            let packed = adv.pack(0);
387            let check = |data: &[u8]| {
388                let new = ResourceAdvertisement::unpack(data).ok();
389                assert!(
390                    same(&new, &unpack_reference(data)),
391                    "diverged on {data:02x?}"
392                );
393            };
394            check(&packed);
395            // Every truncation, every single-byte change at a few values, and
396            // random multi-byte flips.
397            for len in 0..packed.len() {
398                check(&packed[..len]);
399            }
400            let mut extended = packed.clone();
401            extended.push(0xc0);
402            check(&extended);
403            if round.is_multiple_of(8) {
404                for at in 0..packed.len() {
405                    for value in [
406                        0x00, 0x7f, 0x80, 0xa1, 0xc0, 0xc1, 0xc4, 0xcc, 0xd0, 0xd9, 0xdc, 0xde,
407                        0xff,
408                    ] {
409                        let mut mutated = packed.clone();
410                        mutated[at] = value;
411                        check(&mutated);
412                        cases += 1;
413                    }
414                }
415            }
416            for _ in 0..20 {
417                let mut mutated = packed.clone();
418                for _ in 0..(1 + rng.next() % 4) {
419                    let at = (rng.next() as usize) % mutated.len();
420                    mutated[at] = rng.next() as u8;
421                }
422                check(&mutated);
423                cases += 1;
424            }
425        }
426        // Duplicate keys: the first entry wins in both decoders.
427        let adv = random_adv(&mut rng);
428        let mut packed = adv.pack(0);
429        packed[0] = 0x80 | 12;
430        msgpack::pack_str("t", &mut packed);
431        msgpack::pack_uint(5, &mut packed);
432        assert!(same(
433            &ResourceAdvertisement::unpack(&packed).ok(),
434            &unpack_reference(&packed)
435        ));
436        assert!(cases > 10_000);
437    }
438
439    fn make_adv(flags: AdvFlags) -> ResourceAdvertisement {
440        ResourceAdvertisement {
441            transfer_size: 1000,
442            data_size: 950,
443            num_parts: 3,
444            resource_hash: vec![0x11; 32],
445            random_hash: vec![0xAA, 0xBB, 0xCC, 0xDD],
446            original_hash: vec![0x22; 32],
447            hashmap: vec![
448                0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C,
449            ],
450            flags,
451            segment_index: 1,
452            total_segments: 1,
453            request_id: None,
454        }
455    }
456
457    #[test]
458    fn test_pack_unpack_roundtrip() {
459        let flags = AdvFlags {
460            encrypted: true,
461            compressed: false,
462            split: false,
463            is_request: false,
464            is_response: false,
465            has_metadata: false,
466            app_flag: false,
467        };
468        let adv = make_adv(flags);
469        let packed = adv.pack(0);
470        let unpacked = ResourceAdvertisement::unpack(&packed).unwrap();
471
472        assert_eq!(unpacked.transfer_size, 1000);
473        assert_eq!(unpacked.data_size, 950);
474        assert_eq!(unpacked.num_parts, 3);
475        assert_eq!(unpacked.resource_hash, vec![0x11; 32]);
476        assert_eq!(unpacked.random_hash, vec![0xAA, 0xBB, 0xCC, 0xDD]);
477        assert_eq!(unpacked.original_hash, vec![0x22; 32]);
478        assert_eq!(unpacked.flags, flags);
479        assert_eq!(unpacked.segment_index, 1);
480        assert_eq!(unpacked.total_segments, 1);
481        assert!(unpacked.request_id.is_none());
482    }
483
484    #[test]
485    fn test_flags_encrypted_compressed() {
486        let flags = AdvFlags {
487            encrypted: true,
488            compressed: true,
489            split: false,
490            is_request: false,
491            is_response: false,
492            has_metadata: false,
493            app_flag: false,
494        };
495        let adv = make_adv(flags);
496        let packed = adv.pack(0);
497        let unpacked = ResourceAdvertisement::unpack(&packed).unwrap();
498        assert!(unpacked.flags.encrypted);
499        assert!(unpacked.flags.compressed);
500        assert!(!unpacked.flags.split);
501    }
502
503    #[test]
504    fn test_flags_with_metadata() {
505        let flags = AdvFlags {
506            encrypted: true,
507            compressed: false,
508            split: false,
509            is_request: false,
510            is_response: false,
511            has_metadata: true,
512            app_flag: false,
513        };
514        let adv = make_adv(flags);
515        let packed = adv.pack(0);
516        let unpacked = ResourceAdvertisement::unpack(&packed).unwrap();
517        assert!(unpacked.flags.has_metadata);
518    }
519
520    #[test]
521    fn test_multi_segment() {
522        let flags = AdvFlags {
523            encrypted: true,
524            compressed: false,
525            split: true,
526            is_request: false,
527            is_response: false,
528            has_metadata: false,
529            app_flag: false,
530        };
531        let mut adv = make_adv(flags);
532        adv.segment_index = 2;
533        adv.total_segments = 5;
534        let packed = adv.pack(0);
535        let unpacked = ResourceAdvertisement::unpack(&packed).unwrap();
536        assert!(unpacked.flags.split);
537        assert_eq!(unpacked.segment_index, 2);
538        assert_eq!(unpacked.total_segments, 5);
539    }
540
541    #[test]
542    fn test_with_request_id() {
543        let flags = AdvFlags {
544            encrypted: true,
545            compressed: false,
546            split: false,
547            is_request: true,
548            is_response: false,
549            has_metadata: false,
550            app_flag: false,
551        };
552        let mut adv = make_adv(flags);
553        adv.request_id = Some(vec![0xDE, 0xAD, 0xBE, 0xEF]);
554        let packed = adv.pack(0);
555        let unpacked = ResourceAdvertisement::unpack(&packed).unwrap();
556        assert!(unpacked.is_request());
557        assert!(!unpacked.is_response());
558        assert_eq!(unpacked.request_id, Some(vec![0xDE, 0xAD, 0xBE, 0xEF]));
559    }
560
561    #[test]
562    fn test_is_response() {
563        let flags = AdvFlags {
564            encrypted: true,
565            compressed: false,
566            split: false,
567            is_request: false,
568            is_response: true,
569            has_metadata: false,
570            app_flag: false,
571        };
572        let mut adv = make_adv(flags);
573        adv.request_id = Some(vec![0x42; 16]);
574        assert!(adv.is_response());
575        assert!(!adv.is_request());
576    }
577
578    #[test]
579    fn test_nil_request_id() {
580        let flags = AdvFlags {
581            encrypted: true,
582            compressed: false,
583            split: false,
584            is_request: false,
585            is_response: false,
586            has_metadata: false,
587            app_flag: false,
588        };
589        let adv = make_adv(flags);
590        let packed = adv.pack(0);
591        let unpacked = ResourceAdvertisement::unpack(&packed).unwrap();
592        assert!(unpacked.request_id.is_none());
593        assert!(!unpacked.is_request());
594        assert!(!unpacked.is_response());
595    }
596
597    #[test]
598    fn test_hashmap_segmentation() {
599        // Create a large hashmap with > HASHMAP_MAX_LEN(74) hashes
600        let num_hashes = 100;
601        let hashmap: Vec<u8> = (0..num_hashes).flat_map(|i| vec![i as u8; 4]).collect();
602
603        let flags = AdvFlags {
604            encrypted: true,
605            compressed: false,
606            split: false,
607            is_request: false,
608            is_response: false,
609            has_metadata: false,
610            app_flag: false,
611        };
612        let adv = ResourceAdvertisement {
613            transfer_size: 50000,
614            data_size: 48000,
615            num_parts: num_hashes,
616            resource_hash: vec![0x11; 32],
617            random_hash: vec![0xAA; 4],
618            original_hash: vec![0x22; 32],
619            hashmap: hashmap.clone(),
620            flags,
621            segment_index: 1,
622            total_segments: 1,
623            request_id: None,
624        };
625
626        // Segment 0: first 74 hashes = 296 bytes
627        let packed0 = adv.pack(0);
628        let unpacked0 = ResourceAdvertisement::unpack(&packed0).unwrap();
629        assert_eq!(unpacked0.hashmap.len(), 74 * 4);
630
631        // Segment 1: remaining 26 hashes = 104 bytes
632        let packed1 = adv.pack(1);
633        let unpacked1 = ResourceAdvertisement::unpack(&packed1).unwrap();
634        assert_eq!(unpacked1.hashmap.len(), 26 * 4);
635    }
636
637    #[test]
638    fn test_hashmap_segments_count() {
639        let flags = AdvFlags {
640            encrypted: true,
641            compressed: false,
642            split: false,
643            is_request: false,
644            is_response: false,
645            has_metadata: false,
646            app_flag: false,
647        };
648        let mut adv = make_adv(flags);
649
650        adv.num_parts = 74; // exactly HASHMAP_MAX_LEN
651        assert_eq!(adv.hashmap_segments(), 1);
652
653        adv.num_parts = 75;
654        assert_eq!(adv.hashmap_segments(), 2);
655
656        adv.num_parts = 148;
657        assert_eq!(adv.hashmap_segments(), 2);
658
659        adv.num_parts = 149;
660        assert_eq!(adv.hashmap_segments(), 3);
661    }
662
663    #[test]
664    fn test_unpack_invalid_data() {
665        assert!(ResourceAdvertisement::unpack(&[]).is_err());
666        assert!(ResourceAdvertisement::unpack(&[0xc0]).is_err()); // nil
667        assert!(ResourceAdvertisement::unpack(&[0x01, 0x02]).is_err()); // not a map
668    }
669
670    #[test]
671    fn test_unpack_accepts_exact_upstream_transfer_limit() {
672        let flags = AdvFlags {
673            encrypted: true,
674            compressed: false,
675            split: false,
676            is_request: false,
677            is_response: false,
678            has_metadata: false,
679            app_flag: false,
680        };
681        let mut adv = make_adv(flags);
682        adv.transfer_size = (crate::constants::RESOURCE_MAX_EFFICIENT_SIZE * 3) as u64;
683
684        let unpacked = ResourceAdvertisement::unpack(&adv.pack(0)).unwrap();
685        assert_eq!(unpacked.transfer_size, adv.transfer_size);
686    }
687
688    #[test]
689    fn test_unpack_rejects_transfer_above_upstream_limit() {
690        let flags = AdvFlags {
691            encrypted: true,
692            compressed: false,
693            split: false,
694            is_request: false,
695            is_response: false,
696            has_metadata: false,
697            app_flag: false,
698        };
699        let mut adv = make_adv(flags);
700        adv.transfer_size = (crate::constants::RESOURCE_MAX_EFFICIENT_SIZE * 3 + 1) as u64;
701
702        assert_eq!(
703            ResourceAdvertisement::unpack(&adv.pack(0)).unwrap_err(),
704            ResourceError::InvalidAdvertisement
705        );
706    }
707}