hdf5-pure 0.45.0

Pure-Rust HDF5 library: read, write, and edit files in place (WASM-compatible, no C dependencies)
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
//! Pure-Rust HDF5 file reading, writing, and in-place editing library.
//!
//! `hdf5-pure` is a zero-C-dependency crate for creating, reading, and editing
//! HDF5 files. It is WASM-compatible and supports `no_std` environments with
//! `alloc`.
//!
//! # Design goals
//!
//! - **Zero C dependencies.** The HDF5 binary format is implemented directly in
//!   Rust. There is no `libhdf5`, no `build.rs` linking step, and no system
//!   package to install.
//! - **Portable.** The crate compiles to `wasm32-unknown-unknown` and to bare-metal
//!   `no_std` targets (with `alloc`). The high-level filesystem API is `std`-gated.
//!   The in-memory parsing and serialization machinery is not.
//! - **Interoperable.** Files this crate writes are read by the reference HDF5 C
//!   library, h5py, and MATLAB, and vice versa. Interop is verified by crosscheck
//!   tests that compare byte-for-byte against fixtures produced by those tools.
//! - **Faithful or nothing.** Every operation that cannot reproduce data exactly
//!   fails with a named error. See [Fidelity](#fidelity-faithful-or-nothing) below.
//!
//! # Fidelity: faithful or nothing
//!
//! Editing ([`File::open_rw`]) and repacking ([`repack`](fn@repack))
//! each reject what they cannot reproduce exactly. A `File::open_rw` session that cannot reproduce
//! an object faithfully fails with [`Error::EditUnsupported`]. `repack` fails with
//! [`Error::RepackUnsupported`], which identifies the object, before it writes a byte to the
//! destination.
//!
//! # Writing files
//!
//! ```rust
//! use hdf5_pure::{AttrValue, FileBuilder};
//!
//! let mut builder = FileBuilder::new();
//! builder.create_dataset("data")
//!     .with_f64_data(&[1.0, 2.0, 3.0])
//!     .with_shape(&[3])
//!     .set_attr("unit", AttrValue::String("m/s".into()));
//! builder.set_attr("version", AttrValue::I64(2));
//! let bytes = builder.finish()?;
//! # assert!(hdf5_pure::is_hdf5_bytes(&bytes));
//! # Ok::<(), hdf5_pure::Error>(())
//! ```
//!
//! [`FileBuilder::finish`] serializes the file into a `Vec<u8>`, for a WASM build or a caller
//! that sends the bytes on, and [`FileBuilder::write`] serializes the same file to a path.
//!
//! # Reading files
//!
//! ```rust
//! # let dir = tempfile::tempdir()?;
//! # let path = dir.path().join("data.h5");
//! # let mut builder = hdf5_pure::FileBuilder::new();
//! # builder.create_dataset("data")
//! #     .with_f64_data(&[1.0, 2.0, 3.0])
//! #     .set_attr("unit", AttrValue::String("m/s".into()));
//! # builder.set_attr("version", AttrValue::I64(2));
//! # builder.write(&path)?;
//! use hdf5_pure::{AttrValue, File};
//!
//! let file = File::open(&path)?;
//! let dataset = file.dataset("data")?;
//!
//! assert_eq!(dataset.shape()?, vec![3]);
//! assert_eq!(dataset.read_f64()?, vec![1.0, 2.0, 3.0]);
//! assert_eq!(dataset.attrs()?["unit"], AttrValue::String("m/s".into()));
//! assert_eq!(file.root().attrs()?["version"], AttrValue::I64(2));
//! # Ok::<(), hdf5_pure::Error>(())
//! ```
//!
//! [`File::from_bytes`] reads the same file from a `Vec<u8>` the caller already holds.
//!
//! # Generic over the element type
//!
//! The typed `with_*_data` / `read_*` methods have generic counterparts bounded
//! by [`H5Element`]: [`DatasetBuilder::with_data`] writes a flat slice of any
//! supported scalar and [`Dataset::read`] reads one back, so you can write code
//! generic over the stored type.
//!
//! ```rust
//! use hdf5_pure::{File, FileBuilder, H5Element};
//!
//! fn store<T: H5Element>(fb: &mut FileBuilder, name: &str, values: &[T]) {
//!     fb.create_dataset(name).with_data(values);
//! }
//!
//! let mut fb = FileBuilder::new();
//! store(&mut fb, "counts", &[1u32, 2, 3]);
//! let file = File::from_bytes(fb.finish()?)?;
//! let counts: Vec<u32> = file.dataset("counts")?.read()?;
//! assert_eq!(counts, vec![1, 2, 3]);
//! # Ok::<(), hdf5_pure::Error>(())
//! ```
//!
//! # Editing files in place
//!
//! Open an existing file for reading **and** writing with [`File::open_rw`] and
//! reach every object by name through owned [`Dataset`] and [`Group`] handles
//! that add, delete, copy, or overwrite objects without rewriting the file from
//! scratch. New data and rebuilt object headers are appended at end-of-file
//! and the superblock is repointed last, so the cost is proportional to what
//! changes rather than to the file size. It edits files written by this crate,
//! the reference HDF5 C library, and h5py across all of their on-disk formats.
//! Memory follows the file, not the caller's choice of function: a latest-format
//! file with no userblock is edited without ever building a whole-file copy of it
//! (see [`MemoryStrategy`]).
//!
//! ```rust
//! # let dir = tempfile::tempdir()?;
//! # let path = dir.path().join("data.h5");
//! # let mut builder = hdf5_pure::FileBuilder::new();
//! # builder.create_dataset("data").with_f64_data(&[1.0, 2.0, 3.0]);
//! # builder.create_dataset("old").with_i32_data(&[0]);
//! # builder.write(&path)?;
//! use hdf5_pure::File;
//!
//! let file = File::open_rw(&path)?;
//! let root = file.root();
//! root.create_dataset("extra", |b| { b.with_f64_data(&[4.0, 5.0]); })?;
//! file.dataset("data")?.write(&[7.0, 8.0, 9.0])?; // H5Dwrite (overwrite)
//! root.delete("old")?;                            // H5Ldelete
//! file.commit()?;                                 // apply staged edits
//! # drop(root);
//! # drop(file);
//! # let reopened = File::open(&path)?;
//! # assert_eq!(reopened.root().datasets()?, vec!["data", "extra"]);
//! # assert_eq!(reopened.dataset("data")?.read_f64()?, vec![7.0, 8.0, 9.0]);
//! # Ok::<(), hdf5_pure::Error>(())
//! ```
//!
//! A value overwrite ([`Dataset::write`]) must match the on-disk datatype and
//! shape (it is a value write, not a reshape/retype); a same-length contiguous
//! overwrite is applied straight into the existing data block without rewriting
//! any header. Immediate, amortized-`O(1)` row appends use [`Dataset::append`]
//! and need no commit.
//!
//! # Streaming large files
//!
//! [`File::open`] reads the whole file into memory. To read a file too large to
//! buffer (for example a multi-gigabyte file on a 32-bit host, where it exceeds
//! the address space), use [`File::open_streaming`], which fetches metadata and
//! dataset chunks from the file on demand instead of buffering it whole. The
//! reading API is identical; only the backing store differs. Contiguous,
//! compact, and every chunked layout read the same way, as do both group forms
//! and attributes.
//!
//! ```rust
//! # let dir = tempfile::tempdir()?;
//! # let path = dir.path().join("signals.h5");
//! # let mut builder = hdf5_pure::FileBuilder::new();
//! # builder.create_dataset("signal").with_f64_data(&[1.0, 2.0, 3.0]);
//! # builder.write(&path)?;
//! use hdf5_pure::File;
//!
//! let file = File::open_streaming(&path)?;
//! let values = file.dataset("signal")?.read_f64()?;
//! # assert_eq!(values, vec![1.0, 2.0, 3.0]);
//! # Ok::<(), hdf5_pure::Error>(())
//! ```
//!
//! When the bytes are not a path at all — an object store addressed by range
//! request, a sandboxed guest handed byte ranges by its host — implement
//! [`Source`] over them and open with [`File::from_source`], which is the same
//! lazy backing store fed from somewhere else.
//!
//! # N-dimensional arrays (`ndarray` feature)
//!
//! With the `ndarray` feature, datasets can be written from and read back into
//! [`ndarray`] arrays of any rank, in row-major (C) order:
//!
//! ```rust
//! # #[cfg(feature = "ndarray")] {
//! use hdf5_pure::{File, FileBuilder};
//! use ndarray::{array, Array2};
//!
//! let a: Array2<f64> = array![[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]];
//! let mut fb = FileBuilder::new();
//! fb.create_dataset("m").with_ndarray(&a);
//! let bytes = fb.finish()?;
//!
//! let file = File::from_bytes(bytes)?;
//! let back: Array2<f64> = file.dataset("m")?.read_array()?;
//! assert_eq!(a, back);
//! # }
//! # Ok::<(), hdf5_pure::Error>(())
//! ```
//!
//! # Cargo features
//!
//! The crate is split into Cargo features, so a build compiles the parts it uses. The defaults
//! cover filesystem I/O with the high-level reader and writer, and the optional features add
//! MATLAB `.mat` support, a second compression backend, N-dimensional array I/O, and data
//! provenance. The [Installation section of the README][readme-install] shows how to declare them
//! in `Cargo.toml`.
//!
//! | Feature | Default | Pulls in | Implies | Description |
//! |---|---|---|---|---|
//! | `std` | yes | nothing | nothing | file I/O and the high-level reader and writer API |
//! | `checksum` | yes | nothing | nothing | the Jenkins hash that validates checksummed metadata |
//! | `deflate` | yes | `flate2`, Rust backend | nothing | deflate (zlib) compression, pure-Rust backend |
//! | `serde` | no | `serde` | `std` | serialization of MATLAB v7.3 `.mat` files through serde |
//! | `fast-deflate` | no | `flate2/zlib-ng` | nothing | the zlib-ng backend for deflate |
//! | `ndarray` | no | the `ndarray` crate | `std` | N-dimensional array I/O through the [`ndarray`](https://docs.rs/ndarray) crate |
//! | `num-complex` | no | `num-complex` | `serde` | [`mat::ComplexElement`] for `num_complex::Complex<T>`, for the bulk complex-array helpers |
//! | `provenance` | no | `sha2` | nothing | SHA-256 data provenance tracking |
//! | `zfp` | no | nothing | nothing | ZFP fixed-rate compression (HDF5 filter 32013), `f32`, `f64`, `i32` and `i64` in ranks 1 to 4 |
//! | `heap-baseline` | no | nothing | nothing | maintainer only: check the recorded allocation figures (see below) |
//!
//! The default feature set is `std`, `checksum`, and `deflate`. `serde` and `ndarray` both imply
//! `std`, since they build on the [`File`], [`Group`], and [`Dataset`] reader APIs and the
//! [`FileBuilder`] writer, which require the standard library. Enabling either one enables `std`.
//!
//! ## `std`
//!
//! Enables the standard library, and with it the whole high-level reader and writer surface:
//! [`File`], [`FileBuilder`], [`Group`], [`Dataset`], [`File::open_rw`],
//! [`File::open_swmr_writer`], [`Dataset::append`], [`repack`](fn@repack), the [`mat`] module, and
//! the in-memory, filesystem, and caller-supplied-source entry points ([`FileBuilder::finish`],
//! [`File::from_bytes`], [`File::open`], [`File::open_streaming`], [`File::from_source`],
//! [`FileBuilder::write`]). The whole high-level API is `std`-gated: with `std` disabled the crate
//! is `no_std` and exposes the lower-level datatype and builder primitives alone. `std` is
//! available on `wasm32-unknown-unknown`, so a WASM build keeps it, which [Platform
//! support](#platform-support) covers.
//!
//! ## `checksum`
//!
//! Enables the Jenkins lookup3 hash used to validate and emit the checksums HDF5 puts on its
//! metadata: version 2 and later object headers, the superblock, version 2 B-tree nodes, fractal
//! heaps, and the Extensible-Array and Fixed-Array chunk indexes. It has no extra dependency. Keep
//! it enabled for broad compatibility with the files the reference HDF5 C library and h5py
//! produce. Keep it on a WASM target too, beside `std`.
//!
//! ## `deflate`
//!
//! Enables Deflate (zlib) compression and decompression through a pure-Rust backend (`flate2` with
//! its `rust_backend`). This is what backs [`DatasetBuilder::with_deflate`]. See the
//! [compression guide](crate::_guide::compression) for usage.
//!
//! ## `serde`
//!
//! Adds serde-based (de)serialization of MATLAB v7.3 `.mat` files through the [`mat`] module
//! ([`mat::to_file`], [`mat::to_writer`], [`mat::from_file`], [`Matrix`](crate::mat::Matrix),
//! [`Complex32`](crate::mat::Complex32), [`Complex64`](crate::mat::Complex64)). It pulls in the
//! `serde` dependency and implies `std`.
//!
//! Only the serde-driven entry points are gated. The mid-level [`mat::MatBuilder`], including
//! [`write_blocks`](crate::mat::MatBuilder::write_blocks),
//! [`finish_to`](crate::mat::MatBuilder::finish_to) and [`write`](crate::mat::MatBuilder::write),
//! needs `std` and not `serde`, so a writer that builds its `.mat` explicitly does not pay for the
//! dependency.
//!
//! The `matlab_fixtures` example requires this feature and runs with `cargo run --example
//! matlab_fixtures --features serde`. The `mat_streaming` example needs only the defaults: `cargo
//! run --example mat_streaming`.
//!
//! ## `fast-deflate`
//!
//! Switches the deflate backend to zlib-ng through `flate2/zlib-ng`, which compresses faster than
//! the pure-Rust backend. It complements `deflate` and leaves the deflate API as it is. zlib-ng is
//! a native dependency, so this feature is for native builds, and the pure-Rust `deflate` backend
//! is what a WASM target compiles.
//!
//! ## `ndarray`
//!
//! Adds ergonomic N-dimensional array I/O via the [`ndarray`](https://docs.rs/ndarray) crate:
//! [`DatasetBuilder::with_ndarray`] to write and [`Dataset::read_array`] /
//! [`Dataset::read_array_dyn`] to read. Shape and datatype come from the array, and data is stored
//! row-major (C order). It pulls in the `ndarray` crate and implies `std`. See the
//! [ndarray guide](crate::_guide::ndarray).
//!
//! The `ndarray_io` example requires this feature and runs with `cargo run --example ndarray_io
//! --features ndarray`.
//!
//! ## `num-complex`
//!
//! Implements [`mat::ComplexElement`] for `num_complex::Complex<T>`, so a slice of the de-facto
//! standard Rust complex type can be handed to the bulk array helpers ([`mat::complex::i16_array`]
//! and friends) without a conversion pass. It implies `serde`. See
//! [large complex arrays](crate::mat#large-complex-arrays).
//!
//! `ComplexElement` is `unsafe` and asserts a memory layout, and the orphan rule allows an
//! implementation only from a crate that owns one of the two types, so these implementations ship
//! here. A complex type of the caller's own needs no feature: implement the trait for it directly.
//!
//! ## `provenance`
//!
//! Adds SHA-256 data provenance tracking, pulling in `sha2`.
//! [`DatasetBuilder::with_provenance(creator, timestamp, source)`](DatasetBuilder::with_provenance)
//! stores the SHA-256 digest of the data beside a dataset, together with the creator, the
//! timestamp and, where the caller passes one, the source, and [`Dataset::verify_provenance`]
//! recomputes the digest and returns a [`VerifyResult`]. The attributes have conventional names:
//! `_provenance_sha256`, `_provenance_creator`, `_provenance_timestamp` and `_provenance_source`.
//! `verify_provenance` and [`VerifyResult`] require `std` as well.
//!
//! ```rust
//! # #[cfg(feature = "provenance")] {
//! use hdf5_pure::{File, FileBuilder};
//!
//! let mut builder = FileBuilder::new();
//! builder.create_dataset("measurements")
//!     .with_f64_data(&[1.0, 2.0, 3.0])
//!     .with_provenance("acquisition-rig", "2026-06-16T00:00:00Z", None);
//! let bytes = builder.finish().unwrap();
//!
//! let file = File::from_bytes(bytes).unwrap();
//! let result = file.dataset("measurements").unwrap().verify_provenance().unwrap();
//! # assert_eq!(result, hdf5_pure::VerifyResult::Ok);
//! # }
//! ```
//!
//! ## `zfp`
//!
//! Enables a pure-Rust fixed-rate port of the LLNL/zfp codec, registered as HDF5 filter ID 32013,
//! exposed through [`DatasetBuilder::with_zfp(rate)`](DatasetBuilder::with_zfp). It supports `f32`,
//! `f64`, `i32`, and `i64` datasets in ranks 1D through 4D in fixed-rate mode. Files written with
//! it are byte-for-byte interoperable with the reference H5Z-ZFP plugin (`h5py` + `hdf5plugin`). It
//! has no extra crate dependency. See the [compression guide](crate::_guide::compression).
//!
//! ```rust
//! # #[cfg(feature = "zfp")] {
//! # let (ny, nx) = (16usize, 16usize);
//! # let data: Vec<f32> = (0..ny * nx).map(|i| i as f32).collect();
//! let mut builder = hdf5_pure::FileBuilder::new();
//! builder.create_dataset("temperature")
//!     .with_f32_data(&data)
//!     .with_shape(&[ny as u64, nx as u64])
//!     .with_chunks(&[ny as u64, nx as u64])
//!     .with_zfp(16.0);  // 16 bits per value
//! # let bytes = builder.finish().unwrap();
//! # let file = hdf5_pure::File::from_bytes(bytes).unwrap();
//! # assert_eq!(file.dataset("temperature").unwrap().shape().unwrap(), vec![16, 16]);
//! # }
//! ```
//!
//! ## `heap-baseline`
//!
//! A test-only, maintainer feature. It enables `tests/allocation_baseline.rs`, which checks the
//! crate's exact allocation counts and byte totals for one write-then-read cycle against the
//! figures committed under `tests/baselines/`. It pulls in nothing (the heap profiler it uses,
//! [`heapscope`](https://crates.io/crates/heapscope), is an unconditional dev-dependency), it is
//! not a run-time dependency, and end users do not need it.
//!
//! The figures it checks belong to one target, one toolchain and one feature set, so the test
//! compiles only under the crate's default features and CI runs it in a single pinned job. The
//! bounds that hold everywhere are in `tests/allocation_bounds.rs` and need no feature: a windowed
//! read allocates on the order of its window, and a chunked read costs a constant per chunk. On
//! x86_64, both need the frame pointers `.cargo/config.toml` sets.
//!
//! # Platform support
//!
//! The crate builds for `wasm32-unknown-unknown` with no C dependencies. `std` is available on
//! that target and the high-level API is `std`-gated, so a WASM build keeps the default features,
//! which include `std`. Turning them off compiles [`File`] and [`FileBuilder`] away. Add the
//! target and build:
//!
//! ```console
//! $ rustup target add wasm32-unknown-unknown
//! $ cargo build --target wasm32-unknown-unknown
//! ```
//!
//! In the browser the in-memory entry points are the ones to use, [`FileBuilder::finish`], which
//! returns a `Vec<u8>`, and [`File::from_bytes`]. The path-based entry points compile, and a
//! browser gives them no filesystem to reach at runtime.
//!
//! For bare-metal `no_std` (for example `thumbv7em-none-eabi`), turn the default features off and
//! keep `checksum`:
//!
//! ```toml
//! [dependencies]
//! hdf5-pure = { version = "0.44", default-features = false, features = ["checksum"] }
//! ```
//!
//! The crate then compiles as `#![no_std]` with `alloc` alone, and the `std`-gated [`File`] and
//! [`FileBuilder`] API is absent: a `no_std` build exposes the lower-level primitives, and
//! building or reading a whole file needs `std`. `fast-deflate` uses the native zlib-ng backend
//! and is for native builds, and the pure-Rust `deflate` backend is what a WASM target compiles.
//! See the [portability guide](crate::_guide::portability) for the full per-target breakdown.
//!
//! # Write paths
//!
//! Three paths put bytes on disk, with different cost models:
//!
//! | Path | Entry point | What it writes |
//! |---|---|---|
//! | **Whole-file writer** | [`FileBuilder`] | Serializes a brand-new file from scratch |
//! | **In-place editor** | [`File::open_rw`] | Appends new bytes to the existing file and patches a small, fixed set of locations |
//! | **Repack** | [`repack`](fn@repack) | Reads a source file and writes a fresh, compact copy to a separate destination through `FileBuilder` |
//!
//! `File::open_rw` is an **append-and-patch** editor. It reads and patches a latest-format file
//! with no userblock where it lies, through positioned I/O, and mirrors any other file whole in
//! memory (`O(file size)`), which is a statement about *memory*, not about what gets written
//! back. An immediate `Dataset::append` writes the new chunks, fills Extensible-Array index
//! slots, and publishes the grown dataspace dimension last under `fsync` barriers, so each append
//! is durable and crash-atomic before the call returns (`SyncPolicy::OnClose` keeps the order and
//! drops the barriers, leaving durability to `File::sync`). Every other edit is staged and applied
//! by one `commit()`: new data and new object headers are appended at the end of the file (or
//! placed into space freed by earlier commits), a rewritten header for each touched group and its
//! ancestors up to the root is appended, and the superblock is repointed at the new root **last**,
//! as the single crash-atomic commit point. A same-length value overwrite patches the existing
//! bytes where they lie. Space freed by deletions and relocations goes to a session free list for
//! reuse. A freed run reaching the end of the file is truncated, and on a file that persists its
//! free space the free list is instead serialized into on-disk free-space managers that survive
//! reopen.
//!
//! **Nothing re-serializes the file on commit.** The cost of a commit is proportional to the edit,
//! not to the file size: a staged append re-encodes at most the trailing partial chunk and carries
//! every other kept chunk into the rebuilt index by metadata alone, and no existing object moves
//! except the object headers on the edited path. A failed or interrupted commit leaves the file
//! valid: structural changes become visible only at the superblock repoint, so a crash before it
//! leaves the old object tree in place. (Same-length value overwrites patch live data blocks
//! directly and are the one staged edit outside that gate.)
//!
//! The bounded backing is the same engine minus the mirror: reads are positioned I/O through
//! bounded caches (the read-write sibling of [`File::open_streaming`]),
//! `Dataset::append` reads and patches only the metadata windows it touches with the same
//! crash-atomicity (and under the same `SyncPolicy`), and `close()` rewrites the on-disk free-space
//! managers of a file that persists them - including the per-page-type managers of a paged file,
//! whose appends stay page-homogeneous. The staged edit surface works here too: it builds what the
//! edit needs, not a copy of the file. What separates the two backings is which files they accept -
//! the bounded one requires a latest-format file with 8-byte offsets and no userblock, and
//! `open_rw` mirrors any other file.
//!
//! The SWMR writer is a restriction of the same immediate append engine - unfiltered, chunk-aligned
//! appends only - chosen so a concurrent reader never observes a torn view.
//!
//! `repack` is the one operation that rewrites a file from scratch, and it never does so in place:
//! it reads every surviving object and writes a fresh, compact copy at a separate destination path,
//! rejecting (`Error::RepackUnsupported`) anything it cannot reproduce faithfully.
//!
//! # On-disk format coverage
//!
//! The reader and editor handle the formats the reference C library and h5py
//! produce in the wild:
//!
//! - **Superblocks** version 0, 1, 2, and 3.
//! - **Object headers** version 1 (with continuation blocks) and version 2,
//!   including multi-chunk headers.
//! - **Storage layouts** - contiguous, compact, and chunked.
//! - **Chunk indexes** - B-tree v1, single chunk, implicit, fixed array (including
//!   the paged data-block layout), and extensible array (which also backs SWMR
//!   append and the in-place append paths: `Dataset::append_staged` and the
//!   owned-handle `File::open_rw` + `Dataset::append`).
//! - **Groups** - both the old symbol-table form (v0/v1) and the modern
//!   compact-link and dense (fractal-heap + v2 B-tree) forms.
//! - **Datatypes** - fixed-point, floating-point, string (fixed and
//!   variable-length), bit-field, opaque, compound, enumeration, array, and
//!   reference classes.
//!
//! The crate writes one modern format by default (the HDF5 1.10 version-3 superblock with
//! latest-format object headers), and [`FileBuilder::with_libver_bounds`] takes it back to the
//! 1.8 format at the oldest ([`LibVer::WRITER_OLDEST`]), so its output stays compact and
//! consistent while its reader remains broad.
//!
//! # Limitations
//!
//! The crate reads and writes a broad, interoperable subset of the HDF5 format. Where it cannot
//! handle something, it returns a typed error, so every gap below is a deliberate, well-messaged
//! rejection and never a silent misread.
//!
//! The rejections fall into two kinds:
//!
//! - [Deliberately unsupported](#deliberately-unsupported): by-design constraints, and guards
//!   against file formats outside the range the crate models. These are not planned to change.
//! - [Planned support](#planned-support): what the crate rejects for now, each entry tracked by a
//!   GitHub issue. These error messages read `... not supported yet` or `... cannot be ... yet`.
//!
//! Malformed-file errors (a truncated or garbled header, an address that exceeds the platform's
//! pointer width) and API-contract errors (deleting or copying the root group, conflicting edits
//! in one commit) are ordinary runtime errors and not capability limits, so this catalog leaves
//! them out.
//!
//! ## Deliberately unsupported
//!
//! ### Reading non-modeled formats
//!
//! | Rejected | Error | Reason |
//! |---|---|---|
//! | a superblock version above 3 | [`FormatError::UnsupportedVersion`] | versions 0 to 3 are read, and the released format defines no higher one |
//! | an unrecognized object-header message flagged must-understand | [`FormatError::UnsupportedMessage`] | the format requires a reader that does not recognize a must-understand message to reject the file |
//! | a File Space Info message version other than 1 | [`FormatError::UnsupportedFileSpaceInfoVersion`] | version 1 is the only one defined for the layouts this crate writes and reads |
//!
//! These guard against files outside the format-version range the crate models, and they are not
//! features to add.
//!
//! ### Numeric element width
//!
//! | Rejected | Error | Reason |
//! |---|---|---|
//! | an integer or float element wider than 8 bytes | [`FormatError::NumericElementTooWide`] | the typed numeric readers model an element as a 64-bit word taken from its leading eight bytes, and which part of a wider element that is depends on its byte order ([#361](https://github.com/CramBL/hdf5-pure/issues/361)) |
//!
//! The rejection keeps a partial value from passing for a whole one: a 9-byte integer holding 2^64
//! would otherwise read back as `0`, and big-endian it would read back as 2^56, since the bytes
//! kept are the most significant ones. It covers datasets and attributes alike, and an attribute
//! it rejects is omitted from [`attrs`](Dataset::attrs) while
//! [`attr_datatypes`](Dataset::attr_datatypes) still reports its width.
//! [`read_raw`](Dataset::read_raw) and [`read_u8`](Dataset::read_u8) /
//! [`read_i8`](Dataset::read_i8) hand back the bytes either way. Decoding these widths is a
//! separate feature: it means 128-bit variants throughout the [`AttrValue`] and `read_*` surfaces.
//!
//! ### Compression
//!
//! | Rejected | Error | Reason |
//! |---|---|---|
//! | a filter whose backend is not compiled in | [`FormatError::UnsupportedFilter`] | enable the `deflate` or `zfp` Cargo feature, which [Cargo features](#cargo-features) covers |
//! | ZFP outside fixed-rate mode, ranks 1 to 4, and the datatypes `f32`, `f64`, `i32` and `i64` | [`FormatError::UnsupportedZfp`] | the supported scope of the bundled ZFP codec, which the [compression guide](crate::_guide::compression) covers |
//!
//! ### Producer-backed datasets
//!
//! A dataset staged with [`MatBuilder::write_blocks`](crate::mat::MatBuilder::write_blocks) is
//! always stored uncompressed, and requesting one on a builder configured for deflate is rejected
//! with
//! [`MatError::CompressionUnsupportedForBlocks`](crate::mat::MatError::CompressionUnsupportedForBlocks).
//! The writer places every object before it emits a byte, so it needs the data region's exact size
//! up front: unfiltered that is pure geometry, and compressed it is knowable only by compressing,
//! which buffers the data this path exists to avoid buffering. Supporting it takes either a
//! two-pass producer contract (compress to measure, then compress again to emit, which requires a
//! deterministic producer) or a spill file, so it is a separate design.
//!
//! A producer that fails partway leaves a partial file on the sink, which is inherent to a
//! non-seekable destination. Write to a temporary path and rename on success where the result has
//! to be all-or-nothing.
//!
//! ### External data files
//!
//! A dataset created with `H5Pset_external` keeps its elements in one or more files beside the
//! HDF5 file. Its layout message records a contiguous layout with an undefined data address,
//! byte-for-byte what a never-written dataset records, and a separate header message lists the
//! files. Reading one is rejected with [`FormatError::UnsupportedExternalStorage`], since the
//! alternative is returning the fill value for data that exists elsewhere, and
//! [`repack`](fn@repack) rejects it, since the alternative is a copy holding none of it
//! ([#331](https://github.com/CramBL/hdf5-pure/issues/331),
//! [#293](https://github.com/CramBL/hdf5-pure/issues/293)).
//!
//! Writing is rejected for the same reason, with [`Error::EditUnsupported`]: [`Dataset::write`],
//! [`write_staged`](Dataset::write_staged), and [`append_staged`](Dataset::append_staged) would
//! otherwise take the address-less layout for never-allocated storage, append the new elements to
//! the HDF5 file and point the layout at them, leaving the file disagreeing with the external
//! files about where its data lives. Delete the dataset and create it again in the same commit to
//! replace it.
//!
//! Copying is rejected too, by name: [`File::copy`] and [`File::copy_from`] reproduce a dataset
//! whose storage was never allocated as the empty storage it is, and an externally stored dataset
//! carries that same address-less layout over data that exists, so a copy that treated the two
//! alike would report success having written the schema and none of the elements
//! ([#336](https://github.com/CramBL/hdf5-pure/issues/336)).
//!
//! What still works is reading the metadata, the shape, the datatype, and the address-less layout,
//! which is the evidence a caller needs, together with setting and removing attributes. Following
//! the external files is a separate feature: it means resolving each file the message lists
//! against the reading process's own filesystem, which is outside what a self-contained HDF5 file
//! describes.
//!
//! ### Repack faithfulness
//!
//! [`repack`](fn@repack) rewrites a file and rejects lossy filter re-encoding (lossy float
//! scale-offset, ZFP), since re-compressing lossy data would change the values. Lossless integer
//! scale-offset is the one pipeline it re-encodes faithfully, and the dataset's fill value comes
//! with it, in whichever of the two forms the source's filter recorded. Where it can, repack
//! copies already-compressed chunks verbatim, which preserves a lossy filter byte-exact.
//!
//! ### SWMR
//!
//! SWMR append requires a latest-format file (a v3 superblock) with no userblock, which mirrors
//! the HDF5 SWMR model, defined for the latest format alone. The C library rejects an older
//! superblock for SWMR writing too, and neither library reads the SWMR-write flag back on one.
//!
//! ### In-place editing
//!
//! In-place editing operates on files with 8-byte offsets and lengths, which is what the writer
//! emits and what modern files use. Other offset and length widths are not editable in place.
//!
//! ### Bounded-memory read-write
//!
//! A file [`File::open_rw`] edits [bounded](crate::_guide::editing#bounded-memory-appends) offers
//! the same edit surface as one it mirrors: reads, immediate [`Dataset::append`], and the staged
//! surface ([`write`](Dataset::write), attribute edits, `create_*` and `delete`,
//! [`copy`](File::copy), [`commit`](File::commit),
//! [`space_accounting`](File::space_accounting)), with a commit whose resident memory the edit
//! bounds and the file size does not ([`File::copy`] excepted: copying an object reads the whole
//! of it into memory). The bounded backing needs a latest-format file with 8-byte offsets and no
//! userblock, and any other file is mirrored. Its reads have the [streaming backend's
//! capabilities](crate::_guide::streaming). It grows a file that persists its free space,
//! including a genuine paged file (`H5F_FSPACE_STRATEGY_PAGE`). A paged file that does not persist
//! its free space is rejected with [`Error::EditUnsupported`] under either backing, and recreating
//! it with `persist = true` makes it editable.
//!
//! Adding an **object-reference dataset** ([`Group::create_dataset`] with
//! [`with_path_references`](DatasetBuilder::with_path_references)) resolves a path target against
//! every object this commit places, once that object has been placed and no sooner.
//! [`commit`](File::commit) processes groups deepest-first and, within a group, non-reference
//! datasets before reference ones. A target that is itself still being written when the reference
//! is resolved (an ancestor group, a same-depth sibling group ordered later, a copy destination or
//! its interior, or a [`Dataset::write`] target) is rejected, since resolving it would give a
//! stale or wrong address. A target the same commit deletes, the deleted path or anything under
//! it, is rejected too, since a deletion reclaims the whole subtree and the reference would point
//! into space the file is about to hand out again. A target supplied as an address, through
//! [`with_reference_data`](DatasetBuilder::with_reference_data) or
//! [`with_raw_data`](DatasetBuilder::with_raw_data) over a datatype that holds a reference, is
//! screened by address and not by name, wherever the commit writes one: a new dataset, a
//! [`Dataset::write_staged`] overwrite, and an in-file [`copy`](File::copy), which re-emits its
//! source's references verbatim ([#317](https://github.com/CramBL/hdf5-pure/issues/317)). A
//! supplied address is screened against both halves of what a commit can vacate, the objects it
//! deletes and the headers it rewrites elsewhere (a group it dirties, a dataset whose write
//! relocates it). The second rejection points at the path form, since the object still exists,
//! though only a dirty group is resolvable that way, once this commit has placed it. A relocating
//! dataset write is rejected by name outright, which is why that message points at separate
//! commits too. A copied address is screened against deletions alone: a target that merely moves
//! needs no rejection on either side, because the commit repoints every reachable stored reference
//! once it has published its tree ([#324](https://github.com/CramBL/hdf5-pure/issues/324)), the
//! copy's included.
//!
//! In a commit that deletes, three things are rejected outright, without that screen:
//!
//! - copying a chunked object-reference dataset, whose addresses sit inside chunks the copy path
//!   carries compressed and never decodes, the same limit that makes [`repack`](fn@repack) reject
//!   one outright
//! - copying an object carrying a shared (SOHM) attribute message, whose bytes this path cannot
//!   reach
//! - copying an object whose datatype or attribute this parser cannot read, since an unreadable
//!   datatype cannot be shown to be free of references
//!
//! A committed (`H5Tcommit`) datatype is not one of these: it is resolved, so an object with a
//! named type still copies. Separately, a datatype whose references this screen cannot read out of
//! the element bytes is rejected, since writing it would leave it unscreened: one wider than 8
//! bytes, a dataset-region reference, a variable length of references (whose addresses live in the
//! global heap the elements point at), or a compound holding one of those beside a reference the
//! screen can read. A staged dataset is rejected in any commit that rebuilds a header, which is
//! every commit except one whose only staged edit is a same-length in-place overwrite, and that
//! one vacates nothing to dangle into. A copied one is rejected only beside a deletion, as the
//! three above are. No API here builds such a type:
//! [`with_raw_data`](DatasetBuilder::with_raw_data) is one way in, and an in-file
//! [`copy`](File::copy) of a file the reference C library wrote is the other. A path the commit
//! deletes and then [replaces](crate::_guide::editing#replacing-an-object) resolves to the
//! replacement once the replacement has been placed, so the placement-order rule above still
//! governs it. A target untouched by the commit resolves against the pre-commit file, and a path
//! that resolves nowhere at all becomes an undefined reference, matching [`FileBuilder`]'s
//! resolution convention for the same builder type. This is a permanent scope line, and not a
//! `... yet` gap: reproducing the whole-file writer's two-pass dummy-then-real-address scheme
//! inside the editor's single-pass commit would be a large rewrite of the core apply loop for a
//! narrow benefit.
//!
//! ### Object header message size
//!
//! A version 2 object header describes each message's length in a 2-byte field, so no message it
//! carries may exceed 65,535 bytes. The whole-file writer rejects an oversized message and never
//! truncates one:
//!
//! - A compact attribute past the limit would be rejected with
//!   [`FormatError::AttributeMessageTooLarge`], which reports the attribute. The limit is on the
//!   message, its name, datatype, dataspace, and data together, and not on the element count.
//! - Every other oversized message, most often a Link message from a very long dataset or group
//!   name, is rejected with [`FormatError::ObjectHeaderMessageTooLarge`], which carries the message
//!   type.
//!
//! An attribute that would exceed the limit selects fractal-heap storage, where no such field
//! bounds it. See [dense attribute storage](#dense-attribute-storage) below.
//! `AttributeMessageTooLarge` is therefore a backstop no input reaches today, since the writer
//! sends exactly those attributes to a heap, kept because the limit it describes is a real
//! property of the object header. `ObjectHeaderMessageTooLarge`, which covers the messages with no
//! heap alternative, is the one reachable on size.
//!
//! The [in-place editor](crate::_guide::editing) meets the limit the same way: an attribute
//! whose message would exceed it is written to a heap, and the messages with no heap alternative
//! are rejected with [`Error::EditUnsupported`]. What no storage lifts is the 2-byte field inside
//! the attribute message that describes its name, datatype or dataspace, which is
//! [`FormatError::AttributeFieldTooLong`] on either path.
//!
//! ### Dense attribute storage
//!
//! An object stores its attributes in a fractal heap when it has more than eight of them, or when
//! any one of them is too large for an object-header message. That is the same disjunction the
//! reference C library uses, and it is why a single large attribute is written and not rejected.
//!
//! The writer emits the same heap geometry the reference C library uses for an attribute heap: a
//! doubling table of direct blocks from 1 KiB up to 64 KiB, reached through a root indirect block
//! once one block no longer holds everything, and indexed by B-trees of fixed 512-byte nodes that
//! grow internal levels as the record count rises. An attribute serializing past 65,514 bytes does
//! not fit a heap managed object, so it is written as a huge object: its bytes go outside the
//! managed blocks and a huge-objects B-tree maps a generated ID to them. An attribute's size has
//! no limit, and the number of attributes an object may carry has none either. What is still
//! rejected:
//!
//! - An attribute whose name, datatype, or dataspace serializes past 65,535 bytes is rejected with
//!   [`FormatError::AttributeFieldTooLong`], which reports the attribute and the field. Each has a
//!   2-byte length field in the attribute message, and huge storage lifts the limit on an
//!   attribute's data alone.
//! - About a terabyte of managed attributes on one object is rejected with
//!   [`FormatError::DenseAttributeHeapTooLarge`]: the heap's offsets are 40 bits wide, so its
//!   blocks cannot span more than that between them.
//!
//! The [in-place editor](crate::_guide::editing) emits the same storage through the same builder:
//! an attribute edit that outgrows the object header moves the whole set to a heap, an object
//! already using one is rebuilt around the edit, and a dataset or group created in place may carry
//! one ([#102](https://github.com/CramBL/hdf5-pure/issues/102)). Three things bound it there: an
//! attribute holding an object reference a commit could still repoint is left in the header, a
//! shared (SOHM) attribute message is not rewritten, and the heap's own bounds above apply as they
//! do to the whole-file writer. The heap a rebuild supersedes is left as dead bytes for
//! [`repack`](fn@repack), as every dense heap this editor replaces is.
//!
//! Eight is fixed, on both paths. A version 2 object header may store its own
//! `H5Pset_attr_phase_change` thresholds, and the in-place editor preserves that block across a
//! rewrite, so the reference C library still reports the pair a caller set. The switch to a heap
//! is still made at eight attributes whatever the stored pair says
//! ([#422](https://github.com/CramBL/hdf5-pure/pull/422)).
//!
//! The total is otherwise not limited, and the table adds blocks in place of rounding the whole
//! heap up to a power of two. Space is still lost per block. An attribute that does not fit what
//! remains of a block moves to the next one, and an attribute just over half of the largest
//! block's size leaves most of a block unused, so a set of such attributes can still occupy close
//! to twice its own size. The writer only ever appends to the block it filled last, and an earlier
//! block's remainder stays unused, where the reference C library's free-space manager goes back
//! for it.
//!
//! ### Group creation property list
//!
//! There is no property-list API for group creation, and none of its settings are configurable.
//! Every group the crate writes, the root group included, has one fixed shape: a new-style (v2
//! object header) group with compact link storage and no stored timestamps. An object header the
//! [in-place editor](crate::_guide::editing) rewrites keeps whatever times it already stored, with
//! the modification and change times moved to the edit, so a header created from nothing is the
//! one that gets this shape. It is equivalent to creating every group with `obj_track_times =
//! false` and never switching to old-style (symbol-table) or dense (fractal-heap) link storage,
//! whatever the file version or the child count. The reference library's GCPL defaults vary by
//! version, and this shape is fixed on purpose: it keeps output byte-for-byte reproducible, which
//! is what makes the crate a good fit for stable snapshot files. See
//! [#131](https://github.com/CramBL/hdf5-pure/issues/131).
//!
//! ## Planned support
//!
//! Rejected today with a `... yet` message, and intended to land. Each row links to its tracking
//! issue.
//!
//! ### In-place editing
//!
//! | Capability | Tracking |
//! |---|---|
//! | Dense (fractal-heap) link storage, a group with more links than it keeps compactly | [#102](https://github.com/CramBL/hdf5-pure/issues/102) |
//! | Editing across soft and external links | [#103](https://github.com/CramBL/hdf5-pure/issues/103) |
//! | Copying a version-1 object (attribute creation-order tracking is edited as of [#416](https://github.com/CramBL/hdf5-pure/issues/416)) | [#104](https://github.com/CramBL/hdf5-pure/issues/104) |
//! | Adding a link to a group that tracks link creation order, as netCDF-4 writes, once the addition would take the group past the compact-storage threshold its group-info message declares (8 links by default). Past it the links move to a fractal heap with a creation-order B-tree beside the name index, which is dense link storage, the row above, tracked by [#102](https://github.com/CramBL/hdf5-pure/issues/102). Below it such a group is edited as any other is, as of [#416](https://github.com/CramBL/hdf5-pure/issues/416): an added link takes the next creation index, a removed one leaves a gap without lowering the group's counter, and a copy of the group keeps the order it had | [#102](https://github.com/CramBL/hdf5-pure/issues/102) |
//! | Changing a shared (SOHM) message's reference count. A file with a shared-message table is read and edited, and a commit rejects what it would strand: an object whose attributes are a shared message is not rewritten, and an object header the file's shared-message index refers to is neither moved nor removed. A commit that deletes an object using a shared message leaves the count one too high, so the message stays readable and the entry is never reclaimed. [`repack`](fn@repack) rewrites the file with every message inline | [#417](https://github.com/CramBL/hdf5-pure/issues/417) |
//! | Adding chunked or extensible variable-length-string datasets | [#105](https://github.com/CramBL/hdf5-pure/issues/105) |
//! | Overwriting ([`Dataset::write_staged`]) with [`with_path_references`](DatasetBuilder::with_path_references), whose staged element bytes are placeholder addresses that only the dataset-creation path resolves. The rejection is on that builder and not on the datatype: a reference dataset still overwrites through [`with_reference_data`](DatasetBuilder::with_reference_data), which supplies resolved addresses, or [`with_raw_data`](DatasetBuilder::with_raw_data). [`with_vlen_strings`](DatasetBuilder::with_vlen_strings) overwrites as of [#321](https://github.com/CramBL/hdf5-pure/issues/321). The global heap collections it can prove are its own are reclaimed, and [`repack`](fn@repack) recovers the rest, which the [editing guide](crate::_guide::editing#staging-and-committing-edits) covers | [#321](https://github.com/CramBL/hdf5-pure/issues/321) |
//! | Cross-file copy of variable-length, reference, or shared data, including an attribute that refers to a committed datatype | [#106](https://github.com/CramBL/hdf5-pure/issues/106) |
//! | Keeping an object reference already stored in the file valid when its target's header is rewritten, where the address cannot be reached. By storage: inside a chunked dataset's chunks, in a dense (fractal-heap) attribute or one held as a shared (SOHM) record, and in an attribute of a version 1 object header (such a header's element data is reached, through the parser that reads both versions, and version 1 is what the reference C library and h5py write by default). By datatype: an object reference wider than 8 bytes, a dataset-region reference, a variable length of references (the encoding the dimension-scale attribute `DIMENSION_LIST` uses), an enumeration over one, and any compound or array reaching one of those. What a commit does repoint as its last act is the rest: a contiguous or compact dataset's elements and an attribute held in the header, holding an 8-byte object reference at any depth of compound or array nesting, including through a committed (`H5Tcommit`) datatype. An unreached reference is left as it was. Nothing is rejected for being unreachable, since every commit rebuilds its root group and would therefore reject every commit on such a file | [#324](https://github.com/CramBL/hdf5-pure/issues/324) |
//!
//! ### Repack
//!
//! | Capability | Tracking |
//! |---|---|
//! | Repack of region references, non-8-byte object references, chunked, filtered or resizable reference datasets, and unrecognized filter pipelines (time, variable-length sequences, and 8-byte object references repack faithfully, and chunked, filtered, and resizable variable-length datasets repack as of [#109](https://github.com/CramBL/hdf5-pure/issues/109)) | [#107](https://github.com/CramBL/hdf5-pure/issues/107) |
//! | Dropping a committed (`H5Tcommit`) datatype a surviving dataset or attribute still refers to, and a committed datatype no hard link reaches, both rejected because the copy would point at nothing (a committed datatype otherwise repacks: the object is recreated and its users still refer to it) | [#254](https://github.com/CramBL/hdf5-pure/issues/254) |
//!
//! ### Reading
//!
//! | Capability | Tracking |
//! |---|---|
//! | Filter-encoded fractal-heap objects | [#108](https://github.com/CramBL/hdf5-pure/issues/108) |
//! | Virtual (VDS) datasets | [#111](https://github.com/CramBL/hdf5-pure/issues/111) |
//! | Writing shared (SOHM) messages: this crate's writer stores every message privately, and [`repack`](fn@repack) rewrites a shared-message file that way. Reading one is supported as of [#417](https://github.com/CramBL/hdf5-pure/issues/417), covering the shared-message table, both index kinds, and the heap lookup, for the datatype, dataspace, fill value, filter pipeline and attribute messages `H5Pset_shared_mesg_index` can share | [#417](https://github.com/CramBL/hdf5-pure/issues/417) |
//!
//! [`repack`](fn@repack) rejects a virtual dataset as well, since it cannot relocate data living
//! outside the file. That lifts together with VDS read support
//! ([#111](https://github.com/CramBL/hdf5-pure/issues/111)).
//!
//! ### Writing
//!
//! | Capability | Tracking |
//! |---|---|
//! | Append to a variable-length dataset (writing one resizable is supported, and growing it is not) | [#109](https://github.com/CramBL/hdf5-pure/issues/109) |
//! | More than one unlimited dimension in a `maxshape`. The reference library indexes that dataspace with a version-2 B-tree, which this crate neither writes nor reads. One unlimited dimension is supported at any rank | [#299](https://github.com/CramBL/hdf5-pure/issues/299) |
//! | A `maxshape` whose chunk index would spend more than 32 MiB on elements describing no chunk. An Extensible Array allocates only the blocks its chunks land in, as the reference library does, so this applies where the slack inside those blocks is itself large. A Fixed Array is dense by format and costs the same here as in the reference library, so for a fixed `maxshape` this is a bound on what this writer holds in memory while building the index in one pass | [#299](https://github.com/CramBL/hdf5-pure/issues/299) |
//! | Add a chunked variable-length dataset to an existing file in place | [#109](https://github.com/CramBL/hdf5-pure/issues/109) |
//! | Add a dataset or attribute that refers to a committed (`H5Tcommit`) datatype to an existing file in place, where the in-place engine appends into a fixed layout and has nowhere to place the named type object. [`FileBuilder::commit_datatype`] writes one when the whole file is written, and [`repack`](fn@repack) carries them across | [#254](https://github.com/CramBL/hdf5-pure/issues/254) |
//!
//! A dataset whose storage was never allocated keeps that state through [`repack`](fn@repack)
//! ([#293](https://github.com/CramBL/hdf5-pure/issues/293)), so a schema-only file stays one and
//! is never written out full of its fill value. Three things bound that. A dataset that stores
//! only some of its chunks is unaffected: a sparse grid cannot take the verbatim path, so it falls
//! back to read-and-re-encode and the destination stores every slot, measured at one chunk in and
//! ten out. A resizable destination is given the eagerly built Extensible Array this crate gives
//! every empty resizable dataset, because an in-place append needs the index to exist before the
//! first chunk arrives. Neither form stores a chunk, and the index costs a few hundred bytes the
//! source did not spend. And the `maxshape` bound in the table above does not apply to a dataset
//! that stores nothing, since both halves of it are sized from the slots an index spans and an
//! unallocated one spans none: such a dataset is written, and rejected when its first chunk
//! arrives.
//!
//! Two gaps remain around variable-length datasets, which write chunked, filtered and resizable
//! ([#109](https://github.com/CramBL/hdf5-pure/issues/109)). A resizable one can be created and
//! not grown: [`Dataset::append`] is typed and [`H5Element`] covers numeric scalars alone, and
//! [`append_raw`](Dataset::append_raw) rejects a variable-length datatype, since it cannot encode
//! the heap references. And adding such a dataset to an existing file through the in-place edit
//! engine is rejected, since the engine appends into a fixed layout with nowhere to place the heap
//! collections ahead of the chunks.
//!
//! ### SWMR
//!
//! | Capability | Tracking |
//! |---|---|
//! | Append to multi-dimensional and filtered datasets | [#110](https://github.com/CramBL/hdf5-pure/issues/110) |
//!
//! This gap is specific to SWMR (concurrent-reader) append. Appending to a filtered 1-D unlimited
//! dataset without concurrent readers is supported at any length, through
//! [`Dataset::append_staged`] and the streaming [`Dataset::append`] alike, the streaming one under
//! a lossless pipeline, since growing a lossy dataset's trailing chunk would re-encode committed
//! values.
//!
//! # Safety and robustness
//!
//! - **Almost entirely safe Rust.** The default feature set contains no non-trivial
//!   `unsafe`. Adding `serde` compiles the tiled row-major/column-major transpose
//!   used by the MATLAB writer, which is exercised under
//!   [Miri](https://github.com/rust-lang/miri) with `-Zmiri-strict-provenance` in CI.
//!   A `no_std` build has neither, and compiles a single-threaded `Mutex`
//!   replacement instead, whose `Send`/`Sync` rest on the target being
//!   single-threaded.
//! - **32-bit safe.** Every file-derived offset and length is narrowed through
//!   checked conversions, so a 64-bit value that does not fit a 32-bit `usize`
//!   errors where a cast would truncate. CI runs the suite on `i686` under QEMU and
//!   builds for `thumbv7em-none-eabi`.
//! - **Property-tested.** The write/read roundtrip and parser robustness are
//!   covered by property-based tests in addition to the example- and
//!   fixture-driven suites.
//!
//! # Origins and licenses
//!
//! The crate is licensed under MIT or Apache-2.0, at your option, and parts of it come from other
//! projects. The scale-offset filter is a port of the HDF5 library's `H5Zscaleoffset.c` and the
//! ZFP codec a port of LLNL's reference implementation, both BSD 3-Clause, and the HDF5 format
//! parsing and low-level I/O modules come from rustyhdf5 by the RustyStack project, MIT licensed.
//! The README's License section links every license text.
//!
//! [readme-install]: https://github.com/CramBL/hdf5-pure#installation

