1#![allow(unsafe_op_in_unsafe_fn)]
26
27use gbp_stack::core::{ControlOpcode, NodeState, PayloadCodec, SignalType, StreamType};
28use gbp_stack::{
29 CipherSuite, DeliveredPayload, ErrorObject, Event, GapAccept, GapClient, GbpFrame, GroupNode,
30 GspAccept, GspClient, GtpAccept, GtpClient, MlsContext, OutboundFrame, ProcessedKind,
31 SFrameDecryptor, SFrameEncryptor, SFrameSession, StreamLabel,
32};
33use openmls::prelude::tls_codec::Serialize as _;
34use openmls::prelude::*;
35use serde::Serialize;
36use std::cell::RefCell;
37use std::collections::HashMap;
38use std::ffi::{CString, c_char};
39use std::sync::atomic::{AtomicI32, Ordering};
40use std::sync::{Arc, Mutex};
41
42#[repr(C)]
49pub struct GbpBuffer {
50 pub ptr: *mut u8,
52 pub len: usize,
54 pub cap: usize,
56}
57
58impl GbpBuffer {
59 fn empty() -> Self {
60 Self {
61 ptr: std::ptr::null_mut(),
62 len: 0,
63 cap: 0,
64 }
65 }
66 fn from_vec(mut v: Vec<u8>) -> Self {
67 let ptr = v.as_mut_ptr();
68 let len = v.len();
69 let cap = v.capacity();
70 std::mem::forget(v);
71 Self { ptr, len, cap }
72 }
73}
74
75#[unsafe(no_mangle)]
81pub unsafe extern "C" fn gbp_buffer_free(buf: GbpBuffer) {
82 if buf.ptr.is_null() {
83 return;
84 }
85 unsafe {
86 let _ = Vec::from_raw_parts(buf.ptr, buf.len, buf.cap);
87 }
88}
89
90#[unsafe(no_mangle)]
95pub unsafe extern "C" fn gbp_string_free(ptr: *mut c_char) {
96 if ptr.is_null() {
97 return;
98 }
99 unsafe {
100 let _ = CString::from_raw(ptr);
101 }
102}
103
104fn alloc_cstring(s: &str) -> *mut c_char {
105 CString::new(s.as_bytes())
106 .unwrap_or_else(|_| CString::new(s.replace('\0', "?")).unwrap())
107 .into_raw()
108}
109
110thread_local! {
115 static LAST_ERROR: RefCell<String> = const { RefCell::new(String::new()) };
116}
117
118fn set_last_error(e: impl ToString) {
119 LAST_ERROR.with(|s| *s.borrow_mut() = e.to_string());
120}
121
122fn clear_last_error() {
123 LAST_ERROR.with(|s| s.borrow_mut().clear());
124}
125
126#[unsafe(no_mangle)]
128pub extern "C" fn gbp_last_error() -> *mut c_char {
129 LAST_ERROR.with(|s| alloc_cstring(&s.borrow()))
130}
131
132macro_rules! registry {
137 ($vis:vis $name:ident<$t:ty>) => {
138 $vis struct $name {
139 next: AtomicI32,
140 map: Mutex<HashMap<i32, Arc<Mutex<$t>>>>,
141 }
142 impl $name {
143 fn new() -> Self {
144 Self { next: AtomicI32::new(1), map: Mutex::new(HashMap::new()) }
145 }
146 fn insert(&self, v: $t) -> i32 {
147 let id = self.next.fetch_add(1, Ordering::Relaxed);
148 self.map.lock().unwrap().insert(id, Arc::new(Mutex::new(v)));
149 id
150 }
151 fn remove(&self, id: i32) {
152 self.map.lock().unwrap().remove(&id);
153 }
154 fn get(&self, id: i32) -> Option<Arc<Mutex<$t>>> {
155 self.map.lock().unwrap().get(&id).cloned()
156 }
157 }
158 };
159}
160
161registry!(MlsRegistry<MlsContext>);
162registry!(NodeRegistry<GroupNode>);
163registry!(GtpRegistry<GtpClient>);
164registry!(GapRegistry<GapClient>);
165registry!(GspRegistry<GspClient>);
166registry!(SFrameSessionRegistry<SFrameDecryptor>);
167registry!(SFrameEncryptorRegistry<SFrameEncryptor>);
168
169struct MlsBundles {
170 map: Mutex<HashMap<i32, KeyPackageBundle>>,
171}
172impl MlsBundles {
173 fn new() -> Self {
174 Self {
175 map: Mutex::new(HashMap::new()),
176 }
177 }
178}
179
180fn mls() -> &'static MlsRegistry {
181 use std::sync::OnceLock;
182 static R: OnceLock<MlsRegistry> = OnceLock::new();
183 R.get_or_init(MlsRegistry::new)
184}
185fn mls_bundles() -> &'static MlsBundles {
186 use std::sync::OnceLock;
187 static R: OnceLock<MlsBundles> = OnceLock::new();
188 R.get_or_init(MlsBundles::new)
189}
190fn nodes() -> &'static NodeRegistry {
191 use std::sync::OnceLock;
192 static R: OnceLock<NodeRegistry> = OnceLock::new();
193 R.get_or_init(NodeRegistry::new)
194}
195fn gtps() -> &'static GtpRegistry {
196 use std::sync::OnceLock;
197 static R: OnceLock<GtpRegistry> = OnceLock::new();
198 R.get_or_init(GtpRegistry::new)
199}
200fn gaps() -> &'static GapRegistry {
201 use std::sync::OnceLock;
202 static R: OnceLock<GapRegistry> = OnceLock::new();
203 R.get_or_init(GapRegistry::new)
204}
205fn gsps() -> &'static GspRegistry {
206 use std::sync::OnceLock;
207 static R: OnceLock<GspRegistry> = OnceLock::new();
208 R.get_or_init(GspRegistry::new)
209}
210fn sframe_sessions() -> &'static SFrameSessionRegistry {
211 use std::sync::OnceLock;
212 static R: OnceLock<SFrameSessionRegistry> = OnceLock::new();
213 R.get_or_init(SFrameSessionRegistry::new)
214}
215fn sframe_encryptors() -> &'static SFrameEncryptorRegistry {
216 use std::sync::OnceLock;
217 static R: OnceLock<SFrameEncryptorRegistry> = OnceLock::new();
218 R.get_or_init(SFrameEncryptorRegistry::new)
219}
220
221struct SFrameEncryptorDedup {
233 by_session_leaf: Mutex<HashMap<(i32, u32), SFrameEncryptor>>,
234}
235impl SFrameEncryptorDedup {
236 fn new() -> Self {
237 Self {
238 by_session_leaf: Mutex::new(HashMap::new()),
239 }
240 }
241}
242fn sframe_encryptor_dedup() -> &'static SFrameEncryptorDedup {
243 use std::sync::OnceLock;
244 static R: OnceLock<SFrameEncryptorDedup> = OnceLock::new();
245 R.get_or_init(SFrameEncryptorDedup::new)
246}
247
248#[unsafe(no_mangle)]
254pub extern "C" fn gbp_version() -> *mut c_char {
255 alloc_cstring(&format!(
256 "group-protocol-stack {} (gbp + gtp + gap + gsp)",
257 env!("CARGO_PKG_VERSION")
258 ))
259}
260
261#[unsafe(no_mangle)]
270pub unsafe extern "C" fn gbp_mls_create(identity_ptr: *const u8, identity_len: usize) -> i32 {
271 clear_last_error();
272 let ident = unsafe { std::slice::from_raw_parts(identity_ptr, identity_len) };
273 match MlsContext::new_member(ident) {
274 Ok((ctx, kp)) => {
275 let id = mls().insert(ctx);
276 mls_bundles().map.lock().unwrap().insert(id, kp);
277 id
278 }
279 Err(e) => {
280 set_last_error(e);
281 0
282 }
283 }
284}
285
286#[unsafe(no_mangle)]
288pub extern "C" fn gbp_mls_destroy(h: i32) {
289 mls().remove(h);
290 mls_bundles().map.lock().unwrap().remove(&h);
291}
292
293#[unsafe(no_mangle)]
295pub extern "C" fn gbp_mls_epoch(h: i32) -> u64 {
296 mls().get(h).map(|c| c.lock().unwrap().epoch()).unwrap_or(0)
297}
298
299#[unsafe(no_mangle)]
304pub unsafe extern "C" fn gbp_mls_group_id(h: i32, out16: *mut u8) -> bool {
305 clear_last_error();
306 let Some(ctx_arc) = mls().get(h) else {
307 set_last_error("invalid MLS handle");
308 return false;
309 };
310 let ctx = ctx_arc.lock().unwrap();
311 let gid = ctx.group_id_16();
312 unsafe { std::ptr::copy_nonoverlapping(gid.as_ptr(), out16, 16) };
313 true
314}
315
316#[unsafe(no_mangle)]
319pub extern "C" fn gbp_mls_export_key_package(h: i32) -> GbpBuffer {
320 clear_last_error();
321 let bundles = mls_bundles().map.lock().unwrap();
322 let Some(b) = bundles.get(&h) else {
323 set_last_error("invalid MLS handle");
324 return GbpBuffer::empty();
325 };
326 match b.key_package().tls_serialize_detached() {
327 Ok(b) => GbpBuffer::from_vec(b),
328 Err(e) => {
329 set_last_error(format!("kp serialize: {e:?}"));
330 GbpBuffer::empty()
331 }
332 }
333}
334
335#[unsafe(no_mangle)]
342pub unsafe extern "C" fn gbp_mls_invite(h: i32, kp_ptr: *const u8, kp_len: usize) -> GbpBuffer {
343 clear_last_error();
344 let bytes = unsafe { std::slice::from_raw_parts(kp_ptr, kp_len) };
345 let Some(ctx_arc) = mls().get(h) else {
346 set_last_error("invalid MLS handle");
347 return GbpBuffer::empty();
348 };
349 let mut ctx = ctx_arc.lock().unwrap();
350 let kp_in = match KeyPackageIn::tls_deserialize_exact_bytes(bytes) {
351 Ok(v) => v,
352 Err(e) => {
353 set_last_error(format!("kp parse: {e:?}"));
354 return GbpBuffer::empty();
355 }
356 };
357 let validated = match kp_in.validate(ctx.provider.crypto(), ProtocolVersion::Mls10) {
358 Ok(v) => v,
359 Err(e) => {
360 set_last_error(format!("kp validate: {e:?}"));
361 return GbpBuffer::empty();
362 }
363 };
364 match ctx.invite(&[validated]) {
365 Ok(welcome) => GbpBuffer::from_vec(welcome),
366 Err(e) => {
367 set_last_error(e);
368 GbpBuffer::empty()
369 }
370 }
371}
372
373#[unsafe(no_mangle)]
383pub unsafe extern "C" fn gbp_mls_invite_full(
384 h: i32,
385 kp_ptr: *const u8,
386 kp_len: usize,
387) -> GbpBuffer {
388 clear_last_error();
389 let bytes = unsafe { std::slice::from_raw_parts(kp_ptr, kp_len) };
390 let Some(ctx_arc) = mls().get(h) else {
391 set_last_error("invalid MLS handle");
392 return GbpBuffer::empty();
393 };
394 let mut ctx = ctx_arc.lock().unwrap();
395 let kp_in = match KeyPackageIn::tls_deserialize_exact_bytes(bytes) {
396 Ok(v) => v,
397 Err(e) => {
398 set_last_error(format!("kp parse: {e:?}"));
399 return GbpBuffer::empty();
400 }
401 };
402 let validated = match kp_in.validate(ctx.provider.crypto(), ProtocolVersion::Mls10) {
403 Ok(v) => v,
404 Err(e) => {
405 set_last_error(format!("kp validate: {e:?}"));
406 return GbpBuffer::empty();
407 }
408 };
409 match ctx.invite_full(&[validated]) {
410 Ok((commit, welcome)) => {
411 let mut out = Vec::with_capacity(4 + commit.len() + welcome.len());
412 out.extend_from_slice(&(commit.len() as u32).to_le_bytes());
413 out.extend_from_slice(&commit);
414 out.extend_from_slice(&welcome);
415 GbpBuffer::from_vec(out)
416 }
417 Err(e) => {
418 set_last_error(e);
419 GbpBuffer::empty()
420 }
421 }
422}
423
424#[unsafe(no_mangle)]
428pub extern "C" fn gbp_mls_remove(h: i32, leaf_index: u32) -> GbpBuffer {
429 clear_last_error();
430 let Some(ctx_arc) = mls().get(h) else {
431 set_last_error("invalid MLS handle");
432 return GbpBuffer::empty();
433 };
434 let mut ctx = ctx_arc.lock().unwrap();
435 match ctx.remove_members(&[leaf_index]) {
436 Ok(commit) => GbpBuffer::from_vec(commit),
437 Err(e) => {
438 set_last_error(e);
439 GbpBuffer::empty()
440 }
441 }
442}
443
444#[unsafe(no_mangle)]
455pub unsafe extern "C" fn gbp_mls_process_message(
456 h: i32,
457 msg_ptr: *const u8,
458 msg_len: usize,
459) -> u32 {
460 clear_last_error();
461 let bytes = unsafe { std::slice::from_raw_parts(msg_ptr, msg_len) };
462 let Some(ctx_arc) = mls().get(h) else {
463 set_last_error("invalid MLS handle");
464 return 0;
465 };
466 let mut ctx = ctx_arc.lock().unwrap();
467 match ctx.process_message(bytes) {
468 Ok(ProcessedKind::Commit) => 1,
469 Ok(ProcessedKind::Application) => 2,
470 Ok(ProcessedKind::Proposal) => 3,
471 Ok(ProcessedKind::External) => 4,
472 Err(e) => {
473 set_last_error(e);
474 0
475 }
476 }
477}
478
479#[unsafe(no_mangle)]
482pub extern "C" fn gbp_mls_finalize_commit(h: i32) -> bool {
483 clear_last_error();
484 let Some(ctx_arc) = mls().get(h) else {
485 set_last_error("invalid MLS handle");
486 return false;
487 };
488 let mut ctx = ctx_arc.lock().unwrap();
489 match ctx.finalize_pending_commit() {
490 Ok(()) => true,
491 Err(e) => {
492 set_last_error(e);
493 false
494 }
495 }
496}
497
498#[unsafe(no_mangle)]
501pub extern "C" fn gbp_mls_clear_pending_commit(h: i32) -> bool {
502 clear_last_error();
503 let Some(ctx_arc) = mls().get(h) else {
504 set_last_error("invalid MLS handle");
505 return false;
506 };
507 let mut ctx = ctx_arc.lock().unwrap();
508 match ctx.clear_pending_commit() {
509 Ok(()) => true,
510 Err(e) => {
511 set_last_error(e);
512 false
513 }
514 }
515}
516
517#[unsafe(no_mangle)]
522pub unsafe extern "C" fn gbp_mls_accept_welcome(
523 h: i32,
524 welcome_ptr: *const u8,
525 welcome_len: usize,
526) -> bool {
527 clear_last_error();
528 let bytes = unsafe { std::slice::from_raw_parts(welcome_ptr, welcome_len) };
529 let Some(ctx_arc) = mls().get(h) else {
530 set_last_error("invalid MLS handle");
531 return false;
532 };
533 let mut ctx = ctx_arc.lock().unwrap();
534 match ctx.accept_welcome(bytes) {
535 Ok(()) => true,
536 Err(e) => {
537 set_last_error(e);
538 false
539 }
540 }
541}
542
543#[unsafe(no_mangle)]
550pub extern "C" fn gbp_mls_export_state(h: i32) -> GbpBuffer {
551 clear_last_error();
552 let Some(ctx_arc) = mls().get(h) else {
553 set_last_error("invalid MLS handle");
554 return GbpBuffer::empty();
555 };
556 let ctx = ctx_arc.lock().unwrap();
557 match ctx.export_state() {
558 Ok(bytes) => GbpBuffer::from_vec(bytes),
559 Err(e) => {
560 set_last_error(e);
561 GbpBuffer::empty()
562 }
563 }
564}
565
566#[unsafe(no_mangle)]
573pub unsafe extern "C" fn gbp_mls_restore_state(ptr: *const u8, len: usize) -> i32 {
574 clear_last_error();
575 let bytes = unsafe { std::slice::from_raw_parts(ptr, len) };
576 match MlsContext::restore_state(bytes) {
577 Ok(ctx) => mls().insert(ctx),
578 Err(e) => {
579 set_last_error(e);
580 0
581 }
582 }
583}
584
585#[unsafe(no_mangle)]
594pub unsafe extern "C" fn gbp_node_create(member_id: u32, group_id_16: *const u8) -> i32 {
595 clear_last_error();
596 let mut gid = [0u8; 16];
597 unsafe { std::ptr::copy_nonoverlapping(group_id_16, gid.as_mut_ptr(), 16) };
598 nodes().insert(GroupNode::new(member_id, gid))
599}
600
601#[unsafe(no_mangle)]
603pub extern "C" fn gbp_node_destroy(h: i32) {
604 nodes().remove(h);
605}
606
607#[unsafe(no_mangle)]
609pub extern "C" fn gbp_node_bootstrap_creator(h: i32, epoch: u64) -> bool {
610 let Some(n_arc) = nodes().get(h) else {
611 return false;
612 };
613 n_arc.lock().unwrap().bootstrap_as_creator(epoch);
614 true
615}
616
617#[unsafe(no_mangle)]
622pub extern "C" fn gbp_node_bootstrap_joiner(h: i32, epoch: u64, expected_first_tid: u32) -> bool {
623 let Some(n_arc) = nodes().get(h) else {
624 return false;
625 };
626 n_arc
627 .lock()
628 .unwrap()
629 .bootstrap_as_joiner(epoch, expected_first_tid);
630 true
631}
632
633#[unsafe(no_mangle)]
635pub extern "C" fn gbp_node_state(h: i32) -> u32 {
636 nodes()
637 .get(h)
638 .map(|n| n.lock().unwrap().state as u32)
639 .unwrap_or(u32::MAX)
640}
641
642#[unsafe(no_mangle)]
644pub extern "C" fn gbp_node_epoch(h: i32) -> u64 {
645 nodes()
646 .get(h)
647 .map(|n| n.lock().unwrap().current_epoch)
648 .unwrap_or(0)
649}
650
651#[unsafe(no_mangle)]
653pub extern "C" fn gbp_node_last_transition_id(h: i32) -> u32 {
654 nodes()
655 .get(h)
656 .map(|n| n.lock().unwrap().last_transition_id)
657 .unwrap_or(0)
658}
659
660#[unsafe(no_mangle)]
663pub extern "C" fn gbp_node_set_epoch(h: i32, epoch: u64) -> bool {
664 let Some(n_arc) = nodes().get(h) else {
665 return false;
666 };
667 n_arc.lock().unwrap().current_epoch = epoch;
668 true
669}
670
671#[unsafe(no_mangle)]
673pub extern "C" fn gbp_node_apply_transition(h: i32, tid: u32) -> bool {
674 let Some(n_arc) = nodes().get(h) else {
675 return false;
676 };
677 n_arc.lock().unwrap().apply_transition(tid);
678 true
679}
680
681#[unsafe(no_mangle)]
688pub unsafe extern "C" fn gbp_node_send_control(
689 nh: i32,
690 mh: i32,
691 target: u32,
692 opcode: u16,
693 transition_id: u32,
694 request_id: u32,
695 args_ptr: *const u8,
696 args_len: usize,
697) -> GbpBuffer {
698 clear_last_error();
699 let op = match ControlOpcode::try_from(opcode) {
700 Ok(o) => o,
701 Err(_) => {
702 set_last_error(format!("bad opcode 0x{opcode:04X}"));
703 return GbpBuffer::empty();
704 }
705 };
706 let args = if args_len == 0 {
707 Vec::new()
708 } else {
709 unsafe { std::slice::from_raw_parts(args_ptr, args_len) }.to_vec()
710 };
711 let (n_arc, m_arc) = (nodes().get(nh), mls().get(mh));
712 let (Some(n_arc), Some(m_arc)) = (n_arc, m_arc) else {
713 set_last_error("bad node/mls handle");
714 return GbpBuffer::empty();
715 };
716 let mut n = n_arc.lock().unwrap();
717 let mut m = m_arc.lock().unwrap();
718 match n.send_control(&mut *m, target, op, transition_id, request_id, args) {
719 Ok(of) => outbound_to_buffer(of),
720 Err(e) => {
721 set_last_error(e.to_string());
722 GbpBuffer::empty()
723 }
724 }
725}
726
727#[unsafe(no_mangle)]
732pub unsafe extern "C" fn gbp_node_on_wire(
733 nh: i32,
734 mh: i32,
735 wire_ptr: *const u8,
736 wire_len: usize,
737) -> *mut c_char {
738 clear_last_error();
739 let wire = unsafe { std::slice::from_raw_parts(wire_ptr, wire_len) };
740 let (n_arc, m_arc) = (nodes().get(nh), mls().get(mh));
741 let (Some(n_arc), Some(m_arc)) = (n_arc, m_arc) else {
742 set_last_error("bad node/mls handle");
743 return alloc_cstring("[]");
744 };
745 let mut n = n_arc.lock().unwrap();
746 let mut m = m_arc.lock().unwrap();
747 let events = match n.on_wire(&mut *m, wire) {
748 Ok(e) => e,
749 Err(e) => {
750 set_last_error(e.to_string());
751 return alloc_cstring("[]");
752 }
753 };
754 alloc_cstring(&events_to_json(&events))
755}
756
757#[unsafe(no_mangle)]
759pub extern "C" fn gbp_node_drain_events(nh: i32) -> *mut c_char {
760 let Some(n_arc) = nodes().get(nh) else {
761 return alloc_cstring("[]");
762 };
763 alloc_cstring(&events_to_json(&n_arc.lock().unwrap().drain_events()))
764}
765
766fn outbound_to_buffer(of: OutboundFrame) -> GbpBuffer {
767 let mut out = Vec::with_capacity(4 + of.wire.len());
768 out.extend_from_slice(&of.to.to_le_bytes());
769 out.extend_from_slice(&of.wire);
770 GbpBuffer::from_vec(out)
771}
772
773#[unsafe(no_mangle)]
779pub extern "C" fn gtp_client_create() -> i32 {
780 gtps().insert(GtpClient::new())
781}
782
783#[unsafe(no_mangle)]
785pub extern "C" fn gtp_client_destroy(h: i32) {
786 gtps().remove(h);
787}
788
789#[unsafe(no_mangle)]
791pub extern "C" fn gtp_client_reset(h: i32) {
792 if let Some(c) = gtps().get(h) {
793 c.lock().unwrap().reset();
794 }
795}
796
797#[unsafe(no_mangle)]
805pub unsafe extern "C" fn gtp_client_send(
806 ch: i32,
807 nh: i32,
808 mh: i32,
809 target: u32,
810 message_id: u64,
811 text_ptr: *const u8,
812 text_len: usize,
813 codec: u8,
814) -> GbpBuffer {
815 clear_last_error();
816 let text = unsafe { std::slice::from_raw_parts(text_ptr, text_len) };
817 let text = match std::str::from_utf8(text) {
818 Ok(s) => s,
819 Err(e) => {
820 set_last_error(format!("utf8: {e}"));
821 return GbpBuffer::empty();
822 }
823 };
824 let codec = PayloadCodec::from_u8(codec).unwrap_or(PayloadCodec::Cbor);
825 let (c_arc, n_arc, m_arc) = (gtps().get(ch), nodes().get(nh), mls().get(mh));
826 let (Some(c_arc), Some(n_arc), Some(m_arc)) = (c_arc, n_arc, m_arc) else {
827 set_last_error("bad handle");
828 return GbpBuffer::empty();
829 };
830 let mut c = c_arc.lock().unwrap();
831 let mut n = n_arc.lock().unwrap();
832 let mut m = m_arc.lock().unwrap();
833 #[allow(clippy::explicit_auto_deref)]
837 match c.send(&mut n, &mut *m, target, message_id, text, codec) {
838 Ok(of) => outbound_to_buffer(of),
839 Err(e) => {
840 set_last_error(e.to_string());
841 GbpBuffer::empty()
842 }
843 }
844}
845
846#[unsafe(no_mangle)]
858pub unsafe extern "C" fn gtp_client_accept(
859 ch: i32,
860 current_epoch: u64,
861 pt_ptr: *const u8,
862 pt_len: usize,
863 codec: u8,
864) -> *mut c_char {
865 clear_last_error();
866 let pt = unsafe { std::slice::from_raw_parts(pt_ptr, pt_len) };
867 let codec = PayloadCodec::from_u8(codec).unwrap_or(PayloadCodec::Cbor);
868 let Some(c_arc) = gtps().get(ch) else {
869 return alloc_cstring(r#"{"status":"error","reason":"bad client"}"#);
870 };
871 let mut c = c_arc.lock().unwrap();
872 #[derive(Serialize)]
873 struct Out<'a> {
874 status: &'a str,
875 sender: Option<u32>,
876 message_id: Option<u64>,
877 text: Option<String>,
878 reason: Option<String>,
879 }
880 let out = match c.accept(pt, current_epoch, codec) {
881 Ok(GtpAccept::New(m)) => Out {
882 status: "new",
883 sender: Some(m.sender_id),
884 message_id: Some(m.message_id),
885 text: Some(m.text().unwrap_or("<binary>").to_string()),
886 reason: None,
887 },
888 Ok(GtpAccept::Duplicate(m)) => Out {
889 status: "duplicate",
890 sender: Some(m.sender_id),
891 message_id: Some(m.message_id),
892 text: Some(m.text().unwrap_or("<binary>").to_string()),
893 reason: None,
894 },
895 Err(e) => Out {
896 status: "error",
897 sender: None,
898 message_id: None,
899 text: None,
900 reason: Some(e.to_string()),
901 },
902 };
903 alloc_cstring(&serde_json::to_string(&out).unwrap_or_default())
904}
905
906#[unsafe(no_mangle)]
912pub extern "C" fn gap_client_create() -> i32 {
913 gaps().insert(GapClient::new())
914}
915
916#[unsafe(no_mangle)]
918pub extern "C" fn gap_client_destroy(h: i32) {
919 gaps().remove(h);
920}
921
922#[unsafe(no_mangle)]
924pub extern "C" fn gap_client_reset(h: i32) {
925 if let Some(c) = gaps().get(h) {
926 c.lock().unwrap().reset();
927 }
928}
929
930#[unsafe(no_mangle)]
938pub unsafe extern "C" fn gap_client_send(
939 ch: i32,
940 nh: i32,
941 mh: i32,
942 target: u32,
943 media_source_id: u32,
944 rtp_timestamp: u64,
945 opus_ptr: *const u8,
946 opus_len: usize,
947 codec: u8,
948) -> GbpBuffer {
949 clear_last_error();
950 let opus = unsafe { std::slice::from_raw_parts(opus_ptr, opus_len) }.to_vec();
951 let codec = PayloadCodec::from_u8(codec).unwrap_or(PayloadCodec::Cbor);
952 let (c_arc, n_arc, m_arc) = (gaps().get(ch), nodes().get(nh), mls().get(mh));
953 let (Some(c_arc), Some(n_arc), Some(m_arc)) = (c_arc, n_arc, m_arc) else {
954 set_last_error("bad handle");
955 return GbpBuffer::empty();
956 };
957 let mut c = c_arc.lock().unwrap();
958 let mut n = n_arc.lock().unwrap();
959 let mut m = m_arc.lock().unwrap();
960 #[allow(clippy::explicit_auto_deref)]
964 match c.send(
965 &mut n,
966 &mut *m,
967 target,
968 media_source_id,
969 rtp_timestamp,
970 opus,
971 codec,
972 ) {
973 Ok(of) => outbound_to_buffer(of),
974 Err(e) => {
975 set_last_error(e.to_string());
976 GbpBuffer::empty()
977 }
978 }
979}
980
981#[unsafe(no_mangle)]
989pub unsafe extern "C" fn gap_client_accept(
990 ch: i32,
991 current_epoch: u64,
992 pt_ptr: *const u8,
993 pt_len: usize,
994 codec: u8,
995) -> *mut c_char {
996 clear_last_error();
997 let pt = unsafe { std::slice::from_raw_parts(pt_ptr, pt_len) };
998 let codec = PayloadCodec::from_u8(codec).unwrap_or(PayloadCodec::Cbor);
999 let Some(c_arc) = gaps().get(ch) else {
1000 return alloc_cstring(r#"{"status":"error","reason":"bad client"}"#);
1001 };
1002 let mut c = c_arc.lock().unwrap();
1003 #[derive(Serialize)]
1004 struct Out<'a> {
1005 status: &'a str,
1006 source: Option<u32>,
1007 seq: Option<u32>,
1008 bytes: Option<usize>,
1009 reason: Option<String>,
1010 }
1011 let out = match c.accept(pt, current_epoch, codec) {
1012 Ok(GapAccept::New(p)) => Out {
1013 status: "new",
1014 source: Some(p.media_source_id),
1015 seq: Some(p.rtp_sequence),
1016 bytes: Some(p.opus_frame.len()),
1017 reason: None,
1018 },
1019 Ok(GapAccept::Late(p)) => Out {
1020 status: "late",
1021 source: Some(p.media_source_id),
1022 seq: Some(p.rtp_sequence),
1023 bytes: Some(p.opus_frame.len()),
1024 reason: None,
1025 },
1026 Err(e) => Out {
1027 status: "error",
1028 source: None,
1029 seq: None,
1030 bytes: None,
1031 reason: Some(e.to_string()),
1032 },
1033 };
1034 alloc_cstring(&serde_json::to_string(&out).unwrap_or_default())
1035}
1036
1037#[unsafe(no_mangle)]
1043pub extern "C" fn gsp_client_create() -> i32 {
1044 gsps().insert(GspClient::new())
1045}
1046
1047#[unsafe(no_mangle)]
1049pub extern "C" fn gsp_client_destroy(h: i32) {
1050 gsps().remove(h);
1051}
1052
1053#[unsafe(no_mangle)]
1055pub extern "C" fn gsp_client_reset(h: i32) {
1056 if let Some(c) = gsps().get(h) {
1057 c.lock().unwrap().reset();
1058 }
1059}
1060
1061#[unsafe(no_mangle)]
1066pub extern "C" fn gsp_client_send(
1067 ch: i32,
1068 nh: i32,
1069 mh: i32,
1070 target: u32,
1071 signal_type: u32,
1072 role_claim: u32,
1073 request_id: u32,
1074 codec: u8,
1075) -> GbpBuffer {
1076 clear_last_error();
1077 let sig = match SignalType::try_from(signal_type) {
1078 Ok(s) => s,
1079 Err(_) => {
1080 set_last_error(format!("bad signal {signal_type}"));
1081 return GbpBuffer::empty();
1082 }
1083 };
1084 let codec = PayloadCodec::from_u8(codec).unwrap_or(PayloadCodec::Cbor);
1085 let (c_arc, n_arc, m_arc) = (gsps().get(ch), nodes().get(nh), mls().get(mh));
1086 let (Some(c_arc), Some(n_arc), Some(m_arc)) = (c_arc, n_arc, m_arc) else {
1087 set_last_error("bad handle");
1088 return GbpBuffer::empty();
1089 };
1090 let mut c = c_arc.lock().unwrap();
1091 let mut n = n_arc.lock().unwrap();
1092 let mut m = m_arc.lock().unwrap();
1093 #[allow(clippy::explicit_auto_deref)]
1097 match c.send(&mut n, &mut *m, target, sig, role_claim, request_id, codec) {
1098 Ok(of) => outbound_to_buffer(of),
1099 Err(e) => {
1100 set_last_error(e.to_string());
1101 GbpBuffer::empty()
1102 }
1103 }
1104}
1105
1106#[unsafe(no_mangle)]
1116pub unsafe extern "C" fn gsp_client_send_with_args(
1117 ch: i32,
1118 nh: i32,
1119 mh: i32,
1120 target: u32,
1121 signal_type: u32,
1122 role_claim: u32,
1123 request_id: u32,
1124 args_ptr: *const u8,
1125 args_len: usize,
1126 codec: u8,
1127) -> GbpBuffer {
1128 clear_last_error();
1129 let args: &[u8] = if args_len == 0 || args_ptr.is_null() {
1130 &[]
1131 } else {
1132 unsafe { std::slice::from_raw_parts(args_ptr, args_len) }
1133 };
1134 let sig = match SignalType::try_from(signal_type) {
1135 Ok(s) => s,
1136 Err(_) => {
1137 set_last_error(format!("bad signal {signal_type}"));
1138 return GbpBuffer::empty();
1139 }
1140 };
1141 let codec = PayloadCodec::from_u8(codec).unwrap_or(PayloadCodec::Cbor);
1142 let (c_arc, n_arc, m_arc) = (gsps().get(ch), nodes().get(nh), mls().get(mh));
1143 let (Some(c_arc), Some(n_arc), Some(m_arc)) = (c_arc, n_arc, m_arc) else {
1144 set_last_error("bad handle");
1145 return GbpBuffer::empty();
1146 };
1147 let mut c = c_arc.lock().unwrap();
1148 let mut n = n_arc.lock().unwrap();
1149 let mut m = m_arc.lock().unwrap();
1150 #[allow(clippy::explicit_auto_deref)]
1154 match c.send_with_args(
1155 &mut n, &mut *m, target, sig, role_claim, request_id, args, codec,
1156 ) {
1157 Ok(of) => outbound_to_buffer(of),
1158 Err(e) => {
1159 set_last_error(e.to_string());
1160 GbpBuffer::empty()
1161 }
1162 }
1163}
1164
1165#[unsafe(no_mangle)]
1175pub unsafe extern "C" fn gsp_client_accept(
1176 ch: i32,
1177 current_epoch: u64,
1178 pt_ptr: *const u8,
1179 pt_len: usize,
1180 codec: u8,
1181) -> *mut c_char {
1182 clear_last_error();
1183 let pt = unsafe { std::slice::from_raw_parts(pt_ptr, pt_len) };
1184 let codec = PayloadCodec::from_u8(codec).unwrap_or(PayloadCodec::Cbor);
1185 let Some(c_arc) = gsps().get(ch) else {
1186 return alloc_cstring(r#"{"status":"error","reason":"bad client"}"#);
1187 };
1188 let mut c = c_arc.lock().unwrap();
1189 #[derive(Serialize)]
1190 struct Out<'a> {
1191 status: &'a str,
1192 signal: Option<&'a str>,
1193 signal_code: Option<u32>,
1194 sender: Option<u32>,
1195 role_claim: Option<u32>,
1196 request_id: Option<u32>,
1197 reason: Option<String>,
1198 }
1199 let out = match c.accept(pt, current_epoch, codec) {
1200 Ok(GspAccept {
1201 signal,
1202 sender_id,
1203 role_claim,
1204 request_id,
1205 }) => Out {
1206 status: "new",
1207 signal: Some(signal.name()),
1208 signal_code: Some(signal as u32),
1209 sender: Some(sender_id),
1210 role_claim: Some(role_claim),
1211 request_id: Some(request_id),
1212 reason: None,
1213 },
1214 Err(gbp_stack::GspError::DuplicateRequest(rid)) => Out {
1215 status: "duplicate",
1216 signal: None,
1217 signal_code: None,
1218 sender: None,
1219 role_claim: None,
1220 request_id: Some(rid),
1221 reason: None,
1222 },
1223 Err(e) => Out {
1224 status: "error",
1225 signal: None,
1226 signal_code: None,
1227 sender: None,
1228 role_claim: None,
1229 request_id: None,
1230 reason: Some(e.to_string()),
1231 },
1232 };
1233 alloc_cstring(&serde_json::to_string(&out).unwrap_or_default())
1234}
1235
1236#[unsafe(no_mangle)]
1245pub unsafe extern "C" fn gbp_frame_encode_v(
1246 version: u8,
1247 group_id_16: *const u8,
1248 epoch: u64,
1249 transition_id: u32,
1250 stream_type: u32,
1251 stream_id: u32,
1252 flags: u16,
1253 sequence_no: u32,
1254 payload_ptr: *const u8,
1255 payload_len: usize,
1256) -> GbpBuffer {
1257 clear_last_error();
1258 let mut gid = [0u8; 16];
1259 unsafe { std::ptr::copy_nonoverlapping(group_id_16, gid.as_mut_ptr(), 16) };
1260 let st_u8 = StreamType::try_from(stream_type)
1261 .map(|s| s as u8)
1262 .unwrap_or(stream_type as u8);
1263 let payload: Vec<u8> = if payload_len == 0 || payload_ptr.is_null() {
1264 Vec::new()
1265 } else {
1266 unsafe { std::slice::from_raw_parts(payload_ptr, payload_len) }.to_vec()
1267 };
1268 let frame = gbp_stack::gbp::GbpFrame {
1269 version,
1270 group_id: serde_bytes::ByteBuf::from(gid.to_vec()),
1271 epoch,
1272 transition_id,
1273 stream_type: st_u8,
1274 stream_id,
1275 flags,
1276 sequence_no,
1277 payload_format: 0u8,
1278 payload_size: payload.len() as u32,
1279 encrypted_payload: serde_bytes::ByteBuf::from(payload),
1280 };
1281 GbpBuffer::from_vec(frame.to_cbor())
1282}
1283
1284#[unsafe(no_mangle)]
1286pub extern "C" fn gbp_error_lookup(code: u16) -> GbpBuffer {
1287 use gbp_stack::core::errors::ErrorSpec;
1288 match ErrorSpec::lookup(code) {
1289 Some(spec) => GbpBuffer::from_vec(ErrorObject::from_spec(spec, spec.name).to_cbor()),
1290 None => {
1291 set_last_error(format!("unknown error code 0x{code:04X}"));
1292 GbpBuffer::empty()
1293 }
1294 }
1295}
1296
1297#[allow(dead_code)]
1298fn _link(_f: &GbpFrame, _l: StreamLabel) {}
1299
1300#[derive(Serialize)]
1305#[serde(tag = "kind", rename_all = "snake_case")]
1306enum EventDto<'a> {
1307 StateChanged {
1308 from: String,
1309 to: String,
1310 },
1311 PayloadReceived {
1312 stream_type: &'a str,
1313 stream_type_code: u32,
1314 stream_id: u32,
1315 sequence_no: u32,
1316 flags: u16,
1317 codec: u8,
1318 plaintext_b64: String,
1319 },
1320 Control {
1321 from: u32,
1322 opcode: &'a str,
1323 opcode_code: u16,
1324 transition_id: u32,
1325 request_id: u32,
1326 args_b64: String,
1327 },
1328 Error {
1329 code: u16,
1330 code_hex: String,
1331 class: u8,
1332 retryable: bool,
1333 fatal: bool,
1334 reason: String,
1335 },
1336 EpochAdvanced {
1337 epoch: u64,
1338 transition_id: u32,
1339 },
1340 CoordinatorElectionNeeded {},
1341 BecameCoordinator {},
1342 CoordinatorClaim {
1343 claimant: u32,
1344 },
1345}
1346
1347fn b64(b: &[u8]) -> String {
1348 const A: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
1349 let mut out = String::with_capacity(b.len().div_ceil(3) * 4);
1350 let mut i = 0;
1351 while i + 3 <= b.len() {
1352 let n = ((b[i] as u32) << 16) | ((b[i + 1] as u32) << 8) | (b[i + 2] as u32);
1353 out.push(A[(n >> 18) as usize & 0x3F] as char);
1354 out.push(A[(n >> 12) as usize & 0x3F] as char);
1355 out.push(A[(n >> 6) as usize & 0x3F] as char);
1356 out.push(A[n as usize & 0x3F] as char);
1357 i += 3;
1358 }
1359 let rem = b.len() - i;
1360 if rem == 1 {
1361 let n = (b[i] as u32) << 16;
1362 out.push(A[(n >> 18) as usize & 0x3F] as char);
1363 out.push(A[(n >> 12) as usize & 0x3F] as char);
1364 out.push('=');
1365 out.push('=');
1366 } else if rem == 2 {
1367 let n = ((b[i] as u32) << 16) | ((b[i + 1] as u32) << 8);
1368 out.push(A[(n >> 18) as usize & 0x3F] as char);
1369 out.push(A[(n >> 12) as usize & 0x3F] as char);
1370 out.push(A[(n >> 6) as usize & 0x3F] as char);
1371 out.push('=');
1372 }
1373 out
1374}
1375
1376fn dto<'a>(e: &'a Event) -> EventDto<'a> {
1377 match e {
1378 Event::StateChanged { from, to } => EventDto::StateChanged {
1379 from: from.to_string(),
1380 to: to.to_string(),
1381 },
1382 Event::PayloadReceived(DeliveredPayload {
1383 stream_type,
1384 stream_id,
1385 sequence_no,
1386 flags,
1387 plaintext,
1388 codec,
1389 }) => EventDto::PayloadReceived {
1390 stream_type: match stream_type {
1391 StreamType::Control => "control",
1392 StreamType::Audio => "audio",
1393 StreamType::Text => "text",
1394 StreamType::Signal => "signal",
1395 },
1396 stream_type_code: *stream_type as u32,
1397 stream_id: *stream_id,
1398 sequence_no: *sequence_no,
1399 flags: *flags,
1400 codec: codec.as_u8(),
1401 plaintext_b64: b64(plaintext),
1402 },
1403 Event::Control {
1404 from,
1405 opcode,
1406 transition_id,
1407 request_id,
1408 args,
1409 } => EventDto::Control {
1410 from: *from,
1411 opcode: opcode.name(),
1412 opcode_code: *opcode as u16,
1413 transition_id: *transition_id,
1414 request_id: *request_id,
1415 args_b64: b64(args),
1416 },
1417 Event::Error {
1418 code,
1419 class,
1420 retryable,
1421 fatal,
1422 reason,
1423 } => EventDto::Error {
1424 code: *code,
1425 code_hex: format!("0x{code:04X}"),
1426 class: *class as u8,
1427 retryable: *retryable,
1428 fatal: *fatal,
1429 reason: reason.clone(),
1430 },
1431 Event::EpochAdvanced {
1432 epoch,
1433 transition_id,
1434 } => EventDto::EpochAdvanced {
1435 epoch: *epoch,
1436 transition_id: *transition_id,
1437 },
1438 Event::CoordinatorElectionNeeded => EventDto::CoordinatorElectionNeeded {},
1439 Event::BecameCoordinator => EventDto::BecameCoordinator {},
1440 Event::CoordinatorClaim { claimant } => EventDto::CoordinatorClaim {
1441 claimant: *claimant,
1442 },
1443 }
1444}
1445
1446fn events_to_json(events: &[Event]) -> String {
1447 let dtos: Vec<EventDto> = events.iter().map(dto).collect();
1448 serde_json::to_string(&dtos).unwrap_or_else(|_| "[]".to_string())
1449}
1450
1451#[allow(dead_code)]
1452const _STATES: [NodeState; 7] = [
1453 NodeState::Idle,
1454 NodeState::Connecting,
1455 NodeState::EstablishingGroup,
1456 NodeState::Active,
1457 NodeState::Resyncing,
1458 NodeState::Failed,
1459 NodeState::Closed,
1460];
1461
1462#[unsafe(no_mangle)]
1483pub unsafe extern "C" fn gbp_sframe_session_create(
1484 mls_handle: i32,
1485 suite: u8,
1486 label_ptr: *const u8,
1487 label_len: usize,
1488) -> i32 {
1489 clear_last_error();
1490 let suite = match CipherSuite::from_u8(suite) {
1491 Some(s) => s,
1492 None => {
1493 set_last_error(format!("unknown ciphersuite {suite}"));
1494 return 0;
1495 }
1496 };
1497 let label = unsafe {
1498 match std::str::from_utf8(std::slice::from_raw_parts(label_ptr, label_len)) {
1499 Ok(s) => s,
1500 Err(e) => {
1501 set_last_error(e);
1502 return 0;
1503 }
1504 }
1505 };
1506 let Some(mls_arc) = mls().get(mls_handle) else {
1507 set_last_error("invalid MLS handle");
1508 return 0;
1509 };
1510 let mls = mls_arc.lock().unwrap();
1511 match SFrameSession::from_mls(&mls, label, suite) {
1512 Ok(session) => sframe_sessions().insert(session.decryptor()),
1513 Err(e) => {
1514 set_last_error(e);
1515 0
1516 }
1517 }
1518}
1519
1520#[unsafe(no_mangle)]
1526pub extern "C" fn gbp_sframe_session_free(handle: i32) {
1527 sframe_sessions().remove(handle);
1528 sframe_encryptor_dedup()
1529 .by_session_leaf
1530 .lock()
1531 .unwrap()
1532 .retain(|(session_handle, _), _| *session_handle != handle);
1533}
1534
1535#[unsafe(no_mangle)]
1549pub unsafe extern "C" fn gbp_sframe_encryptor_create(
1550 mls_handle: i32,
1551 session_handle: i32,
1552 leaf_index: u32,
1553 suite: u8,
1554 label_ptr: *const u8,
1555 label_len: usize,
1556) -> i32 {
1557 clear_last_error();
1558 let suite = match CipherSuite::from_u8(suite) {
1559 Some(s) => s,
1560 None => {
1561 set_last_error(format!("unknown ciphersuite {suite}"));
1562 return 0;
1563 }
1564 };
1565 let label = unsafe {
1566 match std::str::from_utf8(std::slice::from_raw_parts(label_ptr, label_len)) {
1567 Ok(s) => s,
1568 Err(e) => {
1569 set_last_error(e);
1570 return 0;
1571 }
1572 }
1573 };
1574 if sframe_sessions().get(session_handle).is_none() {
1576 set_last_error("invalid session handle");
1577 return 0;
1578 }
1579 let dedup_key = (session_handle, leaf_index);
1580 if let Some(existing) = sframe_encryptor_dedup()
1581 .by_session_leaf
1582 .lock()
1583 .unwrap()
1584 .get(&dedup_key)
1585 {
1586 return sframe_encryptors().insert(existing.clone());
1587 }
1588
1589 let Some(mls_arc) = mls().get(mls_handle) else {
1590 set_last_error("invalid MLS handle");
1591 return 0;
1592 };
1593 let mls = mls_arc.lock().unwrap();
1594 match SFrameSession::from_mls(&mls, label, suite) {
1595 Ok(session) => {
1596 let encryptor = session.encryptor(leaf_index);
1597 sframe_encryptor_dedup()
1598 .by_session_leaf
1599 .lock()
1600 .unwrap()
1601 .insert(dedup_key, encryptor.clone());
1602 sframe_encryptors().insert(encryptor)
1603 }
1604 Err(e) => {
1605 set_last_error(e);
1606 0
1607 }
1608 }
1609}
1610
1611#[unsafe(no_mangle)]
1620pub extern "C" fn gbp_sframe_encryptor_free(handle: i32) {
1621 sframe_encryptors().remove(handle);
1622}
1623
1624#[unsafe(no_mangle)]
1637pub unsafe extern "C" fn gbp_sframe_encrypt(
1638 enc_handle: i32,
1639 plaintext_ptr: *const u8,
1640 plaintext_len: usize,
1641 aad_ptr: *const u8,
1642 aad_len: usize,
1643) -> GbpBuffer {
1644 clear_last_error();
1645 let Some(enc_arc) = sframe_encryptors().get(enc_handle) else {
1646 set_last_error("invalid encryptor handle");
1647 return GbpBuffer::empty();
1648 };
1649 let plaintext = unsafe { std::slice::from_raw_parts(plaintext_ptr, plaintext_len) };
1650 let aad = if aad_ptr.is_null() || aad_len == 0 {
1651 &[][..]
1652 } else {
1653 unsafe { std::slice::from_raw_parts(aad_ptr, aad_len) }
1654 };
1655 let mut enc = enc_arc.lock().unwrap();
1656 match enc.encrypt(plaintext, aad) {
1657 Ok(payload) => GbpBuffer::from_vec(payload),
1658 Err(e) => {
1659 set_last_error(e);
1660 GbpBuffer::empty()
1661 }
1662 }
1663}
1664
1665#[unsafe(no_mangle)]
1677pub unsafe extern "C" fn gbp_sframe_decrypt(
1678 session_handle: i32,
1679 payload_ptr: *const u8,
1680 payload_len: usize,
1681 aad_ptr: *const u8,
1682 aad_len: usize,
1683 sender_leaf_out: *mut u32,
1684) -> GbpBuffer {
1685 clear_last_error();
1686 let Some(session_arc) = sframe_sessions().get(session_handle) else {
1687 set_last_error("invalid session handle");
1688 return GbpBuffer::empty();
1689 };
1690 let payload = unsafe { std::slice::from_raw_parts(payload_ptr, payload_len) };
1691 let aad = if aad_ptr.is_null() || aad_len == 0 {
1692 &[][..]
1693 } else {
1694 unsafe { std::slice::from_raw_parts(aad_ptr, aad_len) }
1695 };
1696 let mut dec = session_arc.lock().unwrap();
1697 match dec.decrypt(payload, aad) {
1698 Ok((plaintext, leaf)) => {
1699 if !sender_leaf_out.is_null() {
1700 unsafe {
1701 *sender_leaf_out = leaf;
1702 }
1703 }
1704 GbpBuffer::from_vec(plaintext)
1705 }
1706 Err(e) => {
1707 set_last_error(e);
1708 GbpBuffer::empty()
1709 }
1710 }
1711}
1712
1713#[cfg(test)]
1714mod tests {
1715 use super::b64;
1716
1717 #[test]
1718 fn b64_empty() {
1719 assert_eq!(b64(b""), "");
1720 }
1721
1722 #[test]
1723 fn b64_single_byte() {
1724 let s = b64(b"f");
1731 assert_eq!(s, "Zg==");
1733 }
1734
1735 #[test]
1736 fn b64_two_bytes() {
1737 let s = b64(b"fo");
1738 assert_eq!(s, "Zm8=");
1742 }
1743
1744 #[test]
1745 fn b64_three_bytes() {
1746 let s = b64(b"foo");
1747 assert_eq!(s, "Zm9v");
1749 }
1750
1751 #[test]
1752 fn b64_known_vectors() {
1753 assert_eq!(b64(b""), "");
1755 assert_eq!(b64(b"f"), "Zg==");
1756 assert_eq!(b64(b"fo"), "Zm8=");
1757 assert_eq!(b64(b"foo"), "Zm9v");
1758 assert_eq!(b64(b"foob"), "Zm9vYg==");
1759 assert_eq!(b64(b"fooba"), "Zm9vYmE=");
1760 assert_eq!(b64(b"foobar"), "Zm9vYmFy");
1761 }
1762
1763 #[test]
1764 fn b64_padding_roundtrip() {
1765 for b in 0u8..=255 {
1767 let input = [b];
1768 let enc = b64(&input);
1769 assert_eq!(enc.len(), 4, "len mismatch for 0x{b:02X}: {enc}");
1771 assert!(enc.ends_with("=="), "missing padding for 0x{b:02X}: {enc}");
1773 }
1774 }
1775}