1#![allow(clippy::not_unsafe_ptr_arg_deref)]
13
14use std::collections::BTreeMap;
42use std::ffi::{CStr, CString};
43use std::os::raw::c_char;
44use std::path::Path;
45use std::ptr;
46use std::slice;
47
48#[cfg(feature = "async")]
49pub mod async_core;
50#[cfg(feature = "async")]
51pub mod async_streaming;
52
53use tensogram::encode::MaskMethod;
54use tensogram::validate::{
55 ValidateOptions, ValidationLevel, validate_file as core_validate_file, validate_message,
56};
57use tensogram::{
58 DataObjectDescriptor, DecodeOptions, EncodeOptions, GlobalMetadata, RESERVED_KEY,
59 StreamingEncoder, TensogramError, TensogramFile, decode, decode_metadata, decode_object,
60 decode_range, encode, encode_pre_encoded, parse_hash_name, scan,
61};
62
63pub const TGM_WIRE_VERSION: u16 = 3;
81const _: () = assert!(
82 TGM_WIRE_VERSION == tensogram::WIRE_VERSION,
83 "TGM_WIRE_VERSION must equal tensogram::WIRE_VERSION"
84);
85
86#[repr(C)]
91#[derive(Debug, Clone, Copy, PartialEq, Eq)]
92pub enum TgmError {
93 Ok = 0,
94 Framing = 1,
95 Metadata = 2,
96 Encoding = 3,
97 Compression = 4,
98 Object = 5,
99 Io = 6,
100 HashMismatch = 7,
101 InvalidArg = 8,
102 EndOfIter = 9,
104 Remote = 10,
105 MissingHash = 11,
113 Timeout = 12,
116 Cancelled = 13,
119}
120
121fn to_error_code(e: &TensogramError) -> TgmError {
122 match e {
123 TensogramError::Framing(_) => TgmError::Framing,
124 TensogramError::Metadata(_) => TgmError::Metadata,
125 TensogramError::Encoding(_) => TgmError::Encoding,
126 TensogramError::Compression(_) => TgmError::Compression,
127 TensogramError::Object(_) => TgmError::Object,
128 TensogramError::Io(_) => TgmError::Io,
129 TensogramError::HashMismatch { .. } => TgmError::HashMismatch,
130 TensogramError::MissingHash { .. } => TgmError::MissingHash,
131 TensogramError::Remote(_) => TgmError::Remote,
132 _ => TgmError::Encoding,
140 }
141}
142
143thread_local! {
145 static LAST_ERROR: std::cell::RefCell<Option<CString>> = const { std::cell::RefCell::new(None) };
146}
147
148fn set_last_error(msg: &str) {
149 LAST_ERROR.with(|cell| {
150 *cell.borrow_mut() = CString::new(msg).ok();
151 });
152}
153
154#[unsafe(no_mangle)]
157pub extern "C" fn tgm_last_error() -> *const c_char {
158 LAST_ERROR.with(|cell| {
159 cell.borrow()
160 .as_ref()
161 .map(|s| s.as_ptr())
162 .unwrap_or(ptr::null())
163 })
164}
165
166#[repr(C)]
172pub struct TgmBytes {
173 pub data: *mut u8,
174 pub len: usize,
175}
176
177#[repr(C)]
194pub struct TgmEncodeMaskOptions {
195 pub allow_nan: bool,
196 pub allow_inf: bool,
197 pub nan_mask_method: *const c_char,
198 pub pos_inf_mask_method: *const c_char,
199 pub neg_inf_mask_method: *const c_char,
200 pub small_mask_threshold_bytes: isize,
201}
202
203unsafe fn parse_mask_method_cstr(
213 ptr: *const c_char,
214 default: MaskMethod,
215) -> Result<MaskMethod, String> {
216 if ptr.is_null() {
217 return Ok(default);
218 }
219 let s = unsafe { CStr::from_ptr(ptr) }
220 .to_str()
221 .map_err(|_| "mask method name is not valid UTF-8".to_string())?;
222 MaskMethod::from_name(s).map_err(|e| e.to_string())
223}
224
225unsafe fn apply_mask_options(
236 encode_opts: &mut EncodeOptions,
237 opts: *const TgmEncodeMaskOptions,
238) -> Result<(), String> {
239 if opts.is_null() {
240 return Ok(());
241 }
242 let opts = unsafe { &*opts };
243 encode_opts.allow_nan = opts.allow_nan;
244 encode_opts.allow_inf = opts.allow_inf;
245 encode_opts.nan_mask_method =
246 unsafe { parse_mask_method_cstr(opts.nan_mask_method, MaskMethod::default())? };
247 encode_opts.pos_inf_mask_method =
248 unsafe { parse_mask_method_cstr(opts.pos_inf_mask_method, MaskMethod::default())? };
249 encode_opts.neg_inf_mask_method =
250 unsafe { parse_mask_method_cstr(opts.neg_inf_mask_method, MaskMethod::default())? };
251 if opts.small_mask_threshold_bytes >= 0 {
252 encode_opts.small_mask_threshold_bytes = opts.small_mask_threshold_bytes as usize;
253 }
254 Ok(())
255}
256
257#[repr(C)]
261pub struct TgmDecodeMaskOptions {
262 pub restore_non_finite: bool,
263}
264
265unsafe fn apply_decode_mask_options(
273 decode_opts: &mut DecodeOptions,
274 opts: *const TgmDecodeMaskOptions,
275) {
276 if opts.is_null() {
277 return;
278 }
279 let opts = unsafe { &*opts };
280 decode_opts.restore_non_finite = opts.restore_non_finite;
281}
282
283#[unsafe(no_mangle)]
285pub extern "C" fn tgm_bytes_free(buf: TgmBytes) {
286 if !buf.data.is_null() {
287 unsafe {
288 drop(Vec::from_raw_parts(buf.data, buf.len, buf.len));
289 }
290 }
291}
292
293pub struct TgmMessage {
299 global_metadata: GlobalMetadata,
300 objects: Vec<(DataObjectDescriptor, Vec<u8>)>,
302 dtype_strings: Vec<CString>,
304 type_strings: Vec<CString>,
306 byte_order_strings: Vec<CString>,
308 filter_strings: Vec<CString>,
310 compression_strings: Vec<CString>,
312 encoding_strings: Vec<CString>,
314 hash_type_strings: Vec<Option<CString>>,
316 hash_value_strings: Vec<Option<CString>>,
318}
319
320pub struct TgmMetadata {
322 global_metadata: GlobalMetadata,
323 cache: std::cell::RefCell<BTreeMap<String, CString>>,
325}
326
327pub struct TgmFile {
329 file: TensogramFile,
330 path_string: CString,
332}
333
334#[repr(C)]
336#[derive(Clone, Copy)]
337pub struct TgmScanEntry {
338 pub offset: usize,
339 pub length: usize,
340}
341
342pub struct TgmScanResult {
344 entries: Vec<TgmScanEntry>,
345}
346
347#[derive(serde::Deserialize)]
361struct EncodeJson {
362 #[serde(default)]
367 version: Option<u16>,
368 #[serde(default)]
369 descriptors: Vec<DataObjectDescriptor>,
370 #[serde(default)]
372 base: Vec<BTreeMap<String, serde_json::Value>>,
373 #[serde(flatten)]
375 extra: BTreeMap<String, serde_json::Value>,
376}
377
378fn json_to_cbor(v: serde_json::Value) -> ciborium::Value {
381 match v {
382 serde_json::Value::Null => ciborium::Value::Null,
383 serde_json::Value::Bool(b) => ciborium::Value::Bool(b),
384 serde_json::Value::Number(n) => {
385 if let Some(i) = n.as_i64() {
386 ciborium::Value::Integer(i.into())
387 } else if let Some(f) = n.as_f64() {
388 ciborium::Value::Float(f)
389 } else {
390 ciborium::Value::Null
391 }
392 }
393 serde_json::Value::String(s) => ciborium::Value::Text(s),
394 serde_json::Value::Array(arr) => {
395 ciborium::Value::Array(arr.into_iter().map(json_to_cbor).collect())
396 }
397 serde_json::Value::Object(map) => ciborium::Value::Map(
398 map.into_iter()
399 .map(|(k, v)| (ciborium::Value::Text(k), json_to_cbor(v)))
400 .collect(),
401 ),
402 }
403}
404
405fn parse_encode_json(
417 json_str: &str,
418) -> Result<(GlobalMetadata, Vec<DataObjectDescriptor>), String> {
419 let parsed: EncodeJson = serde_json::from_str(json_str)
420 .map_err(|e| format!("failed to parse metadata JSON: {e}"))?;
421
422 let cbor_base: Vec<BTreeMap<String, ciborium::Value>> = parsed
423 .base
424 .into_iter()
425 .map(|entry| {
426 entry
427 .into_iter()
428 .map(|(k, v)| (k, json_to_cbor(v)))
429 .collect()
430 })
431 .collect();
432
433 for (i, entry) in cbor_base.iter().enumerate() {
435 if entry.contains_key(RESERVED_KEY) {
436 return Err(format!(
437 "base[{i}] must not contain '{RESERVED_KEY}' key — the encoder populates it"
438 ));
439 }
440 }
441
442 let cbor_extra = merge_flattened_extras_with_version(parsed.extra, parsed.version)?;
443
444 let global_metadata = GlobalMetadata {
445 base: cbor_base,
446 extra: cbor_extra,
447 ..Default::default()
448 };
449
450 Ok((global_metadata, parsed.descriptors))
451}
452
453fn merge_flattened_extras_with_version(
471 mut flattened: BTreeMap<String, serde_json::Value>,
472 legacy_version: Option<u16>,
473) -> Result<BTreeMap<String, ciborium::Value>, String> {
474 fn take_extra_map(
475 flattened: &mut BTreeMap<String, serde_json::Value>,
476 key: &str,
477 ) -> Result<Option<BTreeMap<String, serde_json::Value>>, String> {
478 match flattened.remove(key) {
479 None => Ok(None),
480 Some(serde_json::Value::Object(map)) => Ok(Some(map.into_iter().collect())),
481 Some(_) => Err(format!(
482 "'{key}' must be a JSON object when supplied at the top level"
483 )),
484 }
485 }
486
487 let under = take_extra_map(&mut flattened, "_extra_")?;
488 let plain = take_extra_map(&mut flattened, "extra")?;
489 let explicit_extra = match (under, plain) {
490 (Some(_), Some(_)) => {
491 return Err(
492 "both '_extra_' and 'extra' supplied at the top level — choose one".to_string(),
493 );
494 }
495 (Some(m), None) | (None, Some(m)) => m,
496 (None, None) => BTreeMap::new(),
497 };
498
499 let mut cbor_extra: BTreeMap<String, ciborium::Value> = explicit_extra
500 .into_iter()
501 .map(|(k, v)| (k, json_to_cbor(v)))
502 .collect();
503 for (k, v) in flattened {
504 cbor_extra.entry(k).or_insert_with(|| json_to_cbor(v));
507 }
508 if let Some(v) = legacy_version {
509 cbor_extra
510 .entry("version".to_string())
511 .or_insert_with(|| ciborium::Value::Integer(u64::from(v).into()));
512 }
513 Ok(cbor_extra)
514}
515
516struct MessageCaches {
522 dtype_strings: Vec<CString>,
523 type_strings: Vec<CString>,
524 byte_order_strings: Vec<CString>,
525 filter_strings: Vec<CString>,
526 compression_strings: Vec<CString>,
527 encoding_strings: Vec<CString>,
528 hash_type_strings: Vec<Option<CString>>,
529 hash_value_strings: Vec<Option<CString>>,
530}
531
532fn extract_inline_hashes(buf: &[u8]) -> Vec<Option<u64>> {
549 use tensogram::framing::{data_object_inline_hashes, scan};
550
551 let messages = scan(buf);
552 let Some(&(msg_off, msg_len)) = messages.first() else {
553 return Vec::new();
554 };
555 let msg = &buf[msg_off..msg_off + msg_len];
556 data_object_inline_hashes(msg).unwrap_or_default()
557}
558
559fn build_message_caches(
567 objects: &[(DataObjectDescriptor, Vec<u8>)],
568 inline_hashes: &[Option<u64>],
569) -> MessageCaches {
570 let dtype_strings = objects
571 .iter()
572 .map(|(desc, _)| CString::new(desc.dtype.to_string()).unwrap_or_default())
573 .collect();
574 let type_strings = objects
575 .iter()
576 .map(|(desc, _)| CString::new(desc.obj_type.as_str()).unwrap_or_default())
577 .collect();
578 let byte_order_strings = objects
579 .iter()
580 .map(|(desc, _)| {
581 let s = match desc.byte_order {
582 tensogram::ByteOrder::Big => "big",
583 tensogram::ByteOrder::Little => "little",
584 };
585 CString::new(s).unwrap_or_default()
586 })
587 .collect();
588 let filter_strings = objects
589 .iter()
590 .map(|(desc, _)| CString::new(desc.filter.as_str()).unwrap_or_default())
591 .collect();
592 let compression_strings = objects
593 .iter()
594 .map(|(desc, _)| CString::new(desc.compression.as_str()).unwrap_or_default())
595 .collect();
596 let encoding_strings = objects
597 .iter()
598 .map(|(desc, _)| CString::new(desc.encoding.as_str()).unwrap_or_default())
599 .collect();
600
601 let hash_type_strings: Vec<Option<CString>> = (0..objects.len())
606 .map(|i| {
607 inline_hashes
608 .get(i)
609 .and_then(|h| h.as_ref())
610 .map(|_| CString::new("xxh3").unwrap_or_default())
611 })
612 .collect();
613 let hash_value_strings: Vec<Option<CString>> = (0..objects.len())
614 .map(|i| {
615 inline_hashes
616 .get(i)
617 .and_then(|h| h.as_ref())
618 .map(|digest| CString::new(format!("{digest:016x}")).unwrap_or_default())
619 })
620 .collect();
621
622 MessageCaches {
623 dtype_strings,
624 type_strings,
625 byte_order_strings,
626 filter_strings,
627 compression_strings,
628 encoding_strings,
629 hash_type_strings,
630 hash_value_strings,
631 }
632}
633
634struct ParsedEncode<'a> {
640 global_metadata: GlobalMetadata,
641 descriptors: Vec<DataObjectDescriptor>,
642 data_slices: Vec<&'a [u8]>,
643 options: EncodeOptions,
644}
645
646pub(crate) fn parse_hash_algo(hash_algo: *const c_char) -> Result<bool, (TgmError, String)> {
662 if hash_algo.is_null() {
663 return Ok(false);
664 }
665 let s = unsafe { CStr::from_ptr(hash_algo) }.to_str().map_err(|_| {
666 (
667 TgmError::InvalidArg,
668 "invalid UTF-8 in hash_algo".to_string(),
669 )
670 })?;
671 parse_hash_name(Some(s)).map_err(|e| (TgmError::InvalidArg, e.to_string()))
672}
673
674unsafe fn collect_data_slices<'a>(
682 data_ptrs: *const *const u8,
683 data_lens: *const usize,
684 num_objects: usize,
685) -> Result<Vec<&'a [u8]>, (TgmError, String)> {
686 if num_objects == 0 {
687 return Ok(vec![]);
688 }
689 if data_ptrs.is_null() || data_lens.is_null() {
690 return Err((
691 TgmError::InvalidArg,
692 "null data_ptrs or data_lens".to_string(),
693 ));
694 }
695 let ptrs = unsafe { slice::from_raw_parts(data_ptrs, num_objects) };
696 let lens = unsafe { slice::from_raw_parts(data_lens, num_objects) };
697 for (i, (&p, &l)) in ptrs.iter().zip(lens.iter()).enumerate() {
698 if p.is_null() && l > 0 {
699 return Err((
700 TgmError::InvalidArg,
701 format!("null data pointer at index {i}"),
702 ));
703 }
704 }
705 Ok(ptrs
706 .iter()
707 .zip(lens.iter())
708 .map(|(&p, &l)| {
709 if l == 0 {
710 &[] as &[u8]
714 } else {
715 unsafe { slice::from_raw_parts(p, l) }
716 }
717 })
718 .collect())
719}
720
721unsafe fn parse_encode_args<'a>(
728 json_str: &str,
729 data_ptrs: *const *const u8,
730 data_lens: *const usize,
731 num_objects: usize,
732 hash_algo: *const c_char,
733 threads: u32,
734) -> Result<ParsedEncode<'a>, (TgmError, String)> {
735 let (global_metadata, descriptors) =
736 parse_encode_json(json_str).map_err(|e| (TgmError::Metadata, e))?;
737
738 if descriptors.len() != num_objects {
739 return Err((
740 TgmError::InvalidArg,
741 format!(
742 "descriptors array length {} does not match num_objects {}",
743 descriptors.len(),
744 num_objects
745 ),
746 ));
747 }
748
749 let data_slices = unsafe { collect_data_slices(data_ptrs, data_lens, num_objects) }?;
750 let hashing = parse_hash_algo(hash_algo)?;
751 let options = EncodeOptions {
752 hashing,
753 threads,
754 ..Default::default()
755 };
756
757 Ok(ParsedEncode {
758 global_metadata,
759 descriptors,
760 data_slices,
761 options,
762 })
763}
764
765#[unsafe(no_mangle)]
793pub extern "C" fn tgm_encode(
794 metadata_json: *const c_char,
795 data_ptrs: *const *const u8,
796 data_lens: *const usize,
797 num_objects: usize,
798 hash_algo: *const c_char,
799 threads: u32,
800 out: *mut TgmBytes,
801) -> TgmError {
802 if metadata_json.is_null() || out.is_null() {
803 set_last_error("null argument");
804 return TgmError::InvalidArg;
805 }
806
807 let json_str = match unsafe { CStr::from_ptr(metadata_json) }.to_str() {
808 Ok(s) => s,
809 Err(e) => {
810 set_last_error(&format!("invalid UTF-8 in metadata_json: {e}"));
811 return TgmError::InvalidArg;
812 }
813 };
814
815 let parsed = match unsafe {
816 parse_encode_args(
817 json_str,
818 data_ptrs,
819 data_lens,
820 num_objects,
821 hash_algo,
822 threads,
823 )
824 } {
825 Ok(p) => p,
826 Err((code, msg)) => {
827 set_last_error(&msg);
828 return code;
829 }
830 };
831
832 let pairs: Vec<(&DataObjectDescriptor, &[u8])> = parsed
834 .descriptors
835 .iter()
836 .zip(parsed.data_slices.iter())
837 .map(|(d, s)| (d, *s))
838 .collect();
839
840 match encode(&parsed.global_metadata, &pairs, &parsed.options) {
841 Ok(bytes) => {
842 let mut bytes = bytes.into_boxed_slice().into_vec();
844 let result = TgmBytes {
845 data: bytes.as_mut_ptr(),
846 len: bytes.len(),
847 };
848 std::mem::forget(bytes); unsafe {
850 *out = result;
851 }
852 TgmError::Ok
853 }
854 Err(e) => {
855 set_last_error(&e.to_string());
856 to_error_code(&e)
857 }
858 }
859}
860
861#[unsafe(no_mangle)]
867#[allow(clippy::too_many_arguments)]
868pub extern "C" fn tgm_encode_with_options(
869 metadata_json: *const c_char,
870 data_ptrs: *const *const u8,
871 data_lens: *const usize,
872 num_objects: usize,
873 hash_algo: *const c_char,
874 threads: u32,
875 mask_options: *const TgmEncodeMaskOptions,
876 out: *mut TgmBytes,
877) -> TgmError {
878 if metadata_json.is_null() || out.is_null() {
879 set_last_error("null argument");
880 return TgmError::InvalidArg;
881 }
882
883 let json_str = match unsafe { CStr::from_ptr(metadata_json) }.to_str() {
884 Ok(s) => s,
885 Err(e) => {
886 set_last_error(&format!("invalid UTF-8 in metadata_json: {e}"));
887 return TgmError::InvalidArg;
888 }
889 };
890
891 let mut parsed = match unsafe {
892 parse_encode_args(
893 json_str,
894 data_ptrs,
895 data_lens,
896 num_objects,
897 hash_algo,
898 threads,
899 )
900 } {
901 Ok(p) => p,
902 Err((code, msg)) => {
903 set_last_error(&msg);
904 return code;
905 }
906 };
907 if let Err(msg) = unsafe { apply_mask_options(&mut parsed.options, mask_options) } {
908 set_last_error(&msg);
909 return TgmError::InvalidArg;
910 }
911
912 let pairs: Vec<(&DataObjectDescriptor, &[u8])> = parsed
913 .descriptors
914 .iter()
915 .zip(parsed.data_slices.iter())
916 .map(|(d, s)| (d, *s))
917 .collect();
918
919 match encode(&parsed.global_metadata, &pairs, &parsed.options) {
920 Ok(bytes) => {
921 let mut bytes = bytes.into_boxed_slice().into_vec();
922 let result = TgmBytes {
923 data: bytes.as_mut_ptr(),
924 len: bytes.len(),
925 };
926 std::mem::forget(bytes);
927 unsafe {
928 *out = result;
929 }
930 TgmError::Ok
931 }
932 Err(e) => {
933 set_last_error(&e.to_string());
934 to_error_code(&e)
935 }
936 }
937}
938
939#[unsafe(no_mangle)]
945#[allow(clippy::too_many_arguments)]
946pub extern "C" fn tgm_decode_with_options(
947 buf: *const u8,
948 buf_len: usize,
949 native_byte_order: i32,
950 threads: u32,
951 verify_hash: i32,
952 mask_options: *const TgmDecodeMaskOptions,
953 out: *mut *mut TgmMessage,
954) -> TgmError {
955 if buf.is_null() || out.is_null() {
956 set_last_error("null argument");
957 return TgmError::InvalidArg;
958 }
959
960 let data = unsafe { slice::from_raw_parts(buf, buf_len) };
961 let mut options = DecodeOptions {
962 native_byte_order: native_byte_order != 0,
963 threads,
964 verify_hash: verify_hash != 0,
965 ..Default::default()
966 };
967 unsafe { apply_decode_mask_options(&mut options, mask_options) };
968
969 match decode(data, &options) {
970 Ok((global_metadata, objects)) => {
971 let inline_hashes = extract_inline_hashes(data);
972 let caches = build_message_caches(&objects, &inline_hashes);
973 let msg = Box::new(TgmMessage {
974 global_metadata,
975 objects,
976 dtype_strings: caches.dtype_strings,
977 type_strings: caches.type_strings,
978 byte_order_strings: caches.byte_order_strings,
979 filter_strings: caches.filter_strings,
980 compression_strings: caches.compression_strings,
981 encoding_strings: caches.encoding_strings,
982 hash_type_strings: caches.hash_type_strings,
983 hash_value_strings: caches.hash_value_strings,
984 });
985 unsafe {
986 *out = Box::into_raw(msg);
987 }
988 TgmError::Ok
989 }
990 Err(e) => {
991 set_last_error(&e.to_string());
992 to_error_code(&e)
993 }
994 }
995}
996
997#[unsafe(no_mangle)]
1003#[allow(clippy::too_many_arguments)]
1004pub extern "C" fn tgm_streaming_encoder_create_with_options(
1005 path: *const c_char,
1006 metadata_json: *const c_char,
1007 hash_algo: *const c_char,
1008 threads: u32,
1009 mask_options: *const TgmEncodeMaskOptions,
1010 out: *mut *mut TgmStreamingEncoder,
1011) -> TgmError {
1012 if path.is_null() || metadata_json.is_null() || out.is_null() {
1025 set_last_error("null argument");
1026 return TgmError::InvalidArg;
1027 }
1028 let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
1029 Ok(s) => s,
1030 Err(e) => {
1031 set_last_error(&format!("invalid UTF-8 in path: {e}"));
1032 return TgmError::InvalidArg;
1033 }
1034 };
1035 let json_str = match unsafe { CStr::from_ptr(metadata_json) }.to_str() {
1036 Ok(s) => s,
1037 Err(e) => {
1038 set_last_error(&format!("invalid UTF-8 in metadata_json: {e}"));
1039 return TgmError::InvalidArg;
1040 }
1041 };
1042 let global_metadata = match parse_streaming_metadata_json(json_str) {
1043 Ok(m) => m,
1044 Err(e) => {
1045 set_last_error(&e);
1046 return TgmError::Metadata;
1047 }
1048 };
1049 let hashing = match parse_hash_algo(hash_algo) {
1050 Ok(b) => b,
1051 Err((code, msg)) => {
1052 set_last_error(&msg);
1053 return code;
1054 }
1055 };
1056 let file = match std::fs::File::create(path_str) {
1057 Ok(f) => f,
1058 Err(e) => {
1059 set_last_error(&e.to_string());
1060 return TgmError::Io;
1061 }
1062 };
1063 let mut options = EncodeOptions {
1064 hashing,
1065 threads,
1066 ..Default::default()
1067 };
1068 if let Err(msg) = unsafe { apply_mask_options(&mut options, mask_options) } {
1069 set_last_error(&msg);
1070 return TgmError::InvalidArg;
1071 }
1072 let writer = std::io::BufWriter::new(file);
1073 match StreamingEncoder::new(writer, &global_metadata, &options) {
1074 Ok(enc) => {
1075 let handle = Box::new(TgmStreamingEncoder { inner: Some(enc) });
1076 unsafe {
1077 *out = Box::into_raw(handle);
1078 }
1079 TgmError::Ok
1080 }
1081 Err(e) => {
1082 set_last_error(&e.to_string());
1083 to_error_code(&e)
1084 }
1085 }
1086}
1087
1088#[unsafe(no_mangle)]
1093#[allow(clippy::too_many_arguments)]
1094pub extern "C" fn tgm_file_append_with_options(
1095 file: *mut TgmFile,
1096 metadata_json: *const c_char,
1097 data_ptrs: *const *const u8,
1098 data_lens: *const usize,
1099 num_objects: usize,
1100 hash_algo: *const c_char,
1101 threads: u32,
1102 mask_options: *const TgmEncodeMaskOptions,
1103) -> TgmError {
1104 if file.is_null() || metadata_json.is_null() {
1105 set_last_error("null argument");
1106 return TgmError::InvalidArg;
1107 }
1108 let json_str = match unsafe { CStr::from_ptr(metadata_json) }.to_str() {
1109 Ok(s) => s,
1110 Err(e) => {
1111 set_last_error(&format!("invalid UTF-8 in metadata_json: {e}"));
1112 return TgmError::InvalidArg;
1113 }
1114 };
1115 let mut parsed = match unsafe {
1116 parse_encode_args(
1117 json_str,
1118 data_ptrs,
1119 data_lens,
1120 num_objects,
1121 hash_algo,
1122 threads,
1123 )
1124 } {
1125 Ok(p) => p,
1126 Err((code, msg)) => {
1127 set_last_error(&msg);
1128 return code;
1129 }
1130 };
1131 if let Err(msg) = unsafe { apply_mask_options(&mut parsed.options, mask_options) } {
1132 set_last_error(&msg);
1133 return TgmError::InvalidArg;
1134 }
1135 let pairs: Vec<(&DataObjectDescriptor, &[u8])> = parsed
1136 .descriptors
1137 .iter()
1138 .zip(parsed.data_slices.iter())
1139 .map(|(d, s)| (d, *s))
1140 .collect();
1141 let f = unsafe { &mut (*file).file };
1142 match f.append(&parsed.global_metadata, &pairs, &parsed.options) {
1143 Ok(()) => TgmError::Ok,
1144 Err(e) => {
1145 set_last_error(&e.to_string());
1146 to_error_code(&e)
1147 }
1148 }
1149}
1150
1151#[unsafe(no_mangle)]
1181pub extern "C" fn tgm_encode_pre_encoded(
1182 metadata_json: *const c_char,
1183 data_ptrs: *const *const u8,
1184 data_lens: *const usize,
1185 num_objects: usize,
1186 hash_algo: *const c_char,
1187 threads: u32,
1188 out: *mut TgmBytes,
1189) -> TgmError {
1190 if metadata_json.is_null() || out.is_null() {
1191 set_last_error("null argument");
1192 return TgmError::InvalidArg;
1193 }
1194
1195 let json_str = match unsafe { CStr::from_ptr(metadata_json) }.to_str() {
1196 Ok(s) => s,
1197 Err(e) => {
1198 set_last_error(&format!("invalid UTF-8 in metadata_json: {e}"));
1199 return TgmError::InvalidArg;
1200 }
1201 };
1202
1203 let parsed = match unsafe {
1204 parse_encode_args(
1205 json_str,
1206 data_ptrs,
1207 data_lens,
1208 num_objects,
1209 hash_algo,
1210 threads,
1211 )
1212 } {
1213 Ok(p) => p,
1214 Err((code, msg)) => {
1215 set_last_error(&msg);
1216 return code;
1217 }
1218 };
1219
1220 let pairs: Vec<(&DataObjectDescriptor, &[u8])> = parsed
1222 .descriptors
1223 .iter()
1224 .zip(parsed.data_slices.iter())
1225 .map(|(d, s)| (d, *s))
1226 .collect();
1227
1228 match encode_pre_encoded(&parsed.global_metadata, &pairs, &parsed.options) {
1229 Ok(bytes) => {
1230 let mut bytes = bytes.into_boxed_slice().into_vec();
1232 let result = TgmBytes {
1233 data: bytes.as_mut_ptr(),
1234 len: bytes.len(),
1235 };
1236 std::mem::forget(bytes); unsafe {
1238 *out = result;
1239 }
1240 TgmError::Ok
1241 }
1242 Err(e) => {
1243 set_last_error(&e.to_string());
1244 to_error_code(&e)
1245 }
1246 }
1247}
1248
1249#[unsafe(no_mangle)]
1260pub extern "C" fn tgm_decode(
1261 buf: *const u8,
1262 buf_len: usize,
1263 native_byte_order: i32,
1264 threads: u32,
1265 verify_hash: i32,
1266 out: *mut *mut TgmMessage,
1267) -> TgmError {
1268 if buf.is_null() || out.is_null() {
1269 set_last_error("null argument");
1270 return TgmError::InvalidArg;
1271 }
1272
1273 let data = unsafe { slice::from_raw_parts(buf, buf_len) };
1274 let options = DecodeOptions {
1275 native_byte_order: native_byte_order != 0,
1276 threads,
1277 verify_hash: verify_hash != 0,
1278 ..Default::default()
1279 };
1280
1281 match decode(data, &options) {
1282 Ok((global_metadata, objects)) => {
1283 let inline_hashes = extract_inline_hashes(data);
1284 let caches = build_message_caches(&objects, &inline_hashes);
1285 let msg = Box::new(TgmMessage {
1286 global_metadata,
1287 objects,
1288 dtype_strings: caches.dtype_strings,
1289 type_strings: caches.type_strings,
1290 byte_order_strings: caches.byte_order_strings,
1291 filter_strings: caches.filter_strings,
1292 compression_strings: caches.compression_strings,
1293 encoding_strings: caches.encoding_strings,
1294 hash_type_strings: caches.hash_type_strings,
1295 hash_value_strings: caches.hash_value_strings,
1296 });
1297 unsafe {
1298 *out = Box::into_raw(msg);
1299 }
1300 TgmError::Ok
1301 }
1302 Err(e) => {
1303 set_last_error(&e.to_string());
1304 to_error_code(&e)
1305 }
1306 }
1307}
1308
1309#[unsafe(no_mangle)]
1311pub extern "C" fn tgm_decode_metadata(
1312 buf: *const u8,
1313 buf_len: usize,
1314 out: *mut *mut TgmMetadata,
1315) -> TgmError {
1316 if buf.is_null() || out.is_null() {
1317 set_last_error("null argument");
1318 return TgmError::InvalidArg;
1319 }
1320
1321 let data = unsafe { slice::from_raw_parts(buf, buf_len) };
1322
1323 match decode_metadata(data) {
1324 Ok(global_metadata) => {
1325 let m = Box::new(TgmMetadata {
1326 global_metadata,
1327 cache: std::cell::RefCell::new(BTreeMap::new()),
1328 });
1329 unsafe {
1330 *out = Box::into_raw(m);
1331 }
1332 TgmError::Ok
1333 }
1334 Err(e) => {
1335 set_last_error(&e.to_string());
1336 to_error_code(&e)
1337 }
1338 }
1339}
1340
1341#[unsafe(no_mangle)]
1346pub extern "C" fn tgm_decode_object(
1347 buf: *const u8,
1348 buf_len: usize,
1349 index: usize,
1350 native_byte_order: i32,
1351 threads: u32,
1352 verify_hash: i32,
1353 out: *mut *mut TgmMessage,
1354) -> TgmError {
1355 if buf.is_null() || out.is_null() {
1356 set_last_error("null argument");
1357 return TgmError::InvalidArg;
1358 }
1359
1360 let data = unsafe { slice::from_raw_parts(buf, buf_len) };
1361 let options = DecodeOptions {
1362 native_byte_order: native_byte_order != 0,
1363 threads,
1364 verify_hash: verify_hash != 0,
1365 ..Default::default()
1366 };
1367
1368 match decode_object(data, index, &options) {
1369 Ok((global_metadata, descriptor, obj_bytes)) => {
1370 let objects = vec![(descriptor, obj_bytes)];
1371 let inline_hashes = extract_inline_hashes(data);
1372 let caches = build_message_caches(&objects, &inline_hashes);
1373 let msg = Box::new(TgmMessage {
1374 global_metadata,
1375 objects,
1376 dtype_strings: caches.dtype_strings,
1377 type_strings: caches.type_strings,
1378 byte_order_strings: caches.byte_order_strings,
1379 filter_strings: caches.filter_strings,
1380 compression_strings: caches.compression_strings,
1381 encoding_strings: caches.encoding_strings,
1382 hash_type_strings: caches.hash_type_strings,
1383 hash_value_strings: caches.hash_value_strings,
1384 });
1385 unsafe {
1386 *out = Box::into_raw(msg);
1387 }
1388 TgmError::Ok
1389 }
1390 Err(e) => {
1391 set_last_error(&e.to_string());
1392 to_error_code(&e)
1393 }
1394 }
1395}
1396
1397#[unsafe(no_mangle)]
1410#[allow(clippy::too_many_arguments)]
1411pub extern "C" fn tgm_decode_range(
1412 buf: *const u8,
1413 buf_len: usize,
1414 object_index: usize,
1415 ranges_offsets: *const u64,
1416 ranges_counts: *const u64,
1417 num_ranges: usize,
1418 native_byte_order: i32,
1419 threads: u32,
1420 join: i32,
1421 out: *mut TgmBytes,
1422 out_count: *mut usize,
1423) -> TgmError {
1424 if buf.is_null() || out.is_null() || out_count.is_null() {
1425 set_last_error("null argument");
1426 return TgmError::InvalidArg;
1427 }
1428 if num_ranges > 0 && (ranges_offsets.is_null() || ranges_counts.is_null()) {
1429 set_last_error("null ranges_offsets or ranges_counts");
1430 return TgmError::InvalidArg;
1431 }
1432
1433 let data = unsafe { slice::from_raw_parts(buf, buf_len) };
1434 let options = DecodeOptions {
1435 native_byte_order: native_byte_order != 0,
1436 threads,
1437 ..Default::default()
1438 };
1439
1440 let ranges: Vec<(u64, u64)> = if num_ranges == 0 {
1441 vec![]
1442 } else {
1443 unsafe {
1444 let offsets = slice::from_raw_parts(ranges_offsets, num_ranges);
1445 let counts = slice::from_raw_parts(ranges_counts, num_ranges);
1446 offsets
1447 .iter()
1448 .zip(counts.iter())
1449 .map(|(&o, &c)| (o, c))
1450 .collect()
1451 }
1452 };
1453
1454 match decode_range(data, object_index, &ranges, &options) {
1455 Ok((_, parts)) => {
1456 if join != 0 {
1457 let joined: Vec<u8> = parts.into_iter().flatten().collect();
1459 let mut joined = joined.into_boxed_slice().into_vec();
1460 let result = TgmBytes {
1461 data: joined.as_mut_ptr(),
1462 len: joined.len(),
1463 };
1464 std::mem::forget(joined);
1465 unsafe {
1466 *out = result;
1467 *out_count = 1;
1468 }
1469 } else {
1470 let n = parts.len();
1472 for (i, part) in parts.into_iter().enumerate() {
1473 let mut part = part.into_boxed_slice().into_vec();
1474 let result = TgmBytes {
1475 data: part.as_mut_ptr(),
1476 len: part.len(),
1477 };
1478 std::mem::forget(part);
1479 unsafe {
1480 *out.add(i) = result;
1481 }
1482 }
1483 unsafe {
1484 *out_count = n;
1485 }
1486 }
1487 TgmError::Ok
1488 }
1489 Err(e) => {
1490 set_last_error(&e.to_string());
1491 to_error_code(&e)
1492 }
1493 }
1494}
1495
1496#[unsafe(no_mangle)]
1505pub extern "C" fn tgm_scan(
1506 buf: *const u8,
1507 buf_len: usize,
1508 out: *mut *mut TgmScanResult,
1509) -> TgmError {
1510 if buf.is_null() || out.is_null() {
1511 set_last_error("null argument");
1512 return TgmError::InvalidArg;
1513 }
1514
1515 let data = unsafe { slice::from_raw_parts(buf, buf_len) };
1516 let offsets = scan(data);
1517 let entries: Vec<TgmScanEntry> = offsets
1518 .into_iter()
1519 .map(|(offset, length)| TgmScanEntry { offset, length })
1520 .collect();
1521 let result = Box::new(TgmScanResult { entries });
1522 unsafe {
1523 *out = Box::into_raw(result);
1524 }
1525 TgmError::Ok
1526}
1527
1528unsafe fn as_scan(result: *const TgmScanResult) -> Option<&'static TgmScanResult> {
1530 unsafe {
1531 if result.is_null() {
1532 None
1533 } else {
1534 Some(&*result)
1535 }
1536 }
1537}
1538
1539unsafe fn as_msg(msg: *const TgmMessage) -> Option<&'static TgmMessage> {
1541 unsafe { if msg.is_null() { None } else { Some(&*msg) } }
1542}
1543
1544#[unsafe(no_mangle)]
1546pub extern "C" fn tgm_scan_count(result: *const TgmScanResult) -> usize {
1547 unsafe { as_scan(result).map(|r| r.entries.len()).unwrap_or(0) }
1548}
1549
1550#[unsafe(no_mangle)]
1551pub extern "C" fn tgm_scan_entry(result: *const TgmScanResult, index: usize) -> TgmScanEntry {
1552 let fallback = TgmScanEntry {
1553 offset: usize::MAX,
1554 length: 0,
1555 };
1556 unsafe {
1557 match as_scan(result) {
1558 Some(r) => match r.entries.get(index) {
1559 Some(entry) => *entry,
1560 None => {
1561 set_last_error(&format!(
1562 "scan entry index {} out of range (count={})",
1563 index,
1564 r.entries.len()
1565 ));
1566 fallback
1567 }
1568 },
1569 None => {
1570 set_last_error("null scan result handle");
1571 fallback
1572 }
1573 }
1574 }
1575}
1576
1577#[unsafe(no_mangle)]
1579pub extern "C" fn tgm_scan_free(result: *mut TgmScanResult) {
1580 if !result.is_null() {
1581 unsafe {
1582 drop(Box::from_raw(result));
1583 }
1584 }
1585}
1586
1587#[unsafe(no_mangle)]
1599pub extern "C" fn tgm_message_version(msg: *const TgmMessage) -> u64 {
1600 unsafe {
1601 as_msg(msg)
1602 .map(|_| tensogram::WIRE_VERSION as u64)
1603 .unwrap_or(0)
1604 }
1605}
1606
1607#[unsafe(no_mangle)]
1611pub extern "C" fn tgm_message_num_objects(msg: *const TgmMessage) -> usize {
1612 unsafe { as_msg(msg).map(|m| m.objects.len()).unwrap_or(0) }
1613}
1614
1615#[unsafe(no_mangle)]
1618pub extern "C" fn tgm_message_num_decoded(msg: *const TgmMessage) -> usize {
1619 unsafe { as_msg(msg).map(|m| m.objects.len()).unwrap_or(0) }
1620}
1621
1622#[unsafe(no_mangle)]
1624pub extern "C" fn tgm_object_ndim(msg: *const TgmMessage, index: usize) -> u64 {
1625 unsafe {
1626 as_msg(msg)
1627 .and_then(|m| m.objects.get(index))
1628 .map(|(desc, _)| desc.ndim)
1629 .unwrap_or(0)
1630 }
1631}
1632
1633#[unsafe(no_mangle)]
1636pub extern "C" fn tgm_object_shape(msg: *const TgmMessage, index: usize) -> *const u64 {
1637 unsafe {
1638 as_msg(msg)
1639 .and_then(|m| m.objects.get(index))
1640 .map(|(desc, _)| desc.shape.as_ptr())
1641 .unwrap_or(ptr::null())
1642 }
1643}
1644
1645#[unsafe(no_mangle)]
1647pub extern "C" fn tgm_object_strides(msg: *const TgmMessage, index: usize) -> *const u64 {
1648 unsafe {
1649 as_msg(msg)
1650 .and_then(|m| m.objects.get(index))
1651 .map(|(desc, _)| desc.strides.as_ptr())
1652 .unwrap_or(ptr::null())
1653 }
1654}
1655
1656#[unsafe(no_mangle)]
1659pub extern "C" fn tgm_object_dtype(msg: *const TgmMessage, index: usize) -> *const c_char {
1660 unsafe {
1661 as_msg(msg)
1662 .and_then(|m| m.dtype_strings.get(index))
1663 .map(|s| s.as_ptr())
1664 .unwrap_or(ptr::null())
1665 }
1666}
1667
1668#[unsafe(no_mangle)]
1673pub extern "C" fn tgm_object_data(
1674 msg: *const TgmMessage,
1675 decoded_index: usize,
1676 out_len: *mut usize,
1677) -> *const u8 {
1678 unsafe {
1679 match as_msg(msg).and_then(|m| m.objects.get(decoded_index)) {
1680 Some((_, data)) => {
1681 if !out_len.is_null() {
1682 *out_len = data.len();
1683 }
1684 data.as_ptr()
1685 }
1686 None => {
1687 if !out_len.is_null() {
1688 *out_len = 0;
1689 }
1690 ptr::null()
1691 }
1692 }
1693 }
1694}
1695
1696#[unsafe(no_mangle)]
1699pub extern "C" fn tgm_payload_encoding(msg: *const TgmMessage, index: usize) -> *const c_char {
1700 unsafe {
1701 as_msg(msg)
1702 .and_then(|m| m.encoding_strings.get(index))
1703 .map(|s| s.as_ptr())
1704 .unwrap_or(ptr::null())
1705 }
1706}
1707
1708#[unsafe(no_mangle)]
1729pub extern "C" fn tgm_payload_has_hash(msg: *const TgmMessage, index: usize) -> i32 {
1730 unsafe {
1731 as_msg(msg)
1732 .and_then(|m| m.hash_value_strings.get(index))
1733 .map(|opt| opt.is_some() as i32)
1734 .unwrap_or(0)
1735 }
1736}
1737
1738#[unsafe(no_mangle)]
1741pub extern "C" fn tgm_message_metadata(
1742 msg: *const TgmMessage,
1743 out: *mut *mut TgmMetadata,
1744) -> TgmError {
1745 if msg.is_null() || out.is_null() {
1746 set_last_error("null argument");
1747 return TgmError::InvalidArg;
1748 }
1749 let m = unsafe { &*msg };
1750 let meta = Box::new(TgmMetadata {
1751 global_metadata: m.global_metadata.clone(),
1752 cache: std::cell::RefCell::new(BTreeMap::new()),
1753 });
1754 unsafe {
1755 *out = Box::into_raw(meta);
1756 }
1757 TgmError::Ok
1758}
1759
1760#[unsafe(no_mangle)]
1762pub extern "C" fn tgm_object_type(msg: *const TgmMessage, index: usize) -> *const c_char {
1763 unsafe {
1764 as_msg(msg)
1765 .and_then(|m| m.type_strings.get(index))
1766 .map(|s| s.as_ptr())
1767 .unwrap_or(ptr::null())
1768 }
1769}
1770
1771#[unsafe(no_mangle)]
1773pub extern "C" fn tgm_object_byte_order(msg: *const TgmMessage, index: usize) -> *const c_char {
1774 unsafe {
1775 as_msg(msg)
1776 .and_then(|m| m.byte_order_strings.get(index))
1777 .map(|s| s.as_ptr())
1778 .unwrap_or(ptr::null())
1779 }
1780}
1781
1782#[unsafe(no_mangle)]
1784pub extern "C" fn tgm_object_filter(msg: *const TgmMessage, index: usize) -> *const c_char {
1785 unsafe {
1786 as_msg(msg)
1787 .and_then(|m| m.filter_strings.get(index))
1788 .map(|s| s.as_ptr())
1789 .unwrap_or(ptr::null())
1790 }
1791}
1792
1793#[unsafe(no_mangle)]
1795pub extern "C" fn tgm_object_compression(msg: *const TgmMessage, index: usize) -> *const c_char {
1796 unsafe {
1797 as_msg(msg)
1798 .and_then(|m| m.compression_strings.get(index))
1799 .map(|s| s.as_ptr())
1800 .unwrap_or(ptr::null())
1801 }
1802}
1803
1804#[unsafe(no_mangle)]
1806pub extern "C" fn tgm_object_hash_type(msg: *const TgmMessage, index: usize) -> *const c_char {
1807 unsafe {
1808 as_msg(msg)
1809 .and_then(|m| m.hash_type_strings.get(index))
1810 .and_then(|opt| opt.as_ref())
1811 .map(|s| s.as_ptr())
1812 .unwrap_or(ptr::null())
1813 }
1814}
1815
1816#[unsafe(no_mangle)]
1818pub extern "C" fn tgm_object_hash_value(msg: *const TgmMessage, index: usize) -> *const c_char {
1819 unsafe {
1820 as_msg(msg)
1821 .and_then(|m| m.hash_value_strings.get(index))
1822 .and_then(|opt| opt.as_ref())
1823 .map(|s| s.as_ptr())
1824 .unwrap_or(ptr::null())
1825 }
1826}
1827
1828#[unsafe(no_mangle)]
1830pub extern "C" fn tgm_message_free(msg: *mut TgmMessage) {
1831 if !msg.is_null() {
1832 unsafe {
1833 drop(Box::from_raw(msg));
1834 }
1835 }
1836}
1837
1838#[unsafe(no_mangle)]
1849pub extern "C" fn tgm_metadata_version(meta: *const TgmMetadata) -> u64 {
1850 if meta.is_null() {
1851 return 0;
1852 }
1853 tensogram::WIRE_VERSION as u64
1854}
1855
1856#[unsafe(no_mangle)]
1860pub extern "C" fn tgm_metadata_num_objects(meta: *const TgmMetadata) -> usize {
1861 if meta.is_null() {
1862 return 0;
1863 }
1864 unsafe { (*meta).global_metadata.base.len() }
1865}
1866
1867#[unsafe(no_mangle)]
1871pub extern "C" fn tgm_metadata_get_string(
1872 meta: *const TgmMetadata,
1873 key: *const c_char,
1874) -> *const c_char {
1875 if meta.is_null() || key.is_null() {
1876 return ptr::null();
1877 }
1878
1879 let key_str = match unsafe { CStr::from_ptr(key) }.to_str() {
1880 Ok(s) => s,
1881 Err(_) => return ptr::null(),
1882 };
1883
1884 let m = unsafe { &(*meta) };
1885 let value = lookup_string_key(&m.global_metadata, key_str);
1886
1887 match value {
1888 Some(s) => {
1889 let mut cache = m.cache.borrow_mut();
1890 let entry = cache
1891 .entry(key_str.to_string())
1892 .or_insert_with(|| CString::new(s.clone()).unwrap_or_default());
1893 entry.as_ptr()
1894 }
1895 None => ptr::null(),
1896 }
1897}
1898
1899#[unsafe(no_mangle)]
1902pub extern "C" fn tgm_metadata_get_int(
1903 meta: *const TgmMetadata,
1904 key: *const c_char,
1905 default_val: i64,
1906) -> i64 {
1907 if meta.is_null() || key.is_null() {
1908 return default_val;
1909 }
1910
1911 let key_str = match unsafe { CStr::from_ptr(key) }.to_str() {
1912 Ok(s) => s,
1913 Err(_) => return default_val,
1914 };
1915
1916 let m = unsafe { &(*meta) };
1917 lookup_int_key(&m.global_metadata, key_str).unwrap_or(default_val)
1918}
1919
1920#[unsafe(no_mangle)]
1922pub extern "C" fn tgm_metadata_get_float(
1923 meta: *const TgmMetadata,
1924 key: *const c_char,
1925 default_val: f64,
1926) -> f64 {
1927 if meta.is_null() || key.is_null() {
1928 return default_val;
1929 }
1930
1931 let key_str = match unsafe { CStr::from_ptr(key) }.to_str() {
1932 Ok(s) => s,
1933 Err(_) => return default_val,
1934 };
1935
1936 let m = unsafe { &(*meta) };
1937 lookup_float_key(&m.global_metadata, key_str).unwrap_or(default_val)
1938}
1939
1940#[unsafe(no_mangle)]
1942pub extern "C" fn tgm_metadata_free(meta: *mut TgmMetadata) {
1943 if !meta.is_null() {
1944 unsafe {
1945 drop(Box::from_raw(meta));
1946 }
1947 }
1948}
1949
1950#[unsafe(no_mangle)]
1956pub extern "C" fn tgm_file_open(path: *const c_char, out: *mut *mut TgmFile) -> TgmError {
1957 if path.is_null() || out.is_null() {
1958 set_last_error("null argument");
1959 return TgmError::InvalidArg;
1960 }
1961
1962 let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
1963 Ok(s) => s,
1964 Err(e) => {
1965 set_last_error(&format!("invalid UTF-8 in path: {e}"));
1966 return TgmError::InvalidArg;
1967 }
1968 };
1969
1970 match TensogramFile::open(path_str) {
1971 Ok(file) => {
1972 let path_string = CString::new(path_str).unwrap_or_default();
1973 let handle = Box::new(TgmFile { file, path_string });
1974 unsafe {
1975 *out = Box::into_raw(handle);
1976 }
1977 TgmError::Ok
1978 }
1979 Err(e) => {
1980 set_last_error(&e.to_string());
1981 to_error_code(&e)
1982 }
1983 }
1984}
1985
1986#[unsafe(no_mangle)]
1988pub extern "C" fn tgm_file_create(path: *const c_char, out: *mut *mut TgmFile) -> TgmError {
1989 if path.is_null() || out.is_null() {
1990 set_last_error("null argument");
1991 return TgmError::InvalidArg;
1992 }
1993
1994 let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
1995 Ok(s) => s,
1996 Err(e) => {
1997 set_last_error(&format!("invalid UTF-8 in path: {e}"));
1998 return TgmError::InvalidArg;
1999 }
2000 };
2001
2002 match TensogramFile::create(path_str) {
2003 Ok(file) => {
2004 let path_string = CString::new(path_str).unwrap_or_default();
2005 let handle = Box::new(TgmFile { file, path_string });
2006 unsafe {
2007 *out = Box::into_raw(handle);
2008 }
2009 TgmError::Ok
2010 }
2011 Err(e) => {
2012 set_last_error(&e.to_string());
2013 to_error_code(&e)
2014 }
2015 }
2016}
2017
2018#[unsafe(no_mangle)]
2020pub extern "C" fn tgm_file_message_count(file: *mut TgmFile, out_count: *mut usize) -> TgmError {
2021 if file.is_null() || out_count.is_null() {
2022 set_last_error("null argument");
2023 return TgmError::InvalidArg;
2024 }
2025
2026 let f = unsafe { &(*file).file };
2027 match f.message_count() {
2028 Ok(count) => {
2029 unsafe {
2030 *out_count = count;
2031 }
2032 TgmError::Ok
2033 }
2034 Err(e) => {
2035 set_last_error(&e.to_string());
2036 to_error_code(&e)
2037 }
2038 }
2039}
2040
2041#[unsafe(no_mangle)]
2044pub extern "C" fn tgm_file_decode_message(
2045 file: *mut TgmFile,
2046 index: usize,
2047 native_byte_order: i32,
2048 threads: u32,
2049 verify_hash: i32,
2050 out: *mut *mut TgmMessage,
2051) -> TgmError {
2052 if file.is_null() || out.is_null() {
2053 set_last_error("null argument");
2054 return TgmError::InvalidArg;
2055 }
2056
2057 let f = unsafe { &(*file).file };
2058 let options = DecodeOptions {
2059 native_byte_order: native_byte_order != 0,
2060 threads,
2061 verify_hash: verify_hash != 0,
2062 ..Default::default()
2063 };
2064
2065 match f.decode_message(index, &options) {
2066 Ok((global_metadata, objects)) => {
2067 let inline_hashes = f
2075 .read_message(index)
2076 .ok()
2077 .and_then(|bytes| tensogram::framing::data_object_inline_hashes(&bytes).ok())
2078 .unwrap_or_default();
2079 let caches = build_message_caches(&objects, &inline_hashes);
2080 let msg = Box::new(TgmMessage {
2081 global_metadata,
2082 objects,
2083 dtype_strings: caches.dtype_strings,
2084 type_strings: caches.type_strings,
2085 byte_order_strings: caches.byte_order_strings,
2086 filter_strings: caches.filter_strings,
2087 compression_strings: caches.compression_strings,
2088 encoding_strings: caches.encoding_strings,
2089 hash_type_strings: caches.hash_type_strings,
2090 hash_value_strings: caches.hash_value_strings,
2091 });
2092 unsafe {
2093 *out = Box::into_raw(msg);
2094 }
2095 TgmError::Ok
2096 }
2097 Err(e) => {
2098 set_last_error(&e.to_string());
2099 to_error_code(&e)
2100 }
2101 }
2102}
2103
2104#[unsafe(no_mangle)]
2107pub extern "C" fn tgm_file_read_message(
2108 file: *mut TgmFile,
2109 index: usize,
2110 out: *mut TgmBytes,
2111) -> TgmError {
2112 if file.is_null() || out.is_null() {
2113 set_last_error("null argument");
2114 return TgmError::InvalidArg;
2115 }
2116
2117 let f = unsafe { &(*file).file };
2118
2119 match f.read_message(index) {
2120 Ok(bytes) => {
2121 let mut bytes = bytes.into_boxed_slice().into_vec();
2123 let result = TgmBytes {
2124 data: bytes.as_mut_ptr(),
2125 len: bytes.len(),
2126 };
2127 std::mem::forget(bytes);
2128 unsafe {
2129 *out = result;
2130 }
2131 TgmError::Ok
2132 }
2133 Err(e) => {
2134 set_last_error(&e.to_string());
2135 to_error_code(&e)
2136 }
2137 }
2138}
2139
2140#[unsafe(no_mangle)]
2142pub extern "C" fn tgm_file_append_raw(
2143 file: *mut TgmFile,
2144 buf: *const u8,
2145 buf_len: usize,
2146) -> TgmError {
2147 if file.is_null() || buf.is_null() {
2148 set_last_error("null argument");
2149 return TgmError::InvalidArg;
2150 }
2151
2152 let data = unsafe { slice::from_raw_parts(buf, buf_len) };
2153 let f = unsafe { &mut (*file).file };
2154
2155 use std::io::Write;
2157 let path = match f.path() {
2158 Some(p) => p.to_path_buf(),
2159 None => {
2160 set_last_error("append_raw not supported on remote files");
2161 return TgmError::Remote;
2162 }
2163 };
2164 let result = std::fs::OpenOptions::new()
2165 .create(true)
2166 .append(true)
2167 .open(&path)
2168 .and_then(|mut fh| fh.write_all(data));
2169
2170 match result {
2171 Ok(()) => {
2172 f.invalidate_offsets();
2173 TgmError::Ok
2174 }
2175 Err(e) => {
2176 set_last_error(&e.to_string());
2177 TgmError::Io
2178 }
2179 }
2180}
2181
2182#[unsafe(no_mangle)]
2185pub extern "C" fn tgm_file_path(file: *const TgmFile) -> *const c_char {
2186 if file.is_null() {
2187 return ptr::null();
2188 }
2189 unsafe { (*file).path_string.as_ptr() }
2190}
2191
2192#[unsafe(no_mangle)]
2197pub extern "C" fn tgm_file_append(
2198 file: *mut TgmFile,
2199 metadata_json: *const c_char,
2200 data_ptrs: *const *const u8,
2201 data_lens: *const usize,
2202 num_objects: usize,
2203 hash_algo: *const c_char,
2204 threads: u32,
2205) -> TgmError {
2206 if file.is_null() || metadata_json.is_null() {
2207 set_last_error("null argument");
2208 return TgmError::InvalidArg;
2209 }
2210
2211 let json_str = match unsafe { CStr::from_ptr(metadata_json) }.to_str() {
2212 Ok(s) => s,
2213 Err(e) => {
2214 set_last_error(&format!("invalid UTF-8 in metadata_json: {e}"));
2215 return TgmError::InvalidArg;
2216 }
2217 };
2218
2219 let parsed = match unsafe {
2220 parse_encode_args(
2221 json_str,
2222 data_ptrs,
2223 data_lens,
2224 num_objects,
2225 hash_algo,
2226 threads,
2227 )
2228 } {
2229 Ok(p) => p,
2230 Err((code, msg)) => {
2231 set_last_error(&msg);
2232 return code;
2233 }
2234 };
2235
2236 let pairs: Vec<(&DataObjectDescriptor, &[u8])> = parsed
2237 .descriptors
2238 .iter()
2239 .zip(parsed.data_slices.iter())
2240 .map(|(d, s)| (d, *s))
2241 .collect();
2242
2243 let f = unsafe { &mut (*file).file };
2244 match f.append(&parsed.global_metadata, &pairs, &parsed.options) {
2245 Ok(()) => TgmError::Ok,
2246 Err(e) => {
2247 set_last_error(&e.to_string());
2248 to_error_code(&e)
2249 }
2250 }
2251}
2252
2253#[unsafe(no_mangle)]
2255pub extern "C" fn tgm_file_close(file: *mut TgmFile) {
2256 if !file.is_null() {
2257 unsafe {
2258 drop(Box::from_raw(file));
2259 }
2260 }
2261}
2262
2263fn lookup_cbor_value<'a>(
2272 global_metadata: &'a GlobalMetadata,
2273 key: &str,
2274) -> Option<&'a ciborium::Value> {
2275 if key.is_empty() {
2276 return None;
2277 }
2278 let parts: Vec<&str> = key.split('.').collect();
2279
2280 if parts.is_empty() || parts[0].is_empty() {
2281 return None;
2282 }
2283 if parts[0] == "version" {
2284 return None; }
2286
2287 if parts[0] == "_extra_" || parts[0] == "extra" {
2289 if parts.len() > 1 {
2290 return resolve_in_btree(&global_metadata.extra, &parts[1..]);
2291 }
2292 return None;
2293 }
2294
2295 for entry in &global_metadata.base {
2297 if let Some(val) = resolve_in_btree_skip_reserved(entry, &parts) {
2298 return Some(val);
2299 }
2300 }
2301 resolve_in_btree(&global_metadata.extra, &parts)
2303}
2304
2305fn resolve_in_btree_skip_reserved<'a>(
2307 map: &'a BTreeMap<String, ciborium::Value>,
2308 parts: &[&str],
2309) -> Option<&'a ciborium::Value> {
2310 let (first, rest) = parts.split_first()?;
2311 if *first == RESERVED_KEY {
2312 return None;
2313 }
2314 let value = map.get(*first)?;
2315 resolve_cbor_path(value, rest)
2316}
2317
2318fn resolve_in_btree<'a>(
2320 map: &'a BTreeMap<String, ciborium::Value>,
2321 parts: &[&str],
2322) -> Option<&'a ciborium::Value> {
2323 let (first, rest) = parts.split_first()?;
2324 let value = map.get(*first)?;
2325 resolve_cbor_path(value, rest)
2326}
2327
2328fn resolve_cbor_path<'a>(
2333 value: &'a ciborium::Value,
2334 remaining: &[&str],
2335) -> Option<&'a ciborium::Value> {
2336 if remaining.is_empty() {
2337 return Some(value);
2338 }
2339 if let ciborium::Value::Map(entries) = value {
2340 for (k, v) in entries {
2341 if matches!(k, ciborium::Value::Text(s) if s == remaining[0]) {
2342 return resolve_cbor_path(v, &remaining[1..]);
2343 }
2344 }
2345 }
2346 None
2347}
2348
2349fn lookup_string_key(global_metadata: &GlobalMetadata, key: &str) -> Option<String> {
2350 if key.is_empty() {
2351 return None;
2352 }
2353 if key == "version" {
2358 return Some(tensogram::WIRE_VERSION.to_string());
2359 }
2360
2361 lookup_cbor_value(global_metadata, key).and_then(|v| match v {
2362 ciborium::Value::Text(s) => Some(s.clone()),
2363 ciborium::Value::Integer(i) => {
2364 let n: i128 = (*i).into();
2365 Some(n.to_string())
2366 }
2367 ciborium::Value::Float(f) => Some(f.to_string()),
2368 ciborium::Value::Bool(b) => Some(b.to_string()),
2369 _ => None,
2370 })
2371}
2372
2373fn lookup_int_key(global_metadata: &GlobalMetadata, key: &str) -> Option<i64> {
2374 if key == "version" {
2376 return Some(tensogram::WIRE_VERSION as i64);
2377 }
2378
2379 lookup_cbor_value(global_metadata, key).and_then(|v| match v {
2380 ciborium::Value::Integer(i) => {
2381 let n: i128 = (*i).into();
2382 i64::try_from(n).ok()
2383 }
2384 _ => None,
2385 })
2386}
2387
2388fn lookup_float_key(global_metadata: &GlobalMetadata, key: &str) -> Option<f64> {
2389 lookup_cbor_value(global_metadata, key).and_then(|v| match v {
2390 ciborium::Value::Float(f) => Some(*f),
2391 ciborium::Value::Integer(i) => {
2392 let n: i128 = (*i).into();
2393 Some(n as f64)
2396 }
2397 _ => None,
2398 })
2399}
2400
2401#[unsafe(no_mangle)]
2410pub extern "C" fn tgm_simple_packing_compute_params(
2411 values: *const f64,
2412 num_values: usize,
2413 bits_per_value: u32,
2414 decimal_scale_factor: i32,
2415 out_reference_value: *mut f64,
2416 out_binary_scale_factor: *mut i32,
2417) -> TgmError {
2418 if values.is_null() || out_reference_value.is_null() || out_binary_scale_factor.is_null() {
2419 set_last_error("null argument");
2420 return TgmError::InvalidArg;
2421 }
2422
2423 let vals = unsafe { slice::from_raw_parts(values, num_values) };
2424
2425 match tensogram_encodings::simple_packing::compute_params(
2426 vals,
2427 bits_per_value,
2428 decimal_scale_factor,
2429 ) {
2430 Ok(params) => {
2431 unsafe {
2432 *out_reference_value = params.reference_value;
2433 *out_binary_scale_factor = params.binary_scale_factor;
2434 }
2435 TgmError::Ok
2436 }
2437 Err(e) => {
2438 set_last_error(&e.to_string());
2439 TgmError::Encoding
2440 }
2441 }
2442}
2443
2444pub struct TgmBufferIter {
2452 offsets: Vec<(usize, usize)>,
2453 buf_ptr: *const u8,
2454 pos: usize,
2455}
2456
2457#[unsafe(no_mangle)]
2462pub extern "C" fn tgm_buffer_iter_create(
2463 buf: *const u8,
2464 buf_len: usize,
2465 out: *mut *mut TgmBufferIter,
2466) -> TgmError {
2467 if buf.is_null() || out.is_null() {
2468 set_last_error("null argument");
2469 return TgmError::InvalidArg;
2470 }
2471 let data = unsafe { slice::from_raw_parts(buf, buf_len) };
2472 let offsets = scan(data);
2473 let iter = Box::new(TgmBufferIter {
2474 offsets,
2475 buf_ptr: buf,
2476 pos: 0,
2477 });
2478 unsafe {
2479 *out = Box::into_raw(iter);
2480 }
2481 TgmError::Ok
2482}
2483
2484#[unsafe(no_mangle)]
2486pub extern "C" fn tgm_buffer_iter_count(iter: *const TgmBufferIter) -> usize {
2487 if iter.is_null() {
2488 return 0;
2489 }
2490 unsafe { (*iter).offsets.len() }
2491}
2492
2493#[unsafe(no_mangle)]
2499pub extern "C" fn tgm_buffer_iter_next(
2500 iter: *mut TgmBufferIter,
2501 out_buf: *mut *const u8,
2502 out_len: *mut usize,
2503) -> TgmError {
2504 if iter.is_null() || out_buf.is_null() || out_len.is_null() {
2505 set_last_error("null argument");
2506 return TgmError::InvalidArg;
2507 }
2508 let it = unsafe { &mut *iter };
2509 if it.pos >= it.offsets.len() {
2510 return TgmError::EndOfIter;
2511 }
2512 let (offset, length) = it.offsets[it.pos];
2513 it.pos += 1;
2514 unsafe {
2515 *out_buf = it.buf_ptr.add(offset);
2516 *out_len = length;
2517 }
2518 TgmError::Ok
2519}
2520
2521#[unsafe(no_mangle)]
2523pub extern "C" fn tgm_buffer_iter_free(iter: *mut TgmBufferIter) {
2524 if !iter.is_null() {
2525 unsafe {
2526 drop(Box::from_raw(iter));
2527 }
2528 }
2529}
2530
2531pub struct TgmFileIter {
2533 inner: tensogram::FileMessageIter,
2534}
2535
2536#[unsafe(no_mangle)]
2541pub extern "C" fn tgm_file_iter_create(file: *mut TgmFile, out: *mut *mut TgmFileIter) -> TgmError {
2542 if file.is_null() || out.is_null() {
2543 set_last_error("null argument");
2544 return TgmError::InvalidArg;
2545 }
2546 let f = unsafe { &(*file).file };
2547 match f.iter() {
2548 Ok(inner) => {
2549 let iter = Box::new(TgmFileIter { inner });
2550 unsafe {
2551 *out = Box::into_raw(iter);
2552 }
2553 TgmError::Ok
2554 }
2555 Err(e) => {
2556 set_last_error(&e.to_string());
2557 to_error_code(&e)
2558 }
2559 }
2560}
2561
2562#[unsafe(no_mangle)]
2569pub extern "C" fn tgm_file_iter_next(iter: *mut TgmFileIter, out: *mut TgmBytes) -> TgmError {
2570 if iter.is_null() || out.is_null() {
2571 set_last_error("null argument");
2572 return TgmError::InvalidArg;
2573 }
2574 let it = unsafe { &mut (*iter).inner };
2575 match it.next() {
2576 None => TgmError::EndOfIter,
2577 Some(Err(e)) => {
2578 set_last_error(&e.to_string());
2579 to_error_code(&e)
2580 }
2581 Some(Ok(bytes)) => {
2582 let mut bytes = bytes.into_boxed_slice().into_vec();
2584 let result = TgmBytes {
2585 data: bytes.as_mut_ptr(),
2586 len: bytes.len(),
2587 };
2588 std::mem::forget(bytes);
2589 unsafe {
2590 *out = result;
2591 }
2592 TgmError::Ok
2593 }
2594 }
2595}
2596
2597#[unsafe(no_mangle)]
2599pub extern "C" fn tgm_file_iter_free(iter: *mut TgmFileIter) {
2600 if !iter.is_null() {
2601 unsafe {
2602 drop(Box::from_raw(iter));
2603 }
2604 }
2605}
2606
2607pub struct TgmObjectIter {
2609 inner: tensogram::ObjectIter,
2610 global_metadata: GlobalMetadata,
2613}
2614
2615#[unsafe(no_mangle)]
2621pub extern "C" fn tgm_object_iter_create(
2622 buf: *const u8,
2623 buf_len: usize,
2624 native_byte_order: i32,
2625 verify_hash: i32,
2626 out: *mut *mut TgmObjectIter,
2627) -> TgmError {
2628 if buf.is_null() || out.is_null() {
2629 set_last_error("null argument");
2630 return TgmError::InvalidArg;
2631 }
2632 let data = unsafe { slice::from_raw_parts(buf, buf_len) };
2633 let options = DecodeOptions {
2634 native_byte_order: native_byte_order != 0,
2635 verify_hash: verify_hash != 0,
2636 ..Default::default()
2637 };
2638
2639 let global_metadata = decode_metadata(data).unwrap_or_default();
2642
2643 match tensogram::objects(data, options) {
2644 Ok(inner) => {
2645 let iter = Box::new(TgmObjectIter {
2646 inner,
2647 global_metadata,
2648 });
2649 unsafe {
2650 *out = Box::into_raw(iter);
2651 }
2652 TgmError::Ok
2653 }
2654 Err(e) => {
2655 set_last_error(&e.to_string());
2656 to_error_code(&e)
2657 }
2658 }
2659}
2660
2661#[unsafe(no_mangle)]
2668pub extern "C" fn tgm_object_iter_next(
2669 iter: *mut TgmObjectIter,
2670 out: *mut *mut TgmMessage,
2671) -> TgmError {
2672 if iter.is_null() || out.is_null() {
2673 set_last_error("null argument");
2674 return TgmError::InvalidArg;
2675 }
2676 let it = unsafe { &mut *iter };
2677 match it.inner.next() {
2678 None => TgmError::EndOfIter,
2679 Some(Err(e)) => {
2680 set_last_error(&e.to_string());
2681 to_error_code(&e)
2682 }
2683 Some(Ok((descriptor, data))) => {
2684 let global_metadata = it.global_metadata.clone();
2685 let objects = vec![(descriptor, data)];
2686 let caches = build_message_caches(&objects, &[]);
2695 let msg = Box::new(TgmMessage {
2696 global_metadata,
2697 objects,
2698 dtype_strings: caches.dtype_strings,
2699 type_strings: caches.type_strings,
2700 byte_order_strings: caches.byte_order_strings,
2701 filter_strings: caches.filter_strings,
2702 compression_strings: caches.compression_strings,
2703 encoding_strings: caches.encoding_strings,
2704 hash_type_strings: caches.hash_type_strings,
2705 hash_value_strings: caches.hash_value_strings,
2706 });
2707 unsafe {
2708 *out = Box::into_raw(msg);
2709 }
2710 TgmError::Ok
2711 }
2712 }
2713}
2714
2715#[unsafe(no_mangle)]
2717pub extern "C" fn tgm_object_iter_free(iter: *mut TgmObjectIter) {
2718 if !iter.is_null() {
2719 unsafe {
2720 drop(Box::from_raw(iter));
2721 }
2722 }
2723}
2724
2725#[unsafe(no_mangle)]
2735pub extern "C" fn tgm_error_string(err: TgmError) -> *const c_char {
2736 let code = err as i32;
2738 let s: &[u8] = match code {
2739 0 => b"ok\0",
2740 1 => b"framing error\0",
2741 2 => b"metadata error\0",
2742 3 => b"encoding error\0",
2743 4 => b"compression error\0",
2744 5 => b"object error\0",
2745 6 => b"I/O error\0",
2746 7 => b"hash mismatch\0",
2747 8 => b"invalid argument\0",
2748 9 => b"end of iteration\0",
2749 10 => b"remote error\0",
2750 11 => b"missing hash\0",
2751 12 => b"async task timed out\0",
2752 13 => b"async task cancelled\0",
2753 _ => b"unknown error\0",
2754 };
2755 s.as_ptr() as *const c_char
2756}
2757
2758#[cfg(test)]
2767mod tests {
2768 use super::*;
2769 use std::collections::BTreeMap;
2770
2771 fn make_meta(
2772 base: Vec<BTreeMap<String, ciborium::Value>>,
2773 extra: BTreeMap<String, ciborium::Value>,
2774 ) -> GlobalMetadata {
2775 GlobalMetadata {
2776 base,
2777 extra,
2778 ..Default::default()
2779 }
2780 }
2781
2782 #[test]
2785 fn lookup_cbor_empty_key() {
2786 let meta = make_meta(vec![], BTreeMap::new());
2787 assert!(lookup_cbor_value(&meta, "").is_none());
2788 }
2789
2790 #[test]
2791 fn lookup_cbor_dot_only() {
2792 let meta = make_meta(vec![], BTreeMap::new());
2793 assert!(lookup_cbor_value(&meta, ".").is_none());
2794 }
2795
2796 #[test]
2797 fn lookup_cbor_version_returns_none() {
2798 let meta = make_meta(vec![], BTreeMap::new());
2800 assert!(lookup_cbor_value(&meta, "version").is_none());
2801 }
2802
2803 #[test]
2804 fn lookup_cbor_base_match() {
2805 let mut entry = BTreeMap::new();
2806 entry.insert("centre".into(), ciborium::Value::Text("ecmwf".into()));
2807 let meta = make_meta(vec![entry], BTreeMap::new());
2808 let val = lookup_cbor_value(&meta, "centre");
2809 assert!(matches!(val, Some(ciborium::Value::Text(s)) if s == "ecmwf"));
2810 }
2811
2812 #[test]
2813 fn lookup_cbor_extra_fallback() {
2814 let mut extra = BTreeMap::new();
2816 extra.insert("source".into(), ciborium::Value::Text("test".into()));
2817 let meta = make_meta(vec![], extra);
2818 let val = lookup_cbor_value(&meta, "source");
2819 assert!(matches!(val, Some(ciborium::Value::Text(s)) if s == "test"));
2820 }
2821
2822 #[test]
2823 fn lookup_cbor_no_match() {
2824 let meta = make_meta(vec![], BTreeMap::new());
2825 assert!(lookup_cbor_value(&meta, "nonexistent").is_none());
2826 }
2827
2828 #[test]
2829 fn lookup_cbor_reserved_skipped() {
2830 let mut entry = BTreeMap::new();
2831 entry.insert(
2832 "_reserved_".into(),
2833 ciborium::Value::Map(vec![(
2834 ciborium::Value::Text("tensor".into()),
2835 ciborium::Value::Text("internal".into()),
2836 )]),
2837 );
2838 entry.insert("param".into(), ciborium::Value::Text("2t".into()));
2839 let meta = make_meta(vec![entry], BTreeMap::new());
2840 assert!(lookup_cbor_value(&meta, "_reserved_.tensor").is_none());
2842 assert!(lookup_cbor_value(&meta, "param").is_some());
2844 }
2845
2846 #[test]
2847 fn lookup_cbor_extra_prefix() {
2848 let mut extra = BTreeMap::new();
2849 extra.insert("custom".into(), ciborium::Value::Text("val".into()));
2850 let meta = make_meta(vec![], extra);
2851 let val = lookup_cbor_value(&meta, "_extra_.custom");
2853 assert!(matches!(val, Some(ciborium::Value::Text(s)) if s == "val"));
2854 }
2855
2856 #[test]
2857 fn lookup_cbor_extra_alias_prefix() {
2858 let mut extra = BTreeMap::new();
2859 extra.insert("custom".into(), ciborium::Value::Text("val".into()));
2860 let meta = make_meta(vec![], extra);
2861 let val = lookup_cbor_value(&meta, "extra.custom");
2863 assert!(matches!(val, Some(ciborium::Value::Text(s)) if s == "val"));
2864 }
2865
2866 #[test]
2867 fn lookup_cbor_extra_prefix_alone_returns_none() {
2868 let meta = make_meta(vec![], BTreeMap::new());
2869 assert!(lookup_cbor_value(&meta, "_extra_").is_none());
2871 assert!(lookup_cbor_value(&meta, "extra").is_none());
2872 }
2873
2874 #[test]
2875 fn lookup_cbor_base_wins_over_extra() {
2876 let mut entry = BTreeMap::new();
2877 entry.insert("shared".into(), ciborium::Value::Text("from_base".into()));
2878 let mut extra = BTreeMap::new();
2879 extra.insert("shared".into(), ciborium::Value::Text("from_extra".into()));
2880 let meta = make_meta(vec![entry], extra);
2881 let val = lookup_cbor_value(&meta, "shared");
2882 assert!(matches!(val, Some(ciborium::Value::Text(s)) if s == "from_base"));
2883 }
2884
2885 #[test]
2886 fn lookup_cbor_deeply_nested() {
2887 let e_val = ciborium::Value::Map(vec![(
2888 ciborium::Value::Text("e".into()),
2889 ciborium::Value::Text("deep".into()),
2890 )]);
2891 let d_val = ciborium::Value::Map(vec![(ciborium::Value::Text("d".into()), e_val)]);
2892 let c_val = ciborium::Value::Map(vec![(ciborium::Value::Text("c".into()), d_val)]);
2893 let b_val = ciborium::Value::Map(vec![(ciborium::Value::Text("b".into()), c_val)]);
2894 let mut entry = BTreeMap::new();
2895 entry.insert("a".into(), b_val);
2896 let meta = make_meta(vec![entry], BTreeMap::new());
2897 let val = lookup_cbor_value(&meta, "a.b.c.d.e");
2898 assert!(matches!(val, Some(ciborium::Value::Text(s)) if s == "deep"));
2899 }
2900
2901 #[test]
2902 fn lookup_cbor_multi_base_first_match() {
2903 let mut entry0 = BTreeMap::new();
2904 entry0.insert("param".into(), ciborium::Value::Text("2t".into()));
2905 let mut entry1 = BTreeMap::new();
2906 entry1.insert("param".into(), ciborium::Value::Text("msl".into()));
2907 let meta = make_meta(vec![entry0, entry1], BTreeMap::new());
2908 let val = lookup_cbor_value(&meta, "param");
2909 assert!(matches!(val, Some(ciborium::Value::Text(s)) if s == "2t"));
2910 }
2911
2912 #[test]
2915 fn resolve_cbor_path_empty_remaining() {
2916 let value = ciborium::Value::Text("hello".into());
2917 assert_eq!(resolve_cbor_path(&value, &[]), Some(&value));
2918 }
2919
2920 #[test]
2921 fn resolve_cbor_path_non_map_with_remaining() {
2922 let value = ciborium::Value::Text("hello".into());
2923 assert!(resolve_cbor_path(&value, &["key"]).is_none());
2924 }
2925
2926 #[test]
2927 fn resolve_cbor_path_map_missing_key() {
2928 let value = ciborium::Value::Map(vec![(
2929 ciborium::Value::Text("a".into()),
2930 ciborium::Value::Text("b".into()),
2931 )]);
2932 assert!(resolve_cbor_path(&value, &["missing"]).is_none());
2933 }
2934
2935 #[test]
2938 fn lookup_string_key_version() {
2939 let meta = make_meta(vec![], BTreeMap::new());
2940 assert_eq!(lookup_string_key(&meta, "version"), Some("3".into()));
2941 }
2942
2943 #[test]
2944 fn lookup_string_key_empty() {
2945 let meta = make_meta(vec![], BTreeMap::new());
2946 assert!(lookup_string_key(&meta, "").is_none());
2947 }
2948
2949 #[test]
2950 fn lookup_string_key_integer_value() {
2951 let mut entry = BTreeMap::new();
2952 entry.insert("count".into(), ciborium::Value::Integer(42.into()));
2953 let meta = make_meta(vec![entry], BTreeMap::new());
2954 assert_eq!(lookup_string_key(&meta, "count"), Some("42".into()));
2955 }
2956
2957 #[test]
2958 fn lookup_string_key_float_value() {
2959 let mut extra = BTreeMap::new();
2960 extra.insert("temperature".into(), ciborium::Value::Float(98.6));
2961 let meta = make_meta(vec![], extra);
2962 assert_eq!(lookup_string_key(&meta, "temperature"), Some("98.6".into()));
2963 }
2964
2965 #[test]
2966 fn lookup_string_key_bool_value() {
2967 let mut extra = BTreeMap::new();
2968 extra.insert("flag".into(), ciborium::Value::Bool(true));
2969 let meta = make_meta(vec![], extra);
2970 assert_eq!(lookup_string_key(&meta, "flag"), Some("true".into()));
2971 }
2972
2973 #[test]
2974 fn lookup_string_key_null_returns_none() {
2975 let mut extra = BTreeMap::new();
2976 extra.insert("nothing".into(), ciborium::Value::Null);
2977 let meta = make_meta(vec![], extra);
2978 assert!(lookup_string_key(&meta, "nothing").is_none());
2980 }
2981
2982 #[test]
2985 fn lookup_int_key_version() {
2986 let meta = make_meta(vec![], BTreeMap::new());
2987 assert_eq!(lookup_int_key(&meta, "version"), Some(3));
2988 }
2989
2990 #[test]
2991 fn lookup_int_key_non_integer() {
2992 let mut extra = BTreeMap::new();
2993 extra.insert("str".into(), ciborium::Value::Text("not_int".into()));
2994 let meta = make_meta(vec![], extra);
2995 assert!(lookup_int_key(&meta, "str").is_none());
2996 }
2997
2998 #[test]
3001 fn lookup_float_key_float() {
3002 let mut extra = BTreeMap::new();
3003 extra.insert("val".into(), ciborium::Value::Float(98.6));
3004 let meta = make_meta(vec![], extra);
3005 assert_eq!(lookup_float_key(&meta, "val"), Some(98.6));
3006 }
3007
3008 #[test]
3009 fn lookup_float_key_integer_coercion() {
3010 let mut extra = BTreeMap::new();
3011 extra.insert("count".into(), ciborium::Value::Integer(42.into()));
3012 let meta = make_meta(vec![], extra);
3013 assert_eq!(lookup_float_key(&meta, "count"), Some(42.0));
3014 }
3015
3016 #[test]
3017 fn lookup_float_key_non_numeric() {
3018 let mut extra = BTreeMap::new();
3019 extra.insert("str".into(), ciborium::Value::Text("hello".into()));
3020 let meta = make_meta(vec![], extra);
3021 assert!(lookup_float_key(&meta, "str").is_none());
3022 }
3023
3024 #[test]
3027 fn parse_encode_json_with_base() {
3028 let json = r#"{"version":3,"base":[{"mars":{"param":"2t"}}],"descriptors":[]}"#;
3029 let (gm, descs) = parse_encode_json(json).unwrap();
3030 assert_eq!(gm.base.len(), 1);
3031 assert!(gm.base[0].contains_key("mars"));
3032 assert!(descs.is_empty());
3033 }
3034
3035 #[test]
3036 fn parse_encode_json_legacy_version_routed_to_extra() {
3037 let json = r#"{"version":3,"descriptors":[]}"#;
3042 let (gm, _) = parse_encode_json(json).unwrap();
3043 assert_eq!(
3044 gm.extra.get("version"),
3045 Some(&ciborium::Value::Integer(3u64.into())),
3046 "legacy JSON `version` must round-trip via `_extra_`"
3047 );
3048 }
3049
3050 #[test]
3051 fn parse_encode_json_free_form_top_level_routed_to_extra() {
3052 let json = r#"{"source":"test","count":42,"descriptors":[]}"#;
3055 let (gm, _) = parse_encode_json(json).unwrap();
3056 assert_eq!(
3057 gm.extra.get("source"),
3058 Some(&ciborium::Value::Text("test".to_string()))
3059 );
3060 assert_eq!(
3061 gm.extra.get("count"),
3062 Some(&ciborium::Value::Integer(42u64.into()))
3063 );
3064 }
3065
3066 #[test]
3067 fn parse_encode_json_explicit_extra_unpacked() {
3068 let json = r#"{"_extra_":{"foo":"bar","count":7},"descriptors":[]}"#;
3075 let (gm, _) = parse_encode_json(json).unwrap();
3076 assert_eq!(
3077 gm.extra.get("foo"),
3078 Some(&ciborium::Value::Text("bar".to_string())),
3079 "explicit `_extra_.foo` must surface at the top level of `extra`"
3080 );
3081 assert_eq!(
3082 gm.extra.get("count"),
3083 Some(&ciborium::Value::Integer(7u64.into())),
3084 "explicit `_extra_.count` must surface at the top level of `extra`"
3085 );
3086 assert!(
3087 !gm.extra.contains_key("_extra_"),
3088 "there must be no nested `_extra_` key inside `extra`"
3089 );
3090 }
3091
3092 #[test]
3093 fn parse_encode_json_explicit_extra_beats_free_form() {
3094 let json = r#"{"version":99,"_extra_":{"version":1},"descriptors":[]}"#;
3098 let (gm, _) = parse_encode_json(json).unwrap();
3099 assert_eq!(
3100 gm.extra.get("version"),
3101 Some(&ciborium::Value::Integer(1u64.into())),
3102 "explicit _extra_.version must win over top-level version"
3103 );
3104 }
3105
3106 #[test]
3107 fn parse_encode_json_without_base() {
3108 let json = r#"{"version":3,"descriptors":[]}"#;
3109 let (gm, _) = parse_encode_json(json).unwrap();
3110 assert!(gm.base.is_empty());
3111 }
3112
3113 #[test]
3114 fn parse_encode_json_reserved_in_base_rejected() {
3115 let json = r#"{"version":3,"base":[{"_reserved_":{"tensor":{}}}],"descriptors":[]}"#;
3116 let result = parse_encode_json(json);
3117 assert!(result.is_err());
3118 assert!(result.unwrap_err().contains("_reserved_"));
3119 }
3120
3121 #[test]
3122 fn parse_encode_json_extra_keys() {
3123 let json = r#"{"version":3,"descriptors":[],"source":"test","count":42}"#;
3124 let (gm, _) = parse_encode_json(json).unwrap();
3125 assert!(gm.extra.contains_key("source"));
3126 assert!(gm.extra.contains_key("count"));
3127 }
3128
3129 #[test]
3132 fn parse_streaming_json_with_base() {
3133 let json = r#"{"version":3,"base":[{"mars":{"param":"2t"}}]}"#;
3134 let gm = parse_streaming_metadata_json(json).unwrap();
3135 assert_eq!(gm.base.len(), 1);
3136 }
3137
3138 #[test]
3139 fn parse_streaming_json_reserved_rejected() {
3140 let json = r#"{"version":3,"base":[{"_reserved_":{"tensor":{}}}]}"#;
3141 let result = parse_streaming_metadata_json(json);
3142 assert!(result.is_err());
3143 assert!(result.unwrap_err().contains("_reserved_"));
3144 }
3145
3146 #[test]
3147 fn parse_streaming_json_no_base() {
3148 let json = r#"{"version":3,"source":"stream"}"#;
3149 let gm = parse_streaming_metadata_json(json).unwrap();
3150 assert!(gm.base.is_empty());
3151 assert!(gm.extra.contains_key("source"));
3152 }
3153
3154 #[test]
3155 fn parse_streaming_json_explicit_extra_unpacked() {
3156 let json = r#"{"_extra_":{"foo":"bar"}}"#;
3159 let gm = parse_streaming_metadata_json(json).unwrap();
3160 assert_eq!(
3161 gm.extra.get("foo"),
3162 Some(&ciborium::Value::Text("bar".to_string()))
3163 );
3164 assert!(!gm.extra.contains_key("_extra_"));
3165 }
3166
3167 #[test]
3168 fn parse_streaming_json_explicit_extra_beats_free_form() {
3169 let json = r#"{"version":99,"_extra_":{"version":1}}"#;
3171 let gm = parse_streaming_metadata_json(json).unwrap();
3172 assert_eq!(
3173 gm.extra.get("version"),
3174 Some(&ciborium::Value::Integer(1u64.into()))
3175 );
3176 }
3177
3178 #[test]
3179 fn parse_streaming_json_invalid_json() {
3180 assert!(parse_streaming_metadata_json("not json").is_err());
3181 }
3182
3183 #[test]
3184 fn parse_encode_json_rejects_both_extra_aliases() {
3185 let json = r#"{"_extra_":{"a":1},"extra":{"b":2},"descriptors":[]}"#;
3188 let err = parse_encode_json(json).unwrap_err();
3189 assert!(
3190 err.contains("both '_extra_' and 'extra'"),
3191 "unexpected error: {err}"
3192 );
3193 }
3194
3195 #[test]
3196 fn parse_encode_json_rejects_non_object_extra() {
3197 let json = r#"{"_extra_":42,"descriptors":[]}"#;
3200 let err = parse_encode_json(json).unwrap_err();
3201 assert!(
3202 err.contains("'_extra_' must be a JSON object"),
3203 "unexpected error: {err}"
3204 );
3205 }
3206
3207 #[test]
3208 fn parse_encode_json_invalid_json() {
3209 assert!(parse_encode_json("not json").is_err());
3210 }
3211
3212 #[test]
3215 fn json_to_cbor_null() {
3216 assert_eq!(json_to_cbor(serde_json::Value::Null), ciborium::Value::Null);
3217 }
3218
3219 #[test]
3220 fn json_to_cbor_bool() {
3221 assert_eq!(
3222 json_to_cbor(serde_json::Value::Bool(true)),
3223 ciborium::Value::Bool(true)
3224 );
3225 }
3226
3227 #[test]
3228 fn json_to_cbor_integer() {
3229 let val = serde_json::json!(42);
3230 let cbor = json_to_cbor(val);
3231 assert!(matches!(cbor, ciborium::Value::Integer(_)));
3232 }
3233
3234 #[test]
3235 fn json_to_cbor_float() {
3236 let val = serde_json::json!(98.6);
3237 let cbor = json_to_cbor(val);
3238 assert!(matches!(cbor, ciborium::Value::Float(_)));
3239 }
3240
3241 #[test]
3242 fn json_to_cbor_string() {
3243 let val = serde_json::json!("hello");
3244 let cbor = json_to_cbor(val);
3245 assert!(matches!(cbor, ciborium::Value::Text(s) if s == "hello"));
3246 }
3247
3248 #[test]
3249 fn json_to_cbor_array() {
3250 let val = serde_json::json!([1, 2, 3]);
3251 let cbor = json_to_cbor(val);
3252 assert!(matches!(cbor, ciborium::Value::Array(_)));
3253 }
3254
3255 #[test]
3256 fn json_to_cbor_object() {
3257 let val = serde_json::json!({"key": "value"});
3258 let cbor = json_to_cbor(val);
3259 assert!(matches!(cbor, ciborium::Value::Map(_)));
3260 }
3261
3262 #[test]
3263 fn json_to_cbor_u64_fallback_to_float() {
3264 let val = serde_json::json!(18446744073709551615u64);
3267 let cbor = json_to_cbor(val);
3268 assert!(!matches!(cbor, ciborium::Value::Null));
3270 }
3271
3272 #[test]
3275 fn resolve_in_btree_skip_reserved_blocks_reserved() {
3276 let mut map = BTreeMap::new();
3277 map.insert("_reserved_".into(), ciborium::Value::Text("secret".into()));
3278 assert!(resolve_in_btree_skip_reserved(&map, &["_reserved_"]).is_none());
3279 }
3280
3281 #[test]
3282 fn resolve_in_btree_empty_parts() {
3283 let map = BTreeMap::new();
3284 assert!(resolve_in_btree(&map, &[]).is_none());
3285 }
3286
3287 #[test]
3288 fn resolve_in_btree_skip_reserved_empty_parts() {
3289 let map = BTreeMap::new();
3290 assert!(resolve_in_btree_skip_reserved(&map, &[]).is_none());
3291 }
3292
3293 #[test]
3296 fn parse_validate_options_default() {
3297 let opts = match super::parse_validate_options(ptr::null(), 0) {
3298 Ok(opts) => opts,
3299 Err((_code, msg)) => panic!("expected default options, got error: {msg}"),
3300 };
3301 assert_eq!(opts.max_level, ValidationLevel::Integrity);
3302 assert!(!opts.check_canonical);
3303 assert!(!opts.checksum_only);
3304 }
3305
3306 #[test]
3307 fn parse_validate_options_quick() {
3308 let level = CString::new("quick").unwrap();
3309 let opts = match super::parse_validate_options(level.as_ptr(), 0) {
3310 Ok(opts) => opts,
3311 Err((_code, msg)) => panic!("expected quick options, got error: {msg}"),
3312 };
3313 assert_eq!(opts.max_level, ValidationLevel::Structure);
3314 }
3315
3316 #[test]
3317 fn parse_validate_options_full_canonical() {
3318 let level = CString::new("full").unwrap();
3319 let opts = match super::parse_validate_options(level.as_ptr(), 1) {
3320 Ok(opts) => opts,
3321 Err((_code, msg)) => panic!("expected full options, got error: {msg}"),
3322 };
3323 assert_eq!(opts.max_level, ValidationLevel::Fidelity);
3324 assert!(opts.check_canonical);
3325 }
3326
3327 #[test]
3328 fn parse_validate_options_unknown_level() {
3329 let level = CString::new("bogus").unwrap();
3330 let result = super::parse_validate_options(level.as_ptr(), 0);
3331 assert!(result.is_err());
3332 }
3333
3334 #[test]
3335 fn parse_validate_options_checksum() {
3336 let level = CString::new("checksum").unwrap();
3337 let opts = match super::parse_validate_options(level.as_ptr(), 0) {
3338 Ok(opts) => opts,
3339 Err((_code, msg)) => panic!("expected checksum options, got error: {msg}"),
3340 };
3341 assert_eq!(opts.max_level, ValidationLevel::Integrity);
3342 assert!(opts.checksum_only);
3343 }
3344
3345 fn encode_test_message() -> Vec<u8> {
3348 let meta = GlobalMetadata::default();
3349 let desc = DataObjectDescriptor {
3350 obj_type: "ntensor".to_string(),
3351 ndim: 1,
3352 shape: vec![4],
3353 strides: vec![1],
3354 dtype: tensogram::Dtype::Float32,
3355 byte_order: tensogram::ByteOrder::native(),
3356 encoding: "none".to_string(),
3357 filter: "none".to_string(),
3358 compression: "none".to_string(),
3359 params: BTreeMap::new(),
3360 masks: None,
3361 };
3362 let data: Vec<u8> = [1.0f32, 2.0, 3.0, 4.0]
3363 .iter()
3364 .flat_map(|v| v.to_ne_bytes())
3365 .collect();
3366 tensogram::encode(&meta, &[(&desc, data.as_slice())], &Default::default()).unwrap()
3367 }
3368
3369 #[test]
3370 fn tgm_validate_valid_message() {
3371 let msg = encode_test_message();
3372 let mut out = super::TgmBytes {
3373 data: ptr::null_mut(),
3374 len: 0,
3375 };
3376 let err = super::tgm_validate(msg.as_ptr(), msg.len(), ptr::null(), 0, &mut out);
3377 assert!(matches!(err, super::TgmError::Ok));
3378 assert!(!out.data.is_null());
3379 assert!(out.len > 0);
3380 let json_str =
3381 unsafe { std::str::from_utf8(std::slice::from_raw_parts(out.data, out.len)).unwrap() };
3382 assert!(json_str.contains("\"issues\":[]"));
3383 assert!(json_str.contains("\"object_count\":1"));
3384 super::tgm_bytes_free(out);
3385 }
3386
3387 #[test]
3388 fn tgm_validate_empty_buffer() {
3389 let mut out = super::TgmBytes {
3390 data: ptr::null_mut(),
3391 len: 0,
3392 };
3393 let err = super::tgm_validate(ptr::null(), 0, ptr::null(), 0, &mut out);
3394 assert!(matches!(err, super::TgmError::Ok));
3395 let json_str =
3396 unsafe { std::str::from_utf8(std::slice::from_raw_parts(out.data, out.len)).unwrap() };
3397 assert!(json_str.contains("\"buffer_too_short\""));
3398 super::tgm_bytes_free(out);
3399 }
3400
3401 #[test]
3402 fn tgm_validate_invalid_level() {
3403 let msg = encode_test_message();
3404 let level = CString::new("bogus").unwrap();
3405 let mut out = super::TgmBytes {
3406 data: ptr::null_mut(),
3407 len: 0,
3408 };
3409 let err = super::tgm_validate(msg.as_ptr(), msg.len(), level.as_ptr(), 0, &mut out);
3410 assert!(matches!(err, super::TgmError::InvalidArg));
3411 }
3412
3413 #[test]
3414 fn tgm_validate_null_out() {
3415 let msg = encode_test_message();
3416 let err = super::tgm_validate(msg.as_ptr(), msg.len(), ptr::null(), 0, ptr::null_mut());
3417 assert!(matches!(err, super::TgmError::InvalidArg));
3418 }
3419
3420 #[test]
3421 fn tgm_validate_file_nonexistent() {
3422 let path = CString::new("/nonexistent/path/to/file.tgm").unwrap();
3423 let mut out = super::TgmBytes {
3424 data: ptr::null_mut(),
3425 len: 0,
3426 };
3427 let err = super::tgm_validate_file(path.as_ptr(), ptr::null(), 0, &mut out);
3428 assert!(matches!(err, super::TgmError::Io));
3429 }
3430
3431 #[test]
3432 fn tgm_validate_file_null_out() {
3433 let path = CString::new("/tmp/dummy.tgm").unwrap();
3434 let err = super::tgm_validate_file(path.as_ptr(), ptr::null(), 0, ptr::null_mut());
3435 assert!(matches!(err, super::TgmError::InvalidArg));
3436 }
3437
3438 #[test]
3439 fn tgm_validate_file_invalid_level() {
3440 let path = CString::new("/tmp/dummy.tgm").unwrap();
3441 let level = CString::new("bogus").unwrap();
3442 let mut out = super::TgmBytes {
3443 data: ptr::null_mut(),
3444 len: 0,
3445 };
3446 let err = super::tgm_validate_file(path.as_ptr(), level.as_ptr(), 0, &mut out);
3447 assert!(matches!(err, super::TgmError::InvalidArg));
3448 }
3449
3450 fn ffi_encode_single_f32_tensor(values: &[f32], extra_json: &str) -> Vec<u8> {
3457 let shape_str = format!("[{}]", values.len());
3458 let json = format!(
3459 r#"{{"version":3,"descriptors":[{{"type":"ntensor","ndim":1,"shape":{shape},"strides":[1],"dtype":"float32","byte_order":"{bo}","encoding":"none","filter":"none","compression":"none"}}]{extra}}}"#,
3460 shape = shape_str,
3461 bo = if cfg!(target_endian = "little") {
3462 "little"
3463 } else {
3464 "big"
3465 },
3466 extra = if extra_json.is_empty() {
3467 String::new()
3468 } else {
3469 format!(",{extra_json}")
3470 },
3471 );
3472
3473 let data: Vec<u8> = values.iter().flat_map(|v| v.to_ne_bytes()).collect();
3474 let c_json = CString::new(json).unwrap();
3475 let data_ptr: *const u8 = data.as_ptr();
3476 let data_len: usize = data.len();
3477
3478 let mut out = super::TgmBytes {
3479 data: ptr::null_mut(),
3480 len: 0,
3481 };
3482
3483 let err = super::tgm_encode(
3484 c_json.as_ptr(),
3485 &data_ptr as *const *const u8,
3486 &data_len as *const usize,
3487 1,
3488 ptr::null(), 0, &mut out,
3491 );
3492 assert!(matches!(err, super::TgmError::Ok), "tgm_encode failed");
3493 assert!(!out.data.is_null());
3494 assert!(out.len > 0);
3495
3496 let encoded = unsafe { slice::from_raw_parts(out.data, out.len) }.to_vec();
3497 super::tgm_bytes_free(out);
3498 encoded
3499 }
3500
3501 fn ffi_encode_with_hash(values: &[f32]) -> Vec<u8> {
3503 let shape_str = format!("[{}]", values.len());
3504 let json = format!(
3505 r#"{{"version":3,"descriptors":[{{"type":"ntensor","ndim":1,"shape":{shape},"strides":[1],"dtype":"float32","byte_order":"{bo}","encoding":"none","filter":"none","compression":"none"}}]}}"#,
3506 shape = shape_str,
3507 bo = if cfg!(target_endian = "little") {
3508 "little"
3509 } else {
3510 "big"
3511 },
3512 );
3513
3514 let data: Vec<u8> = values.iter().flat_map(|v| v.to_ne_bytes()).collect();
3515 let c_json = CString::new(json).unwrap();
3516 let hash_algo = CString::new("xxh3").unwrap();
3517 let data_ptr: *const u8 = data.as_ptr();
3518 let data_len: usize = data.len();
3519
3520 let mut out = super::TgmBytes {
3521 data: ptr::null_mut(),
3522 len: 0,
3523 };
3524
3525 let err = super::tgm_encode(
3526 c_json.as_ptr(),
3527 &data_ptr as *const *const u8,
3528 &data_len as *const usize,
3529 1,
3530 hash_algo.as_ptr(),
3531 0,
3532 &mut out,
3533 );
3534 assert!(
3535 matches!(err, super::TgmError::Ok),
3536 "tgm_encode with hash failed"
3537 );
3538
3539 let encoded = unsafe { slice::from_raw_parts(out.data, out.len) }.to_vec();
3540 super::tgm_bytes_free(out);
3541 encoded
3542 }
3543
3544 #[test]
3547 fn ffi_encode_decode_round_trip() {
3548 let values = [1.0f32, 2.0, 3.0, 4.0];
3549 let encoded = ffi_encode_single_f32_tensor(&values, "");
3550
3551 let mut msg: *mut super::TgmMessage = ptr::null_mut();
3553 let err = super::tgm_decode(
3554 encoded.as_ptr(),
3555 encoded.len(),
3556 0, 0, 0, &mut msg,
3560 );
3561 assert!(matches!(err, super::TgmError::Ok));
3562 assert!(!msg.is_null());
3563
3564 assert_eq!(super::tgm_message_version(msg), 3);
3566 assert_eq!(super::tgm_message_num_objects(msg), 1);
3567 assert_eq!(super::tgm_message_num_decoded(msg), 1);
3568
3569 assert_eq!(super::tgm_object_ndim(msg, 0), 1);
3571
3572 let shape_ptr = super::tgm_object_shape(msg, 0);
3573 assert!(!shape_ptr.is_null());
3574 assert_eq!(unsafe { *shape_ptr }, 4);
3575
3576 let strides_ptr = super::tgm_object_strides(msg, 0);
3577 assert!(!strides_ptr.is_null());
3578 assert_eq!(unsafe { *strides_ptr }, 1);
3579
3580 let dtype_ptr = super::tgm_object_dtype(msg, 0);
3582 assert!(!dtype_ptr.is_null());
3583 let dtype_str = unsafe { CStr::from_ptr(dtype_ptr) }.to_str().unwrap();
3584 assert_eq!(dtype_str, "float32");
3585
3586 let type_ptr = super::tgm_object_type(msg, 0);
3588 assert!(!type_ptr.is_null());
3589 let type_str = unsafe { CStr::from_ptr(type_ptr) }.to_str().unwrap();
3590 assert_eq!(type_str, "ntensor");
3591
3592 let bo_ptr = super::tgm_object_byte_order(msg, 0);
3594 assert!(!bo_ptr.is_null());
3595 let bo_str = unsafe { CStr::from_ptr(bo_ptr) }.to_str().unwrap();
3596 assert!(bo_str == "little" || bo_str == "big");
3597
3598 let filter_ptr = super::tgm_object_filter(msg, 0);
3600 assert!(!filter_ptr.is_null());
3601 let filter_str = unsafe { CStr::from_ptr(filter_ptr) }.to_str().unwrap();
3602 assert_eq!(filter_str, "none");
3603
3604 let comp_ptr = super::tgm_object_compression(msg, 0);
3606 assert!(!comp_ptr.is_null());
3607 let comp_str = unsafe { CStr::from_ptr(comp_ptr) }.to_str().unwrap();
3608 assert_eq!(comp_str, "none");
3609
3610 let enc_ptr = super::tgm_payload_encoding(msg, 0);
3612 assert!(!enc_ptr.is_null());
3613 let enc_str = unsafe { CStr::from_ptr(enc_ptr) }.to_str().unwrap();
3614 assert_eq!(enc_str, "none");
3615
3616 let mut data_len: usize = 0;
3618 let data_ptr = super::tgm_object_data(msg, 0, &mut data_len);
3619 assert!(!data_ptr.is_null());
3620 assert_eq!(data_len, 16); let decoded_bytes = unsafe { slice::from_raw_parts(data_ptr, data_len) };
3623 let decoded_values: Vec<f32> = decoded_bytes
3624 .chunks_exact(4)
3625 .map(|c| f32::from_ne_bytes(c.try_into().unwrap()))
3626 .collect();
3627 assert_eq!(decoded_values, values);
3628
3629 super::tgm_message_free(msg);
3630 }
3631
3632 #[test]
3633 fn ffi_encode_decode_with_hash() {
3634 let values = [10.0f32, 20.0, 30.0];
3635 let encoded = ffi_encode_with_hash(&values);
3636
3637 let mut msg: *mut super::TgmMessage = ptr::null_mut();
3638 let err = super::tgm_decode(
3639 encoded.as_ptr(),
3640 encoded.len(),
3641 0,
3642 0, 0, &mut msg,
3645 );
3646 assert!(matches!(err, super::TgmError::Ok));
3647
3648 assert_eq!(super::tgm_payload_has_hash(msg, 0), 1);
3650
3651 let ht_ptr = super::tgm_object_hash_type(msg, 0);
3652 assert!(!ht_ptr.is_null());
3653 let ht_str = unsafe { CStr::from_ptr(ht_ptr) }.to_str().unwrap();
3654 assert_eq!(ht_str, "xxh3");
3655
3656 let hv_ptr = super::tgm_object_hash_value(msg, 0);
3657 assert!(!hv_ptr.is_null());
3658 let hv_str = unsafe { CStr::from_ptr(hv_ptr) }.to_str().unwrap();
3659 assert!(!hv_str.is_empty());
3660
3661 super::tgm_message_free(msg);
3662 }
3663
3664 #[test]
3665 fn ffi_encode_decode_no_hash() {
3666 let values = [5.0f32];
3667 let encoded = ffi_encode_single_f32_tensor(&values, "");
3668
3669 let mut msg: *mut super::TgmMessage = ptr::null_mut();
3670 let err = super::tgm_decode(encoded.as_ptr(), encoded.len(), 0, 0, 0, &mut msg);
3671 assert!(matches!(err, super::TgmError::Ok));
3672
3673 assert_eq!(super::tgm_payload_has_hash(msg, 0), 0);
3674 assert!(super::tgm_object_hash_type(msg, 0).is_null());
3675 assert!(super::tgm_object_hash_value(msg, 0).is_null());
3676
3677 super::tgm_message_free(msg);
3678 }
3679
3680 #[test]
3683 fn ffi_decode_verify_hash_succeeds_on_hashed_message() {
3684 let encoded = ffi_encode_with_hash(&[1.0f32, 2.0]);
3686 let mut msg: *mut super::TgmMessage = ptr::null_mut();
3687 let err = super::tgm_decode(
3688 encoded.as_ptr(),
3689 encoded.len(),
3690 0,
3691 0,
3692 1, &mut msg,
3694 );
3695 assert!(matches!(err, super::TgmError::Ok));
3696 assert!(!msg.is_null());
3697 super::tgm_message_free(msg);
3698 }
3699
3700 #[test]
3701 fn ffi_decode_verify_hash_returns_missing_hash_on_unhashed_message() {
3702 let encoded = ffi_encode_single_f32_tensor(&[5.0f32], "");
3704 let mut msg: *mut super::TgmMessage = ptr::null_mut();
3705 let err = super::tgm_decode(
3706 encoded.as_ptr(),
3707 encoded.len(),
3708 0,
3709 0,
3710 1, &mut msg,
3712 );
3713 assert!(
3714 matches!(err, super::TgmError::MissingHash),
3715 "expected MissingHash, got error code {}",
3716 err as i32
3717 );
3718 let last = unsafe { CStr::from_ptr(super::tgm_last_error()) }
3719 .to_str()
3720 .unwrap();
3721 assert!(
3722 last.contains("object 0"),
3723 "last error should name the offending object: {last}"
3724 );
3725 }
3727
3728 #[test]
3729 fn ffi_decode_verify_hash_returns_hash_mismatch_on_tampered_slot() {
3730 let mut encoded = ffi_encode_with_hash(&[10.0f32, 20.0, 30.0]);
3738 let frame_start = {
3743 let mut pos = 24usize;
3744 loop {
3745 assert!(pos + 16 <= encoded.len(), "frame not found");
3746 if &encoded[pos..pos + 2] == b"FR"
3747 && tensogram::wire::FrameHeader::read_from(&encoded[pos..])
3748 .map(|fh| fh.frame_type.is_data_object())
3749 .unwrap_or(false)
3750 {
3751 break pos;
3752 }
3753 pos += 1;
3754 }
3755 };
3756 let fh = tensogram::wire::FrameHeader::read_from(&encoded[frame_start..]).unwrap();
3757 let frame_end = frame_start + fh.total_length as usize;
3758 let slot_byte = frame_end - 12; encoded[slot_byte] ^= 0xFF;
3760
3761 let mut msg: *mut super::TgmMessage = ptr::null_mut();
3762 let err = super::tgm_decode(
3763 encoded.as_ptr(),
3764 encoded.len(),
3765 0,
3766 0,
3767 1, &mut msg,
3769 );
3770 assert!(
3771 matches!(err, super::TgmError::HashMismatch),
3772 "expected HashMismatch, got error code {}",
3773 err as i32
3774 );
3775 let last = unsafe { CStr::from_ptr(super::tgm_last_error()) }
3776 .to_str()
3777 .unwrap();
3778 assert!(
3779 last.contains("object 0"),
3780 "last error should name the offending object: {last}"
3781 );
3782 }
3783
3784 #[test]
3785 fn ffi_decode_verify_hash_off_silently_decodes_unhashed_message() {
3786 let encoded = ffi_encode_single_f32_tensor(&[5.0f32], "");
3791 let mut msg: *mut super::TgmMessage = ptr::null_mut();
3792 let err = super::tgm_decode(
3793 encoded.as_ptr(),
3794 encoded.len(),
3795 0,
3796 0,
3797 0, &mut msg,
3799 );
3800 assert!(matches!(err, super::TgmError::Ok));
3801 super::tgm_message_free(msg);
3802 }
3803
3804 #[test]
3805 fn ffi_decode_object_verify_hash_returns_missing_hash_on_unhashed() {
3806 let encoded = ffi_encode_single_f32_tensor(&[5.0f32], "");
3808 let mut msg: *mut super::TgmMessage = ptr::null_mut();
3809 let err = super::tgm_decode_object(
3810 encoded.as_ptr(),
3811 encoded.len(),
3812 0,
3813 0,
3814 0,
3815 1, &mut msg,
3817 );
3818 assert!(
3819 matches!(err, super::TgmError::MissingHash),
3820 "expected MissingHash, got error code {}",
3821 err as i32
3822 );
3823 }
3824
3825 #[test]
3826 fn ffi_encode_with_extra_metadata() {
3827 let values = [1.0f32, 2.0];
3828 let encoded = ffi_encode_single_f32_tensor(&values, r#""source":"test_source","count":42"#);
3829
3830 let mut msg: *mut super::TgmMessage = ptr::null_mut();
3831 let err = super::tgm_decode(encoded.as_ptr(), encoded.len(), 0, 0, 0, &mut msg);
3832 assert!(matches!(err, super::TgmError::Ok));
3833
3834 let mut meta: *mut super::TgmMetadata = ptr::null_mut();
3836 let err = super::tgm_message_metadata(msg, &mut meta);
3837 assert!(matches!(err, super::TgmError::Ok));
3838
3839 let key = CString::new("source").unwrap();
3840 let val_ptr = super::tgm_metadata_get_string(meta, key.as_ptr());
3841 assert!(!val_ptr.is_null());
3842 let val_str = unsafe { CStr::from_ptr(val_ptr) }.to_str().unwrap();
3843 assert_eq!(val_str, "test_source");
3844
3845 let key_count = CString::new("count").unwrap();
3846 let val_int = super::tgm_metadata_get_int(meta, key_count.as_ptr(), -1);
3847 assert_eq!(val_int, 42);
3848
3849 super::tgm_metadata_free(meta);
3850 super::tgm_message_free(msg);
3851 }
3852
3853 #[test]
3856 fn ffi_encode_null_json() {
3857 let mut out = super::TgmBytes {
3858 data: ptr::null_mut(),
3859 len: 0,
3860 };
3861 let err = super::tgm_encode(
3862 ptr::null(),
3863 ptr::null(),
3864 ptr::null(),
3865 0,
3866 ptr::null(),
3867 0,
3868 &mut out,
3869 );
3870 assert!(matches!(err, super::TgmError::InvalidArg));
3871 }
3872
3873 #[test]
3874 fn ffi_encode_null_out() {
3875 let json = CString::new(r#"{"version":3,"descriptors":[]}"#).unwrap();
3876 let err = super::tgm_encode(
3877 json.as_ptr(),
3878 ptr::null(),
3879 ptr::null(),
3880 0,
3881 ptr::null(),
3882 0,
3883 ptr::null_mut(),
3884 );
3885 assert!(matches!(err, super::TgmError::InvalidArg));
3886 }
3887
3888 #[test]
3889 fn ffi_encode_descriptor_count_mismatch() {
3890 let json = CString::new(r#"{"version":3,"descriptors":[]}"#).unwrap();
3892 let data: [u8; 4] = [0; 4];
3893 let data_ptr: *const u8 = data.as_ptr();
3894 let data_len: usize = 4;
3895 let mut out = super::TgmBytes {
3896 data: ptr::null_mut(),
3897 len: 0,
3898 };
3899 let err = super::tgm_encode(
3900 json.as_ptr(),
3901 &data_ptr as *const *const u8,
3902 &data_len as *const usize,
3903 1, ptr::null(),
3905 0, &mut out,
3907 );
3908 assert!(matches!(err, super::TgmError::InvalidArg));
3909 }
3910
3911 #[test]
3912 fn ffi_encode_invalid_json() {
3913 let json = CString::new("not valid json").unwrap();
3914 let mut out = super::TgmBytes {
3915 data: ptr::null_mut(),
3916 len: 0,
3917 };
3918 let err = super::tgm_encode(
3919 json.as_ptr(),
3920 ptr::null(),
3921 ptr::null(),
3922 0,
3923 ptr::null(),
3924 0,
3925 &mut out,
3926 );
3927 assert!(matches!(err, super::TgmError::Metadata));
3928 }
3929
3930 #[test]
3933 fn ffi_decode_null_buf() {
3934 let mut msg: *mut super::TgmMessage = ptr::null_mut();
3935 let err = super::tgm_decode(ptr::null(), 0, 0, 0, 0, &mut msg);
3936 assert!(matches!(err, super::TgmError::InvalidArg));
3937 }
3938
3939 #[test]
3940 fn ffi_decode_null_out() {
3941 let data = [0u8; 10];
3942 let err = super::tgm_decode(data.as_ptr(), data.len(), 0, 0, 0, ptr::null_mut());
3943 assert!(matches!(err, super::TgmError::InvalidArg));
3944 }
3945
3946 #[test]
3947 fn ffi_decode_garbage_data() {
3948 let data = [0u8; 10];
3949 let mut msg: *mut super::TgmMessage = ptr::null_mut();
3950 let err = super::tgm_decode(data.as_ptr(), data.len(), 0, 0, 0, &mut msg);
3951 assert!(!matches!(err, super::TgmError::Ok));
3953 }
3954
3955 #[test]
3958 fn ffi_decode_metadata_round_trip() {
3959 let values = [1.0f32, 2.0];
3960 let encoded = ffi_encode_single_f32_tensor(&values, r#""source":"meta_test""#);
3961
3962 let mut meta: *mut super::TgmMetadata = ptr::null_mut();
3963 let err = super::tgm_decode_metadata(encoded.as_ptr(), encoded.len(), &mut meta);
3964 assert!(matches!(err, super::TgmError::Ok));
3965 assert!(!meta.is_null());
3966
3967 assert_eq!(super::tgm_metadata_version(meta), 3);
3969
3970 assert_eq!(super::tgm_metadata_num_objects(meta), 1);
3972
3973 let key = CString::new("source").unwrap();
3975 let val_ptr = super::tgm_metadata_get_string(meta, key.as_ptr());
3976 assert!(!val_ptr.is_null());
3977 let val_str = unsafe { CStr::from_ptr(val_ptr) }.to_str().unwrap();
3978 assert_eq!(val_str, "meta_test");
3979
3980 let bad_key = CString::new("nonexistent").unwrap();
3982 assert!(super::tgm_metadata_get_string(meta, bad_key.as_ptr()).is_null());
3983
3984 let bad_key2 = CString::new("missing_int").unwrap();
3986 assert_eq!(
3987 super::tgm_metadata_get_int(meta, bad_key2.as_ptr(), -999),
3988 -999
3989 );
3990
3991 let bad_key3 = CString::new("missing_float").unwrap();
3993 let fval = super::tgm_metadata_get_float(meta, bad_key3.as_ptr(), 3.25);
3994 assert!((fval - 3.25).abs() < f64::EPSILON);
3995
3996 super::tgm_metadata_free(meta);
3997 }
3998
3999 #[test]
4000 fn ffi_decode_metadata_null_args() {
4001 let mut meta: *mut super::TgmMetadata = ptr::null_mut();
4002 let err = super::tgm_decode_metadata(ptr::null(), 0, &mut meta);
4003 assert!(matches!(err, super::TgmError::InvalidArg));
4004
4005 let data = [0u8; 10];
4006 let err = super::tgm_decode_metadata(data.as_ptr(), data.len(), ptr::null_mut());
4007 assert!(matches!(err, super::TgmError::InvalidArg));
4008 }
4009
4010 #[test]
4013 fn ffi_metadata_accessors_null_handle() {
4014 assert_eq!(super::tgm_metadata_version(ptr::null()), 0);
4015 assert_eq!(super::tgm_metadata_num_objects(ptr::null()), 0);
4016 assert!(super::tgm_metadata_get_string(ptr::null(), ptr::null()).is_null());
4017 assert_eq!(
4018 super::tgm_metadata_get_int(ptr::null(), ptr::null(), -1),
4019 -1
4020 );
4021 assert_eq!(
4022 super::tgm_metadata_get_float(ptr::null(), ptr::null(), 1.5),
4023 1.5
4024 );
4025 }
4026
4027 #[test]
4028 fn ffi_metadata_get_string_null_key() {
4029 let encoded = ffi_encode_single_f32_tensor(&[1.0f32], "");
4030 let mut meta: *mut super::TgmMetadata = ptr::null_mut();
4031 let err = super::tgm_decode_metadata(encoded.as_ptr(), encoded.len(), &mut meta);
4032 assert!(matches!(err, super::TgmError::Ok));
4033
4034 assert!(super::tgm_metadata_get_string(meta, ptr::null()).is_null());
4035 assert_eq!(super::tgm_metadata_get_int(meta, ptr::null(), -1), -1);
4036 assert_eq!(super::tgm_metadata_get_float(meta, ptr::null(), 1.5), 1.5);
4037
4038 super::tgm_metadata_free(meta);
4039 }
4040
4041 #[test]
4042 fn ffi_metadata_get_version_via_string() {
4043 let encoded = ffi_encode_single_f32_tensor(&[1.0f32], "");
4044 let mut meta: *mut super::TgmMetadata = ptr::null_mut();
4045 let err = super::tgm_decode_metadata(encoded.as_ptr(), encoded.len(), &mut meta);
4046 assert!(matches!(err, super::TgmError::Ok));
4047
4048 let key = CString::new("version").unwrap();
4049 let val_ptr = super::tgm_metadata_get_string(meta, key.as_ptr());
4050 assert!(!val_ptr.is_null());
4051 let val_str = unsafe { CStr::from_ptr(val_ptr) }.to_str().unwrap();
4052 assert_eq!(val_str, "3");
4053
4054 let ival = super::tgm_metadata_get_int(meta, key.as_ptr(), -1);
4055 assert_eq!(ival, 3);
4056
4057 super::tgm_metadata_free(meta);
4058 }
4059
4060 #[test]
4061 fn ffi_metadata_get_float_value() {
4062 let values = [1.0f32];
4063 let encoded = ffi_encode_single_f32_tensor(&values, r#""temperature":98.6"#);
4064
4065 let mut meta: *mut super::TgmMetadata = ptr::null_mut();
4066 let err = super::tgm_decode_metadata(encoded.as_ptr(), encoded.len(), &mut meta);
4067 assert!(matches!(err, super::TgmError::Ok));
4068
4069 let key = CString::new("temperature").unwrap();
4070 let fval = super::tgm_metadata_get_float(meta, key.as_ptr(), 0.0);
4071 assert!((fval - 98.6).abs() < 0.01);
4072
4073 super::tgm_metadata_free(meta);
4074 }
4075
4076 #[test]
4079 fn ffi_decode_object_round_trip() {
4080 let values = [10.0f32, 20.0, 30.0, 40.0];
4081 let encoded = ffi_encode_single_f32_tensor(&values, "");
4082
4083 let mut msg: *mut super::TgmMessage = ptr::null_mut();
4084 let err = super::tgm_decode_object(
4085 encoded.as_ptr(),
4086 encoded.len(),
4087 0, 0, 0, 0, &mut msg,
4092 );
4093 assert!(matches!(err, super::TgmError::Ok));
4094 assert!(!msg.is_null());
4095
4096 assert_eq!(super::tgm_message_num_objects(msg), 1);
4098 assert_eq!(super::tgm_object_ndim(msg, 0), 1);
4099
4100 let mut data_len: usize = 0;
4101 let data_ptr = super::tgm_object_data(msg, 0, &mut data_len);
4102 assert!(!data_ptr.is_null());
4103 let decoded_bytes = unsafe { slice::from_raw_parts(data_ptr, data_len) };
4104 let decoded_values: Vec<f32> = decoded_bytes
4105 .chunks_exact(4)
4106 .map(|c| f32::from_ne_bytes(c.try_into().unwrap()))
4107 .collect();
4108 assert_eq!(decoded_values, values);
4109
4110 super::tgm_message_free(msg);
4111 }
4112
4113 #[test]
4114 fn ffi_decode_object_out_of_range() {
4115 let encoded = ffi_encode_single_f32_tensor(&[1.0f32], "");
4116 let mut msg: *mut super::TgmMessage = ptr::null_mut();
4117 let err = super::tgm_decode_object(
4118 encoded.as_ptr(),
4119 encoded.len(),
4120 999, 0,
4122 0, 0, &mut msg,
4125 );
4126 assert!(!matches!(err, super::TgmError::Ok));
4127 }
4128
4129 #[test]
4130 fn ffi_decode_object_null_args() {
4131 let mut msg: *mut super::TgmMessage = ptr::null_mut();
4132 let err = super::tgm_decode_object(ptr::null(), 0, 0, 0, 0, 0, &mut msg);
4133 assert!(matches!(err, super::TgmError::InvalidArg));
4134
4135 let data = [0u8; 10];
4136 let err = super::tgm_decode_object(data.as_ptr(), data.len(), 0, 0, 0, 0, ptr::null_mut());
4137 assert!(matches!(err, super::TgmError::InvalidArg));
4138 }
4139
4140 #[test]
4143 fn ffi_message_metadata_null_args() {
4144 let err = super::tgm_message_metadata(ptr::null(), ptr::null_mut());
4145 assert!(matches!(err, super::TgmError::InvalidArg));
4146
4147 let encoded = ffi_encode_single_f32_tensor(&[1.0f32], "");
4148 let mut msg: *mut super::TgmMessage = ptr::null_mut();
4149 let err = super::tgm_decode(encoded.as_ptr(), encoded.len(), 0, 0, 0, &mut msg);
4150 assert!(matches!(err, super::TgmError::Ok));
4151
4152 let err = super::tgm_message_metadata(msg, ptr::null_mut());
4153 assert!(matches!(err, super::TgmError::InvalidArg));
4154
4155 super::tgm_message_free(msg);
4156 }
4157
4158 #[test]
4161 fn ffi_message_accessors_null_handle() {
4162 assert_eq!(super::tgm_message_version(ptr::null()), 0);
4163 assert_eq!(super::tgm_message_num_objects(ptr::null()), 0);
4164 assert_eq!(super::tgm_message_num_decoded(ptr::null()), 0);
4165 assert_eq!(super::tgm_object_ndim(ptr::null(), 0), 0);
4166 assert!(super::tgm_object_shape(ptr::null(), 0).is_null());
4167 assert!(super::tgm_object_strides(ptr::null(), 0).is_null());
4168 assert!(super::tgm_object_dtype(ptr::null(), 0).is_null());
4169 assert!(super::tgm_object_type(ptr::null(), 0).is_null());
4170 assert!(super::tgm_object_byte_order(ptr::null(), 0).is_null());
4171 assert!(super::tgm_object_filter(ptr::null(), 0).is_null());
4172 assert!(super::tgm_object_compression(ptr::null(), 0).is_null());
4173 assert!(super::tgm_payload_encoding(ptr::null(), 0).is_null());
4174 assert_eq!(super::tgm_payload_has_hash(ptr::null(), 0), 0);
4175 assert!(super::tgm_object_hash_type(ptr::null(), 0).is_null());
4176 assert!(super::tgm_object_hash_value(ptr::null(), 0).is_null());
4177
4178 let mut data_len: usize = 99;
4179 let data_ptr = super::tgm_object_data(ptr::null(), 0, &mut data_len);
4180 assert!(data_ptr.is_null());
4181 assert_eq!(data_len, 0);
4182 }
4183
4184 #[test]
4187 fn ffi_message_accessors_out_of_bounds() {
4188 let encoded = ffi_encode_single_f32_tensor(&[1.0f32], "");
4189 let mut msg: *mut super::TgmMessage = ptr::null_mut();
4190 let err = super::tgm_decode(encoded.as_ptr(), encoded.len(), 0, 0, 0, &mut msg);
4191 assert!(matches!(err, super::TgmError::Ok));
4192
4193 assert_eq!(super::tgm_object_ndim(msg, 1), 0);
4195 assert!(super::tgm_object_shape(msg, 1).is_null());
4196 assert!(super::tgm_object_strides(msg, 1).is_null());
4197 assert!(super::tgm_object_dtype(msg, 1).is_null());
4198 assert!(super::tgm_object_type(msg, 1).is_null());
4199 assert!(super::tgm_object_byte_order(msg, 1).is_null());
4200 assert!(super::tgm_object_filter(msg, 1).is_null());
4201 assert!(super::tgm_object_compression(msg, 1).is_null());
4202 assert!(super::tgm_payload_encoding(msg, 1).is_null());
4203 assert_eq!(super::tgm_payload_has_hash(msg, 1), 0);
4204 assert!(super::tgm_object_hash_type(msg, 1).is_null());
4205 assert!(super::tgm_object_hash_value(msg, 1).is_null());
4206
4207 let mut data_len: usize = 99;
4208 let data_ptr = super::tgm_object_data(msg, 1, &mut data_len);
4209 assert!(data_ptr.is_null());
4210 assert_eq!(data_len, 0);
4211
4212 super::tgm_message_free(msg);
4213 }
4214
4215 #[test]
4218 fn ffi_object_data_null_out_len() {
4219 let encoded = ffi_encode_single_f32_tensor(&[1.0f32], "");
4220 let mut msg: *mut super::TgmMessage = ptr::null_mut();
4221 let err = super::tgm_decode(encoded.as_ptr(), encoded.len(), 0, 0, 0, &mut msg);
4222 assert!(matches!(err, super::TgmError::Ok));
4223
4224 let data_ptr = super::tgm_object_data(msg, 0, ptr::null_mut());
4226 assert!(!data_ptr.is_null());
4227
4228 super::tgm_message_free(msg);
4229 }
4230
4231 #[test]
4234 fn ffi_decode_range_round_trip() {
4235 let values = [1.0f32, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
4236 let encoded = ffi_encode_single_f32_tensor(&values, "");
4237
4238 let range_offset: u64 = 2;
4240 let range_count: u64 = 3;
4241 let mut out_buf = super::TgmBytes {
4242 data: ptr::null_mut(),
4243 len: 0,
4244 };
4245 let mut out_count: usize = 0;
4246
4247 let err = super::tgm_decode_range(
4248 encoded.as_ptr(),
4249 encoded.len(),
4250 0,
4251 &range_offset as *const u64,
4252 &range_count as *const u64,
4253 1,
4254 0, 0, 1, &mut out_buf,
4258 &mut out_count,
4259 );
4260 assert!(matches!(err, super::TgmError::Ok));
4261 assert_eq!(out_count, 1);
4262 assert!(!out_buf.data.is_null());
4263
4264 let decoded_bytes = unsafe { slice::from_raw_parts(out_buf.data, out_buf.len) };
4265 let decoded_values: Vec<f32> = decoded_bytes
4266 .chunks_exact(4)
4267 .map(|c| f32::from_ne_bytes(c.try_into().unwrap()))
4268 .collect();
4269 assert_eq!(decoded_values, [3.0, 4.0, 5.0]);
4270
4271 super::tgm_bytes_free(out_buf);
4272 }
4273
4274 #[test]
4275 fn ffi_decode_range_split_mode() {
4276 let values = [10.0f32, 20.0, 30.0, 40.0];
4277 let encoded = ffi_encode_single_f32_tensor(&values, "");
4278
4279 let range_offsets: [u64; 2] = [0, 2];
4281 let range_counts: [u64; 2] = [2, 2];
4282 let mut out_bufs = [
4283 super::TgmBytes {
4284 data: ptr::null_mut(),
4285 len: 0,
4286 },
4287 super::TgmBytes {
4288 data: ptr::null_mut(),
4289 len: 0,
4290 },
4291 ];
4292 let mut out_count: usize = 0;
4293
4294 let err = super::tgm_decode_range(
4295 encoded.as_ptr(),
4296 encoded.len(),
4297 0,
4298 range_offsets.as_ptr(),
4299 range_counts.as_ptr(),
4300 2,
4301 0,
4302 0, 0, out_bufs.as_mut_ptr(),
4305 &mut out_count,
4306 );
4307 assert!(matches!(err, super::TgmError::Ok));
4308 assert_eq!(out_count, 2);
4309
4310 let bytes0 = unsafe { slice::from_raw_parts(out_bufs[0].data, out_bufs[0].len) };
4312 let vals0: Vec<f32> = bytes0
4313 .chunks_exact(4)
4314 .map(|c| f32::from_ne_bytes(c.try_into().unwrap()))
4315 .collect();
4316 assert_eq!(vals0, [10.0, 20.0]);
4317
4318 let bytes1 = unsafe { slice::from_raw_parts(out_bufs[1].data, out_bufs[1].len) };
4320 let vals1: Vec<f32> = bytes1
4321 .chunks_exact(4)
4322 .map(|c| f32::from_ne_bytes(c.try_into().unwrap()))
4323 .collect();
4324 assert_eq!(vals1, [30.0, 40.0]);
4325
4326 super::tgm_bytes_free(super::TgmBytes {
4328 data: out_bufs[0].data,
4329 len: out_bufs[0].len,
4330 });
4331 super::tgm_bytes_free(super::TgmBytes {
4332 data: out_bufs[1].data,
4333 len: out_bufs[1].len,
4334 });
4335 }
4336
4337 #[test]
4338 fn ffi_decode_range_null_args() {
4339 let mut out_buf = super::TgmBytes {
4340 data: ptr::null_mut(),
4341 len: 0,
4342 };
4343 let mut out_count: usize = 0;
4344
4345 let err = super::tgm_decode_range(
4347 ptr::null(),
4348 0,
4349 0,
4350 ptr::null(),
4351 ptr::null(),
4352 0,
4353 0,
4354 0,
4355 0,
4356 &mut out_buf,
4357 &mut out_count,
4358 );
4359 assert!(matches!(err, super::TgmError::InvalidArg));
4360
4361 let data = [0u8; 10];
4363 let err = super::tgm_decode_range(
4364 data.as_ptr(),
4365 data.len(),
4366 0,
4367 ptr::null(),
4368 ptr::null(),
4369 0,
4370 0,
4371 0,
4372 0,
4373 ptr::null_mut(),
4374 &mut out_count,
4375 );
4376 assert!(matches!(err, super::TgmError::InvalidArg));
4377
4378 let err = super::tgm_decode_range(
4380 data.as_ptr(),
4381 data.len(),
4382 0,
4383 ptr::null(),
4384 ptr::null(),
4385 0,
4386 0,
4387 0,
4388 0,
4389 &mut out_buf,
4390 ptr::null_mut(),
4391 );
4392 assert!(matches!(err, super::TgmError::InvalidArg));
4393 }
4394
4395 #[test]
4396 fn ffi_decode_range_null_ranges_with_nonzero_count() {
4397 let encoded = ffi_encode_single_f32_tensor(&[1.0f32], "");
4398 let mut out_buf = super::TgmBytes {
4399 data: ptr::null_mut(),
4400 len: 0,
4401 };
4402 let mut out_count: usize = 0;
4403
4404 let err = super::tgm_decode_range(
4405 encoded.as_ptr(),
4406 encoded.len(),
4407 0,
4408 ptr::null(), ptr::null(), 1, 0,
4412 0, 0,
4414 &mut out_buf,
4415 &mut out_count,
4416 );
4417 assert!(matches!(err, super::TgmError::InvalidArg));
4418 }
4419
4420 #[test]
4423 fn ffi_scan_single_message() {
4424 let encoded = ffi_encode_single_f32_tensor(&[1.0f32, 2.0], "");
4425
4426 let mut result: *mut super::TgmScanResult = ptr::null_mut();
4427 let err = super::tgm_scan(encoded.as_ptr(), encoded.len(), &mut result);
4428 assert!(matches!(err, super::TgmError::Ok));
4429 assert!(!result.is_null());
4430
4431 assert_eq!(super::tgm_scan_count(result), 1);
4432
4433 let entry = super::tgm_scan_entry(result, 0);
4434 assert_eq!(entry.offset, 0);
4435 assert_eq!(entry.length, encoded.len());
4436
4437 let bad = super::tgm_scan_entry(result, 999);
4440 assert_eq!(bad.offset, usize::MAX);
4441 assert_eq!(bad.length, 0);
4442 let err_ptr = super::tgm_last_error();
4443 assert!(!err_ptr.is_null());
4444 let err_str = unsafe { CStr::from_ptr(err_ptr) }.to_str().unwrap();
4445 assert!(
4446 err_str.contains("out of range"),
4447 "expected OOB error, got: {err_str}"
4448 );
4449
4450 super::tgm_scan_free(result);
4451 }
4452
4453 #[test]
4454 fn ffi_scan_null_args() {
4455 let mut result: *mut super::TgmScanResult = ptr::null_mut();
4456 let err = super::tgm_scan(ptr::null(), 0, &mut result);
4457 assert!(matches!(err, super::TgmError::InvalidArg));
4458
4459 let data = [0u8; 10];
4460 let err = super::tgm_scan(data.as_ptr(), data.len(), ptr::null_mut());
4461 assert!(matches!(err, super::TgmError::InvalidArg));
4462 }
4463
4464 #[test]
4465 fn ffi_scan_null_handle_accessors() {
4466 assert_eq!(super::tgm_scan_count(ptr::null()), 0);
4467 let entry = super::tgm_scan_entry(ptr::null(), 0);
4468 assert_eq!(entry.offset, usize::MAX);
4469 assert_eq!(entry.length, 0);
4470 }
4471
4472 #[test]
4473 fn ffi_scan_concatenated_messages() {
4474 let msg1 = ffi_encode_single_f32_tensor(&[1.0f32], "");
4475 let msg2 = ffi_encode_single_f32_tensor(&[2.0f32], "");
4476 let mut concat = msg1.clone();
4477 concat.extend_from_slice(&msg2);
4478
4479 let mut result: *mut super::TgmScanResult = ptr::null_mut();
4480 let err = super::tgm_scan(concat.as_ptr(), concat.len(), &mut result);
4481 assert!(matches!(err, super::TgmError::Ok));
4482
4483 assert_eq!(super::tgm_scan_count(result), 2);
4484
4485 let e0 = super::tgm_scan_entry(result, 0);
4486 assert_eq!(e0.offset, 0);
4487 assert_eq!(e0.length, msg1.len());
4488
4489 let e1 = super::tgm_scan_entry(result, 1);
4490 assert_eq!(e1.offset, msg1.len());
4491 assert_eq!(e1.length, msg2.len());
4492
4493 super::tgm_scan_free(result);
4494 }
4495
4496 #[test]
4499 fn ffi_file_create_append_count_decode_close() {
4500 let dir = std::env::temp_dir();
4501 let path = dir.join("ffi_test_file.tgm");
4502 let _ = std::fs::remove_file(&path);
4503
4504 let c_path = CString::new(path.to_str().unwrap()).unwrap();
4505
4506 let mut file: *mut super::TgmFile = ptr::null_mut();
4508 let err = super::tgm_file_create(c_path.as_ptr(), &mut file);
4509 assert!(matches!(err, super::TgmError::Ok));
4510 assert!(!file.is_null());
4511
4512 let values = [10.0f32, 20.0, 30.0];
4514 let shape_str = format!("[{}]", values.len());
4515 let json = format!(
4516 r#"{{"version":3,"descriptors":[{{"type":"ntensor","ndim":1,"shape":{shape},"strides":[1],"dtype":"float32","byte_order":"{bo}","encoding":"none","filter":"none","compression":"none"}}]}}"#,
4517 shape = shape_str,
4518 bo = if cfg!(target_endian = "little") {
4519 "little"
4520 } else {
4521 "big"
4522 },
4523 );
4524 let c_json = CString::new(json).unwrap();
4525 let data: Vec<u8> = values.iter().flat_map(|v| v.to_ne_bytes()).collect();
4526 let data_ptr: *const u8 = data.as_ptr();
4527 let data_len: usize = data.len();
4528
4529 let err = super::tgm_file_append(
4530 file,
4531 c_json.as_ptr(),
4532 &data_ptr as *const *const u8,
4533 &data_len as *const usize,
4534 1,
4535 ptr::null(),
4536 0,
4537 );
4538 assert!(matches!(err, super::TgmError::Ok));
4539
4540 let path_ptr = super::tgm_file_path(file);
4542 assert!(!path_ptr.is_null());
4543 let path_str = unsafe { CStr::from_ptr(path_ptr) }.to_str().unwrap();
4544 assert!(path_str.contains("ffi_test_file.tgm"));
4545
4546 super::tgm_file_close(file);
4547
4548 let mut file2: *mut super::TgmFile = ptr::null_mut();
4550 let err = super::tgm_file_open(c_path.as_ptr(), &mut file2);
4551 assert!(matches!(err, super::TgmError::Ok));
4552
4553 let mut count: usize = 0;
4555 let err = super::tgm_file_message_count(file2, &mut count);
4556 assert!(matches!(err, super::TgmError::Ok));
4557 assert_eq!(count, 1);
4558
4559 let mut msg: *mut super::TgmMessage = ptr::null_mut();
4561 let err = super::tgm_file_decode_message(file2, 0, 0, 0, 0, &mut msg);
4562 assert!(matches!(err, super::TgmError::Ok));
4563
4564 assert_eq!(super::tgm_message_num_objects(msg), 1);
4565 let mut data_len2: usize = 0;
4566 let dp = super::tgm_object_data(msg, 0, &mut data_len2);
4567 assert!(!dp.is_null());
4568 let decoded_bytes = unsafe { slice::from_raw_parts(dp, data_len2) };
4569 let decoded_values: Vec<f32> = decoded_bytes
4570 .chunks_exact(4)
4571 .map(|c| f32::from_ne_bytes(c.try_into().unwrap()))
4572 .collect();
4573 assert_eq!(decoded_values, values);
4574
4575 super::tgm_message_free(msg);
4576
4577 let mut raw = super::TgmBytes {
4579 data: ptr::null_mut(),
4580 len: 0,
4581 };
4582 let err = super::tgm_file_read_message(file2, 0, &mut raw);
4583 assert!(matches!(err, super::TgmError::Ok));
4584 assert!(!raw.data.is_null());
4585 assert!(raw.len > 0);
4586 super::tgm_bytes_free(raw);
4587
4588 super::tgm_file_close(file2);
4589 let _ = std::fs::remove_file(&path);
4590 }
4591
4592 #[test]
4593 fn ffi_file_open_nonexistent() {
4594 let c_path = CString::new("/nonexistent/file.tgm").unwrap();
4595 let mut file: *mut super::TgmFile = ptr::null_mut();
4596 let err = super::tgm_file_open(c_path.as_ptr(), &mut file);
4597 assert!(!matches!(err, super::TgmError::Ok));
4598 }
4599
4600 #[test]
4601 fn ffi_file_null_args() {
4602 let mut file: *mut super::TgmFile = ptr::null_mut();
4603
4604 let err = super::tgm_file_open(ptr::null(), &mut file);
4606 assert!(matches!(err, super::TgmError::InvalidArg));
4607
4608 let c_path = CString::new("/tmp/test.tgm").unwrap();
4610 let err = super::tgm_file_open(c_path.as_ptr(), ptr::null_mut());
4611 assert!(matches!(err, super::TgmError::InvalidArg));
4612
4613 let err = super::tgm_file_create(ptr::null(), &mut file);
4615 assert!(matches!(err, super::TgmError::InvalidArg));
4616
4617 let err = super::tgm_file_create(c_path.as_ptr(), ptr::null_mut());
4619 assert!(matches!(err, super::TgmError::InvalidArg));
4620
4621 let err = super::tgm_file_message_count(ptr::null_mut(), ptr::null_mut());
4623 assert!(matches!(err, super::TgmError::InvalidArg));
4624
4625 let err = super::tgm_file_decode_message(ptr::null_mut(), 0, 0, 0, 0, ptr::null_mut());
4627 assert!(matches!(err, super::TgmError::InvalidArg));
4628
4629 let err = super::tgm_file_read_message(ptr::null_mut(), 0, ptr::null_mut());
4631 assert!(matches!(err, super::TgmError::InvalidArg));
4632
4633 let err = super::tgm_file_append(
4635 ptr::null_mut(),
4636 ptr::null(),
4637 ptr::null(),
4638 ptr::null(),
4639 0,
4640 ptr::null(),
4641 0,
4642 );
4643 assert!(matches!(err, super::TgmError::InvalidArg));
4644
4645 let err = super::tgm_file_append_raw(ptr::null_mut(), ptr::null(), 0);
4647 assert!(matches!(err, super::TgmError::InvalidArg));
4648
4649 assert!(super::tgm_file_path(ptr::null()).is_null());
4651 }
4652
4653 #[test]
4654 fn ffi_file_append_raw_round_trip() {
4655 let dir = std::env::temp_dir();
4656 let path = dir.join("ffi_test_append_raw.tgm");
4657 let _ = std::fs::remove_file(&path);
4658
4659 let c_path = CString::new(path.to_str().unwrap()).unwrap();
4660
4661 let mut file: *mut super::TgmFile = ptr::null_mut();
4663 let err = super::tgm_file_create(c_path.as_ptr(), &mut file);
4664 assert!(matches!(err, super::TgmError::Ok));
4665
4666 let encoded = ffi_encode_single_f32_tensor(&[1.0f32, 2.0], "");
4668 let err = super::tgm_file_append_raw(file, encoded.as_ptr(), encoded.len());
4669 assert!(matches!(err, super::TgmError::Ok));
4670
4671 let mut count: usize = 0;
4673 let err = super::tgm_file_message_count(file, &mut count);
4674 assert!(matches!(err, super::TgmError::Ok));
4675 assert_eq!(count, 1);
4676
4677 super::tgm_file_close(file);
4678 let _ = std::fs::remove_file(&path);
4679 }
4680
4681 #[test]
4684 fn ffi_streaming_encoder_round_trip() {
4685 let dir = std::env::temp_dir();
4686 let path = dir.join("ffi_streaming_test.tgm");
4687 let _ = std::fs::remove_file(&path);
4688
4689 let c_path = CString::new(path.to_str().unwrap()).unwrap();
4690 let meta_json = CString::new(r#"{"version":3}"#).unwrap();
4691
4692 let mut enc: *mut super::TgmStreamingEncoder = ptr::null_mut();
4694 let err = super::tgm_streaming_encoder_create(
4695 c_path.as_ptr(),
4696 meta_json.as_ptr(),
4697 ptr::null(), 0, &mut enc,
4700 );
4701 assert!(matches!(err, super::TgmError::Ok));
4702 assert!(!enc.is_null());
4703
4704 let values = [100.0f32, 200.0, 300.0];
4706 let data: Vec<u8> = values.iter().flat_map(|v| v.to_ne_bytes()).collect();
4707 let desc_json = CString::new(format!(
4708 r#"{{"type":"ntensor","ndim":1,"shape":[{len}],"strides":[1],"dtype":"float32","byte_order":"{bo}","encoding":"none","filter":"none","compression":"none"}}"#,
4709 len = values.len(),
4710 bo = if cfg!(target_endian = "little") { "little" } else { "big" },
4711 )).unwrap();
4712
4713 let err =
4714 super::tgm_streaming_encoder_write(enc, desc_json.as_ptr(), data.as_ptr(), data.len());
4715 assert!(matches!(err, super::TgmError::Ok));
4716
4717 assert_eq!(super::tgm_streaming_encoder_count(enc), 1);
4719
4720 let err = super::tgm_streaming_encoder_finish(enc);
4722 assert!(matches!(err, super::TgmError::Ok));
4723
4724 let err = super::tgm_streaming_encoder_finish(enc);
4726 assert!(matches!(err, super::TgmError::InvalidArg));
4727
4728 assert_eq!(super::tgm_streaming_encoder_count(enc), 0);
4730
4731 super::tgm_streaming_encoder_free(enc);
4733
4734 let mut file: *mut super::TgmFile = ptr::null_mut();
4736 let err = super::tgm_file_open(c_path.as_ptr(), &mut file);
4737 assert!(matches!(err, super::TgmError::Ok));
4738
4739 let mut count: usize = 0;
4740 let err = super::tgm_file_message_count(file, &mut count);
4741 assert!(matches!(err, super::TgmError::Ok));
4742 assert_eq!(count, 1);
4743
4744 let mut msg: *mut super::TgmMessage = ptr::null_mut();
4745 let err = super::tgm_file_decode_message(file, 0, 0, 0, 0, &mut msg);
4746 assert!(matches!(err, super::TgmError::Ok));
4747
4748 let mut data_len: usize = 0;
4749 let dp = super::tgm_object_data(msg, 0, &mut data_len);
4750 let decoded_bytes = unsafe { slice::from_raw_parts(dp, data_len) };
4751 let decoded_values: Vec<f32> = decoded_bytes
4752 .chunks_exact(4)
4753 .map(|c| f32::from_ne_bytes(c.try_into().unwrap()))
4754 .collect();
4755 assert_eq!(decoded_values, values);
4756
4757 super::tgm_message_free(msg);
4758 super::tgm_file_close(file);
4759 let _ = std::fs::remove_file(&path);
4760 }
4761
4762 #[test]
4763 fn ffi_streaming_encoder_null_args() {
4764 let mut enc: *mut super::TgmStreamingEncoder = ptr::null_mut();
4765
4766 let meta = CString::new(r#"{"version":3}"#).unwrap();
4768 let err = super::tgm_streaming_encoder_create(
4769 ptr::null(),
4770 meta.as_ptr(),
4771 ptr::null(),
4772 0,
4773 &mut enc,
4774 );
4775 assert!(matches!(err, super::TgmError::InvalidArg));
4776
4777 let p = CString::new("/tmp/dummy.tgm").unwrap();
4779 let err =
4780 super::tgm_streaming_encoder_create(p.as_ptr(), ptr::null(), ptr::null(), 0, &mut enc);
4781 assert!(matches!(err, super::TgmError::InvalidArg));
4782
4783 let err = super::tgm_streaming_encoder_create(
4785 p.as_ptr(),
4786 meta.as_ptr(),
4787 ptr::null(),
4788 0,
4789 ptr::null_mut(),
4790 );
4791 assert!(matches!(err, super::TgmError::InvalidArg));
4792
4793 let desc = CString::new(r#"{}"#).unwrap();
4795 let data = [0u8; 4];
4796 let err = super::tgm_streaming_encoder_write(
4797 ptr::null_mut(),
4798 desc.as_ptr(),
4799 data.as_ptr(),
4800 data.len(),
4801 );
4802 assert!(matches!(err, super::TgmError::InvalidArg));
4803
4804 let err = super::tgm_streaming_encoder_finish(ptr::null_mut());
4809 assert!(matches!(err, super::TgmError::InvalidArg));
4810
4811 assert_eq!(super::tgm_streaming_encoder_count(ptr::null()), 0);
4813
4814 super::tgm_streaming_encoder_free(ptr::null_mut());
4816 }
4817
4818 #[test]
4819 fn ffi_streaming_encoder_write_null_data() {
4820 let dir = std::env::temp_dir();
4821 let path = dir.join("ffi_streaming_null_data.tgm");
4822 let _ = std::fs::remove_file(&path);
4823
4824 let c_path = CString::new(path.to_str().unwrap()).unwrap();
4825 let meta_json = CString::new(r#"{"version":3}"#).unwrap();
4826
4827 let mut enc: *mut super::TgmStreamingEncoder = ptr::null_mut();
4828 let err = super::tgm_streaming_encoder_create(
4829 c_path.as_ptr(),
4830 meta_json.as_ptr(),
4831 ptr::null(),
4832 0,
4833 &mut enc,
4834 );
4835 assert!(matches!(err, super::TgmError::Ok));
4836
4837 let desc = CString::new(r#"{"type":"ntensor","ndim":1,"shape":[1],"strides":[1],"dtype":"float32","byte_order":"little","encoding":"none","filter":"none","compression":"none"}"#).unwrap();
4838 let err = super::tgm_streaming_encoder_write(enc, desc.as_ptr(), ptr::null(), 4);
4839 assert!(matches!(err, super::TgmError::InvalidArg));
4840
4841 let data = [0u8; 4];
4843 let err = super::tgm_streaming_encoder_write(enc, ptr::null(), data.as_ptr(), data.len());
4844 assert!(matches!(err, super::TgmError::InvalidArg));
4845
4846 super::tgm_streaming_encoder_free(enc);
4847 let _ = std::fs::remove_file(&path);
4848 }
4849
4850 #[test]
4851 fn ffi_streaming_encoder_with_preceder() {
4852 let dir = std::env::temp_dir();
4853 let path = dir.join("ffi_streaming_preceder.tgm");
4854 let _ = std::fs::remove_file(&path);
4855
4856 let c_path = CString::new(path.to_str().unwrap()).unwrap();
4857 let meta_json = CString::new(r#"{"version":3}"#).unwrap();
4858
4859 let mut enc: *mut super::TgmStreamingEncoder = ptr::null_mut();
4860 let err = super::tgm_streaming_encoder_create(
4861 c_path.as_ptr(),
4862 meta_json.as_ptr(),
4863 ptr::null(),
4864 0,
4865 &mut enc,
4866 );
4867 assert!(matches!(err, super::TgmError::Ok));
4868
4869 let preceder_json = CString::new(r#"{"param":"2t","source":"test"}"#).unwrap();
4871 let err = super::tgm_streaming_encoder_write_preceder(enc, preceder_json.as_ptr());
4872 assert!(matches!(err, super::TgmError::Ok));
4873
4874 let values = [42.0f32];
4876 let data: Vec<u8> = values.iter().flat_map(|v| v.to_ne_bytes()).collect();
4877 let desc_json = CString::new(format!(
4878 r#"{{"type":"ntensor","ndim":1,"shape":[1],"strides":[1],"dtype":"float32","byte_order":"{bo}","encoding":"none","filter":"none","compression":"none"}}"#,
4879 bo = if cfg!(target_endian = "little") { "little" } else { "big" },
4880 )).unwrap();
4881
4882 let err =
4883 super::tgm_streaming_encoder_write(enc, desc_json.as_ptr(), data.as_ptr(), data.len());
4884 assert!(matches!(err, super::TgmError::Ok));
4885
4886 let err = super::tgm_streaming_encoder_finish(enc);
4887 assert!(matches!(err, super::TgmError::Ok));
4888 super::tgm_streaming_encoder_free(enc);
4889
4890 let mut file: *mut super::TgmFile = ptr::null_mut();
4892 let err = super::tgm_file_open(c_path.as_ptr(), &mut file);
4893 assert!(matches!(err, super::TgmError::Ok));
4894
4895 let mut msg: *mut super::TgmMessage = ptr::null_mut();
4896 let err = super::tgm_file_decode_message(file, 0, 0, 0, 0, &mut msg);
4897 assert!(matches!(err, super::TgmError::Ok));
4898
4899 let mut meta: *mut super::TgmMetadata = ptr::null_mut();
4900 let err = super::tgm_message_metadata(msg, &mut meta);
4901 assert!(matches!(err, super::TgmError::Ok));
4902
4903 let key = CString::new("param").unwrap();
4904 let val_ptr = super::tgm_metadata_get_string(meta, key.as_ptr());
4905 assert!(!val_ptr.is_null());
4906 let val_str = unsafe { CStr::from_ptr(val_ptr) }.to_str().unwrap();
4907 assert_eq!(val_str, "2t");
4908
4909 super::tgm_metadata_free(meta);
4910 super::tgm_message_free(msg);
4911 super::tgm_file_close(file);
4912 let _ = std::fs::remove_file(&path);
4913 }
4914
4915 #[test]
4916 fn ffi_streaming_encoder_write_preceder_null_args() {
4917 let err = super::tgm_streaming_encoder_write_preceder(ptr::null_mut(), ptr::null());
4918 assert!(matches!(err, super::TgmError::InvalidArg));
4919 }
4920
4921 #[test]
4922 fn ffi_streaming_encoder_write_pre_encoded_null_args() {
4923 let err = super::tgm_streaming_encoder_write_pre_encoded(
4924 ptr::null_mut(),
4925 ptr::null(),
4926 ptr::null(),
4927 0,
4928 );
4929 assert!(matches!(err, super::TgmError::InvalidArg));
4930 }
4931
4932 #[test]
4935 fn ffi_compute_hash_xxh3() {
4936 let data = b"hello world";
4937 let mut out = super::TgmBytes {
4938 data: ptr::null_mut(),
4939 len: 0,
4940 };
4941 let err = super::tgm_compute_hash(
4942 data.as_ptr(),
4943 data.len(),
4944 ptr::null(), &mut out,
4946 );
4947 assert!(matches!(err, super::TgmError::Ok));
4948 assert!(!out.data.is_null());
4949 assert!(out.len > 0);
4950
4951 let hex = unsafe { std::str::from_utf8(slice::from_raw_parts(out.data, out.len)).unwrap() };
4952 assert!(!hex.is_empty());
4954 assert!(hex.chars().all(|c| c.is_ascii_hexdigit()));
4955
4956 super::tgm_bytes_free(out);
4957 }
4958
4959 #[test]
4960 fn ffi_compute_hash_explicit_xxh3() {
4961 let data = b"test data";
4962 let algo = CString::new("xxh3").unwrap();
4963 let mut out = super::TgmBytes {
4964 data: ptr::null_mut(),
4965 len: 0,
4966 };
4967 let err = super::tgm_compute_hash(data.as_ptr(), data.len(), algo.as_ptr(), &mut out);
4968 assert!(matches!(err, super::TgmError::Ok));
4969 assert!(out.len > 0);
4970 super::tgm_bytes_free(out);
4971 }
4972
4973 #[test]
4974 fn ffi_compute_hash_null_data() {
4975 let mut out = super::TgmBytes {
4976 data: ptr::null_mut(),
4977 len: 0,
4978 };
4979 let err = super::tgm_compute_hash(ptr::null(), 0, ptr::null(), &mut out);
4980 assert!(matches!(err, super::TgmError::InvalidArg));
4981 }
4982
4983 #[test]
4984 fn ffi_compute_hash_null_out() {
4985 let data = b"hello";
4986 let err = super::tgm_compute_hash(data.as_ptr(), data.len(), ptr::null(), ptr::null_mut());
4987 assert!(matches!(err, super::TgmError::InvalidArg));
4988 }
4989
4990 #[test]
4991 fn ffi_compute_hash_invalid_algo() {
4992 let data = b"hello";
4993 let algo = CString::new("bogus_algo").unwrap();
4994 let mut out = super::TgmBytes {
4995 data: ptr::null_mut(),
4996 len: 0,
4997 };
4998 let err = super::tgm_compute_hash(data.as_ptr(), data.len(), algo.as_ptr(), &mut out);
4999 assert!(matches!(err, super::TgmError::InvalidArg));
5000 }
5001
5002 #[test]
5005 fn ffi_simple_packing_compute_params() {
5006 let values = [100.0f64, 200.0, 300.0, 400.0];
5007 let mut ref_val: f64 = 0.0;
5008 let mut bin_scale: i32 = 0;
5009 let err = super::tgm_simple_packing_compute_params(
5010 values.as_ptr(),
5011 values.len(),
5012 16,
5013 0,
5014 &mut ref_val,
5015 &mut bin_scale,
5016 );
5017 assert!(matches!(err, super::TgmError::Ok));
5018 assert!(ref_val <= 100.0);
5020 }
5021
5022 #[test]
5023 fn ffi_simple_packing_compute_params_null_args() {
5024 let mut ref_val: f64 = 0.0;
5025 let mut bin_scale: i32 = 0;
5026
5027 let err = super::tgm_simple_packing_compute_params(
5029 ptr::null(),
5030 0,
5031 16,
5032 0,
5033 &mut ref_val,
5034 &mut bin_scale,
5035 );
5036 assert!(matches!(err, super::TgmError::InvalidArg));
5037
5038 let values = [1.0f64];
5040 let err = super::tgm_simple_packing_compute_params(
5041 values.as_ptr(),
5042 values.len(),
5043 16,
5044 0,
5045 ptr::null_mut(),
5046 &mut bin_scale,
5047 );
5048 assert!(matches!(err, super::TgmError::InvalidArg));
5049
5050 let err = super::tgm_simple_packing_compute_params(
5052 values.as_ptr(),
5053 values.len(),
5054 16,
5055 0,
5056 &mut ref_val,
5057 ptr::null_mut(),
5058 );
5059 assert!(matches!(err, super::TgmError::InvalidArg));
5060 }
5061
5062 #[test]
5065 fn ffi_last_error_after_success() {
5066 let values = [1.0f32];
5069 let _ = ffi_encode_single_f32_tensor(&values, "");
5070 }
5072
5073 #[test]
5074 fn ffi_last_error_after_failure() {
5075 let mut out = super::TgmBytes {
5077 data: ptr::null_mut(),
5078 len: 0,
5079 };
5080 let _ = super::tgm_encode(
5081 ptr::null(),
5082 ptr::null(),
5083 ptr::null(),
5084 0,
5085 ptr::null(),
5086 0,
5087 &mut out,
5088 );
5089
5090 let err_ptr = super::tgm_last_error();
5091 assert!(!err_ptr.is_null());
5092 let err_str = unsafe { CStr::from_ptr(err_ptr) }.to_str().unwrap();
5093 assert!(err_str.contains("null"));
5094 }
5095
5096 #[test]
5099 fn ffi_error_string_all_variants() {
5100 let check = |err: super::TgmError, expected: &str| {
5101 let ptr = super::tgm_error_string(err);
5102 assert!(!ptr.is_null());
5103 let s = unsafe { CStr::from_ptr(ptr) }.to_str().unwrap();
5104 assert_eq!(s, expected);
5105 };
5106
5107 check(super::TgmError::Ok, "ok");
5108 check(super::TgmError::Framing, "framing error");
5109 check(super::TgmError::Metadata, "metadata error");
5110 check(super::TgmError::Encoding, "encoding error");
5111 check(super::TgmError::Compression, "compression error");
5112 check(super::TgmError::Object, "object error");
5113 check(super::TgmError::Io, "I/O error");
5114 check(super::TgmError::HashMismatch, "hash mismatch");
5115 check(super::TgmError::InvalidArg, "invalid argument");
5116 check(super::TgmError::EndOfIter, "end of iteration");
5117 check(super::TgmError::Remote, "remote error");
5118 }
5119
5120 #[test]
5123 fn ffi_bytes_free_null_data() {
5124 let buf = super::TgmBytes {
5125 data: ptr::null_mut(),
5126 len: 0,
5127 };
5128 super::tgm_bytes_free(buf); }
5130
5131 #[test]
5134 fn ffi_free_null_handles() {
5135 super::tgm_message_free(ptr::null_mut());
5136 super::tgm_metadata_free(ptr::null_mut());
5137 super::tgm_scan_free(ptr::null_mut());
5138 super::tgm_file_close(ptr::null_mut());
5139 super::tgm_streaming_encoder_free(ptr::null_mut());
5140 }
5142
5143 #[test]
5146 fn ffi_encode_pre_encoded_null_args() {
5147 let mut out = super::TgmBytes {
5148 data: ptr::null_mut(),
5149 len: 0,
5150 };
5151 let err = super::tgm_encode_pre_encoded(
5152 ptr::null(),
5153 ptr::null(),
5154 ptr::null(),
5155 0,
5156 ptr::null(),
5157 0,
5158 &mut out,
5159 );
5160 assert!(matches!(err, super::TgmError::InvalidArg));
5161
5162 let json = CString::new(r#"{"version":3,"descriptors":[]}"#).unwrap();
5163 let err = super::tgm_encode_pre_encoded(
5164 json.as_ptr(),
5165 ptr::null(),
5166 ptr::null(),
5167 0,
5168 ptr::null(),
5169 0,
5170 ptr::null_mut(),
5171 );
5172 assert!(matches!(err, super::TgmError::InvalidArg));
5173 }
5174
5175 #[test]
5176 fn ffi_encode_pre_encoded_round_trip() {
5177 let values = [5.0f32, 6.0, 7.0];
5179 let data: Vec<u8> = values.iter().flat_map(|v| v.to_ne_bytes()).collect();
5180
5181 let json = format!(
5182 r#"{{"version":3,"descriptors":[{{"type":"ntensor","ndim":1,"shape":[{len}],"strides":[1],"dtype":"float32","byte_order":"{bo}","encoding":"none","filter":"none","compression":"none"}}]}}"#,
5183 len = values.len(),
5184 bo = if cfg!(target_endian = "little") {
5185 "little"
5186 } else {
5187 "big"
5188 },
5189 );
5190 let c_json = CString::new(json).unwrap();
5191 let data_ptr: *const u8 = data.as_ptr();
5192 let data_len: usize = data.len();
5193
5194 let mut out = super::TgmBytes {
5195 data: ptr::null_mut(),
5196 len: 0,
5197 };
5198 let err = super::tgm_encode_pre_encoded(
5199 c_json.as_ptr(),
5200 &data_ptr as *const *const u8,
5201 &data_len as *const usize,
5202 1,
5203 ptr::null(),
5204 0,
5205 &mut out,
5206 );
5207 assert!(matches!(err, super::TgmError::Ok));
5208
5209 let encoded = unsafe { slice::from_raw_parts(out.data, out.len) }.to_vec();
5210 super::tgm_bytes_free(out);
5211
5212 let mut msg: *mut super::TgmMessage = ptr::null_mut();
5214 let err = super::tgm_decode(encoded.as_ptr(), encoded.len(), 0, 0, 0, &mut msg);
5215 assert!(matches!(err, super::TgmError::Ok));
5216
5217 let mut dl: usize = 0;
5218 let dp = super::tgm_object_data(msg, 0, &mut dl);
5219 let decoded_bytes = unsafe { slice::from_raw_parts(dp, dl) };
5220 let decoded_values: Vec<f32> = decoded_bytes
5221 .chunks_exact(4)
5222 .map(|c| f32::from_ne_bytes(c.try_into().unwrap()))
5223 .collect();
5224 assert_eq!(decoded_values, values);
5225
5226 super::tgm_message_free(msg);
5227 }
5228
5229 #[test]
5232 fn ffi_buffer_iter_round_trip() {
5233 let msg1 = ffi_encode_single_f32_tensor(&[1.0f32], "");
5234 let msg2 = ffi_encode_single_f32_tensor(&[2.0f32], "");
5235 let mut concat = msg1.clone();
5236 concat.extend_from_slice(&msg2);
5237
5238 let mut iter: *mut super::TgmBufferIter = ptr::null_mut();
5239 let err = super::tgm_buffer_iter_create(concat.as_ptr(), concat.len(), &mut iter);
5240 assert!(matches!(err, super::TgmError::Ok));
5241 assert!(!iter.is_null());
5242
5243 assert_eq!(super::tgm_buffer_iter_count(iter), 2);
5244
5245 let mut out_buf: *const u8 = ptr::null();
5247 let mut out_len: usize = 0;
5248 let err = super::tgm_buffer_iter_next(iter, &mut out_buf, &mut out_len);
5249 assert!(matches!(err, super::TgmError::Ok));
5250 assert!(!out_buf.is_null());
5251 assert_eq!(out_len, msg1.len());
5252
5253 let err = super::tgm_buffer_iter_next(iter, &mut out_buf, &mut out_len);
5255 assert!(matches!(err, super::TgmError::Ok));
5256 assert_eq!(out_len, msg2.len());
5257
5258 let err = super::tgm_buffer_iter_next(iter, &mut out_buf, &mut out_len);
5260 assert!(matches!(err, super::TgmError::EndOfIter));
5261
5262 super::tgm_buffer_iter_free(iter);
5263 }
5264
5265 #[test]
5266 fn ffi_buffer_iter_null_args() {
5267 let mut iter: *mut super::TgmBufferIter = ptr::null_mut();
5268 let err = super::tgm_buffer_iter_create(ptr::null(), 0, &mut iter);
5269 assert!(matches!(err, super::TgmError::InvalidArg));
5270
5271 let data = [0u8; 10];
5272 let err = super::tgm_buffer_iter_create(data.as_ptr(), data.len(), ptr::null_mut());
5273 assert!(matches!(err, super::TgmError::InvalidArg));
5274
5275 let mut out_buf: *const u8 = ptr::null();
5277 let mut out_len: usize = 0;
5278 let err = super::tgm_buffer_iter_next(ptr::null_mut(), &mut out_buf, &mut out_len);
5279 assert!(matches!(err, super::TgmError::InvalidArg));
5280
5281 let err = super::tgm_buffer_iter_next(ptr::null_mut(), ptr::null_mut(), &mut out_len);
5284 assert!(matches!(err, super::TgmError::InvalidArg));
5285
5286 assert_eq!(super::tgm_buffer_iter_count(ptr::null()), 0);
5288
5289 super::tgm_buffer_iter_free(ptr::null_mut());
5291 }
5292
5293 #[test]
5296 fn ffi_object_iter_round_trip() {
5297 let encoded = ffi_encode_single_f32_tensor(&[10.0f32, 20.0], "");
5298
5299 let mut iter: *mut super::TgmObjectIter = ptr::null_mut();
5300 let err = super::tgm_object_iter_create(
5301 encoded.as_ptr(),
5302 encoded.len(),
5303 0, 0, &mut iter,
5306 );
5307 assert!(matches!(err, super::TgmError::Ok));
5308 assert!(!iter.is_null());
5309
5310 let mut msg: *mut super::TgmMessage = ptr::null_mut();
5312 let err = super::tgm_object_iter_next(iter, &mut msg);
5313 assert!(matches!(err, super::TgmError::Ok));
5314 assert!(!msg.is_null());
5315
5316 assert_eq!(super::tgm_message_num_objects(msg), 1);
5317 assert_eq!(super::tgm_message_version(msg), 3);
5318
5319 let mut data_len: usize = 0;
5320 let dp = super::tgm_object_data(msg, 0, &mut data_len);
5321 assert!(!dp.is_null());
5322 assert_eq!(data_len, 8); super::tgm_message_free(msg);
5325
5326 let mut msg2: *mut super::TgmMessage = ptr::null_mut();
5328 let err = super::tgm_object_iter_next(iter, &mut msg2);
5329 assert!(matches!(err, super::TgmError::EndOfIter));
5330
5331 super::tgm_object_iter_free(iter);
5332 }
5333
5334 #[test]
5335 fn ffi_object_iter_null_args() {
5336 let mut iter: *mut super::TgmObjectIter = ptr::null_mut();
5337 let err = super::tgm_object_iter_create(ptr::null(), 0, 0, 0, &mut iter);
5338 assert!(matches!(err, super::TgmError::InvalidArg));
5339
5340 let data = [0u8; 10];
5341 let err = super::tgm_object_iter_create(data.as_ptr(), data.len(), 0, 0, ptr::null_mut());
5342 assert!(matches!(err, super::TgmError::InvalidArg));
5343
5344 let mut msg: *mut super::TgmMessage = ptr::null_mut();
5346 let err = super::tgm_object_iter_next(ptr::null_mut(), &mut msg);
5347 assert!(matches!(err, super::TgmError::InvalidArg));
5348
5349 let err = super::tgm_object_iter_next(ptr::null_mut(), ptr::null_mut());
5351 assert!(matches!(err, super::TgmError::InvalidArg));
5352
5353 super::tgm_object_iter_free(ptr::null_mut());
5355 }
5356
5357 #[test]
5360 fn ffi_file_iter_round_trip() {
5361 let dir = std::env::temp_dir();
5362 let path = dir.join("ffi_file_iter_test.tgm");
5363 let _ = std::fs::remove_file(&path);
5364
5365 let c_path = CString::new(path.to_str().unwrap()).unwrap();
5366
5367 let mut file: *mut super::TgmFile = ptr::null_mut();
5369 let err = super::tgm_file_create(c_path.as_ptr(), &mut file);
5370 assert!(matches!(err, super::TgmError::Ok));
5371
5372 let msg1 = ffi_encode_single_f32_tensor(&[1.0f32], "");
5373 let msg2 = ffi_encode_single_f32_tensor(&[2.0f32], "");
5374 let err = super::tgm_file_append_raw(file, msg1.as_ptr(), msg1.len());
5375 assert!(matches!(err, super::TgmError::Ok));
5376 let err = super::tgm_file_append_raw(file, msg2.as_ptr(), msg2.len());
5377 assert!(matches!(err, super::TgmError::Ok));
5378
5379 let mut iter: *mut super::TgmFileIter = ptr::null_mut();
5381 let err = super::tgm_file_iter_create(file, &mut iter);
5382 assert!(matches!(err, super::TgmError::Ok));
5383 assert!(!iter.is_null());
5384
5385 let mut out = super::TgmBytes {
5387 data: ptr::null_mut(),
5388 len: 0,
5389 };
5390 let err = super::tgm_file_iter_next(iter, &mut out);
5391 assert!(matches!(err, super::TgmError::Ok));
5392 assert!(!out.data.is_null());
5393 assert_eq!(out.len, msg1.len());
5394 super::tgm_bytes_free(out);
5395
5396 let mut out2 = super::TgmBytes {
5398 data: ptr::null_mut(),
5399 len: 0,
5400 };
5401 let err = super::tgm_file_iter_next(iter, &mut out2);
5402 assert!(matches!(err, super::TgmError::Ok));
5403 super::tgm_bytes_free(out2);
5404
5405 let mut out3 = super::TgmBytes {
5407 data: ptr::null_mut(),
5408 len: 0,
5409 };
5410 let err = super::tgm_file_iter_next(iter, &mut out3);
5411 assert!(matches!(err, super::TgmError::EndOfIter));
5412
5413 super::tgm_file_iter_free(iter);
5414 super::tgm_file_close(file);
5415 let _ = std::fs::remove_file(&path);
5416 }
5417
5418 #[test]
5419 fn ffi_file_iter_null_args() {
5420 let mut iter: *mut super::TgmFileIter = ptr::null_mut();
5421 let err = super::tgm_file_iter_create(ptr::null_mut(), &mut iter);
5422 assert!(matches!(err, super::TgmError::InvalidArg));
5423
5424 let err = super::tgm_file_iter_create(ptr::null_mut(), ptr::null_mut());
5425 assert!(matches!(err, super::TgmError::InvalidArg));
5426
5427 let mut out = super::TgmBytes {
5429 data: ptr::null_mut(),
5430 len: 0,
5431 };
5432 let err = super::tgm_file_iter_next(ptr::null_mut(), &mut out);
5433 assert!(matches!(err, super::TgmError::InvalidArg));
5434
5435 let err = super::tgm_file_iter_next(ptr::null_mut(), ptr::null_mut());
5436 assert!(matches!(err, super::TgmError::InvalidArg));
5437
5438 super::tgm_file_iter_free(ptr::null_mut());
5440 }
5441
5442 #[test]
5445 fn ffi_encode_decode_zero_objects() {
5446 let json = CString::new(r#"{"version":3,"descriptors":[],"source":"empty"}"#).unwrap();
5447 let mut out = super::TgmBytes {
5448 data: ptr::null_mut(),
5449 len: 0,
5450 };
5451 let err = super::tgm_encode(
5452 json.as_ptr(),
5453 ptr::null(),
5454 ptr::null(),
5455 0,
5456 ptr::null(),
5457 0,
5458 &mut out,
5459 );
5460 assert!(matches!(err, super::TgmError::Ok));
5461
5462 let encoded = unsafe { slice::from_raw_parts(out.data, out.len) }.to_vec();
5463 super::tgm_bytes_free(out);
5464
5465 let mut msg: *mut super::TgmMessage = ptr::null_mut();
5466 let err = super::tgm_decode(encoded.as_ptr(), encoded.len(), 0, 0, 0, &mut msg);
5467 assert!(matches!(err, super::TgmError::Ok));
5468
5469 assert_eq!(super::tgm_message_version(msg), 3);
5470 assert_eq!(super::tgm_message_num_objects(msg), 0);
5471
5472 super::tgm_message_free(msg);
5473 }
5474
5475 #[test]
5487 fn ffi_streaming_encoder_with_hash() {
5488 let dir = std::env::temp_dir();
5489 let path = dir.join("ffi_streaming_hash.tgm");
5490 let _ = std::fs::remove_file(&path);
5491
5492 let c_path = CString::new(path.to_str().unwrap()).unwrap();
5493 let meta_json = CString::new(r#"{"version":3}"#).unwrap();
5494 let hash_algo = CString::new("xxh3").unwrap();
5495
5496 let mut enc: *mut super::TgmStreamingEncoder = ptr::null_mut();
5497 let err = super::tgm_streaming_encoder_create(
5498 c_path.as_ptr(),
5499 meta_json.as_ptr(),
5500 hash_algo.as_ptr(),
5501 0,
5502 &mut enc,
5503 );
5504 assert!(matches!(err, super::TgmError::Ok));
5505
5506 let values = [1.0f32, 2.0];
5507 let data: Vec<u8> = values.iter().flat_map(|v| v.to_ne_bytes()).collect();
5508 let desc_json = CString::new(format!(
5509 r#"{{"type":"ntensor","ndim":1,"shape":[{len}],"strides":[1],"dtype":"float32","byte_order":"{bo}","encoding":"none","filter":"none","compression":"none"}}"#,
5510 len = values.len(),
5511 bo = if cfg!(target_endian = "little") { "little" } else { "big" },
5512 )).unwrap();
5513
5514 let err =
5515 super::tgm_streaming_encoder_write(enc, desc_json.as_ptr(), data.as_ptr(), data.len());
5516 assert!(matches!(err, super::TgmError::Ok));
5517
5518 let err = super::tgm_streaming_encoder_finish(enc);
5519 assert!(matches!(err, super::TgmError::Ok));
5520 super::tgm_streaming_encoder_free(enc);
5521
5522 let bytes = std::fs::read(&path).expect("read file");
5525 let mut msg: *mut super::TgmMessage = ptr::null_mut();
5526 let err = super::tgm_decode(bytes.as_ptr(), bytes.len(), 0, 0, 0, &mut msg);
5527 assert!(matches!(err, super::TgmError::Ok));
5528
5529 assert_eq!(super::tgm_payload_has_hash(msg, 0), 1);
5530 let ht = unsafe { CStr::from_ptr(super::tgm_object_hash_type(msg, 0)) }
5531 .to_str()
5532 .unwrap();
5533 assert_eq!(ht, "xxh3");
5534 let hv = unsafe { CStr::from_ptr(super::tgm_object_hash_value(msg, 0)) }
5535 .to_str()
5536 .unwrap();
5537 assert_eq!(hv.len(), 16, "xxh3 digest is 16 hex chars");
5538
5539 super::tgm_message_free(msg);
5540 let _ = std::fs::remove_file(&path);
5541 }
5542
5543 #[test]
5550 fn ffi_file_decode_surfaces_inline_hash() {
5551 let dir = std::env::temp_dir();
5556 let path = dir.join("ffi_file_hash.tgm");
5557 let _ = std::fs::remove_file(&path);
5558
5559 let c_path = CString::new(path.to_str().unwrap()).unwrap();
5560 let meta_json = CString::new(r#"{"version":3}"#).unwrap();
5561 let hash_algo = CString::new("xxh3").unwrap();
5562 let mut enc: *mut super::TgmStreamingEncoder = ptr::null_mut();
5563 assert!(matches!(
5564 super::tgm_streaming_encoder_create(
5565 c_path.as_ptr(),
5566 meta_json.as_ptr(),
5567 hash_algo.as_ptr(),
5568 0,
5569 &mut enc,
5570 ),
5571 super::TgmError::Ok
5572 ));
5573 let values = [1.0f32, 2.0, 3.0];
5574 let data: Vec<u8> = values.iter().flat_map(|v| v.to_ne_bytes()).collect();
5575 let desc_json = CString::new(format!(
5576 r#"{{"type":"ntensor","ndim":1,"shape":[{}],"strides":[1],"dtype":"float32","byte_order":"{}","encoding":"none","filter":"none","compression":"none"}}"#,
5577 values.len(),
5578 if cfg!(target_endian = "little") { "little" } else { "big" },
5579 )).unwrap();
5580 assert!(matches!(
5581 super::tgm_streaming_encoder_write(enc, desc_json.as_ptr(), data.as_ptr(), data.len(),),
5582 super::TgmError::Ok
5583 ));
5584 assert!(matches!(
5585 super::tgm_streaming_encoder_finish(enc),
5586 super::TgmError::Ok
5587 ));
5588 super::tgm_streaming_encoder_free(enc);
5589
5590 let mut file: *mut super::TgmFile = ptr::null_mut();
5592 assert!(matches!(
5593 super::tgm_file_open(c_path.as_ptr(), &mut file),
5594 super::TgmError::Ok
5595 ));
5596 let mut file_msg: *mut super::TgmMessage = ptr::null_mut();
5597 assert!(matches!(
5598 super::tgm_file_decode_message(file, 0, 0, 0, 0, &mut file_msg),
5599 super::TgmError::Ok
5600 ));
5601 let file_has = super::tgm_payload_has_hash(file_msg, 0);
5602 let file_hv_ptr = super::tgm_object_hash_value(file_msg, 0);
5603 assert_eq!(file_has, 1);
5604 assert!(!file_hv_ptr.is_null());
5605 let file_hv = unsafe { CStr::from_ptr(file_hv_ptr) }
5606 .to_str()
5607 .unwrap()
5608 .to_string();
5609
5610 let bytes = std::fs::read(&path).unwrap();
5612 let mut buf_msg: *mut super::TgmMessage = ptr::null_mut();
5613 assert!(matches!(
5614 super::tgm_decode(bytes.as_ptr(), bytes.len(), 0, 0, 0, &mut buf_msg),
5615 super::TgmError::Ok
5616 ));
5617 let buf_hv_ptr = super::tgm_object_hash_value(buf_msg, 0);
5618 let buf_hv = unsafe { CStr::from_ptr(buf_hv_ptr) }
5619 .to_str()
5620 .unwrap()
5621 .to_string();
5622
5623 assert_eq!(
5624 file_hv, buf_hv,
5625 "file-path and buffer-path hash values must agree"
5626 );
5627 assert_eq!(file_hv.len(), 16, "xxh3 digest is 16 hex chars");
5628
5629 super::tgm_message_free(file_msg);
5630 super::tgm_message_free(buf_msg);
5631 super::tgm_file_close(file);
5632 let _ = std::fs::remove_file(&path);
5633 }
5634
5635 #[test]
5638 fn ffi_streaming_encoder_invalid_hash_algo() {
5639 let dir = std::env::temp_dir();
5640 let path = dir.join("ffi_streaming_bad_hash.tgm");
5641 let c_path = CString::new(path.to_str().unwrap()).unwrap();
5642 let meta_json = CString::new(r#"{"version":3}"#).unwrap();
5643 let bad_algo = CString::new("bogus_hash").unwrap();
5644
5645 let mut enc: *mut super::TgmStreamingEncoder = ptr::null_mut();
5646 let err = super::tgm_streaming_encoder_create(
5647 c_path.as_ptr(),
5648 meta_json.as_ptr(),
5649 bad_algo.as_ptr(),
5650 0,
5651 &mut enc,
5652 );
5653 assert!(matches!(err, super::TgmError::InvalidArg));
5654 let _ = std::fs::remove_file(&path);
5655 }
5656
5657 #[test]
5660 fn ffi_streaming_encoder_invalid_metadata() {
5661 let dir = std::env::temp_dir();
5662 let path = dir.join("ffi_streaming_bad_meta.tgm");
5663 let c_path = CString::new(path.to_str().unwrap()).unwrap();
5664 let bad_meta = CString::new("not json").unwrap();
5665
5666 let mut enc: *mut super::TgmStreamingEncoder = ptr::null_mut();
5667 let err = super::tgm_streaming_encoder_create(
5668 c_path.as_ptr(),
5669 bad_meta.as_ptr(),
5670 ptr::null(),
5671 0,
5672 &mut enc,
5673 );
5674 assert!(matches!(err, super::TgmError::Metadata));
5675 let _ = std::fs::remove_file(&path);
5676 }
5677
5678 #[test]
5681 fn ffi_encode_decode_multiple_objects() {
5682 let vals1 = [1.0f32, 2.0];
5683 let vals2 = [10.0f32, 20.0, 30.0];
5684 let bo = if cfg!(target_endian = "little") {
5685 "little"
5686 } else {
5687 "big"
5688 };
5689
5690 let json = format!(
5691 r#"{{"version":3,"descriptors":[{{"type":"ntensor","ndim":1,"shape":[{len1}],"strides":[1],"dtype":"float32","byte_order":"{bo}","encoding":"none","filter":"none","compression":"none"}},{{"type":"ntensor","ndim":1,"shape":[{len2}],"strides":[1],"dtype":"float32","byte_order":"{bo}","encoding":"none","filter":"none","compression":"none"}}]}}"#,
5692 len1 = vals1.len(),
5693 len2 = vals2.len(),
5694 bo = bo,
5695 );
5696
5697 let data1: Vec<u8> = vals1.iter().flat_map(|v| v.to_ne_bytes()).collect();
5698 let data2: Vec<u8> = vals2.iter().flat_map(|v| v.to_ne_bytes()).collect();
5699
5700 let c_json = CString::new(json).unwrap();
5701 let data_ptrs: [*const u8; 2] = [data1.as_ptr(), data2.as_ptr()];
5702 let data_lens: [usize; 2] = [data1.len(), data2.len()];
5703
5704 let mut out = super::TgmBytes {
5705 data: ptr::null_mut(),
5706 len: 0,
5707 };
5708 let err = super::tgm_encode(
5709 c_json.as_ptr(),
5710 data_ptrs.as_ptr(),
5711 data_lens.as_ptr(),
5712 2,
5713 ptr::null(),
5714 0,
5715 &mut out,
5716 );
5717 assert!(matches!(err, super::TgmError::Ok));
5718
5719 let encoded = unsafe { slice::from_raw_parts(out.data, out.len) }.to_vec();
5720 super::tgm_bytes_free(out);
5721
5722 let mut msg: *mut super::TgmMessage = ptr::null_mut();
5724 let err = super::tgm_decode(encoded.as_ptr(), encoded.len(), 0, 0, 0, &mut msg);
5725 assert!(matches!(err, super::TgmError::Ok));
5726 assert_eq!(super::tgm_message_num_objects(msg), 2);
5727
5728 let mut dl: usize = 0;
5730 let dp = super::tgm_object_data(msg, 0, &mut dl);
5731 let decoded0: Vec<f32> = unsafe { slice::from_raw_parts(dp, dl) }
5732 .chunks_exact(4)
5733 .map(|c| f32::from_ne_bytes(c.try_into().unwrap()))
5734 .collect();
5735 assert_eq!(decoded0, vals1);
5736
5737 let dp1 = super::tgm_object_data(msg, 1, &mut dl);
5739 let decoded1: Vec<f32> = unsafe { slice::from_raw_parts(dp1, dl) }
5740 .chunks_exact(4)
5741 .map(|c| f32::from_ne_bytes(c.try_into().unwrap()))
5742 .collect();
5743 assert_eq!(decoded1, vals2);
5744
5745 let shape0 = super::tgm_object_shape(msg, 0);
5747 assert_eq!(unsafe { *shape0 }, vals1.len() as u64);
5748 let shape1 = super::tgm_object_shape(msg, 1);
5749 assert_eq!(unsafe { *shape1 }, vals2.len() as u64);
5750
5751 super::tgm_message_free(msg);
5752 }
5753
5754 #[test]
5757 fn ffi_encode_decode_with_base_metadata() {
5758 let bo = if cfg!(target_endian = "little") {
5759 "little"
5760 } else {
5761 "big"
5762 };
5763 let json = format!(
5764 r#"{{"version":3,"base":[{{"param":"2t","level":"surface"}}],"descriptors":[{{"type":"ntensor","ndim":1,"shape":[2],"strides":[1],"dtype":"float32","byte_order":"{bo}","encoding":"none","filter":"none","compression":"none"}}]}}"#,
5765 bo = bo,
5766 );
5767
5768 let data: Vec<u8> = [1.0f32, 2.0].iter().flat_map(|v| v.to_ne_bytes()).collect();
5769 let c_json = CString::new(json).unwrap();
5770 let data_ptr: *const u8 = data.as_ptr();
5771 let data_len: usize = data.len();
5772
5773 let mut out = super::TgmBytes {
5774 data: ptr::null_mut(),
5775 len: 0,
5776 };
5777 let err = super::tgm_encode(
5778 c_json.as_ptr(),
5779 &data_ptr as *const *const u8,
5780 &data_len as *const usize,
5781 1,
5782 ptr::null(),
5783 0,
5784 &mut out,
5785 );
5786 assert!(matches!(err, super::TgmError::Ok));
5787
5788 let encoded = unsafe { slice::from_raw_parts(out.data, out.len) }.to_vec();
5789 super::tgm_bytes_free(out);
5790
5791 let mut meta: *mut super::TgmMetadata = ptr::null_mut();
5793 let err = super::tgm_decode_metadata(encoded.as_ptr(), encoded.len(), &mut meta);
5794 assert!(matches!(err, super::TgmError::Ok));
5795
5796 let key = CString::new("param").unwrap();
5797 let val_ptr = super::tgm_metadata_get_string(meta, key.as_ptr());
5798 assert!(!val_ptr.is_null());
5799 let val_str = unsafe { CStr::from_ptr(val_ptr) }.to_str().unwrap();
5800 assert_eq!(val_str, "2t");
5801
5802 let key2 = CString::new("level").unwrap();
5803 let val_ptr2 = super::tgm_metadata_get_string(meta, key2.as_ptr());
5804 assert!(!val_ptr2.is_null());
5805 let val_str2 = unsafe { CStr::from_ptr(val_ptr2) }.to_str().unwrap();
5806 assert_eq!(val_str2, "surface");
5807
5808 super::tgm_metadata_free(meta);
5809 }
5810
5811 #[test]
5814 fn ffi_compute_hash_deterministic() {
5815 let data = b"deterministic hash test";
5816 let mut out1 = super::TgmBytes {
5817 data: ptr::null_mut(),
5818 len: 0,
5819 };
5820 let mut out2 = super::TgmBytes {
5821 data: ptr::null_mut(),
5822 len: 0,
5823 };
5824
5825 let err = super::tgm_compute_hash(data.as_ptr(), data.len(), ptr::null(), &mut out1);
5826 assert!(matches!(err, super::TgmError::Ok));
5827
5828 let err = super::tgm_compute_hash(data.as_ptr(), data.len(), ptr::null(), &mut out2);
5829 assert!(matches!(err, super::TgmError::Ok));
5830
5831 let hex1 = unsafe { slice::from_raw_parts(out1.data, out1.len) };
5832 let hex2 = unsafe { slice::from_raw_parts(out2.data, out2.len) };
5833 assert_eq!(hex1, hex2);
5834
5835 super::tgm_bytes_free(out1);
5836 super::tgm_bytes_free(out2);
5837 }
5838
5839 #[test]
5842 fn ffi_streaming_encoder_write_pre_encoded_round_trip() {
5843 let dir = std::env::temp_dir();
5844 let path = dir.join("ffi_streaming_pre_encoded.tgm");
5845 let _ = std::fs::remove_file(&path);
5846
5847 let c_path = CString::new(path.to_str().unwrap()).unwrap();
5848 let meta_json = CString::new(r#"{"version":3}"#).unwrap();
5849
5850 let mut enc: *mut super::TgmStreamingEncoder = ptr::null_mut();
5851 let err = super::tgm_streaming_encoder_create(
5852 c_path.as_ptr(),
5853 meta_json.as_ptr(),
5854 ptr::null(),
5855 0,
5856 &mut enc,
5857 );
5858 assert!(matches!(err, super::TgmError::Ok));
5859
5860 let values = [7.0f32, 8.0];
5861 let data: Vec<u8> = values.iter().flat_map(|v| v.to_ne_bytes()).collect();
5862 let desc_json = CString::new(format!(
5863 r#"{{"type":"ntensor","ndim":1,"shape":[{len}],"strides":[1],"dtype":"float32","byte_order":"{bo}","encoding":"none","filter":"none","compression":"none"}}"#,
5864 len = values.len(),
5865 bo = if cfg!(target_endian = "little") { "little" } else { "big" },
5866 )).unwrap();
5867
5868 let err = super::tgm_streaming_encoder_write_pre_encoded(
5869 enc,
5870 desc_json.as_ptr(),
5871 data.as_ptr(),
5872 data.len(),
5873 );
5874 assert!(matches!(err, super::TgmError::Ok));
5875
5876 let err = super::tgm_streaming_encoder_finish(enc);
5877 assert!(matches!(err, super::TgmError::Ok));
5878 super::tgm_streaming_encoder_free(enc);
5879
5880 let mut file: *mut super::TgmFile = ptr::null_mut();
5882 let err = super::tgm_file_open(c_path.as_ptr(), &mut file);
5883 assert!(matches!(err, super::TgmError::Ok));
5884
5885 let mut msg: *mut super::TgmMessage = ptr::null_mut();
5886 let err = super::tgm_file_decode_message(file, 0, 0, 0, 0, &mut msg);
5887 assert!(matches!(err, super::TgmError::Ok));
5888
5889 let mut dl: usize = 0;
5890 let dp = super::tgm_object_data(msg, 0, &mut dl);
5891 let decoded: Vec<f32> = unsafe { slice::from_raw_parts(dp, dl) }
5892 .chunks_exact(4)
5893 .map(|c| f32::from_ne_bytes(c.try_into().unwrap()))
5894 .collect();
5895 assert_eq!(decoded, values);
5896
5897 super::tgm_message_free(msg);
5898 super::tgm_file_close(file);
5899 let _ = std::fs::remove_file(&path);
5900 }
5901
5902 #[test]
5905 fn ffi_encode_invalid_hash_algo() {
5906 let json = CString::new(r#"{"version":3,"descriptors":[]}"#).unwrap();
5907 let bad_algo = CString::new("bogus").unwrap();
5908 let mut out = super::TgmBytes {
5909 data: ptr::null_mut(),
5910 len: 0,
5911 };
5912
5913 let err = super::tgm_encode(
5914 json.as_ptr(),
5915 ptr::null(),
5916 ptr::null(),
5917 0,
5918 bad_algo.as_ptr(),
5919 0,
5920 &mut out,
5921 );
5922 assert!(matches!(err, super::TgmError::InvalidArg));
5923 }
5924
5925 #[test]
5928 fn ffi_scan_entry_oob_returns_sentinel() {
5929 let encoded = ffi_encode_single_f32_tensor(&[1.0f32], "");
5930
5931 let mut result: *mut super::TgmScanResult = ptr::null_mut();
5932 let err = super::tgm_scan(encoded.as_ptr(), encoded.len(), &mut result);
5933 assert!(matches!(err, super::TgmError::Ok));
5934
5935 assert_eq!(super::tgm_scan_count(result), 1);
5936
5937 let good = super::tgm_scan_entry(result, 0);
5939 assert_eq!(good.offset, 0);
5940 assert!(good.length > 0);
5941
5942 let bad = super::tgm_scan_entry(result, 1);
5944 assert_eq!(bad.offset, usize::MAX);
5945 assert_eq!(bad.length, 0);
5946
5947 let err_ptr = super::tgm_last_error();
5949 assert!(!err_ptr.is_null());
5950 let err_str = unsafe { CStr::from_ptr(err_ptr) }.to_str().unwrap();
5951 assert!(
5952 err_str.contains("out of range"),
5953 "expected OOB error, got: {err_str}"
5954 );
5955
5956 super::tgm_scan_free(result);
5957 }
5958
5959 #[test]
5962 fn ffi_encode_zero_length_null_data_accepted() {
5963 let json = CString::new(
5966 r#"{"version":3,"descriptors":[{"type":"ntensor","ndim":1,"shape":[0],"strides":[1],"dtype":"float32","byte_order":"little","encoding":"none","filter":"none","compression":"none"}]}"#,
5967 )
5968 .unwrap();
5969 let data_ptrs: [*const u8; 1] = [ptr::null()];
5970 let data_lens: [usize; 1] = [0];
5971 let mut out = super::TgmBytes {
5972 data: ptr::null_mut(),
5973 len: 0,
5974 };
5975
5976 let err = super::tgm_encode(
5977 json.as_ptr(),
5978 data_ptrs.as_ptr(),
5979 data_lens.as_ptr(),
5980 1,
5981 ptr::null(), 0, &mut out,
5984 );
5985 assert!(
5986 matches!(err, super::TgmError::Ok),
5987 "encoding zero-length data with null pointer should succeed"
5988 );
5989 if !out.data.is_null() {
5990 super::tgm_bytes_free(out);
5991 }
5992 }
5993
5994 #[test]
5999 fn ffi_validate_null_out() {
6000 let err = super::tgm_validate(ptr::null(), 0, ptr::null(), 0, ptr::null_mut());
6001 assert!(matches!(err, super::TgmError::InvalidArg));
6002 }
6003
6004 #[test]
6005 fn ffi_validate_null_buf_with_nonzero_len() {
6006 let mut out = super::TgmBytes {
6007 data: ptr::null_mut(),
6008 len: 0,
6009 };
6010 let err = super::tgm_validate(ptr::null(), 42, ptr::null(), 0, &mut out);
6011 assert!(matches!(err, super::TgmError::InvalidArg));
6012 }
6013
6014 #[test]
6015 fn ffi_validate_empty_buffer_ok() {
6016 let mut out = super::TgmBytes {
6018 data: ptr::null_mut(),
6019 len: 0,
6020 };
6021 let err = super::tgm_validate(ptr::null(), 0, ptr::null(), 0, &mut out);
6022 assert!(matches!(err, super::TgmError::Ok));
6023 assert!(!out.data.is_null());
6024 assert!(out.len > 0);
6025 super::tgm_bytes_free(out);
6026 }
6027
6028 #[test]
6029 fn ffi_validate_valid_message_all_levels() {
6030 let encoded = ffi_encode_single_f32_tensor(&[1.0f32, 2.0, 3.0, 4.0], "");
6031 for level_str in &["quick", "checksum", "default", "full"] {
6032 let level = CString::new(*level_str).unwrap();
6033 let mut out = super::TgmBytes {
6034 data: ptr::null_mut(),
6035 len: 0,
6036 };
6037 let err =
6038 super::tgm_validate(encoded.as_ptr(), encoded.len(), level.as_ptr(), 0, &mut out);
6039 assert!(matches!(err, super::TgmError::Ok), "level {level_str}");
6040 super::tgm_bytes_free(out);
6041 }
6042 }
6043
6044 #[test]
6045 fn ffi_validate_canonical_flag() {
6046 let encoded = ffi_encode_single_f32_tensor(&[1.0f32], "");
6047 let mut out = super::TgmBytes {
6048 data: ptr::null_mut(),
6049 len: 0,
6050 };
6051 let err = super::tgm_validate(
6052 encoded.as_ptr(),
6053 encoded.len(),
6054 ptr::null(),
6055 1, &mut out,
6057 );
6058 assert!(matches!(err, super::TgmError::Ok));
6059 super::tgm_bytes_free(out);
6060 }
6061
6062 #[test]
6063 fn ffi_validate_invalid_level_string() {
6064 let encoded = ffi_encode_single_f32_tensor(&[1.0f32], "");
6065 let bogus = CString::new("bogus-level-name").unwrap();
6066 let mut out = super::TgmBytes {
6067 data: ptr::null_mut(),
6068 len: 0,
6069 };
6070 let err = super::tgm_validate(encoded.as_ptr(), encoded.len(), bogus.as_ptr(), 0, &mut out);
6071 assert!(matches!(err, super::TgmError::InvalidArg));
6072 }
6073
6074 #[test]
6075 fn ffi_validate_garbage_reports_issues() {
6076 let garbage = [0xDEu8; 100];
6077 let mut out = super::TgmBytes {
6078 data: ptr::null_mut(),
6079 len: 0,
6080 };
6081 let err = super::tgm_validate(garbage.as_ptr(), garbage.len(), ptr::null(), 0, &mut out);
6082 assert!(matches!(err, super::TgmError::Ok));
6083 let json = unsafe { slice::from_raw_parts(out.data, out.len) };
6084 let s = std::str::from_utf8(json).unwrap();
6085 assert!(s.contains("issues"));
6086 super::tgm_bytes_free(out);
6087 }
6088
6089 #[test]
6092 fn ffi_validate_file_null_args() {
6093 let mut out = super::TgmBytes {
6094 data: ptr::null_mut(),
6095 len: 0,
6096 };
6097 let err = super::tgm_validate_file(ptr::null(), ptr::null(), 0, &mut out);
6098 assert!(matches!(err, super::TgmError::InvalidArg));
6099 let path = CString::new("/tmp/x.tgm").unwrap();
6100 let err = super::tgm_validate_file(path.as_ptr(), ptr::null(), 0, ptr::null_mut());
6101 assert!(matches!(err, super::TgmError::InvalidArg));
6102 }
6103
6104 #[test]
6105 fn ffi_validate_file_nonexistent() {
6106 let path = CString::new("/nonexistent/path/to/missing-file.tgm").unwrap();
6107 let mut out = super::TgmBytes {
6108 data: ptr::null_mut(),
6109 len: 0,
6110 };
6111 let err = super::tgm_validate_file(path.as_ptr(), ptr::null(), 0, &mut out);
6112 assert!(matches!(err, super::TgmError::Io));
6113 }
6114
6115 #[test]
6116 fn ffi_validate_file_valid_round_trip() {
6117 use std::io::Write;
6118 let encoded = ffi_encode_single_f32_tensor(&[1.0f32, 2.0, 3.0], "");
6119 let tmp = std::env::temp_dir().join(format!(
6120 "tensogram-ffi-validate-file-{}.tgm",
6121 std::process::id(),
6122 ));
6123 std::fs::File::create(&tmp)
6124 .unwrap()
6125 .write_all(&encoded)
6126 .unwrap();
6127 let path = CString::new(tmp.to_str().unwrap()).unwrap();
6128 let mut out = super::TgmBytes {
6129 data: ptr::null_mut(),
6130 len: 0,
6131 };
6132 let err = super::tgm_validate_file(path.as_ptr(), ptr::null(), 0, &mut out);
6133 assert!(matches!(err, super::TgmError::Ok));
6134 super::tgm_bytes_free(out);
6135 let _ = std::fs::remove_file(&tmp);
6136 }
6137
6138 #[test]
6139 fn ffi_validate_file_invalid_level() {
6140 let path = CString::new("/tmp/dummy.tgm").unwrap();
6141 let level = CString::new("bogus").unwrap();
6142 let mut out = super::TgmBytes {
6143 data: ptr::null_mut(),
6144 len: 0,
6145 };
6146 let err = super::tgm_validate_file(path.as_ptr(), level.as_ptr(), 0, &mut out);
6147 assert!(matches!(err, super::TgmError::InvalidArg));
6148 }
6149
6150 #[test]
6162 fn ffi_validate_detects_tampered_payload() {
6163 let values = vec![1.0f32; 256];
6164 let encoded = ffi_encode_with_hash(&values);
6165 let mut tampered = encoded.clone();
6166 let pos = (tampered.len() * 75) / 100;
6169 tampered[pos] ^= 0xFF;
6170 tampered[pos + 1] ^= 0xFF;
6171
6172 let mut out = super::TgmBytes {
6173 data: ptr::null_mut(),
6174 len: 0,
6175 };
6176 let level = CString::new("checksum").unwrap();
6177 let err = super::tgm_validate(
6178 tampered.as_ptr(),
6179 tampered.len(),
6180 level.as_ptr(),
6181 0,
6182 &mut out,
6183 );
6184 assert!(matches!(err, super::TgmError::Ok));
6188 assert!(!out.data.is_null());
6189 let json_bytes = unsafe { slice::from_raw_parts(out.data, out.len) };
6190 let json = std::str::from_utf8(json_bytes).unwrap();
6191 assert!(
6192 json.contains("HashMismatch") || json.contains("hash mismatch"),
6193 "expected HashMismatch in validate report, got: {json}"
6194 );
6195 super::tgm_bytes_free(out);
6196 }
6197
6198 #[test]
6201 fn ffi_object_data_null_out_len_no_crash() {
6202 let encoded = ffi_encode_single_f32_tensor(&[1.0f32], "");
6203 let mut msg: *mut super::TgmMessage = ptr::null_mut();
6204 let err = super::tgm_decode(
6205 encoded.as_ptr(),
6206 encoded.len(),
6207 0,
6208 0,
6209 0, &mut msg,
6211 );
6212 assert!(matches!(err, super::TgmError::Ok));
6213 let data = super::tgm_object_data(msg, 0, ptr::null_mut());
6215 assert!(!data.is_null());
6216 super::tgm_message_free(msg);
6217 }
6218
6219 #[test]
6222 fn ffi_decode_range_on_compressed_without_offsets() {
6223 let json = CString::new(
6226 r#"{"version":3,"descriptors":[{"type":"ntensor","ndim":1,"shape":[100],"strides":[1],"dtype":"float32","byte_order":"little","encoding":"none","filter":"none","compression":"zstd"}]}"#,
6227 )
6228 .unwrap();
6229 let data: Vec<u8> = vec![0u8; 400];
6230 let data_ptr: *const u8 = data.as_ptr();
6231 let data_len = data.len();
6232 let mut out = super::TgmBytes {
6233 data: ptr::null_mut(),
6234 len: 0,
6235 };
6236 let err = super::tgm_encode(
6237 json.as_ptr(),
6238 &data_ptr as *const *const u8,
6239 &data_len as *const usize,
6240 1,
6241 ptr::null(),
6242 0,
6243 &mut out,
6244 );
6245 assert!(matches!(err, super::TgmError::Ok));
6246 let encoded = unsafe { slice::from_raw_parts(out.data, out.len) }.to_vec();
6247 super::tgm_bytes_free(out);
6248
6249 let range_offset: u64 = 10;
6251 let range_count: u64 = 20;
6252 let mut out_buf = super::TgmBytes {
6253 data: ptr::null_mut(),
6254 len: 0,
6255 };
6256 let mut out_count: usize = 0;
6257 let err = super::tgm_decode_range(
6258 encoded.as_ptr(),
6259 encoded.len(),
6260 0,
6261 &range_offset as *const u64,
6262 &range_count as *const u64,
6263 1,
6264 0,
6265 0,
6266 1,
6267 &mut out_buf,
6268 &mut out_count,
6269 );
6270 assert!(!matches!(err, super::TgmError::Ok));
6271 }
6272
6273 #[test]
6276 fn ffi_simple_packing_null_values() {
6277 let mut ref_val: f64 = 0.0;
6278 let mut bsf: i32 = 0;
6279 let err =
6280 super::tgm_simple_packing_compute_params(ptr::null(), 0, 16, 0, &mut ref_val, &mut bsf);
6281 assert!(matches!(err, super::TgmError::InvalidArg));
6282 }
6283
6284 #[test]
6285 fn ffi_simple_packing_null_out_ref() {
6286 let values: [f64; 3] = [1.0, 2.0, 3.0];
6287 let mut bsf: i32 = 0;
6288 let err = super::tgm_simple_packing_compute_params(
6289 values.as_ptr(),
6290 3,
6291 16,
6292 0,
6293 ptr::null_mut(),
6294 &mut bsf,
6295 );
6296 assert!(matches!(err, super::TgmError::InvalidArg));
6297 }
6298
6299 #[test]
6300 fn ffi_simple_packing_null_out_bsf() {
6301 let values: [f64; 3] = [1.0, 2.0, 3.0];
6302 let mut ref_val: f64 = 0.0;
6303 let err = super::tgm_simple_packing_compute_params(
6304 values.as_ptr(),
6305 3,
6306 16,
6307 0,
6308 &mut ref_val,
6309 ptr::null_mut(),
6310 );
6311 assert!(matches!(err, super::TgmError::InvalidArg));
6312 }
6313
6314 #[test]
6319 fn ffi_encode_with_options_null_mask_ptr() {
6320 let values = [1.0f32, 2.0, 3.0, 4.0];
6321 let data: Vec<u8> = values.iter().flat_map(|v| v.to_ne_bytes()).collect();
6322 let json = format!(
6323 r#"{{"version":3,"descriptors":[{{"type":"ntensor","ndim":1,"shape":[{}],"strides":[1],"dtype":"float32","byte_order":"{}","encoding":"none","filter":"none","compression":"none"}}]}}"#,
6324 values.len(),
6325 if cfg!(target_endian = "little") {
6326 "little"
6327 } else {
6328 "big"
6329 },
6330 );
6331 let c_json = CString::new(json).unwrap();
6332 let hash_algo = CString::new("xxh3").unwrap();
6333 let data_ptr: *const u8 = data.as_ptr();
6334 let data_len: usize = data.len();
6335
6336 let mut out = super::TgmBytes {
6337 data: ptr::null_mut(),
6338 len: 0,
6339 };
6340 let err = super::tgm_encode_with_options(
6341 c_json.as_ptr(),
6342 &data_ptr as *const *const u8,
6343 &data_len as *const usize,
6344 1,
6345 hash_algo.as_ptr(),
6346 0, ptr::null(), &mut out,
6349 );
6350 assert!(matches!(err, super::TgmError::Ok));
6351 assert!(!out.data.is_null() && out.len > 0);
6352 super::tgm_bytes_free(out);
6353 }
6354
6355 #[test]
6359 fn ffi_decode_with_options_null_mask_ptr() {
6360 let values = [1.0f32, 2.0];
6361 let encoded = ffi_encode_single_f32_tensor(&values, "");
6362
6363 let mut msg: *mut super::TgmMessage = ptr::null_mut();
6364 let err = super::tgm_decode_with_options(
6365 encoded.as_ptr(),
6366 encoded.len(),
6367 0,
6368 0,
6369 0, ptr::null(), &mut msg,
6372 );
6373 assert!(matches!(err, super::TgmError::Ok));
6374 assert!(!msg.is_null());
6375 super::tgm_message_free(msg);
6376 }
6377
6378 #[test]
6382 fn ffi_decode_with_options_explicit_restore_false() {
6383 let values = [1.0f32, 2.0];
6384 let encoded = ffi_encode_single_f32_tensor(&values, "");
6385
6386 let mask_opts = super::TgmDecodeMaskOptions {
6387 restore_non_finite: false,
6388 };
6389 let mut msg: *mut super::TgmMessage = ptr::null_mut();
6390 let err = super::tgm_decode_with_options(
6391 encoded.as_ptr(),
6392 encoded.len(),
6393 0,
6394 0,
6395 0, &mask_opts,
6397 &mut msg,
6398 );
6399 assert!(matches!(err, super::TgmError::Ok));
6400 super::tgm_message_free(msg);
6401 }
6402
6403 #[test]
6406 fn ffi_decode_with_options_null_out() {
6407 let err =
6408 super::tgm_decode_with_options(b"x".as_ptr(), 1, 0, 0, 0, ptr::null(), ptr::null_mut());
6409 assert!(matches!(err, super::TgmError::InvalidArg));
6410 let msg = unsafe { CStr::from_ptr(super::tgm_last_error()) }
6411 .to_str()
6412 .unwrap();
6413 assert!(msg.contains("null"), "expected null-arg msg, got: {msg}");
6414 }
6415
6416 #[test]
6419 fn ffi_doctor_to_json_returns_parseable_json() {
6420 let mut out = super::TgmBytes {
6421 data: ptr::null_mut(),
6422 len: 0,
6423 };
6424 let err = super::tgm_doctor_to_json(&mut out);
6425 assert!(matches!(err, super::TgmError::Ok));
6426 assert!(!out.data.is_null());
6427 assert!(out.len > 0);
6428
6429 let json_bytes = unsafe { slice::from_raw_parts(out.data, out.len) };
6430 let json_str = std::str::from_utf8(json_bytes).expect("doctor JSON is UTF-8");
6431 let parsed: serde_json::Value = serde_json::from_str(json_str).expect("doctor JSON parses");
6432
6433 let obj = parsed.as_object().expect("top-level object");
6435 for key in ["build", "features", "self_test"] {
6436 assert!(obj.contains_key(key), "missing key '{key}' in: {obj:?}");
6437 }
6438
6439 super::tgm_bytes_free(out);
6440 }
6441
6442 #[test]
6443 fn ffi_doctor_to_json_null_out() {
6444 let err = super::tgm_doctor_to_json(ptr::null_mut());
6445 assert!(matches!(err, super::TgmError::InvalidArg));
6446 let msg = unsafe { CStr::from_ptr(super::tgm_last_error()) }
6447 .to_str()
6448 .unwrap();
6449 assert!(msg.contains("null"), "expected null-arg msg, got: {msg}");
6450 }
6451}
6452
6453#[unsafe(no_mangle)]
6458pub extern "C" fn tgm_compute_hash(
6459 data: *const u8,
6460 data_len: usize,
6461 algo: *const c_char,
6462 out: *mut TgmBytes,
6463) -> TgmError {
6464 if data.is_null() || out.is_null() {
6465 set_last_error("null argument");
6466 return TgmError::InvalidArg;
6467 }
6468
6469 if !algo.is_null() {
6477 let s = match unsafe { CStr::from_ptr(algo) }.to_str() {
6478 Ok(s) => s,
6479 Err(_) => {
6480 set_last_error("invalid UTF-8 in algo");
6481 return TgmError::InvalidArg;
6482 }
6483 };
6484 if let Err(e) = parse_hash_name(Some(s)) {
6485 set_last_error(&e.to_string());
6486 return TgmError::InvalidArg;
6487 }
6488 }
6489
6490 let input = unsafe { slice::from_raw_parts(data, data_len) };
6491 let hex = tensogram::hash::compute_hash(input);
6492 let mut bytes = hex.into_bytes().into_boxed_slice().into_vec();
6494 let result = TgmBytes {
6495 data: bytes.as_mut_ptr(),
6496 len: bytes.len(),
6497 };
6498 std::mem::forget(bytes);
6499 unsafe {
6500 *out = result;
6501 }
6502 TgmError::Ok
6503}
6504
6505#[unsafe(no_mangle)]
6539pub extern "C" fn tgm_doctor_to_json(out: *mut TgmBytes) -> TgmError {
6540 if out.is_null() {
6541 set_last_error("null out pointer");
6542 return TgmError::InvalidArg;
6543 }
6544
6545 let report = tensogram::doctor::run_diagnostics();
6546 let json = match serde_json::to_string(&report) {
6547 Ok(s) => s,
6548 Err(e) => {
6549 set_last_error(&format!("failed to serialise doctor report: {e}"));
6550 return TgmError::Encoding;
6551 }
6552 };
6553
6554 let mut bytes = json.into_bytes().into_boxed_slice().into_vec();
6556 let result = TgmBytes {
6557 data: bytes.as_mut_ptr(),
6558 len: bytes.len(),
6559 };
6560 std::mem::forget(bytes); unsafe {
6562 *out = result;
6563 }
6564 TgmError::Ok
6565}
6566
6567pub struct TgmStreamingEncoder {
6573 inner: Option<StreamingEncoder<std::io::BufWriter<std::fs::File>>>,
6574}
6575
6576#[derive(serde::Deserialize)]
6584struct StreamingEncodeJson {
6585 #[serde(default)]
6586 version: Option<u16>,
6587 #[serde(default)]
6588 base: Vec<BTreeMap<String, serde_json::Value>>,
6589 #[serde(flatten)]
6590 extra: BTreeMap<String, serde_json::Value>,
6591}
6592
6593pub(crate) fn parse_streaming_metadata_json(json_str: &str) -> Result<GlobalMetadata, String> {
6595 let parsed: StreamingEncodeJson = serde_json::from_str(json_str)
6596 .map_err(|e| format!("failed to parse metadata JSON: {e}"))?;
6597
6598 let cbor_base: Vec<BTreeMap<String, ciborium::Value>> = parsed
6599 .base
6600 .into_iter()
6601 .map(|entry| {
6602 entry
6603 .into_iter()
6604 .map(|(k, v)| (k, json_to_cbor(v)))
6605 .collect()
6606 })
6607 .collect();
6608
6609 for (i, entry) in cbor_base.iter().enumerate() {
6611 if entry.contains_key(RESERVED_KEY) {
6612 return Err(format!(
6613 "base[{i}] must not contain '{RESERVED_KEY}' key — the encoder populates it"
6614 ));
6615 }
6616 }
6617
6618 let cbor_extra = merge_flattened_extras_with_version(parsed.extra, parsed.version)?;
6622 Ok(GlobalMetadata {
6623 base: cbor_base,
6624 extra: cbor_extra,
6625 ..Default::default()
6626 })
6627}
6628
6629#[unsafe(no_mangle)]
6643pub extern "C" fn tgm_streaming_encoder_create(
6644 path: *const c_char,
6645 metadata_json: *const c_char,
6646 hash_algo: *const c_char,
6647 threads: u32,
6648 out: *mut *mut TgmStreamingEncoder,
6649) -> TgmError {
6650 if path.is_null() || metadata_json.is_null() || out.is_null() {
6651 set_last_error("null argument");
6652 return TgmError::InvalidArg;
6653 }
6654
6655 let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
6656 Ok(s) => s,
6657 Err(e) => {
6658 set_last_error(&format!("invalid UTF-8 in path: {e}"));
6659 return TgmError::InvalidArg;
6660 }
6661 };
6662
6663 let json_str = match unsafe { CStr::from_ptr(metadata_json) }.to_str() {
6664 Ok(s) => s,
6665 Err(e) => {
6666 set_last_error(&format!("invalid UTF-8 in metadata_json: {e}"));
6667 return TgmError::InvalidArg;
6668 }
6669 };
6670
6671 let global_metadata = match parse_streaming_metadata_json(json_str) {
6672 Ok(m) => m,
6673 Err(e) => {
6674 set_last_error(&e);
6675 return TgmError::Metadata;
6676 }
6677 };
6678
6679 let hashing = match parse_hash_algo(hash_algo) {
6680 Ok(b) => b,
6681 Err((code, msg)) => {
6682 set_last_error(&msg);
6683 return code;
6684 }
6685 };
6686
6687 let file = match std::fs::File::create(path_str) {
6688 Ok(f) => f,
6689 Err(e) => {
6690 set_last_error(&e.to_string());
6691 return TgmError::Io;
6692 }
6693 };
6694
6695 let options = EncodeOptions {
6696 hashing,
6697 threads,
6698 ..Default::default()
6699 };
6700 let writer = std::io::BufWriter::new(file);
6701
6702 match StreamingEncoder::new(writer, &global_metadata, &options) {
6703 Ok(enc) => {
6704 let handle = Box::new(TgmStreamingEncoder { inner: Some(enc) });
6705 unsafe {
6706 *out = Box::into_raw(handle);
6707 }
6708 TgmError::Ok
6709 }
6710 Err(e) => {
6711 set_last_error(&e.to_string());
6712 to_error_code(&e)
6713 }
6714 }
6715}
6716
6717#[unsafe(no_mangle)]
6726pub extern "C" fn tgm_streaming_encoder_write_preceder(
6727 enc: *mut TgmStreamingEncoder,
6728 metadata_json: *const c_char,
6729) -> TgmError {
6730 if enc.is_null() || metadata_json.is_null() {
6731 set_last_error("null argument");
6732 return TgmError::InvalidArg;
6733 }
6734
6735 let json_str = match unsafe { CStr::from_ptr(metadata_json) }.to_str() {
6736 Ok(s) => s,
6737 Err(e) => {
6738 set_last_error(&format!("invalid UTF-8 in metadata_json: {e}"));
6739 return TgmError::InvalidArg;
6740 }
6741 };
6742
6743 let map: BTreeMap<String, ciborium::Value> =
6744 match serde_json::from_str::<serde_json::Value>(json_str) {
6745 Ok(serde_json::Value::Object(obj)) => {
6746 obj.into_iter().map(|(k, v)| (k, json_to_cbor(v))).collect()
6747 }
6748 Ok(_) => {
6749 set_last_error("metadata_json must be a JSON object");
6750 return TgmError::Metadata;
6751 }
6752 Err(e) => {
6753 set_last_error(&format!("failed to parse metadata JSON: {e}"));
6754 return TgmError::Metadata;
6755 }
6756 };
6757
6758 let encoder = unsafe { &mut *enc };
6759 match encoder.inner.as_mut() {
6760 Some(inner) => match inner.write_preceder(map) {
6761 Ok(()) => TgmError::Ok,
6762 Err(e) => {
6763 set_last_error(&e.to_string());
6764 to_error_code(&e)
6765 }
6766 },
6767 None => {
6768 set_last_error("streaming encoder already finished");
6769 TgmError::InvalidArg
6770 }
6771 }
6772}
6773
6774#[unsafe(no_mangle)]
6780pub extern "C" fn tgm_streaming_encoder_write(
6781 enc: *mut TgmStreamingEncoder,
6782 descriptor_json: *const c_char,
6783 data: *const u8,
6784 data_len: usize,
6785) -> TgmError {
6786 if enc.is_null() || descriptor_json.is_null() || data.is_null() {
6787 set_last_error("null argument");
6788 return TgmError::InvalidArg;
6789 }
6790
6791 let json_str = match unsafe { CStr::from_ptr(descriptor_json) }.to_str() {
6792 Ok(s) => s,
6793 Err(e) => {
6794 set_last_error(&format!("invalid UTF-8 in descriptor_json: {e}"));
6795 return TgmError::InvalidArg;
6796 }
6797 };
6798
6799 let descriptor: DataObjectDescriptor = match serde_json::from_str(json_str) {
6800 Ok(d) => d,
6801 Err(e) => {
6802 set_last_error(&format!("failed to parse descriptor JSON: {e}"));
6803 return TgmError::Metadata;
6804 }
6805 };
6806
6807 let data_slice = unsafe { slice::from_raw_parts(data, data_len) };
6808 let encoder = unsafe { &mut *enc };
6809
6810 match encoder.inner.as_mut() {
6811 Some(inner) => match inner.write_object(&descriptor, data_slice) {
6812 Ok(()) => TgmError::Ok,
6813 Err(e) => {
6814 set_last_error(&e.to_string());
6815 to_error_code(&e)
6816 }
6817 },
6818 None => {
6819 set_last_error("streaming encoder already finished");
6820 TgmError::InvalidArg
6821 }
6822 }
6823}
6824
6825#[unsafe(no_mangle)]
6845pub extern "C" fn tgm_streaming_encoder_write_pre_encoded(
6846 enc: *mut TgmStreamingEncoder,
6847 descriptor_json: *const c_char,
6848 data: *const u8,
6849 data_len: usize,
6850) -> TgmError {
6851 if enc.is_null() || descriptor_json.is_null() || data.is_null() {
6852 set_last_error("null argument");
6853 return TgmError::InvalidArg;
6854 }
6855
6856 let json_str = match unsafe { CStr::from_ptr(descriptor_json) }.to_str() {
6857 Ok(s) => s,
6858 Err(e) => {
6859 set_last_error(&format!("invalid UTF-8 in descriptor_json: {e}"));
6860 return TgmError::InvalidArg;
6861 }
6862 };
6863
6864 let descriptor: DataObjectDescriptor = match serde_json::from_str(json_str) {
6865 Ok(d) => d,
6866 Err(e) => {
6867 set_last_error(&format!("failed to parse descriptor JSON: {e}"));
6868 return TgmError::Metadata;
6869 }
6870 };
6871
6872 let data_slice = unsafe { slice::from_raw_parts(data, data_len) };
6873 let encoder = unsafe { &mut *enc };
6874
6875 match encoder.inner.as_mut() {
6876 Some(inner) => match inner.write_object_pre_encoded(&descriptor, data_slice) {
6877 Ok(()) => TgmError::Ok,
6878 Err(e) => {
6879 set_last_error(&e.to_string());
6880 to_error_code(&e)
6881 }
6882 },
6883 None => {
6884 set_last_error("streaming encoder already finished");
6885 TgmError::InvalidArg
6886 }
6887 }
6888}
6889
6890#[unsafe(no_mangle)]
6892pub extern "C" fn tgm_streaming_encoder_count(enc: *const TgmStreamingEncoder) -> usize {
6893 if enc.is_null() {
6894 return 0;
6895 }
6896 unsafe { (*enc).inner.as_ref().map(|e| e.object_count()).unwrap_or(0) }
6897}
6898
6899#[unsafe(no_mangle)]
6904pub extern "C" fn tgm_streaming_encoder_finish(enc: *mut TgmStreamingEncoder) -> TgmError {
6905 if enc.is_null() {
6906 set_last_error("null argument");
6907 return TgmError::InvalidArg;
6908 }
6909
6910 let encoder = unsafe { &mut *enc };
6911 match encoder.inner.take() {
6912 Some(inner) => match inner.finish() {
6913 Ok(_writer) => {
6914 TgmError::Ok
6917 }
6918 Err(e) => {
6919 set_last_error(&e.to_string());
6920 to_error_code(&e)
6921 }
6922 },
6923 None => {
6924 set_last_error("streaming encoder already finished");
6925 TgmError::InvalidArg
6926 }
6927 }
6928}
6929
6930#[unsafe(no_mangle)]
6932pub extern "C" fn tgm_streaming_encoder_free(enc: *mut TgmStreamingEncoder) {
6933 if !enc.is_null() {
6934 unsafe {
6935 drop(Box::from_raw(enc));
6936 }
6937 }
6938}
6939
6940fn parse_validate_options(
6946 level: *const c_char,
6947 check_canonical: i32,
6948) -> Result<ValidateOptions, (TgmError, String)> {
6949 let level_str = if level.is_null() {
6950 "default"
6951 } else {
6952 unsafe { CStr::from_ptr(level) }
6953 .to_str()
6954 .map_err(|_| (TgmError::InvalidArg, "invalid UTF-8 in level".to_string()))?
6955 };
6956
6957 let (max_level, checksum_only) = match level_str {
6958 "quick" => (ValidationLevel::Structure, false),
6959 "default" => (ValidationLevel::Integrity, false),
6960 "checksum" => (ValidationLevel::Integrity, true),
6961 "full" => (ValidationLevel::Fidelity, false),
6962 other => {
6963 return Err((
6964 TgmError::InvalidArg,
6965 format!(
6966 "unknown validation level: '{}', expected one of: quick, default, checksum, full",
6967 other
6968 ),
6969 ));
6970 }
6971 };
6972
6973 Ok(ValidateOptions {
6974 max_level,
6975 check_canonical: check_canonical != 0,
6976 checksum_only,
6977 })
6978}
6979
6980#[unsafe(no_mangle)]
6998pub extern "C" fn tgm_validate(
6999 buf: *const u8,
7000 buf_len: usize,
7001 level: *const c_char,
7002 check_canonical: i32,
7003 out: *mut TgmBytes,
7004) -> TgmError {
7005 if out.is_null() {
7006 set_last_error("null argument");
7007 return TgmError::InvalidArg;
7008 }
7009 if buf.is_null() && buf_len > 0 {
7011 set_last_error("null buf with non-zero buf_len");
7012 return TgmError::InvalidArg;
7013 }
7014
7015 let options = match parse_validate_options(level, check_canonical) {
7016 Ok(o) => o,
7017 Err((code, msg)) => {
7018 set_last_error(&msg);
7019 return code;
7020 }
7021 };
7022
7023 let data = if buf.is_null() {
7024 &[]
7025 } else {
7026 unsafe { slice::from_raw_parts(buf, buf_len) }
7027 };
7028 let report = validate_message(data, &options);
7029
7030 match serde_json::to_vec(&report) {
7031 Ok(json_bytes) => {
7032 let mut json_bytes = json_bytes.into_boxed_slice().into_vec();
7033 let result = TgmBytes {
7034 data: json_bytes.as_mut_ptr(),
7035 len: json_bytes.len(),
7036 };
7037 std::mem::forget(json_bytes);
7038 unsafe {
7039 *out = result;
7040 }
7041 TgmError::Ok
7042 }
7043 Err(e) => {
7044 set_last_error(&format!("JSON serialization failed: {e}"));
7045 TgmError::Encoding
7046 }
7047 }
7048}
7049
7050#[unsafe(no_mangle)]
7064pub extern "C" fn tgm_validate_file(
7065 path: *const c_char,
7066 level: *const c_char,
7067 check_canonical: i32,
7068 out: *mut TgmBytes,
7069) -> TgmError {
7070 if path.is_null() || out.is_null() {
7071 set_last_error("null argument");
7072 return TgmError::InvalidArg;
7073 }
7074
7075 let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
7076 Ok(s) => s,
7077 Err(e) => {
7078 set_last_error(&format!("invalid UTF-8 in path: {e}"));
7079 return TgmError::InvalidArg;
7080 }
7081 };
7082
7083 let options = match parse_validate_options(level, check_canonical) {
7084 Ok(o) => o,
7085 Err((code, msg)) => {
7086 set_last_error(&msg);
7087 return code;
7088 }
7089 };
7090
7091 let report = match core_validate_file(Path::new(path_str), &options) {
7092 Ok(r) => r,
7093 Err(e) => {
7094 set_last_error(&e.to_string());
7095 return TgmError::Io;
7096 }
7097 };
7098
7099 match serde_json::to_vec(&report) {
7100 Ok(json_bytes) => {
7101 let mut json_bytes = json_bytes.into_boxed_slice().into_vec();
7102 let result = TgmBytes {
7103 data: json_bytes.as_mut_ptr(),
7104 len: json_bytes.len(),
7105 };
7106 std::mem::forget(json_bytes);
7107 unsafe {
7108 *out = result;
7109 }
7110 TgmError::Ok
7111 }
7112 Err(e) => {
7113 set_last_error(&format!("JSON serialization failed: {e}"));
7114 TgmError::Encoding
7115 }
7116 }
7117}