#![cfg_attr(docsrs, feature(doc_cfg))]
#![cfg_attr(not(feature = "std"), no_std)]
// In `no_std` builds the high-level entry points that consume the parsing and
// serialization machinery — the `reader`, `writer`, and `edit` modules —
// are `std`-gated and therefore absent, leaving much of that machinery without
// an in-crate consumer. It is deliberately kept available for future `no_std`
// readers/writers rather than cfg-gating every module to `std`, so `dead_code`
// is allowed only in `no_std` builds. `std` builds (the primary target) keep
// full `dead_code` enforcement.
#![cfg_attr(not(feature = "std"), allow(dead_code))]

#[cfg(not(feature = "std"))]
extern crate alloc;

// ---------------------------------------------------------------------------
// The guide.
// ---------------------------------------------------------------------------

#[cfg(doc)]
pub mod _guide;

// ---------------------------------------------------------------------------
// Internal modules (encapsulated as `pub(crate)`; the curated public surface is
// re-exported at the bottom of this file).
// ---------------------------------------------------------------------------

pub(crate) mod address;
pub(crate) mod attribute;
pub(crate) mod attribute_info;
pub(crate) mod btree_v1;
pub(crate) mod btree_v2;
pub(crate) mod btree_v2_write;
pub(crate) mod bytes;
pub(crate) mod checksum;
pub(crate) mod chunk_cache;
pub(crate) mod chunk_grid;
pub(crate) mod chunk_span;
pub(crate) mod chunked_read;
pub(crate) mod chunked_write;
pub(crate) mod compound;
pub(crate) mod convert;
pub(crate) mod data_layout;
pub(crate) mod data_read;
pub(crate) mod dataspace;
pub(crate) mod datatype;
pub(crate) mod display;
pub(crate) mod error;
pub(crate) mod extensible_array;
pub(crate) mod file_create_properties;
pub(crate) mod file_space_info;
pub(crate) mod file_writer;
pub(crate) mod fill_value;
pub(crate) mod filter_pipeline;
pub(crate) mod filters;
pub(crate) mod fixed_array;
pub(crate) mod fractal_heap;
pub(crate) mod fractal_heap_write;
pub(crate) mod free_space_manager;
pub(crate) mod global_heap;
pub(crate) mod group_v1;
pub(crate) mod group_v2;
pub(crate) mod layout_info;
pub(crate) mod libver;
pub(crate) mod link_info;
pub(crate) mod link_message;
pub(crate) mod local_heap;
pub(crate) mod lzf;
pub(crate) mod message_type;
pub(crate) mod object_header;
pub(crate) mod object_header_writer;
pub(crate) mod read_spec;
pub(crate) mod scaleoffset;
pub(crate) mod shared_message;
pub(crate) mod signature;
pub(crate) mod sohm;
pub(crate) mod source;
pub(crate) mod superblock;
pub(crate) mod symbol_table;
pub(crate) mod type_builders;
pub(crate) mod vl_data;
pub(crate) mod width;
#[cfg(feature = "zfp")]
pub(crate) mod zfp;

