1use core::mem::{align_of, offset_of, size_of};
66use std::time::Duration;
67
68use crate::{FrameMessageKind, FramePriority, PayloadEncoding, UCode, UFrameMetadata, UUri, UUID};
69
70pub use crate::frame::codec::{
73 FIELD_COMM_STATUS, FIELD_MASK_V1, FIELD_PAYLOAD_ENCODING, FIELD_PERMISSION_LEVEL, FIELD_REQID,
74 FIELD_SINK, FIELD_TOKEN, FIELD_TRACEPARENT, FIELD_TTL,
75};
76
77pub const UFRAME_ABI_MAGIC: [u8; 4] = *b"UFA1";
79
80pub const UFRAME_ABI_VERSION: u8 = 1;
82
83pub const UFRAME_ABI_SIZE: usize = 1096;
85
86pub const UFRAME_ABI_ALIGN: usize = 8;
88
89pub const UFRAME_ABI_TYPE_NAME: &str = "uprotocol.v2.UFrameMetadataAbiV1";
92
93pub const UFRAME_ABI_FLAG_LITTLE_ENDIAN: u8 = 0b0000_0001;
96
97pub const UFRAME_ABI_AUTHORITY_CAPACITY: usize = 128;
99pub const UFRAME_ABI_LITERAL_ID_CAPACITY: usize = 64;
101pub const UFRAME_ABI_CONTENT_TYPE_CAPACITY: usize = 100;
103pub const UFRAME_ABI_TRACEPARENT_CAPACITY: usize = 63;
105pub const UFRAME_ABI_TOKEN_CAPACITY: usize = 510;
110
111#[derive(Clone, Debug, Eq, PartialEq)]
113pub enum UFrameAbiError {
114 CapacityExceeded {
116 field: &'static str,
118 },
119 InvalidProfile(String),
121 Metadata(crate::UFrameMetadataError),
123}
124
125impl std::fmt::Display for UFrameAbiError {
126 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
127 match self {
128 Self::CapacityExceeded { field } => {
129 write!(f, "value of `{field}` exceeds the ABI profile capacity")
130 }
131 Self::InvalidProfile(message) => write!(f, "invalid ABI profile value: {message}"),
132 Self::Metadata(error) => write!(f, "invalid frame metadata: {error}"),
133 }
134 }
135}
136
137impl std::error::Error for UFrameAbiError {}
138
139impl From<crate::UFrameMetadataError> for UFrameAbiError {
140 fn from(value: crate::UFrameMetadataError) -> Self {
141 Self::Metadata(value)
142 }
143}
144
145#[repr(C)]
147#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
148pub struct UUuidAbi {
149 pub msb: u64,
151 pub lsb: u64,
153}
154
155#[repr(C)]
157#[derive(Clone, Copy, Debug, Eq, PartialEq)]
158pub struct UUriAbi {
159 pub ue_id: u32,
161 pub resource_id: u16,
163 pub ue_version_major: u8,
165 pub authority_name_len: u8,
167 pub authority_name: [u8; UFRAME_ABI_AUTHORITY_CAPACITY],
169}
170
171impl Default for UUriAbi {
172 fn default() -> Self {
173 Self {
174 ue_id: 0,
175 resource_id: 0,
176 ue_version_major: 0,
177 authority_name_len: 0,
178 authority_name: [0; UFRAME_ABI_AUTHORITY_CAPACITY],
179 }
180 }
181}
182
183#[repr(C)]
189#[derive(Clone, Copy, Debug, Eq, PartialEq)]
190pub struct UPayloadEncodingAbi {
191 pub registry_id: u32,
193 pub literal_id_len: u8,
195 pub content_type_len: u8,
197 pub component_flags: u8,
199 pub _reserved: u8,
201 pub literal_id: [u8; UFRAME_ABI_LITERAL_ID_CAPACITY],
203 pub content_type: [u8; UFRAME_ABI_CONTENT_TYPE_CAPACITY],
205}
206
207pub const UFRAME_ABI_ENCODING_HAS_REGISTRY_ID: u8 = 0b0000_0001;
209
210impl Default for UPayloadEncodingAbi {
211 fn default() -> Self {
212 Self {
213 registry_id: 0,
214 literal_id_len: 0,
215 content_type_len: 0,
216 component_flags: 0,
217 _reserved: 0,
218 literal_id: [0; UFRAME_ABI_LITERAL_ID_CAPACITY],
219 content_type: [0; UFRAME_ABI_CONTENT_TYPE_CAPACITY],
220 }
221 }
222}
223
224#[repr(C)]
226#[derive(Clone, Copy, Debug, Eq, PartialEq)]
227pub struct UTraceparentAbi {
228 pub len: u8,
230 pub bytes: [u8; UFRAME_ABI_TRACEPARENT_CAPACITY],
232}
233
234impl Default for UTraceparentAbi {
235 fn default() -> Self {
236 Self {
237 len: 0,
238 bytes: [0; UFRAME_ABI_TRACEPARENT_CAPACITY],
239 }
240 }
241}
242
243#[repr(C)]
245#[derive(Clone, Copy, Debug, Eq, PartialEq)]
246pub struct UTokenAbi {
247 pub len: u16,
249 pub bytes: [u8; UFRAME_ABI_TOKEN_CAPACITY],
251}
252
253impl Default for UTokenAbi {
254 fn default() -> Self {
255 Self {
256 len: 0,
257 bytes: [0; UFRAME_ABI_TOKEN_CAPACITY],
258 }
259 }
260}
261
262#[repr(C)]
270#[derive(Clone, Copy, Debug, PartialEq)]
271pub struct UFrameMetadataAbiV1 {
272 pub magic: [u8; 4],
275 pub version: u8,
277 pub flags: u8,
279 pub metadata_size: u16,
282
283 pub presence: u32,
287 pub kind: u8,
289 pub priority: u8,
291 pub comm_status: u8,
293 pub _reserved0: u8,
295
296 pub ttl_ns: u64,
299 pub permission_level: u32,
301 pub _reserved1: u32,
303
304 pub id: UUuidAbi,
307 pub reqid: UUuidAbi,
309
310 pub payload_size: u64,
314 pub payload_encoding: UPayloadEncodingAbi,
316
317 pub source: UUriAbi,
320 pub sink: UUriAbi,
322
323 pub traceparent: UTraceparentAbi,
326 pub token: UTokenAbi,
328
329 pub _reserved_tail: [u8; 4],
332}
333
334impl Default for UFrameMetadataAbiV1 {
335 fn default() -> Self {
336 Self {
337 magic: UFRAME_ABI_MAGIC,
338 version: UFRAME_ABI_VERSION,
339 flags: UFRAME_ABI_FLAG_LITTLE_ENDIAN,
340 metadata_size: UFRAME_ABI_SIZE as u16,
341 presence: 0,
342 kind: 0,
343 priority: 0,
344 comm_status: 0,
345 _reserved0: 0,
346 ttl_ns: 0,
347 permission_level: 0,
348 _reserved1: 0,
349 id: UUuidAbi::default(),
350 reqid: UUuidAbi::default(),
351 payload_size: 0,
352 payload_encoding: UPayloadEncodingAbi::default(),
353 source: UUriAbi::default(),
354 sink: UUriAbi::default(),
355 traceparent: UTraceparentAbi::default(),
356 token: UTokenAbi::default(),
357 _reserved_tail: [0; 4],
358 }
359 }
360}
361
362fn fill_str(
363 target: &mut [u8],
364 target_len_max: usize,
365 value: &str,
366 field: &'static str,
367) -> Result<usize, UFrameAbiError> {
368 let bytes = value.as_bytes();
369 if bytes.len() > target_len_max {
370 return Err(UFrameAbiError::CapacityExceeded { field });
371 }
372 let target = target
373 .get_mut(..bytes.len())
374 .ok_or(UFrameAbiError::CapacityExceeded { field })?;
375 target.copy_from_slice(bytes);
376 Ok(bytes.len())
377}
378
379fn read_str<'a>(
380 bytes: &'a [u8],
381 len: usize,
382 capacity: usize,
383 field: &'static str,
384) -> Result<&'a str, UFrameAbiError> {
385 if len > capacity {
386 return Err(UFrameAbiError::InvalidProfile(format!(
387 "`{field}` length {len} exceeds capacity {capacity}"
388 )));
389 }
390 let bytes = bytes.get(..len).ok_or_else(|| {
391 UFrameAbiError::InvalidProfile(format!(
392 "`{field}` length {len} exceeds available bytes {}",
393 bytes.len()
394 ))
395 })?;
396 core::str::from_utf8(bytes)
397 .map_err(|error| UFrameAbiError::InvalidProfile(format!("`{field}` is not UTF-8: {error}")))
398}
399
400fn uuri_to_abi(uri: &UUri, field: &'static str) -> Result<UUriAbi, UFrameAbiError> {
401 let mut abi = UUriAbi {
402 ue_id: (u32::from(uri.uentity_instance_id()) << 16) | u32::from(uri.uentity_type_id()),
403 resource_id: uri.resource_id(),
404 ue_version_major: uri.uentity_major_version(),
405 ..Default::default()
406 };
407 let len = fill_str(
408 &mut abi.authority_name,
409 UFRAME_ABI_AUTHORITY_CAPACITY,
410 uri.authority_name(),
411 field,
412 )?;
413 abi.authority_name_len = len as u8;
414 Ok(abi)
415}
416
417fn uuri_from_abi(abi: &UUriAbi, field: &'static str) -> Result<UUri, UFrameAbiError> {
418 let authority = read_str(
419 &abi.authority_name,
420 usize::from(abi.authority_name_len),
421 UFRAME_ABI_AUTHORITY_CAPACITY,
422 field,
423 )?;
424 UUri::try_from_parts(authority, abi.ue_id, abi.ue_version_major, abi.resource_id)
425 .map_err(|error| UFrameAbiError::InvalidProfile(format!("invalid `{field}`: {error}")))
426}
427
428impl UFrameMetadataAbiV1 {
429 pub fn try_from_metadata(
441 metadata: &UFrameMetadata,
442 payload_size: Option<u64>,
443 ) -> Result<Self, UFrameAbiError> {
444 metadata.validate()?;
445 match (
446 payload_size.is_some(),
447 metadata.payload_encoding().is_some(),
448 ) {
449 (true, true) | (false, false) => {}
450 (true, false) => {
451 return Err(UFrameAbiError::Metadata(
452 crate::UFrameMetadataError::PayloadWithoutEncoding,
453 ))
454 }
455 (false, true) => {
456 return Err(UFrameAbiError::Metadata(
457 crate::UFrameMetadataError::EncodingWithoutPayload,
458 ))
459 }
460 }
461
462 let (msb, lsb) = metadata.id().as_u64_pair();
463 let mut abi = Self {
464 kind: metadata.kind().wire_code(),
465 priority: metadata.priority().map_or(0, FramePriority::wire_code),
466 id: UUuidAbi { msb, lsb },
467 source: uuri_to_abi(metadata.source(), "source.authority_name")?,
468 ..Self::default()
469 };
470
471 if let Some(sink) = metadata.sink() {
472 abi.sink = uuri_to_abi(sink, "sink.authority_name")?;
473 abi.presence |= FIELD_SINK;
474 }
475 if let Some(reqid) = metadata.reqid() {
476 let (msb, lsb) = reqid.as_u64_pair();
477 abi.reqid = UUuidAbi { msb, lsb };
478 abi.presence |= FIELD_REQID;
479 }
480 if let Some(ttl) = metadata.ttl() {
481 abi.ttl_ns = u64::try_from(ttl.as_nanos())
482 .map_err(|_| UFrameAbiError::CapacityExceeded { field: "ttl" })?;
483 abi.presence |= FIELD_TTL;
484 }
485 if let Some(comm_status) = metadata.comm_status() {
486 abi.comm_status = u8::try_from(comm_status.value()).map_err(|_| {
487 UFrameAbiError::CapacityExceeded {
488 field: "comm_status",
489 }
490 })?;
491 abi.presence |= FIELD_COMM_STATUS;
492 }
493 if let Some(permission_level) = metadata.permission_level() {
494 abi.permission_level = permission_level;
495 abi.presence |= FIELD_PERMISSION_LEVEL;
496 }
497 if let Some(token) = metadata.token() {
498 let len = fill_str(
499 &mut abi.token.bytes,
500 UFRAME_ABI_TOKEN_CAPACITY,
501 token,
502 "token",
503 )?;
504 abi.token.len = len as u16;
505 abi.presence |= FIELD_TOKEN;
506 }
507 if let Some(traceparent) = metadata.traceparent() {
508 let len = fill_str(
509 &mut abi.traceparent.bytes,
510 UFRAME_ABI_TRACEPARENT_CAPACITY,
511 traceparent,
512 "traceparent",
513 )?;
514 abi.traceparent.len = len as u8;
515 abi.presence |= FIELD_TRACEPARENT;
516 }
517 if let Some(encoding) = metadata.payload_encoding() {
518 if let Some(registry_id) = encoding.registry_id() {
519 abi.payload_encoding.registry_id = registry_id;
520 abi.payload_encoding.component_flags |= UFRAME_ABI_ENCODING_HAS_REGISTRY_ID;
521 }
522 if let Some(literal_id) = encoding.literal_id() {
523 let len = fill_str(
524 &mut abi.payload_encoding.literal_id,
525 UFRAME_ABI_LITERAL_ID_CAPACITY,
526 literal_id,
527 "payload_encoding.literal_id",
528 )?;
529 abi.payload_encoding.literal_id_len = len as u8;
530 }
531 if let Some(content_type) = encoding.content_type() {
532 let len = fill_str(
533 &mut abi.payload_encoding.content_type,
534 UFRAME_ABI_CONTENT_TYPE_CAPACITY,
535 content_type,
536 "payload_encoding.content_type",
537 )?;
538 abi.payload_encoding.content_type_len = len as u8;
539 }
540 abi.payload_size = payload_size.unwrap_or_default();
541 abi.presence |= FIELD_PAYLOAD_ENCODING;
542 }
543 Ok(abi)
544 }
545
546 pub fn try_to_metadata(&self) -> Result<(UFrameMetadata, Option<u64>), UFrameAbiError> {
554 if self.magic != UFRAME_ABI_MAGIC {
555 return Err(UFrameAbiError::InvalidProfile("wrong magic".to_string()));
556 }
557 if self.version != UFRAME_ABI_VERSION {
558 return Err(UFrameAbiError::InvalidProfile(format!(
559 "unsupported profile version {}",
560 self.version
561 )));
562 }
563 if usize::from(self.metadata_size) < UFRAME_ABI_SIZE {
564 return Err(UFrameAbiError::InvalidProfile(
565 "declared metadata_size is smaller than profile v1".to_string(),
566 ));
567 }
568 if self.presence & !FIELD_MASK_V1 != 0 {
569 return Err(UFrameAbiError::InvalidProfile(
570 "unknown presence bits".to_string(),
571 ));
572 }
573 if self._reserved0 != 0 || self._reserved1 != 0 || self._reserved_tail != [0; 4] {
574 return Err(UFrameAbiError::InvalidProfile(
575 "reserved fields must be zero".to_string(),
576 ));
577 }
578
579 let kind = FrameMessageKind::from_wire_code(self.kind).ok_or_else(|| {
580 UFrameAbiError::InvalidProfile(format!("unknown kind code {}", self.kind))
581 })?;
582 let priority = match self.priority {
583 0 => None,
584 code => Some(FramePriority::from_wire_code(code).ok_or_else(|| {
585 UFrameAbiError::InvalidProfile(format!("unknown priority code {code}"))
586 })?),
587 };
588 let id = UUID::from_u64_pair(self.id.msb, self.id.lsb)
589 .map_err(|error| UFrameAbiError::InvalidProfile(format!("invalid id: {error}")))?;
590 let source = uuri_from_abi(&self.source, "source")?;
591 let sink = if self.presence & FIELD_SINK != 0 {
592 Some(uuri_from_abi(&self.sink, "sink")?)
593 } else {
594 None
595 };
596 let reqid = if self.presence & FIELD_REQID != 0 {
597 Some(
598 UUID::from_u64_pair(self.reqid.msb, self.reqid.lsb).map_err(|error| {
599 UFrameAbiError::InvalidProfile(format!("invalid reqid: {error}"))
600 })?,
601 )
602 } else {
603 None
604 };
605 let ttl = (self.presence & FIELD_TTL != 0).then(|| Duration::from_nanos(self.ttl_ns));
606 let comm_status = if self.presence & FIELD_COMM_STATUS != 0 {
607 Some(
608 UCode::try_from_i32(i32::from(self.comm_status)).map_err(|_| {
609 UFrameAbiError::InvalidProfile(format!(
610 "unknown communication status code {}",
611 self.comm_status
612 ))
613 })?,
614 )
615 } else {
616 None
617 };
618 let permission_level =
619 (self.presence & FIELD_PERMISSION_LEVEL != 0).then_some(self.permission_level);
620 let token = if self.presence & FIELD_TOKEN != 0 {
621 Some(
622 read_str(
623 &self.token.bytes,
624 usize::from(self.token.len),
625 UFRAME_ABI_TOKEN_CAPACITY,
626 "token",
627 )?
628 .to_owned(),
629 )
630 } else {
631 None
632 };
633 let traceparent = if self.presence & FIELD_TRACEPARENT != 0 {
634 Some(
635 read_str(
636 &self.traceparent.bytes,
637 usize::from(self.traceparent.len),
638 UFRAME_ABI_TRACEPARENT_CAPACITY,
639 "traceparent",
640 )?
641 .to_owned(),
642 )
643 } else {
644 None
645 };
646 let (payload_encoding, payload_size) = if self.presence & FIELD_PAYLOAD_ENCODING != 0 {
647 let enc = &self.payload_encoding;
648 if enc.component_flags & !UFRAME_ABI_ENCODING_HAS_REGISTRY_ID != 0 || enc._reserved != 0
649 {
650 return Err(UFrameAbiError::InvalidProfile(
651 "unknown payload encoding component flags".to_string(),
652 ));
653 }
654 let registry_id = (enc.component_flags & UFRAME_ABI_ENCODING_HAS_REGISTRY_ID != 0)
655 .then_some(enc.registry_id);
656 let literal_id = if enc.literal_id_len > 0 {
657 Some(
658 read_str(
659 &enc.literal_id,
660 usize::from(enc.literal_id_len),
661 UFRAME_ABI_LITERAL_ID_CAPACITY,
662 "payload_encoding.literal_id",
663 )?
664 .to_owned(),
665 )
666 } else {
667 None
668 };
669 let content_type = if enc.content_type_len > 0 {
670 Some(
671 read_str(
672 &enc.content_type,
673 usize::from(enc.content_type_len),
674 UFRAME_ABI_CONTENT_TYPE_CAPACITY,
675 "payload_encoding.content_type",
676 )?
677 .to_owned(),
678 )
679 } else {
680 None
681 };
682 (
683 Some(PayloadEncoding::from_parts(
684 registry_id,
685 literal_id,
686 content_type,
687 )?),
688 Some(self.payload_size),
689 )
690 } else {
691 if self.payload_size != 0 {
692 return Err(UFrameAbiError::InvalidProfile(
693 "payload_size must be zero without a payload encoding".to_string(),
694 ));
695 }
696 (None, None)
697 };
698
699 let metadata = UFrameMetadata::from_decoded_parts(
700 kind,
701 id,
702 source,
703 sink,
704 reqid,
705 priority,
706 ttl,
707 comm_status,
708 permission_level,
709 token,
710 traceparent,
711 payload_encoding,
712 );
713 metadata.validate()?;
714 Ok((metadata, payload_size))
715 }
716}
717
718const _: () = {
723 assert!(size_of::<UUuidAbi>() == 16);
724 assert!(align_of::<UUuidAbi>() == 8);
725 assert!(size_of::<UUriAbi>() == 136);
726 assert!(align_of::<UUriAbi>() == 4);
727 assert!(size_of::<UPayloadEncodingAbi>() == 172);
728 assert!(align_of::<UPayloadEncodingAbi>() == 4);
729 assert!(size_of::<UTraceparentAbi>() == 64);
730 assert!(align_of::<UTraceparentAbi>() == 1);
731 assert!(size_of::<UTokenAbi>() == 512);
732 assert!(align_of::<UTokenAbi>() == 2);
733
734 assert!(size_of::<UFrameMetadataAbiV1>() == UFRAME_ABI_SIZE);
735 assert!(align_of::<UFrameMetadataAbiV1>() == UFRAME_ABI_ALIGN);
736
737 assert!(offset_of!(UFrameMetadataAbiV1, magic) == 0);
738 assert!(offset_of!(UFrameMetadataAbiV1, version) == 4);
739 assert!(offset_of!(UFrameMetadataAbiV1, flags) == 5);
740 assert!(offset_of!(UFrameMetadataAbiV1, metadata_size) == 6);
741 assert!(offset_of!(UFrameMetadataAbiV1, presence) == 8);
742 assert!(offset_of!(UFrameMetadataAbiV1, kind) == 12);
743 assert!(offset_of!(UFrameMetadataAbiV1, priority) == 13);
744 assert!(offset_of!(UFrameMetadataAbiV1, comm_status) == 14);
745 assert!(offset_of!(UFrameMetadataAbiV1, _reserved0) == 15);
746 assert!(offset_of!(UFrameMetadataAbiV1, ttl_ns) == 16);
747 assert!(offset_of!(UFrameMetadataAbiV1, permission_level) == 24);
748 assert!(offset_of!(UFrameMetadataAbiV1, _reserved1) == 28);
749 assert!(offset_of!(UFrameMetadataAbiV1, id) == 32);
750 assert!(offset_of!(UFrameMetadataAbiV1, reqid) == 48);
751 assert!(offset_of!(UFrameMetadataAbiV1, payload_size) == 64);
752 assert!(offset_of!(UFrameMetadataAbiV1, payload_encoding) == 72);
753 assert!(offset_of!(UFrameMetadataAbiV1, source) == 244);
754 assert!(offset_of!(UFrameMetadataAbiV1, sink) == 380);
755 assert!(offset_of!(UFrameMetadataAbiV1, traceparent) == 516);
756 assert!(offset_of!(UFrameMetadataAbiV1, token) == 580);
757 assert!(offset_of!(UFrameMetadataAbiV1, _reserved_tail) == 1092);
758
759 assert!(offset_of!(UPayloadEncodingAbi, registry_id) == 0);
760 assert!(offset_of!(UPayloadEncodingAbi, literal_id_len) == 4);
761 assert!(offset_of!(UPayloadEncodingAbi, content_type_len) == 5);
762 assert!(offset_of!(UPayloadEncodingAbi, component_flags) == 6);
763 assert!(offset_of!(UPayloadEncodingAbi, _reserved) == 7);
764 assert!(offset_of!(UPayloadEncodingAbi, literal_id) == 8);
765 assert!(offset_of!(UPayloadEncodingAbi, content_type) == 72);
766};
767
768#[cfg(test)]
769mod tests {
770 use pretty_assertions::assert_eq;
771
772 use super::*;
773
774 fn topic() -> UUri {
775 UUri::try_from_parts("vehicle", 0x4210, 0x01, 0x9000).expect("topic")
776 }
777
778 fn method() -> UUri {
779 UUri::try_from_parts("vehicle", 0x4210, 0x01, 0x00b1).expect("method")
780 }
781
782 fn reply_to() -> UUri {
783 UUri::try_from_parts("cloud", 0x10ab, 0x02, 0x0000).expect("reply-to")
784 }
785
786 #[test]
787 fn minimal_publish_metadata_round_trips_through_profile() {
788 let metadata = UFrameMetadata::publish(topic()).build().expect("metadata");
789 let abi = UFrameMetadataAbiV1::try_from_metadata(&metadata, None).expect("profile");
790 let (decoded, payload_size) = abi.try_to_metadata().expect("metadata");
791 assert_eq!(decoded, metadata);
792 assert_eq!(payload_size, None);
793 }
794
795 #[test]
796 fn fully_populated_request_round_trips_through_profile() {
797 let metadata = UFrameMetadata::request(method(), reply_to(), Duration::from_millis(250))
798 .with_priority(FramePriority::CS6)
799 .with_comm_status(UCode::Ok)
800 .with_permission_level(3)
801 .with_token("token-value")
802 .with_traceparent("00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01")
803 .with_payload_encoding(
804 PayloadEncoding::custom("up.xcdr-v2", "application/vnd.uprotocol.xcdr-v2")
805 .expect("encoding"),
806 )
807 .build()
808 .expect("metadata");
809
810 let abi = UFrameMetadataAbiV1::try_from_metadata(&metadata, Some(64)).expect("profile");
811 let (decoded, payload_size) = abi.try_to_metadata().expect("metadata");
812 assert_eq!(decoded, metadata);
813 assert_eq!(payload_size, Some(64));
814 }
815
816 #[test]
817 fn oversized_token_fails_instead_of_truncating() {
818 let metadata = UFrameMetadata::publish(topic())
819 .with_token("t".repeat(UFRAME_ABI_TOKEN_CAPACITY + 1))
820 .build()
821 .expect("metadata");
822 assert_eq!(
823 UFrameMetadataAbiV1::try_from_metadata(&metadata, None).unwrap_err(),
824 UFrameAbiError::CapacityExceeded { field: "token" }
825 );
826 }
827
828 #[test]
829 fn payload_size_and_encoding_presence_must_agree() {
830 let metadata = UFrameMetadata::publish(topic()).build().expect("metadata");
831 assert!(matches!(
832 UFrameMetadataAbiV1::try_from_metadata(&metadata, Some(8)).unwrap_err(),
833 UFrameAbiError::Metadata(crate::UFrameMetadataError::PayloadWithoutEncoding)
834 ));
835
836 let metadata = UFrameMetadata::publish(topic())
837 .with_payload_encoding(PayloadEncoding::RAW)
838 .build()
839 .expect("metadata");
840 assert!(matches!(
841 UFrameMetadataAbiV1::try_from_metadata(&metadata, None).unwrap_err(),
842 UFrameAbiError::Metadata(crate::UFrameMetadataError::EncodingWithoutPayload)
843 ));
844 }
845
846 #[test]
847 fn structural_validation_rejects_corrupted_profiles() {
848 let metadata = UFrameMetadata::publish(topic()).build().expect("metadata");
849 let abi = UFrameMetadataAbiV1::try_from_metadata(&metadata, None).expect("profile");
850
851 let mut bad = abi;
852 bad.magic = *b"XXXX";
853 assert!(bad.try_to_metadata().is_err());
854
855 let mut bad = abi;
856 bad.presence |= 1 << 30;
857 assert!(bad.try_to_metadata().is_err());
858
859 let mut bad = abi;
860 bad._reserved_tail = [1; 4];
861 assert!(bad.try_to_metadata().is_err());
862 }
863}