1use std::collections::VecDeque;
10use std::pin::Pin;
11use std::task::{Context, Poll};
12#[cfg(test)]
13use std::time::{Duration, SystemTime, UNIX_EPOCH};
14
15#[cfg(test)]
16use asupersync::Budget as AsBudget;
17use asupersync::error::ErrorKind as AsErrorKind;
18use asupersync::raptorq::{RaptorQReceiverBuilder, RaptorQSenderBuilder};
19use asupersync::security::AuthenticationTag;
20use asupersync::security::authenticated::AuthenticatedSymbol;
21use asupersync::transport::error::{SinkError, StreamError};
22use asupersync::transport::sink::SymbolSink;
23use asupersync::transport::stream::SymbolStream;
24#[cfg(test)]
25use asupersync::types::Time as AsTime;
26use asupersync::types::{
27 CancelKind as AsCancelKind, CancelReason as AsCancelReason, ObjectId as AsObjectId,
28 ObjectParams, Symbol, SymbolId, SymbolKind,
29};
30use asupersync::{Cx as AsCx, RaptorQConfig};
31
32use fsqlite_error::{FrankenError, Result};
33use fsqlite_types::cx::Cx;
34
35use crate::raptorq_integration::{
36 CodecDecodeResult, CodecEncodeResult, DecodeFailureReason, SymbolCodec,
37};
38
39const BEAD_ID: &str = "bd-3sj9w";
40
41const PRODUCTION_OBJECT_ID: u64 = 0xF5_3D9A_0001;
46
47const PACKED_KIND_REPAIR_BIT: u32 = 1_u32 << 31;
57const PACKED_SBN_SHIFT: u32 = 23;
58const PACKED_SBN_MASK: u32 = 0xFF;
59const PACKED_ESI_MASK: u32 = 0x7F_FFFF;
60
61pub fn pack_symbol_key(kind: SymbolKind, sbn: u8, esi: u32) -> Result<u32> {
65 if esi > PACKED_ESI_MASK {
66 return Err(FrankenError::OutOfRange {
67 what: "packed symbol esi (must fit 23 bits)".to_owned(),
68 value: esi.to_string(),
69 });
70 }
71
72 let kind_bit = if kind.is_repair() {
73 PACKED_KIND_REPAIR_BIT
74 } else {
75 0
76 };
77 Ok(kind_bit | (u32::from(sbn) << PACKED_SBN_SHIFT) | esi)
78}
79
80#[must_use]
82pub fn unpack_symbol_key(packed: u32) -> (SymbolKind, u8, u32) {
83 let kind = if packed & PACKED_KIND_REPAIR_BIT == 0 {
84 SymbolKind::Source
85 } else {
86 SymbolKind::Repair
87 };
88 #[allow(clippy::cast_possible_truncation)]
89 let sbn = ((packed >> PACKED_SBN_SHIFT) & PACKED_SBN_MASK) as u8;
90 let esi = packed & PACKED_ESI_MASK;
91 (kind, sbn, esi)
92}
93
94#[derive(Debug)]
100struct VecTransportSink {
101 symbols: Vec<Symbol>,
102}
103
104impl VecTransportSink {
105 fn new() -> Self {
106 Self {
107 symbols: Vec::new(),
108 }
109 }
110}
111
112impl SymbolSink for VecTransportSink {
113 fn poll_send(
114 mut self: Pin<&mut Self>,
115 _cx: &mut Context<'_>,
116 symbol: AuthenticatedSymbol,
117 ) -> Poll<std::result::Result<(), SinkError>> {
118 self.symbols.push(symbol.into_symbol());
119 Poll::Ready(Ok(()))
120 }
121
122 fn poll_flush(
123 self: Pin<&mut Self>,
124 _cx: &mut Context<'_>,
125 ) -> Poll<std::result::Result<(), SinkError>> {
126 Poll::Ready(Ok(()))
127 }
128
129 fn poll_close(
130 self: Pin<&mut Self>,
131 _cx: &mut Context<'_>,
132 ) -> Poll<std::result::Result<(), SinkError>> {
133 Poll::Ready(Ok(()))
134 }
135
136 fn poll_ready(
137 self: Pin<&mut Self>,
138 _cx: &mut Context<'_>,
139 ) -> Poll<std::result::Result<(), SinkError>> {
140 Poll::Ready(Ok(()))
141 }
142}
143
144#[derive(Debug)]
146struct VecTransportStream {
147 symbols: VecDeque<AuthenticatedSymbol>,
148}
149
150impl VecTransportStream {
151 fn new(symbols: Vec<Symbol>) -> Self {
152 let symbols = symbols
153 .into_iter()
154 .map(|symbol| AuthenticatedSymbol::from_parts(symbol, AuthenticationTag::zero()))
155 .collect();
156 Self { symbols }
157 }
158}
159
160impl SymbolStream for VecTransportStream {
161 fn poll_next(
162 mut self: Pin<&mut Self>,
163 _cx: &mut Context<'_>,
164 ) -> Poll<Option<std::result::Result<AuthenticatedSymbol, StreamError>>> {
165 match self.symbols.pop_front() {
166 Some(symbol) => Poll::Ready(Some(Ok(symbol))),
167 None => Poll::Ready(None),
168 }
169 }
170
171 fn size_hint(&self) -> (usize, Option<usize>) {
172 (self.symbols.len(), Some(self.symbols.len()))
173 }
174
175 fn is_exhausted(&self) -> bool {
176 self.symbols.is_empty()
177 }
178}
179
180#[derive(Debug, Clone)]
197pub struct AsupersyncCodec {
198 max_block_size: usize,
200}
201
202impl AsupersyncCodec {
203 #[must_use]
205 pub const fn new(max_block_size: usize) -> Self {
206 Self { max_block_size }
207 }
208}
209
210impl Default for AsupersyncCodec {
211 fn default() -> Self {
212 Self::new(64 * 1024)
213 }
214}
215
216#[cfg(test)]
217fn native_budget_from_local(cx: &Cx) -> AsBudget {
218 let budget = cx.budget();
219 let mut native_budget = AsBudget::new()
220 .with_poll_quota(budget.poll_quota)
221 .with_priority(budget.priority);
222 if let Some(cost_quota) = budget.cost_quota {
223 native_budget = native_budget.with_cost_quota(cost_quota);
224 }
225 if let Some(deadline) = budget.deadline {
226 native_budget = native_budget.with_deadline(local_deadline_to_native_time(deadline));
227 }
228 native_budget
229}
230
231#[cfg(test)]
232fn wall_clock_now_since_epoch() -> Duration {
233 SystemTime::now()
234 .duration_since(UNIX_EPOCH)
235 .unwrap_or(Duration::ZERO)
236}
237
238#[cfg(test)]
239fn local_deadline_to_native_time(deadline: Duration) -> AsTime {
240 let absolute_deadline = wall_clock_now_since_epoch()
241 .checked_add(deadline)
242 .unwrap_or(Duration::MAX);
243 let nanos = u64::try_from(absolute_deadline.as_nanos()).unwrap_or(u64::MAX);
244 AsTime::from_nanos(nanos)
245}
246
247fn is_native_abort(kind: AsErrorKind) -> bool {
248 matches!(
249 kind,
250 AsErrorKind::Cancelled
251 | AsErrorKind::CancelTimeout
252 | AsErrorKind::DeadlineExceeded
253 | AsErrorKind::PollQuotaExhausted
254 | AsErrorKind::CostQuotaExhausted
255 )
256}
257
258fn native_reason_to_local(reason: &AsCancelReason) -> fsqlite_types::cx::CancelReason {
259 match reason.kind {
260 AsCancelKind::User => fsqlite_types::cx::CancelReason::UserInterrupt,
261 AsCancelKind::Timeout
262 | AsCancelKind::Deadline
263 | AsCancelKind::PollQuota
264 | AsCancelKind::CostBudget => fsqlite_types::cx::CancelReason::Timeout,
265 AsCancelKind::FailFast
266 | AsCancelKind::RaceLost
267 | AsCancelKind::ParentCancelled
268 | AsCancelKind::Shutdown
269 | AsCancelKind::LinkedExit => fsqlite_types::cx::CancelReason::RegionClose,
270 AsCancelKind::ResourceUnavailable => fsqlite_types::cx::CancelReason::Abort,
271 }
272}
273
274fn sync_local_cancel_from_native(codec_cx: &Cx, native_cx: &AsCx) {
275 if let Some(reason) = native_cx.cancel_reason() {
276 codec_cx.cancel_with_reason(native_reason_to_local(&reason));
277 } else if native_cx.is_cancel_requested() {
278 codec_cx.cancel();
279 }
280}
281
282fn missing_native_cx_error() -> FrankenError {
283 FrankenError::Internal(format!(
284 "{BEAD_ID}: asupersync RaptorQ codec requires an attached native Cx or an ambient asupersync runtime Cx"
285 ))
286}
287
288fn derive_native_request_cx(cx: &Cx) -> Result<(Cx, AsCx)> {
289 let codec_cx = cx.create_child();
290 if let Some(reason) = cx.cancel_reason() {
291 codec_cx.cancel_with_reason(reason);
292 } else if cx.is_cancel_requested() {
293 codec_cx.cancel();
294 }
295
296 let native_cx = cx
297 .attached_native_cx()
298 .or_else(AsCx::current)
299 .ok_or_else(missing_native_cx_error)?;
300 sync_local_cancel_from_native(&codec_cx, &native_cx);
301 codec_cx.set_native_cx(native_cx.clone());
302 Ok((codec_cx, native_cx))
303}
304
305fn decode_object_params(
306 object_id: AsObjectId,
307 k_source: u32,
308 symbol_size: u32,
309 max_block_size: usize,
310) -> Result<ObjectParams> {
311 let object_size = u64::from(k_source)
312 .checked_mul(u64::from(symbol_size))
313 .ok_or_else(|| FrankenError::OutOfRange {
314 what: "object_size for decode params".to_owned(),
315 value: format!("{k_source}*{symbol_size}"),
316 })?;
317 let symbol_size_u16 = u16::try_from(symbol_size).map_err(|_| FrankenError::OutOfRange {
318 what: "symbol_size as u16".to_owned(),
319 value: symbol_size.to_string(),
320 })?;
321 if object_size == 0 {
322 return Ok(ObjectParams::new(object_id, 0, symbol_size_u16, 0, 0));
323 }
324 if max_block_size == 0 {
325 return Err(FrankenError::OutOfRange {
326 what: "max_block_size (must be > 0)".to_owned(),
327 value: "0".to_owned(),
328 });
329 }
330
331 let max_block_size_u64 =
332 u64::try_from(max_block_size).map_err(|_| FrankenError::OutOfRange {
333 what: "max_block_size as u64".to_owned(),
334 value: max_block_size.to_string(),
335 })?;
336 let source_blocks = u16::try_from(object_size.div_ceil(max_block_size_u64)).map_err(|_| {
337 FrankenError::OutOfRange {
338 what: "source_blocks as u16".to_owned(),
339 value: object_size.div_ceil(max_block_size_u64).to_string(),
340 }
341 })?;
342 let symbols_per_block = u16::try_from(
343 object_size
344 .min(max_block_size_u64)
345 .div_ceil(u64::from(symbol_size_u16)),
346 )
347 .map_err(|_| FrankenError::OutOfRange {
348 what: "symbols_per_block as u16".to_owned(),
349 value: object_size
350 .min(max_block_size_u64)
351 .div_ceil(u64::from(symbol_size_u16))
352 .to_string(),
353 })?;
354
355 Ok(ObjectParams::new(
356 object_id,
357 object_size,
358 symbol_size_u16,
359 source_blocks,
360 symbols_per_block,
361 ))
362}
363
364#[allow(
365 clippy::cast_possible_truncation,
366 clippy::cast_lossless,
367 clippy::cast_precision_loss,
368 clippy::cast_sign_loss
369)]
370impl SymbolCodec for AsupersyncCodec {
371 fn encode(
372 &self,
373 cx: &Cx,
374 source_data: &[u8],
375 symbol_size: u32,
376 repair_overhead: f64,
377 ) -> Result<CodecEncodeResult> {
378 if symbol_size == 0 {
379 return Err(FrankenError::OutOfRange {
380 what: "symbol_size (must be > 0)".to_owned(),
381 value: "0".to_owned(),
382 });
383 }
384 let mut config = RaptorQConfig::default();
385 config.encoding.symbol_size = symbol_size as u16;
386 config.encoding.max_block_size = self.max_block_size;
387 config.encoding.repair_overhead = repair_overhead;
388
389 let (codec_cx, native_cx) = derive_native_request_cx(cx)?;
390 codec_cx.checkpoint().map_err(|_| FrankenError::Abort)?;
391 let object_id = AsObjectId::new_for_test(PRODUCTION_OBJECT_ID);
392 let mut sender = RaptorQSenderBuilder::new()
393 .config(config)
394 .transport(VecTransportSink::new())
395 .build()
396 .map_err(|e| FrankenError::Internal(format!("{BEAD_ID}: sender build: {e}")))?;
397
398 let outcome = sender
399 .send_object(&native_cx, object_id, source_data)
400 .map_err(|e| {
401 if is_native_abort(e.kind()) {
402 FrankenError::Abort
403 } else {
404 FrankenError::Internal(format!("{BEAD_ID}: send_object: {e}"))
405 }
406 })?;
407
408 let symbols = std::mem::take(&mut sender.transport_mut().symbols);
409 let k = outcome.source_symbols as u32;
410
411 let mut source_symbols = Vec::new();
412 let mut repair_symbols = Vec::new();
413 for s in &symbols {
414 let packed_key = pack_symbol_key(s.kind(), s.sbn(), s.esi())?;
415 if s.kind().is_source() {
416 source_symbols.push((packed_key, s.data().to_vec()));
417 } else {
418 repair_symbols.push((packed_key, s.data().to_vec()));
419 }
420 }
421
422 Ok(CodecEncodeResult {
423 source_symbols,
424 repair_symbols,
425 k_source: k,
426 })
427 }
428
429 fn decode(
430 &self,
431 cx: &Cx,
432 symbols: &[(u32, Vec<u8>)],
433 k_source: u32,
434 symbol_size: u32,
435 ) -> Result<CodecDecodeResult> {
436 if symbols.is_empty() {
437 return Ok(CodecDecodeResult::Failure {
438 reason: DecodeFailureReason::InsufficientSymbols,
439 symbols_received: 0,
440 k_required: k_source,
441 });
442 }
443
444 if symbol_size == 0 {
445 return Err(FrankenError::OutOfRange {
446 what: "symbol_size (must be > 0)".to_owned(),
447 value: "0".to_owned(),
448 });
449 }
450
451 let object_id = AsObjectId::new_for_test(PRODUCTION_OBJECT_ID);
452 let mut config = RaptorQConfig::default();
453 config.encoding.symbol_size = symbol_size as u16;
454 config.encoding.max_block_size = self.max_block_size;
455 let params = decode_object_params(object_id, k_source, symbol_size, self.max_block_size)?;
456
457 let mut rebuilt = Vec::with_capacity(symbols.len());
458 for (packed, data) in symbols {
459 let (kind, sbn, esi) = unpack_symbol_key(*packed);
460 rebuilt.push(Symbol::new(
461 SymbolId::new(object_id, sbn, esi),
462 data.clone(),
463 kind,
464 ));
465 }
466
467 let (codec_cx, native_cx) = derive_native_request_cx(cx)?;
468 codec_cx.checkpoint().map_err(|_| FrankenError::Abort)?;
469 let mut receiver = RaptorQReceiverBuilder::new()
470 .config(config)
471 .source(VecTransportStream::new(rebuilt))
472 .build()
473 .map_err(|e| FrankenError::Internal(format!("{BEAD_ID}: receiver build: {e}")))?;
474
475 match receiver.receive_object(&native_cx, ¶ms) {
476 Ok(outcome) => Ok(CodecDecodeResult::Success {
477 data: outcome.data,
478 symbols_used: outcome.symbols_received as u32,
479 peeled_count: 0,
480 inactivated_count: 0,
481 }),
482 Err(err) if is_native_abort(err.kind()) => Err(FrankenError::Abort),
483 Err(err) => {
484 let reason = match err.kind() {
485 AsErrorKind::InsufficientSymbols => DecodeFailureReason::InsufficientSymbols,
486 _ => DecodeFailureReason::SingularMatrix,
487 };
488 Ok(CodecDecodeResult::Failure {
489 reason,
490 symbols_received: symbols.len() as u32,
491 k_required: k_source,
492 })
493 }
494 }
495 }
496}
497
498#[cfg(test)]
503mod tests {
504 use super::*;
505 use fsqlite_types::cx::{CancelReason, Cx};
506
507 fn test_cx() -> Cx {
508 let cx = Cx::new();
509 cx.set_native_cx(AsCx::for_testing());
510 cx
511 }
512
513 #[test]
514 fn test_pack_unpack_source_symbol() {
515 let packed = pack_symbol_key(SymbolKind::Source, 0, 42).unwrap();
516 let (kind, sbn, esi) = unpack_symbol_key(packed);
517 assert_eq!(kind, SymbolKind::Source);
518 assert_eq!(sbn, 0);
519 assert_eq!(esi, 42);
520 }
521
522 #[test]
523 fn test_pack_unpack_repair_symbol() {
524 let packed = pack_symbol_key(SymbolKind::Repair, 3, 100).unwrap();
525 let (kind, sbn, esi) = unpack_symbol_key(packed);
526 assert_eq!(kind, SymbolKind::Repair);
527 assert_eq!(sbn, 3);
528 assert_eq!(esi, 100);
529 }
530
531 #[test]
532 fn test_pack_esi_overflow() {
533 let result = pack_symbol_key(SymbolKind::Source, 0, PACKED_ESI_MASK + 1);
534 assert!(result.is_err());
535 }
536
537 #[test]
538 fn test_pack_max_esi() {
539 let packed = pack_symbol_key(SymbolKind::Source, 0, PACKED_ESI_MASK).unwrap();
540 let (_, _, esi) = unpack_symbol_key(packed);
541 assert_eq!(esi, PACKED_ESI_MASK);
542 }
543
544 #[test]
545 fn test_pack_max_sbn() {
546 let packed = pack_symbol_key(SymbolKind::Repair, 255, 0).unwrap();
547 let (kind, sbn, esi) = unpack_symbol_key(packed);
548 assert_eq!(kind, SymbolKind::Repair);
549 assert_eq!(sbn, 255);
550 assert_eq!(esi, 0);
551 }
552
553 #[test]
554 fn test_codec_encode_decode_roundtrip() {
555 let codec = AsupersyncCodec::default();
556 let cx = test_cx();
557 let data = vec![0xAB_u8; 4096];
558 let symbol_size = 512_u32;
559 let repair_overhead = 1.25;
560
561 let encoded = codec
562 .encode(&cx, &data, symbol_size, repair_overhead)
563 .unwrap();
564 assert!(encoded.k_source > 0);
565 assert!(!encoded.source_symbols.is_empty());
566 assert!(!encoded.repair_symbols.is_empty());
567
568 let mut all_symbols: Vec<(u32, Vec<u8>)> = encoded.source_symbols.clone();
570 all_symbols.extend(encoded.repair_symbols.clone());
571
572 let decoded = codec
573 .decode(&cx, &all_symbols, encoded.k_source, symbol_size)
574 .unwrap();
575 match decoded {
576 CodecDecodeResult::Success {
577 data: recovered, ..
578 } => {
579 assert_eq!(recovered, data);
580 }
581 CodecDecodeResult::Failure { reason, .. } => {
582 panic!("decode failed: {reason:?}");
583 }
584 }
585 }
586
587 #[test]
588 fn test_codec_decode_source_only() {
589 let codec = AsupersyncCodec::default();
590 let cx = test_cx();
591 let data = vec![0xCD_u8; 2048];
592 let symbol_size = 512_u32;
593
594 let encoded = codec.encode(&cx, &data, symbol_size, 1.25).unwrap();
595
596 let decoded = codec
598 .decode(&cx, &encoded.source_symbols, encoded.k_source, symbol_size)
599 .unwrap();
600 match decoded {
601 CodecDecodeResult::Success {
602 data: recovered, ..
603 } => {
604 assert_eq!(recovered, data);
605 }
606 CodecDecodeResult::Failure { reason, .. } => {
607 panic!("source-only decode failed: {reason:?}");
608 }
609 }
610 }
611
612 #[test]
613 fn test_codec_decode_with_erasures() {
614 let codec = AsupersyncCodec::default();
615 let cx = test_cx();
616 let data = vec![0xEF_u8; 4096];
617 let symbol_size = 512_u32;
618
619 let encoded = codec.encode(&cx, &data, symbol_size, 1.5).unwrap();
620 let k = encoded.k_source as usize;
621
622 let mut symbols: Vec<(u32, Vec<u8>)> = encoded.source_symbols[1..].to_vec();
624 symbols.extend(encoded.repair_symbols.iter().take(2).cloned());
625
626 assert!(symbols.len() >= k, "need at least K symbols");
627
628 let decoded = codec
629 .decode(&cx, &symbols, encoded.k_source, symbol_size)
630 .unwrap();
631 match decoded {
632 CodecDecodeResult::Success {
633 data: recovered, ..
634 } => {
635 assert_eq!(recovered, data);
636 }
637 CodecDecodeResult::Failure { reason, .. } => {
638 panic!("erasure decode failed: {reason:?}");
639 }
640 }
641 }
642
643 #[test]
644 fn test_codec_decode_empty() {
645 let codec = AsupersyncCodec::default();
646 let cx = test_cx();
647 let result = codec.decode(&cx, &[], 4, 512).unwrap();
648 assert!(matches!(
649 result,
650 CodecDecodeResult::Failure {
651 reason: DecodeFailureReason::InsufficientSymbols,
652 ..
653 }
654 ));
655 }
656
657 #[test]
658 fn test_codec_default_max_block_size() {
659 let codec = AsupersyncCodec::default();
660 assert_eq!(codec.max_block_size, 64 * 1024);
661 }
662
663 #[test]
664 fn test_codec_custom_max_block_size() {
665 let codec = AsupersyncCodec::new(128 * 1024);
666 let cx = test_cx();
667 assert_eq!(codec.max_block_size, 128 * 1024);
668
669 let data = vec![0x42_u8; 2048];
671 let encoded = codec.encode(&cx, &data, 512, 1.25).unwrap();
672 let decoded = codec
673 .decode(&cx, &encoded.source_symbols, encoded.k_source, 512)
674 .unwrap();
675 assert!(matches!(decoded, CodecDecodeResult::Success { .. }));
676 }
677
678 #[test]
679 fn test_codec_send_sync() {
680 fn assert_send_sync<T: Send + Sync>() {}
682 assert_send_sync::<AsupersyncCodec>();
683 }
684
685 #[test]
686 fn test_codec_large_data_4096_page() {
687 let codec = AsupersyncCodec::default();
688 let cx = test_cx();
689 let data = vec![0x77_u8; 4 * 4096];
691 let encoded = codec.encode(&cx, &data, 4096, 1.25).unwrap();
692 assert!(encoded.k_source >= 4);
693
694 let decoded = codec
695 .decode(&cx, &encoded.source_symbols, encoded.k_source, 4096)
696 .unwrap();
697 match decoded {
698 CodecDecodeResult::Success {
699 data: recovered, ..
700 } => {
701 assert_eq!(recovered, data);
702 }
703 CodecDecodeResult::Failure { reason, .. } => {
704 panic!("large page decode failed: {reason:?}");
705 }
706 }
707 }
708
709 #[test]
710 fn test_codec_repair_symbol_count_scales_with_overhead() {
711 let codec = AsupersyncCodec::default();
712 let cx = test_cx();
713 let data = vec![0x55_u8; 8192];
714
715 let low = codec.encode(&cx, &data, 512, 1.1).unwrap();
716 let high = codec.encode(&cx, &data, 512, 2.0).unwrap();
717
718 assert!(
720 high.repair_symbols.len() > low.repair_symbols.len(),
721 "2.0x overhead ({}) should produce more repairs than 1.1x ({})",
722 high.repair_symbols.len(),
723 low.repair_symbols.len()
724 );
725 }
726
727 #[test]
728 fn test_codec_decode_multiple_source_blocks_roundtrip() {
729 let codec = AsupersyncCodec::new(1024);
730 let cx = test_cx();
731 let data = vec![0x5A_u8; 3 * 1024];
732 let symbol_size = 512_u32;
733
734 let encoded = codec.encode(&cx, &data, symbol_size, 1.25).unwrap();
735 assert!(
736 encoded.source_symbols.iter().any(|(packed, _)| {
737 let (_, sbn, _) = unpack_symbol_key(*packed);
738 sbn > 0
739 }),
740 "test data should span multiple source blocks"
741 );
742
743 let decoded = codec
744 .decode(&cx, &encoded.source_symbols, encoded.k_source, symbol_size)
745 .unwrap();
746 match decoded {
747 CodecDecodeResult::Success {
748 data: recovered, ..
749 } => {
750 assert_eq!(recovered, data);
751 }
752 CodecDecodeResult::Failure { reason, .. } => {
753 panic!("multi-block decode failed: {reason:?}");
754 }
755 }
756 }
757
758 #[test]
759 fn test_pack_all_bits_combined() {
760 let packed = pack_symbol_key(SymbolKind::Repair, 127, 0x3F_FFFF).unwrap();
762 let (kind, sbn, esi) = unpack_symbol_key(packed);
763 assert_eq!(kind, SymbolKind::Repair);
764 assert_eq!(sbn, 127);
765 assert_eq!(esi, 0x3F_FFFF);
766 }
767
768 #[test]
769 fn test_codec_encode_respects_cancelled_cx() {
770 let codec = AsupersyncCodec::default();
771 let cx = test_cx();
772 cx.cancel_with_reason(CancelReason::Abort);
773
774 let err = codec.encode(&cx, &[0xAB; 512], 512, 1.25).unwrap_err();
775 assert!(matches!(err, FrankenError::Abort));
776 }
777
778 #[test]
779 fn test_codec_decode_respects_cancelled_cx() {
780 let codec = AsupersyncCodec::default();
781 let setup_cx = test_cx();
782 let encoded = codec.encode(&setup_cx, &[0xBC; 512], 512, 1.25).unwrap();
783
784 let cx = test_cx();
785 cx.cancel_with_reason(CancelReason::Abort);
786
787 let err = codec
788 .decode(&cx, &encoded.source_symbols, encoded.k_source, 512)
789 .unwrap_err();
790 assert!(matches!(err, FrankenError::Abort));
791 }
792
793 #[test]
794 fn test_local_deadline_converts_to_future_native_time() {
795 let before = wall_clock_now_since_epoch();
796 let cx = Cx::with_budget(
797 fsqlite_types::cx::Budget::INFINITE.with_deadline(Duration::from_millis(50)),
798 );
799
800 let native_budget = native_budget_from_local(&cx);
801 let native_deadline = Duration::from_nanos(
802 native_budget
803 .deadline
804 .expect("native budget should carry a deadline")
805 .as_nanos(),
806 );
807 let lower_bound = before
808 .checked_add(Duration::from_millis(25))
809 .unwrap_or(Duration::MAX);
810
811 assert!(
812 native_deadline >= lower_bound,
813 "native deadline should be an absolute future instant, got {native_deadline:?}"
814 );
815 }
816
817 #[test]
818 fn test_codec_encode_respects_attached_native_cancellation() {
819 let codec = AsupersyncCodec::default();
820 let cx = test_cx();
821 let native = AsCx::for_testing();
822 cx.set_native_cx(native.clone());
823 native.set_cancel_reason(AsCancelReason::timeout());
824
825 let err = codec.encode(&cx, &[0xAB; 512], 512, 1.25).unwrap_err();
826 assert!(matches!(err, FrankenError::Abort));
827 }
828
829 #[test]
830 fn test_codec_decode_respects_attached_native_cancellation() {
831 let codec = AsupersyncCodec::default();
832 let setup_cx = test_cx();
833 let encoded = codec.encode(&setup_cx, &[0xBC; 512], 512, 1.25).unwrap();
834
835 let cx = test_cx();
836 let native = AsCx::for_testing();
837 cx.set_native_cx(native.clone());
838 native.set_cancel_reason(AsCancelReason::timeout());
839
840 let err = codec
841 .decode(&cx, &encoded.source_symbols, encoded.k_source, 512)
842 .unwrap_err();
843 assert!(matches!(err, FrankenError::Abort));
844 }
845
846 #[test]
847 fn test_derive_native_request_cx_mirrors_attached_native_cancellation() {
848 let cx = test_cx();
849 let native = AsCx::for_testing();
850 cx.set_native_cx(native.clone());
851 native.set_cancel_reason(AsCancelReason::timeout());
852
853 let (codec_cx, derived_native) =
854 derive_native_request_cx(&cx).expect("attached native cx should derive");
855
856 assert_eq!(codec_cx.cancel_reason(), Some(CancelReason::Timeout));
857 assert!(codec_cx.is_cancel_requested());
858 assert!(codec_cx.checkpoint().is_err());
859 assert!(derived_native.is_cancel_requested());
860 }
861
862 #[test]
863 fn test_derive_native_request_cx_requires_runtime_or_attachment() {
864 let cx = Cx::new();
865
866 let err = derive_native_request_cx(&cx).unwrap_err();
867
868 assert!(
869 matches!(err, FrankenError::Internal(message) if message.contains("requires an attached native Cx"))
870 );
871 }
872}