#[cfg(feature = "provenance")]
pub(crate) mod provenance;

#[cfg(not(feature = "std"))]
pub(crate) mod nosync;

// ---------------------------------------------------------------------------
// High-level modules
// ---------------------------------------------------------------------------

#[cfg(feature = "std")]
pub(crate) mod appender;
#[cfg(feature = "std")]
pub(crate) mod chunk_index_inplace;
#[cfg(all(test, feature = "std"))]
mod crash_replay;
#[cfg(feature = "std")]
pub(crate) mod edit;
#[cfg(feature = "std")]
pub(crate) mod file_lock;
#[cfg(feature = "std")]
pub(crate) mod free_space;
#[cfg(feature = "std")]
pub(crate) mod image;
#[cfg(feature = "std")]
pub(crate) mod reader;
#[cfg(feature = "std")]
pub(crate) mod reference_patch;
#[cfg(feature = "std")]
pub(crate) mod repack;
#[cfg(test)]
mod test_data;
#[cfg(feature = "std")]
pub(crate) mod types;
#[cfg(feature = "std")]
pub(crate) mod writer;

#[cfg(feature = "std")]
pub mod mat;

#[cfg(feature = "std")]
pub(crate) mod element;

#[cfg(feature = "ndarray")]
pub(crate) mod ndarray_support;

