1use crate::format::checksum::checksum_metadata;
45use crate::format::selection::Selection;
46use crate::format::{FormatContext, FormatError, FormatResult};
47
48const ENC_VERS_0: u8 = 0;
49const ENC_VERS_1: u8 = 1;
50
51const SOURCE_FILE_SHARED: u8 = 0x01;
52const SOURCE_DSET_SHARED: u8 = 0x02;
53const SOURCE_SAME_FILE: u8 = 0x04;
54const ALL_FLAGS: u8 = SOURCE_FILE_SHARED | SOURCE_DSET_SHARED | SOURCE_SAME_FILE;
55
56#[derive(Debug, Clone, PartialEq, Eq)]
58pub struct VirtualMapping {
59 pub source_file_name: String,
63 pub source_dset_name: String,
65 pub source_selection: Selection,
67 pub virtual_selection: Selection,
69}
70
71#[derive(Debug, Clone, PartialEq, Eq)]
83pub struct ParsedSourceName {
84 segments: Vec<String>,
90}
91
92impl ParsedSourceName {
93 pub fn nsubs(&self) -> usize {
97 self.segments.len() - 1
98 }
99
100 pub fn build(&self, blockno: u64) -> String {
106 self.segments.join(&blockno.to_string())
107 }
108}
109
110pub fn parse_source_name(name: &str) -> FormatResult<ParsedSourceName> {
113 let mut segments = vec![String::new()];
114 let mut rest = name;
115 while let Some(pct) = rest.find('%') {
116 let (literal, tail) = rest.split_at(pct);
117 segments.last_mut().expect("never empty").push_str(literal);
118 match tail.as_bytes().get(1) {
119 Some(b'b') => segments.push(String::new()),
120 Some(b'%') => segments.last_mut().expect("never empty").push('%'),
121 _ => {
122 return Err(FormatError::InvalidData(format!(
123 "invalid format specifier in virtual dataset source name {name:?}: only \
124 %b (block index) and %% (escaped percent) are legal"
125 )))
126 }
127 }
128 rest = &tail[2.min(tail.len())..];
129 }
130 segments.last_mut().expect("never empty").push_str(rest);
131 Ok(ParsedSourceName { segments })
132}
133
134#[derive(Debug, Clone, PartialEq, Eq)]
136pub struct VirtualMappingList {
137 pub mappings: Vec<VirtualMapping>,
138}
139
140impl VirtualMappingList {
141 pub fn encode(&self, ctx: &FormatContext) -> FormatResult<Vec<u8>> {
157 let ss = ctx.sizeof_size as usize;
158 let mut buf = Vec::new();
159 buf.push(ENC_VERS_0);
160 buf.extend_from_slice(&(self.mappings.len() as u64).to_le_bytes()[..ss]);
161 for m in &self.mappings {
162 push_cstr(&mut buf, &m.source_file_name, "source file")?;
163 push_cstr(&mut buf, &m.source_dset_name, "source dataset")?;
164 buf.extend_from_slice(&m.source_selection.encode()?);
165 buf.extend_from_slice(&m.virtual_selection.encode()?);
166 }
167 let cksum = checksum_metadata(&buf);
168 buf.extend_from_slice(&cksum.to_le_bytes());
169 Ok(buf)
170 }
171
172 pub fn decode(buf: &[u8], ctx: &FormatContext) -> FormatResult<Self> {
181 let ss = ctx.sizeof_size as usize;
182 if buf.is_empty() {
183 return Err(FormatError::BufferTooShort {
184 needed: 1,
185 available: 0,
186 });
187 }
188 let heap_version = buf[0];
189 if heap_version != ENC_VERS_0 && heap_version != ENC_VERS_1 {
190 return Err(FormatError::InvalidVersion(heap_version));
191 }
192 let mut pos = 1;
193
194 if buf.len() < pos + ss {
195 return Err(FormatError::BufferTooShort {
196 needed: pos + ss,
197 available: buf.len(),
198 });
199 }
200 let num_entries = crate::format::bytes::read_le_uint(&buf[pos..], ss) as usize;
201 pos += ss;
202
203 let mut mappings: Vec<VirtualMapping> = Vec::new();
204 for i in 0..num_entries {
205 let flags = if heap_version >= ENC_VERS_1 {
206 if buf.len() < pos + 1 {
207 return Err(FormatError::BufferTooShort {
208 needed: pos + 1,
209 available: buf.len(),
210 });
211 }
212 let f = buf[pos];
213 pos += 1;
214 if f & !ALL_FLAGS != 0 {
215 return Err(FormatError::InvalidData(format!(
216 "unknown virtual dataset mapping flag bits in {f:#x}"
217 )));
218 }
219 f
220 } else {
221 0
222 };
223
224 let source_file_name = if flags & SOURCE_SAME_FILE != 0 {
225 ".".to_string()
226 } else if flags & SOURCE_FILE_SHARED != 0 {
227 let origin = read_origin_index(buf, &mut pos, ss, i)?;
228 mappings[origin].source_file_name.clone()
229 } else {
230 read_cstr(buf, &mut pos)?
231 };
232
233 let source_dset_name = if flags & SOURCE_DSET_SHARED != 0 {
234 let origin = read_origin_index(buf, &mut pos, ss, i)?;
235 mappings[origin].source_dset_name.clone()
236 } else {
237 read_cstr(buf, &mut pos)?
238 };
239
240 let (source_selection, consumed) = Selection::decode(&buf[pos..])?;
241 pos += consumed;
242 let (virtual_selection, consumed) = Selection::decode(&buf[pos..])?;
243 pos += consumed;
244
245 parse_source_name(&source_file_name)?;
249 parse_source_name(&source_dset_name)?;
250
251 mappings.push(VirtualMapping {
252 source_file_name,
253 source_dset_name,
254 source_selection,
255 virtual_selection,
256 });
257 }
258
259 if buf.len() < pos + 4 {
260 return Err(FormatError::BufferTooShort {
261 needed: pos + 4,
262 available: buf.len(),
263 });
264 }
265 let stored_cksum = u32::from_le_bytes([buf[pos], buf[pos + 1], buf[pos + 2], buf[pos + 3]]);
266 let computed_cksum = checksum_metadata(&buf[..pos]);
267 if stored_cksum != computed_cksum {
268 return Err(FormatError::ChecksumMismatch {
269 expected: stored_cksum,
270 computed: computed_cksum,
271 });
272 }
273 pos += 4;
274
275 if pos != buf.len() {
276 return Err(FormatError::InvalidData(format!(
277 "virtual dataset mapping list declares {pos} bytes but the heap object holds {}",
278 buf.len()
279 )));
280 }
281
282 Ok(Self { mappings })
283 }
284}
285
286fn push_cstr(buf: &mut Vec<u8>, name: &str, what: &str) -> FormatResult<()> {
289 if name.as_bytes().contains(&0) {
290 return Err(FormatError::InvalidData(format!(
291 "virtual dataset {what} name {name:?} contains a NUL, which terminates a \
292 name on the wire"
293 )));
294 }
295 buf.extend_from_slice(name.as_bytes());
296 buf.push(0);
297 Ok(())
298}
299
300fn read_origin_index(
304 buf: &[u8],
305 pos: &mut usize,
306 ss: usize,
307 this_entry: usize,
308) -> FormatResult<usize> {
309 if buf.len() < *pos + ss {
310 return Err(FormatError::BufferTooShort {
311 needed: *pos + ss,
312 available: buf.len(),
313 });
314 }
315 let origin = crate::format::bytes::read_le_uint(&buf[*pos..], ss) as usize;
316 *pos += ss;
317 if origin >= this_entry {
318 return Err(FormatError::InvalidData(format!(
319 "virtual dataset mapping entry {this_entry} shares a name with entry {origin}, \
320 which is not an earlier entry"
321 )));
322 }
323 Ok(origin)
324}
325
326fn read_cstr(buf: &[u8], pos: &mut usize) -> FormatResult<String> {
331 let start = *pos;
332 let nul = buf[start..].iter().position(|&b| b == 0).ok_or_else(|| {
333 FormatError::InvalidData(
334 "virtual dataset mapping entry has an unterminated name string".into(),
335 )
336 })?;
337 let s = String::from_utf8_lossy(&buf[start..start + nul]).into_owned();
338 *pos = start + nul + 1;
339 Ok(s)
340}
341
342#[cfg(test)]
345mod tests {
346 use super::*;
347 use crate::format::selection::{Hyperslab, HyperslabBlock};
348
349 fn ctx8() -> FormatContext {
350 FormatContext {
351 sizeof_addr: 8,
352 sizeof_size: 8,
353 }
354 }
355
356 fn all_selection_bytes() -> Vec<u8> {
357 let mut b = vec![0x03, 0, 0, 0]; b.extend_from_slice(&1u32.to_le_bytes()); b.extend_from_slice(&[0u8; 8]); b
361 }
362
363 fn single_entry_all_block() -> Vec<u8> {
368 let mut body = vec![ENC_VERS_0];
369 body.extend_from_slice(&1u64.to_le_bytes()); body.extend_from_slice(b"src.h5\0");
371 body.extend_from_slice(b"data\0");
372 body.extend_from_slice(&all_selection_bytes()); body.extend_from_slice(&all_selection_bytes()); let cksum = checksum_metadata(&body);
375 body.extend_from_slice(&cksum.to_le_bytes());
376 body
377 }
378
379 #[test]
380 fn decode_single_all_mapping() {
381 let buf = single_entry_all_block();
382 let list = VirtualMappingList::decode(&buf, &ctx8()).unwrap();
383 assert_eq!(list.mappings.len(), 1);
384 let m = &list.mappings[0];
385 assert_eq!(m.source_file_name, "src.h5");
386 assert_eq!(m.source_dset_name, "data");
387 assert_eq!(m.source_selection, Selection::All);
388 assert_eq!(m.virtual_selection, Selection::All);
389 }
390
391 #[test]
392 fn decode_empty_mapping_list() {
393 let mut body = vec![ENC_VERS_0];
394 body.extend_from_slice(&0u64.to_le_bytes());
395 let cksum = checksum_metadata(&body);
396 body.extend_from_slice(&cksum.to_le_bytes());
397 let list = VirtualMappingList::decode(&body, &ctx8()).unwrap();
398 assert!(list.mappings.is_empty());
399 }
400
401 #[test]
402 fn decode_rejects_bad_checksum() {
403 let mut buf = single_entry_all_block();
404 let last = buf.len() - 1;
405 buf[last] ^= 0xFF;
406 let err = VirtualMappingList::decode(&buf, &ctx8()).unwrap_err();
407 assert!(matches!(err, FormatError::ChecksumMismatch { .. }));
408 }
409
410 #[test]
411 fn decode_rejects_bad_heap_version() {
412 let mut buf = single_entry_all_block();
413 buf[0] = 2;
414 let err = VirtualMappingList::decode(&buf, &ctx8()).unwrap_err();
415 assert!(matches!(err, FormatError::InvalidVersion(2)));
416 }
417
418 #[test]
419 fn decode_rejects_unterminated_name() {
420 let mut body = vec![ENC_VERS_0];
421 body.extend_from_slice(&1u64.to_le_bytes());
422 body.extend_from_slice(b"no_nul_here"); let err = VirtualMappingList::decode(&body, &ctx8()).unwrap_err();
424 assert!(matches!(err, FormatError::InvalidData(_)));
425 }
426
427 #[test]
431 fn decode_heap_version_1_same_file() {
432 let mut body = vec![ENC_VERS_1];
433 body.extend_from_slice(&2u64.to_le_bytes()); body.push(SOURCE_SAME_FILE);
437 body.extend_from_slice(b"a\0");
438 body.extend_from_slice(&all_selection_bytes());
439 body.extend_from_slice(&all_selection_bytes());
440
441 body.push(SOURCE_SAME_FILE);
443 body.extend_from_slice(b"b\0");
444 body.extend_from_slice(&all_selection_bytes());
445 body.extend_from_slice(&all_selection_bytes());
446
447 let cksum = checksum_metadata(&body);
448 body.extend_from_slice(&cksum.to_le_bytes());
449
450 let list = VirtualMappingList::decode(&body, &ctx8()).unwrap();
451 assert_eq!(list.mappings.len(), 2);
452 assert_eq!(list.mappings[0].source_file_name, ".");
453 assert_eq!(list.mappings[1].source_file_name, ".");
454 assert_eq!(list.mappings[0].source_dset_name, "a");
455 assert_eq!(list.mappings[1].source_dset_name, "b");
456 }
457
458 #[test]
461 fn decode_heap_version_1_shared_names() {
462 let mut body = vec![ENC_VERS_1];
463 body.extend_from_slice(&2u64.to_le_bytes());
464
465 body.push(0);
467 body.extend_from_slice(b"shared.h5\0");
468 body.extend_from_slice(b"data\0");
469 body.extend_from_slice(&all_selection_bytes());
470 body.extend_from_slice(&all_selection_bytes());
471
472 body.push(SOURCE_FILE_SHARED | SOURCE_DSET_SHARED);
474 body.extend_from_slice(&0u64.to_le_bytes()); body.extend_from_slice(&0u64.to_le_bytes()); body.extend_from_slice(&all_selection_bytes());
477 body.extend_from_slice(&all_selection_bytes());
478
479 let cksum = checksum_metadata(&body);
480 body.extend_from_slice(&cksum.to_le_bytes());
481
482 let list = VirtualMappingList::decode(&body, &ctx8()).unwrap();
483 assert_eq!(list.mappings[1].source_file_name, "shared.h5");
484 assert_eq!(list.mappings[1].source_dset_name, "data");
485 }
486
487 #[test]
491 fn decode_rejects_non_earlier_shared_origin() {
492 let mut body = vec![ENC_VERS_1];
493 body.extend_from_slice(&1u64.to_le_bytes());
494 body.push(SOURCE_FILE_SHARED);
495 body.extend_from_slice(&0u64.to_le_bytes()); let err = VirtualMappingList::decode(&body, &ctx8()).unwrap_err();
497 assert!(matches!(err, FormatError::InvalidData(_)));
498 }
499
500 #[test]
501 fn decode_rejects_unknown_flag_bits() {
502 let mut body = vec![ENC_VERS_1];
503 body.extend_from_slice(&1u64.to_le_bytes());
504 body.push(0x08); let err = VirtualMappingList::decode(&body, &ctx8()).unwrap_err();
506 assert!(matches!(err, FormatError::InvalidData(_)));
507 }
508
509 #[test]
512 fn decode_mapping_with_hyperslab_virtual_selection() {
513 let mut hyper = vec![0x02, 0, 0, 0]; hyper.extend_from_slice(&1u32.to_le_bytes()); hyper.extend_from_slice(&[0u8; 8]);
516 hyper.extend_from_slice(&1u32.to_le_bytes()); hyper.extend_from_slice(&1u32.to_le_bytes()); hyper.extend_from_slice(&4u32.to_le_bytes()); hyper.extend_from_slice(&11u32.to_le_bytes()); let mut body = vec![ENC_VERS_0];
522 body.extend_from_slice(&1u64.to_le_bytes());
523 body.extend_from_slice(b"src.h5\0");
524 body.extend_from_slice(b"data\0");
525 body.extend_from_slice(&all_selection_bytes()); body.extend_from_slice(&hyper); let cksum = checksum_metadata(&body);
528 body.extend_from_slice(&cksum.to_le_bytes());
529
530 let list = VirtualMappingList::decode(&body, &ctx8()).unwrap();
531 match &list.mappings[0].virtual_selection {
532 Selection::Hyperslab {
533 rank: 1,
534 form: Hyperslab::Blocks(blocks),
535 } => {
536 assert_eq!(
537 blocks,
538 &vec![HyperslabBlock {
539 start: vec![4],
540 end: vec![11],
541 }]
542 );
543 }
544 other => panic!("expected a rank-1 hyperslab, got {other:?}"),
545 }
546 }
547
548 #[test]
549 fn decode_truncated_num_entries() {
550 let buf = [ENC_VERS_0, 0, 0, 0];
551 let err = VirtualMappingList::decode(&buf, &ctx8()).unwrap_err();
552 assert!(matches!(err, FormatError::BufferTooShort { .. }));
553 }
554
555 #[test]
556 fn decode_empty_buffer() {
557 let err = VirtualMappingList::decode(&[], &ctx8()).unwrap_err();
558 assert!(matches!(err, FormatError::BufferTooShort { .. }));
559 }
560
561 #[test]
567 fn encode_matches_the_captured_libhdf5_block() {
568 let list = VirtualMappingList {
569 mappings: vec![VirtualMapping {
570 source_file_name: "vds_src.h5".into(),
571 source_dset_name: "src".into(),
572 source_selection: Selection::All,
573 virtual_selection: Selection::All,
574 }],
575 };
576 let captured = [
577 0x00, 0x01, 0, 0, 0, 0, 0, 0, 0, b'v', b'd', b's', b'_', b's', b'r', b'c', b'.', b'h', b'5', 0x00, b's', b'r', b'c',
580 0x00, 0x03, 0, 0, 0, 0x01, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x03, 0, 0, 0, 0x01, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xcd, 0xe5, 0xe5, 0xed, ];
585 assert_eq!(list.encode(&ctx8()).unwrap(), captured);
586 }
587
588 #[test]
589 fn encode_roundtrips_a_hyperslab_mapping_at_ctx4() {
590 let ctx4 = FormatContext {
591 sizeof_addr: 4,
592 sizeof_size: 4,
593 };
594 let list = VirtualMappingList {
595 mappings: vec![
596 VirtualMapping {
597 source_file_name: "a.h5".into(),
598 source_dset_name: "one".into(),
599 source_selection: Selection::All,
600 virtual_selection: Selection::Hyperslab {
601 rank: 1,
602 form: Hyperslab::Blocks(vec![HyperslabBlock {
603 start: vec![0],
604 end: vec![7],
605 }]),
606 },
607 },
608 VirtualMapping {
609 source_file_name: "b.h5".into(),
610 source_dset_name: "two".into(),
611 source_selection: Selection::All,
612 virtual_selection: Selection::Hyperslab {
613 rank: 1,
614 form: Hyperslab::Blocks(vec![HyperslabBlock {
615 start: vec![8],
616 end: vec![15],
617 }]),
618 },
619 },
620 ],
621 };
622 let encoded = list.encode(&ctx4).unwrap();
623 assert_eq!(VirtualMappingList::decode(&encoded, &ctx4).unwrap(), list);
624 }
625
626 #[test]
627 fn encode_empty_list_roundtrips() {
628 let list = VirtualMappingList {
629 mappings: Vec::new(),
630 };
631 let encoded = list.encode(&ctx8()).unwrap();
632 assert_eq!(encoded.len(), 1 + 8 + 4);
633 assert_eq!(VirtualMappingList::decode(&encoded, &ctx8()).unwrap(), list);
634 }
635
636 #[test]
637 fn encode_rejects_a_name_holding_a_nul() {
638 let list = VirtualMappingList {
639 mappings: vec![VirtualMapping {
640 source_file_name: "sr\0c.h5".into(),
641 source_dset_name: "src".into(),
642 source_selection: Selection::All,
643 virtual_selection: Selection::All,
644 }],
645 };
646 let err = list.encode(&ctx8()).unwrap_err();
647 assert!(matches!(err, FormatError::InvalidData(_)), "{err:?}");
648 }
649
650 #[test]
651 fn decode_rejects_trailing_garbage() {
652 let mut buf = single_entry_all_block();
653 buf.push(0xAB);
654 let err = VirtualMappingList::decode(&buf, &ctx8()).unwrap_err();
655 assert!(matches!(err, FormatError::InvalidData(_)));
656 }
657 #[test]
661 fn source_names_parse_and_build_the_way_libhdf5_does() {
662 for (name, nsubs, block7) in [
663 ("plain.h5", 0, "plain.h5"),
664 ("f%b.h5", 1, "f7.h5"),
665 ("%b", 1, "7"),
666 ("a%b%bc", 2, "a77c"),
667 ("od%%d", 0, "od%d"),
670 ("%%%b%%", 1, "%7%"),
671 ] {
672 let parsed = parse_source_name(name).unwrap();
673 assert_eq!(parsed.nsubs(), nsubs, "{name}");
674 assert_eq!(parsed.build(7), block7, "{name}");
675 }
676 assert_eq!(parse_source_name("b%b_%b").unwrap().build(123), "b123_123");
678 for bad in ["%z", "50%", "%d.h5", "%"] {
679 let err = parse_source_name(bad).unwrap_err();
680 assert!(
681 matches!(&err, FormatError::InvalidData(m) if m.contains("invalid format specifier")),
682 "{bad}: {err:?}"
683 );
684 }
685 }
686
687 #[test]
691 fn decode_rejects_an_illegal_format_specifier_in_a_stored_name() {
692 let list = VirtualMappingList {
693 mappings: vec![VirtualMapping {
694 source_file_name: "src.h5".into(),
695 source_dset_name: "d".into(),
696 source_selection: Selection::All,
697 virtual_selection: Selection::All,
698 }],
699 };
700 let mut buf = list.encode(&ctx8()).unwrap();
701 let at = buf
704 .windows(6)
705 .position(|w| w == b"src.h5")
706 .expect("name is inline");
707 buf[at..at + 6].copy_from_slice(b"s%z.h5");
708 let end = buf.len() - 4;
709 let cksum = checksum_metadata(&buf[..end]);
710 buf[end..].copy_from_slice(&cksum.to_le_bytes());
711 let err = VirtualMappingList::decode(&buf, &ctx8()).unwrap_err();
712 assert!(
713 matches!(&err, FormatError::InvalidData(m) if m.contains("invalid format specifier")),
714 "{err:?}"
715 );
716 }
717}