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)]
1879pub extern "C" fn tgm_metadata_get_string(
1880 meta: *const TgmMetadata,
1881 key: *const c_char,
1882) -> *const c_char {
1883 if meta.is_null() || key.is_null() {
1884 return ptr::null();
1885 }
1886
1887 let key_str = match unsafe { CStr::from_ptr(key) }.to_str() {
1888 Ok(s) => s,
1889 Err(_) => return ptr::null(),
1890 };
1891
1892 let m = unsafe { &(*meta) };
1893 match lookup_string_key(&m.global_metadata, key_str) {
1894 Some(s) => cache_cstr(m, key_str.to_string(), || s),
1895 None => ptr::null(),
1896 }
1897}
1898
1899#[unsafe(no_mangle)]
1906pub extern "C" fn tgm_metadata_get_int(
1907 meta: *const TgmMetadata,
1908 key: *const c_char,
1909 default_val: i64,
1910) -> i64 {
1911 if meta.is_null() || key.is_null() {
1912 return default_val;
1913 }
1914
1915 let key_str = match unsafe { CStr::from_ptr(key) }.to_str() {
1916 Ok(s) => s,
1917 Err(_) => return default_val,
1918 };
1919
1920 let m = unsafe { &(*meta) };
1921 lookup_int_key(&m.global_metadata, key_str).unwrap_or(default_val)
1922}
1923
1924#[unsafe(no_mangle)]
1928pub extern "C" fn tgm_metadata_get_float(
1929 meta: *const TgmMetadata,
1930 key: *const c_char,
1931 default_val: f64,
1932) -> f64 {
1933 if meta.is_null() || key.is_null() {
1934 return default_val;
1935 }
1936
1937 let key_str = match unsafe { CStr::from_ptr(key) }.to_str() {
1938 Ok(s) => s,
1939 Err(_) => return default_val,
1940 };
1941
1942 let m = unsafe { &(*meta) };
1943 lookup_float_key(&m.global_metadata, key_str).unwrap_or(default_val)
1944}
1945
1946#[unsafe(no_mangle)]
1968pub extern "C" fn tgm_metadata_get_string_at(
1969 meta: *const TgmMetadata,
1970 obj_index: usize,
1971 key: *const c_char,
1972) -> *const c_char {
1973 if meta.is_null() || key.is_null() {
1974 return ptr::null();
1975 }
1976 let key_str = match unsafe { CStr::from_ptr(key) }.to_str() {
1977 Ok(s) => s,
1978 Err(_) => return ptr::null(),
1979 };
1980 let m = unsafe { &(*meta) };
1981 let Some(entry) = m.global_metadata.base.get(obj_index) else {
1982 return ptr::null();
1983 };
1984 match lookup_cbor_in_object(entry, key_str).and_then(cbor_as_string) {
1985 Some(s) => cache_cstr(m, format!("{obj_index}\0{key_str}"), || s),
1986 None => ptr::null(),
1987 }
1988}
1989
1990#[unsafe(no_mangle)]
1995pub extern "C" fn tgm_metadata_get_int_at(
1996 meta: *const TgmMetadata,
1997 obj_index: usize,
1998 key: *const c_char,
1999 default_val: i64,
2000) -> i64 {
2001 if meta.is_null() || key.is_null() {
2002 return default_val;
2003 }
2004 let key_str = match unsafe { CStr::from_ptr(key) }.to_str() {
2005 Ok(s) => s,
2006 Err(_) => return default_val,
2007 };
2008 let m = unsafe { &(*meta) };
2009 match m.global_metadata.base.get(obj_index) {
2010 Some(entry) => lookup_cbor_in_object(entry, key_str)
2011 .and_then(cbor_as_i64)
2012 .unwrap_or(default_val),
2013 None => default_val,
2014 }
2015}
2016
2017#[unsafe(no_mangle)]
2022pub extern "C" fn tgm_metadata_get_float_at(
2023 meta: *const TgmMetadata,
2024 obj_index: usize,
2025 key: *const c_char,
2026 default_val: f64,
2027) -> f64 {
2028 if meta.is_null() || key.is_null() {
2029 return default_val;
2030 }
2031 let key_str = match unsafe { CStr::from_ptr(key) }.to_str() {
2032 Ok(s) => s,
2033 Err(_) => return default_val,
2034 };
2035 let m = unsafe { &(*meta) };
2036 match m.global_metadata.base.get(obj_index) {
2037 Some(entry) => lookup_cbor_in_object(entry, key_str)
2038 .and_then(cbor_as_f64)
2039 .unwrap_or(default_val),
2040 None => default_val,
2041 }
2042}
2043
2044#[unsafe(no_mangle)]
2053pub extern "C" fn tgm_metadata_object_to_json(
2054 meta: *const TgmMetadata,
2055 obj_index: usize,
2056) -> *const c_char {
2057 if meta.is_null() {
2058 return ptr::null();
2059 }
2060 let m = unsafe { &(*meta) };
2061 let Some(entry) = m.global_metadata.base.get(obj_index) else {
2062 return ptr::null();
2063 };
2064 cache_cstr(m, format!("\0json\0{obj_index}"), || {
2065 cbor_map_to_json(entry).to_string()
2066 })
2067}
2068
2069#[unsafe(no_mangle)]
2074pub extern "C" fn tgm_metadata_to_json(meta: *const TgmMetadata) -> *const c_char {
2075 if meta.is_null() {
2076 return ptr::null();
2077 }
2078 let m = unsafe { &(*meta) };
2079 cache_cstr(m, "\0json".to_string(), || {
2080 let gm = &m.global_metadata;
2081 let mut root = serde_json::Map::new();
2082 if !gm.base.is_empty() {
2083 let base: Vec<serde_json::Value> = gm.base.iter().map(cbor_map_to_json).collect();
2084 root.insert("base".to_string(), serde_json::Value::Array(base));
2085 }
2086 if !gm.reserved.is_empty() {
2087 root.insert("_reserved_".to_string(), cbor_map_to_json(&gm.reserved));
2088 }
2089 if !gm.extra.is_empty() {
2090 root.insert("_extra_".to_string(), cbor_map_to_json(&gm.extra));
2091 }
2092 serde_json::Value::Object(root).to_string()
2093 })
2094}
2095
2096#[unsafe(no_mangle)]
2098pub extern "C" fn tgm_metadata_free(meta: *mut TgmMetadata) {
2099 if !meta.is_null() {
2100 unsafe {
2101 drop(Box::from_raw(meta));
2102 }
2103 }
2104}
2105
2106#[unsafe(no_mangle)]
2112pub extern "C" fn tgm_file_open(path: *const c_char, out: *mut *mut TgmFile) -> TgmError {
2113 if path.is_null() || out.is_null() {
2114 set_last_error("null argument");
2115 return TgmError::InvalidArg;
2116 }
2117
2118 let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
2119 Ok(s) => s,
2120 Err(e) => {
2121 set_last_error(&format!("invalid UTF-8 in path: {e}"));
2122 return TgmError::InvalidArg;
2123 }
2124 };
2125
2126 match TensogramFile::open(path_str) {
2127 Ok(file) => {
2128 let path_string = CString::new(path_str).unwrap_or_default();
2129 let handle = Box::new(TgmFile { file, path_string });
2130 unsafe {
2131 *out = Box::into_raw(handle);
2132 }
2133 TgmError::Ok
2134 }
2135 Err(e) => {
2136 set_last_error(&e.to_string());
2137 to_error_code(&e)
2138 }
2139 }
2140}
2141
2142#[unsafe(no_mangle)]
2144pub extern "C" fn tgm_file_create(path: *const c_char, out: *mut *mut TgmFile) -> TgmError {
2145 if path.is_null() || out.is_null() {
2146 set_last_error("null argument");
2147 return TgmError::InvalidArg;
2148 }
2149
2150 let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
2151 Ok(s) => s,
2152 Err(e) => {
2153 set_last_error(&format!("invalid UTF-8 in path: {e}"));
2154 return TgmError::InvalidArg;
2155 }
2156 };
2157
2158 match TensogramFile::create(path_str) {
2159 Ok(file) => {
2160 let path_string = CString::new(path_str).unwrap_or_default();
2161 let handle = Box::new(TgmFile { file, path_string });
2162 unsafe {
2163 *out = Box::into_raw(handle);
2164 }
2165 TgmError::Ok
2166 }
2167 Err(e) => {
2168 set_last_error(&e.to_string());
2169 to_error_code(&e)
2170 }
2171 }
2172}
2173
2174#[unsafe(no_mangle)]
2176pub extern "C" fn tgm_file_message_count(file: *mut TgmFile, out_count: *mut usize) -> TgmError {
2177 if file.is_null() || out_count.is_null() {
2178 set_last_error("null argument");
2179 return TgmError::InvalidArg;
2180 }
2181
2182 let f = unsafe { &(*file).file };
2183 match f.message_count() {
2184 Ok(count) => {
2185 unsafe {
2186 *out_count = count;
2187 }
2188 TgmError::Ok
2189 }
2190 Err(e) => {
2191 set_last_error(&e.to_string());
2192 to_error_code(&e)
2193 }
2194 }
2195}
2196
2197#[unsafe(no_mangle)]
2200pub extern "C" fn tgm_file_decode_message(
2201 file: *mut TgmFile,
2202 index: usize,
2203 native_byte_order: i32,
2204 threads: u32,
2205 verify_hash: i32,
2206 out: *mut *mut TgmMessage,
2207) -> TgmError {
2208 if file.is_null() || out.is_null() {
2209 set_last_error("null argument");
2210 return TgmError::InvalidArg;
2211 }
2212
2213 let f = unsafe { &(*file).file };
2214 let options = DecodeOptions {
2215 native_byte_order: native_byte_order != 0,
2216 threads,
2217 verify_hash: verify_hash != 0,
2218 ..Default::default()
2219 };
2220
2221 match f.decode_message(index, &options) {
2222 Ok((global_metadata, objects)) => {
2223 let inline_hashes = f
2231 .read_message(index)
2232 .ok()
2233 .and_then(|bytes| tensogram::framing::data_object_inline_hashes(&bytes).ok())
2234 .unwrap_or_default();
2235 let caches = build_message_caches(&objects, &inline_hashes);
2236 let msg = Box::new(TgmMessage {
2237 global_metadata,
2238 objects,
2239 dtype_strings: caches.dtype_strings,
2240 type_strings: caches.type_strings,
2241 byte_order_strings: caches.byte_order_strings,
2242 filter_strings: caches.filter_strings,
2243 compression_strings: caches.compression_strings,
2244 encoding_strings: caches.encoding_strings,
2245 hash_type_strings: caches.hash_type_strings,
2246 hash_value_strings: caches.hash_value_strings,
2247 });
2248 unsafe {
2249 *out = Box::into_raw(msg);
2250 }
2251 TgmError::Ok
2252 }
2253 Err(e) => {
2254 set_last_error(&e.to_string());
2255 to_error_code(&e)
2256 }
2257 }
2258}
2259
2260#[unsafe(no_mangle)]
2263pub extern "C" fn tgm_file_read_message(
2264 file: *mut TgmFile,
2265 index: usize,
2266 out: *mut TgmBytes,
2267) -> TgmError {
2268 if file.is_null() || out.is_null() {
2269 set_last_error("null argument");
2270 return TgmError::InvalidArg;
2271 }
2272
2273 let f = unsafe { &(*file).file };
2274
2275 match f.read_message(index) {
2276 Ok(bytes) => {
2277 let mut bytes = bytes.into_boxed_slice().into_vec();
2279 let result = TgmBytes {
2280 data: bytes.as_mut_ptr(),
2281 len: bytes.len(),
2282 };
2283 std::mem::forget(bytes);
2284 unsafe {
2285 *out = result;
2286 }
2287 TgmError::Ok
2288 }
2289 Err(e) => {
2290 set_last_error(&e.to_string());
2291 to_error_code(&e)
2292 }
2293 }
2294}
2295
2296#[unsafe(no_mangle)]
2298pub extern "C" fn tgm_file_append_raw(
2299 file: *mut TgmFile,
2300 buf: *const u8,
2301 buf_len: usize,
2302) -> TgmError {
2303 if file.is_null() || buf.is_null() {
2304 set_last_error("null argument");
2305 return TgmError::InvalidArg;
2306 }
2307
2308 let data = unsafe { slice::from_raw_parts(buf, buf_len) };
2309 let f = unsafe { &mut (*file).file };
2310
2311 use std::io::Write;
2313 let path = match f.path() {
2314 Some(p) => p.to_path_buf(),
2315 None => {
2316 set_last_error("append_raw not supported on remote files");
2317 return TgmError::Remote;
2318 }
2319 };
2320 let result = std::fs::OpenOptions::new()
2321 .create(true)
2322 .append(true)
2323 .open(&path)
2324 .and_then(|mut fh| fh.write_all(data));
2325
2326 match result {
2327 Ok(()) => {
2328 f.invalidate_offsets();
2329 TgmError::Ok
2330 }
2331 Err(e) => {
2332 set_last_error(&e.to_string());
2333 TgmError::Io
2334 }
2335 }
2336}
2337
2338#[unsafe(no_mangle)]
2341pub extern "C" fn tgm_file_path(file: *const TgmFile) -> *const c_char {
2342 if file.is_null() {
2343 return ptr::null();
2344 }
2345 unsafe { (*file).path_string.as_ptr() }
2346}
2347
2348#[unsafe(no_mangle)]
2353pub extern "C" fn tgm_file_append(
2354 file: *mut TgmFile,
2355 metadata_json: *const c_char,
2356 data_ptrs: *const *const u8,
2357 data_lens: *const usize,
2358 num_objects: usize,
2359 hash_algo: *const c_char,
2360 threads: u32,
2361) -> TgmError {
2362 if file.is_null() || metadata_json.is_null() {
2363 set_last_error("null argument");
2364 return TgmError::InvalidArg;
2365 }
2366
2367 let json_str = match unsafe { CStr::from_ptr(metadata_json) }.to_str() {
2368 Ok(s) => s,
2369 Err(e) => {
2370 set_last_error(&format!("invalid UTF-8 in metadata_json: {e}"));
2371 return TgmError::InvalidArg;
2372 }
2373 };
2374
2375 let parsed = match unsafe {
2376 parse_encode_args(
2377 json_str,
2378 data_ptrs,
2379 data_lens,
2380 num_objects,
2381 hash_algo,
2382 threads,
2383 )
2384 } {
2385 Ok(p) => p,
2386 Err((code, msg)) => {
2387 set_last_error(&msg);
2388 return code;
2389 }
2390 };
2391
2392 let pairs: Vec<(&DataObjectDescriptor, &[u8])> = parsed
2393 .descriptors
2394 .iter()
2395 .zip(parsed.data_slices.iter())
2396 .map(|(d, s)| (d, *s))
2397 .collect();
2398
2399 let f = unsafe { &mut (*file).file };
2400 match f.append(&parsed.global_metadata, &pairs, &parsed.options) {
2401 Ok(()) => TgmError::Ok,
2402 Err(e) => {
2403 set_last_error(&e.to_string());
2404 to_error_code(&e)
2405 }
2406 }
2407}
2408
2409#[unsafe(no_mangle)]
2411pub extern "C" fn tgm_file_close(file: *mut TgmFile) {
2412 if !file.is_null() {
2413 unsafe {
2414 drop(Box::from_raw(file));
2415 }
2416 }
2417}
2418
2419fn lookup_cbor_value<'a>(
2428 global_metadata: &'a GlobalMetadata,
2429 key: &str,
2430) -> Option<&'a ciborium::Value> {
2431 if key.is_empty() {
2432 return None;
2433 }
2434 let parts: Vec<&str> = key.split('.').collect();
2435
2436 if parts.is_empty() || parts[0].is_empty() {
2437 return None;
2438 }
2439 if parts[0] == "version" {
2440 return None; }
2442
2443 if parts[0] == "_extra_" || parts[0] == "extra" {
2445 if parts.len() > 1 {
2446 return resolve_in_btree(&global_metadata.extra, &parts[1..]);
2447 }
2448 return None;
2449 }
2450
2451 for entry in &global_metadata.base {
2453 if let Some(val) = resolve_in_btree_skip_reserved(entry, &parts) {
2454 return Some(val);
2455 }
2456 }
2457 resolve_in_btree(&global_metadata.extra, &parts)
2459}
2460
2461fn resolve_in_btree_skip_reserved<'a>(
2463 map: &'a BTreeMap<String, ciborium::Value>,
2464 parts: &[&str],
2465) -> Option<&'a ciborium::Value> {
2466 let (first, rest) = parts.split_first()?;
2467 if *first == RESERVED_KEY {
2468 return None;
2469 }
2470 let value = map.get(*first)?;
2471 resolve_cbor_path(value, rest)
2472}
2473
2474fn resolve_in_btree<'a>(
2476 map: &'a BTreeMap<String, ciborium::Value>,
2477 parts: &[&str],
2478) -> Option<&'a ciborium::Value> {
2479 let (first, rest) = parts.split_first()?;
2480 let value = map.get(*first)?;
2481 resolve_cbor_path(value, rest)
2482}
2483
2484fn resolve_cbor_path<'a>(
2489 value: &'a ciborium::Value,
2490 remaining: &[&str],
2491) -> Option<&'a ciborium::Value> {
2492 if remaining.is_empty() {
2493 return Some(value);
2494 }
2495 if let ciborium::Value::Map(entries) = value {
2496 for (k, v) in entries {
2497 if matches!(k, ciborium::Value::Text(s) if s == remaining[0]) {
2498 return resolve_cbor_path(v, &remaining[1..]);
2499 }
2500 }
2501 }
2502 None
2503}
2504
2505fn lookup_cbor_in_object<'a>(
2512 entry: &'a BTreeMap<String, ciborium::Value>,
2513 key: &str,
2514) -> Option<&'a ciborium::Value> {
2515 if key.is_empty() {
2516 return None;
2517 }
2518 let parts: Vec<&str> = key.split('.').collect();
2519 if parts.is_empty() || parts[0].is_empty() {
2520 return None;
2521 }
2522 resolve_in_btree_skip_reserved(entry, &parts)
2523}
2524
2525fn cbor_as_string(v: &ciborium::Value) -> Option<String> {
2527 match v {
2528 ciborium::Value::Text(s) => Some(s.clone()),
2529 ciborium::Value::Integer(i) => {
2530 let n: i128 = (*i).into();
2531 Some(n.to_string())
2532 }
2533 ciborium::Value::Float(f) => Some(f.to_string()),
2534 ciborium::Value::Bool(b) => Some(b.to_string()),
2535 _ => None,
2536 }
2537}
2538
2539fn cbor_as_i64(v: &ciborium::Value) -> Option<i64> {
2541 match v {
2542 ciborium::Value::Integer(i) => {
2543 let n: i128 = (*i).into();
2544 i64::try_from(n).ok()
2545 }
2546 _ => None,
2547 }
2548}
2549
2550fn cbor_as_f64(v: &ciborium::Value) -> Option<f64> {
2552 match v {
2553 ciborium::Value::Float(f) => Some(*f),
2554 ciborium::Value::Integer(i) => {
2555 let n: i128 = (*i).into();
2556 Some(n as f64)
2559 }
2560 _ => None,
2561 }
2562}
2563
2564fn lookup_string_key(global_metadata: &GlobalMetadata, key: &str) -> Option<String> {
2565 if key.is_empty() {
2566 return None;
2567 }
2568 if key == "version" {
2573 return Some(tensogram::WIRE_VERSION.to_string());
2574 }
2575
2576 lookup_cbor_value(global_metadata, key).and_then(cbor_as_string)
2577}
2578
2579fn lookup_int_key(global_metadata: &GlobalMetadata, key: &str) -> Option<i64> {
2580 if key == "version" {
2582 return Some(tensogram::WIRE_VERSION as i64);
2583 }
2584
2585 lookup_cbor_value(global_metadata, key).and_then(cbor_as_i64)
2586}
2587
2588fn lookup_float_key(global_metadata: &GlobalMetadata, key: &str) -> Option<f64> {
2589 lookup_cbor_value(global_metadata, key).and_then(cbor_as_f64)
2590}
2591
2592fn cbor_to_json(v: &ciborium::Value) -> serde_json::Value {
2601 use serde_json::Value as J;
2602 match v {
2603 ciborium::Value::Null => J::Null,
2604 ciborium::Value::Bool(b) => J::Bool(*b),
2605 ciborium::Value::Integer(i) => {
2606 let n: i128 = (*i).into();
2607 if let Ok(x) = i64::try_from(n) {
2608 J::from(x)
2609 } else if let Ok(x) = u64::try_from(n) {
2610 J::from(x)
2611 } else {
2612 J::String(n.to_string())
2613 }
2614 }
2615 ciborium::Value::Float(f) => serde_json::Number::from_f64(*f).map_or(J::Null, J::Number),
2616 ciborium::Value::Text(s) => J::String(s.clone()),
2617 ciborium::Value::Bytes(b) => {
2618 use std::fmt::Write as _;
2620 let mut hex = String::with_capacity(b.len() * 2);
2621 for byte in b {
2622 let _ = write!(hex, "{byte:02x}");
2623 }
2624 J::String(hex)
2625 }
2626 ciborium::Value::Array(a) => J::Array(a.iter().map(cbor_to_json).collect()),
2627 ciborium::Value::Map(m) => {
2628 let mut obj = serde_json::Map::with_capacity(m.len());
2629 for (k, val) in m {
2630 let key = match k {
2631 ciborium::Value::Text(s) => s.clone(),
2632 other => format!("{other:?}"),
2633 };
2634 obj.insert(key, cbor_to_json(val));
2635 }
2636 J::Object(obj)
2637 }
2638 _ => J::Null,
2640 }
2641}
2642
2643fn cbor_map_to_json(map: &BTreeMap<String, ciborium::Value>) -> serde_json::Value {
2647 serde_json::Value::Object(
2648 map.iter()
2649 .map(|(k, v)| (k.clone(), cbor_to_json(v)))
2650 .collect(),
2651 )
2652}
2653
2654fn cache_cstr(m: &TgmMetadata, key: String, build: impl FnOnce() -> String) -> *const c_char {
2675 use std::collections::btree_map::Entry;
2676 match m.cache.borrow_mut().entry(key) {
2677 Entry::Occupied(e) => e.into_mut().as_ptr(),
2678 Entry::Vacant(slot) => match CString::new(build()) {
2679 Ok(cs) => slot.insert(cs).as_ptr(),
2680 Err(_) => ptr::null(),
2681 },
2682 }
2683}
2684
2685#[unsafe(no_mangle)]
2694pub extern "C" fn tgm_simple_packing_compute_params(
2695 values: *const f64,
2696 num_values: usize,
2697 bits_per_value: u32,
2698 decimal_scale_factor: i32,
2699 out_reference_value: *mut f64,
2700 out_binary_scale_factor: *mut i32,
2701) -> TgmError {
2702 if values.is_null() || out_reference_value.is_null() || out_binary_scale_factor.is_null() {
2703 set_last_error("null argument");
2704 return TgmError::InvalidArg;
2705 }
2706
2707 let vals = unsafe { slice::from_raw_parts(values, num_values) };
2708
2709 match tensogram_encodings::simple_packing::compute_params(
2710 vals,
2711 bits_per_value,
2712 decimal_scale_factor,
2713 ) {
2714 Ok(params) => {
2715 unsafe {
2716 *out_reference_value = params.reference_value;
2717 *out_binary_scale_factor = params.binary_scale_factor;
2718 }
2719 TgmError::Ok
2720 }
2721 Err(e) => {
2722 set_last_error(&e.to_string());
2723 TgmError::Encoding
2724 }
2725 }
2726}
2727
2728pub struct TgmBufferIter {
2736 offsets: Vec<(usize, usize)>,
2737 buf_ptr: *const u8,
2738 pos: usize,
2739}
2740
2741#[unsafe(no_mangle)]
2746pub extern "C" fn tgm_buffer_iter_create(
2747 buf: *const u8,
2748 buf_len: usize,
2749 out: *mut *mut TgmBufferIter,
2750) -> TgmError {
2751 if buf.is_null() || out.is_null() {
2752 set_last_error("null argument");
2753 return TgmError::InvalidArg;
2754 }
2755 let data = unsafe { slice::from_raw_parts(buf, buf_len) };
2756 let offsets = scan(data);
2757 let iter = Box::new(TgmBufferIter {
2758 offsets,
2759 buf_ptr: buf,
2760 pos: 0,
2761 });
2762 unsafe {
2763 *out = Box::into_raw(iter);
2764 }
2765 TgmError::Ok
2766}
2767
2768#[unsafe(no_mangle)]
2770pub extern "C" fn tgm_buffer_iter_count(iter: *const TgmBufferIter) -> usize {
2771 if iter.is_null() {
2772 return 0;
2773 }
2774 unsafe { (*iter).offsets.len() }
2775}
2776
2777#[unsafe(no_mangle)]
2783pub extern "C" fn tgm_buffer_iter_next(
2784 iter: *mut TgmBufferIter,
2785 out_buf: *mut *const u8,
2786 out_len: *mut usize,
2787) -> TgmError {
2788 if iter.is_null() || out_buf.is_null() || out_len.is_null() {
2789 set_last_error("null argument");
2790 return TgmError::InvalidArg;
2791 }
2792 let it = unsafe { &mut *iter };
2793 if it.pos >= it.offsets.len() {
2794 return TgmError::EndOfIter;
2795 }
2796 let (offset, length) = it.offsets[it.pos];
2797 it.pos += 1;
2798 unsafe {
2799 *out_buf = it.buf_ptr.add(offset);
2800 *out_len = length;
2801 }
2802 TgmError::Ok
2803}
2804
2805#[unsafe(no_mangle)]
2807pub extern "C" fn tgm_buffer_iter_free(iter: *mut TgmBufferIter) {
2808 if !iter.is_null() {
2809 unsafe {
2810 drop(Box::from_raw(iter));
2811 }
2812 }
2813}
2814
2815pub struct TgmFileIter {
2817 inner: tensogram::FileMessageIter,
2818}
2819
2820#[unsafe(no_mangle)]
2825pub extern "C" fn tgm_file_iter_create(file: *mut TgmFile, out: *mut *mut TgmFileIter) -> TgmError {
2826 if file.is_null() || out.is_null() {
2827 set_last_error("null argument");
2828 return TgmError::InvalidArg;
2829 }
2830 let f = unsafe { &(*file).file };
2831 match f.iter() {
2832 Ok(inner) => {
2833 let iter = Box::new(TgmFileIter { inner });
2834 unsafe {
2835 *out = Box::into_raw(iter);
2836 }
2837 TgmError::Ok
2838 }
2839 Err(e) => {
2840 set_last_error(&e.to_string());
2841 to_error_code(&e)
2842 }
2843 }
2844}
2845
2846#[unsafe(no_mangle)]
2853pub extern "C" fn tgm_file_iter_next(iter: *mut TgmFileIter, out: *mut TgmBytes) -> TgmError {
2854 if iter.is_null() || out.is_null() {
2855 set_last_error("null argument");
2856 return TgmError::InvalidArg;
2857 }
2858 let it = unsafe { &mut (*iter).inner };
2859 match it.next() {
2860 None => TgmError::EndOfIter,
2861 Some(Err(e)) => {
2862 set_last_error(&e.to_string());
2863 to_error_code(&e)
2864 }
2865 Some(Ok(bytes)) => {
2866 let mut bytes = bytes.into_boxed_slice().into_vec();
2868 let result = TgmBytes {
2869 data: bytes.as_mut_ptr(),
2870 len: bytes.len(),
2871 };
2872 std::mem::forget(bytes);
2873 unsafe {
2874 *out = result;
2875 }
2876 TgmError::Ok
2877 }
2878 }
2879}
2880
2881#[unsafe(no_mangle)]
2883pub extern "C" fn tgm_file_iter_free(iter: *mut TgmFileIter) {
2884 if !iter.is_null() {
2885 unsafe {
2886 drop(Box::from_raw(iter));
2887 }
2888 }
2889}
2890
2891pub struct TgmObjectIter {
2893 inner: tensogram::ObjectIter,
2894 global_metadata: GlobalMetadata,
2897}
2898
2899#[unsafe(no_mangle)]
2905pub extern "C" fn tgm_object_iter_create(
2906 buf: *const u8,
2907 buf_len: usize,
2908 native_byte_order: i32,
2909 verify_hash: i32,
2910 out: *mut *mut TgmObjectIter,
2911) -> TgmError {
2912 if buf.is_null() || out.is_null() {
2913 set_last_error("null argument");
2914 return TgmError::InvalidArg;
2915 }
2916 let data = unsafe { slice::from_raw_parts(buf, buf_len) };
2917 let options = DecodeOptions {
2918 native_byte_order: native_byte_order != 0,
2919 verify_hash: verify_hash != 0,
2920 ..Default::default()
2921 };
2922
2923 let global_metadata = decode_metadata(data).unwrap_or_default();
2926
2927 match tensogram::objects(data, options) {
2928 Ok(inner) => {
2929 let iter = Box::new(TgmObjectIter {
2930 inner,
2931 global_metadata,
2932 });
2933 unsafe {
2934 *out = Box::into_raw(iter);
2935 }
2936 TgmError::Ok
2937 }
2938 Err(e) => {
2939 set_last_error(&e.to_string());
2940 to_error_code(&e)
2941 }
2942 }
2943}
2944
2945#[unsafe(no_mangle)]
2952pub extern "C" fn tgm_object_iter_next(
2953 iter: *mut TgmObjectIter,
2954 out: *mut *mut TgmMessage,
2955) -> TgmError {
2956 if iter.is_null() || out.is_null() {
2957 set_last_error("null argument");
2958 return TgmError::InvalidArg;
2959 }
2960 let it = unsafe { &mut *iter };
2961 match it.inner.next() {
2962 None => TgmError::EndOfIter,
2963 Some(Err(e)) => {
2964 set_last_error(&e.to_string());
2965 to_error_code(&e)
2966 }
2967 Some(Ok((descriptor, data))) => {
2968 let global_metadata = it.global_metadata.clone();
2969 let objects = vec![(descriptor, data)];
2970 let caches = build_message_caches(&objects, &[]);
2979 let msg = Box::new(TgmMessage {
2980 global_metadata,
2981 objects,
2982 dtype_strings: caches.dtype_strings,
2983 type_strings: caches.type_strings,
2984 byte_order_strings: caches.byte_order_strings,
2985 filter_strings: caches.filter_strings,
2986 compression_strings: caches.compression_strings,
2987 encoding_strings: caches.encoding_strings,
2988 hash_type_strings: caches.hash_type_strings,
2989 hash_value_strings: caches.hash_value_strings,
2990 });
2991 unsafe {
2992 *out = Box::into_raw(msg);
2993 }
2994 TgmError::Ok
2995 }
2996 }
2997}
2998
2999#[unsafe(no_mangle)]
3001pub extern "C" fn tgm_object_iter_free(iter: *mut TgmObjectIter) {
3002 if !iter.is_null() {
3003 unsafe {
3004 drop(Box::from_raw(iter));
3005 }
3006 }
3007}
3008
3009#[unsafe(no_mangle)]
3019pub extern "C" fn tgm_error_string(err: TgmError) -> *const c_char {
3020 let code = err as i32;
3022 let s: &[u8] = match code {
3023 0 => b"ok\0",
3024 1 => b"framing error\0",
3025 2 => b"metadata error\0",
3026 3 => b"encoding error\0",
3027 4 => b"compression error\0",
3028 5 => b"object error\0",
3029 6 => b"I/O error\0",
3030 7 => b"hash mismatch\0",
3031 8 => b"invalid argument\0",
3032 9 => b"end of iteration\0",
3033 10 => b"remote error\0",
3034 11 => b"missing hash\0",
3035 12 => b"async task timed out\0",
3036 13 => b"async task cancelled\0",
3037 _ => b"unknown error\0",
3038 };
3039 s.as_ptr() as *const c_char
3040}
3041
3042#[cfg(test)]
3051mod tests {
3052 use super::*;
3053 use std::collections::BTreeMap;
3054
3055 fn make_meta(
3056 base: Vec<BTreeMap<String, ciborium::Value>>,
3057 extra: BTreeMap<String, ciborium::Value>,
3058 ) -> GlobalMetadata {
3059 GlobalMetadata {
3060 base,
3061 extra,
3062 ..Default::default()
3063 }
3064 }
3065
3066 #[test]
3069 fn lookup_cbor_empty_key() {
3070 let meta = make_meta(vec![], BTreeMap::new());
3071 assert!(lookup_cbor_value(&meta, "").is_none());
3072 }
3073
3074 #[test]
3075 fn lookup_cbor_dot_only() {
3076 let meta = make_meta(vec![], BTreeMap::new());
3077 assert!(lookup_cbor_value(&meta, ".").is_none());
3078 }
3079
3080 #[test]
3081 fn lookup_cbor_version_returns_none() {
3082 let meta = make_meta(vec![], BTreeMap::new());
3084 assert!(lookup_cbor_value(&meta, "version").is_none());
3085 }
3086
3087 #[test]
3088 fn lookup_cbor_base_match() {
3089 let mut entry = BTreeMap::new();
3090 entry.insert("centre".into(), ciborium::Value::Text("ecmwf".into()));
3091 let meta = make_meta(vec![entry], BTreeMap::new());
3092 let val = lookup_cbor_value(&meta, "centre");
3093 assert!(matches!(val, Some(ciborium::Value::Text(s)) if s == "ecmwf"));
3094 }
3095
3096 #[test]
3097 fn lookup_cbor_extra_fallback() {
3098 let mut extra = BTreeMap::new();
3100 extra.insert("source".into(), ciborium::Value::Text("test".into()));
3101 let meta = make_meta(vec![], extra);
3102 let val = lookup_cbor_value(&meta, "source");
3103 assert!(matches!(val, Some(ciborium::Value::Text(s)) if s == "test"));
3104 }
3105
3106 #[test]
3107 fn lookup_cbor_no_match() {
3108 let meta = make_meta(vec![], BTreeMap::new());
3109 assert!(lookup_cbor_value(&meta, "nonexistent").is_none());
3110 }
3111
3112 #[test]
3113 fn lookup_cbor_reserved_skipped() {
3114 let mut entry = BTreeMap::new();
3115 entry.insert(
3116 "_reserved_".into(),
3117 ciborium::Value::Map(vec![(
3118 ciborium::Value::Text("tensor".into()),
3119 ciborium::Value::Text("internal".into()),
3120 )]),
3121 );
3122 entry.insert("param".into(), ciborium::Value::Text("2t".into()));
3123 let meta = make_meta(vec![entry], BTreeMap::new());
3124 assert!(lookup_cbor_value(&meta, "_reserved_.tensor").is_none());
3126 assert!(lookup_cbor_value(&meta, "param").is_some());
3128 }
3129
3130 #[test]
3131 fn lookup_cbor_extra_prefix() {
3132 let mut extra = BTreeMap::new();
3133 extra.insert("custom".into(), ciborium::Value::Text("val".into()));
3134 let meta = make_meta(vec![], extra);
3135 let val = lookup_cbor_value(&meta, "_extra_.custom");
3137 assert!(matches!(val, Some(ciborium::Value::Text(s)) if s == "val"));
3138 }
3139
3140 #[test]
3141 fn lookup_cbor_extra_alias_prefix() {
3142 let mut extra = BTreeMap::new();
3143 extra.insert("custom".into(), ciborium::Value::Text("val".into()));
3144 let meta = make_meta(vec![], extra);
3145 let val = lookup_cbor_value(&meta, "extra.custom");
3147 assert!(matches!(val, Some(ciborium::Value::Text(s)) if s == "val"));
3148 }
3149
3150 #[test]
3151 fn lookup_cbor_extra_prefix_alone_returns_none() {
3152 let meta = make_meta(vec![], BTreeMap::new());
3153 assert!(lookup_cbor_value(&meta, "_extra_").is_none());
3155 assert!(lookup_cbor_value(&meta, "extra").is_none());
3156 }
3157
3158 #[test]
3159 fn lookup_cbor_base_wins_over_extra() {
3160 let mut entry = BTreeMap::new();
3161 entry.insert("shared".into(), ciborium::Value::Text("from_base".into()));
3162 let mut extra = BTreeMap::new();
3163 extra.insert("shared".into(), ciborium::Value::Text("from_extra".into()));
3164 let meta = make_meta(vec![entry], extra);
3165 let val = lookup_cbor_value(&meta, "shared");
3166 assert!(matches!(val, Some(ciborium::Value::Text(s)) if s == "from_base"));
3167 }
3168
3169 #[test]
3170 fn lookup_cbor_deeply_nested() {
3171 let e_val = ciborium::Value::Map(vec![(
3172 ciborium::Value::Text("e".into()),
3173 ciborium::Value::Text("deep".into()),
3174 )]);
3175 let d_val = ciborium::Value::Map(vec![(ciborium::Value::Text("d".into()), e_val)]);
3176 let c_val = ciborium::Value::Map(vec![(ciborium::Value::Text("c".into()), d_val)]);
3177 let b_val = ciborium::Value::Map(vec![(ciborium::Value::Text("b".into()), c_val)]);
3178 let mut entry = BTreeMap::new();
3179 entry.insert("a".into(), b_val);
3180 let meta = make_meta(vec![entry], BTreeMap::new());
3181 let val = lookup_cbor_value(&meta, "a.b.c.d.e");
3182 assert!(matches!(val, Some(ciborium::Value::Text(s)) if s == "deep"));
3183 }
3184
3185 #[test]
3186 fn lookup_cbor_multi_base_first_match() {
3187 let mut entry0 = BTreeMap::new();
3188 entry0.insert("param".into(), ciborium::Value::Text("2t".into()));
3189 let mut entry1 = BTreeMap::new();
3190 entry1.insert("param".into(), ciborium::Value::Text("msl".into()));
3191 let meta = make_meta(vec![entry0, entry1], BTreeMap::new());
3192 let val = lookup_cbor_value(&meta, "param");
3193 assert!(matches!(val, Some(ciborium::Value::Text(s)) if s == "2t"));
3194 }
3195
3196 #[test]
3199 fn resolve_cbor_path_empty_remaining() {
3200 let value = ciborium::Value::Text("hello".into());
3201 assert_eq!(resolve_cbor_path(&value, &[]), Some(&value));
3202 }
3203
3204 #[test]
3205 fn resolve_cbor_path_non_map_with_remaining() {
3206 let value = ciborium::Value::Text("hello".into());
3207 assert!(resolve_cbor_path(&value, &["key"]).is_none());
3208 }
3209
3210 #[test]
3211 fn resolve_cbor_path_map_missing_key() {
3212 let value = ciborium::Value::Map(vec![(
3213 ciborium::Value::Text("a".into()),
3214 ciborium::Value::Text("b".into()),
3215 )]);
3216 assert!(resolve_cbor_path(&value, &["missing"]).is_none());
3217 }
3218
3219 #[test]
3222 fn lookup_string_key_version() {
3223 let meta = make_meta(vec![], BTreeMap::new());
3224 assert_eq!(lookup_string_key(&meta, "version"), Some("3".into()));
3225 }
3226
3227 #[test]
3228 fn lookup_string_key_empty() {
3229 let meta = make_meta(vec![], BTreeMap::new());
3230 assert!(lookup_string_key(&meta, "").is_none());
3231 }
3232
3233 #[test]
3234 fn lookup_string_key_integer_value() {
3235 let mut entry = BTreeMap::new();
3236 entry.insert("count".into(), ciborium::Value::Integer(42.into()));
3237 let meta = make_meta(vec![entry], BTreeMap::new());
3238 assert_eq!(lookup_string_key(&meta, "count"), Some("42".into()));
3239 }
3240
3241 #[test]
3242 fn lookup_string_key_float_value() {
3243 let mut extra = BTreeMap::new();
3244 extra.insert("temperature".into(), ciborium::Value::Float(98.6));
3245 let meta = make_meta(vec![], extra);
3246 assert_eq!(lookup_string_key(&meta, "temperature"), Some("98.6".into()));
3247 }
3248
3249 #[test]
3250 fn lookup_string_key_bool_value() {
3251 let mut extra = BTreeMap::new();
3252 extra.insert("flag".into(), ciborium::Value::Bool(true));
3253 let meta = make_meta(vec![], extra);
3254 assert_eq!(lookup_string_key(&meta, "flag"), Some("true".into()));
3255 }
3256
3257 #[test]
3258 fn lookup_string_key_null_returns_none() {
3259 let mut extra = BTreeMap::new();
3260 extra.insert("nothing".into(), ciborium::Value::Null);
3261 let meta = make_meta(vec![], extra);
3262 assert!(lookup_string_key(&meta, "nothing").is_none());
3264 }
3265
3266 #[test]
3269 fn lookup_int_key_version() {
3270 let meta = make_meta(vec![], BTreeMap::new());
3271 assert_eq!(lookup_int_key(&meta, "version"), Some(3));
3272 }
3273
3274 #[test]
3275 fn lookup_int_key_non_integer() {
3276 let mut extra = BTreeMap::new();
3277 extra.insert("str".into(), ciborium::Value::Text("not_int".into()));
3278 let meta = make_meta(vec![], extra);
3279 assert!(lookup_int_key(&meta, "str").is_none());
3280 }
3281
3282 #[test]
3285 fn lookup_float_key_float() {
3286 let mut extra = BTreeMap::new();
3287 extra.insert("val".into(), ciborium::Value::Float(98.6));
3288 let meta = make_meta(vec![], extra);
3289 assert_eq!(lookup_float_key(&meta, "val"), Some(98.6));
3290 }
3291
3292 #[test]
3293 fn lookup_float_key_integer_coercion() {
3294 let mut extra = BTreeMap::new();
3295 extra.insert("count".into(), ciborium::Value::Integer(42.into()));
3296 let meta = make_meta(vec![], extra);
3297 assert_eq!(lookup_float_key(&meta, "count"), Some(42.0));
3298 }
3299
3300 #[test]
3301 fn lookup_float_key_non_numeric() {
3302 let mut extra = BTreeMap::new();
3303 extra.insert("str".into(), ciborium::Value::Text("hello".into()));
3304 let meta = make_meta(vec![], extra);
3305 assert!(lookup_float_key(&meta, "str").is_none());
3306 }
3307
3308 fn two_object_meta() -> GlobalMetadata {
3313 let mut e0 = BTreeMap::new();
3314 e0.insert("shortName".into(), ciborium::Value::Text("2t".into()));
3315 e0.insert("level".into(), ciborium::Value::Integer(0.into()));
3316 e0.insert(
3317 "_reserved_".into(),
3318 ciborium::Value::Map(vec![(
3319 ciborium::Value::Text("tensor".into()),
3320 ciborium::Value::Map(vec![(
3321 ciborium::Value::Text("dtype".into()),
3322 ciborium::Value::Text("float64".into()),
3323 )]),
3324 )]),
3325 );
3326
3327 let mut e1 = BTreeMap::new();
3328 e1.insert("shortName".into(), ciborium::Value::Text("msl".into()));
3329 e1.insert("level".into(), ciborium::Value::Integer(500.into()));
3330 e1.insert("scale".into(), ciborium::Value::Float(0.01));
3331 e1.insert(
3332 "geometry".into(),
3333 ciborium::Value::Map(vec![(
3334 ciborium::Value::Text("gridType".into()),
3335 ciborium::Value::Text("regular_ll".into()),
3336 )]),
3337 );
3338 make_meta(vec![e0, e1], BTreeMap::new())
3339 }
3340
3341 #[test]
3342 fn lookup_in_object_scopes_to_index() {
3343 let m = two_object_meta();
3344 assert!(matches!(
3345 lookup_cbor_in_object(&m.base[0], "shortName"),
3346 Some(ciborium::Value::Text(s)) if s == "2t"
3347 ));
3348 assert!(matches!(
3349 lookup_cbor_in_object(&m.base[1], "shortName"),
3350 Some(ciborium::Value::Text(s)) if s == "msl"
3351 ));
3352 }
3353
3354 #[test]
3355 fn lookup_in_object_nested_dot_path() {
3356 let m = two_object_meta();
3357 assert!(matches!(
3358 lookup_cbor_in_object(&m.base[1], "geometry.gridType"),
3359 Some(ciborium::Value::Text(s)) if s == "regular_ll"
3360 ));
3361 assert!(lookup_cbor_in_object(&m.base[0], "geometry.gridType").is_none());
3363 }
3364
3365 #[test]
3366 fn lookup_in_object_skips_reserved() {
3367 let m = two_object_meta();
3368 assert!(lookup_cbor_in_object(&m.base[0], "_reserved_.tensor.dtype").is_none());
3369 }
3370
3371 #[test]
3372 fn lookup_in_object_empty_key() {
3373 let m = two_object_meta();
3374 assert!(lookup_cbor_in_object(&m.base[0], "").is_none());
3375 }
3376
3377 fn make_handle(gm: GlobalMetadata) -> TgmMetadata {
3379 TgmMetadata {
3380 global_metadata: gm,
3381 cache: std::cell::RefCell::new(BTreeMap::new()),
3382 }
3383 }
3384
3385 #[test]
3386 fn get_string_at_reads_each_object() {
3387 let h = make_handle(two_object_meta());
3388 let hp = &h as *const TgmMetadata;
3389 let k = CString::new("shortName").unwrap();
3390 let s0 = tgm_metadata_get_string_at(hp, 0, k.as_ptr());
3391 let s1 = tgm_metadata_get_string_at(hp, 1, k.as_ptr());
3392 assert!(!s0.is_null() && !s1.is_null());
3393 let v0 = unsafe { CStr::from_ptr(s0) }.to_str().unwrap();
3394 let v1 = unsafe { CStr::from_ptr(s1) }.to_str().unwrap();
3395 assert_eq!((v0, v1), ("2t", "msl"));
3396 }
3397
3398 #[test]
3399 fn get_int_and_float_at() {
3400 let h = make_handle(two_object_meta());
3401 let hp = &h as *const TgmMetadata;
3402 let level = CString::new("level").unwrap();
3403 assert_eq!(tgm_metadata_get_int_at(hp, 0, level.as_ptr(), -1), 0);
3404 assert_eq!(tgm_metadata_get_int_at(hp, 1, level.as_ptr(), -1), 500);
3405 let scale = CString::new("scale").unwrap();
3406 assert_eq!(tgm_metadata_get_float_at(hp, 1, scale.as_ptr(), 0.0), 0.01);
3407 let missing = CString::new("nope").unwrap();
3409 assert_eq!(tgm_metadata_get_int_at(hp, 0, missing.as_ptr(), 42), 42);
3410 assert_eq!(tgm_metadata_get_int_at(hp, 1, scale.as_ptr(), 42), 42);
3411 }
3412
3413 #[test]
3414 fn get_at_out_of_range_and_null() {
3415 let h = make_handle(two_object_meta());
3416 let hp = &h as *const TgmMetadata;
3417 let k = CString::new("shortName").unwrap();
3418 assert!(tgm_metadata_get_string_at(hp, 9, k.as_ptr()).is_null());
3419 assert_eq!(tgm_metadata_get_int_at(hp, 9, k.as_ptr(), 7), 7);
3420 assert!(tgm_metadata_get_string_at(ptr::null(), 0, k.as_ptr()).is_null());
3421 }
3422
3423 #[test]
3424 fn object_to_json_roundtrips_via_serde() {
3425 let h = make_handle(two_object_meta());
3426 let hp = &h as *const TgmMetadata;
3427 let p = tgm_metadata_object_to_json(hp, 1);
3428 assert!(!p.is_null());
3429 let text = unsafe { CStr::from_ptr(p) }.to_str().unwrap();
3430 let v: serde_json::Value = serde_json::from_str(text).unwrap();
3431 assert_eq!(v["shortName"], "msl");
3432 assert_eq!(v["level"], 500);
3433 assert_eq!(v["geometry"]["gridType"], "regular_ll");
3434 assert!(tgm_metadata_object_to_json(hp, 9).is_null());
3436 }
3437
3438 #[test]
3439 fn to_json_has_base_array() {
3440 let h = make_handle(two_object_meta());
3441 let p = tgm_metadata_to_json(&h as *const TgmMetadata);
3442 let text = unsafe { CStr::from_ptr(p) }.to_str().unwrap();
3443 let v: serde_json::Value = serde_json::from_str(text).unwrap();
3444 assert_eq!(v["base"].as_array().unwrap().len(), 2);
3445 assert_eq!(v["base"][0]["shortName"], "2t");
3446 }
3447
3448 #[test]
3449 fn cbor_to_json_handles_awkward_kinds() {
3450 assert_eq!(
3452 cbor_to_json(&ciborium::Value::Bytes(vec![0xde, 0xad])),
3453 serde_json::Value::String("dead".into())
3454 );
3455 assert_eq!(
3456 cbor_to_json(&ciborium::Value::Float(f64::NAN)),
3457 serde_json::Value::Null
3458 );
3459 assert_eq!(
3461 cbor_to_json(&ciborium::Value::Integer(u64::MAX.into())),
3462 serde_json::Value::from(u64::MAX)
3463 );
3464 }
3465
3466 #[test]
3467 fn get_string_at_container_value_is_null() {
3468 let h = make_handle(two_object_meta());
3470 let k = CString::new("geometry").unwrap();
3471 assert!(tgm_metadata_get_string_at(&h as *const TgmMetadata, 1, k.as_ptr()).is_null());
3472 }
3473
3474 #[test]
3475 fn get_at_exact_boundary_index() {
3476 let h = make_handle(two_object_meta());
3478 let hp = &h as *const TgmMetadata;
3479 let k = CString::new("shortName").unwrap();
3480 assert!(tgm_metadata_get_string_at(hp, 2, k.as_ptr()).is_null());
3481 assert_eq!(tgm_metadata_get_int_at(hp, 2, k.as_ptr(), -1), -1);
3482 assert!(tgm_metadata_object_to_json(hp, 2).is_null());
3483 }
3484
3485 #[test]
3486 fn json_exporters_null_handle() {
3487 let k = CString::new("x").unwrap();
3488 assert!(tgm_metadata_object_to_json(ptr::null(), 0).is_null());
3489 assert!(tgm_metadata_to_json(ptr::null()).is_null());
3490 assert!(tgm_metadata_get_int_at(ptr::null(), 0, k.as_ptr(), 5) == 5);
3491 assert!(tgm_metadata_get_float_at(ptr::null(), 0, k.as_ptr(), 1.5) == 1.5);
3492 }
3493
3494 #[test]
3495 fn to_json_on_empty_metadata_is_empty_object() {
3496 let h = make_handle(make_meta(vec![], BTreeMap::new()));
3497 let p = tgm_metadata_to_json(&h as *const TgmMetadata);
3498 let text = unsafe { CStr::from_ptr(p) }.to_str().unwrap();
3499 assert_eq!(text, "{}");
3500 }
3501
3502 #[test]
3503 fn cache_cstr_interior_nul_returns_null() {
3504 let h = make_handle(make_meta(vec![], BTreeMap::new()));
3507 assert!(cache_cstr(&h, "k".to_string(), || "a\0b".to_string()).is_null());
3508 }
3509
3510 #[test]
3511 fn get_string_at_no_cross_object_bleed() {
3512 let h = make_handle(two_object_meta());
3514 let hp = &h as *const TgmMetadata;
3515 let k = CString::new("geometry.gridType").unwrap();
3516 assert!(tgm_metadata_get_string_at(hp, 0, k.as_ptr()).is_null());
3517 let s1 = tgm_metadata_get_string_at(hp, 1, k.as_ptr());
3518 assert_eq!(
3519 unsafe { CStr::from_ptr(s1) }.to_str().unwrap(),
3520 "regular_ll"
3521 );
3522 }
3523
3524 #[test]
3525 fn get_string_at_nul_value_returns_null() {
3526 let mut e = BTreeMap::new();
3529 e.insert("weird".into(), ciborium::Value::Text("a\0b".into()));
3530 let h = make_handle(make_meta(vec![e], BTreeMap::new()));
3531 let k = CString::new("weird").unwrap();
3532 assert!(tgm_metadata_get_string_at(&h as *const TgmMetadata, 0, k.as_ptr()).is_null());
3533 }
3534
3535 #[test]
3536 fn get_float_at_wrong_type_returns_default() {
3537 let h = make_handle(two_object_meta());
3539 let k = CString::new("shortName").unwrap();
3540 assert_eq!(
3541 tgm_metadata_get_float_at(&h as *const TgmMetadata, 0, k.as_ptr(), 9.5),
3542 9.5
3543 );
3544 }
3545
3546 #[test]
3547 fn object_to_json_serialises_arrays() {
3548 let mut e = BTreeMap::new();
3549 e.insert(
3550 "pl".into(),
3551 ciborium::Value::Array(vec![
3552 ciborium::Value::Integer(1.into()),
3553 ciborium::Value::Integer(2.into()),
3554 ]),
3555 );
3556 let h = make_handle(make_meta(vec![e], BTreeMap::new()));
3557 let p = tgm_metadata_object_to_json(&h as *const TgmMetadata, 0);
3558 let text = unsafe { CStr::from_ptr(p) }.to_str().unwrap();
3559 let v: serde_json::Value = serde_json::from_str(text).unwrap();
3560 assert_eq!(v["pl"], serde_json::json!([1, 2]));
3561 }
3562
3563 #[test]
3564 fn object_to_json_caches_pointer_across_calls() {
3565 let h = make_handle(two_object_meta());
3568 let hp = &h as *const TgmMetadata;
3569 let p1 = tgm_metadata_object_to_json(hp, 0);
3570 let p2 = tgm_metadata_object_to_json(hp, 0);
3571 assert!(!p1.is_null());
3572 assert_eq!(p1, p2);
3573 }
3574
3575 #[test]
3578 fn parse_encode_json_with_base() {
3579 let json = r#"{"version":3,"base":[{"mars":{"param":"2t"}}],"descriptors":[]}"#;
3580 let (gm, descs) = parse_encode_json(json).unwrap();
3581 assert_eq!(gm.base.len(), 1);
3582 assert!(gm.base[0].contains_key("mars"));
3583 assert!(descs.is_empty());
3584 }
3585
3586 #[test]
3587 fn parse_encode_json_legacy_version_routed_to_extra() {
3588 let json = r#"{"version":3,"descriptors":[]}"#;
3593 let (gm, _) = parse_encode_json(json).unwrap();
3594 assert_eq!(
3595 gm.extra.get("version"),
3596 Some(&ciborium::Value::Integer(3u64.into())),
3597 "legacy JSON `version` must round-trip via `_extra_`"
3598 );
3599 }
3600
3601 #[test]
3602 fn parse_encode_json_free_form_top_level_routed_to_extra() {
3603 let json = r#"{"source":"test","count":42,"descriptors":[]}"#;
3606 let (gm, _) = parse_encode_json(json).unwrap();
3607 assert_eq!(
3608 gm.extra.get("source"),
3609 Some(&ciborium::Value::Text("test".to_string()))
3610 );
3611 assert_eq!(
3612 gm.extra.get("count"),
3613 Some(&ciborium::Value::Integer(42u64.into()))
3614 );
3615 }
3616
3617 #[test]
3618 fn parse_encode_json_explicit_extra_unpacked() {
3619 let json = r#"{"_extra_":{"foo":"bar","count":7},"descriptors":[]}"#;
3626 let (gm, _) = parse_encode_json(json).unwrap();
3627 assert_eq!(
3628 gm.extra.get("foo"),
3629 Some(&ciborium::Value::Text("bar".to_string())),
3630 "explicit `_extra_.foo` must surface at the top level of `extra`"
3631 );
3632 assert_eq!(
3633 gm.extra.get("count"),
3634 Some(&ciborium::Value::Integer(7u64.into())),
3635 "explicit `_extra_.count` must surface at the top level of `extra`"
3636 );
3637 assert!(
3638 !gm.extra.contains_key("_extra_"),
3639 "there must be no nested `_extra_` key inside `extra`"
3640 );
3641 }
3642
3643 #[test]
3644 fn parse_encode_json_explicit_extra_beats_free_form() {
3645 let json = r#"{"version":99,"_extra_":{"version":1},"descriptors":[]}"#;
3649 let (gm, _) = parse_encode_json(json).unwrap();
3650 assert_eq!(
3651 gm.extra.get("version"),
3652 Some(&ciborium::Value::Integer(1u64.into())),
3653 "explicit _extra_.version must win over top-level version"
3654 );
3655 }
3656
3657 #[test]
3658 fn parse_encode_json_without_base() {
3659 let json = r#"{"version":3,"descriptors":[]}"#;
3660 let (gm, _) = parse_encode_json(json).unwrap();
3661 assert!(gm.base.is_empty());
3662 }
3663
3664 #[test]
3665 fn parse_encode_json_reserved_in_base_rejected() {
3666 let json = r#"{"version":3,"base":[{"_reserved_":{"tensor":{}}}],"descriptors":[]}"#;
3667 let result = parse_encode_json(json);
3668 assert!(result.is_err());
3669 assert!(result.unwrap_err().contains("_reserved_"));
3670 }
3671
3672 #[test]
3673 fn parse_encode_json_extra_keys() {
3674 let json = r#"{"version":3,"descriptors":[],"source":"test","count":42}"#;
3675 let (gm, _) = parse_encode_json(json).unwrap();
3676 assert!(gm.extra.contains_key("source"));
3677 assert!(gm.extra.contains_key("count"));
3678 }
3679
3680 #[test]
3683 fn parse_streaming_json_with_base() {
3684 let json = r#"{"version":3,"base":[{"mars":{"param":"2t"}}]}"#;
3685 let gm = parse_streaming_metadata_json(json).unwrap();
3686 assert_eq!(gm.base.len(), 1);
3687 }
3688
3689 #[test]
3690 fn parse_streaming_json_reserved_rejected() {
3691 let json = r#"{"version":3,"base":[{"_reserved_":{"tensor":{}}}]}"#;
3692 let result = parse_streaming_metadata_json(json);
3693 assert!(result.is_err());
3694 assert!(result.unwrap_err().contains("_reserved_"));
3695 }
3696
3697 #[test]
3698 fn parse_streaming_json_no_base() {
3699 let json = r#"{"version":3,"source":"stream"}"#;
3700 let gm = parse_streaming_metadata_json(json).unwrap();
3701 assert!(gm.base.is_empty());
3702 assert!(gm.extra.contains_key("source"));
3703 }
3704
3705 #[test]
3706 fn parse_streaming_json_explicit_extra_unpacked() {
3707 let json = r#"{"_extra_":{"foo":"bar"}}"#;
3710 let gm = parse_streaming_metadata_json(json).unwrap();
3711 assert_eq!(
3712 gm.extra.get("foo"),
3713 Some(&ciborium::Value::Text("bar".to_string()))
3714 );
3715 assert!(!gm.extra.contains_key("_extra_"));
3716 }
3717
3718 #[test]
3719 fn parse_streaming_json_explicit_extra_beats_free_form() {
3720 let json = r#"{"version":99,"_extra_":{"version":1}}"#;
3722 let gm = parse_streaming_metadata_json(json).unwrap();
3723 assert_eq!(
3724 gm.extra.get("version"),
3725 Some(&ciborium::Value::Integer(1u64.into()))
3726 );
3727 }
3728
3729 #[test]
3730 fn parse_streaming_json_invalid_json() {
3731 assert!(parse_streaming_metadata_json("not json").is_err());
3732 }
3733
3734 #[test]
3735 fn parse_encode_json_rejects_both_extra_aliases() {
3736 let json = r#"{"_extra_":{"a":1},"extra":{"b":2},"descriptors":[]}"#;
3739 let err = parse_encode_json(json).unwrap_err();
3740 assert!(
3741 err.contains("both '_extra_' and 'extra'"),
3742 "unexpected error: {err}"
3743 );
3744 }
3745
3746 #[test]
3747 fn parse_encode_json_rejects_non_object_extra() {
3748 let json = r#"{"_extra_":42,"descriptors":[]}"#;
3751 let err = parse_encode_json(json).unwrap_err();
3752 assert!(
3753 err.contains("'_extra_' must be a JSON object"),
3754 "unexpected error: {err}"
3755 );
3756 }
3757
3758 #[test]
3759 fn parse_encode_json_invalid_json() {
3760 assert!(parse_encode_json("not json").is_err());
3761 }
3762
3763 #[test]
3766 fn json_to_cbor_null() {
3767 assert_eq!(json_to_cbor(serde_json::Value::Null), ciborium::Value::Null);
3768 }
3769
3770 #[test]
3771 fn json_to_cbor_bool() {
3772 assert_eq!(
3773 json_to_cbor(serde_json::Value::Bool(true)),
3774 ciborium::Value::Bool(true)
3775 );
3776 }
3777
3778 #[test]
3779 fn json_to_cbor_integer() {
3780 let val = serde_json::json!(42);
3781 let cbor = json_to_cbor(val);
3782 assert!(matches!(cbor, ciborium::Value::Integer(_)));
3783 }
3784
3785 #[test]
3786 fn json_to_cbor_float() {
3787 let val = serde_json::json!(98.6);
3788 let cbor = json_to_cbor(val);
3789 assert!(matches!(cbor, ciborium::Value::Float(_)));
3790 }
3791
3792 #[test]
3793 fn json_to_cbor_string() {
3794 let val = serde_json::json!("hello");
3795 let cbor = json_to_cbor(val);
3796 assert!(matches!(cbor, ciborium::Value::Text(s) if s == "hello"));
3797 }
3798
3799 #[test]
3800 fn json_to_cbor_array() {
3801 let val = serde_json::json!([1, 2, 3]);
3802 let cbor = json_to_cbor(val);
3803 assert!(matches!(cbor, ciborium::Value::Array(_)));
3804 }
3805
3806 #[test]
3807 fn json_to_cbor_object() {
3808 let val = serde_json::json!({"key": "value"});
3809 let cbor = json_to_cbor(val);
3810 assert!(matches!(cbor, ciborium::Value::Map(_)));
3811 }
3812
3813 #[test]
3814 fn json_to_cbor_u64_fallback_to_float() {
3815 let val = serde_json::json!(18446744073709551615u64);
3818 let cbor = json_to_cbor(val);
3819 assert!(!matches!(cbor, ciborium::Value::Null));
3821 }
3822
3823 #[test]
3826 fn resolve_in_btree_skip_reserved_blocks_reserved() {
3827 let mut map = BTreeMap::new();
3828 map.insert("_reserved_".into(), ciborium::Value::Text("secret".into()));
3829 assert!(resolve_in_btree_skip_reserved(&map, &["_reserved_"]).is_none());
3830 }
3831
3832 #[test]
3833 fn resolve_in_btree_empty_parts() {
3834 let map = BTreeMap::new();
3835 assert!(resolve_in_btree(&map, &[]).is_none());
3836 }
3837
3838 #[test]
3839 fn resolve_in_btree_skip_reserved_empty_parts() {
3840 let map = BTreeMap::new();
3841 assert!(resolve_in_btree_skip_reserved(&map, &[]).is_none());
3842 }
3843
3844 #[test]
3847 fn parse_validate_options_default() {
3848 let opts = match super::parse_validate_options(ptr::null(), 0) {
3849 Ok(opts) => opts,
3850 Err((_code, msg)) => panic!("expected default options, got error: {msg}"),
3851 };
3852 assert_eq!(opts.max_level, ValidationLevel::Integrity);
3853 assert!(!opts.check_canonical);
3854 assert!(!opts.checksum_only);
3855 }
3856
3857 #[test]
3858 fn parse_validate_options_quick() {
3859 let level = CString::new("quick").unwrap();
3860 let opts = match super::parse_validate_options(level.as_ptr(), 0) {
3861 Ok(opts) => opts,
3862 Err((_code, msg)) => panic!("expected quick options, got error: {msg}"),
3863 };
3864 assert_eq!(opts.max_level, ValidationLevel::Structure);
3865 }
3866
3867 #[test]
3868 fn parse_validate_options_full_canonical() {
3869 let level = CString::new("full").unwrap();
3870 let opts = match super::parse_validate_options(level.as_ptr(), 1) {
3871 Ok(opts) => opts,
3872 Err((_code, msg)) => panic!("expected full options, got error: {msg}"),
3873 };
3874 assert_eq!(opts.max_level, ValidationLevel::Fidelity);
3875 assert!(opts.check_canonical);
3876 }
3877
3878 #[test]
3879 fn parse_validate_options_unknown_level() {
3880 let level = CString::new("bogus").unwrap();
3881 let result = super::parse_validate_options(level.as_ptr(), 0);
3882 assert!(result.is_err());
3883 }
3884
3885 #[test]
3886 fn parse_validate_options_checksum() {
3887 let level = CString::new("checksum").unwrap();
3888 let opts = match super::parse_validate_options(level.as_ptr(), 0) {
3889 Ok(opts) => opts,
3890 Err((_code, msg)) => panic!("expected checksum options, got error: {msg}"),
3891 };
3892 assert_eq!(opts.max_level, ValidationLevel::Integrity);
3893 assert!(opts.checksum_only);
3894 }
3895
3896 fn encode_test_message() -> Vec<u8> {
3899 let meta = GlobalMetadata::default();
3900 let desc = DataObjectDescriptor {
3901 obj_type: "ntensor".to_string(),
3902 ndim: 1,
3903 shape: vec![4],
3904 strides: vec![1],
3905 dtype: tensogram::Dtype::Float32,
3906 byte_order: tensogram::ByteOrder::native(),
3907 encoding: "none".to_string(),
3908 filter: "none".to_string(),
3909 compression: "none".to_string(),
3910 params: BTreeMap::new(),
3911 masks: None,
3912 };
3913 let data: Vec<u8> = [1.0f32, 2.0, 3.0, 4.0]
3914 .iter()
3915 .flat_map(|v| v.to_ne_bytes())
3916 .collect();
3917 tensogram::encode(&meta, &[(&desc, data.as_slice())], &Default::default()).unwrap()
3918 }
3919
3920 #[test]
3921 fn tgm_validate_valid_message() {
3922 let msg = encode_test_message();
3923 let mut out = super::TgmBytes {
3924 data: ptr::null_mut(),
3925 len: 0,
3926 };
3927 let err = super::tgm_validate(msg.as_ptr(), msg.len(), ptr::null(), 0, &mut out);
3928 assert!(matches!(err, super::TgmError::Ok));
3929 assert!(!out.data.is_null());
3930 assert!(out.len > 0);
3931 let json_str =
3932 unsafe { std::str::from_utf8(std::slice::from_raw_parts(out.data, out.len)).unwrap() };
3933 assert!(json_str.contains("\"issues\":[]"));
3934 assert!(json_str.contains("\"object_count\":1"));
3935 super::tgm_bytes_free(out);
3936 }
3937
3938 #[test]
3939 fn tgm_validate_empty_buffer() {
3940 let mut out = super::TgmBytes {
3941 data: ptr::null_mut(),
3942 len: 0,
3943 };
3944 let err = super::tgm_validate(ptr::null(), 0, ptr::null(), 0, &mut out);
3945 assert!(matches!(err, super::TgmError::Ok));
3946 let json_str =
3947 unsafe { std::str::from_utf8(std::slice::from_raw_parts(out.data, out.len)).unwrap() };
3948 assert!(json_str.contains("\"buffer_too_short\""));
3949 super::tgm_bytes_free(out);
3950 }
3951
3952 #[test]
3953 fn tgm_validate_invalid_level() {
3954 let msg = encode_test_message();
3955 let level = CString::new("bogus").unwrap();
3956 let mut out = super::TgmBytes {
3957 data: ptr::null_mut(),
3958 len: 0,
3959 };
3960 let err = super::tgm_validate(msg.as_ptr(), msg.len(), level.as_ptr(), 0, &mut out);
3961 assert!(matches!(err, super::TgmError::InvalidArg));
3962 }
3963
3964 #[test]
3965 fn tgm_validate_null_out() {
3966 let msg = encode_test_message();
3967 let err = super::tgm_validate(msg.as_ptr(), msg.len(), ptr::null(), 0, ptr::null_mut());
3968 assert!(matches!(err, super::TgmError::InvalidArg));
3969 }
3970
3971 #[test]
3972 fn tgm_validate_file_nonexistent() {
3973 let path = CString::new("/nonexistent/path/to/file.tgm").unwrap();
3974 let mut out = super::TgmBytes {
3975 data: ptr::null_mut(),
3976 len: 0,
3977 };
3978 let err = super::tgm_validate_file(path.as_ptr(), ptr::null(), 0, &mut out);
3979 assert!(matches!(err, super::TgmError::Io));
3980 }
3981
3982 #[test]
3983 fn tgm_validate_file_null_out() {
3984 let path = CString::new("/tmp/dummy.tgm").unwrap();
3985 let err = super::tgm_validate_file(path.as_ptr(), ptr::null(), 0, ptr::null_mut());
3986 assert!(matches!(err, super::TgmError::InvalidArg));
3987 }
3988
3989 #[test]
3990 fn tgm_validate_file_invalid_level() {
3991 let path = CString::new("/tmp/dummy.tgm").unwrap();
3992 let level = CString::new("bogus").unwrap();
3993 let mut out = super::TgmBytes {
3994 data: ptr::null_mut(),
3995 len: 0,
3996 };
3997 let err = super::tgm_validate_file(path.as_ptr(), level.as_ptr(), 0, &mut out);
3998 assert!(matches!(err, super::TgmError::InvalidArg));
3999 }
4000
4001 fn ffi_encode_single_f32_tensor(values: &[f32], extra_json: &str) -> Vec<u8> {
4008 let shape_str = format!("[{}]", values.len());
4009 let json = format!(
4010 r#"{{"version":3,"descriptors":[{{"type":"ntensor","ndim":1,"shape":{shape},"strides":[1],"dtype":"float32","byte_order":"{bo}","encoding":"none","filter":"none","compression":"none"}}]{extra}}}"#,
4011 shape = shape_str,
4012 bo = if cfg!(target_endian = "little") {
4013 "little"
4014 } else {
4015 "big"
4016 },
4017 extra = if extra_json.is_empty() {
4018 String::new()
4019 } else {
4020 format!(",{extra_json}")
4021 },
4022 );
4023
4024 let data: Vec<u8> = values.iter().flat_map(|v| v.to_ne_bytes()).collect();
4025 let c_json = CString::new(json).unwrap();
4026 let data_ptr: *const u8 = data.as_ptr();
4027 let data_len: usize = data.len();
4028
4029 let mut out = super::TgmBytes {
4030 data: ptr::null_mut(),
4031 len: 0,
4032 };
4033
4034 let err = super::tgm_encode(
4035 c_json.as_ptr(),
4036 &data_ptr as *const *const u8,
4037 &data_len as *const usize,
4038 1,
4039 ptr::null(), 0, &mut out,
4042 );
4043 assert!(matches!(err, super::TgmError::Ok), "tgm_encode failed");
4044 assert!(!out.data.is_null());
4045 assert!(out.len > 0);
4046
4047 let encoded = unsafe { slice::from_raw_parts(out.data, out.len) }.to_vec();
4048 super::tgm_bytes_free(out);
4049 encoded
4050 }
4051
4052 fn ffi_encode_with_hash(values: &[f32]) -> Vec<u8> {
4054 let shape_str = format!("[{}]", values.len());
4055 let json = format!(
4056 r#"{{"version":3,"descriptors":[{{"type":"ntensor","ndim":1,"shape":{shape},"strides":[1],"dtype":"float32","byte_order":"{bo}","encoding":"none","filter":"none","compression":"none"}}]}}"#,
4057 shape = shape_str,
4058 bo = if cfg!(target_endian = "little") {
4059 "little"
4060 } else {
4061 "big"
4062 },
4063 );
4064
4065 let data: Vec<u8> = values.iter().flat_map(|v| v.to_ne_bytes()).collect();
4066 let c_json = CString::new(json).unwrap();
4067 let hash_algo = CString::new("xxh3").unwrap();
4068 let data_ptr: *const u8 = data.as_ptr();
4069 let data_len: usize = data.len();
4070
4071 let mut out = super::TgmBytes {
4072 data: ptr::null_mut(),
4073 len: 0,
4074 };
4075
4076 let err = super::tgm_encode(
4077 c_json.as_ptr(),
4078 &data_ptr as *const *const u8,
4079 &data_len as *const usize,
4080 1,
4081 hash_algo.as_ptr(),
4082 0,
4083 &mut out,
4084 );
4085 assert!(
4086 matches!(err, super::TgmError::Ok),
4087 "tgm_encode with hash failed"
4088 );
4089
4090 let encoded = unsafe { slice::from_raw_parts(out.data, out.len) }.to_vec();
4091 super::tgm_bytes_free(out);
4092 encoded
4093 }
4094
4095 #[test]
4098 fn ffi_encode_decode_round_trip() {
4099 let values = [1.0f32, 2.0, 3.0, 4.0];
4100 let encoded = ffi_encode_single_f32_tensor(&values, "");
4101
4102 let mut msg: *mut super::TgmMessage = ptr::null_mut();
4104 let err = super::tgm_decode(
4105 encoded.as_ptr(),
4106 encoded.len(),
4107 0, 0, 0, &mut msg,
4111 );
4112 assert!(matches!(err, super::TgmError::Ok));
4113 assert!(!msg.is_null());
4114
4115 assert_eq!(super::tgm_message_version(msg), 3);
4117 assert_eq!(super::tgm_message_num_objects(msg), 1);
4118 assert_eq!(super::tgm_message_num_decoded(msg), 1);
4119
4120 assert_eq!(super::tgm_object_ndim(msg, 0), 1);
4122
4123 let shape_ptr = super::tgm_object_shape(msg, 0);
4124 assert!(!shape_ptr.is_null());
4125 assert_eq!(unsafe { *shape_ptr }, 4);
4126
4127 let strides_ptr = super::tgm_object_strides(msg, 0);
4128 assert!(!strides_ptr.is_null());
4129 assert_eq!(unsafe { *strides_ptr }, 1);
4130
4131 let dtype_ptr = super::tgm_object_dtype(msg, 0);
4133 assert!(!dtype_ptr.is_null());
4134 let dtype_str = unsafe { CStr::from_ptr(dtype_ptr) }.to_str().unwrap();
4135 assert_eq!(dtype_str, "float32");
4136
4137 let type_ptr = super::tgm_object_type(msg, 0);
4139 assert!(!type_ptr.is_null());
4140 let type_str = unsafe { CStr::from_ptr(type_ptr) }.to_str().unwrap();
4141 assert_eq!(type_str, "ntensor");
4142
4143 let bo_ptr = super::tgm_object_byte_order(msg, 0);
4145 assert!(!bo_ptr.is_null());
4146 let bo_str = unsafe { CStr::from_ptr(bo_ptr) }.to_str().unwrap();
4147 assert!(bo_str == "little" || bo_str == "big");
4148
4149 let filter_ptr = super::tgm_object_filter(msg, 0);
4151 assert!(!filter_ptr.is_null());
4152 let filter_str = unsafe { CStr::from_ptr(filter_ptr) }.to_str().unwrap();
4153 assert_eq!(filter_str, "none");
4154
4155 let comp_ptr = super::tgm_object_compression(msg, 0);
4157 assert!(!comp_ptr.is_null());
4158 let comp_str = unsafe { CStr::from_ptr(comp_ptr) }.to_str().unwrap();
4159 assert_eq!(comp_str, "none");
4160
4161 let enc_ptr = super::tgm_payload_encoding(msg, 0);
4163 assert!(!enc_ptr.is_null());
4164 let enc_str = unsafe { CStr::from_ptr(enc_ptr) }.to_str().unwrap();
4165 assert_eq!(enc_str, "none");
4166
4167 let mut data_len: usize = 0;
4169 let data_ptr = super::tgm_object_data(msg, 0, &mut data_len);
4170 assert!(!data_ptr.is_null());
4171 assert_eq!(data_len, 16); let decoded_bytes = unsafe { slice::from_raw_parts(data_ptr, data_len) };
4174 let decoded_values: Vec<f32> = decoded_bytes
4175 .chunks_exact(4)
4176 .map(|c| f32::from_ne_bytes(c.try_into().unwrap()))
4177 .collect();
4178 assert_eq!(decoded_values, values);
4179
4180 super::tgm_message_free(msg);
4181 }
4182
4183 #[test]
4184 fn ffi_encode_decode_with_hash() {
4185 let values = [10.0f32, 20.0, 30.0];
4186 let encoded = ffi_encode_with_hash(&values);
4187
4188 let mut msg: *mut super::TgmMessage = ptr::null_mut();
4189 let err = super::tgm_decode(
4190 encoded.as_ptr(),
4191 encoded.len(),
4192 0,
4193 0, 0, &mut msg,
4196 );
4197 assert!(matches!(err, super::TgmError::Ok));
4198
4199 assert_eq!(super::tgm_payload_has_hash(msg, 0), 1);
4201
4202 let ht_ptr = super::tgm_object_hash_type(msg, 0);
4203 assert!(!ht_ptr.is_null());
4204 let ht_str = unsafe { CStr::from_ptr(ht_ptr) }.to_str().unwrap();
4205 assert_eq!(ht_str, "xxh3");
4206
4207 let hv_ptr = super::tgm_object_hash_value(msg, 0);
4208 assert!(!hv_ptr.is_null());
4209 let hv_str = unsafe { CStr::from_ptr(hv_ptr) }.to_str().unwrap();
4210 assert!(!hv_str.is_empty());
4211
4212 super::tgm_message_free(msg);
4213 }
4214
4215 #[test]
4216 fn ffi_encode_decode_no_hash() {
4217 let values = [5.0f32];
4218 let encoded = ffi_encode_single_f32_tensor(&values, "");
4219
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 assert_eq!(super::tgm_payload_has_hash(msg, 0), 0);
4225 assert!(super::tgm_object_hash_type(msg, 0).is_null());
4226 assert!(super::tgm_object_hash_value(msg, 0).is_null());
4227
4228 super::tgm_message_free(msg);
4229 }
4230
4231 #[test]
4234 fn ffi_decode_verify_hash_succeeds_on_hashed_message() {
4235 let encoded = ffi_encode_with_hash(&[1.0f32, 2.0]);
4237 let mut msg: *mut super::TgmMessage = ptr::null_mut();
4238 let err = super::tgm_decode(
4239 encoded.as_ptr(),
4240 encoded.len(),
4241 0,
4242 0,
4243 1, &mut msg,
4245 );
4246 assert!(matches!(err, super::TgmError::Ok));
4247 assert!(!msg.is_null());
4248 super::tgm_message_free(msg);
4249 }
4250
4251 #[test]
4252 fn ffi_decode_verify_hash_returns_missing_hash_on_unhashed_message() {
4253 let encoded = ffi_encode_single_f32_tensor(&[5.0f32], "");
4255 let mut msg: *mut super::TgmMessage = ptr::null_mut();
4256 let err = super::tgm_decode(
4257 encoded.as_ptr(),
4258 encoded.len(),
4259 0,
4260 0,
4261 1, &mut msg,
4263 );
4264 assert!(
4265 matches!(err, super::TgmError::MissingHash),
4266 "expected MissingHash, got error code {}",
4267 err as i32
4268 );
4269 let last = unsafe { CStr::from_ptr(super::tgm_last_error()) }
4270 .to_str()
4271 .unwrap();
4272 assert!(
4273 last.contains("object 0"),
4274 "last error should name the offending object: {last}"
4275 );
4276 }
4278
4279 #[test]
4280 fn ffi_decode_verify_hash_returns_hash_mismatch_on_tampered_slot() {
4281 let mut encoded = ffi_encode_with_hash(&[10.0f32, 20.0, 30.0]);
4289 let frame_start = {
4294 let mut pos = 24usize;
4295 loop {
4296 assert!(pos + 16 <= encoded.len(), "frame not found");
4297 if &encoded[pos..pos + 2] == b"FR"
4298 && tensogram::wire::FrameHeader::read_from(&encoded[pos..])
4299 .map(|fh| fh.frame_type.is_data_object())
4300 .unwrap_or(false)
4301 {
4302 break pos;
4303 }
4304 pos += 1;
4305 }
4306 };
4307 let fh = tensogram::wire::FrameHeader::read_from(&encoded[frame_start..]).unwrap();
4308 let frame_end = frame_start + fh.total_length as usize;
4309 let slot_byte = frame_end - 12; encoded[slot_byte] ^= 0xFF;
4311
4312 let mut msg: *mut super::TgmMessage = ptr::null_mut();
4313 let err = super::tgm_decode(
4314 encoded.as_ptr(),
4315 encoded.len(),
4316 0,
4317 0,
4318 1, &mut msg,
4320 );
4321 assert!(
4322 matches!(err, super::TgmError::HashMismatch),
4323 "expected HashMismatch, got error code {}",
4324 err as i32
4325 );
4326 let last = unsafe { CStr::from_ptr(super::tgm_last_error()) }
4327 .to_str()
4328 .unwrap();
4329 assert!(
4330 last.contains("object 0"),
4331 "last error should name the offending object: {last}"
4332 );
4333 }
4334
4335 #[test]
4336 fn ffi_decode_verify_hash_off_silently_decodes_unhashed_message() {
4337 let encoded = ffi_encode_single_f32_tensor(&[5.0f32], "");
4342 let mut msg: *mut super::TgmMessage = ptr::null_mut();
4343 let err = super::tgm_decode(
4344 encoded.as_ptr(),
4345 encoded.len(),
4346 0,
4347 0,
4348 0, &mut msg,
4350 );
4351 assert!(matches!(err, super::TgmError::Ok));
4352 super::tgm_message_free(msg);
4353 }
4354
4355 #[test]
4356 fn ffi_decode_object_verify_hash_returns_missing_hash_on_unhashed() {
4357 let encoded = ffi_encode_single_f32_tensor(&[5.0f32], "");
4359 let mut msg: *mut super::TgmMessage = ptr::null_mut();
4360 let err = super::tgm_decode_object(
4361 encoded.as_ptr(),
4362 encoded.len(),
4363 0,
4364 0,
4365 0,
4366 1, &mut msg,
4368 );
4369 assert!(
4370 matches!(err, super::TgmError::MissingHash),
4371 "expected MissingHash, got error code {}",
4372 err as i32
4373 );
4374 }
4375
4376 #[test]
4377 fn ffi_encode_with_extra_metadata() {
4378 let values = [1.0f32, 2.0];
4379 let encoded = ffi_encode_single_f32_tensor(&values, r#""source":"test_source","count":42"#);
4380
4381 let mut msg: *mut super::TgmMessage = ptr::null_mut();
4382 let err = super::tgm_decode(encoded.as_ptr(), encoded.len(), 0, 0, 0, &mut msg);
4383 assert!(matches!(err, super::TgmError::Ok));
4384
4385 let mut meta: *mut super::TgmMetadata = ptr::null_mut();
4387 let err = super::tgm_message_metadata(msg, &mut meta);
4388 assert!(matches!(err, super::TgmError::Ok));
4389
4390 let key = CString::new("source").unwrap();
4391 let val_ptr = super::tgm_metadata_get_string(meta, key.as_ptr());
4392 assert!(!val_ptr.is_null());
4393 let val_str = unsafe { CStr::from_ptr(val_ptr) }.to_str().unwrap();
4394 assert_eq!(val_str, "test_source");
4395
4396 let key_count = CString::new("count").unwrap();
4397 let val_int = super::tgm_metadata_get_int(meta, key_count.as_ptr(), -1);
4398 assert_eq!(val_int, 42);
4399
4400 super::tgm_metadata_free(meta);
4401 super::tgm_message_free(msg);
4402 }
4403
4404 #[test]
4407 fn ffi_encode_null_json() {
4408 let mut out = super::TgmBytes {
4409 data: ptr::null_mut(),
4410 len: 0,
4411 };
4412 let err = super::tgm_encode(
4413 ptr::null(),
4414 ptr::null(),
4415 ptr::null(),
4416 0,
4417 ptr::null(),
4418 0,
4419 &mut out,
4420 );
4421 assert!(matches!(err, super::TgmError::InvalidArg));
4422 }
4423
4424 #[test]
4425 fn ffi_encode_null_out() {
4426 let json = CString::new(r#"{"version":3,"descriptors":[]}"#).unwrap();
4427 let err = super::tgm_encode(
4428 json.as_ptr(),
4429 ptr::null(),
4430 ptr::null(),
4431 0,
4432 ptr::null(),
4433 0,
4434 ptr::null_mut(),
4435 );
4436 assert!(matches!(err, super::TgmError::InvalidArg));
4437 }
4438
4439 #[test]
4440 fn ffi_encode_descriptor_count_mismatch() {
4441 let json = CString::new(r#"{"version":3,"descriptors":[]}"#).unwrap();
4443 let data: [u8; 4] = [0; 4];
4444 let data_ptr: *const u8 = data.as_ptr();
4445 let data_len: usize = 4;
4446 let mut out = super::TgmBytes {
4447 data: ptr::null_mut(),
4448 len: 0,
4449 };
4450 let err = super::tgm_encode(
4451 json.as_ptr(),
4452 &data_ptr as *const *const u8,
4453 &data_len as *const usize,
4454 1, ptr::null(),
4456 0, &mut out,
4458 );
4459 assert!(matches!(err, super::TgmError::InvalidArg));
4460 }
4461
4462 #[test]
4463 fn ffi_encode_invalid_json() {
4464 let json = CString::new("not valid json").unwrap();
4465 let mut out = super::TgmBytes {
4466 data: ptr::null_mut(),
4467 len: 0,
4468 };
4469 let err = super::tgm_encode(
4470 json.as_ptr(),
4471 ptr::null(),
4472 ptr::null(),
4473 0,
4474 ptr::null(),
4475 0,
4476 &mut out,
4477 );
4478 assert!(matches!(err, super::TgmError::Metadata));
4479 }
4480
4481 #[test]
4484 fn ffi_decode_null_buf() {
4485 let mut msg: *mut super::TgmMessage = ptr::null_mut();
4486 let err = super::tgm_decode(ptr::null(), 0, 0, 0, 0, &mut msg);
4487 assert!(matches!(err, super::TgmError::InvalidArg));
4488 }
4489
4490 #[test]
4491 fn ffi_decode_null_out() {
4492 let data = [0u8; 10];
4493 let err = super::tgm_decode(data.as_ptr(), data.len(), 0, 0, 0, ptr::null_mut());
4494 assert!(matches!(err, super::TgmError::InvalidArg));
4495 }
4496
4497 #[test]
4498 fn ffi_decode_garbage_data() {
4499 let data = [0u8; 10];
4500 let mut msg: *mut super::TgmMessage = ptr::null_mut();
4501 let err = super::tgm_decode(data.as_ptr(), data.len(), 0, 0, 0, &mut msg);
4502 assert!(!matches!(err, super::TgmError::Ok));
4504 }
4505
4506 #[test]
4509 fn ffi_decode_metadata_round_trip() {
4510 let values = [1.0f32, 2.0];
4511 let encoded = ffi_encode_single_f32_tensor(&values, r#""source":"meta_test""#);
4512
4513 let mut meta: *mut super::TgmMetadata = ptr::null_mut();
4514 let err = super::tgm_decode_metadata(encoded.as_ptr(), encoded.len(), &mut meta);
4515 assert!(matches!(err, super::TgmError::Ok));
4516 assert!(!meta.is_null());
4517
4518 assert_eq!(super::tgm_metadata_version(meta), 3);
4520
4521 assert_eq!(super::tgm_metadata_num_objects(meta), 1);
4523
4524 let key = CString::new("source").unwrap();
4526 let val_ptr = super::tgm_metadata_get_string(meta, key.as_ptr());
4527 assert!(!val_ptr.is_null());
4528 let val_str = unsafe { CStr::from_ptr(val_ptr) }.to_str().unwrap();
4529 assert_eq!(val_str, "meta_test");
4530
4531 let bad_key = CString::new("nonexistent").unwrap();
4533 assert!(super::tgm_metadata_get_string(meta, bad_key.as_ptr()).is_null());
4534
4535 let bad_key2 = CString::new("missing_int").unwrap();
4537 assert_eq!(
4538 super::tgm_metadata_get_int(meta, bad_key2.as_ptr(), -999),
4539 -999
4540 );
4541
4542 let bad_key3 = CString::new("missing_float").unwrap();
4544 let fval = super::tgm_metadata_get_float(meta, bad_key3.as_ptr(), 3.25);
4545 assert!((fval - 3.25).abs() < f64::EPSILON);
4546
4547 super::tgm_metadata_free(meta);
4548 }
4549
4550 #[test]
4551 fn ffi_decode_metadata_null_args() {
4552 let mut meta: *mut super::TgmMetadata = ptr::null_mut();
4553 let err = super::tgm_decode_metadata(ptr::null(), 0, &mut meta);
4554 assert!(matches!(err, super::TgmError::InvalidArg));
4555
4556 let data = [0u8; 10];
4557 let err = super::tgm_decode_metadata(data.as_ptr(), data.len(), ptr::null_mut());
4558 assert!(matches!(err, super::TgmError::InvalidArg));
4559 }
4560
4561 #[test]
4564 fn ffi_metadata_accessors_null_handle() {
4565 assert_eq!(super::tgm_metadata_version(ptr::null()), 0);
4566 assert_eq!(super::tgm_metadata_num_objects(ptr::null()), 0);
4567 assert!(super::tgm_metadata_get_string(ptr::null(), ptr::null()).is_null());
4568 assert_eq!(
4569 super::tgm_metadata_get_int(ptr::null(), ptr::null(), -1),
4570 -1
4571 );
4572 assert_eq!(
4573 super::tgm_metadata_get_float(ptr::null(), ptr::null(), 1.5),
4574 1.5
4575 );
4576 }
4577
4578 #[test]
4579 fn ffi_metadata_get_string_null_key() {
4580 let encoded = ffi_encode_single_f32_tensor(&[1.0f32], "");
4581 let mut meta: *mut super::TgmMetadata = ptr::null_mut();
4582 let err = super::tgm_decode_metadata(encoded.as_ptr(), encoded.len(), &mut meta);
4583 assert!(matches!(err, super::TgmError::Ok));
4584
4585 assert!(super::tgm_metadata_get_string(meta, ptr::null()).is_null());
4586 assert_eq!(super::tgm_metadata_get_int(meta, ptr::null(), -1), -1);
4587 assert_eq!(super::tgm_metadata_get_float(meta, ptr::null(), 1.5), 1.5);
4588
4589 super::tgm_metadata_free(meta);
4590 }
4591
4592 #[test]
4593 fn ffi_metadata_get_version_via_string() {
4594 let encoded = ffi_encode_single_f32_tensor(&[1.0f32], "");
4595 let mut meta: *mut super::TgmMetadata = ptr::null_mut();
4596 let err = super::tgm_decode_metadata(encoded.as_ptr(), encoded.len(), &mut meta);
4597 assert!(matches!(err, super::TgmError::Ok));
4598
4599 let key = CString::new("version").unwrap();
4600 let val_ptr = super::tgm_metadata_get_string(meta, key.as_ptr());
4601 assert!(!val_ptr.is_null());
4602 let val_str = unsafe { CStr::from_ptr(val_ptr) }.to_str().unwrap();
4603 assert_eq!(val_str, "3");
4604
4605 let ival = super::tgm_metadata_get_int(meta, key.as_ptr(), -1);
4606 assert_eq!(ival, 3);
4607
4608 super::tgm_metadata_free(meta);
4609 }
4610
4611 #[test]
4612 fn ffi_metadata_get_float_value() {
4613 let values = [1.0f32];
4614 let encoded = ffi_encode_single_f32_tensor(&values, r#""temperature":98.6"#);
4615
4616 let mut meta: *mut super::TgmMetadata = ptr::null_mut();
4617 let err = super::tgm_decode_metadata(encoded.as_ptr(), encoded.len(), &mut meta);
4618 assert!(matches!(err, super::TgmError::Ok));
4619
4620 let key = CString::new("temperature").unwrap();
4621 let fval = super::tgm_metadata_get_float(meta, key.as_ptr(), 0.0);
4622 assert!((fval - 98.6).abs() < 0.01);
4623
4624 super::tgm_metadata_free(meta);
4625 }
4626
4627 #[test]
4630 fn ffi_decode_object_round_trip() {
4631 let values = [10.0f32, 20.0, 30.0, 40.0];
4632 let encoded = ffi_encode_single_f32_tensor(&values, "");
4633
4634 let mut msg: *mut super::TgmMessage = ptr::null_mut();
4635 let err = super::tgm_decode_object(
4636 encoded.as_ptr(),
4637 encoded.len(),
4638 0, 0, 0, 0, &mut msg,
4643 );
4644 assert!(matches!(err, super::TgmError::Ok));
4645 assert!(!msg.is_null());
4646
4647 assert_eq!(super::tgm_message_num_objects(msg), 1);
4649 assert_eq!(super::tgm_object_ndim(msg, 0), 1);
4650
4651 let mut data_len: usize = 0;
4652 let data_ptr = super::tgm_object_data(msg, 0, &mut data_len);
4653 assert!(!data_ptr.is_null());
4654 let decoded_bytes = unsafe { slice::from_raw_parts(data_ptr, data_len) };
4655 let decoded_values: Vec<f32> = decoded_bytes
4656 .chunks_exact(4)
4657 .map(|c| f32::from_ne_bytes(c.try_into().unwrap()))
4658 .collect();
4659 assert_eq!(decoded_values, values);
4660
4661 super::tgm_message_free(msg);
4662 }
4663
4664 #[test]
4665 fn ffi_decode_object_out_of_range() {
4666 let encoded = ffi_encode_single_f32_tensor(&[1.0f32], "");
4667 let mut msg: *mut super::TgmMessage = ptr::null_mut();
4668 let err = super::tgm_decode_object(
4669 encoded.as_ptr(),
4670 encoded.len(),
4671 999, 0,
4673 0, 0, &mut msg,
4676 );
4677 assert!(!matches!(err, super::TgmError::Ok));
4678 }
4679
4680 #[test]
4681 fn ffi_decode_object_null_args() {
4682 let mut msg: *mut super::TgmMessage = ptr::null_mut();
4683 let err = super::tgm_decode_object(ptr::null(), 0, 0, 0, 0, 0, &mut msg);
4684 assert!(matches!(err, super::TgmError::InvalidArg));
4685
4686 let data = [0u8; 10];
4687 let err = super::tgm_decode_object(data.as_ptr(), data.len(), 0, 0, 0, 0, ptr::null_mut());
4688 assert!(matches!(err, super::TgmError::InvalidArg));
4689 }
4690
4691 #[test]
4694 fn ffi_message_metadata_null_args() {
4695 let err = super::tgm_message_metadata(ptr::null(), ptr::null_mut());
4696 assert!(matches!(err, super::TgmError::InvalidArg));
4697
4698 let encoded = ffi_encode_single_f32_tensor(&[1.0f32], "");
4699 let mut msg: *mut super::TgmMessage = ptr::null_mut();
4700 let err = super::tgm_decode(encoded.as_ptr(), encoded.len(), 0, 0, 0, &mut msg);
4701 assert!(matches!(err, super::TgmError::Ok));
4702
4703 let err = super::tgm_message_metadata(msg, ptr::null_mut());
4704 assert!(matches!(err, super::TgmError::InvalidArg));
4705
4706 super::tgm_message_free(msg);
4707 }
4708
4709 #[test]
4712 fn ffi_message_accessors_null_handle() {
4713 assert_eq!(super::tgm_message_version(ptr::null()), 0);
4714 assert_eq!(super::tgm_message_num_objects(ptr::null()), 0);
4715 assert_eq!(super::tgm_message_num_decoded(ptr::null()), 0);
4716 assert_eq!(super::tgm_object_ndim(ptr::null(), 0), 0);
4717 assert!(super::tgm_object_shape(ptr::null(), 0).is_null());
4718 assert!(super::tgm_object_strides(ptr::null(), 0).is_null());
4719 assert!(super::tgm_object_dtype(ptr::null(), 0).is_null());
4720 assert!(super::tgm_object_type(ptr::null(), 0).is_null());
4721 assert!(super::tgm_object_byte_order(ptr::null(), 0).is_null());
4722 assert!(super::tgm_object_filter(ptr::null(), 0).is_null());
4723 assert!(super::tgm_object_compression(ptr::null(), 0).is_null());
4724 assert!(super::tgm_payload_encoding(ptr::null(), 0).is_null());
4725 assert_eq!(super::tgm_payload_has_hash(ptr::null(), 0), 0);
4726 assert!(super::tgm_object_hash_type(ptr::null(), 0).is_null());
4727 assert!(super::tgm_object_hash_value(ptr::null(), 0).is_null());
4728
4729 let mut data_len: usize = 99;
4730 let data_ptr = super::tgm_object_data(ptr::null(), 0, &mut data_len);
4731 assert!(data_ptr.is_null());
4732 assert_eq!(data_len, 0);
4733 }
4734
4735 #[test]
4738 fn ffi_message_accessors_out_of_bounds() {
4739 let encoded = ffi_encode_single_f32_tensor(&[1.0f32], "");
4740 let mut msg: *mut super::TgmMessage = ptr::null_mut();
4741 let err = super::tgm_decode(encoded.as_ptr(), encoded.len(), 0, 0, 0, &mut msg);
4742 assert!(matches!(err, super::TgmError::Ok));
4743
4744 assert_eq!(super::tgm_object_ndim(msg, 1), 0);
4746 assert!(super::tgm_object_shape(msg, 1).is_null());
4747 assert!(super::tgm_object_strides(msg, 1).is_null());
4748 assert!(super::tgm_object_dtype(msg, 1).is_null());
4749 assert!(super::tgm_object_type(msg, 1).is_null());
4750 assert!(super::tgm_object_byte_order(msg, 1).is_null());
4751 assert!(super::tgm_object_filter(msg, 1).is_null());
4752 assert!(super::tgm_object_compression(msg, 1).is_null());
4753 assert!(super::tgm_payload_encoding(msg, 1).is_null());
4754 assert_eq!(super::tgm_payload_has_hash(msg, 1), 0);
4755 assert!(super::tgm_object_hash_type(msg, 1).is_null());
4756 assert!(super::tgm_object_hash_value(msg, 1).is_null());
4757
4758 let mut data_len: usize = 99;
4759 let data_ptr = super::tgm_object_data(msg, 1, &mut data_len);
4760 assert!(data_ptr.is_null());
4761 assert_eq!(data_len, 0);
4762
4763 super::tgm_message_free(msg);
4764 }
4765
4766 #[test]
4769 fn ffi_object_data_null_out_len() {
4770 let encoded = ffi_encode_single_f32_tensor(&[1.0f32], "");
4771 let mut msg: *mut super::TgmMessage = ptr::null_mut();
4772 let err = super::tgm_decode(encoded.as_ptr(), encoded.len(), 0, 0, 0, &mut msg);
4773 assert!(matches!(err, super::TgmError::Ok));
4774
4775 let data_ptr = super::tgm_object_data(msg, 0, ptr::null_mut());
4777 assert!(!data_ptr.is_null());
4778
4779 super::tgm_message_free(msg);
4780 }
4781
4782 #[test]
4785 fn ffi_decode_range_round_trip() {
4786 let values = [1.0f32, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
4787 let encoded = ffi_encode_single_f32_tensor(&values, "");
4788
4789 let range_offset: u64 = 2;
4791 let range_count: u64 = 3;
4792 let mut out_buf = super::TgmBytes {
4793 data: ptr::null_mut(),
4794 len: 0,
4795 };
4796 let mut out_count: usize = 0;
4797
4798 let err = super::tgm_decode_range(
4799 encoded.as_ptr(),
4800 encoded.len(),
4801 0,
4802 &range_offset as *const u64,
4803 &range_count as *const u64,
4804 1,
4805 0, 0, 1, &mut out_buf,
4809 &mut out_count,
4810 );
4811 assert!(matches!(err, super::TgmError::Ok));
4812 assert_eq!(out_count, 1);
4813 assert!(!out_buf.data.is_null());
4814
4815 let decoded_bytes = unsafe { slice::from_raw_parts(out_buf.data, out_buf.len) };
4816 let decoded_values: Vec<f32> = decoded_bytes
4817 .chunks_exact(4)
4818 .map(|c| f32::from_ne_bytes(c.try_into().unwrap()))
4819 .collect();
4820 assert_eq!(decoded_values, [3.0, 4.0, 5.0]);
4821
4822 super::tgm_bytes_free(out_buf);
4823 }
4824
4825 #[test]
4826 fn ffi_decode_range_split_mode() {
4827 let values = [10.0f32, 20.0, 30.0, 40.0];
4828 let encoded = ffi_encode_single_f32_tensor(&values, "");
4829
4830 let range_offsets: [u64; 2] = [0, 2];
4832 let range_counts: [u64; 2] = [2, 2];
4833 let mut out_bufs = [
4834 super::TgmBytes {
4835 data: ptr::null_mut(),
4836 len: 0,
4837 },
4838 super::TgmBytes {
4839 data: ptr::null_mut(),
4840 len: 0,
4841 },
4842 ];
4843 let mut out_count: usize = 0;
4844
4845 let err = super::tgm_decode_range(
4846 encoded.as_ptr(),
4847 encoded.len(),
4848 0,
4849 range_offsets.as_ptr(),
4850 range_counts.as_ptr(),
4851 2,
4852 0,
4853 0, 0, out_bufs.as_mut_ptr(),
4856 &mut out_count,
4857 );
4858 assert!(matches!(err, super::TgmError::Ok));
4859 assert_eq!(out_count, 2);
4860
4861 let bytes0 = unsafe { slice::from_raw_parts(out_bufs[0].data, out_bufs[0].len) };
4863 let vals0: Vec<f32> = bytes0
4864 .chunks_exact(4)
4865 .map(|c| f32::from_ne_bytes(c.try_into().unwrap()))
4866 .collect();
4867 assert_eq!(vals0, [10.0, 20.0]);
4868
4869 let bytes1 = unsafe { slice::from_raw_parts(out_bufs[1].data, out_bufs[1].len) };
4871 let vals1: Vec<f32> = bytes1
4872 .chunks_exact(4)
4873 .map(|c| f32::from_ne_bytes(c.try_into().unwrap()))
4874 .collect();
4875 assert_eq!(vals1, [30.0, 40.0]);
4876
4877 super::tgm_bytes_free(super::TgmBytes {
4879 data: out_bufs[0].data,
4880 len: out_bufs[0].len,
4881 });
4882 super::tgm_bytes_free(super::TgmBytes {
4883 data: out_bufs[1].data,
4884 len: out_bufs[1].len,
4885 });
4886 }
4887
4888 #[test]
4889 fn ffi_decode_range_null_args() {
4890 let mut out_buf = super::TgmBytes {
4891 data: ptr::null_mut(),
4892 len: 0,
4893 };
4894 let mut out_count: usize = 0;
4895
4896 let err = super::tgm_decode_range(
4898 ptr::null(),
4899 0,
4900 0,
4901 ptr::null(),
4902 ptr::null(),
4903 0,
4904 0,
4905 0,
4906 0,
4907 &mut out_buf,
4908 &mut out_count,
4909 );
4910 assert!(matches!(err, super::TgmError::InvalidArg));
4911
4912 let data = [0u8; 10];
4914 let err = super::tgm_decode_range(
4915 data.as_ptr(),
4916 data.len(),
4917 0,
4918 ptr::null(),
4919 ptr::null(),
4920 0,
4921 0,
4922 0,
4923 0,
4924 ptr::null_mut(),
4925 &mut out_count,
4926 );
4927 assert!(matches!(err, super::TgmError::InvalidArg));
4928
4929 let err = super::tgm_decode_range(
4931 data.as_ptr(),
4932 data.len(),
4933 0,
4934 ptr::null(),
4935 ptr::null(),
4936 0,
4937 0,
4938 0,
4939 0,
4940 &mut out_buf,
4941 ptr::null_mut(),
4942 );
4943 assert!(matches!(err, super::TgmError::InvalidArg));
4944 }
4945
4946 #[test]
4947 fn ffi_decode_range_null_ranges_with_nonzero_count() {
4948 let encoded = ffi_encode_single_f32_tensor(&[1.0f32], "");
4949 let mut out_buf = super::TgmBytes {
4950 data: ptr::null_mut(),
4951 len: 0,
4952 };
4953 let mut out_count: usize = 0;
4954
4955 let err = super::tgm_decode_range(
4956 encoded.as_ptr(),
4957 encoded.len(),
4958 0,
4959 ptr::null(), ptr::null(), 1, 0,
4963 0, 0,
4965 &mut out_buf,
4966 &mut out_count,
4967 );
4968 assert!(matches!(err, super::TgmError::InvalidArg));
4969 }
4970
4971 #[test]
4974 fn ffi_scan_single_message() {
4975 let encoded = ffi_encode_single_f32_tensor(&[1.0f32, 2.0], "");
4976
4977 let mut result: *mut super::TgmScanResult = ptr::null_mut();
4978 let err = super::tgm_scan(encoded.as_ptr(), encoded.len(), &mut result);
4979 assert!(matches!(err, super::TgmError::Ok));
4980 assert!(!result.is_null());
4981
4982 assert_eq!(super::tgm_scan_count(result), 1);
4983
4984 let entry = super::tgm_scan_entry(result, 0);
4985 assert_eq!(entry.offset, 0);
4986 assert_eq!(entry.length, encoded.len());
4987
4988 let bad = super::tgm_scan_entry(result, 999);
4991 assert_eq!(bad.offset, usize::MAX);
4992 assert_eq!(bad.length, 0);
4993 let err_ptr = super::tgm_last_error();
4994 assert!(!err_ptr.is_null());
4995 let err_str = unsafe { CStr::from_ptr(err_ptr) }.to_str().unwrap();
4996 assert!(
4997 err_str.contains("out of range"),
4998 "expected OOB error, got: {err_str}"
4999 );
5000
5001 super::tgm_scan_free(result);
5002 }
5003
5004 #[test]
5005 fn ffi_scan_null_args() {
5006 let mut result: *mut super::TgmScanResult = ptr::null_mut();
5007 let err = super::tgm_scan(ptr::null(), 0, &mut result);
5008 assert!(matches!(err, super::TgmError::InvalidArg));
5009
5010 let data = [0u8; 10];
5011 let err = super::tgm_scan(data.as_ptr(), data.len(), ptr::null_mut());
5012 assert!(matches!(err, super::TgmError::InvalidArg));
5013 }
5014
5015 #[test]
5016 fn ffi_scan_null_handle_accessors() {
5017 assert_eq!(super::tgm_scan_count(ptr::null()), 0);
5018 let entry = super::tgm_scan_entry(ptr::null(), 0);
5019 assert_eq!(entry.offset, usize::MAX);
5020 assert_eq!(entry.length, 0);
5021 }
5022
5023 #[test]
5024 fn ffi_scan_concatenated_messages() {
5025 let msg1 = ffi_encode_single_f32_tensor(&[1.0f32], "");
5026 let msg2 = ffi_encode_single_f32_tensor(&[2.0f32], "");
5027 let mut concat = msg1.clone();
5028 concat.extend_from_slice(&msg2);
5029
5030 let mut result: *mut super::TgmScanResult = ptr::null_mut();
5031 let err = super::tgm_scan(concat.as_ptr(), concat.len(), &mut result);
5032 assert!(matches!(err, super::TgmError::Ok));
5033
5034 assert_eq!(super::tgm_scan_count(result), 2);
5035
5036 let e0 = super::tgm_scan_entry(result, 0);
5037 assert_eq!(e0.offset, 0);
5038 assert_eq!(e0.length, msg1.len());
5039
5040 let e1 = super::tgm_scan_entry(result, 1);
5041 assert_eq!(e1.offset, msg1.len());
5042 assert_eq!(e1.length, msg2.len());
5043
5044 super::tgm_scan_free(result);
5045 }
5046
5047 #[test]
5050 fn ffi_file_create_append_count_decode_close() {
5051 let dir = std::env::temp_dir();
5052 let path = dir.join("ffi_test_file.tgm");
5053 let _ = std::fs::remove_file(&path);
5054
5055 let c_path = CString::new(path.to_str().unwrap()).unwrap();
5056
5057 let mut file: *mut super::TgmFile = ptr::null_mut();
5059 let err = super::tgm_file_create(c_path.as_ptr(), &mut file);
5060 assert!(matches!(err, super::TgmError::Ok));
5061 assert!(!file.is_null());
5062
5063 let values = [10.0f32, 20.0, 30.0];
5065 let shape_str = format!("[{}]", values.len());
5066 let json = format!(
5067 r#"{{"version":3,"descriptors":[{{"type":"ntensor","ndim":1,"shape":{shape},"strides":[1],"dtype":"float32","byte_order":"{bo}","encoding":"none","filter":"none","compression":"none"}}]}}"#,
5068 shape = shape_str,
5069 bo = if cfg!(target_endian = "little") {
5070 "little"
5071 } else {
5072 "big"
5073 },
5074 );
5075 let c_json = CString::new(json).unwrap();
5076 let data: Vec<u8> = values.iter().flat_map(|v| v.to_ne_bytes()).collect();
5077 let data_ptr: *const u8 = data.as_ptr();
5078 let data_len: usize = data.len();
5079
5080 let err = super::tgm_file_append(
5081 file,
5082 c_json.as_ptr(),
5083 &data_ptr as *const *const u8,
5084 &data_len as *const usize,
5085 1,
5086 ptr::null(),
5087 0,
5088 );
5089 assert!(matches!(err, super::TgmError::Ok));
5090
5091 let path_ptr = super::tgm_file_path(file);
5093 assert!(!path_ptr.is_null());
5094 let path_str = unsafe { CStr::from_ptr(path_ptr) }.to_str().unwrap();
5095 assert!(path_str.contains("ffi_test_file.tgm"));
5096
5097 super::tgm_file_close(file);
5098
5099 let mut file2: *mut super::TgmFile = ptr::null_mut();
5101 let err = super::tgm_file_open(c_path.as_ptr(), &mut file2);
5102 assert!(matches!(err, super::TgmError::Ok));
5103
5104 let mut count: usize = 0;
5106 let err = super::tgm_file_message_count(file2, &mut count);
5107 assert!(matches!(err, super::TgmError::Ok));
5108 assert_eq!(count, 1);
5109
5110 let mut msg: *mut super::TgmMessage = ptr::null_mut();
5112 let err = super::tgm_file_decode_message(file2, 0, 0, 0, 0, &mut msg);
5113 assert!(matches!(err, super::TgmError::Ok));
5114
5115 assert_eq!(super::tgm_message_num_objects(msg), 1);
5116 let mut data_len2: usize = 0;
5117 let dp = super::tgm_object_data(msg, 0, &mut data_len2);
5118 assert!(!dp.is_null());
5119 let decoded_bytes = unsafe { slice::from_raw_parts(dp, data_len2) };
5120 let decoded_values: Vec<f32> = decoded_bytes
5121 .chunks_exact(4)
5122 .map(|c| f32::from_ne_bytes(c.try_into().unwrap()))
5123 .collect();
5124 assert_eq!(decoded_values, values);
5125
5126 super::tgm_message_free(msg);
5127
5128 let mut raw = super::TgmBytes {
5130 data: ptr::null_mut(),
5131 len: 0,
5132 };
5133 let err = super::tgm_file_read_message(file2, 0, &mut raw);
5134 assert!(matches!(err, super::TgmError::Ok));
5135 assert!(!raw.data.is_null());
5136 assert!(raw.len > 0);
5137 super::tgm_bytes_free(raw);
5138
5139 super::tgm_file_close(file2);
5140 let _ = std::fs::remove_file(&path);
5141 }
5142
5143 #[test]
5144 fn ffi_file_open_nonexistent() {
5145 let c_path = CString::new("/nonexistent/file.tgm").unwrap();
5146 let mut file: *mut super::TgmFile = ptr::null_mut();
5147 let err = super::tgm_file_open(c_path.as_ptr(), &mut file);
5148 assert!(!matches!(err, super::TgmError::Ok));
5149 }
5150
5151 #[test]
5152 fn ffi_file_null_args() {
5153 let mut file: *mut super::TgmFile = ptr::null_mut();
5154
5155 let err = super::tgm_file_open(ptr::null(), &mut file);
5157 assert!(matches!(err, super::TgmError::InvalidArg));
5158
5159 let c_path = CString::new("/tmp/test.tgm").unwrap();
5161 let err = super::tgm_file_open(c_path.as_ptr(), ptr::null_mut());
5162 assert!(matches!(err, super::TgmError::InvalidArg));
5163
5164 let err = super::tgm_file_create(ptr::null(), &mut file);
5166 assert!(matches!(err, super::TgmError::InvalidArg));
5167
5168 let err = super::tgm_file_create(c_path.as_ptr(), ptr::null_mut());
5170 assert!(matches!(err, super::TgmError::InvalidArg));
5171
5172 let err = super::tgm_file_message_count(ptr::null_mut(), ptr::null_mut());
5174 assert!(matches!(err, super::TgmError::InvalidArg));
5175
5176 let err = super::tgm_file_decode_message(ptr::null_mut(), 0, 0, 0, 0, ptr::null_mut());
5178 assert!(matches!(err, super::TgmError::InvalidArg));
5179
5180 let err = super::tgm_file_read_message(ptr::null_mut(), 0, ptr::null_mut());
5182 assert!(matches!(err, super::TgmError::InvalidArg));
5183
5184 let err = super::tgm_file_append(
5186 ptr::null_mut(),
5187 ptr::null(),
5188 ptr::null(),
5189 ptr::null(),
5190 0,
5191 ptr::null(),
5192 0,
5193 );
5194 assert!(matches!(err, super::TgmError::InvalidArg));
5195
5196 let err = super::tgm_file_append_raw(ptr::null_mut(), ptr::null(), 0);
5198 assert!(matches!(err, super::TgmError::InvalidArg));
5199
5200 assert!(super::tgm_file_path(ptr::null()).is_null());
5202 }
5203
5204 #[test]
5205 fn ffi_file_append_raw_round_trip() {
5206 let dir = std::env::temp_dir();
5207 let path = dir.join("ffi_test_append_raw.tgm");
5208 let _ = std::fs::remove_file(&path);
5209
5210 let c_path = CString::new(path.to_str().unwrap()).unwrap();
5211
5212 let mut file: *mut super::TgmFile = ptr::null_mut();
5214 let err = super::tgm_file_create(c_path.as_ptr(), &mut file);
5215 assert!(matches!(err, super::TgmError::Ok));
5216
5217 let encoded = ffi_encode_single_f32_tensor(&[1.0f32, 2.0], "");
5219 let err = super::tgm_file_append_raw(file, encoded.as_ptr(), encoded.len());
5220 assert!(matches!(err, super::TgmError::Ok));
5221
5222 let mut count: usize = 0;
5224 let err = super::tgm_file_message_count(file, &mut count);
5225 assert!(matches!(err, super::TgmError::Ok));
5226 assert_eq!(count, 1);
5227
5228 super::tgm_file_close(file);
5229 let _ = std::fs::remove_file(&path);
5230 }
5231
5232 #[test]
5235 fn ffi_streaming_encoder_round_trip() {
5236 let dir = std::env::temp_dir();
5237 let path = dir.join("ffi_streaming_test.tgm");
5238 let _ = std::fs::remove_file(&path);
5239
5240 let c_path = CString::new(path.to_str().unwrap()).unwrap();
5241 let meta_json = CString::new(r#"{"version":3}"#).unwrap();
5242
5243 let mut enc: *mut super::TgmStreamingEncoder = ptr::null_mut();
5245 let err = super::tgm_streaming_encoder_create(
5246 c_path.as_ptr(),
5247 meta_json.as_ptr(),
5248 ptr::null(), 0, &mut enc,
5251 );
5252 assert!(matches!(err, super::TgmError::Ok));
5253 assert!(!enc.is_null());
5254
5255 let values = [100.0f32, 200.0, 300.0];
5257 let data: Vec<u8> = values.iter().flat_map(|v| v.to_ne_bytes()).collect();
5258 let desc_json = CString::new(format!(
5259 r#"{{"type":"ntensor","ndim":1,"shape":[{len}],"strides":[1],"dtype":"float32","byte_order":"{bo}","encoding":"none","filter":"none","compression":"none"}}"#,
5260 len = values.len(),
5261 bo = if cfg!(target_endian = "little") { "little" } else { "big" },
5262 )).unwrap();
5263
5264 let err =
5265 super::tgm_streaming_encoder_write(enc, desc_json.as_ptr(), data.as_ptr(), data.len());
5266 assert!(matches!(err, super::TgmError::Ok));
5267
5268 assert_eq!(super::tgm_streaming_encoder_count(enc), 1);
5270
5271 let err = super::tgm_streaming_encoder_finish(enc);
5273 assert!(matches!(err, super::TgmError::Ok));
5274
5275 let err = super::tgm_streaming_encoder_finish(enc);
5277 assert!(matches!(err, super::TgmError::InvalidArg));
5278
5279 assert_eq!(super::tgm_streaming_encoder_count(enc), 0);
5281
5282 super::tgm_streaming_encoder_free(enc);
5284
5285 let mut file: *mut super::TgmFile = ptr::null_mut();
5287 let err = super::tgm_file_open(c_path.as_ptr(), &mut file);
5288 assert!(matches!(err, super::TgmError::Ok));
5289
5290 let mut count: usize = 0;
5291 let err = super::tgm_file_message_count(file, &mut count);
5292 assert!(matches!(err, super::TgmError::Ok));
5293 assert_eq!(count, 1);
5294
5295 let mut msg: *mut super::TgmMessage = ptr::null_mut();
5296 let err = super::tgm_file_decode_message(file, 0, 0, 0, 0, &mut msg);
5297 assert!(matches!(err, super::TgmError::Ok));
5298
5299 let mut data_len: usize = 0;
5300 let dp = super::tgm_object_data(msg, 0, &mut data_len);
5301 let decoded_bytes = unsafe { slice::from_raw_parts(dp, data_len) };
5302 let decoded_values: Vec<f32> = decoded_bytes
5303 .chunks_exact(4)
5304 .map(|c| f32::from_ne_bytes(c.try_into().unwrap()))
5305 .collect();
5306 assert_eq!(decoded_values, values);
5307
5308 super::tgm_message_free(msg);
5309 super::tgm_file_close(file);
5310 let _ = std::fs::remove_file(&path);
5311 }
5312
5313 #[test]
5314 fn ffi_streaming_encoder_null_args() {
5315 let mut enc: *mut super::TgmStreamingEncoder = ptr::null_mut();
5316
5317 let meta = CString::new(r#"{"version":3}"#).unwrap();
5319 let err = super::tgm_streaming_encoder_create(
5320 ptr::null(),
5321 meta.as_ptr(),
5322 ptr::null(),
5323 0,
5324 &mut enc,
5325 );
5326 assert!(matches!(err, super::TgmError::InvalidArg));
5327
5328 let p = CString::new("/tmp/dummy.tgm").unwrap();
5330 let err =
5331 super::tgm_streaming_encoder_create(p.as_ptr(), ptr::null(), ptr::null(), 0, &mut enc);
5332 assert!(matches!(err, super::TgmError::InvalidArg));
5333
5334 let err = super::tgm_streaming_encoder_create(
5336 p.as_ptr(),
5337 meta.as_ptr(),
5338 ptr::null(),
5339 0,
5340 ptr::null_mut(),
5341 );
5342 assert!(matches!(err, super::TgmError::InvalidArg));
5343
5344 let desc = CString::new(r#"{}"#).unwrap();
5346 let data = [0u8; 4];
5347 let err = super::tgm_streaming_encoder_write(
5348 ptr::null_mut(),
5349 desc.as_ptr(),
5350 data.as_ptr(),
5351 data.len(),
5352 );
5353 assert!(matches!(err, super::TgmError::InvalidArg));
5354
5355 let err = super::tgm_streaming_encoder_finish(ptr::null_mut());
5360 assert!(matches!(err, super::TgmError::InvalidArg));
5361
5362 assert_eq!(super::tgm_streaming_encoder_count(ptr::null()), 0);
5364
5365 super::tgm_streaming_encoder_free(ptr::null_mut());
5367 }
5368
5369 #[test]
5370 fn ffi_streaming_encoder_write_null_data() {
5371 let dir = std::env::temp_dir();
5372 let path = dir.join("ffi_streaming_null_data.tgm");
5373 let _ = std::fs::remove_file(&path);
5374
5375 let c_path = CString::new(path.to_str().unwrap()).unwrap();
5376 let meta_json = CString::new(r#"{"version":3}"#).unwrap();
5377
5378 let mut enc: *mut super::TgmStreamingEncoder = ptr::null_mut();
5379 let err = super::tgm_streaming_encoder_create(
5380 c_path.as_ptr(),
5381 meta_json.as_ptr(),
5382 ptr::null(),
5383 0,
5384 &mut enc,
5385 );
5386 assert!(matches!(err, super::TgmError::Ok));
5387
5388 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();
5389 let err = super::tgm_streaming_encoder_write(enc, desc.as_ptr(), ptr::null(), 4);
5390 assert!(matches!(err, super::TgmError::InvalidArg));
5391
5392 let data = [0u8; 4];
5394 let err = super::tgm_streaming_encoder_write(enc, ptr::null(), data.as_ptr(), data.len());
5395 assert!(matches!(err, super::TgmError::InvalidArg));
5396
5397 super::tgm_streaming_encoder_free(enc);
5398 let _ = std::fs::remove_file(&path);
5399 }
5400
5401 #[test]
5402 fn ffi_streaming_encoder_with_preceder() {
5403 let dir = std::env::temp_dir();
5404 let path = dir.join("ffi_streaming_preceder.tgm");
5405 let _ = std::fs::remove_file(&path);
5406
5407 let c_path = CString::new(path.to_str().unwrap()).unwrap();
5408 let meta_json = CString::new(r#"{"version":3}"#).unwrap();
5409
5410 let mut enc: *mut super::TgmStreamingEncoder = ptr::null_mut();
5411 let err = super::tgm_streaming_encoder_create(
5412 c_path.as_ptr(),
5413 meta_json.as_ptr(),
5414 ptr::null(),
5415 0,
5416 &mut enc,
5417 );
5418 assert!(matches!(err, super::TgmError::Ok));
5419
5420 let preceder_json = CString::new(r#"{"param":"2t","source":"test"}"#).unwrap();
5422 let err = super::tgm_streaming_encoder_write_preceder(enc, preceder_json.as_ptr());
5423 assert!(matches!(err, super::TgmError::Ok));
5424
5425 let values = [42.0f32];
5427 let data: Vec<u8> = values.iter().flat_map(|v| v.to_ne_bytes()).collect();
5428 let desc_json = CString::new(format!(
5429 r#"{{"type":"ntensor","ndim":1,"shape":[1],"strides":[1],"dtype":"float32","byte_order":"{bo}","encoding":"none","filter":"none","compression":"none"}}"#,
5430 bo = if cfg!(target_endian = "little") { "little" } else { "big" },
5431 )).unwrap();
5432
5433 let err =
5434 super::tgm_streaming_encoder_write(enc, desc_json.as_ptr(), data.as_ptr(), data.len());
5435 assert!(matches!(err, super::TgmError::Ok));
5436
5437 let err = super::tgm_streaming_encoder_finish(enc);
5438 assert!(matches!(err, super::TgmError::Ok));
5439 super::tgm_streaming_encoder_free(enc);
5440
5441 let mut file: *mut super::TgmFile = ptr::null_mut();
5443 let err = super::tgm_file_open(c_path.as_ptr(), &mut file);
5444 assert!(matches!(err, super::TgmError::Ok));
5445
5446 let mut msg: *mut super::TgmMessage = ptr::null_mut();
5447 let err = super::tgm_file_decode_message(file, 0, 0, 0, 0, &mut msg);
5448 assert!(matches!(err, super::TgmError::Ok));
5449
5450 let mut meta: *mut super::TgmMetadata = ptr::null_mut();
5451 let err = super::tgm_message_metadata(msg, &mut meta);
5452 assert!(matches!(err, super::TgmError::Ok));
5453
5454 let key = CString::new("param").unwrap();
5455 let val_ptr = super::tgm_metadata_get_string(meta, key.as_ptr());
5456 assert!(!val_ptr.is_null());
5457 let val_str = unsafe { CStr::from_ptr(val_ptr) }.to_str().unwrap();
5458 assert_eq!(val_str, "2t");
5459
5460 super::tgm_metadata_free(meta);
5461 super::tgm_message_free(msg);
5462 super::tgm_file_close(file);
5463 let _ = std::fs::remove_file(&path);
5464 }
5465
5466 #[test]
5467 fn ffi_streaming_encoder_write_preceder_null_args() {
5468 let err = super::tgm_streaming_encoder_write_preceder(ptr::null_mut(), ptr::null());
5469 assert!(matches!(err, super::TgmError::InvalidArg));
5470 }
5471
5472 #[test]
5473 fn ffi_streaming_encoder_write_pre_encoded_null_args() {
5474 let err = super::tgm_streaming_encoder_write_pre_encoded(
5475 ptr::null_mut(),
5476 ptr::null(),
5477 ptr::null(),
5478 0,
5479 );
5480 assert!(matches!(err, super::TgmError::InvalidArg));
5481 }
5482
5483 #[test]
5486 fn ffi_compute_hash_xxh3() {
5487 let data = b"hello world";
5488 let mut out = super::TgmBytes {
5489 data: ptr::null_mut(),
5490 len: 0,
5491 };
5492 let err = super::tgm_compute_hash(
5493 data.as_ptr(),
5494 data.len(),
5495 ptr::null(), &mut out,
5497 );
5498 assert!(matches!(err, super::TgmError::Ok));
5499 assert!(!out.data.is_null());
5500 assert!(out.len > 0);
5501
5502 let hex = unsafe { std::str::from_utf8(slice::from_raw_parts(out.data, out.len)).unwrap() };
5503 assert!(!hex.is_empty());
5505 assert!(hex.chars().all(|c| c.is_ascii_hexdigit()));
5506
5507 super::tgm_bytes_free(out);
5508 }
5509
5510 #[test]
5511 fn ffi_compute_hash_explicit_xxh3() {
5512 let data = b"test data";
5513 let algo = CString::new("xxh3").unwrap();
5514 let mut out = super::TgmBytes {
5515 data: ptr::null_mut(),
5516 len: 0,
5517 };
5518 let err = super::tgm_compute_hash(data.as_ptr(), data.len(), algo.as_ptr(), &mut out);
5519 assert!(matches!(err, super::TgmError::Ok));
5520 assert!(out.len > 0);
5521 super::tgm_bytes_free(out);
5522 }
5523
5524 #[test]
5525 fn ffi_compute_hash_null_data() {
5526 let mut out = super::TgmBytes {
5527 data: ptr::null_mut(),
5528 len: 0,
5529 };
5530 let err = super::tgm_compute_hash(ptr::null(), 0, ptr::null(), &mut out);
5531 assert!(matches!(err, super::TgmError::InvalidArg));
5532 }
5533
5534 #[test]
5535 fn ffi_compute_hash_null_out() {
5536 let data = b"hello";
5537 let err = super::tgm_compute_hash(data.as_ptr(), data.len(), ptr::null(), ptr::null_mut());
5538 assert!(matches!(err, super::TgmError::InvalidArg));
5539 }
5540
5541 #[test]
5542 fn ffi_compute_hash_invalid_algo() {
5543 let data = b"hello";
5544 let algo = CString::new("bogus_algo").unwrap();
5545 let mut out = super::TgmBytes {
5546 data: ptr::null_mut(),
5547 len: 0,
5548 };
5549 let err = super::tgm_compute_hash(data.as_ptr(), data.len(), algo.as_ptr(), &mut out);
5550 assert!(matches!(err, super::TgmError::InvalidArg));
5551 }
5552
5553 #[test]
5556 fn ffi_simple_packing_compute_params() {
5557 let values = [100.0f64, 200.0, 300.0, 400.0];
5558 let mut ref_val: f64 = 0.0;
5559 let mut bin_scale: i32 = 0;
5560 let err = super::tgm_simple_packing_compute_params(
5561 values.as_ptr(),
5562 values.len(),
5563 16,
5564 0,
5565 &mut ref_val,
5566 &mut bin_scale,
5567 );
5568 assert!(matches!(err, super::TgmError::Ok));
5569 assert!(ref_val <= 100.0);
5571 }
5572
5573 #[test]
5574 fn ffi_simple_packing_compute_params_null_args() {
5575 let mut ref_val: f64 = 0.0;
5576 let mut bin_scale: i32 = 0;
5577
5578 let err = super::tgm_simple_packing_compute_params(
5580 ptr::null(),
5581 0,
5582 16,
5583 0,
5584 &mut ref_val,
5585 &mut bin_scale,
5586 );
5587 assert!(matches!(err, super::TgmError::InvalidArg));
5588
5589 let values = [1.0f64];
5591 let err = super::tgm_simple_packing_compute_params(
5592 values.as_ptr(),
5593 values.len(),
5594 16,
5595 0,
5596 ptr::null_mut(),
5597 &mut bin_scale,
5598 );
5599 assert!(matches!(err, super::TgmError::InvalidArg));
5600
5601 let err = super::tgm_simple_packing_compute_params(
5603 values.as_ptr(),
5604 values.len(),
5605 16,
5606 0,
5607 &mut ref_val,
5608 ptr::null_mut(),
5609 );
5610 assert!(matches!(err, super::TgmError::InvalidArg));
5611 }
5612
5613 #[test]
5616 fn ffi_last_error_after_success() {
5617 let values = [1.0f32];
5620 let _ = ffi_encode_single_f32_tensor(&values, "");
5621 }
5623
5624 #[test]
5625 fn ffi_last_error_after_failure() {
5626 let mut out = super::TgmBytes {
5628 data: ptr::null_mut(),
5629 len: 0,
5630 };
5631 let _ = super::tgm_encode(
5632 ptr::null(),
5633 ptr::null(),
5634 ptr::null(),
5635 0,
5636 ptr::null(),
5637 0,
5638 &mut out,
5639 );
5640
5641 let err_ptr = super::tgm_last_error();
5642 assert!(!err_ptr.is_null());
5643 let err_str = unsafe { CStr::from_ptr(err_ptr) }.to_str().unwrap();
5644 assert!(err_str.contains("null"));
5645 }
5646
5647 #[test]
5650 fn ffi_error_string_all_variants() {
5651 let check = |err: super::TgmError, expected: &str| {
5652 let ptr = super::tgm_error_string(err);
5653 assert!(!ptr.is_null());
5654 let s = unsafe { CStr::from_ptr(ptr) }.to_str().unwrap();
5655 assert_eq!(s, expected);
5656 };
5657
5658 check(super::TgmError::Ok, "ok");
5659 check(super::TgmError::Framing, "framing error");
5660 check(super::TgmError::Metadata, "metadata error");
5661 check(super::TgmError::Encoding, "encoding error");
5662 check(super::TgmError::Compression, "compression error");
5663 check(super::TgmError::Object, "object error");
5664 check(super::TgmError::Io, "I/O error");
5665 check(super::TgmError::HashMismatch, "hash mismatch");
5666 check(super::TgmError::InvalidArg, "invalid argument");
5667 check(super::TgmError::EndOfIter, "end of iteration");
5668 check(super::TgmError::Remote, "remote error");
5669 }
5670
5671 #[test]
5674 fn ffi_bytes_free_null_data() {
5675 let buf = super::TgmBytes {
5676 data: ptr::null_mut(),
5677 len: 0,
5678 };
5679 super::tgm_bytes_free(buf); }
5681
5682 #[test]
5685 fn ffi_free_null_handles() {
5686 super::tgm_message_free(ptr::null_mut());
5687 super::tgm_metadata_free(ptr::null_mut());
5688 super::tgm_scan_free(ptr::null_mut());
5689 super::tgm_file_close(ptr::null_mut());
5690 super::tgm_streaming_encoder_free(ptr::null_mut());
5691 }
5693
5694 #[test]
5697 fn ffi_encode_pre_encoded_null_args() {
5698 let mut out = super::TgmBytes {
5699 data: ptr::null_mut(),
5700 len: 0,
5701 };
5702 let err = super::tgm_encode_pre_encoded(
5703 ptr::null(),
5704 ptr::null(),
5705 ptr::null(),
5706 0,
5707 ptr::null(),
5708 0,
5709 &mut out,
5710 );
5711 assert!(matches!(err, super::TgmError::InvalidArg));
5712
5713 let json = CString::new(r#"{"version":3,"descriptors":[]}"#).unwrap();
5714 let err = super::tgm_encode_pre_encoded(
5715 json.as_ptr(),
5716 ptr::null(),
5717 ptr::null(),
5718 0,
5719 ptr::null(),
5720 0,
5721 ptr::null_mut(),
5722 );
5723 assert!(matches!(err, super::TgmError::InvalidArg));
5724 }
5725
5726 #[test]
5727 fn ffi_encode_pre_encoded_round_trip() {
5728 let values = [5.0f32, 6.0, 7.0];
5730 let data: Vec<u8> = values.iter().flat_map(|v| v.to_ne_bytes()).collect();
5731
5732 let json = format!(
5733 r#"{{"version":3,"descriptors":[{{"type":"ntensor","ndim":1,"shape":[{len}],"strides":[1],"dtype":"float32","byte_order":"{bo}","encoding":"none","filter":"none","compression":"none"}}]}}"#,
5734 len = values.len(),
5735 bo = if cfg!(target_endian = "little") {
5736 "little"
5737 } else {
5738 "big"
5739 },
5740 );
5741 let c_json = CString::new(json).unwrap();
5742 let data_ptr: *const u8 = data.as_ptr();
5743 let data_len: usize = data.len();
5744
5745 let mut out = super::TgmBytes {
5746 data: ptr::null_mut(),
5747 len: 0,
5748 };
5749 let err = super::tgm_encode_pre_encoded(
5750 c_json.as_ptr(),
5751 &data_ptr as *const *const u8,
5752 &data_len as *const usize,
5753 1,
5754 ptr::null(),
5755 0,
5756 &mut out,
5757 );
5758 assert!(matches!(err, super::TgmError::Ok));
5759
5760 let encoded = unsafe { slice::from_raw_parts(out.data, out.len) }.to_vec();
5761 super::tgm_bytes_free(out);
5762
5763 let mut msg: *mut super::TgmMessage = ptr::null_mut();
5765 let err = super::tgm_decode(encoded.as_ptr(), encoded.len(), 0, 0, 0, &mut msg);
5766 assert!(matches!(err, super::TgmError::Ok));
5767
5768 let mut dl: usize = 0;
5769 let dp = super::tgm_object_data(msg, 0, &mut dl);
5770 let decoded_bytes = unsafe { slice::from_raw_parts(dp, dl) };
5771 let decoded_values: Vec<f32> = decoded_bytes
5772 .chunks_exact(4)
5773 .map(|c| f32::from_ne_bytes(c.try_into().unwrap()))
5774 .collect();
5775 assert_eq!(decoded_values, values);
5776
5777 super::tgm_message_free(msg);
5778 }
5779
5780 #[test]
5783 fn ffi_buffer_iter_round_trip() {
5784 let msg1 = ffi_encode_single_f32_tensor(&[1.0f32], "");
5785 let msg2 = ffi_encode_single_f32_tensor(&[2.0f32], "");
5786 let mut concat = msg1.clone();
5787 concat.extend_from_slice(&msg2);
5788
5789 let mut iter: *mut super::TgmBufferIter = ptr::null_mut();
5790 let err = super::tgm_buffer_iter_create(concat.as_ptr(), concat.len(), &mut iter);
5791 assert!(matches!(err, super::TgmError::Ok));
5792 assert!(!iter.is_null());
5793
5794 assert_eq!(super::tgm_buffer_iter_count(iter), 2);
5795
5796 let mut out_buf: *const u8 = ptr::null();
5798 let mut out_len: usize = 0;
5799 let err = super::tgm_buffer_iter_next(iter, &mut out_buf, &mut out_len);
5800 assert!(matches!(err, super::TgmError::Ok));
5801 assert!(!out_buf.is_null());
5802 assert_eq!(out_len, msg1.len());
5803
5804 let err = super::tgm_buffer_iter_next(iter, &mut out_buf, &mut out_len);
5806 assert!(matches!(err, super::TgmError::Ok));
5807 assert_eq!(out_len, msg2.len());
5808
5809 let err = super::tgm_buffer_iter_next(iter, &mut out_buf, &mut out_len);
5811 assert!(matches!(err, super::TgmError::EndOfIter));
5812
5813 super::tgm_buffer_iter_free(iter);
5814 }
5815
5816 #[test]
5817 fn ffi_buffer_iter_null_args() {
5818 let mut iter: *mut super::TgmBufferIter = ptr::null_mut();
5819 let err = super::tgm_buffer_iter_create(ptr::null(), 0, &mut iter);
5820 assert!(matches!(err, super::TgmError::InvalidArg));
5821
5822 let data = [0u8; 10];
5823 let err = super::tgm_buffer_iter_create(data.as_ptr(), data.len(), ptr::null_mut());
5824 assert!(matches!(err, super::TgmError::InvalidArg));
5825
5826 let mut out_buf: *const u8 = ptr::null();
5828 let mut out_len: usize = 0;
5829 let err = super::tgm_buffer_iter_next(ptr::null_mut(), &mut out_buf, &mut out_len);
5830 assert!(matches!(err, super::TgmError::InvalidArg));
5831
5832 let err = super::tgm_buffer_iter_next(ptr::null_mut(), ptr::null_mut(), &mut out_len);
5835 assert!(matches!(err, super::TgmError::InvalidArg));
5836
5837 assert_eq!(super::tgm_buffer_iter_count(ptr::null()), 0);
5839
5840 super::tgm_buffer_iter_free(ptr::null_mut());
5842 }
5843
5844 #[test]
5847 fn ffi_object_iter_round_trip() {
5848 let encoded = ffi_encode_single_f32_tensor(&[10.0f32, 20.0], "");
5849
5850 let mut iter: *mut super::TgmObjectIter = ptr::null_mut();
5851 let err = super::tgm_object_iter_create(
5852 encoded.as_ptr(),
5853 encoded.len(),
5854 0, 0, &mut iter,
5857 );
5858 assert!(matches!(err, super::TgmError::Ok));
5859 assert!(!iter.is_null());
5860
5861 let mut msg: *mut super::TgmMessage = ptr::null_mut();
5863 let err = super::tgm_object_iter_next(iter, &mut msg);
5864 assert!(matches!(err, super::TgmError::Ok));
5865 assert!(!msg.is_null());
5866
5867 assert_eq!(super::tgm_message_num_objects(msg), 1);
5868 assert_eq!(super::tgm_message_version(msg), 3);
5869
5870 let mut data_len: usize = 0;
5871 let dp = super::tgm_object_data(msg, 0, &mut data_len);
5872 assert!(!dp.is_null());
5873 assert_eq!(data_len, 8); super::tgm_message_free(msg);
5876
5877 let mut msg2: *mut super::TgmMessage = ptr::null_mut();
5879 let err = super::tgm_object_iter_next(iter, &mut msg2);
5880 assert!(matches!(err, super::TgmError::EndOfIter));
5881
5882 super::tgm_object_iter_free(iter);
5883 }
5884
5885 #[test]
5886 fn ffi_object_iter_null_args() {
5887 let mut iter: *mut super::TgmObjectIter = ptr::null_mut();
5888 let err = super::tgm_object_iter_create(ptr::null(), 0, 0, 0, &mut iter);
5889 assert!(matches!(err, super::TgmError::InvalidArg));
5890
5891 let data = [0u8; 10];
5892 let err = super::tgm_object_iter_create(data.as_ptr(), data.len(), 0, 0, ptr::null_mut());
5893 assert!(matches!(err, super::TgmError::InvalidArg));
5894
5895 let mut msg: *mut super::TgmMessage = ptr::null_mut();
5897 let err = super::tgm_object_iter_next(ptr::null_mut(), &mut msg);
5898 assert!(matches!(err, super::TgmError::InvalidArg));
5899
5900 let err = super::tgm_object_iter_next(ptr::null_mut(), ptr::null_mut());
5902 assert!(matches!(err, super::TgmError::InvalidArg));
5903
5904 super::tgm_object_iter_free(ptr::null_mut());
5906 }
5907
5908 #[test]
5911 fn ffi_file_iter_round_trip() {
5912 let dir = std::env::temp_dir();
5913 let path = dir.join("ffi_file_iter_test.tgm");
5914 let _ = std::fs::remove_file(&path);
5915
5916 let c_path = CString::new(path.to_str().unwrap()).unwrap();
5917
5918 let mut file: *mut super::TgmFile = ptr::null_mut();
5920 let err = super::tgm_file_create(c_path.as_ptr(), &mut file);
5921 assert!(matches!(err, super::TgmError::Ok));
5922
5923 let msg1 = ffi_encode_single_f32_tensor(&[1.0f32], "");
5924 let msg2 = ffi_encode_single_f32_tensor(&[2.0f32], "");
5925 let err = super::tgm_file_append_raw(file, msg1.as_ptr(), msg1.len());
5926 assert!(matches!(err, super::TgmError::Ok));
5927 let err = super::tgm_file_append_raw(file, msg2.as_ptr(), msg2.len());
5928 assert!(matches!(err, super::TgmError::Ok));
5929
5930 let mut iter: *mut super::TgmFileIter = ptr::null_mut();
5932 let err = super::tgm_file_iter_create(file, &mut iter);
5933 assert!(matches!(err, super::TgmError::Ok));
5934 assert!(!iter.is_null());
5935
5936 let mut out = super::TgmBytes {
5938 data: ptr::null_mut(),
5939 len: 0,
5940 };
5941 let err = super::tgm_file_iter_next(iter, &mut out);
5942 assert!(matches!(err, super::TgmError::Ok));
5943 assert!(!out.data.is_null());
5944 assert_eq!(out.len, msg1.len());
5945 super::tgm_bytes_free(out);
5946
5947 let mut out2 = super::TgmBytes {
5949 data: ptr::null_mut(),
5950 len: 0,
5951 };
5952 let err = super::tgm_file_iter_next(iter, &mut out2);
5953 assert!(matches!(err, super::TgmError::Ok));
5954 super::tgm_bytes_free(out2);
5955
5956 let mut out3 = super::TgmBytes {
5958 data: ptr::null_mut(),
5959 len: 0,
5960 };
5961 let err = super::tgm_file_iter_next(iter, &mut out3);
5962 assert!(matches!(err, super::TgmError::EndOfIter));
5963
5964 super::tgm_file_iter_free(iter);
5965 super::tgm_file_close(file);
5966 let _ = std::fs::remove_file(&path);
5967 }
5968
5969 #[test]
5970 fn ffi_file_iter_null_args() {
5971 let mut iter: *mut super::TgmFileIter = ptr::null_mut();
5972 let err = super::tgm_file_iter_create(ptr::null_mut(), &mut iter);
5973 assert!(matches!(err, super::TgmError::InvalidArg));
5974
5975 let err = super::tgm_file_iter_create(ptr::null_mut(), ptr::null_mut());
5976 assert!(matches!(err, super::TgmError::InvalidArg));
5977
5978 let mut out = super::TgmBytes {
5980 data: ptr::null_mut(),
5981 len: 0,
5982 };
5983 let err = super::tgm_file_iter_next(ptr::null_mut(), &mut out);
5984 assert!(matches!(err, super::TgmError::InvalidArg));
5985
5986 let err = super::tgm_file_iter_next(ptr::null_mut(), ptr::null_mut());
5987 assert!(matches!(err, super::TgmError::InvalidArg));
5988
5989 super::tgm_file_iter_free(ptr::null_mut());
5991 }
5992
5993 #[test]
5996 fn ffi_encode_decode_zero_objects() {
5997 let json = CString::new(r#"{"version":3,"descriptors":[],"source":"empty"}"#).unwrap();
5998 let mut out = super::TgmBytes {
5999 data: ptr::null_mut(),
6000 len: 0,
6001 };
6002 let err = super::tgm_encode(
6003 json.as_ptr(),
6004 ptr::null(),
6005 ptr::null(),
6006 0,
6007 ptr::null(),
6008 0,
6009 &mut out,
6010 );
6011 assert!(matches!(err, super::TgmError::Ok));
6012
6013 let encoded = unsafe { slice::from_raw_parts(out.data, out.len) }.to_vec();
6014 super::tgm_bytes_free(out);
6015
6016 let mut msg: *mut super::TgmMessage = ptr::null_mut();
6017 let err = super::tgm_decode(encoded.as_ptr(), encoded.len(), 0, 0, 0, &mut msg);
6018 assert!(matches!(err, super::TgmError::Ok));
6019
6020 assert_eq!(super::tgm_message_version(msg), 3);
6021 assert_eq!(super::tgm_message_num_objects(msg), 0);
6022
6023 super::tgm_message_free(msg);
6024 }
6025
6026 #[test]
6038 fn ffi_streaming_encoder_with_hash() {
6039 let dir = std::env::temp_dir();
6040 let path = dir.join("ffi_streaming_hash.tgm");
6041 let _ = std::fs::remove_file(&path);
6042
6043 let c_path = CString::new(path.to_str().unwrap()).unwrap();
6044 let meta_json = CString::new(r#"{"version":3}"#).unwrap();
6045 let hash_algo = CString::new("xxh3").unwrap();
6046
6047 let mut enc: *mut super::TgmStreamingEncoder = ptr::null_mut();
6048 let err = super::tgm_streaming_encoder_create(
6049 c_path.as_ptr(),
6050 meta_json.as_ptr(),
6051 hash_algo.as_ptr(),
6052 0,
6053 &mut enc,
6054 );
6055 assert!(matches!(err, super::TgmError::Ok));
6056
6057 let values = [1.0f32, 2.0];
6058 let data: Vec<u8> = values.iter().flat_map(|v| v.to_ne_bytes()).collect();
6059 let desc_json = CString::new(format!(
6060 r#"{{"type":"ntensor","ndim":1,"shape":[{len}],"strides":[1],"dtype":"float32","byte_order":"{bo}","encoding":"none","filter":"none","compression":"none"}}"#,
6061 len = values.len(),
6062 bo = if cfg!(target_endian = "little") { "little" } else { "big" },
6063 )).unwrap();
6064
6065 let err =
6066 super::tgm_streaming_encoder_write(enc, desc_json.as_ptr(), data.as_ptr(), data.len());
6067 assert!(matches!(err, super::TgmError::Ok));
6068
6069 let err = super::tgm_streaming_encoder_finish(enc);
6070 assert!(matches!(err, super::TgmError::Ok));
6071 super::tgm_streaming_encoder_free(enc);
6072
6073 let bytes = std::fs::read(&path).expect("read file");
6076 let mut msg: *mut super::TgmMessage = ptr::null_mut();
6077 let err = super::tgm_decode(bytes.as_ptr(), bytes.len(), 0, 0, 0, &mut msg);
6078 assert!(matches!(err, super::TgmError::Ok));
6079
6080 assert_eq!(super::tgm_payload_has_hash(msg, 0), 1);
6081 let ht = unsafe { CStr::from_ptr(super::tgm_object_hash_type(msg, 0)) }
6082 .to_str()
6083 .unwrap();
6084 assert_eq!(ht, "xxh3");
6085 let hv = unsafe { CStr::from_ptr(super::tgm_object_hash_value(msg, 0)) }
6086 .to_str()
6087 .unwrap();
6088 assert_eq!(hv.len(), 16, "xxh3 digest is 16 hex chars");
6089
6090 super::tgm_message_free(msg);
6091 let _ = std::fs::remove_file(&path);
6092 }
6093
6094 #[test]
6101 fn ffi_file_decode_surfaces_inline_hash() {
6102 let dir = std::env::temp_dir();
6107 let path = dir.join("ffi_file_hash.tgm");
6108 let _ = std::fs::remove_file(&path);
6109
6110 let c_path = CString::new(path.to_str().unwrap()).unwrap();
6111 let meta_json = CString::new(r#"{"version":3}"#).unwrap();
6112 let hash_algo = CString::new("xxh3").unwrap();
6113 let mut enc: *mut super::TgmStreamingEncoder = ptr::null_mut();
6114 assert!(matches!(
6115 super::tgm_streaming_encoder_create(
6116 c_path.as_ptr(),
6117 meta_json.as_ptr(),
6118 hash_algo.as_ptr(),
6119 0,
6120 &mut enc,
6121 ),
6122 super::TgmError::Ok
6123 ));
6124 let values = [1.0f32, 2.0, 3.0];
6125 let data: Vec<u8> = values.iter().flat_map(|v| v.to_ne_bytes()).collect();
6126 let desc_json = CString::new(format!(
6127 r#"{{"type":"ntensor","ndim":1,"shape":[{}],"strides":[1],"dtype":"float32","byte_order":"{}","encoding":"none","filter":"none","compression":"none"}}"#,
6128 values.len(),
6129 if cfg!(target_endian = "little") { "little" } else { "big" },
6130 )).unwrap();
6131 assert!(matches!(
6132 super::tgm_streaming_encoder_write(enc, desc_json.as_ptr(), data.as_ptr(), data.len(),),
6133 super::TgmError::Ok
6134 ));
6135 assert!(matches!(
6136 super::tgm_streaming_encoder_finish(enc),
6137 super::TgmError::Ok
6138 ));
6139 super::tgm_streaming_encoder_free(enc);
6140
6141 let mut file: *mut super::TgmFile = ptr::null_mut();
6143 assert!(matches!(
6144 super::tgm_file_open(c_path.as_ptr(), &mut file),
6145 super::TgmError::Ok
6146 ));
6147 let mut file_msg: *mut super::TgmMessage = ptr::null_mut();
6148 assert!(matches!(
6149 super::tgm_file_decode_message(file, 0, 0, 0, 0, &mut file_msg),
6150 super::TgmError::Ok
6151 ));
6152 let file_has = super::tgm_payload_has_hash(file_msg, 0);
6153 let file_hv_ptr = super::tgm_object_hash_value(file_msg, 0);
6154 assert_eq!(file_has, 1);
6155 assert!(!file_hv_ptr.is_null());
6156 let file_hv = unsafe { CStr::from_ptr(file_hv_ptr) }
6157 .to_str()
6158 .unwrap()
6159 .to_string();
6160
6161 let bytes = std::fs::read(&path).unwrap();
6163 let mut buf_msg: *mut super::TgmMessage = ptr::null_mut();
6164 assert!(matches!(
6165 super::tgm_decode(bytes.as_ptr(), bytes.len(), 0, 0, 0, &mut buf_msg),
6166 super::TgmError::Ok
6167 ));
6168 let buf_hv_ptr = super::tgm_object_hash_value(buf_msg, 0);
6169 let buf_hv = unsafe { CStr::from_ptr(buf_hv_ptr) }
6170 .to_str()
6171 .unwrap()
6172 .to_string();
6173
6174 assert_eq!(
6175 file_hv, buf_hv,
6176 "file-path and buffer-path hash values must agree"
6177 );
6178 assert_eq!(file_hv.len(), 16, "xxh3 digest is 16 hex chars");
6179
6180 super::tgm_message_free(file_msg);
6181 super::tgm_message_free(buf_msg);
6182 super::tgm_file_close(file);
6183 let _ = std::fs::remove_file(&path);
6184 }
6185
6186 #[test]
6189 fn ffi_streaming_encoder_invalid_hash_algo() {
6190 let dir = std::env::temp_dir();
6191 let path = dir.join("ffi_streaming_bad_hash.tgm");
6192 let c_path = CString::new(path.to_str().unwrap()).unwrap();
6193 let meta_json = CString::new(r#"{"version":3}"#).unwrap();
6194 let bad_algo = CString::new("bogus_hash").unwrap();
6195
6196 let mut enc: *mut super::TgmStreamingEncoder = ptr::null_mut();
6197 let err = super::tgm_streaming_encoder_create(
6198 c_path.as_ptr(),
6199 meta_json.as_ptr(),
6200 bad_algo.as_ptr(),
6201 0,
6202 &mut enc,
6203 );
6204 assert!(matches!(err, super::TgmError::InvalidArg));
6205 let _ = std::fs::remove_file(&path);
6206 }
6207
6208 #[test]
6211 fn ffi_streaming_encoder_invalid_metadata() {
6212 let dir = std::env::temp_dir();
6213 let path = dir.join("ffi_streaming_bad_meta.tgm");
6214 let c_path = CString::new(path.to_str().unwrap()).unwrap();
6215 let bad_meta = CString::new("not json").unwrap();
6216
6217 let mut enc: *mut super::TgmStreamingEncoder = ptr::null_mut();
6218 let err = super::tgm_streaming_encoder_create(
6219 c_path.as_ptr(),
6220 bad_meta.as_ptr(),
6221 ptr::null(),
6222 0,
6223 &mut enc,
6224 );
6225 assert!(matches!(err, super::TgmError::Metadata));
6226 let _ = std::fs::remove_file(&path);
6227 }
6228
6229 #[test]
6232 fn ffi_encode_decode_multiple_objects() {
6233 let vals1 = [1.0f32, 2.0];
6234 let vals2 = [10.0f32, 20.0, 30.0];
6235 let bo = if cfg!(target_endian = "little") {
6236 "little"
6237 } else {
6238 "big"
6239 };
6240
6241 let json = format!(
6242 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"}}]}}"#,
6243 len1 = vals1.len(),
6244 len2 = vals2.len(),
6245 bo = bo,
6246 );
6247
6248 let data1: Vec<u8> = vals1.iter().flat_map(|v| v.to_ne_bytes()).collect();
6249 let data2: Vec<u8> = vals2.iter().flat_map(|v| v.to_ne_bytes()).collect();
6250
6251 let c_json = CString::new(json).unwrap();
6252 let data_ptrs: [*const u8; 2] = [data1.as_ptr(), data2.as_ptr()];
6253 let data_lens: [usize; 2] = [data1.len(), data2.len()];
6254
6255 let mut out = super::TgmBytes {
6256 data: ptr::null_mut(),
6257 len: 0,
6258 };
6259 let err = super::tgm_encode(
6260 c_json.as_ptr(),
6261 data_ptrs.as_ptr(),
6262 data_lens.as_ptr(),
6263 2,
6264 ptr::null(),
6265 0,
6266 &mut out,
6267 );
6268 assert!(matches!(err, super::TgmError::Ok));
6269
6270 let encoded = unsafe { slice::from_raw_parts(out.data, out.len) }.to_vec();
6271 super::tgm_bytes_free(out);
6272
6273 let mut msg: *mut super::TgmMessage = ptr::null_mut();
6275 let err = super::tgm_decode(encoded.as_ptr(), encoded.len(), 0, 0, 0, &mut msg);
6276 assert!(matches!(err, super::TgmError::Ok));
6277 assert_eq!(super::tgm_message_num_objects(msg), 2);
6278
6279 let mut dl: usize = 0;
6281 let dp = super::tgm_object_data(msg, 0, &mut dl);
6282 let decoded0: Vec<f32> = unsafe { slice::from_raw_parts(dp, dl) }
6283 .chunks_exact(4)
6284 .map(|c| f32::from_ne_bytes(c.try_into().unwrap()))
6285 .collect();
6286 assert_eq!(decoded0, vals1);
6287
6288 let dp1 = super::tgm_object_data(msg, 1, &mut dl);
6290 let decoded1: Vec<f32> = unsafe { slice::from_raw_parts(dp1, dl) }
6291 .chunks_exact(4)
6292 .map(|c| f32::from_ne_bytes(c.try_into().unwrap()))
6293 .collect();
6294 assert_eq!(decoded1, vals2);
6295
6296 let shape0 = super::tgm_object_shape(msg, 0);
6298 assert_eq!(unsafe { *shape0 }, vals1.len() as u64);
6299 let shape1 = super::tgm_object_shape(msg, 1);
6300 assert_eq!(unsafe { *shape1 }, vals2.len() as u64);
6301
6302 super::tgm_message_free(msg);
6303 }
6304
6305 #[test]
6308 fn ffi_encode_decode_with_base_metadata() {
6309 let bo = if cfg!(target_endian = "little") {
6310 "little"
6311 } else {
6312 "big"
6313 };
6314 let json = format!(
6315 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"}}]}}"#,
6316 bo = bo,
6317 );
6318
6319 let data: Vec<u8> = [1.0f32, 2.0].iter().flat_map(|v| v.to_ne_bytes()).collect();
6320 let c_json = CString::new(json).unwrap();
6321 let data_ptr: *const u8 = data.as_ptr();
6322 let data_len: usize = data.len();
6323
6324 let mut out = super::TgmBytes {
6325 data: ptr::null_mut(),
6326 len: 0,
6327 };
6328 let err = super::tgm_encode(
6329 c_json.as_ptr(),
6330 &data_ptr as *const *const u8,
6331 &data_len as *const usize,
6332 1,
6333 ptr::null(),
6334 0,
6335 &mut out,
6336 );
6337 assert!(matches!(err, super::TgmError::Ok));
6338
6339 let encoded = unsafe { slice::from_raw_parts(out.data, out.len) }.to_vec();
6340 super::tgm_bytes_free(out);
6341
6342 let mut meta: *mut super::TgmMetadata = ptr::null_mut();
6344 let err = super::tgm_decode_metadata(encoded.as_ptr(), encoded.len(), &mut meta);
6345 assert!(matches!(err, super::TgmError::Ok));
6346
6347 let key = CString::new("param").unwrap();
6348 let val_ptr = super::tgm_metadata_get_string(meta, key.as_ptr());
6349 assert!(!val_ptr.is_null());
6350 let val_str = unsafe { CStr::from_ptr(val_ptr) }.to_str().unwrap();
6351 assert_eq!(val_str, "2t");
6352
6353 let key2 = CString::new("level").unwrap();
6354 let val_ptr2 = super::tgm_metadata_get_string(meta, key2.as_ptr());
6355 assert!(!val_ptr2.is_null());
6356 let val_str2 = unsafe { CStr::from_ptr(val_ptr2) }.to_str().unwrap();
6357 assert_eq!(val_str2, "surface");
6358
6359 super::tgm_metadata_free(meta);
6360 }
6361
6362 #[test]
6365 fn ffi_compute_hash_deterministic() {
6366 let data = b"deterministic hash test";
6367 let mut out1 = super::TgmBytes {
6368 data: ptr::null_mut(),
6369 len: 0,
6370 };
6371 let mut out2 = super::TgmBytes {
6372 data: ptr::null_mut(),
6373 len: 0,
6374 };
6375
6376 let err = super::tgm_compute_hash(data.as_ptr(), data.len(), ptr::null(), &mut out1);
6377 assert!(matches!(err, super::TgmError::Ok));
6378
6379 let err = super::tgm_compute_hash(data.as_ptr(), data.len(), ptr::null(), &mut out2);
6380 assert!(matches!(err, super::TgmError::Ok));
6381
6382 let hex1 = unsafe { slice::from_raw_parts(out1.data, out1.len) };
6383 let hex2 = unsafe { slice::from_raw_parts(out2.data, out2.len) };
6384 assert_eq!(hex1, hex2);
6385
6386 super::tgm_bytes_free(out1);
6387 super::tgm_bytes_free(out2);
6388 }
6389
6390 #[test]
6393 fn ffi_streaming_encoder_write_pre_encoded_round_trip() {
6394 let dir = std::env::temp_dir();
6395 let path = dir.join("ffi_streaming_pre_encoded.tgm");
6396 let _ = std::fs::remove_file(&path);
6397
6398 let c_path = CString::new(path.to_str().unwrap()).unwrap();
6399 let meta_json = CString::new(r#"{"version":3}"#).unwrap();
6400
6401 let mut enc: *mut super::TgmStreamingEncoder = ptr::null_mut();
6402 let err = super::tgm_streaming_encoder_create(
6403 c_path.as_ptr(),
6404 meta_json.as_ptr(),
6405 ptr::null(),
6406 0,
6407 &mut enc,
6408 );
6409 assert!(matches!(err, super::TgmError::Ok));
6410
6411 let values = [7.0f32, 8.0];
6412 let data: Vec<u8> = values.iter().flat_map(|v| v.to_ne_bytes()).collect();
6413 let desc_json = CString::new(format!(
6414 r#"{{"type":"ntensor","ndim":1,"shape":[{len}],"strides":[1],"dtype":"float32","byte_order":"{bo}","encoding":"none","filter":"none","compression":"none"}}"#,
6415 len = values.len(),
6416 bo = if cfg!(target_endian = "little") { "little" } else { "big" },
6417 )).unwrap();
6418
6419 let err = super::tgm_streaming_encoder_write_pre_encoded(
6420 enc,
6421 desc_json.as_ptr(),
6422 data.as_ptr(),
6423 data.len(),
6424 );
6425 assert!(matches!(err, super::TgmError::Ok));
6426
6427 let err = super::tgm_streaming_encoder_finish(enc);
6428 assert!(matches!(err, super::TgmError::Ok));
6429 super::tgm_streaming_encoder_free(enc);
6430
6431 let mut file: *mut super::TgmFile = ptr::null_mut();
6433 let err = super::tgm_file_open(c_path.as_ptr(), &mut file);
6434 assert!(matches!(err, super::TgmError::Ok));
6435
6436 let mut msg: *mut super::TgmMessage = ptr::null_mut();
6437 let err = super::tgm_file_decode_message(file, 0, 0, 0, 0, &mut msg);
6438 assert!(matches!(err, super::TgmError::Ok));
6439
6440 let mut dl: usize = 0;
6441 let dp = super::tgm_object_data(msg, 0, &mut dl);
6442 let decoded: Vec<f32> = unsafe { slice::from_raw_parts(dp, dl) }
6443 .chunks_exact(4)
6444 .map(|c| f32::from_ne_bytes(c.try_into().unwrap()))
6445 .collect();
6446 assert_eq!(decoded, values);
6447
6448 super::tgm_message_free(msg);
6449 super::tgm_file_close(file);
6450 let _ = std::fs::remove_file(&path);
6451 }
6452
6453 #[test]
6456 fn ffi_encode_invalid_hash_algo() {
6457 let json = CString::new(r#"{"version":3,"descriptors":[]}"#).unwrap();
6458 let bad_algo = CString::new("bogus").unwrap();
6459 let mut out = super::TgmBytes {
6460 data: ptr::null_mut(),
6461 len: 0,
6462 };
6463
6464 let err = super::tgm_encode(
6465 json.as_ptr(),
6466 ptr::null(),
6467 ptr::null(),
6468 0,
6469 bad_algo.as_ptr(),
6470 0,
6471 &mut out,
6472 );
6473 assert!(matches!(err, super::TgmError::InvalidArg));
6474 }
6475
6476 #[test]
6479 fn ffi_scan_entry_oob_returns_sentinel() {
6480 let encoded = ffi_encode_single_f32_tensor(&[1.0f32], "");
6481
6482 let mut result: *mut super::TgmScanResult = ptr::null_mut();
6483 let err = super::tgm_scan(encoded.as_ptr(), encoded.len(), &mut result);
6484 assert!(matches!(err, super::TgmError::Ok));
6485
6486 assert_eq!(super::tgm_scan_count(result), 1);
6487
6488 let good = super::tgm_scan_entry(result, 0);
6490 assert_eq!(good.offset, 0);
6491 assert!(good.length > 0);
6492
6493 let bad = super::tgm_scan_entry(result, 1);
6495 assert_eq!(bad.offset, usize::MAX);
6496 assert_eq!(bad.length, 0);
6497
6498 let err_ptr = super::tgm_last_error();
6500 assert!(!err_ptr.is_null());
6501 let err_str = unsafe { CStr::from_ptr(err_ptr) }.to_str().unwrap();
6502 assert!(
6503 err_str.contains("out of range"),
6504 "expected OOB error, got: {err_str}"
6505 );
6506
6507 super::tgm_scan_free(result);
6508 }
6509
6510 #[test]
6513 fn ffi_encode_zero_length_null_data_accepted() {
6514 let json = CString::new(
6517 r#"{"version":3,"descriptors":[{"type":"ntensor","ndim":1,"shape":[0],"strides":[1],"dtype":"float32","byte_order":"little","encoding":"none","filter":"none","compression":"none"}]}"#,
6518 )
6519 .unwrap();
6520 let data_ptrs: [*const u8; 1] = [ptr::null()];
6521 let data_lens: [usize; 1] = [0];
6522 let mut out = super::TgmBytes {
6523 data: ptr::null_mut(),
6524 len: 0,
6525 };
6526
6527 let err = super::tgm_encode(
6528 json.as_ptr(),
6529 data_ptrs.as_ptr(),
6530 data_lens.as_ptr(),
6531 1,
6532 ptr::null(), 0, &mut out,
6535 );
6536 assert!(
6537 matches!(err, super::TgmError::Ok),
6538 "encoding zero-length data with null pointer should succeed"
6539 );
6540 if !out.data.is_null() {
6541 super::tgm_bytes_free(out);
6542 }
6543 }
6544
6545 #[test]
6550 fn ffi_validate_null_out() {
6551 let err = super::tgm_validate(ptr::null(), 0, ptr::null(), 0, ptr::null_mut());
6552 assert!(matches!(err, super::TgmError::InvalidArg));
6553 }
6554
6555 #[test]
6556 fn ffi_validate_null_buf_with_nonzero_len() {
6557 let mut out = super::TgmBytes {
6558 data: ptr::null_mut(),
6559 len: 0,
6560 };
6561 let err = super::tgm_validate(ptr::null(), 42, ptr::null(), 0, &mut out);
6562 assert!(matches!(err, super::TgmError::InvalidArg));
6563 }
6564
6565 #[test]
6566 fn ffi_validate_empty_buffer_ok() {
6567 let mut out = super::TgmBytes {
6569 data: ptr::null_mut(),
6570 len: 0,
6571 };
6572 let err = super::tgm_validate(ptr::null(), 0, ptr::null(), 0, &mut out);
6573 assert!(matches!(err, super::TgmError::Ok));
6574 assert!(!out.data.is_null());
6575 assert!(out.len > 0);
6576 super::tgm_bytes_free(out);
6577 }
6578
6579 #[test]
6580 fn ffi_validate_valid_message_all_levels() {
6581 let encoded = ffi_encode_single_f32_tensor(&[1.0f32, 2.0, 3.0, 4.0], "");
6582 for level_str in &["quick", "checksum", "default", "full"] {
6583 let level = CString::new(*level_str).unwrap();
6584 let mut out = super::TgmBytes {
6585 data: ptr::null_mut(),
6586 len: 0,
6587 };
6588 let err =
6589 super::tgm_validate(encoded.as_ptr(), encoded.len(), level.as_ptr(), 0, &mut out);
6590 assert!(matches!(err, super::TgmError::Ok), "level {level_str}");
6591 super::tgm_bytes_free(out);
6592 }
6593 }
6594
6595 #[test]
6596 fn ffi_validate_canonical_flag() {
6597 let encoded = ffi_encode_single_f32_tensor(&[1.0f32], "");
6598 let mut out = super::TgmBytes {
6599 data: ptr::null_mut(),
6600 len: 0,
6601 };
6602 let err = super::tgm_validate(
6603 encoded.as_ptr(),
6604 encoded.len(),
6605 ptr::null(),
6606 1, &mut out,
6608 );
6609 assert!(matches!(err, super::TgmError::Ok));
6610 super::tgm_bytes_free(out);
6611 }
6612
6613 #[test]
6614 fn ffi_validate_invalid_level_string() {
6615 let encoded = ffi_encode_single_f32_tensor(&[1.0f32], "");
6616 let bogus = CString::new("bogus-level-name").unwrap();
6617 let mut out = super::TgmBytes {
6618 data: ptr::null_mut(),
6619 len: 0,
6620 };
6621 let err = super::tgm_validate(encoded.as_ptr(), encoded.len(), bogus.as_ptr(), 0, &mut out);
6622 assert!(matches!(err, super::TgmError::InvalidArg));
6623 }
6624
6625 #[test]
6626 fn ffi_validate_garbage_reports_issues() {
6627 let garbage = [0xDEu8; 100];
6628 let mut out = super::TgmBytes {
6629 data: ptr::null_mut(),
6630 len: 0,
6631 };
6632 let err = super::tgm_validate(garbage.as_ptr(), garbage.len(), ptr::null(), 0, &mut out);
6633 assert!(matches!(err, super::TgmError::Ok));
6634 let json = unsafe { slice::from_raw_parts(out.data, out.len) };
6635 let s = std::str::from_utf8(json).unwrap();
6636 assert!(s.contains("issues"));
6637 super::tgm_bytes_free(out);
6638 }
6639
6640 #[test]
6643 fn ffi_validate_file_null_args() {
6644 let mut out = super::TgmBytes {
6645 data: ptr::null_mut(),
6646 len: 0,
6647 };
6648 let err = super::tgm_validate_file(ptr::null(), ptr::null(), 0, &mut out);
6649 assert!(matches!(err, super::TgmError::InvalidArg));
6650 let path = CString::new("/tmp/x.tgm").unwrap();
6651 let err = super::tgm_validate_file(path.as_ptr(), ptr::null(), 0, ptr::null_mut());
6652 assert!(matches!(err, super::TgmError::InvalidArg));
6653 }
6654
6655 #[test]
6656 fn ffi_validate_file_nonexistent() {
6657 let path = CString::new("/nonexistent/path/to/missing-file.tgm").unwrap();
6658 let mut out = super::TgmBytes {
6659 data: ptr::null_mut(),
6660 len: 0,
6661 };
6662 let err = super::tgm_validate_file(path.as_ptr(), ptr::null(), 0, &mut out);
6663 assert!(matches!(err, super::TgmError::Io));
6664 }
6665
6666 #[test]
6667 fn ffi_validate_file_valid_round_trip() {
6668 use std::io::Write;
6669 let encoded = ffi_encode_single_f32_tensor(&[1.0f32, 2.0, 3.0], "");
6670 let tmp = std::env::temp_dir().join(format!(
6671 "tensogram-ffi-validate-file-{}.tgm",
6672 std::process::id(),
6673 ));
6674 std::fs::File::create(&tmp)
6675 .unwrap()
6676 .write_all(&encoded)
6677 .unwrap();
6678 let path = CString::new(tmp.to_str().unwrap()).unwrap();
6679 let mut out = super::TgmBytes {
6680 data: ptr::null_mut(),
6681 len: 0,
6682 };
6683 let err = super::tgm_validate_file(path.as_ptr(), ptr::null(), 0, &mut out);
6684 assert!(matches!(err, super::TgmError::Ok));
6685 super::tgm_bytes_free(out);
6686 let _ = std::fs::remove_file(&tmp);
6687 }
6688
6689 #[test]
6690 fn ffi_validate_file_invalid_level() {
6691 let path = CString::new("/tmp/dummy.tgm").unwrap();
6692 let level = CString::new("bogus").unwrap();
6693 let mut out = super::TgmBytes {
6694 data: ptr::null_mut(),
6695 len: 0,
6696 };
6697 let err = super::tgm_validate_file(path.as_ptr(), level.as_ptr(), 0, &mut out);
6698 assert!(matches!(err, super::TgmError::InvalidArg));
6699 }
6700
6701 #[test]
6713 fn ffi_validate_detects_tampered_payload() {
6714 let values = vec![1.0f32; 256];
6715 let encoded = ffi_encode_with_hash(&values);
6716 let mut tampered = encoded.clone();
6717 let pos = (tampered.len() * 75) / 100;
6720 tampered[pos] ^= 0xFF;
6721 tampered[pos + 1] ^= 0xFF;
6722
6723 let mut out = super::TgmBytes {
6724 data: ptr::null_mut(),
6725 len: 0,
6726 };
6727 let level = CString::new("checksum").unwrap();
6728 let err = super::tgm_validate(
6729 tampered.as_ptr(),
6730 tampered.len(),
6731 level.as_ptr(),
6732 0,
6733 &mut out,
6734 );
6735 assert!(matches!(err, super::TgmError::Ok));
6739 assert!(!out.data.is_null());
6740 let json_bytes = unsafe { slice::from_raw_parts(out.data, out.len) };
6741 let json = std::str::from_utf8(json_bytes).unwrap();
6742 assert!(
6743 json.contains("HashMismatch") || json.contains("hash mismatch"),
6744 "expected HashMismatch in validate report, got: {json}"
6745 );
6746 super::tgm_bytes_free(out);
6747 }
6748
6749 #[test]
6752 fn ffi_object_data_null_out_len_no_crash() {
6753 let encoded = ffi_encode_single_f32_tensor(&[1.0f32], "");
6754 let mut msg: *mut super::TgmMessage = ptr::null_mut();
6755 let err = super::tgm_decode(
6756 encoded.as_ptr(),
6757 encoded.len(),
6758 0,
6759 0,
6760 0, &mut msg,
6762 );
6763 assert!(matches!(err, super::TgmError::Ok));
6764 let data = super::tgm_object_data(msg, 0, ptr::null_mut());
6766 assert!(!data.is_null());
6767 super::tgm_message_free(msg);
6768 }
6769
6770 #[test]
6773 fn ffi_decode_range_on_compressed_without_offsets() {
6774 let json = CString::new(
6777 r#"{"version":3,"descriptors":[{"type":"ntensor","ndim":1,"shape":[100],"strides":[1],"dtype":"float32","byte_order":"little","encoding":"none","filter":"none","compression":"zstd"}]}"#,
6778 )
6779 .unwrap();
6780 let data: Vec<u8> = vec![0u8; 400];
6781 let data_ptr: *const u8 = data.as_ptr();
6782 let data_len = data.len();
6783 let mut out = super::TgmBytes {
6784 data: ptr::null_mut(),
6785 len: 0,
6786 };
6787 let err = super::tgm_encode(
6788 json.as_ptr(),
6789 &data_ptr as *const *const u8,
6790 &data_len as *const usize,
6791 1,
6792 ptr::null(),
6793 0,
6794 &mut out,
6795 );
6796 assert!(matches!(err, super::TgmError::Ok));
6797 let encoded = unsafe { slice::from_raw_parts(out.data, out.len) }.to_vec();
6798 super::tgm_bytes_free(out);
6799
6800 let range_offset: u64 = 10;
6802 let range_count: u64 = 20;
6803 let mut out_buf = super::TgmBytes {
6804 data: ptr::null_mut(),
6805 len: 0,
6806 };
6807 let mut out_count: usize = 0;
6808 let err = super::tgm_decode_range(
6809 encoded.as_ptr(),
6810 encoded.len(),
6811 0,
6812 &range_offset as *const u64,
6813 &range_count as *const u64,
6814 1,
6815 0,
6816 0,
6817 1,
6818 &mut out_buf,
6819 &mut out_count,
6820 );
6821 assert!(!matches!(err, super::TgmError::Ok));
6822 }
6823
6824 #[test]
6827 fn ffi_simple_packing_null_values() {
6828 let mut ref_val: f64 = 0.0;
6829 let mut bsf: i32 = 0;
6830 let err =
6831 super::tgm_simple_packing_compute_params(ptr::null(), 0, 16, 0, &mut ref_val, &mut bsf);
6832 assert!(matches!(err, super::TgmError::InvalidArg));
6833 }
6834
6835 #[test]
6836 fn ffi_simple_packing_null_out_ref() {
6837 let values: [f64; 3] = [1.0, 2.0, 3.0];
6838 let mut bsf: i32 = 0;
6839 let err = super::tgm_simple_packing_compute_params(
6840 values.as_ptr(),
6841 3,
6842 16,
6843 0,
6844 ptr::null_mut(),
6845 &mut bsf,
6846 );
6847 assert!(matches!(err, super::TgmError::InvalidArg));
6848 }
6849
6850 #[test]
6851 fn ffi_simple_packing_null_out_bsf() {
6852 let values: [f64; 3] = [1.0, 2.0, 3.0];
6853 let mut ref_val: f64 = 0.0;
6854 let err = super::tgm_simple_packing_compute_params(
6855 values.as_ptr(),
6856 3,
6857 16,
6858 0,
6859 &mut ref_val,
6860 ptr::null_mut(),
6861 );
6862 assert!(matches!(err, super::TgmError::InvalidArg));
6863 }
6864
6865 #[test]
6870 fn ffi_encode_with_options_null_mask_ptr() {
6871 let values = [1.0f32, 2.0, 3.0, 4.0];
6872 let data: Vec<u8> = values.iter().flat_map(|v| v.to_ne_bytes()).collect();
6873 let json = format!(
6874 r#"{{"version":3,"descriptors":[{{"type":"ntensor","ndim":1,"shape":[{}],"strides":[1],"dtype":"float32","byte_order":"{}","encoding":"none","filter":"none","compression":"none"}}]}}"#,
6875 values.len(),
6876 if cfg!(target_endian = "little") {
6877 "little"
6878 } else {
6879 "big"
6880 },
6881 );
6882 let c_json = CString::new(json).unwrap();
6883 let hash_algo = CString::new("xxh3").unwrap();
6884 let data_ptr: *const u8 = data.as_ptr();
6885 let data_len: usize = data.len();
6886
6887 let mut out = super::TgmBytes {
6888 data: ptr::null_mut(),
6889 len: 0,
6890 };
6891 let err = super::tgm_encode_with_options(
6892 c_json.as_ptr(),
6893 &data_ptr as *const *const u8,
6894 &data_len as *const usize,
6895 1,
6896 hash_algo.as_ptr(),
6897 0, ptr::null(), &mut out,
6900 );
6901 assert!(matches!(err, super::TgmError::Ok));
6902 assert!(!out.data.is_null() && out.len > 0);
6903 super::tgm_bytes_free(out);
6904 }
6905
6906 #[test]
6910 fn ffi_decode_with_options_null_mask_ptr() {
6911 let values = [1.0f32, 2.0];
6912 let encoded = ffi_encode_single_f32_tensor(&values, "");
6913
6914 let mut msg: *mut super::TgmMessage = ptr::null_mut();
6915 let err = super::tgm_decode_with_options(
6916 encoded.as_ptr(),
6917 encoded.len(),
6918 0,
6919 0,
6920 0, ptr::null(), &mut msg,
6923 );
6924 assert!(matches!(err, super::TgmError::Ok));
6925 assert!(!msg.is_null());
6926 super::tgm_message_free(msg);
6927 }
6928
6929 #[test]
6933 fn ffi_decode_with_options_explicit_restore_false() {
6934 let values = [1.0f32, 2.0];
6935 let encoded = ffi_encode_single_f32_tensor(&values, "");
6936
6937 let mask_opts = super::TgmDecodeMaskOptions {
6938 restore_non_finite: false,
6939 };
6940 let mut msg: *mut super::TgmMessage = ptr::null_mut();
6941 let err = super::tgm_decode_with_options(
6942 encoded.as_ptr(),
6943 encoded.len(),
6944 0,
6945 0,
6946 0, &mask_opts,
6948 &mut msg,
6949 );
6950 assert!(matches!(err, super::TgmError::Ok));
6951 super::tgm_message_free(msg);
6952 }
6953
6954 #[test]
6957 fn ffi_decode_with_options_null_out() {
6958 let err =
6959 super::tgm_decode_with_options(b"x".as_ptr(), 1, 0, 0, 0, ptr::null(), ptr::null_mut());
6960 assert!(matches!(err, super::TgmError::InvalidArg));
6961 let msg = unsafe { CStr::from_ptr(super::tgm_last_error()) }
6962 .to_str()
6963 .unwrap();
6964 assert!(msg.contains("null"), "expected null-arg msg, got: {msg}");
6965 }
6966
6967 #[test]
6970 fn ffi_doctor_to_json_returns_parseable_json() {
6971 let mut out = super::TgmBytes {
6972 data: ptr::null_mut(),
6973 len: 0,
6974 };
6975 let err = super::tgm_doctor_to_json(&mut out);
6976 assert!(matches!(err, super::TgmError::Ok));
6977 assert!(!out.data.is_null());
6978 assert!(out.len > 0);
6979
6980 let json_bytes = unsafe { slice::from_raw_parts(out.data, out.len) };
6981 let json_str = std::str::from_utf8(json_bytes).expect("doctor JSON is UTF-8");
6982 let parsed: serde_json::Value = serde_json::from_str(json_str).expect("doctor JSON parses");
6983
6984 let obj = parsed.as_object().expect("top-level object");
6986 for key in ["build", "features", "self_test"] {
6987 assert!(obj.contains_key(key), "missing key '{key}' in: {obj:?}");
6988 }
6989
6990 super::tgm_bytes_free(out);
6991 }
6992
6993 #[test]
6994 fn ffi_doctor_to_json_null_out() {
6995 let err = super::tgm_doctor_to_json(ptr::null_mut());
6996 assert!(matches!(err, super::TgmError::InvalidArg));
6997 let msg = unsafe { CStr::from_ptr(super::tgm_last_error()) }
6998 .to_str()
6999 .unwrap();
7000 assert!(msg.contains("null"), "expected null-arg msg, got: {msg}");
7001 }
7002}
7003
7004#[unsafe(no_mangle)]
7009pub extern "C" fn tgm_compute_hash(
7010 data: *const u8,
7011 data_len: usize,
7012 algo: *const c_char,
7013 out: *mut TgmBytes,
7014) -> TgmError {
7015 if data.is_null() || out.is_null() {
7016 set_last_error("null argument");
7017 return TgmError::InvalidArg;
7018 }
7019
7020 if !algo.is_null() {
7028 let s = match unsafe { CStr::from_ptr(algo) }.to_str() {
7029 Ok(s) => s,
7030 Err(_) => {
7031 set_last_error("invalid UTF-8 in algo");
7032 return TgmError::InvalidArg;
7033 }
7034 };
7035 if let Err(e) = parse_hash_name(Some(s)) {
7036 set_last_error(&e.to_string());
7037 return TgmError::InvalidArg;
7038 }
7039 }
7040
7041 let input = unsafe { slice::from_raw_parts(data, data_len) };
7042 let hex = tensogram::hash::compute_hash(input);
7043 let mut bytes = hex.into_bytes().into_boxed_slice().into_vec();
7045 let result = TgmBytes {
7046 data: bytes.as_mut_ptr(),
7047 len: bytes.len(),
7048 };
7049 std::mem::forget(bytes);
7050 unsafe {
7051 *out = result;
7052 }
7053 TgmError::Ok
7054}
7055
7056#[unsafe(no_mangle)]
7090pub extern "C" fn tgm_doctor_to_json(out: *mut TgmBytes) -> TgmError {
7091 if out.is_null() {
7092 set_last_error("null out pointer");
7093 return TgmError::InvalidArg;
7094 }
7095
7096 let report = tensogram::doctor::run_diagnostics();
7097 let json = match serde_json::to_string(&report) {
7098 Ok(s) => s,
7099 Err(e) => {
7100 set_last_error(&format!("failed to serialise doctor report: {e}"));
7101 return TgmError::Encoding;
7102 }
7103 };
7104
7105 let mut bytes = json.into_bytes().into_boxed_slice().into_vec();
7107 let result = TgmBytes {
7108 data: bytes.as_mut_ptr(),
7109 len: bytes.len(),
7110 };
7111 std::mem::forget(bytes); unsafe {
7113 *out = result;
7114 }
7115 TgmError::Ok
7116}
7117
7118pub struct TgmStreamingEncoder {
7124 inner: Option<StreamingEncoder<std::io::BufWriter<std::fs::File>>>,
7125}
7126
7127#[derive(serde::Deserialize)]
7135struct StreamingEncodeJson {
7136 #[serde(default)]
7137 version: Option<u16>,
7138 #[serde(default)]
7139 base: Vec<BTreeMap<String, serde_json::Value>>,
7140 #[serde(flatten)]
7141 extra: BTreeMap<String, serde_json::Value>,
7142}
7143
7144pub(crate) fn parse_streaming_metadata_json(json_str: &str) -> Result<GlobalMetadata, String> {
7146 let parsed: StreamingEncodeJson = serde_json::from_str(json_str)
7147 .map_err(|e| format!("failed to parse metadata JSON: {e}"))?;
7148
7149 let cbor_base: Vec<BTreeMap<String, ciborium::Value>> = parsed
7150 .base
7151 .into_iter()
7152 .map(|entry| {
7153 entry
7154 .into_iter()
7155 .map(|(k, v)| (k, json_to_cbor(v)))
7156 .collect()
7157 })
7158 .collect();
7159
7160 for (i, entry) in cbor_base.iter().enumerate() {
7162 if entry.contains_key(RESERVED_KEY) {
7163 return Err(format!(
7164 "base[{i}] must not contain '{RESERVED_KEY}' key — the encoder populates it"
7165 ));
7166 }
7167 }
7168
7169 let cbor_extra = merge_flattened_extras_with_version(parsed.extra, parsed.version)?;
7173 Ok(GlobalMetadata {
7174 base: cbor_base,
7175 extra: cbor_extra,
7176 ..Default::default()
7177 })
7178}
7179
7180#[unsafe(no_mangle)]
7194pub extern "C" fn tgm_streaming_encoder_create(
7195 path: *const c_char,
7196 metadata_json: *const c_char,
7197 hash_algo: *const c_char,
7198 threads: u32,
7199 out: *mut *mut TgmStreamingEncoder,
7200) -> TgmError {
7201 if path.is_null() || metadata_json.is_null() || out.is_null() {
7202 set_last_error("null argument");
7203 return TgmError::InvalidArg;
7204 }
7205
7206 let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
7207 Ok(s) => s,
7208 Err(e) => {
7209 set_last_error(&format!("invalid UTF-8 in path: {e}"));
7210 return TgmError::InvalidArg;
7211 }
7212 };
7213
7214 let json_str = match unsafe { CStr::from_ptr(metadata_json) }.to_str() {
7215 Ok(s) => s,
7216 Err(e) => {
7217 set_last_error(&format!("invalid UTF-8 in metadata_json: {e}"));
7218 return TgmError::InvalidArg;
7219 }
7220 };
7221
7222 let global_metadata = match parse_streaming_metadata_json(json_str) {
7223 Ok(m) => m,
7224 Err(e) => {
7225 set_last_error(&e);
7226 return TgmError::Metadata;
7227 }
7228 };
7229
7230 let hashing = match parse_hash_algo(hash_algo) {
7231 Ok(b) => b,
7232 Err((code, msg)) => {
7233 set_last_error(&msg);
7234 return code;
7235 }
7236 };
7237
7238 let file = match std::fs::File::create(path_str) {
7239 Ok(f) => f,
7240 Err(e) => {
7241 set_last_error(&e.to_string());
7242 return TgmError::Io;
7243 }
7244 };
7245
7246 let options = EncodeOptions {
7247 hashing,
7248 threads,
7249 ..Default::default()
7250 };
7251 let writer = std::io::BufWriter::new(file);
7252
7253 match StreamingEncoder::new(writer, &global_metadata, &options) {
7254 Ok(enc) => {
7255 let handle = Box::new(TgmStreamingEncoder { inner: Some(enc) });
7256 unsafe {
7257 *out = Box::into_raw(handle);
7258 }
7259 TgmError::Ok
7260 }
7261 Err(e) => {
7262 set_last_error(&e.to_string());
7263 to_error_code(&e)
7264 }
7265 }
7266}
7267
7268#[unsafe(no_mangle)]
7277pub extern "C" fn tgm_streaming_encoder_write_preceder(
7278 enc: *mut TgmStreamingEncoder,
7279 metadata_json: *const c_char,
7280) -> TgmError {
7281 if enc.is_null() || metadata_json.is_null() {
7282 set_last_error("null argument");
7283 return TgmError::InvalidArg;
7284 }
7285
7286 let json_str = match unsafe { CStr::from_ptr(metadata_json) }.to_str() {
7287 Ok(s) => s,
7288 Err(e) => {
7289 set_last_error(&format!("invalid UTF-8 in metadata_json: {e}"));
7290 return TgmError::InvalidArg;
7291 }
7292 };
7293
7294 let map: BTreeMap<String, ciborium::Value> =
7295 match serde_json::from_str::<serde_json::Value>(json_str) {
7296 Ok(serde_json::Value::Object(obj)) => {
7297 obj.into_iter().map(|(k, v)| (k, json_to_cbor(v))).collect()
7298 }
7299 Ok(_) => {
7300 set_last_error("metadata_json must be a JSON object");
7301 return TgmError::Metadata;
7302 }
7303 Err(e) => {
7304 set_last_error(&format!("failed to parse metadata JSON: {e}"));
7305 return TgmError::Metadata;
7306 }
7307 };
7308
7309 let encoder = unsafe { &mut *enc };
7310 match encoder.inner.as_mut() {
7311 Some(inner) => match inner.write_preceder(map) {
7312 Ok(()) => TgmError::Ok,
7313 Err(e) => {
7314 set_last_error(&e.to_string());
7315 to_error_code(&e)
7316 }
7317 },
7318 None => {
7319 set_last_error("streaming encoder already finished");
7320 TgmError::InvalidArg
7321 }
7322 }
7323}
7324
7325#[unsafe(no_mangle)]
7331pub extern "C" fn tgm_streaming_encoder_write(
7332 enc: *mut TgmStreamingEncoder,
7333 descriptor_json: *const c_char,
7334 data: *const u8,
7335 data_len: usize,
7336) -> TgmError {
7337 if enc.is_null() || descriptor_json.is_null() || data.is_null() {
7338 set_last_error("null argument");
7339 return TgmError::InvalidArg;
7340 }
7341
7342 let json_str = match unsafe { CStr::from_ptr(descriptor_json) }.to_str() {
7343 Ok(s) => s,
7344 Err(e) => {
7345 set_last_error(&format!("invalid UTF-8 in descriptor_json: {e}"));
7346 return TgmError::InvalidArg;
7347 }
7348 };
7349
7350 let descriptor: DataObjectDescriptor = match serde_json::from_str(json_str) {
7351 Ok(d) => d,
7352 Err(e) => {
7353 set_last_error(&format!("failed to parse descriptor JSON: {e}"));
7354 return TgmError::Metadata;
7355 }
7356 };
7357
7358 let data_slice = unsafe { slice::from_raw_parts(data, data_len) };
7359 let encoder = unsafe { &mut *enc };
7360
7361 match encoder.inner.as_mut() {
7362 Some(inner) => match inner.write_object(&descriptor, data_slice) {
7363 Ok(()) => TgmError::Ok,
7364 Err(e) => {
7365 set_last_error(&e.to_string());
7366 to_error_code(&e)
7367 }
7368 },
7369 None => {
7370 set_last_error("streaming encoder already finished");
7371 TgmError::InvalidArg
7372 }
7373 }
7374}
7375
7376#[unsafe(no_mangle)]
7396pub extern "C" fn tgm_streaming_encoder_write_pre_encoded(
7397 enc: *mut TgmStreamingEncoder,
7398 descriptor_json: *const c_char,
7399 data: *const u8,
7400 data_len: usize,
7401) -> TgmError {
7402 if enc.is_null() || descriptor_json.is_null() || data.is_null() {
7403 set_last_error("null argument");
7404 return TgmError::InvalidArg;
7405 }
7406
7407 let json_str = match unsafe { CStr::from_ptr(descriptor_json) }.to_str() {
7408 Ok(s) => s,
7409 Err(e) => {
7410 set_last_error(&format!("invalid UTF-8 in descriptor_json: {e}"));
7411 return TgmError::InvalidArg;
7412 }
7413 };
7414
7415 let descriptor: DataObjectDescriptor = match serde_json::from_str(json_str) {
7416 Ok(d) => d,
7417 Err(e) => {
7418 set_last_error(&format!("failed to parse descriptor JSON: {e}"));
7419 return TgmError::Metadata;
7420 }
7421 };
7422
7423 let data_slice = unsafe { slice::from_raw_parts(data, data_len) };
7424 let encoder = unsafe { &mut *enc };
7425
7426 match encoder.inner.as_mut() {
7427 Some(inner) => match inner.write_object_pre_encoded(&descriptor, data_slice) {
7428 Ok(()) => TgmError::Ok,
7429 Err(e) => {
7430 set_last_error(&e.to_string());
7431 to_error_code(&e)
7432 }
7433 },
7434 None => {
7435 set_last_error("streaming encoder already finished");
7436 TgmError::InvalidArg
7437 }
7438 }
7439}
7440
7441#[unsafe(no_mangle)]
7443pub extern "C" fn tgm_streaming_encoder_count(enc: *const TgmStreamingEncoder) -> usize {
7444 if enc.is_null() {
7445 return 0;
7446 }
7447 unsafe { (*enc).inner.as_ref().map(|e| e.object_count()).unwrap_or(0) }
7448}
7449
7450#[unsafe(no_mangle)]
7455pub extern "C" fn tgm_streaming_encoder_finish(enc: *mut TgmStreamingEncoder) -> TgmError {
7456 if enc.is_null() {
7457 set_last_error("null argument");
7458 return TgmError::InvalidArg;
7459 }
7460
7461 let encoder = unsafe { &mut *enc };
7462 match encoder.inner.take() {
7463 Some(inner) => match inner.finish() {
7464 Ok(_writer) => {
7465 TgmError::Ok
7468 }
7469 Err(e) => {
7470 set_last_error(&e.to_string());
7471 to_error_code(&e)
7472 }
7473 },
7474 None => {
7475 set_last_error("streaming encoder already finished");
7476 TgmError::InvalidArg
7477 }
7478 }
7479}
7480
7481#[unsafe(no_mangle)]
7483pub extern "C" fn tgm_streaming_encoder_free(enc: *mut TgmStreamingEncoder) {
7484 if !enc.is_null() {
7485 unsafe {
7486 drop(Box::from_raw(enc));
7487 }
7488 }
7489}
7490
7491fn parse_validate_options(
7497 level: *const c_char,
7498 check_canonical: i32,
7499) -> Result<ValidateOptions, (TgmError, String)> {
7500 let level_str = if level.is_null() {
7501 "default"
7502 } else {
7503 unsafe { CStr::from_ptr(level) }
7504 .to_str()
7505 .map_err(|_| (TgmError::InvalidArg, "invalid UTF-8 in level".to_string()))?
7506 };
7507
7508 let (max_level, checksum_only) = match level_str {
7509 "quick" => (ValidationLevel::Structure, false),
7510 "default" => (ValidationLevel::Integrity, false),
7511 "checksum" => (ValidationLevel::Integrity, true),
7512 "full" => (ValidationLevel::Fidelity, false),
7513 other => {
7514 return Err((
7515 TgmError::InvalidArg,
7516 format!(
7517 "unknown validation level: '{}', expected one of: quick, default, checksum, full",
7518 other
7519 ),
7520 ));
7521 }
7522 };
7523
7524 Ok(ValidateOptions {
7525 max_level,
7526 check_canonical: check_canonical != 0,
7527 checksum_only,
7528 })
7529}
7530
7531#[unsafe(no_mangle)]
7549pub extern "C" fn tgm_validate(
7550 buf: *const u8,
7551 buf_len: usize,
7552 level: *const c_char,
7553 check_canonical: i32,
7554 out: *mut TgmBytes,
7555) -> TgmError {
7556 if out.is_null() {
7557 set_last_error("null argument");
7558 return TgmError::InvalidArg;
7559 }
7560 if buf.is_null() && buf_len > 0 {
7562 set_last_error("null buf with non-zero buf_len");
7563 return TgmError::InvalidArg;
7564 }
7565
7566 let options = match parse_validate_options(level, check_canonical) {
7567 Ok(o) => o,
7568 Err((code, msg)) => {
7569 set_last_error(&msg);
7570 return code;
7571 }
7572 };
7573
7574 let data = if buf.is_null() {
7575 &[]
7576 } else {
7577 unsafe { slice::from_raw_parts(buf, buf_len) }
7578 };
7579 let report = validate_message(data, &options);
7580
7581 match serde_json::to_vec(&report) {
7582 Ok(json_bytes) => {
7583 let mut json_bytes = json_bytes.into_boxed_slice().into_vec();
7584 let result = TgmBytes {
7585 data: json_bytes.as_mut_ptr(),
7586 len: json_bytes.len(),
7587 };
7588 std::mem::forget(json_bytes);
7589 unsafe {
7590 *out = result;
7591 }
7592 TgmError::Ok
7593 }
7594 Err(e) => {
7595 set_last_error(&format!("JSON serialization failed: {e}"));
7596 TgmError::Encoding
7597 }
7598 }
7599}
7600
7601#[unsafe(no_mangle)]
7615pub extern "C" fn tgm_validate_file(
7616 path: *const c_char,
7617 level: *const c_char,
7618 check_canonical: i32,
7619 out: *mut TgmBytes,
7620) -> TgmError {
7621 if path.is_null() || out.is_null() {
7622 set_last_error("null argument");
7623 return TgmError::InvalidArg;
7624 }
7625
7626 let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
7627 Ok(s) => s,
7628 Err(e) => {
7629 set_last_error(&format!("invalid UTF-8 in path: {e}"));
7630 return TgmError::InvalidArg;
7631 }
7632 };
7633
7634 let options = match parse_validate_options(level, check_canonical) {
7635 Ok(o) => o,
7636 Err((code, msg)) => {
7637 set_last_error(&msg);
7638 return code;
7639 }
7640 };
7641
7642 let report = match core_validate_file(Path::new(path_str), &options) {
7643 Ok(r) => r,
7644 Err(e) => {
7645 set_last_error(&e.to_string());
7646 return TgmError::Io;
7647 }
7648 };
7649
7650 match serde_json::to_vec(&report) {
7651 Ok(json_bytes) => {
7652 let mut json_bytes = json_bytes.into_boxed_slice().into_vec();
7653 let result = TgmBytes {
7654 data: json_bytes.as_mut_ptr(),
7655 len: json_bytes.len(),
7656 };
7657 std::mem::forget(json_bytes);
7658 unsafe {
7659 *out = result;
7660 }
7661 TgmError::Ok
7662 }
7663 Err(e) => {
7664 set_last_error(&format!("JSON serialization failed: {e}"));
7665 TgmError::Encoding
7666 }
7667 }
7668}