// ---------------------------------------------------------------------------
// Public API re-exports
// ---------------------------------------------------------------------------

#[cfg(feature = "std")]
pub use error::Error;
pub use error::{FormatError, OBJECT_HEADER_MESSAGE_MAX};

// Reached by value through `File::superblock` and `Error::MissingMessage`, so
// exported alongside them rather than left nameless — see RELEASES.md, and
// `scripts/check-api-surface.sh` (`just api::api-surface`), which enforces it.
#[cfg(feature = "std")]
pub use address::BaseAddress;
#[cfg(feature = "std")]
pub use message_type::MessageType;
#[cfg(feature = "std")]
pub use superblock::Superblock;

#[cfg(feature = "std")]
pub use reader::{
    Dataset, DatasetAccessProperties, File, FileAccessProperties, Group, Object, StagedGroup,
    is_hdf5, is_hdf5_bytes,
};

// Curated layout / filter introspection (issue #149). Only the std-only reader
// `Dataset` produces these, so gate the re-export to match the reader block
// above; the types themselves are `alloc`-clean.
#[cfg(feature = "std")]
pub use layout_info::{Chunk, ChunkIndex, Filter, Layout};

#[cfg(feature = "std")]
pub use file_lock::{FileLocking, WriteMarkPolicy};

pub use chunk_cache::{ChunkCacheConfig, ChunkCacheStats};

