1use itertools::Itertools;
9use memmap2::Mmap;
10use std::{
11 cmp::{Ord, Ordering, PartialOrd},
12 collections::{HashMap, HashSet},
13 fs::File,
14 io::{self, SeekFrom, prelude::*},
15 num::TryFromIntError,
16 path::{Path, PathBuf},
17 string::FromUtf8Error,
18};
19use thiserror::Error;
20
21const NAME_SIZE: u64 = 64;
23
24const INDEX_ENTRY_SIZE: u64 = 32;
26
27const INDEX_ENTRY_USIZE: usize = 32;
29
30const HEADER_SIZE: u64 = 256;
32
33const HEADER_USIZE: usize = 256;
35
36const MAGIC_ID: u64 = 0x65df_65df_65df_65df;
38
39const CURRENT_FILE_VERSION: (u16, u16) = (2, 1);
41
42const INITIAL_INDEX_SIZE: u64 = 128;
44
45const INITIAL_NAME_LIST_SIZE: u64 = 1024;
47
48const INITIAL_MAXIMUM_WRITE_BUFFER_SIZE: usize = 1024 * 1024;
50
51#[non_exhaustive]
53#[derive(Error, Debug)]
54pub enum DecodeError {
55 #[error("I/O error")]
57 IO(#[from] io::Error),
58
59 #[error("invalid `application`")]
61 InvalidApplication(#[source] FromUtf8Error),
62
63 #[error("invalid `schema`")]
65 InvalidSchema(#[source] FromUtf8Error),
66
67 #[error("invalid file identifier `{0}`")]
69 InvalidFileIdentifier(u64),
70
71 #[error("index out of bounds (location={0}, length={1})")]
73 IndexOutOfBounds(u64, u64),
74
75 #[error("name list out of bounds (location={0}, length={1})")]
77 NameListOutOfBounds(u64, u64),
78
79 #[error("name list not terminated")]
81 NameListNotTerminated,
82
83 #[error("too many chunk names")]
85 NameListOverflow,
86
87 #[error("unsupported GSD file version ({0}, {1})")]
89 UnsupportedVersion(u16, u16),
90
91 #[error("file index not addressable")]
93 UnaddressableIndex(#[source] TryFromIntError),
94
95 #[error("file content not addressable")]
97 UnaddressableContent(#[source] TryFromIntError),
98
99 #[error("invalid chunk name")]
101 InvalidChunkName(#[source] FromUtf8Error),
102
103 #[error("duplicate chunk name")]
105 DuplicateChunkName,
106
107 #[error("corrupt index entry: `{0:?}`")]
109 CorruptIndexEntry(IndexEntry),
110
111 #[error("expected type {0}, found {1}")]
113 InvalidType(u8, u8),
114
115 #[error("expected to read read {0} columns, found {1}")]
117 InvalidColumns(usize, u32),
118
119 #[error("invalid string")]
121 InvalidString(#[source] FromUtf8Error),
122}
123
124#[non_exhaustive]
126#[derive(Error, Debug)]
127pub enum OpenError {
128 #[error("I/O error while creating or opening `{0}`")]
130 IO(PathBuf, #[source] io::Error),
131
132 #[error("cannot decode `{0}`")]
134 Decode(PathBuf, #[source] DecodeError),
135
136 #[error("the name `{0}` is too long")]
138 NameTooLong(String),
139}
140
141#[non_exhaustive]
143#[derive(Error, Debug)]
144pub enum ReadError {
145 #[error("chunk `{0}` not found in frame {1}")]
147 ChunkNotFound(String, u64),
148
149 #[error("cannot decode chunk `{0}` at frame {1}")]
151 Decode(String, u64, #[source] DecodeError),
152}
153
154#[non_exhaustive]
156#[derive(Error, Debug)]
157pub enum EncodeError {
158 #[error("too many chunk names")]
160 NameListOverflow,
161
162 #[error("file opened in read-only mode")]
164 NotWritable,
165
166 #[error("chunk `{0}` has already been written in frame {1}")]
168 DuplicateChunkName(String, u64),
169
170 #[error("the number of columns must be greater than zero and fit in a u32, got {0}")]
172 InvalidColumns(usize),
173}
174
175#[non_exhaustive]
177#[derive(Error, Debug)]
178pub enum SyncError {
179 #[error("I/O error")]
181 IO(#[from] io::Error),
182
183 #[error("file opened in read-only mode")]
185 NotWritable,
186
187 #[error("name list out of bounds (location={0}, length={1})")]
189 NameListOutOfBounds(u64, u64),
190
191 #[error("index out of bounds (location={0}, length={1})")]
193 IndexOutOfBounds(u64, u64),
194}
195
196#[non_exhaustive]
198#[derive(Error, Debug)]
199pub enum WriteError {
200 #[error("cannot encode chunk `{0}` at frame {1}")]
202 Encode(String, u64, #[source] EncodeError),
203
204 #[error("cannot synchronize while writing chunk `{0}` at frame {1}")]
206 Sync(String, u64, #[source] SyncError),
207}
208
209struct ArrayChunks<I, const M: usize> {
211 iter: I,
213}
214
215impl<T, I, const M: usize> Iterator for ArrayChunks<I, M>
216where
217 I: Iterator<Item = T>,
218{
219 type Item = [T; M];
220
221 fn next(&mut self) -> Option<Self::Item> {
222 self.iter.next_array::<M>()
223 }
224}
225
226impl<T, I, const M: usize> ExactSizeIterator for ArrayChunks<I, M>
227where
228 I: ExactSizeIterator<Item = T>,
229{
230 fn len(&self) -> usize {
231 self.iter.len() / M
232 }
233}
234
235mod private {
239 pub trait Sealed {}
241
242 impl Sealed for u8 {}
243 impl Sealed for u16 {}
244 impl Sealed for u32 {}
245 impl Sealed for u64 {}
246 impl Sealed for i8 {}
247 impl Sealed for i16 {}
248 impl Sealed for i32 {}
249 impl Sealed for i64 {}
250 impl Sealed for f32 {}
251 impl Sealed for f64 {}
252}
253
254pub trait Type: private::Sealed {
296 #[doc(hidden)]
298 fn gsd_data_type() -> u8;
299
300 #[doc(hidden)]
305 fn from_ne_byte_slice(bytes: &[u8]) -> Self;
306
307 #[doc(hidden)]
312 fn append_ne_bytes(&self, v: &mut Vec<u8>);
313}
314
315impl Type for u8 {
316 #[inline]
317 fn gsd_data_type() -> u8 {
318 1
319 }
320 #[inline]
321 fn from_ne_byte_slice(bytes: &[u8]) -> Self {
322 bytes[0]
323 }
324 #[inline]
325 fn append_ne_bytes(&self, v: &mut Vec<u8>) {
326 v.extend(&self.to_ne_bytes());
327 }
328}
329impl Type for u16 {
330 #[inline]
331 fn gsd_data_type() -> u8 {
332 2
333 }
334 #[inline]
335 fn from_ne_byte_slice(bytes: &[u8]) -> Self {
336 u16::from_ne_bytes(bytes.try_into().expect("byte slice should contain 2 bytes"))
337 }
338 #[inline]
339 fn append_ne_bytes(&self, v: &mut Vec<u8>) {
340 v.extend(&self.to_ne_bytes());
341 }
342}
343impl Type for u32 {
344 #[inline]
345 fn gsd_data_type() -> u8 {
346 3
347 }
348 #[inline]
349 fn from_ne_byte_slice(bytes: &[u8]) -> Self {
350 u32::from_ne_bytes(bytes.try_into().expect("byte slice should contain 4 bytes"))
351 }
352 #[inline]
353 fn append_ne_bytes(&self, v: &mut Vec<u8>) {
354 v.extend(&self.to_ne_bytes());
355 }
356}
357impl Type for u64 {
358 #[inline]
359 fn gsd_data_type() -> u8 {
360 4
361 }
362 #[inline]
363 fn from_ne_byte_slice(bytes: &[u8]) -> Self {
364 u64::from_ne_bytes(bytes.try_into().expect("byte slice should contain 8 bytes"))
365 }
366 #[inline]
367 fn append_ne_bytes(&self, v: &mut Vec<u8>) {
368 v.extend(&self.to_ne_bytes());
369 }
370}
371impl Type for i8 {
372 #[inline]
373 fn gsd_data_type() -> u8 {
374 5
375 }
376 #[inline]
377 fn from_ne_byte_slice(bytes: &[u8]) -> Self {
378 i8::from_ne_bytes(bytes.try_into().expect("byte slice should contain 1 byte"))
379 }
380 #[inline]
381 fn append_ne_bytes(&self, v: &mut Vec<u8>) {
382 v.extend(&self.to_ne_bytes());
383 }
384}
385impl Type for i16 {
386 #[inline]
387 fn gsd_data_type() -> u8 {
388 6
389 }
390 #[inline]
391 fn from_ne_byte_slice(bytes: &[u8]) -> Self {
392 i16::from_ne_bytes(bytes.try_into().expect("byte slice should contain 2 bytes"))
393 }
394 #[inline]
395 fn append_ne_bytes(&self, v: &mut Vec<u8>) {
396 v.extend(&self.to_ne_bytes());
397 }
398}
399impl Type for i32 {
400 #[inline]
401 fn gsd_data_type() -> u8 {
402 7
403 }
404 #[inline]
405 fn from_ne_byte_slice(bytes: &[u8]) -> Self {
406 i32::from_ne_bytes(bytes.try_into().expect("byte slice should contain 4 bytes"))
407 }
408 #[inline]
409 fn append_ne_bytes(&self, v: &mut Vec<u8>) {
410 v.extend(&self.to_ne_bytes());
411 }
412}
413impl Type for i64 {
414 #[inline]
415 fn gsd_data_type() -> u8 {
416 8
417 }
418 #[inline]
419 fn from_ne_byte_slice(bytes: &[u8]) -> Self {
420 i64::from_ne_bytes(bytes.try_into().expect("byte slice should contain 8 bytes"))
421 }
422 #[inline]
423 fn append_ne_bytes(&self, v: &mut Vec<u8>) {
424 v.extend(&self.to_ne_bytes());
425 }
426}
427impl Type for f32 {
428 #[inline]
429 fn gsd_data_type() -> u8 {
430 9
431 }
432 #[inline]
433 fn from_ne_byte_slice(bytes: &[u8]) -> Self {
434 f32::from_ne_bytes(bytes.try_into().expect("byte slice should contain 8 bytes"))
435 }
436 #[inline]
437 fn append_ne_bytes(&self, v: &mut Vec<u8>) {
438 v.extend(&self.to_ne_bytes());
439 }
440}
441impl Type for f64 {
442 #[inline]
443 fn gsd_data_type() -> u8 {
444 10
445 }
446 #[inline]
447 fn from_ne_byte_slice(bytes: &[u8]) -> Self {
448 f64::from_ne_bytes(bytes.try_into().expect("byte slice should contain 8 bytes"))
449 }
450 #[inline]
451 fn append_ne_bytes(&self, v: &mut Vec<u8>) {
452 v.extend(&self.to_ne_bytes());
453 }
454}
455
456#[derive(Debug, PartialEq)]
458pub(crate) struct GsdHeader {
459 magic: u64,
461
462 index_location: u64,
464
465 index_allocated_entries: u64,
467
468 namelist_location: u64,
470
471 namelist_allocated_entries: u64,
473
474 schema_version: (u16, u16),
476
477 gsd_version: (u16, u16),
479
480 application: String,
482
483 schema: String,
485}
486
487#[derive(Debug)]
489struct NameList {
490 name_id: HashMap<String, u16>,
492
493 n_names: u16,
495
496 insert_position: u64,
498
499 buffer: Vec<u8>,
501}
502
503#[derive(Debug)]
514struct Index {
515 n: u64,
517
518 buffer: Vec<IndexEntry>,
520
521 byte_buffer: Vec<u8>,
523
524 pending: usize,
526
527 frame_names: HashSet<u16>,
529}
530
531#[derive(Debug)]
559pub struct GsdFile {
560 file: File,
562
563 mode: Mode,
565
566 header: GsdHeader,
568
569 mmap: Mmap,
571
572 name_list: NameList,
574
575 index: Index,
577
578 data_buffer: Vec<u8>,
580
581 data_buffer_flushed: bool,
583
584 file_len: u64,
586
587 buffer_frame: u64,
589
590 file_frame: u64,
592
593 maximum_write_buffer_size: usize,
595}
596
597#[derive(Clone, Debug, PartialEq, Eq)]
604pub struct IndexEntry {
605 frame: u64,
607
608 n: u64,
610
611 location: u64,
613
614 m: u32,
616
617 id: u16,
619
620 data_type: u8,
622
623 flags: u8,
625}
626
627#[derive(Clone, Debug, Eq, PartialEq)]
631#[non_exhaustive]
632pub enum DataType {
633 U8,
635 U16,
637 U32,
639 U64,
641 I8,
643 I16,
645 I32,
647 I64,
649 F32,
651 F64,
653 String,
655}
656
657#[derive(Clone, Debug, PartialEq)]
668#[non_exhaustive]
669pub enum Mode {
670 Read,
672 Write,
674}
675
676#[inline]
682fn extract_ne_u64(bytes: &[u8]) -> (u64, &[u8]) {
683 let (bytes, rest) = bytes.split_at(size_of::<u64>());
684 (
685 u64::from_ne_bytes(
686 bytes
687 .try_into()
688 .expect("bytes slice should contain 8 bytes"),
689 ),
690 rest,
691 )
692}
693
694#[inline]
696fn extract_ne_u32(bytes: &[u8]) -> (u32, &[u8]) {
697 let (bytes, rest) = bytes.split_at(size_of::<u32>());
698 (
699 u32::from_ne_bytes(
700 bytes
701 .try_into()
702 .expect("bytes slice should contain 4 bytes"),
703 ),
704 rest,
705 )
706}
707
708#[inline]
710fn extract_ne_u16(bytes: &[u8]) -> (u16, &[u8]) {
711 let (bytes, rest) = bytes.split_at(size_of::<u16>());
712 (
713 u16::from_ne_bytes(
714 bytes
715 .try_into()
716 .expect("bytes slice should contain 2 bytes"),
717 ),
718 rest,
719 )
720}
721
722#[inline]
727fn extract_null_terminated_utf8(bytes: &[u8]) -> Result<(String, &[u8]), FromUtf8Error> {
728 let null_range_end = bytes
729 .iter()
730 .position(|&c| c == b'\0')
731 .unwrap_or(bytes.len());
732 let (bytes, mut rest) = bytes.split_at(null_range_end);
733 let s = String::from_utf8(bytes.into())?;
734 if !rest.is_empty() {
735 (_, rest) = rest.split_at(1);
736 }
737 Ok((s, rest))
738}
739
740impl PartialOrd for IndexEntry {
741 #[inline]
742 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
743 Some(self.cmp(other))
744 }
745}
746impl Ord for IndexEntry {
747 #[inline]
748 fn cmp(&self, other: &Self) -> Ordering {
749 (self.frame, self.id).cmp(&(other.frame, other.id))
750 }
751}
752
753impl GsdHeader {
754 fn try_from_ne_bytes(value: [u8; HEADER_USIZE]) -> Result<Self, DecodeError> {
756 let (magic, rest) = extract_ne_u64(&value);
760 if magic != MAGIC_ID {
761 return Err(DecodeError::InvalidFileIdentifier(magic));
762 }
763
764 let (index_location, rest) = extract_ne_u64(rest);
765 let (index_allocated_entries, rest) = extract_ne_u64(rest);
766 let (namelist_location, rest) = extract_ne_u64(rest);
767 let (namelist_allocated_entries, rest) = extract_ne_u64(rest);
768
769 usize::try_from(index_location).map_err(DecodeError::UnaddressableIndex)?;
773 usize::try_from(index_allocated_entries).map_err(DecodeError::UnaddressableIndex)?;
774 usize::try_from(namelist_location).map_err(DecodeError::UnaddressableIndex)?;
775 usize::try_from(namelist_allocated_entries).map_err(DecodeError::UnaddressableIndex)?;
776
777 let (schema_version, rest) = extract_ne_u32(rest);
778 let (gsd_version, rest) = extract_ne_u32(rest);
779 let (application, _) =
780 extract_null_terminated_utf8(rest).map_err(DecodeError::InvalidApplication)?;
781 let (schema, _) =
782 extract_null_terminated_utf8(&value[112..178]).map_err(DecodeError::InvalidSchema)?;
783
784 let schema_version = (
785 (schema_version >> 16) as u16,
786 (schema_version & 0xffff) as u16,
787 );
788 let gsd_version = ((gsd_version >> 16) as u16, (gsd_version & 0xffff) as u16);
789
790 if !((2, 0)..(3, 0)).contains(&gsd_version) {
793 return Err(DecodeError::UnsupportedVersion(
794 gsd_version.0,
795 gsd_version.1,
796 ));
797 }
798
799 Ok(GsdHeader {
800 magic,
801 index_location,
802 index_allocated_entries,
803 namelist_location,
804 namelist_allocated_entries,
805 schema_version,
806 gsd_version,
807 application,
808 schema,
809 })
810 }
811
812 #[inline]
814 fn to_ne_bytes(&self) -> [u8; HEADER_USIZE] {
815 let mut result = [0u8; HEADER_USIZE];
816 result[0..8].copy_from_slice(&self.magic.to_ne_bytes());
817 result[8..16].copy_from_slice(&self.index_location.to_ne_bytes());
818 result[16..24].copy_from_slice(&self.index_allocated_entries.to_ne_bytes());
819 result[24..32].copy_from_slice(&self.namelist_location.to_ne_bytes());
820 result[32..40].copy_from_slice(&self.namelist_allocated_entries.to_ne_bytes());
821 let schema_version =
822 u32::from(self.schema_version.0) << 16 | u32::from(self.schema_version.1);
823 result[40..44].copy_from_slice(&schema_version.to_ne_bytes());
824 let gsd_version: u32 = u32::from(self.gsd_version.0) << 16 | u32::from(self.gsd_version.1);
825 result[44..48].copy_from_slice(&gsd_version.to_ne_bytes());
826 result[48..48 + self.application.len()].copy_from_slice(self.application.as_bytes());
827 result[112..112 + self.schema.len()].copy_from_slice(self.schema.as_bytes());
828
829 result
830 }
831}
832
833impl IndexEntry {
834 #[must_use]
836 #[inline]
837 pub fn frame(&self) -> u64 {
838 self.frame
839 }
840
841 #[must_use]
843 #[inline]
844 pub fn rows(&self) -> u64 {
845 self.n
846 }
847
848 #[must_use]
850 #[inline]
851 pub fn columns(&self) -> u32 {
852 self.m
853 }
854
855 #[must_use]
873 #[inline]
874 pub fn data_type(&self) -> Option<DataType> {
875 match self.data_type {
876 1 => Some(DataType::U8),
877 2 => Some(DataType::U16),
878 3 => Some(DataType::U32),
879 4 => Some(DataType::U64),
880 5 => Some(DataType::I8),
881 6 => Some(DataType::I16),
882 7 => Some(DataType::I32),
883 8 => Some(DataType::I64),
884 9 => Some(DataType::F32),
885 10 => Some(DataType::F64),
886 11 => Some(DataType::String),
887 _ => None,
888 }
889 }
890
891 #[inline]
893 fn from_ne_bytes(value: [u8; 32]) -> Self {
894 let (frame, rest) = extract_ne_u64(&value);
895 let (n, rest) = extract_ne_u64(rest);
896 let (location, rest) = extract_ne_u64(rest);
897 let (m, rest) = extract_ne_u32(rest);
898 let (id, rest) = extract_ne_u16(rest);
899 let data_type = rest[0];
900 let flags = rest[1];
901 Self {
902 frame,
903 n,
904 location,
905 m,
906 id,
907 data_type,
908 flags,
909 }
910 }
911
912 #[inline]
914 fn to_ne_bytes(&self) -> [u8; INDEX_ENTRY_USIZE] {
915 let mut result = [0u8; INDEX_ENTRY_USIZE];
916 result[0..8].copy_from_slice(&self.frame.to_ne_bytes());
917 result[8..16].copy_from_slice(&self.n.to_ne_bytes());
918 result[16..24].copy_from_slice(&self.location.to_ne_bytes());
919 result[24..28].copy_from_slice(&self.m.to_ne_bytes());
920 result[28..30].copy_from_slice(&self.id.to_ne_bytes());
921 result[30] = self.data_type;
922 result[31] = self.flags;
923
924 result
925 }
926}
927
928impl GsdFile {
929 #[inline]
966 pub fn open<P: AsRef<Path>>(path: P, mode: Mode) -> Result<Self, OpenError> {
967 let file = File::options()
968 .read(true)
969 .write(mode == Mode::Write)
970 .create(false)
971 .truncate(false)
972 .open(&path)
973 .map_err(|e| OpenError::IO(path.as_ref().into(), e))?;
974 GsdFile::from_file(file, mode).map_err(|e| OpenError::Decode(path.as_ref().into(), e))
975 }
976
977 #[inline]
1008 pub fn create<P: AsRef<Path>>(
1009 path: P,
1010 application: &str,
1011 schema: &str,
1012 schema_version: (u16, u16),
1013 ) -> Result<Self, OpenError> {
1014 let mut file = File::options()
1015 .read(true)
1016 .write(true)
1017 .create(true)
1018 .truncate(true)
1019 .open(&path)
1020 .map_err(|e| OpenError::IO(path.as_ref().into(), e))?;
1021 GsdFile::initialize_file(&mut file, &path, application, schema, schema_version)?;
1022 GsdFile::from_file(file, Mode::Write)
1023 .map_err(|e| OpenError::Decode(path.as_ref().into(), e))
1024 }
1025
1026 #[inline]
1059 pub fn create_new<P: AsRef<Path>>(
1060 path: P,
1061 application: &str,
1062 schema: &str,
1063 schema_version: (u16, u16),
1064 ) -> Result<Self, OpenError> {
1065 let mut file = File::options()
1066 .read(true)
1067 .write(true)
1068 .create_new(true)
1069 .open(&path)
1070 .map_err(|e| OpenError::IO(path.as_ref().into(), e))?;
1071 GsdFile::initialize_file(&mut file, &path, application, schema, schema_version)?;
1072 GsdFile::from_file(file, Mode::Write)
1073 .map_err(|e| OpenError::Decode(path.as_ref().into(), e))
1074 }
1075
1076 fn initialize_file<P: AsRef<Path>>(
1078 file: &mut File,
1079 path: &P,
1080 application: &str,
1081 schema: &str,
1082 schema_version: (u16, u16),
1083 ) -> Result<(), OpenError> {
1084 let application = String::from(application);
1085 if application.len() as u64 > NAME_SIZE - 1 {
1086 return Err(OpenError::NameTooLong(application));
1087 }
1088 let schema = String::from(schema);
1089 if schema.len() as u64 > NAME_SIZE - 1 {
1090 return Err(OpenError::NameTooLong(schema));
1091 }
1092
1093 let header = GsdHeader {
1094 magic: MAGIC_ID,
1095 gsd_version: CURRENT_FILE_VERSION,
1096 application,
1097 schema,
1098 schema_version,
1099 index_location: HEADER_SIZE,
1100 index_allocated_entries: INITIAL_INDEX_SIZE,
1101 namelist_location: HEADER_SIZE + INDEX_ENTRY_SIZE * INITIAL_INDEX_SIZE,
1102 namelist_allocated_entries: INITIAL_NAME_LIST_SIZE / NAME_SIZE,
1103 };
1104
1105 file.write_all(&header.to_ne_bytes())
1106 .map_err(|e| OpenError::IO(path.as_ref().into(), e))?;
1107
1108 file.set_len(HEADER_SIZE + INDEX_ENTRY_SIZE * INITIAL_INDEX_SIZE + INITIAL_NAME_LIST_SIZE)
1109 .map_err(|e| OpenError::IO(path.as_ref().into(), e))?;
1110
1111 file.sync_all()
1112 .map_err(|e| OpenError::IO(path.as_ref().into(), e))?;
1113
1114 Ok(())
1115 }
1116
1117 fn from_file(file: File, mode: Mode) -> Result<GsdFile, DecodeError> {
1119 let mut file = file;
1120 file.rewind()?;
1121
1122 let mut header_bytes = [0_u8; HEADER_USIZE];
1123 file.read_exact(&mut header_bytes)?;
1124 let header = GsdHeader::try_from_ne_bytes(header_bytes)?;
1125
1126 let file_len = file.seek(SeekFrom::End(0))?;
1127 usize::try_from(file_len).map_err(DecodeError::UnaddressableContent)?;
1130
1131 if header.index_location > file_len
1134 || header.index_location + header.index_allocated_entries * INDEX_ENTRY_SIZE > file_len
1135 || header.index_allocated_entries == 0
1136 {
1137 return Err(DecodeError::IndexOutOfBounds(
1138 header.index_location,
1139 header.index_allocated_entries * INDEX_ENTRY_SIZE,
1140 ));
1141 }
1142 let namelist_range_end =
1143 header.namelist_location + header.namelist_allocated_entries * NAME_SIZE;
1144 if namelist_range_end > file_len || header.namelist_allocated_entries == 0 {
1145 return Err(DecodeError::NameListOutOfBounds(
1146 header.namelist_location,
1147 header.namelist_allocated_entries * NAME_SIZE,
1148 ));
1149 }
1150
1151 let mmap = unsafe { Mmap::map(&file)? };
1152 let last_namelist_offset =
1153 usize::try_from(namelist_range_end - 1).map_err(DecodeError::UnaddressableIndex)?;
1154 if mmap[last_namelist_offset] != 0 {
1155 return Err(DecodeError::NameListNotTerminated);
1156 }
1157
1158 let start =
1159 usize::try_from(header.namelist_location).map_err(DecodeError::UnaddressableIndex)?;
1160 let end = usize::try_from(namelist_range_end).map_err(DecodeError::UnaddressableIndex)?;
1161 let name_list = GsdFile::decode_name_map(&mmap[start..end])?;
1162 let index = Index {
1163 n: 0,
1164 buffer: Vec::new(),
1165 byte_buffer: Vec::new(),
1166 pending: 0,
1167 frame_names: HashSet::new(),
1168 };
1169
1170 let mut gsd_file = GsdFile {
1171 file,
1172 mode,
1173 header,
1174 mmap,
1175 file_len,
1176 name_list,
1177 index,
1178 data_buffer: Vec::new(),
1179 data_buffer_flushed: false,
1180 buffer_frame: 0,
1181 file_frame: 0,
1182 maximum_write_buffer_size: INITIAL_MAXIMUM_WRITE_BUFFER_SIZE,
1183 };
1184
1185 gsd_file.index.n = gsd_file.count_index_entries()?;
1186 if gsd_file.index.n > 0 {
1187 let last_entry = gsd_file.get_index(gsd_file.index.n - 1)?;
1188 gsd_file.file_frame = last_entry.frame + 1;
1189 gsd_file.buffer_frame = gsd_file.file_frame;
1190 }
1191
1192 if gsd_file.mode == Mode::Write
1195 && gsd_file.header.gsd_version.0 == CURRENT_FILE_VERSION.0
1196 && gsd_file.header.gsd_version.1 < CURRENT_FILE_VERSION.1
1197 {
1198 gsd_file.header.gsd_version.1 = CURRENT_FILE_VERSION.1;
1199
1200 gsd_file.file.seek(SeekFrom::Start(0))?;
1201 gsd_file.file.write_all(&gsd_file.header.to_ne_bytes())?;
1202 }
1203
1204 Ok(gsd_file)
1205 }
1206
1207 fn decode_name_map(bytes: &[u8]) -> Result<NameList, DecodeError> {
1209 let mut name_id = HashMap::new();
1210 let mut bytes = bytes;
1211
1212 let mut current_id: u16 = 0;
1213 let mut insert_position: u64 = 0;
1214 while !bytes.is_empty() && bytes[0] != 0 {
1215 let (name, rest) =
1216 extract_null_terminated_utf8(bytes).map_err(DecodeError::InvalidChunkName)?;
1217 bytes = rest;
1218
1219 insert_position += (name.len() + 1) as u64;
1221
1222 let previous = name_id.insert(name, current_id);
1223 if previous.is_some() {
1224 return Err(DecodeError::DuplicateChunkName);
1225 }
1226
1227 if current_id == u16::MAX {
1228 return Err(DecodeError::NameListOverflow);
1229 }
1230
1231 current_id += 1;
1232 }
1233
1234 Ok(NameList {
1235 name_id,
1236 n_names: current_id,
1237 insert_position,
1238 buffer: Vec::new(),
1239 })
1240 }
1241
1242 #[inline]
1244 fn get_id(&mut self, name: &str) -> Result<u16, EncodeError> {
1245 if let Some(id) = self.name_list.name_id.get(name) {
1246 return Ok(*id);
1247 }
1248
1249 let new_id = self.name_list.n_names;
1250 if new_id == u16::MAX {
1251 return Err(EncodeError::NameListOverflow);
1252 }
1253
1254 self.name_list.n_names += 1;
1255 self.name_list.buffer.extend(name.as_bytes());
1256 self.name_list.buffer.push(0);
1257 self.name_list.name_id.insert(String::from(name), new_id);
1258 Ok(new_id)
1259 }
1260
1261 #[inline]
1263 #[cfg(target_os = "linux")]
1264 fn remap(&mut self) -> Result<(), io::Error> {
1265 unsafe {
1266 self.mmap.remap(
1267 self.file_len
1268 .try_into()
1269 .expect("file length should be validated elsewhere"),
1270 memmap2::RemapOptions::new().may_move(true),
1271 )?;
1272 }
1273 Ok(())
1274 }
1275
1276 #[inline]
1278 #[cfg(not(target_os = "linux"))]
1279 fn remap(&mut self) -> Result<(), io::Error> {
1280 self.mmap = unsafe { Mmap::map(&self.file)? };
1281 Ok(())
1282 }
1283
1284 #[inline]
1286 fn get_index(&self, i: u64) -> Result<IndexEntry, DecodeError> {
1287 debug_assert_eq!(self.mmap.len() as u64, self.file_len);
1290
1291 let start = self.header.index_location + i * INDEX_ENTRY_SIZE;
1292 let end = start + INDEX_ENTRY_SIZE;
1293 debug_assert!(
1294 end <= self.header.index_location
1295 + self.header.index_allocated_entries * INDEX_ENTRY_SIZE
1296 );
1297
1298 let start = usize::try_from(start).map_err(DecodeError::UnaddressableIndex)?;
1299 let end = usize::try_from(end).map_err(DecodeError::UnaddressableIndex)?;
1300 let bytes: [u8; INDEX_ENTRY_USIZE] = self.mmap[start..end]
1301 .try_into()
1302 .expect("slice should always be the correct size");
1303 Ok(IndexEntry::from_ne_bytes(bytes))
1304 }
1305
1306 #[inline]
1308 fn size_of(data_type: u8) -> Option<usize> {
1309 match data_type {
1310 1 => Some(size_of::<u8>()),
1311 2 => Some(size_of::<u16>()),
1312 3 => Some(size_of::<u32>()),
1313 4 => Some(size_of::<u64>()),
1314 5 => Some(size_of::<i8>()),
1315 6 => Some(size_of::<i16>()),
1316 7 => Some(size_of::<i32>()),
1317 8 => Some(size_of::<i64>()),
1318 9 => Some(size_of::<f32>()),
1319 10 => Some(size_of::<f64>()),
1320 11 => Some(1),
1321 _ => None,
1322 }
1323 }
1324
1325 fn is_entry_valid(&self, entry: &IndexEntry) -> bool {
1327 match GsdFile::size_of(entry.data_type) {
1328 Some(element_size) => {
1329 let total_size = entry.n * u64::from(entry.m) * element_size as u64;
1330 assert!(entry.location + total_size <= self.file_len);
1331 if entry.location + total_size > self.file_len {
1332 return false;
1333 }
1334 }
1335 None => return false,
1336 }
1337
1338 assert!(entry.frame < self.header.index_allocated_entries);
1342 if entry.frame >= self.header.index_allocated_entries {
1343 return false;
1344 }
1345
1346 assert!(entry.id < self.name_list.n_names);
1350 if entry.id >= self.name_list.n_names {
1351 return false;
1352 }
1353
1354 if entry.flags != 0 {
1355 return false;
1356 }
1357
1358 true
1359 }
1360
1361 fn count_index_entries(&self) -> Result<u64, DecodeError> {
1363 let first_entry = self.get_index(0)?;
1364 if first_entry.location != 0 && !self.is_entry_valid(&first_entry) {
1365 return Err(DecodeError::CorruptIndexEntry(first_entry));
1366 }
1367
1368 if first_entry.location == 0 {
1369 return Ok(0);
1370 }
1371
1372 let mut l: u64 = 0;
1375 let mut r = self.header.index_allocated_entries;
1376
1377 loop {
1379 let m = l.midpoint(r);
1380
1381 let entry_m = self.get_index(m)?;
1384 let entry_l = self.get_index(l)?;
1385
1386 if entry_m.location != 0 {
1387 if !self.is_entry_valid(&entry_m) || entry_m.frame < entry_l.frame {
1388 return Err(DecodeError::CorruptIndexEntry(entry_m));
1389 }
1390 l = m;
1391 } else {
1392 r = m;
1393 }
1394
1395 if r - l == 1 {
1396 break;
1397 }
1398 }
1399
1400 Ok(r)
1402 }
1403
1404 #[must_use]
1437 pub fn find_chunk(&self, frame: u64, name: &str) -> Option<IndexEntry> {
1438 if frame >= self.file_frame || self.index.n == 0 {
1439 return None;
1440 }
1441
1442 let id = self.name_list.name_id.get(name)?;
1443
1444 let mut l: u64 = 0;
1446 let mut r = self.index.n - 1;
1447
1448 while l <= r {
1449 let m = l.midpoint(r);
1450
1451 if let Ok(index_entry_m) = self.get_index(m) {
1454 match (index_entry_m.frame, index_entry_m.id).cmp(&(frame, *id)) {
1455 Ordering::Less => l = m + 1,
1456 Ordering::Greater => r = m - 1,
1457 Ordering::Equal => return Some(index_entry_m),
1458 }
1459 } else {
1460 return None;
1461 }
1462 }
1463 None
1464 }
1465
1466 pub fn iter_scalars<T: Type>(
1508 &self,
1509 frame: u64,
1510 name: &str,
1511 ) -> Result<impl ExactSizeIterator<Item = T> + use<'_, T>, ReadError> {
1512 let Some(index_entry) = self.find_chunk(frame, name) else {
1513 return Err(ReadError::ChunkNotFound(name.into(), frame));
1514 };
1515
1516 if index_entry.m as usize != 1 {
1517 return Err(ReadError::Decode(
1518 name.into(),
1519 frame,
1520 DecodeError::InvalidColumns(1, index_entry.m),
1521 ));
1522 }
1523
1524 if index_entry.data_type != T::gsd_data_type() {
1525 return Err(ReadError::Decode(
1526 name.into(),
1527 frame,
1528 DecodeError::InvalidType(T::gsd_data_type(), index_entry.data_type),
1529 ));
1530 }
1531
1532 self.read_details(&index_entry)
1533 .map_err(|e| ReadError::Decode(name.into(), frame, e))
1534 }
1535
1536 pub fn iter_arrays<T: Type, const M: usize>(
1578 &self,
1579 frame: u64,
1580 name: &str,
1581 ) -> Result<impl ExactSizeIterator<Item = [T; M]> + use<'_, T, M>, ReadError> {
1582 let Some(index_entry) = self.find_chunk(frame, name) else {
1583 return Err(ReadError::ChunkNotFound(name.into(), frame));
1584 };
1585
1586 if index_entry.m as usize != M {
1587 return Err(ReadError::Decode(
1588 name.into(),
1589 frame,
1590 DecodeError::InvalidColumns(M, index_entry.m),
1591 ));
1592 }
1593
1594 if index_entry.data_type != T::gsd_data_type() {
1595 return Err(ReadError::Decode(
1596 name.into(),
1597 frame,
1598 DecodeError::InvalidType(T::gsd_data_type(), index_entry.data_type),
1599 ));
1600 }
1601
1602 Ok(ArrayChunks {
1603 iter: self
1604 .read_details::<T>(&index_entry)
1605 .map_err(|e| ReadError::Decode(name.into(), frame, e))?,
1606 })
1607 }
1608
1609 pub fn read_string(&self, frame: u64, name: &str) -> Result<String, ReadError> {
1645 let Some(index_entry) = self.find_chunk(frame, name) else {
1646 return Err(ReadError::ChunkNotFound(name.into(), frame));
1647 };
1648
1649 if index_entry.m as usize != 1 {
1650 return Err(ReadError::Decode(
1651 name.into(),
1652 frame,
1653 DecodeError::InvalidColumns(1, index_entry.m),
1654 ));
1655 }
1656
1657 if index_entry.data_type != 11 {
1658 return Err(ReadError::Decode(
1659 name.into(),
1660 frame,
1661 DecodeError::InvalidType(11, index_entry.data_type),
1662 ));
1663 }
1664
1665 let array = self
1666 .read_details::<u8>(&index_entry)
1667 .map_err(|e| ReadError::Decode(name.into(), frame, e))?
1668 .collect::<Vec<_>>();
1669
1670 String::from_utf8(array)
1671 .map_err(|e| ReadError::Decode(name.into(), frame, DecodeError::InvalidString(e)))
1672 }
1673
1674 fn read_details<T: Type>(
1676 &self,
1677 index_entry: &IndexEntry,
1678 ) -> Result<impl ExactSizeIterator<Item = T> + use<'_, T>, DecodeError> {
1679 let n_elements = index_entry.n * u64::from(index_entry.m);
1680 let n_bytes = usize::try_from(n_elements * size_of::<T>() as u64)
1681 .map_err(DecodeError::UnaddressableContent)?;
1682
1683 let location =
1684 usize::try_from(index_entry.location).map_err(DecodeError::UnaddressableContent)?;
1685
1686 if index_entry.location == 0 {
1687 return Err(DecodeError::CorruptIndexEntry(index_entry.clone()));
1688 }
1689
1690 debug_assert!(location + n_bytes <= self.mmap.len());
1691
1692 Ok(self.mmap[location..location + n_bytes]
1693 .chunks(size_of::<T>())
1694 .map(T::from_ne_byte_slice))
1695 }
1696
1697 pub fn write_scalars<T, I>(&mut self, name: &str, data: I) -> Result<(), WriteError>
1736 where
1737 T: Type,
1738 I: IntoIterator<Item = T>,
1739 {
1740 let data = data.into_iter();
1741
1742 self.write_details(name, 1, T::gsd_data_type(), |buffer: &mut Vec<u8>| -> u64 {
1743 let mut len = 0;
1744 for value in data {
1745 value.append_ne_bytes(buffer);
1746 len += 1;
1747 }
1748 len
1749 })
1750 }
1751
1752 pub fn write_arrays<T, I, const M: usize>(
1791 &mut self,
1792 name: &str,
1793 data: I,
1794 ) -> Result<(), WriteError>
1795 where
1796 T: Type,
1797 I: IntoIterator<Item = [T; M]>,
1798 {
1799 if M == 0 {
1800 return Err(WriteError::Encode(
1801 name.into(),
1802 self.buffer_frame,
1803 EncodeError::InvalidColumns(M),
1804 ));
1805 }
1806
1807 let columns = u32::try_from(M).or(Err(WriteError::Encode(
1808 name.into(),
1809 self.buffer_frame,
1810 EncodeError::InvalidColumns(M),
1811 )))?;
1812
1813 let data = data.into_iter();
1814
1815 self.write_details(
1816 name,
1817 columns,
1818 T::gsd_data_type(),
1819 |buffer: &mut Vec<u8>| -> u64 {
1820 let mut len = 0;
1821 for element in data {
1822 len += 1;
1823 for value in element {
1824 value.append_ne_bytes(buffer);
1825 }
1826 }
1827 len
1828 },
1829 )
1830 }
1831
1832 pub fn write_string(&mut self, name: &str, data: &str) -> Result<(), WriteError> {
1867 let data = data.as_bytes();
1868
1869 self.write_details(name, 1, 11, |buffer: &mut Vec<u8>| -> u64 {
1870 buffer.extend(data);
1871 data.len() as u64
1872 })
1873 }
1874
1875 fn write_details<F>(
1879 &mut self,
1880 name: &str,
1881 columns: u32,
1882 data_type: u8,
1883 append: F,
1884 ) -> Result<(), WriteError>
1885 where
1886 F: FnOnce(&mut Vec<u8>) -> u64,
1887 {
1888 if self.mode != Mode::Write {
1889 return Err(WriteError::Encode(
1890 name.into(),
1891 self.buffer_frame,
1892 EncodeError::NotWritable,
1893 ));
1894 }
1895
1896 let location = self.file_len + self.data_buffer.len() as u64;
1897
1898 let id = self
1899 .get_id(name)
1900 .map_err(|e| WriteError::Encode(name.into(), self.buffer_frame, e))?;
1901
1902 if !self.index.frame_names.insert(id) {
1903 return Err(WriteError::Encode(
1904 name.into(),
1905 self.buffer_frame,
1906 EncodeError::DuplicateChunkName(name.into(), self.buffer_frame),
1907 ));
1908 }
1909
1910 let rows = append(&mut self.data_buffer);
1922
1923 let index_entry = IndexEntry {
1931 frame: self.buffer_frame,
1932 n: rows,
1933 m: columns,
1934 location,
1935 id,
1936 data_type,
1937 flags: 0,
1938 };
1939
1940 self.index.buffer.push(index_entry);
1941 self.index.pending += 1;
1942
1943 if self.data_buffer.len() >= self.maximum_write_buffer_size {
1944 self.flush_data()
1945 .map_err(|e| WriteError::Sync(name.into(), self.buffer_frame, e))?;
1946 self.data_buffer_flushed = true;
1947 }
1948
1949 Ok(())
1950 }
1951
1952 pub fn end_frame(&mut self) -> Result<(), EncodeError> {
1986 if self.mode != Mode::Write {
1987 return Err(EncodeError::NotWritable);
1988 }
1989
1990 self.buffer_frame += 1;
1991 self.index.pending = 0;
1992 self.index.frame_names.clear();
1993
1994 Ok(())
1995 }
1996
1997 #[inline]
1998 #[must_use]
1999 pub fn n_frames(&self) -> u64 {
2029 self.file_frame
2030 }
2031
2032 #[inline]
2033 #[must_use]
2034 pub fn name_id(&self) -> &HashMap<String, u16> {
2066 &self.name_list.name_id
2067 }
2068
2069 #[inline]
2087 #[must_use]
2088 pub fn application(&self) -> &str {
2089 &self.header.application
2090 }
2091
2092 #[inline]
2110 #[must_use]
2111 pub fn schema(&self) -> &str {
2112 &self.header.schema
2113 }
2114
2115 #[inline]
2133 #[must_use]
2134 pub fn schema_version(&self) -> (u16, u16) {
2135 self.header.schema_version
2136 }
2137
2138 #[inline]
2139 #[must_use]
2140 pub fn maximum_write_buffer_size(&self) -> usize {
2142 self.maximum_write_buffer_size
2143 }
2144
2145 #[inline]
2146 #[must_use]
2147 pub fn maximum_write_buffer_size_mut(&mut self) -> &mut usize {
2149 &mut self.maximum_write_buffer_size
2150 }
2151
2152 fn flush_data(&mut self) -> Result<bool, SyncError> {
2156 if self.data_buffer.is_empty() {
2157 Ok(false)
2158 } else {
2159 let current_len = self.file.seek(SeekFrom::End(0))?;
2160 debug_assert_eq!(current_len, self.file_len);
2161 self.file.write_all(&self.data_buffer)?;
2162 self.file_len += self.data_buffer.len() as u64;
2163 self.data_buffer.clear();
2164 Ok(true)
2165 }
2166 }
2167
2168 fn flush_names(&mut self) -> Result<bool, SyncError> {
2172 if self.name_list.buffer.is_empty() {
2173 Ok(false)
2174 } else {
2175 if self.name_list.insert_position + self.name_list.buffer.len() as u64
2176 > self.header.namelist_allocated_entries * NAME_SIZE
2177 {
2178 self.expand_name_list_to(
2179 self.name_list.insert_position + self.name_list.buffer.len() as u64,
2180 )?;
2181 }
2182 debug_assert!(
2183 self.name_list.insert_position + self.name_list.buffer.len() as u64
2184 <= self.header.namelist_allocated_entries * NAME_SIZE
2185 );
2186 self.file.seek(SeekFrom::Start(
2187 self.header.namelist_location + self.name_list.insert_position,
2188 ))?;
2189 self.file.write_all(&self.name_list.buffer)?;
2190 self.name_list.insert_position += self.name_list.buffer.len() as u64;
2191 self.name_list.buffer.clear();
2192 Ok(true)
2193 }
2194 }
2195
2196 pub fn sync_all(&mut self) -> Result<(), SyncError> {
2212 if self.mode != Mode::Write {
2213 return Err(SyncError::NotWritable);
2214 }
2215
2216 let mut need_remap = false;
2217
2218 if self.flush_data()? || self.data_buffer_flushed {
2222 need_remap = true;
2223 self.data_buffer_flushed = false;
2224 self.file.sync_all()?;
2225 }
2226
2227 self.flush_names()?;
2230
2231 let index_entries_to_write = self.index.buffer.len() - self.index.pending;
2236 if index_entries_to_write > 0 {
2237 if self.index.n + index_entries_to_write as u64 > self.header.index_allocated_entries {
2238 need_remap = true;
2239 self.expand_index_to(
2240 (self.index.n + index_entries_to_write as u64) * INDEX_ENTRY_SIZE,
2241 )?;
2242 }
2243 debug_assert!(
2244 self.index.n + index_entries_to_write as u64 <= self.header.index_allocated_entries
2245 );
2246 self.index.buffer[0..index_entries_to_write].sort_unstable();
2247
2248 self.index.byte_buffer.clear();
2251 for entry in self.index.buffer.drain(0..index_entries_to_write) {
2252 self.index.byte_buffer.extend(&entry.to_ne_bytes());
2253 }
2254 self.file.seek(SeekFrom::Start(
2255 self.header.index_location + self.index.n * INDEX_ENTRY_SIZE,
2256 ))?;
2257 self.file.write_all(&self.index.byte_buffer)?;
2258 self.index.n += index_entries_to_write as u64;
2259
2260 self.file.sync_all()?;
2261 }
2262
2263 if need_remap {
2264 self.remap()?;
2265 }
2266
2267 self.file_frame = self.buffer_frame;
2268
2269 Ok(())
2270 }
2271
2272 fn expand_name_list_to(&mut self, capacity: u64) -> Result<(), SyncError> {
2274 let old_size = self.header.namelist_allocated_entries * NAME_SIZE;
2275 let mut new_size = old_size;
2276 while new_size <= capacity {
2277 new_size *= 2;
2278 }
2279
2280 let new_allocated_entries = new_size.div_ceil(NAME_SIZE);
2283 let new_size = new_allocated_entries * NAME_SIZE;
2284 let new_location = self.file.seek(SeekFrom::End(0))?;
2285
2286 usize::try_from(new_location)
2287 .map_err(|_| SyncError::NameListOutOfBounds(new_location, new_size))?;
2288 usize::try_from(new_location + new_size)
2289 .map_err(|_| SyncError::NameListOutOfBounds(new_location, new_size))?;
2290
2291 let old_start = usize::try_from(self.header.namelist_location)
2292 .expect("namelist should be validated addressable previously");
2293 let old_end =
2294 usize::try_from(self.header.namelist_location + self.name_list.insert_position)
2295 .expect("namelist should be validated addressable previously");
2296 self.file.write_all(&self.mmap[old_start..old_end])?;
2297 self.file.set_len(new_location + new_size)?;
2298 self.file_len = new_location + new_size;
2299
2300 self.file.sync_all()?;
2305
2306 self.header.namelist_location = new_location;
2307 self.header.namelist_allocated_entries = new_allocated_entries;
2308 self.file.seek(SeekFrom::Start(0))?;
2309 self.file.write_all(&self.header.to_ne_bytes())?;
2310
2311 self.file.sync_all()?;
2312
2313 Ok(())
2314 }
2315
2316 fn expand_index_to(&mut self, capacity: u64) -> Result<(), SyncError> {
2318 let old_size = self.header.index_allocated_entries * INDEX_ENTRY_SIZE;
2319 let mut new_size = old_size;
2320 while new_size <= capacity {
2321 new_size *= 2;
2322 }
2323
2324 let new_allocated_entries = new_size.div_ceil(INDEX_ENTRY_SIZE);
2327 let new_size = new_allocated_entries * INDEX_ENTRY_SIZE;
2328 let new_location = self.file.seek(SeekFrom::End(0))?;
2329
2330 usize::try_from(new_location)
2331 .map_err(|_| SyncError::IndexOutOfBounds(new_location, new_size))?;
2332 usize::try_from(new_location + new_size)
2333 .map_err(|_| SyncError::IndexOutOfBounds(new_location, new_size))?;
2334
2335 let old_start = usize::try_from(self.header.index_location)
2336 .expect("index should be validated addressable previously");
2337 let old_end = usize::try_from(self.header.index_location + self.index.n * INDEX_ENTRY_SIZE)
2338 .expect("index should be validated addressable previously");
2339 if old_end > self.mmap.len() {
2340 return Err(SyncError::IndexOutOfBounds(
2341 old_start as u64,
2342 old_end as u64,
2343 ));
2344 }
2345 self.file.write_all(&self.mmap[old_start..old_end])?;
2346 self.file.set_len(new_location + new_size)?;
2347 self.file_len = new_location + new_size;
2348
2349 self.file.sync_all()?;
2354
2355 self.header.index_location = new_location;
2356 self.header.index_allocated_entries = new_allocated_entries;
2357 self.file.seek(SeekFrom::Start(0))?;
2358 self.file.write_all(&self.header.to_ne_bytes())?;
2359
2360 self.file.sync_all()?;
2361
2362 Ok(())
2363 }
2364
2365 #[inline]
2367 #[must_use]
2368 pub fn mode(&self) -> &Mode {
2369 &self.mode
2370 }
2371}
2372
2373impl Drop for GsdFile {
2379 fn drop(&mut self) {
2380 let _ = self.sync_all();
2381 }
2382}
2383
2384#[cfg(test)]
2385mod tests {
2386 use super::*;
2387 use tempfile::tempdir;
2388
2389 #[test]
2390 fn create_new() -> anyhow::Result<()> {
2391 let tmp_dir = tempdir()?;
2392 let path = tmp_dir.path().join("test.gsd");
2393 GsdFile::create_new(path.clone(), "application", "schema", (12, 42))?;
2394
2395 let gsd_file = GsdFile::open(path.clone(), Mode::Read)?;
2396 assert_eq!(gsd_file.application(), "application");
2397 assert_eq!(gsd_file.schema(), "schema");
2398 assert_eq!(gsd_file.schema_version(), (12, 42));
2399 assert_eq!(gsd_file.n_frames(), 0);
2400 assert!(gsd_file.name_id().is_empty());
2401
2402 assert!(matches!(
2403 GsdFile::create_new(path.clone(), "application", "schema", (12, 42)),
2404 Err(OpenError::IO(_, _))
2405 ));
2406
2407 Ok(())
2408 }
2409
2410 #[test]
2411 fn open_write() -> anyhow::Result<()> {
2412 let tmp_dir = tempdir()?;
2413 let path = tmp_dir.path().join("test.gsd");
2414 GsdFile::create_new(path.clone(), "application", "schema", (12, 42))?;
2415
2416 let mut gsd_file = GsdFile::open(path.clone(), Mode::Write)?;
2417 gsd_file.write_scalars("a", [1])?;
2418 gsd_file.end_frame()?;
2419 gsd_file.sync_all()?;
2420
2421 Ok(())
2422 }
2423
2424 #[test]
2425 fn create_errors() -> anyhow::Result<()> {
2426 let tmp_dir = tempdir()?;
2427 let path = tmp_dir.path().join("test.gsd");
2428 GsdFile::create(path.clone(), "application", "schema", (12, 42))?;
2429
2430 let long_application = "a".repeat(64);
2431 let result = GsdFile::create(path.clone(), &long_application, "schema", (1, 0));
2432 assert!(matches!(result, Err(OpenError::NameTooLong(_))));
2433
2434 let long_schema = "s".repeat(64);
2435 let result = GsdFile::create(path.clone(), "a", &long_schema, (1, 0));
2436 assert!(matches!(result, Err(OpenError::NameTooLong(_))));
2437
2438 let just_right_application = "a".repeat(63);
2439 let just_right_schema = "s".repeat(63);
2440 let result = GsdFile::create(
2441 path.clone(),
2442 &just_right_application,
2443 &just_right_schema,
2444 (1, 0),
2445 );
2446 assert!(result.is_ok());
2447
2448 Ok(())
2449 }
2450
2451 #[test]
2452 fn maximum_write_buffer_size() -> anyhow::Result<()> {
2453 let tmp_dir = tempdir()?;
2454 let path = tmp_dir.path().join("test.gsd");
2455 let mut gsd_file = GsdFile::create(path.clone(), "a", "s", (1, 0))?;
2456
2457 *gsd_file.maximum_write_buffer_size_mut() = 8;
2458 assert_eq!(gsd_file.maximum_write_buffer_size(), 8);
2459
2460 let initial_size = gsd_file.file.metadata()?.len();
2461 assert_eq!(initial_size, gsd_file.file_len);
2462
2463 gsd_file
2464 .write_scalars::<u64, _>("a", [1])
2465 .expect("write should succeed");
2466 gsd_file.end_frame()?;
2467
2468 let final_size = gsd_file.file.metadata()?.len();
2469 assert_eq!(final_size, gsd_file.file_len);
2470 assert_eq!(final_size, initial_size + 8);
2471
2472 Ok(())
2473 }
2474
2475 #[test]
2476 fn sync_all() -> anyhow::Result<()> {
2477 let tmp_dir = tempdir()?;
2478 let path = tmp_dir.path().join("test.gsd");
2479 let mut gsd_file = GsdFile::create(path.clone(), "a", "s", (1, 0))?;
2480
2481 let initial_size = gsd_file.file.metadata()?.len();
2482
2483 gsd_file.write_scalars::<u64, _>("a", [1])?;
2484 gsd_file.end_frame().expect("write should succeed");
2485
2486 let final_size = gsd_file.file.metadata()?.len();
2487 assert_eq!(final_size, gsd_file.file_len);
2488 assert_eq!(final_size, initial_size);
2489
2490 gsd_file.sync_all().expect("write should succeed");
2491 let final_size = gsd_file.file.metadata()?.len();
2492 assert_eq!(final_size, gsd_file.file_len);
2493 assert_eq!(final_size, initial_size + 8);
2494
2495 Ok(())
2496 }
2497
2498 #[test]
2499 fn pending_index() -> anyhow::Result<()> {
2500 let tmp_dir = tempdir()?;
2501 let path = tmp_dir.path().join("test.gsd");
2502 let mut gsd_file = GsdFile::create(path.clone(), "a", "s", (1, 0))?;
2503
2504 gsd_file.write_scalars("a", [1])?;
2505 gsd_file.end_frame()?;
2506
2507 gsd_file.write_scalars("a", [1])?;
2508 gsd_file.write_scalars("b", [2])?;
2509 gsd_file.write_scalars("c", [3])?;
2510 gsd_file.write_scalars("d", [4])?;
2511 gsd_file.write_scalars("e", [5])?;
2512 gsd_file.write_scalars("f", [6])?;
2513 gsd_file.write_scalars("g", [7])?;
2514 gsd_file.write_scalars("h", [8])?;
2515
2516 assert_eq!(gsd_file.n_frames(), 0);
2517
2518 gsd_file.sync_all().expect("write should succeed");
2519
2520 assert!(gsd_file.find_chunk(0, "a").is_some());
2521 assert_eq!(gsd_file.n_frames(), 1);
2522
2523 assert!(gsd_file.find_chunk(1, "a").is_none());
2525 assert!(gsd_file.find_chunk(1, "b").is_none());
2526 assert!(gsd_file.find_chunk(1, "c").is_none());
2527 assert!(gsd_file.find_chunk(1, "d").is_none());
2528 assert!(gsd_file.find_chunk(1, "e").is_none());
2529 assert!(gsd_file.find_chunk(1, "f").is_none());
2530 assert!(gsd_file.find_chunk(1, "g").is_none());
2531 assert!(gsd_file.find_chunk(1, "h").is_none());
2532
2533 gsd_file.end_frame()?;
2534 assert_eq!(gsd_file.n_frames(), 1);
2535 gsd_file.sync_all()?;
2536 assert_eq!(gsd_file.n_frames(), 2);
2537
2538 assert!(gsd_file.find_chunk(1, "a").is_some());
2540 assert!(gsd_file.find_chunk(1, "b").is_some());
2541 assert!(gsd_file.find_chunk(1, "c").is_some());
2542 assert!(gsd_file.find_chunk(1, "d").is_some());
2543 assert!(gsd_file.find_chunk(1, "e").is_some());
2544 assert!(gsd_file.find_chunk(1, "f").is_some());
2545 assert!(gsd_file.find_chunk(1, "g").is_some());
2546 assert!(gsd_file.find_chunk(1, "h").is_some());
2547
2548 Ok(())
2549 }
2550
2551 #[expect(clippy::too_many_lines, reason = "There are many data types to test")]
2552 #[test]
2553 fn all_types() -> anyhow::Result<()> {
2554 let tmp_dir = tempdir()?;
2555 let path = tmp_dir.path().join("test.gsd");
2556 let mut gsd_file = GsdFile::create(path.clone(), "a", "s", (1, 0))?;
2557
2558 let u8_data = [1, 2, 3];
2559 let u16_data = [4, 5, 6];
2560 let u32_data = [7, 8, 9];
2561 let u64_data = [10, 11, 12];
2562 let i8_data = [-1, -2, -3];
2563 let i16_data = [-4, -5, -6];
2564 let i32_data = [-7, -8, -9];
2565 let i64_data = [-10, -11, -12];
2566 let f32_data = [13.0, 14.0, 15.0];
2567 let f64_data = [16.0, 17.0, 18.0];
2568 let string_data = "Test string.";
2569
2570 gsd_file.write_scalars("u8", u8_data)?;
2571 gsd_file.write_scalars("u16", u16_data)?;
2572 gsd_file.write_scalars("u32", u32_data)?;
2573 gsd_file.write_scalars("u64", u64_data)?;
2574 gsd_file.write_scalars("i8", i8_data)?;
2575 gsd_file.write_scalars("i16", i16_data)?;
2576 gsd_file.write_scalars("i32", i32_data)?;
2577 gsd_file.write_scalars("i64", i64_data)?;
2578 gsd_file.write_scalars("f32", f32_data)?;
2579 gsd_file.write_scalars("f64", f64_data)?;
2580 gsd_file.write_string("string", string_data)?;
2581 gsd_file.end_frame()?;
2582 drop(gsd_file);
2583
2584 let gsd_file = GsdFile::open(path.clone(), Mode::Read)?;
2585 assert_eq!(gsd_file.n_frames(), 1);
2586
2587 let u8_array = gsd_file.iter_scalars::<u8>(0, "u8")?;
2588 let u16_array = gsd_file.iter_scalars::<u16>(0, "u16")?;
2589 let u32_array = gsd_file.iter_scalars::<u32>(0, "u32")?;
2590 let u64_array = gsd_file.iter_scalars::<u64>(0, "u64")?;
2591 let i8_array = gsd_file.iter_scalars::<i8>(0, "i8")?;
2592 let i16_array = gsd_file.iter_scalars::<i16>(0, "i16")?;
2593 let i32_array = gsd_file.iter_scalars::<i32>(0, "i32")?;
2594 let i64_array = gsd_file.iter_scalars::<i64>(0, "i64")?;
2595 let f32_array = gsd_file.iter_scalars::<f32>(0, "f32")?;
2596 let f64_array = gsd_file.iter_scalars::<f64>(0, "f64")?;
2597 let string_array = gsd_file.read_string(0, "string")?;
2598
2599 itertools::assert_equal(u8_array, u8_data);
2600 itertools::assert_equal(u16_array, u16_data);
2601 itertools::assert_equal(u32_array, u32_data);
2602 itertools::assert_equal(u64_array, u64_data);
2603 itertools::assert_equal(i8_array, i8_data);
2604 itertools::assert_equal(i16_array, i16_data);
2605 itertools::assert_equal(i32_array, i32_data);
2606 itertools::assert_equal(i64_array, i64_data);
2607 itertools::assert_equal(f32_array, f32_data);
2608 itertools::assert_equal(f64_array, f64_data);
2609 assert_eq!(string_array, string_data);
2610
2611 assert_eq!(
2612 GsdFile::size_of(u8::gsd_data_type()).expect("data type should be valid"),
2613 size_of::<u8>()
2614 );
2615 assert_eq!(
2616 GsdFile::size_of(u16::gsd_data_type()).expect("data type should be valid"),
2617 size_of::<u16>()
2618 );
2619 assert_eq!(
2620 GsdFile::size_of(u32::gsd_data_type()).expect("data type should be valid"),
2621 size_of::<u32>()
2622 );
2623 assert_eq!(
2624 GsdFile::size_of(u64::gsd_data_type()).expect("data type should be valid"),
2625 size_of::<u64>()
2626 );
2627 assert_eq!(
2628 GsdFile::size_of(i8::gsd_data_type()).expect("data type should be valid"),
2629 size_of::<i8>()
2630 );
2631 assert_eq!(
2632 GsdFile::size_of(i16::gsd_data_type()).expect("data type should be valid"),
2633 size_of::<i16>()
2634 );
2635 assert_eq!(
2636 GsdFile::size_of(i32::gsd_data_type()).expect("data type should be valid"),
2637 size_of::<i32>()
2638 );
2639 assert_eq!(
2640 GsdFile::size_of(i64::gsd_data_type()).expect("data type should be valid"),
2641 size_of::<i64>()
2642 );
2643 assert_eq!(
2644 GsdFile::size_of(f32::gsd_data_type()).expect("data type should be valid"),
2645 size_of::<f32>()
2646 );
2647 assert_eq!(
2648 GsdFile::size_of(f64::gsd_data_type()).expect("data type should be valid"),
2649 size_of::<f64>()
2650 );
2651
2652 assert_eq!(
2653 gsd_file
2654 .find_chunk(0, "u8")
2655 .expect("u8 should be written above")
2656 .data_type(),
2657 Some(DataType::U8)
2658 );
2659 assert_eq!(
2660 gsd_file
2661 .find_chunk(0, "u16")
2662 .expect("u16 should be written above")
2663 .data_type(),
2664 Some(DataType::U16)
2665 );
2666 assert_eq!(
2667 gsd_file
2668 .find_chunk(0, "u32")
2669 .expect("u32 should be written above")
2670 .data_type(),
2671 Some(DataType::U32)
2672 );
2673 assert_eq!(
2674 gsd_file
2675 .find_chunk(0, "u64")
2676 .expect("c should be written above")
2677 .data_type(),
2678 Some(DataType::U64)
2679 );
2680 assert_eq!(
2681 gsd_file
2682 .find_chunk(0, "i8")
2683 .expect("i8 should be written above")
2684 .data_type(),
2685 Some(DataType::I8)
2686 );
2687 assert_eq!(
2688 gsd_file
2689 .find_chunk(0, "i16")
2690 .expect("i16 should be written above")
2691 .data_type(),
2692 Some(DataType::I16)
2693 );
2694 assert_eq!(
2695 gsd_file
2696 .find_chunk(0, "i32")
2697 .expect("i32 should be written above")
2698 .data_type(),
2699 Some(DataType::I32)
2700 );
2701 assert_eq!(
2702 gsd_file
2703 .find_chunk(0, "i64")
2704 .expect("i64 should be written above")
2705 .data_type(),
2706 Some(DataType::I64)
2707 );
2708 assert_eq!(
2709 gsd_file
2710 .find_chunk(0, "f32")
2711 .expect("f32 should be written above")
2712 .data_type(),
2713 Some(DataType::F32)
2714 );
2715 assert_eq!(
2716 gsd_file
2717 .find_chunk(0, "f64")
2718 .expect("f64 should be written above")
2719 .data_type(),
2720 Some(DataType::F64)
2721 );
2722 assert_eq!(
2723 gsd_file
2724 .find_chunk(0, "string")
2725 .expect("string should be written above")
2726 .data_type(),
2727 Some(DataType::String)
2728 );
2729
2730 Ok(())
2731 }
2732
2733 #[test]
2734 fn dimensions() -> anyhow::Result<()> {
2735 let tmp_dir = tempdir()?;
2736 let path = tmp_dir.path().join("test.gsd");
2737 let mut gsd_file = GsdFile::create(path.clone(), "a", "s", (1, 0))?;
2738
2739 let initial_size = gsd_file
2740 .file
2741 .metadata()
2742 .expect("metadata should be valid")
2743 .len();
2744
2745 gsd_file.write_scalars::<u64, _>("a", [])?;
2746 gsd_file.end_frame()?;
2747 gsd_file.write_scalars::<u64, _>("b", [1, 2, 3, 4, 5, 6])?;
2748
2749 gsd_file.write_arrays("c", [[1_u64, 2, 3], [4, 5, 6]])?;
2750 gsd_file.end_frame()?;
2751
2752 gsd_file.sync_all()?;
2753 let final_size = gsd_file.file.metadata()?.len();
2754 assert_eq!(final_size, gsd_file.file_len);
2755 assert_eq!(final_size, initial_size + (12 * size_of::<u64>()) as u64);
2756
2757 drop(gsd_file);
2758
2759 let gsd_file = GsdFile::open(path.clone(), Mode::Read)?;
2760 assert_eq!(gsd_file.n_frames(), 2);
2761
2762 let array_a = gsd_file.iter_scalars::<u64>(0, "a")?;
2763 assert_eq!(array_a.len(), 0);
2764
2765 let array_b = gsd_file.iter_scalars::<u64>(1, "b")?;
2766 assert_eq!(array_b.len(), 6);
2767 itertools::assert_equal(array_b, [1, 2, 3, 4, 5, 6]);
2768
2769 let array_b = gsd_file.iter_arrays::<u64, 1>(1, "b")?;
2771 assert_eq!(array_b.len(), 6);
2772 itertools::assert_equal(array_b, [[1], [2], [3], [4], [5], [6]]);
2773
2774 let array_c = gsd_file.iter_arrays::<u64, 3>(1, "c")?;
2775 itertools::assert_equal(array_c, [[1, 2, 3], [4, 5, 6]]);
2776
2777 let entry_a = gsd_file
2778 .find_chunk(0, "a")
2779 .expect("a should be written above");
2780 assert_eq!(entry_a.frame(), 0);
2781 assert_eq!(entry_a.rows(), 0);
2782 assert_eq!(entry_a.columns(), 1);
2783 assert_eq!(entry_a.data_type(), Some(DataType::U64));
2784
2785 let entry_b = gsd_file
2786 .find_chunk(1, "b")
2787 .expect("a should be written above");
2788 assert_eq!(entry_b.frame(), 1);
2789 assert_eq!(entry_b.rows(), 6);
2790 assert_eq!(entry_b.columns(), 1);
2791 assert_eq!(entry_b.data_type(), Some(DataType::U64));
2792
2793 let entry_c = gsd_file
2794 .find_chunk(1, "c")
2795 .expect("c should be written above");
2796 assert_eq!(entry_c.frame(), 1);
2797 assert_eq!(entry_c.rows(), 2);
2798 assert_eq!(entry_c.columns(), 3);
2799 assert_eq!(entry_c.data_type(), Some(DataType::U64));
2800
2801 Ok(())
2802 }
2803
2804 #[test]
2805 fn invalid_writes() -> anyhow::Result<()> {
2806 let tmp_dir = tempdir()?;
2807 let path = tmp_dir.path().join("test.gsd");
2808 let mut gsd_file = GsdFile::create(path.clone(), "a", "s", (1, 0))?;
2809
2810 let result = gsd_file.write_arrays::<u64, _, 0>("a", []);
2811 assert!(matches!(
2812 result,
2813 Err(WriteError::Encode(_, _, EncodeError::InvalidColumns(_)))
2814 ));
2815
2816 let mut gsd_file = GsdFile::open(path.clone(), Mode::Read)?;
2817
2818 let result = gsd_file.write_scalars::<u64, _>("a", []);
2819 assert!(matches!(
2820 result,
2821 Err(WriteError::Encode(_, _, EncodeError::NotWritable))
2822 ));
2823
2824 let result = gsd_file.end_frame();
2825 assert!(matches!(result, Err(EncodeError::NotWritable)));
2826
2827 let result = gsd_file.sync_all();
2828 assert!(matches!(result, Err(SyncError::NotWritable)));
2829
2830 Ok(())
2831 }
2832
2833 #[test]
2834 fn duplicate_chunk_name() -> anyhow::Result<()> {
2835 let tmp_dir = tempdir()?;
2836 let path = tmp_dir.path().join("test.gsd");
2837 let mut gsd_file = GsdFile::create(path.clone(), "a", "s", (1, 0))?;
2838
2839 gsd_file.write_scalars("a", [1])?;
2840 let result = gsd_file.write_scalars("a", [1, 2]);
2841 assert!(matches!(
2842 result,
2843 Err(WriteError::Encode(
2844 _,
2845 _,
2846 EncodeError::DuplicateChunkName(_, _)
2847 ))
2848 ));
2849
2850 Ok(())
2851 }
2852
2853 #[test]
2854 fn read_invalid_reads() -> anyhow::Result<()> {
2855 let tmp_dir = tempdir()?;
2856 let path = tmp_dir.path().join("test.gsd");
2857 let mut gsd_file = GsdFile::create(path.clone(), "a", "s", (1, 0))?;
2858
2859 gsd_file.write_scalars("a", [1_u64])?;
2860 gsd_file.write_arrays("b", [[1_u64, 2], [3, 4]])?;
2861 gsd_file.end_frame()?;
2862 gsd_file.sync_all()?;
2863
2864 let result = gsd_file.iter_scalars::<u32>(0, "a");
2865 assert!(matches!(
2866 result,
2867 Err(ReadError::Decode(_, _, DecodeError::InvalidType(_, _)))
2868 ));
2869
2870 let result = gsd_file.iter_scalars::<u64>(0, "b");
2871 assert!(matches!(
2872 result,
2873 Err(ReadError::Decode(_, _, DecodeError::InvalidColumns(1, 2)))
2874 ));
2875
2876 let result = gsd_file.iter_arrays::<u64, 2>(0, "a");
2877 assert!(matches!(
2878 result,
2879 Err(ReadError::Decode(_, _, DecodeError::InvalidColumns(2, 1)))
2880 ));
2881
2882 let result = gsd_file.iter_arrays::<u64, 8>(0, "b");
2883 assert!(matches!(
2884 result,
2885 Err(ReadError::Decode(_, _, DecodeError::InvalidColumns(8, 2)))
2886 ));
2887
2888 let result = gsd_file.iter_scalars::<u32>(1, "a");
2889 assert!(matches!(result, Err(ReadError::ChunkNotFound(_, _))));
2890
2891 let result = gsd_file.iter_scalars::<u32>(0, "q");
2892 assert!(matches!(result, Err(ReadError::ChunkNotFound(_, _))));
2893
2894 Ok(())
2895 }
2896
2897 #[test]
2898 fn chunk_name_limit() -> anyhow::Result<()> {
2899 let tmp_dir = tempdir()?;
2900 let path = tmp_dir.path().join("test.gsd");
2901 let mut gsd_file = GsdFile::create(path.clone(), "a", "s", (1, 0))?;
2902
2903 for i in 0..u16::MAX {
2904 gsd_file.write_scalars::<u64, _>(&format!("{i:x}"), [])?;
2905 }
2906
2907 let i = u16::MAX;
2908 let result = gsd_file.write_scalars::<u64, _>(&format!("{i:x}"), []);
2909 assert!(matches!(
2910 result,
2911 Err(WriteError::Encode(_, _, EncodeError::NameListOverflow))
2912 ));
2913
2914 drop(gsd_file);
2915
2916 let gsd_file = GsdFile::open(path.clone(), Mode::Read)?;
2917
2918 assert_eq!(gsd_file.name_id().len(), u16::MAX as usize);
2919 for i in 0..u16::MAX {
2920 assert!(gsd_file.name_id().contains_key(&format!("{i:x}")));
2921 }
2922
2923 let size = gsd_file.file.metadata()?.len();
2924 assert_eq!(size, gsd_file.file_len);
2925
2926 Ok(())
2927 }
2928
2929 #[test]
2930 fn expand_index_multi() -> anyhow::Result<()> {
2931 const N_ENTRIES: u16 = 1024;
2932
2933 let tmp_dir = tempdir()?;
2934 let path = tmp_dir.path().join("test.gsd");
2935 let mut gsd_file = GsdFile::create(path.clone(), "a", "s", (1, 0))?;
2936
2937 for i in 0..N_ENTRIES {
2938 gsd_file.write_scalars::<u16, _>(&format!("{i:x}"), [i])?;
2939 }
2940 gsd_file.end_frame()?;
2941 gsd_file.sync_all()?;
2942
2943 drop(gsd_file);
2944
2945 let gsd_file = GsdFile::open(path.clone(), Mode::Read)?;
2946
2947 assert_eq!(gsd_file.index.n, u64::from(N_ENTRIES));
2948 for i in 0..N_ENTRIES {
2949 let array = gsd_file.iter_scalars::<u16>(0, &format!("{i:x}"))?;
2950 itertools::assert_equal(array, [i]);
2951 }
2952
2953 Ok(())
2954 }
2955
2956 #[test]
2957 fn string() -> anyhow::Result<()> {
2958 let tmp_dir = tempdir()?;
2959 let path = tmp_dir.path().join("test.gsd");
2960 let mut gsd_file = GsdFile::create(path.clone(), "a", "s", (1, 0))?;
2961
2962 gsd_file.write_string("a", "this is a string")?;
2963 gsd_file.end_frame()?;
2964 gsd_file.write_scalars::<u8, _>("b", [0, 159, 146, 150])?;
2965 gsd_file.end_frame()?;
2966 gsd_file.sync_all()?;
2967
2968 let a = gsd_file.read_string(0, "a")?;
2969 assert_eq!(a, "this is a string");
2970
2971 let b = gsd_file.read_string(1, "b");
2972 assert!(matches!(
2973 b,
2974 Err(ReadError::Decode(_, _, DecodeError::InvalidType(11, 1)))
2975 ));
2976
2977 Ok(())
2978 }
2979}