1use crate::format::bytes::read_le_uint as read_uint;
25use crate::format::{FormatContext, FormatError, FormatResult};
26
27const VERSION: u8 = 1;
28
29const FLAG_NAME_LEN_MASK: u8 = 0x03;
30const FLAG_CREATION_ORDER: u8 = 0x04;
31const FLAG_LINK_TYPE: u8 = 0x08;
32const FLAG_CHARSET: u8 = 0x10;
33
34const LINK_TYPE_HARD: u8 = 0;
35const LINK_TYPE_SOFT: u8 = 1;
36const LINK_TYPE_EXTERNAL: u8 = 64;
39
40const EXT_VERSION: u8 = 0;
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
54pub enum CharacterSet {
55 #[default]
58 Ascii,
59 Utf8,
61}
62
63impl CharacterSet {
64 pub fn for_name(name: &str) -> Self {
69 if name.is_ascii() {
70 Self::Ascii
71 } else {
72 Self::Utf8
73 }
74 }
75
76 fn code(self) -> u8 {
77 match self {
78 Self::Ascii => 0,
79 Self::Utf8 => 1,
80 }
81 }
82
83 fn from_code(code: u8) -> FormatResult<Self> {
84 match code {
85 0 => Ok(Self::Ascii),
86 1 => Ok(Self::Utf8),
87 other => Err(FormatError::InvalidData(format!(
88 "link name character set {other} is neither ASCII nor UTF-8"
89 ))),
90 }
91 }
92}
93
94#[derive(Debug, Clone, PartialEq)]
96pub enum LinkTarget {
97 Hard { address: u64 },
99 Soft { target: String },
101 External { file: String, path: String },
103 UserDefined { link_type: u8, udata: Vec<u8> },
107}
108
109#[derive(Debug, Clone, PartialEq)]
111pub struct LinkMessage {
112 pub name: String,
113 pub target: LinkTarget,
114 pub creation_order: Option<i64>,
118 pub cset: CharacterSet,
120}
121
122impl LinkMessage {
123 pub fn hard(name: &str, address: u64) -> Self {
125 Self {
126 name: name.to_string(),
127 target: LinkTarget::Hard { address },
128 creation_order: None,
129 cset: CharacterSet::for_name(name),
130 }
131 }
132
133 pub fn soft(name: &str, target: &str) -> Self {
135 Self {
136 name: name.to_string(),
137 target: LinkTarget::Soft {
138 target: target.to_string(),
139 },
140 creation_order: None,
141 cset: CharacterSet::for_name(name),
142 }
143 }
144
145 pub fn external(name: &str, file: &str, path: &str) -> Self {
147 Self {
148 name: name.to_string(),
149 target: LinkTarget::External {
150 file: file.to_string(),
151 path: path.to_string(),
152 },
153 creation_order: None,
154 cset: CharacterSet::for_name(name),
155 }
156 }
157
158 pub fn with_creation_order(mut self, corder: i64) -> Self {
160 self.creation_order = Some(corder);
161 self
162 }
163
164 pub fn restamp_encoded_creation_order(encoded: &mut [u8], corder: i64) -> bool {
168 if encoded.len() < 2 || encoded[0] != VERSION || encoded[1] & FLAG_CREATION_ORDER == 0 {
169 return false;
170 }
171 let at = 2 + usize::from(encoded[1] & FLAG_LINK_TYPE != 0);
172 let Some(field) = encoded.get_mut(at..at + 8) else {
173 return false;
174 };
175 field.copy_from_slice(&corder.to_le_bytes());
176 true
177 }
178
179 pub fn with_cset(mut self, cset: CharacterSet) -> Self {
188 self.cset = cset;
189 self
190 }
191
192 pub fn fits_symbol_table(&self) -> bool {
206 self.cset == CharacterSet::Ascii
207 && matches!(
208 self.target,
209 LinkTarget::Hard { .. } | LinkTarget::Soft { .. }
210 )
211 }
212
213 pub fn encode(&self, ctx: &FormatContext) -> Vec<u8> {
216 let name_bytes = self.name.as_bytes();
217 let name_len = name_bytes.len();
218 let name_len_size = min_bytes_for_value(name_len as u64);
219 let name_len_code = match name_len_size {
220 1 => 0u8,
221 2 => 1,
222 4 => 2,
223 _ => 3, };
225
226 let link_type = match &self.target {
227 LinkTarget::Hard { .. } => LINK_TYPE_HARD,
228 LinkTarget::Soft { .. } => LINK_TYPE_SOFT,
229 LinkTarget::External { .. } => LINK_TYPE_EXTERNAL,
230 LinkTarget::UserDefined { link_type, .. } => *link_type,
231 };
232
233 let mut flags: u8 = name_len_code & FLAG_NAME_LEN_MASK;
238 if link_type != LINK_TYPE_HARD {
239 flags |= FLAG_LINK_TYPE;
240 }
241 if self.cset != CharacterSet::Ascii {
242 flags |= FLAG_CHARSET;
243 }
244 if self.creation_order.is_some() {
245 flags |= FLAG_CREATION_ORDER;
246 }
247
248 let mut buf = Vec::with_capacity(32);
249 buf.push(VERSION);
250 buf.push(flags);
251
252 if flags & FLAG_LINK_TYPE != 0 {
254 buf.push(link_type);
255 }
256
257 if let Some(corder) = self.creation_order {
259 buf.extend_from_slice(&corder.to_le_bytes());
260 }
261
262 if flags & FLAG_CHARSET != 0 {
263 buf.push(self.cset.code());
264 }
265
266 match name_len_size {
268 1 => buf.push(name_len as u8),
269 2 => buf.extend_from_slice(&(name_len as u16).to_le_bytes()),
270 4 => buf.extend_from_slice(&(name_len as u32).to_le_bytes()),
271 _ => buf.extend_from_slice(&(name_len as u64).to_le_bytes()),
272 }
273
274 buf.extend_from_slice(name_bytes);
276
277 match &self.target {
279 LinkTarget::Hard { address } => {
280 let sa = ctx.sizeof_addr as usize;
281 buf.extend_from_slice(&address.to_le_bytes()[..sa]);
282 }
283 LinkTarget::Soft { target } => {
284 let tbytes = target.as_bytes();
285 buf.extend_from_slice(&(tbytes.len() as u16).to_le_bytes());
286 buf.extend_from_slice(tbytes);
287 }
288 LinkTarget::External { file, path } => {
289 let udata = encode_external_udata(file, path);
290 buf.extend_from_slice(&(udata.len() as u16).to_le_bytes());
291 buf.extend_from_slice(&udata);
292 }
293 LinkTarget::UserDefined { udata, .. } => {
294 buf.extend_from_slice(&(udata.len() as u16).to_le_bytes());
295 buf.extend_from_slice(udata);
296 }
297 }
298
299 buf
300 }
301
302 pub fn decode(buf: &[u8], ctx: &FormatContext) -> FormatResult<(Self, usize)> {
305 if buf.len() < 2 {
306 return Err(FormatError::BufferTooShort {
307 needed: 2,
308 available: buf.len(),
309 });
310 }
311
312 let version = buf[0];
313 if version != VERSION {
314 return Err(FormatError::InvalidVersion(version));
315 }
316
317 let flags = buf[1];
318 let name_len_code = flags & FLAG_NAME_LEN_MASK;
319 let has_creation_order = (flags & FLAG_CREATION_ORDER) != 0;
320 let has_link_type = (flags & FLAG_LINK_TYPE) != 0;
321 let has_charset = (flags & FLAG_CHARSET) != 0;
322
323 let mut pos = 2;
324
325 let link_type = if has_link_type {
327 check_len(buf, pos, 1)?;
328 let lt = buf[pos];
329 pos += 1;
330 lt
331 } else {
332 LINK_TYPE_HARD };
334
335 let creation_order = if has_creation_order {
338 check_len(buf, pos, 8)?;
339 let v = i64::from_le_bytes(buf[pos..pos + 8].try_into().unwrap());
340 pos += 8;
341 Some(v)
342 } else {
343 None
344 };
345
346 let cset = if has_charset {
348 check_len(buf, pos, 1)?;
349 let c = CharacterSet::from_code(buf[pos])?;
350 pos += 1;
351 c
352 } else {
353 CharacterSet::Ascii
354 };
355
356 let name_len_size: usize = match name_len_code {
358 0 => 1,
359 1 => 2,
360 2 => 4,
361 _ => 8,
362 };
363 check_len(buf, pos, name_len_size)?;
364 let name_len = read_uint(&buf[pos..], name_len_size) as usize;
365 pos += name_len_size;
366
367 check_len(buf, pos, name_len)?;
369 let name = std::str::from_utf8(&buf[pos..pos + name_len])
370 .map_err(|e| FormatError::InvalidData(format!("invalid UTF-8 link name: {}", e)))?
371 .to_string();
372 pos += name_len;
373
374 let target = match link_type {
376 LINK_TYPE_HARD => {
377 let sa = ctx.sizeof_addr as usize;
378 check_len(buf, pos, sa)?;
379 let address = read_uint(&buf[pos..], sa);
380 pos += sa;
381 LinkTarget::Hard { address }
382 }
383 LINK_TYPE_SOFT => {
384 check_len(buf, pos, 2)?;
385 let tlen = u16::from_le_bytes([buf[pos], buf[pos + 1]]) as usize;
386 pos += 2;
387 check_len(buf, pos, tlen)?;
388 let target = std::str::from_utf8(&buf[pos..pos + tlen])
389 .map_err(|e| {
390 FormatError::InvalidData(format!("invalid UTF-8 soft link target: {}", e))
391 })?
392 .to_string();
393 pos += tlen;
394 LinkTarget::Soft { target }
395 }
396 ud if ud >= LINK_TYPE_EXTERNAL => {
401 check_len(buf, pos, 2)?;
402 let ulen = u16::from_le_bytes([buf[pos], buf[pos + 1]]) as usize;
403 pos += 2;
404 check_len(buf, pos, ulen)?;
405 let udata = &buf[pos..pos + ulen];
406 pos += ulen;
407 if ud == LINK_TYPE_EXTERNAL {
408 let (file, path) = decode_external_udata(udata)?;
409 LinkTarget::External { file, path }
410 } else {
411 LinkTarget::UserDefined {
412 link_type: ud,
413 udata: udata.to_vec(),
414 }
415 }
416 }
417 other => {
418 return Err(FormatError::InvalidData(format!(
419 "unknown link type {}",
420 other
421 )));
422 }
423 };
424
425 Ok((
426 Self {
427 name,
428 target,
429 creation_order,
430 cset,
431 },
432 pos,
433 ))
434 }
435}
436
437pub(crate) fn normalize_object_path(path: &str) -> String {
446 let mut out = String::with_capacity(path.len());
447 let mut last_slash = false;
448 for c in path.chars() {
449 if c == '/' && last_slash {
450 continue;
451 }
452 last_slash = c == '/';
453 out.push(c);
454 }
455 if out.len() > 1 && out.ends_with('/') {
457 out.pop();
458 }
459 out
460}
461
462fn encode_external_udata(file: &str, path: &str) -> Vec<u8> {
466 let mut udata = Vec::with_capacity(1 + file.len() + path.len() + 2);
467 udata.push(EXT_VERSION << 4);
468 udata.extend_from_slice(file.as_bytes());
469 udata.push(0);
470 udata.extend_from_slice(path.as_bytes());
471 udata.push(0);
472 udata
473}
474
475fn decode_external_udata(udata: &[u8]) -> FormatResult<(String, String)> {
479 if udata.len() < 3 {
480 return Err(FormatError::InvalidData(format!(
481 "external link value is {} bytes, below the 3-byte minimum",
482 udata.len()
483 )));
484 }
485 let version = udata[0] >> 4;
486 let flags = udata[0] & 0x0f;
487 if version != EXT_VERSION {
488 return Err(FormatError::InvalidVersion(version));
489 }
490 if flags != 0 {
491 return Err(FormatError::InvalidData(format!(
492 "external link flags {flags:#x} are not recognized"
493 )));
494 }
495 let body = &udata[1..];
496 let split = body.iter().position(|&b| b == 0).ok_or_else(|| {
497 FormatError::InvalidData("external link file name is not NUL-terminated".into())
498 })?;
499 let file = str_from_utf8(&body[..split], "external link file name")?;
500 let rest = &body[split + 1..];
501 let end = rest.iter().position(|&b| b == 0).unwrap_or(rest.len());
505 let path = str_from_utf8(&rest[..end], "external link object path")?;
506 Ok((file, path))
507}
508
509fn str_from_utf8(bytes: &[u8], what: &str) -> FormatResult<String> {
510 std::str::from_utf8(bytes)
511 .map(|s| s.to_string())
512 .map_err(|e| FormatError::InvalidData(format!("invalid UTF-8 {what}: {e}")))
513}
514
515fn check_len(buf: &[u8], pos: usize, need: usize) -> FormatResult<()> {
516 match pos.checked_add(need) {
520 Some(end) if end <= buf.len() => Ok(()),
521 _ => Err(FormatError::BufferTooShort {
522 needed: pos.saturating_add(need),
523 available: buf.len(),
524 }),
525 }
526}
527
528fn min_bytes_for_value(v: u64) -> usize {
530 if v <= u8::MAX as u64 {
531 1
532 } else if v <= u16::MAX as u64 {
533 2
534 } else if v <= u32::MAX as u64 {
535 4
536 } else {
537 8
538 }
539}
540
541#[cfg(test)]
544mod tests {
545 use super::*;
546
547 fn ctx8() -> FormatContext {
548 FormatContext {
549 sizeof_addr: 8,
550 sizeof_size: 8,
551 }
552 }
553
554 #[test]
555 fn restamp_rewrites_only_the_creation_order_field() {
556 let soft = LinkMessage::soft("s", "/a").with_creation_order(7);
557 let mut encoded = soft.encode(&ctx8());
558 assert!(LinkMessage::restamp_encoded_creation_order(&mut encoded, 2));
559 assert_eq!(encoded, soft.with_creation_order(2).encode(&ctx8()));
560
561 let hard = LinkMessage::hard("h", 0x1000);
562 let mut encoded = hard.encode(&ctx8());
563 let before = encoded.clone();
564 assert!(!LinkMessage::restamp_encoded_creation_order(
565 &mut encoded,
566 2
567 ));
568 assert_eq!(encoded, before);
569 }
570
571 fn ctx4() -> FormatContext {
572 FormatContext {
573 sizeof_addr: 4,
574 sizeof_size: 4,
575 }
576 }
577
578 #[test]
579 fn roundtrip_hard_link() {
580 let msg = LinkMessage::hard("dataset1", 0x1000);
581 let encoded = msg.encode(&ctx8());
582 let (decoded, consumed) = LinkMessage::decode(&encoded, &ctx8()).unwrap();
583 assert_eq!(consumed, encoded.len());
584 assert_eq!(decoded, msg);
585 }
586
587 #[test]
588 fn roundtrip_hard_link_ctx4() {
589 let msg = LinkMessage::hard("grp", 0x2000);
590 let encoded = msg.encode(&ctx4());
591 let (decoded, consumed) = LinkMessage::decode(&encoded, &ctx4()).unwrap();
592 assert_eq!(consumed, encoded.len());
593 assert_eq!(decoded, msg);
594 }
595
596 #[test]
597 fn roundtrip_soft_link() {
598 let msg = LinkMessage::soft("alias", "/group/dataset");
599 let encoded = msg.encode(&ctx8());
600 let (decoded, consumed) = LinkMessage::decode(&encoded, &ctx8()).unwrap();
601 assert_eq!(consumed, encoded.len());
602 assert_eq!(decoded, msg);
603 }
604
605 #[test]
606 fn roundtrip_empty_name() {
607 let msg = LinkMessage::hard("", 0x100);
609 let encoded = msg.encode(&ctx8());
610 let (decoded, _) = LinkMessage::decode(&encoded, &ctx8()).unwrap();
611 assert_eq!(decoded, msg);
612 }
613
614 #[test]
615 fn roundtrip_long_name() {
616 let long_name: String = "a".repeat(300);
618 let msg = LinkMessage::hard(&long_name, 0xABCD);
619 let encoded = msg.encode(&ctx8());
620 let (decoded, consumed) = LinkMessage::decode(&encoded, &ctx8()).unwrap();
621 assert_eq!(consumed, encoded.len());
622 assert_eq!(decoded, msg);
623 }
624
625 #[test]
626 fn roundtrip_unicode_name() {
627 let msg = LinkMessage::hard("日本語データ", 0x4000);
628 let encoded = msg.encode(&ctx8());
629 let (decoded, _) = LinkMessage::decode(&encoded, &ctx8()).unwrap();
630 assert_eq!(decoded, msg);
631 }
632
633 #[test]
634 fn roundtrip_external_link() {
635 let msg = LinkMessage::external("ext", "sibling.h5", "/payload");
636 let encoded = msg.encode(&ctx8());
637 let (decoded, consumed) = LinkMessage::decode(&encoded, &ctx8()).unwrap();
638 assert_eq!(consumed, encoded.len());
639 assert_eq!(decoded, msg);
640 }
641
642 #[test]
647 fn decode_h5py_external_link_bytes() {
648 let mut buf = vec![1u8, 0x08, 64, 3];
649 buf.extend_from_slice(b"ext");
650 let udata = {
651 let mut u = vec![0u8];
652 u.extend_from_slice(b"link_external_ext.h5\0");
653 u.extend_from_slice(b"/payload\0");
654 u
655 };
656 assert_eq!(udata.len(), 31);
657 buf.extend_from_slice(&(udata.len() as u16).to_le_bytes());
658 buf.extend_from_slice(&udata);
659
660 let (decoded, consumed) = LinkMessage::decode(&buf, &ctx8()).unwrap();
661 assert_eq!(consumed, buf.len());
662 assert_eq!(decoded.name, "ext");
663 assert_eq!(
664 decoded.target,
665 LinkTarget::External {
666 file: "link_external_ext.h5".into(),
667 path: "/payload".into(),
668 }
669 );
670 }
671
672 #[test]
678 fn encoded_external_link_is_the_message_h5lcreate_external_builds() {
679 let encoded =
680 LinkMessage::external("ext", "link_external_ext.h5", "/payload").encode(&ctx8());
681 let mut want = vec![1u8, 0x08, 64, 3];
682 want.extend_from_slice(b"ext");
683 let udata = {
684 let mut u = vec![0u8];
685 u.extend_from_slice(b"link_external_ext.h5\0");
686 u.extend_from_slice(b"/payload\0");
687 u
688 };
689 want.extend_from_slice(&(udata.len() as u16).to_le_bytes());
690 want.extend_from_slice(&udata);
691 assert_eq!(encoded, want);
692 }
693
694 #[test]
697 fn roundtrip_unregistered_user_defined_link() {
698 let msg = LinkMessage {
699 name: "ud".into(),
700 target: LinkTarget::UserDefined {
701 link_type: 200,
702 udata: vec![9, 8, 7],
703 },
704 creation_order: None,
705 cset: CharacterSet::Ascii,
706 };
707 let encoded = msg.encode(&ctx8());
708 let (decoded, consumed) = LinkMessage::decode(&encoded, &ctx8()).unwrap();
709 assert_eq!(consumed, encoded.len());
710 assert_eq!(decoded, msg);
711 }
712
713 #[test]
714 fn decode_unknown_link_type_below_user_defined_range() {
715 let buf = [1u8, 0x08, 7, 1, b'x'];
716 match LinkMessage::decode(&buf, &ctx8()).unwrap_err() {
717 FormatError::InvalidData(ref s) => assert!(s.contains("link type 7"), "{s}"),
718 other => panic!("unexpected error: {other:?}"),
719 }
720 }
721
722 #[test]
723 fn decode_external_value_too_short() {
724 let mut buf = vec![1u8, 0x08, 64, 1, b'e'];
725 buf.extend_from_slice(&2u16.to_le_bytes());
726 buf.extend_from_slice(&[0u8, 0]);
727 match LinkMessage::decode(&buf, &ctx8()).unwrap_err() {
728 FormatError::InvalidData(ref s) => assert!(s.contains("3-byte minimum"), "{s}"),
729 other => panic!("unexpected error: {other:?}"),
730 }
731 }
732
733 #[test]
734 fn decode_external_value_bad_version() {
735 let mut buf = vec![1u8, 0x08, 64, 1, b'e'];
736 let udata = [0x10u8, b'f', 0, b'/', 0];
737 buf.extend_from_slice(&(udata.len() as u16).to_le_bytes());
738 buf.extend_from_slice(&udata);
739 match LinkMessage::decode(&buf, &ctx8()).unwrap_err() {
740 FormatError::InvalidVersion(1) => {}
741 other => panic!("unexpected error: {other:?}"),
742 }
743 }
744
745 #[test]
746 fn decode_bad_version() {
747 let buf = [2u8, 0]; let err = LinkMessage::decode(&buf, &ctx8()).unwrap_err();
749 match err {
750 FormatError::InvalidVersion(2) => {}
751 other => panic!("unexpected error: {:?}", other),
752 }
753 }
754
755 #[test]
756 fn decode_buffer_too_short() {
757 let buf = [1u8];
758 let err = LinkMessage::decode(&buf, &ctx8()).unwrap_err();
759 match err {
760 FormatError::BufferTooShort { .. } => {}
761 other => panic!("unexpected error: {:?}", other),
762 }
763 }
764
765 #[test]
766 fn version_byte() {
767 let encoded = LinkMessage::hard("x", 0).encode(&ctx8());
768 assert_eq!(encoded[0], 1);
769 }
770
771 #[test]
772 fn roundtrip_with_creation_order() {
773 let msg = LinkMessage::hard("d00", 0x1000).with_creation_order(7);
774 let encoded = msg.encode(&ctx8());
775 assert_eq!(encoded[1] & FLAG_CREATION_ORDER, FLAG_CREATION_ORDER);
777 assert_eq!(
778 encoded.len(),
779 LinkMessage::hard("d00", 0x1000).encode(&ctx8()).len() + 8
780 );
781 let (decoded, consumed) = LinkMessage::decode(&encoded, &ctx8()).unwrap();
782 assert_eq!(consumed, encoded.len());
783 assert_eq!(decoded, msg);
784 assert_eq!(decoded.creation_order, Some(7));
785 }
786
787 #[test]
791 fn creation_order_precedes_the_charset_byte() {
792 let msg = LinkMessage::soft("별칭", "/orig").with_creation_order(-3);
793 let encoded = msg.encode(&ctx8());
794 assert_eq!(
795 encoded[1],
796 FLAG_CREATION_ORDER | FLAG_LINK_TYPE | FLAG_CHARSET
797 );
798 assert_eq!(&encoded[3..11], &(-3i64).to_le_bytes());
799 assert_eq!(encoded[11], 1, "charset follows the creation order");
800 assert_eq!(LinkMessage::decode(&encoded, &ctx8()).unwrap().0, msg);
801 }
802
803 #[test]
809 fn ascii_hard_link_is_encoded_the_way_libhdf5_wrote_it() {
810 let encoded = LinkMessage::hard("plain", 800).encode(&ctx8());
811 let mut want = vec![1u8, 0x00, 5];
812 want.extend_from_slice(b"plain");
813 want.extend_from_slice(&800u64.to_le_bytes());
814 assert_eq!(encoded, want);
815 }
816
817 #[test]
820 fn non_ascii_hard_link_carries_the_utf8_charset_byte() {
821 let encoded = LinkMessage::hard("비ascii", 1832).encode(&ctx8());
822 let name = "비ascii".as_bytes();
823 assert_eq!(name.len(), 8);
824 let mut want = vec![1u8, 0x10, 1, 8];
825 want.extend_from_slice(name);
826 want.extend_from_slice(&1832u64.to_le_bytes());
827 assert_eq!(encoded, want);
828 }
829
830 #[test]
833 fn a_utf8_name_does_not_fit_a_symbol_table() {
834 assert!(LinkMessage::hard("plain", 1).fits_symbol_table());
835 assert!(LinkMessage::soft("plain", "/x").fits_symbol_table());
836 assert!(!LinkMessage::hard("비ascii", 1).fits_symbol_table());
837 assert!(!LinkMessage::soft("비ascii", "/x").fits_symbol_table());
838 assert!(!LinkMessage::external("ext", "f.h5", "/p").fits_symbol_table());
839 }
840
841 #[test]
845 fn a_symbol_table_name_keeps_the_default_charset() {
846 let msg = LinkMessage::hard("한글", 0x40).with_cset(CharacterSet::Ascii);
847 assert!(msg.fits_symbol_table());
848 let encoded = msg.encode(&ctx8());
849 assert_eq!(encoded[1] & FLAG_CHARSET, 0);
850 assert_eq!(LinkMessage::decode(&encoded, &ctx8()).unwrap().0, msg);
851 }
852
853 #[test]
856 fn decode_rejects_an_unknown_charset() {
857 let buf = [1u8, 0x10, 7, 1, b'x', 0, 0, 0, 0, 0, 0, 0, 0];
858 match LinkMessage::decode(&buf, &ctx8()).unwrap_err() {
859 FormatError::InvalidData(ref s) => assert!(s.contains("character set 7"), "{s}"),
860 other => panic!("unexpected error: {other:?}"),
861 }
862 }
863
864 #[test]
867 fn object_paths_normalize_like_h5g_normalize() {
868 assert_eq!(normalize_object_path("/payload"), "/payload");
869 assert_eq!(normalize_object_path("//a///b"), "/a/b");
870 assert_eq!(normalize_object_path("/a/b/"), "/a/b");
871 assert_eq!(normalize_object_path("/a/b//"), "/a/b");
872 assert_eq!(normalize_object_path("/"), "/");
873 assert_eq!(normalize_object_path("//"), "/");
874 assert_eq!(normalize_object_path("a/b"), "a/b");
875 assert_eq!(normalize_object_path(""), "");
876 }
877
878 #[test]
879 fn min_bytes_for_value_checks() {
880 assert_eq!(min_bytes_for_value(0), 1);
881 assert_eq!(min_bytes_for_value(255), 1);
882 assert_eq!(min_bytes_for_value(256), 2);
883 assert_eq!(min_bytes_for_value(65535), 2);
884 assert_eq!(min_bytes_for_value(65536), 4);
885 assert_eq!(min_bytes_for_value(u32::MAX as u64), 4);
886 assert_eq!(min_bytes_for_value(u32::MAX as u64 + 1), 8);
887 }
888}