pub use source::{MetadataCacheConfig, MetadataCacheStats, Source};

// The `Read + Seek` backend, for a caller who has one but no path. Exported
// beside `Source` because without it every such caller reimplements the
// seek-and-read the crate already carries.
#[cfg(feature = "std")]
pub use source::ReadSeekSource;

#[cfg(feature = "std")]
pub use vl_data::VlenStringReadOptions;

pub use libver::LibVer;

#[cfg(all(feature = "std", feature = "provenance"))]
pub use provenance::VerifyResult;

#[cfg(feature = "std")]
pub use types::{AttrValue, DType};

#[cfg(feature = "std")]
pub use writer::FileBuilder;

#[cfg(feature = "std")]
pub use appender::BufferedAppender;

#[cfg(feature = "std")]
pub use edit::{AppendBuilder, EditBacking, MemoryStrategy, SpaceAccounting, SyncPolicy};

#[cfg(feature = "std")]
pub use repack::{RepackOptions, repack};

#[cfg(feature = "std")]
pub use element::H5Element;

pub use scaleoffset::ScaleOffset;

pub use file_create_properties::FileCreateProperties;
pub use file_space_info::{FileSpaceInfo, FileSpaceStrategy};

// The HDF5 datatype handle returned by the `make_*_type` constructors and
// accepted by the compound/enum builders and `DatasetBuilder::with_dtype`.
pub use compound::{CompoundField, CompoundType};
pub use datatype::{
    CharacterSet, CompoundMember, Datatype, DatatypeByteOrder, EnumMember, ReferenceType,
    StringPadding,
};

pub use type_builders::{
    CompoundTypeBuilder, DatasetBuilder, EnumTypeBuilder, ExplicitCompoundTypeBuilder,
    FinishedGroup, GroupBuilder, make_f32_type, make_f64_type, make_i8_type, make_i16_type,
    make_i32_type, make_i64_type, make_u8_type, make_u16_type, make_u32_type, make_u64_type,
};