1use std::panic::{catch_unwind, AssertUnwindSafe};
17use std::ptr;
18
19use pamoja_serial::{cobs, slip, SerialError};
20
21use crate::{read_bytes, set_last_error, PamojaBuffer, PamojaStatus};
22
23pub const PAMOJA_SERIAL_FRAME_MAX: usize = 2048;
31
32pub struct PamojaFrames {
37 frames: Vec<Vec<u8>>,
38}
39
40pub struct PamojaSlipDecoder {
42 inner: slip::SlipDecoder<PAMOJA_SERIAL_FRAME_MAX>,
43 discarded: u64,
44}
45
46pub struct PamojaCobsDecoder {
48 inner: cobs::CobsDecoder<PAMOJA_SERIAL_FRAME_MAX>,
49 discarded: u64,
50}
51
52#[no_mangle]
66pub unsafe extern "C" fn pamoja_serial_slip_encode(
67 payload: *const u8,
68 payload_len: usize,
69 out_buffer: *mut *mut PamojaBuffer,
70) -> PamojaStatus {
71 frame(
72 payload,
73 payload_len,
74 out_buffer,
75 slip::max_encoded_len,
76 slip::encode,
77 )
78}
79
80#[no_mangle]
95pub unsafe extern "C" fn pamoja_serial_slip_decode(
96 frame: *const u8,
97 frame_len: usize,
98 out_buffer: *mut *mut PamojaBuffer,
99) -> PamojaStatus {
100 unframe(frame, frame_len, out_buffer, slip::decode)
101}
102
103#[no_mangle]
117pub unsafe extern "C" fn pamoja_serial_cobs_encode(
118 payload: *const u8,
119 payload_len: usize,
120 out_buffer: *mut *mut PamojaBuffer,
121) -> PamojaStatus {
122 frame(
123 payload,
124 payload_len,
125 out_buffer,
126 cobs::max_encoded_len,
127 cobs::encode,
128 )
129}
130
131#[no_mangle]
146pub unsafe extern "C" fn pamoja_serial_cobs_decode(
147 frame: *const u8,
148 frame_len: usize,
149 out_buffer: *mut *mut PamojaBuffer,
150) -> PamojaStatus {
151 unframe(frame, frame_len, out_buffer, cobs::decode)
152}
153
154#[no_mangle]
160pub extern "C" fn pamoja_serial_slip_max_encoded_len(payload_len: usize) -> usize {
161 slip::max_encoded_len(payload_len)
162}
163
164#[no_mangle]
170pub extern "C" fn pamoja_serial_cobs_max_encoded_len(payload_len: usize) -> usize {
171 cobs::max_encoded_len(payload_len)
172}
173
174#[no_mangle]
180pub extern "C" fn pamoja_slip_decoder_new() -> *mut PamojaSlipDecoder {
181 Box::into_raw(Box::new(PamojaSlipDecoder {
182 inner: slip::SlipDecoder::new(),
183 discarded: 0,
184 }))
185}
186
187#[no_mangle]
202pub unsafe extern "C" fn pamoja_slip_decoder_feed(
203 decoder: *mut PamojaSlipDecoder,
204 bytes: *const u8,
205 bytes_len: usize,
206 out_frames: *mut *mut PamojaFrames,
207) -> PamojaStatus {
208 let out_frames = match out_slot(out_frames, "out_frames") {
209 Ok(slot) => slot,
210 Err(status) => return status,
211 };
212 if decoder.is_null() {
213 set_last_error("decoder must not be null".to_owned());
214 return PamojaStatus::InvalidArgument;
215 }
216 let bytes = match read_bytes(bytes, bytes_len) {
217 Ok(bytes) => bytes,
218 Err(status) => return status,
219 };
220 let decoder = &mut *decoder;
221 match catch_unwind(AssertUnwindSafe(|| {
222 let mut frames = Vec::new();
223 for &byte in &bytes {
224 match decoder.inner.push(byte) {
225 Ok(Some(frame)) => frames.push(frame.to_vec()),
226 Ok(None) => {}
227 Err(_) => decoder.discarded += 1,
228 }
229 }
230 frames
231 })) {
232 Ok(frames) => {
233 *out_frames = Box::into_raw(Box::new(PamojaFrames { frames }));
234 PamojaStatus::Ok
235 }
236 Err(_) => panicked(),
237 }
238}
239
240#[no_mangle]
250pub unsafe extern "C" fn pamoja_slip_decoder_discarded(decoder: *const PamojaSlipDecoder) -> u64 {
251 if decoder.is_null() {
252 return 0;
253 }
254 (*decoder).discarded
255}
256
257#[no_mangle]
265pub unsafe extern "C" fn pamoja_slip_decoder_reset(decoder: *mut PamojaSlipDecoder) {
266 if !decoder.is_null() {
267 (*decoder).inner.reset();
268 }
269}
270
271#[no_mangle]
280pub unsafe extern "C" fn pamoja_slip_decoder_free(decoder: *mut PamojaSlipDecoder) {
281 if !decoder.is_null() {
282 drop(Box::from_raw(decoder));
283 }
284}
285
286#[no_mangle]
292pub extern "C" fn pamoja_cobs_decoder_new() -> *mut PamojaCobsDecoder {
293 Box::into_raw(Box::new(PamojaCobsDecoder {
294 inner: cobs::CobsDecoder::new(),
295 discarded: 0,
296 }))
297}
298
299#[no_mangle]
313pub unsafe extern "C" fn pamoja_cobs_decoder_feed(
314 decoder: *mut PamojaCobsDecoder,
315 bytes: *const u8,
316 bytes_len: usize,
317 out_frames: *mut *mut PamojaFrames,
318) -> PamojaStatus {
319 let out_frames = match out_slot(out_frames, "out_frames") {
320 Ok(slot) => slot,
321 Err(status) => return status,
322 };
323 if decoder.is_null() {
324 set_last_error("decoder must not be null".to_owned());
325 return PamojaStatus::InvalidArgument;
326 }
327 let bytes = match read_bytes(bytes, bytes_len) {
328 Ok(bytes) => bytes,
329 Err(status) => return status,
330 };
331 let decoder = &mut *decoder;
332 match catch_unwind(AssertUnwindSafe(|| {
333 let mut frames = Vec::new();
334 for &byte in &bytes {
335 match decoder.inner.push(byte) {
336 Ok(Some(frame)) => frames.push(frame.to_vec()),
337 Ok(None) => {}
338 Err(_) => decoder.discarded += 1,
339 }
340 }
341 frames
342 })) {
343 Ok(frames) => {
344 *out_frames = Box::into_raw(Box::new(PamojaFrames { frames }));
345 PamojaStatus::Ok
346 }
347 Err(_) => panicked(),
348 }
349}
350
351#[no_mangle]
361pub unsafe extern "C" fn pamoja_cobs_decoder_discarded(decoder: *const PamojaCobsDecoder) -> u64 {
362 if decoder.is_null() {
363 return 0;
364 }
365 (*decoder).discarded
366}
367
368#[no_mangle]
376pub unsafe extern "C" fn pamoja_cobs_decoder_reset(decoder: *mut PamojaCobsDecoder) {
377 if !decoder.is_null() {
378 (*decoder).inner.reset();
379 }
380}
381
382#[no_mangle]
391pub unsafe extern "C" fn pamoja_cobs_decoder_free(decoder: *mut PamojaCobsDecoder) {
392 if !decoder.is_null() {
393 drop(Box::from_raw(decoder));
394 }
395}
396
397#[no_mangle]
407pub unsafe extern "C" fn pamoja_frames_count(frames: *const PamojaFrames) -> usize {
408 if frames.is_null() {
409 return 0;
410 }
411 (*frames).frames.len()
412}
413
414#[no_mangle]
428pub unsafe extern "C" fn pamoja_frames_data(
429 frames: *const PamojaFrames,
430 index: usize,
431) -> *const u8 {
432 if frames.is_null() {
433 return ptr::null();
434 }
435 let frames = &*frames;
436 match frames.frames.get(index) {
437 Some(frame) => frame.as_ptr(),
438 None => ptr::null(),
439 }
440}
441
442#[no_mangle]
452pub unsafe extern "C" fn pamoja_frames_len(frames: *const PamojaFrames, index: usize) -> usize {
453 if frames.is_null() {
454 return 0;
455 }
456 let frames = &*frames;
457 frames.frames.get(index).map_or(0, Vec::len)
458}
459
460#[no_mangle]
469pub unsafe extern "C" fn pamoja_frames_free(frames: *mut PamojaFrames) {
470 if !frames.is_null() {
471 drop(Box::from_raw(frames));
472 }
473}
474
475unsafe fn frame(
483 payload: *const u8,
484 payload_len: usize,
485 out_buffer: *mut *mut PamojaBuffer,
486 bound: fn(usize) -> usize,
487 encode: fn(&[u8], &mut [u8]) -> Result<usize, SerialError>,
488) -> PamojaStatus {
489 let out_buffer = match out_slot(out_buffer, "out_buffer") {
490 Ok(slot) => slot,
491 Err(status) => return status,
492 };
493 let payload = match read_bytes(payload, payload_len) {
494 Ok(payload) => payload,
495 Err(status) => return status,
496 };
497 match catch_unwind(AssertUnwindSafe(|| {
498 let mut out = vec![0u8; bound(payload.len())];
499 encode(&payload, &mut out).map(|written| {
500 out.truncate(written);
501 out
502 })
503 })) {
504 Ok(Ok(bytes)) => {
505 *out_buffer = PamojaBuffer::into_raw(bytes);
506 PamojaStatus::Ok
507 }
508 Ok(Err(error)) => failed(error),
509 Err(_) => panicked(),
510 }
511}
512
513unsafe fn unframe(
524 frame: *const u8,
525 frame_len: usize,
526 out_buffer: *mut *mut PamojaBuffer,
527 decode: fn(&[u8], &mut [u8]) -> Result<usize, SerialError>,
528) -> PamojaStatus {
529 let out_buffer = match out_slot(out_buffer, "out_buffer") {
530 Ok(slot) => slot,
531 Err(status) => return status,
532 };
533 let frame = match read_bytes(frame, frame_len) {
534 Ok(frame) => frame,
535 Err(status) => return status,
536 };
537 match catch_unwind(AssertUnwindSafe(|| {
538 let mut out = vec![0u8; frame.len()];
539 decode(&frame, &mut out).map(|written| {
540 out.truncate(written);
541 out
542 })
543 })) {
544 Ok(Ok(bytes)) => {
545 *out_buffer = PamojaBuffer::into_raw(bytes);
546 PamojaStatus::Ok
547 }
548 Ok(Err(error)) => failed(error),
549 Err(_) => panicked(),
550 }
551}
552
553unsafe fn out_slot<'a, T>(out: *mut *mut T, name: &str) -> Result<&'a mut *mut T, PamojaStatus> {
559 if out.is_null() {
560 set_last_error(format!("{name} must not be null"));
561 return Err(PamojaStatus::InvalidArgument);
562 }
563 let slot = &mut *out;
564 *slot = ptr::null_mut();
565 Ok(slot)
566}
567
568fn failed(error: SerialError) -> PamojaStatus {
570 set_last_error(error.to_string());
571 match error {
572 SerialError::BufferTooSmall => PamojaStatus::InvalidArgument,
573 SerialError::InvalidEscape | SerialError::TruncatedFrame => PamojaStatus::Codec,
574 }
575}
576
577fn panicked() -> PamojaStatus {
579 set_last_error("panic at the FFI boundary".to_owned());
580 PamojaStatus::Panic
581}
582
583#[cfg(test)]
584mod tests {
585 use super::*;
586 use crate::{pamoja_buffer_data, pamoja_buffer_free, pamoja_buffer_len};
587
588 unsafe fn take(buffer: *mut PamojaBuffer) -> Vec<u8> {
594 let bytes =
595 std::slice::from_raw_parts(pamoja_buffer_data(buffer), pamoja_buffer_len(buffer))
596 .to_vec();
597 pamoja_buffer_free(buffer);
598 bytes
599 }
600
601 unsafe fn drain(frames: *mut PamojaFrames) -> Vec<Vec<u8>> {
607 let collected = (0..pamoja_frames_count(frames))
608 .map(|index| {
609 std::slice::from_raw_parts(
610 pamoja_frames_data(frames, index),
611 pamoja_frames_len(frames, index),
612 )
613 .to_vec()
614 })
615 .collect();
616 pamoja_frames_free(frames);
617 collected
618 }
619
620 #[test]
621 fn a_payload_round_trips_through_slip() {
622 let payload = b"gps:37.42,-122.08\xc0\xdb";
623 let mut framed = ptr::null_mut();
624 let mut back = ptr::null_mut();
625
626 unsafe {
628 assert_eq!(
629 pamoja_serial_slip_encode(payload.as_ptr(), payload.len(), &mut framed),
630 PamojaStatus::Ok
631 );
632 let frame = take(framed);
633 assert_eq!(
634 pamoja_serial_slip_decode(frame.as_ptr(), frame.len(), &mut back),
635 PamojaStatus::Ok
636 );
637 assert_eq!(take(back), payload);
638 }
639 }
640
641 #[test]
642 fn a_payload_round_trips_through_cobs() {
643 let payload = b"\x11\x22\x00\x33";
644 let mut framed = ptr::null_mut();
645 let mut back = ptr::null_mut();
646
647 unsafe {
649 assert_eq!(
650 pamoja_serial_cobs_encode(payload.as_ptr(), payload.len(), &mut framed),
651 PamojaStatus::Ok
652 );
653 let frame = take(framed);
654 assert_eq!(frame[frame.len() - 1], 0x00, "the frame delimiter");
655 assert_eq!(
656 pamoja_serial_cobs_decode(frame.as_ptr(), frame.len(), &mut back),
657 PamojaStatus::Ok
658 );
659 assert_eq!(take(back), payload);
660 }
661 }
662
663 #[test]
664 fn a_corrupt_frame_reports_a_codec_status() {
665 let frame = [0xDBu8, 0x01, 0xC0];
667 let mut out = ptr::null_mut();
668 let status = unsafe { pamoja_serial_slip_decode(frame.as_ptr(), frame.len(), &mut out) };
670 assert_eq!(status, PamojaStatus::Codec);
671 assert!(out.is_null());
672 }
673
674 #[test]
675 fn a_stream_split_across_chunks_still_yields_whole_frames() {
676 let decoder = pamoja_slip_decoder_new();
677 let stream = [b'o', b'k', 0xC0, b'g', b'o', 0xC0];
678 let mut collected = Vec::new();
679
680 unsafe {
682 for chunk in stream.chunks(2) {
683 let mut frames = ptr::null_mut();
684 assert_eq!(
685 pamoja_slip_decoder_feed(decoder, chunk.as_ptr(), chunk.len(), &mut frames),
686 PamojaStatus::Ok
687 );
688 collected.extend(drain(frames));
689 }
690 assert_eq!(pamoja_slip_decoder_discarded(decoder), 0);
691 pamoja_slip_decoder_free(decoder);
692 }
693
694 assert_eq!(collected, vec![b"ok".to_vec(), b"go".to_vec()]);
695 }
696
697 #[test]
698 fn a_corrupt_frame_mid_stream_is_counted_and_the_rest_survive() {
699 let decoder = pamoja_slip_decoder_new();
700 let stream = [b'o', b'k', 0xC0, 0xDB, 0xC0, b'g', b'o', 0xC0];
702 let mut frames = ptr::null_mut();
703
704 let collected = unsafe {
707 assert_eq!(
708 pamoja_slip_decoder_feed(decoder, stream.as_ptr(), stream.len(), &mut frames),
709 PamojaStatus::Ok
710 );
711 let collected = drain(frames);
712 assert_eq!(pamoja_slip_decoder_discarded(decoder), 1);
713 pamoja_slip_decoder_free(decoder);
714 collected
715 };
716
717 assert_eq!(
718 collected,
719 vec![b"ok".to_vec(), b"go".to_vec()],
720 "the frames either side of the corrupt one are still delivered"
721 );
722 }
723
724 #[test]
725 fn a_cobs_stream_reassembles_across_chunks() {
726 let decoder = pamoja_cobs_decoder_new();
727 let stream = [0x03, 0x11, 0x22, 0x02, 0x33, 0x00];
728 let mut collected = Vec::new();
729
730 unsafe {
732 for chunk in stream.chunks(4) {
733 let mut frames = ptr::null_mut();
734 assert_eq!(
735 pamoja_cobs_decoder_feed(decoder, chunk.as_ptr(), chunk.len(), &mut frames),
736 PamojaStatus::Ok
737 );
738 collected.extend(drain(frames));
739 }
740 pamoja_cobs_decoder_reset(decoder);
741 pamoja_cobs_decoder_free(decoder);
742 }
743
744 assert_eq!(collected, vec![vec![0x11, 0x22, 0x00, 0x33]]);
745 }
746
747 #[test]
748 fn a_null_decoder_is_rejected() {
749 let mut frames = ptr::null_mut();
750 let status =
752 unsafe { pamoja_slip_decoder_feed(ptr::null_mut(), ptr::null(), 0, &mut frames) };
753 assert_eq!(status, PamojaStatus::InvalidArgument);
754 assert!(frames.is_null());
755 }
756
757 #[test]
758 fn the_worst_case_bounds_are_reported() {
759 assert_eq!(
760 pamoja_serial_slip_max_encoded_len(4),
761 slip::max_encoded_len(4)
762 );
763 assert_eq!(
764 pamoja_serial_cobs_max_encoded_len(4),
765 cobs::max_encoded_len(4)
766 );
767 }
768}