Compare commits
158
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ef428d756c | ||
|
|
4313917b4d | ||
|
|
011e0dbb96 | ||
|
|
f37e7ae326 | ||
|
|
7447dce121 | ||
|
|
93e2d5f365 | ||
|
|
2893b6c974 | ||
|
|
ea0508aaa5 | ||
|
|
75444950f3 | ||
|
|
8236b0e30a | ||
|
|
c2ae7846c9 | ||
|
|
a69c5be8b2 | ||
|
|
930921e8cb | ||
|
|
159e588550 | ||
|
|
6185874f9c | ||
|
|
89e7977943 | ||
|
|
67e72b30d7 | ||
|
|
0e98ffc498 | ||
|
|
efb88f94e3 | ||
|
|
ef480746da | ||
|
|
c5b2afbc35 | ||
|
|
b086dc3c2b | ||
|
|
30a1ed6b9c | ||
|
|
61e34927dc | ||
|
|
680c90b3a8 | ||
|
|
7d629f49e3 | ||
|
|
c04e34620e | ||
|
|
8df5b209a7 | ||
|
|
e8aaf050be | ||
|
|
5062b907bd | ||
|
|
4f5697fdd9 | ||
|
|
955dd1c691 | ||
|
|
ebe51f8e97 | ||
|
|
4e8109770d | ||
|
|
c513f7e6d7 | ||
|
|
a4f586e657 | ||
|
|
c54c64cc9b | ||
|
|
4ff3e40fea | ||
|
|
0aca0eb724 | ||
|
|
db2554dd81 | ||
|
|
955fdb660d | ||
|
|
304aed5813 | ||
|
|
1ffd013de9 | ||
|
|
dc9cfba6bb | ||
|
|
773f427f16 | ||
|
|
7e5e920c72 | ||
|
|
f191dc09d5 | ||
|
|
1c3ef98828 | ||
|
|
e9c71e5d2e | ||
|
|
17201e279d | ||
|
|
3fa5ed1dda | ||
|
|
42894bf93b | ||
|
|
8f59b2e1c2 | ||
|
|
0645dcf173 | ||
|
|
cadd27df5b | ||
|
|
b49ec39aff | ||
|
|
8fadb9f424 | ||
|
|
c233fbca6e | ||
|
|
437e81cfff | ||
|
|
234dd3e36c | ||
|
|
0e8522cfad | ||
|
|
895c79a2fe | ||
|
|
76c97f6c94 | ||
|
|
e5359354b7 | ||
|
|
052098bf36 | ||
|
|
fe377266e1 | ||
|
|
b668878129 | ||
|
|
485bea0f4f | ||
|
|
1ea9132e10 | ||
|
|
04a7f6f6c7 | ||
|
|
5b3d32b37d | ||
|
|
f7e2ab12f2 | ||
|
|
b6cbd2319f | ||
|
|
92c8285549 | ||
|
|
d2b25f154f | ||
|
|
85efde0b4a | ||
|
|
677dc5ec7c | ||
|
|
3c89a31df0 | ||
|
|
1b4a93f65a | ||
|
|
b41583113a | ||
|
|
02e89c1d2d | ||
|
|
5d17712adb | ||
|
|
476960f4b8 | ||
|
|
24f0c71939 | ||
|
|
5705866d40 | ||
|
|
2b7065998a | ||
|
|
6a535e2651 | ||
|
|
a65a2b7f18 | ||
|
|
2c292404d2 | ||
|
|
ba3f476be6 | ||
|
|
ff6d644391 | ||
|
|
cf2b408a63 | ||
|
|
d97b3d703a | ||
|
|
23a4784e72 | ||
|
|
a0160730f2 | ||
|
|
24cbf12f16 | ||
|
|
bcf3ae4856 | ||
|
|
06625b7470 | ||
|
|
aab7ea9e8f | ||
|
|
6a9bb02f37 | ||
|
|
0d908facd3 | ||
|
|
cd828725c7 | ||
|
|
512a6a753f | ||
|
|
6a4707d791 | ||
|
|
6248b411f0 | ||
|
|
479d8b47e0 | ||
|
|
f2ff2c424f | ||
|
|
c5334b1c97 | ||
|
|
d0e3beb3aa | ||
|
|
9a73299594 | ||
|
|
4b02e7d068 | ||
|
|
d7f07fa5c1 | ||
|
|
bdb2c0e36b | ||
|
|
55e0e7e9cf | ||
|
|
00f94d57ed | ||
|
|
4c01267b76 | ||
|
|
d493d4792e | ||
|
|
6559a91495 | ||
|
|
60502593b7 | ||
|
|
a6ed3a5c7d | ||
|
|
3da118d2ee | ||
|
|
7515e5dcbd | ||
|
|
742ed4dfb8 | ||
|
|
2ba4bc97d8 | ||
|
|
d9e4dfb6e6 | ||
|
|
22dc87b07c | ||
|
|
e05530a805 | ||
|
|
989335b67b | ||
|
|
bf4aefcd00 | ||
|
|
378afa1584 | ||
|
|
67958b08d9 | ||
|
|
f512bf3d09 | ||
|
|
8295d01614 | ||
|
|
94b6df986c | ||
|
|
6b3d003950 | ||
|
|
d110b1d945 | ||
|
|
b3058ca46e | ||
|
|
d3d73676c0 | ||
|
|
56abaec75e | ||
|
|
7334e21c93 | ||
|
|
b22b15f00a | ||
|
|
9e608b975c | ||
|
|
9e9b849dd7 | ||
|
|
193a5f8a82 | ||
|
|
3aab433edb | ||
|
|
1c1af460b6 | ||
|
|
c5cd14c2b2 | ||
|
|
16b7359485 | ||
|
|
1207df5189 | ||
|
|
f0db817678 | ||
|
|
9e59499c56 | ||
|
|
d63c76e7ab | ||
|
|
cc1c872a93 | ||
|
|
7acfb79584 | ||
|
|
de2a53f613 | ||
|
|
83cda847dd | ||
|
|
55309dd242 | ||
|
|
c9c5337a62 |
@@ -484,6 +484,45 @@ explain the slower windows.
|
|||||||
|
|
||||||
## Concurrent reads
|
## Concurrent reads
|
||||||
|
|
||||||
|
### Results after in-place chunk decoding (2026-09-26, tank, `c5334b1`)
|
||||||
|
|
||||||
|
Same machine, files and commands, re-run after chunked reads started
|
||||||
|
decoding into reusable per-thread buffers straight into the (typed) output,
|
||||||
|
with the calling thread decoding alongside the pool. Load average 1.78 at
|
||||||
|
the start; it rose to 6-9 during the runs (the clawhdf5 runs' own threads,
|
||||||
|
and it stayed around 5-6 through the h5py runs, so something else was
|
||||||
|
active). **This run was noisier than the previous one: h5py's own contiguous
|
||||||
|
figures are about 40% lower than in the run below, and ours dropped
|
||||||
|
similarly, so compare ratios within a run rather than MB/s across runs.**
|
||||||
|
h5py was re-run in the same session.
|
||||||
|
|
||||||
|
Each read decoding on its calling thread (`--decode-threads 1`, like h5py):
|
||||||
|
|
||||||
|
| layout | mode | threads | clawhdf5 MB/s (eff) | h5py threads MB/s (eff) | h5py processes MB/s (eff) | vs h5py processes |
|
||||||
|
|---|---|---:|---:|---:|---:|---:|
|
||||||
|
| deflate | distinct | 1 | 670 (1.00) | 410 (1.00) | 397 (1.00) | 1.69x |
|
||||||
|
| deflate | distinct | 4 | 2434 (0.91) | 406 (0.25) | 1470 (0.93) | 1.66x |
|
||||||
|
| deflate | distinct | 8 | 3749 (0.70) | 406 (0.12) | 2398 (0.76) | 1.56x |
|
||||||
|
| deflate | distinct | 16 | 4944 (0.46) | 390 (0.06) | 3135 (0.49) | 1.58x |
|
||||||
|
| deflate | same | 1 | 211 (1.00) | 125 (1.00) | 124 (1.00) | 1.70x |
|
||||||
|
| deflate | same | 16 | 1835 (0.54) | 122 (0.06) | 961 (0.48) | 1.91x |
|
||||||
|
| contiguous | distinct | 1 | 6718 (1.00) | 5545 (1.00) | 5200 (1.00) | 1.29x |
|
||||||
|
| contiguous | distinct | 16 | 11035 (0.10) | 4950 (0.06) | 10558 (0.13) | 1.05x |
|
||||||
|
| contiguous | same | 1 | 14483 (1.00) | 2593 (1.00) | 2737 (1.00) | 5.29x |
|
||||||
|
| contiguous | same | 16 | 132175 (0.57) | 2224 (0.05) | 14809 (0.34) | 8.93x |
|
||||||
|
|
||||||
|
With the default rayon pool, deflate `distinct` reads 6143 MB/s from a single
|
||||||
|
thread (15x h5py's 410 on one call) and 4556 MB/s at 16 threads (1.45x h5py
|
||||||
|
processes); the other rows are within the noise of the table above.
|
||||||
|
|
||||||
|
What changed: full reads of chunked datasets were 0.69x-0.76x of h5py
|
||||||
|
processes at 16 threads in the run below, and are 1.58x here; with one
|
||||||
|
thread they were 1.44x and are 1.69x. Minor page faults for the 16-thread
|
||||||
|
run fell from about 4.6M to 0.2M (`/usr/bin/time -v`, provisional, loaded
|
||||||
|
machine). clawhdf5 now reads faster than 16 h5py processes in every row of
|
||||||
|
this benchmark except contiguous full reads at 16 threads, where both
|
||||||
|
saturate memory bandwidth (1.05x).
|
||||||
|
|
||||||
### Results after the read fixes (2026-09-26, tank, `408f69e`)
|
### Results after the read fixes (2026-09-26, tank, `408f69e`)
|
||||||
|
|
||||||
Same machine, files and commands as the first run below, re-run on an idle
|
Same machine, files and commands as the first run below, re-run on an idle
|
||||||
|
|||||||
+847
@@ -2,6 +2,799 @@
|
|||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
### Range reads, milestone M3: remote files (2026-09-26)
|
||||||
|
- **New crate `clawhdf5-remote`.** `open_url("http://host/file.h5")` gives
|
||||||
|
a `clawhdf5::File` (through `File::open_storage`) that reads the file by
|
||||||
|
HTTP `Range` requests; `storage_for_url` returns the cached storage to
|
||||||
|
read its statistics. Features: `http` (default; `ureq` without TLS, no
|
||||||
|
C), `https` (rustls with ring, which compiles C), `object-store` (any
|
||||||
|
`object_store` store, pure Rust), `s3`/`gcs`/`azure` (`s3://`, `gs://`,
|
||||||
|
`az://` URLs, configured from the environment; object_store's cloud
|
||||||
|
clients use aws-lc-rs, C).
|
||||||
|
- **`BlockCache`** wraps any `Storage`: aligned 1 MiB blocks (the size
|
||||||
|
`docs/design/range-reads.md` §2 measured), LRU with a byte budget
|
||||||
|
(64 MiB), the blocks one read misses fetched with one backend
|
||||||
|
`read_ranges` call as runs of consecutive blocks (a one-block gap is
|
||||||
|
fetched to merge two runs; at most 8 MiB per request). Readers on
|
||||||
|
several threads share it without holding its lock across a request, and
|
||||||
|
a block being fetched is waited for, not fetched again. A read that
|
||||||
|
misses more than half the budget is not kept (a large dataset does not
|
||||||
|
evict the metadata). A failed fetch is an error for every reader waiting
|
||||||
|
on it and is not cached.
|
||||||
|
- **`HttpStorage`**: opening is one ranged `GET` of the first block,
|
||||||
|
whose `Content-Range` gives the length. The file is pinned by its strong
|
||||||
|
`ETag` (`If-Match`) or else `Last-Modified` (`If-Unmodified-Since`), and
|
||||||
|
its length, checked on every response: a file changed while open is
|
||||||
|
`RemoteError::FileChanged`, never mixed data. A server that ignores
|
||||||
|
`Range` is refused without reading the body unless
|
||||||
|
`HttpOptions::allow_full_download`. Connection failures, timeouts,
|
||||||
|
`408`/`429`/`5xx` and cut-off bodies are retried with exponential
|
||||||
|
backoff; bodies are asked for with `Accept-Encoding: identity` and an
|
||||||
|
encoded one is refused. The ranges of one call are fetched in parallel.
|
||||||
|
A `200` answer to the first request whose body fits the range asked
|
||||||
|
for is taken as the whole file (a server may answer so for a small
|
||||||
|
file). Timeouts scale with the request: `HttpOptions::timeout` (30 s)
|
||||||
|
to connect and to get the headers, plus the body's size at
|
||||||
|
`HttpOptions::min_speed` (16 KiB/s), so a slow link is not cut off.
|
||||||
|
Redirects: at most `HttpOptions::max_redirects` (5), never from `https`
|
||||||
|
to `http`, and `HttpOptions::headers` are not sent to another origin.
|
||||||
|
No error or `Debug` output contains a URL's userinfo or query values
|
||||||
|
(`redact_url`; presigned URLs carry their signature there).
|
||||||
|
- **Hostile lengths**: the length a server claims is never used to
|
||||||
|
allocate. The cache's arithmetic is checked (a length near `u64::MAX`
|
||||||
|
used to overflow), a read spanning more than the budget is fetched
|
||||||
|
piece by piece with its output growing as data arrives, and
|
||||||
|
`download(storage, max_bytes)` reads a whole file only up to a limit
|
||||||
|
(`RemoteError::TooLarge` before any request otherwise).
|
||||||
|
- **`ObjectStoreStorage`** reads one object of any `object_store` store,
|
||||||
|
pinned by ETag (else version or modification time) and size. Each read
|
||||||
|
runs on a small tokio runtime the storage owns while the caller waits,
|
||||||
|
so it works from any thread, `spawn_blocking` and other runtimes
|
||||||
|
included.
|
||||||
|
`open_object(store, path, options)` opens a file through a block cache.
|
||||||
|
- Counted on the conformance corpus (tank, 2026-09-26,
|
||||||
|
`CLAWHDF5_REMOTE_CORPUS=conformance/.cache/corpus CLAWHDF5_REMOTE_REPORT=1
|
||||||
|
cargo test --release -p clawhdf5-remote --test http -- --nocapture corpus`):
|
||||||
|
the 621 files that open (254 MB) read over HTTP exactly as through
|
||||||
|
`File::open`; opening and listing them all (every group's entries, every
|
||||||
|
dataset's shape and type) took 640 requests and 55.5 MB, and then
|
||||||
|
reading each file's largest dataset under 64 MiB 96 more requests
|
||||||
|
(171 MB in all). Without the cache the same work is 141 936 requests.
|
||||||
|
Listing the 7.7 MB IMERG file (file A of the design's §2) takes 2
|
||||||
|
requests; the tests hold it to at most 3.
|
||||||
|
- **`h5rs` takes URLs** with the new `remote` feature (`remote-https` for
|
||||||
|
`https://`): `ls`, `dump`, `stat` and `diff` read by range requests;
|
||||||
|
`check` downloads the file whole, since it validates every byte, up to
|
||||||
|
`--max-download N` (1 GiB by default). URLs are printed without their
|
||||||
|
credentials. The
|
||||||
|
tools now read through `File::storage` and the format crate's `*_in`
|
||||||
|
functions; local output is unchanged.
|
||||||
|
- **`File::storage()`** (facade) returns the file's bytes from the
|
||||||
|
superblock on (the `as_bytes` view, cache image laid over) as a
|
||||||
|
`&(dyn Storage + Send + Sync)` for every backend, so code that parses a
|
||||||
|
file itself works on remote files too.
|
||||||
|
- **`VlResolver::element_in` / `string_element_in`** (format): the
|
||||||
|
`element`/`string_element` lookups over any `Storage`.
|
||||||
|
|
||||||
|
### Range reads, milestone M2: raw data and `File::open_storage` (2026-09-26)
|
||||||
|
- **`clawhdf5::File::open_storage(Arc<dyn Storage + Send + Sync>)`** opens
|
||||||
|
a file served by any `clawhdf5_format::storage::Storage` and gives the
|
||||||
|
whole read API over it: groups and paths, datasets, attributes, the
|
||||||
|
`read_*` methods, selections, variable-length strings and sequences, and
|
||||||
|
virtual datasets. Every byte comes through `Storage::read_at` /
|
||||||
|
`read_ranges`; a user block is found and skipped, nothing past the
|
||||||
|
superblock's end of file is read, and a metadata cache image is laid over
|
||||||
|
the reads it covers (new `CacheImage::entries`). External virtual-dataset
|
||||||
|
sources are read through the new `File::set_vds_resolver` (any `File`;
|
||||||
|
without one a storage-backed file cannot follow them). `File::open` and
|
||||||
|
`File::from_bytes` keep their mmap and in-memory paths: the file's view
|
||||||
|
is now a `Storage` whose `as_contiguous()` is that buffer, and every hot
|
||||||
|
loop takes it. New exports: `clawhdf5::{Storage, SharedStorage,
|
||||||
|
VdsResolver}`, `File::contiguous_bytes()`.
|
||||||
|
- Over a storage without the whole file in memory the zero-copy methods
|
||||||
|
(`read_raw_ref`, `read_as_slice`, `read_*_zerocopy`) answer
|
||||||
|
`FormatError::ContiguousStorageRequired`, and `File::as_bytes` panics
|
||||||
|
(documented; use `contiguous_bytes`). The typed readers keep their fast
|
||||||
|
paths: a contiguous dataset is read in one piece and converted, and a
|
||||||
|
contiguous selection of a native type reads only its runs
|
||||||
|
(`data_read::read_selection_native_in`).
|
||||||
|
- **Nothing in the format crate needs the whole file any more.** The
|
||||||
|
structures M1 left to `ContiguousStorageRequired` read through `Storage`:
|
||||||
|
v2 B-trees (`BTreeV2Header::parse_in`, `collect_btree_v2_records_in`,
|
||||||
|
`find_btree_v2_records_in`; one bounded read per node, whose size is known
|
||||||
|
before it is read), hence dense attributes, a SOHM B-tree index, huge
|
||||||
|
fractal-heap objects, and dense groups; v1 and v2 group listings, lookups
|
||||||
|
and paths (`group_v2::resolve_group_children_in`, `resolve_child_in`,
|
||||||
|
`resolve_path_any_in`, `group_v1::*_in`).
|
||||||
|
- **Raw data reads through `Storage`**, each with a generic `*_in` core and
|
||||||
|
its `&[u8]` function as a thin wrapper (callers do not change):
|
||||||
|
`data_read` (`read_raw_data*_in`, `read_raw_data_selection_in`,
|
||||||
|
`read_chunked_native_in`), `chunked_read` (the v1 B-tree chunk index —
|
||||||
|
one read of each node's header, one of its entries — `list_chunks_in`,
|
||||||
|
and the full, cached, sweep and indexed reads), `parallel_read`,
|
||||||
|
`partial_read`, `fill_value` (`read_full_with_fill_in`,
|
||||||
|
`apply_to_unallocated_chunks_in`; `dataset_fill_value_from_storage` is
|
||||||
|
now generic, so a `&dyn Storage` still works), `vds`
|
||||||
|
(`read_virtual_dataset_in`, `virtual_dataset_extent_in`: the virtual
|
||||||
|
file through `Storage`, external source files still loaded whole through
|
||||||
|
the resolver), `vl_data` (`VlResolver<'a, S = [u8]>` with `new_in`;
|
||||||
|
`read_vl_strings_in`, `read_vl_bytes_in`),
|
||||||
|
`AttributeMessage::read_vl_strings_in`, `provenance::verify_dataset_in`.
|
||||||
|
- A chunked read first lists the chunks it needs, then fetches all their
|
||||||
|
stored bytes with **one `read_ranges` call** per batch of at most 64 MiB
|
||||||
|
(`storage::RAW_BATCH_BYTES`), so a remote backend can coalesce and
|
||||||
|
parallelise them, then decodes each batch as before (in parallel with
|
||||||
|
the `parallel` feature) before fetching the next. Every path that reads
|
||||||
|
chunks — full, cached, indexed, sweep, selection and the
|
||||||
|
`parallel_read` decoders — goes through the same batching, and no chunk
|
||||||
|
fetches more of its stored bytes than its decoded size can need (the
|
||||||
|
chunk size if unfiltered; else each applied filter's worst-case growth,
|
||||||
|
generously: `n + n/4 + 4096` per codec, unbounded only for a codec the
|
||||||
|
application registered). A crafted chunk index that points every chunk
|
||||||
|
at one huge extent therefore costs a bounded fetch, not
|
||||||
|
`chunks x extent` bytes (`tests/raw_fetch_bounds.rs`). Chunks the
|
||||||
|
file's chunk cache already holds are not fetched. A selection fetches
|
||||||
|
only the chunks its bounding box overlaps; a contiguous selection only
|
||||||
|
its runs, merged into reads of up to 8 MiB across gaps of up to 4 KiB
|
||||||
|
(a stride-2 selection of 32M `f32` is 32 reads and 0.3 s over a
|
||||||
|
`CountingStorage`, where one read per element was 16.8M reads, 2.0 s
|
||||||
|
and 2.1 GB peak). A global-heap collection is read once
|
||||||
|
per resolver and kept (within the resolver's 32 MiB budget).
|
||||||
|
- Each extent's bounds error is the one the slice readers gave, reported
|
||||||
|
when the read reaches that extent, so a damaged file fails with the
|
||||||
|
same error, in the same order, through either path.
|
||||||
|
- A backend that answers a read with more bytes than asked (breaking
|
||||||
|
`read_at`'s contract) never has the extra bytes used: every read is
|
||||||
|
cut to the range asked for (`storage::exact_len`), and a short answer
|
||||||
|
inside the file is an error.
|
||||||
|
- **No behaviour change for in-memory and mapped files:** with
|
||||||
|
`as_contiguous()` every path slices the file as before (checked below).
|
||||||
|
- **Speed on local files** (provisional: tank was shared with other jobs,
|
||||||
|
load 4–16 during the runs; Criterion `local_metadata_bench`, `main`
|
||||||
|
`8f59b2e` and this branch as separate binaries, 8 alternating rounds,
|
||||||
|
best round of each). Listing the 400-group version-1 file through the
|
||||||
|
facade (`File::open`, mmap) had become 7–10% slower than `main` (8.95–9.04
|
||||||
|
vs 8.14–8.36 ms), from the M2 merge on (bisected over the merges: `main`
|
||||||
|
8.17–8.27, `2893b6c` 8.69–8.76, `93e2d5f`/`7447dce`/`4313917` the same).
|
||||||
|
The parsers were unchanged: the facade now called the generic `*_in`
|
||||||
|
entry points with the slice (`with_bytes!`), which instantiates each
|
||||||
|
parser in the facade crate, where the format crate's private helpers do
|
||||||
|
not inline (no LTO); `main` called the `&[u8]` wrappers, compiled in the
|
||||||
|
format crate. Routing just `resolve_child`/`resolve_group_children` to
|
||||||
|
the wrappers took the listing from 8.66–8.70 to 8.33–8.46 ms. Fixed in
|
||||||
|
the format crate: `ObjectHeader::parse_in`, `group_v2::{resolve_child_in,
|
||||||
|
resolve_group_children_in, resolve_path_any_in}` and
|
||||||
|
`attribute::{extract_attributes_tolerant_in, find_attribute_in}` hand a
|
||||||
|
storage with `as_contiguous()` to their non-generic slice entry point,
|
||||||
|
compiled once in the format crate; everything else goes to the same
|
||||||
|
generic core as before, so `File::open_storage` is unchanged. Best
|
||||||
|
rounds, `main` / before / after: facade listing 8.49 / 9.07 / 8.68 ms
|
||||||
|
(+2.2% on `main`, was +6.9%); `ObjectHeader::parse` ×401 24.91 / 25.90 /
|
||||||
|
25.51 µs; symbol-table nodes 1.99 / 1.94 / 1.94 µs; group B-tree walk
|
||||||
|
360.6 / 365.0 / 361.1 ns. The B-tree walk's `btree_v1.rs` is identical to
|
||||||
|
`main` and it calls only format-crate code; its earlier +5–10% (323–355
|
||||||
|
vs 353–363 ns) was run-to-run layout noise (at `2893b6c` it measured
|
||||||
|
357–374 ns against `main`'s 355–371 in the same rounds).
|
||||||
|
- Tests (2026-09-26, tank):
|
||||||
|
- `clawhdf5-format/tests/storage_equivalence.rs` now also reads every
|
||||||
|
dataset — whole, fill-aware, through a chunk cache (twice) and the
|
||||||
|
indexed path, three selections, virtual datasets with their sibling
|
||||||
|
sources, VL strings, sequences and bytes — through the read_at-only
|
||||||
|
`CountingStorage` and requires the slice results, and fails on any
|
||||||
|
`ContiguousStorageRequired`. With
|
||||||
|
`CLAWHDF5_STORAGE_CORPUS=conformance/.cache/corpus`, all 653 HDF5
|
||||||
|
files of the corpus agree (82 396 checks). The cached and indexed
|
||||||
|
paths are compared on values only when a read fails: they order
|
||||||
|
chunks by hash map, so which failing chunk a damaged dataset reports
|
||||||
|
varies between two caches even for the same slice (seen on
|
||||||
|
`cve-2025-2310.h5`; see `docs/known-issues.md`).
|
||||||
|
- A misbehaving storage (fails its N-th read; serves short reads) over
|
||||||
|
every fixture: each listing and dataset read is an error or exactly the
|
||||||
|
in-memory result, never other data (1 137 runs).
|
||||||
|
- A chunked read issues one `read_ranges` call with one range per chunk,
|
||||||
|
and a one-chunk selection one call with one range.
|
||||||
|
- `clawhdf5/tests/storage_equivalence.rs` reads every fixture (61 files)
|
||||||
|
and, with `CLAWHDF5_STORAGE_CORPUS`, every corpus file (701 files, 621
|
||||||
|
that open) through `File::open` and through `File::open_storage` over
|
||||||
|
`CountingStorage`: the tree, every attribute (all, and each by name),
|
||||||
|
every dataset's shape, types and values (all bytes, `f64`, `f32`,
|
||||||
|
`i64`, a box hyperslab, a strided one, out-of-order points, strings, VL
|
||||||
|
sequences) must be identical, and are — errors included, in full (open
|
||||||
|
errors and every read's). The one allowance is a line on which
|
||||||
|
`File::open` itself varies between two opens (the chunk cache lists a
|
||||||
|
damaged dataset's chunks in hash-map order, so which failing chunk a
|
||||||
|
full read reports varies: `cve-2025-2310.h5`), and then only if a fresh
|
||||||
|
`File::open` reproduces the storage's error. No storage read may answer
|
||||||
|
`ContiguousStorageRequired`. A storage that returns more bytes than
|
||||||
|
asked reads every fixture identically too.
|
||||||
|
It also counts what one pass — open, list, read every attribute and
|
||||||
|
every dataset once — asks of a storage with no cache: 176 092 `read_at`
|
||||||
|
calls and 208 MB for the 621 corpus files (254 MB of files); the most
|
||||||
|
are `h5stat_newgrat.h5` (35 001 groups: 92 489 calls) and
|
||||||
|
`ref_hdf5_compat1.nc` (16 062). A remote backend needs the block cache
|
||||||
|
of milestone M3. Command: `CLAWHDF5_STORAGE_CORPUS=… cargo test
|
||||||
|
--release -p clawhdf5 --test storage_equivalence -- --nocapture`.
|
||||||
|
- Conformance sweep (`conformance/run.sh --no-fetch`): 600 of 697 files
|
||||||
|
ok, `results.json` byte-identical to `8f59b2e`.
|
||||||
|
|
||||||
|
### Correctness: Fletcher-32 (2026-09-26)
|
||||||
|
- **Fletcher-32 checksums disagreed with libhdf5's on about one chunk in
|
||||||
|
32768** (fixed 2026-09-26). **Every release is affected, v2.1.0 through
|
||||||
|
v2.7.0**, both directions: `FileBuilder`/`FileWriter` (`with_fletcher32`)
|
||||||
|
and, before release, `FileEditor` wrote chunks that h5py and libhdf5
|
||||||
|
refuse ("filter returned failure during read"), and every reader
|
||||||
|
rejected valid libhdf5-written chunks with `Fletcher32Mismatch`. Our
|
||||||
|
checksum reduced its sums with `% 65535`; libhdf5's
|
||||||
|
`H5_checksum_fletcher32` uses the ones'-complement fold
|
||||||
|
`(s & 0xffff) + (s >> 16)`, which leaves 0xffff where the modulo leaves
|
||||||
|
0, so the two differ whenever a sum is a non-zero multiple of 65535.
|
||||||
|
`clawhdf5_format::checksum::fletcher32` (new, public) is a port of
|
||||||
|
`H5_checksum_fletcher32` and the only implementation; the filter writes
|
||||||
|
and verifies with it, and, as libhdf5 does, also accepts a stored
|
||||||
|
checksum with the bytes of each 16-bit half swapped (libhdf5 1.6.2 and
|
||||||
|
earlier) and the `% 65535` form v2.7.0 and earlier wrote, so their files
|
||||||
|
stay readable. Tests: `crates/clawhdf5/tests/fletcher32_interop.rs` compares
|
||||||
|
it with libhdf5's own function (through ctypes) on every 1- and 2-byte
|
||||||
|
input and 40 000 random and fold-heavy ones, and has h5py read
|
||||||
|
fold-case chunks written by `FileBuilder` and `FileEditor` and us read
|
||||||
|
h5py's. Files written by earlier releases read with a fixed build; to
|
||||||
|
make one readable by libhdf5, rewrite its Fletcher-32 datasets with a
|
||||||
|
fixed build (see `docs/known-issues.md`). `clawhdf5_accel::checksum_fletcher32` is a
|
||||||
|
different, textbook Fletcher-32 (sums start at 0xffff) and is not used
|
||||||
|
for HDF5.
|
||||||
|
|
||||||
|
### In-place editing: version-2 B-tree indexes, shrinking, dense attributes (2026-09-26)
|
||||||
|
- **`FileEditor` adds, moves and resizes chunks of datasets with two or
|
||||||
|
more unlimited dimensions** (version-2 B-tree chunk index, record types
|
||||||
|
10/11), as libhdf5's `H5B2` code does: `H5B2_update`'s insert-or-modify,
|
||||||
|
the preemptive split/redistribute loop, `split1`/`split_root` (depth
|
||||||
|
growth), `redistribute2/3`, and removal with `merge2/3`, root collapse
|
||||||
|
and the internal-record swap; node pointer widths and cumulative record
|
||||||
|
counts per depth; a missing index is created from the layout message's
|
||||||
|
parameters. After the same growth libhdf5's and the editor's trees are
|
||||||
|
node for node the same (tested through a depth increase).
|
||||||
|
- **`FileEditor::resize` shrinks** along any dimension (h5py's
|
||||||
|
`Dataset.resize` to a smaller shape), as `H5D__chunk_prune_by_extent`
|
||||||
|
does, visiting the same chunks in the same order: chunks wholly outside
|
||||||
|
the new extent leave the index (version-1 B-tree removal with libhdf5's
|
||||||
|
sibling key and link fix-ups and empty-root case, version-2 B-tree
|
||||||
|
removal, Fixed/Extensible Array elements reset; an implicit index keeps
|
||||||
|
its chunks, as in libhdf5) and their space is freed; the part of a
|
||||||
|
partial edge chunk outside the extent is overwritten with the fill value,
|
||||||
|
so it reads as fill after a later growth. Growth under early allocation
|
||||||
|
now allocates and fills the new chunks (`H5D__chunk_allocate`), which an
|
||||||
|
implicit index needs. Shrinking was `Error::Unsupported`. Only the
|
||||||
|
chunks that exist are visited (placed in libhdf5's order), so shrinking
|
||||||
|
a sparse dataset costs memory and time in its chunks, not in the
|
||||||
|
coordinates cut off (a 2 x 10^12-coordinate shrink takes 0.6 s).
|
||||||
|
- **`FileEditor::set_attr` handles dense attribute storage and creation
|
||||||
|
order**: objects that track (and index) attribute creation order; the
|
||||||
|
move to dense storage when an object reaches its compact limit (or an
|
||||||
|
attribute is too large for a header message), as `H5O__attr_create`
|
||||||
|
does it (new fractal heap, name index, creation-order index when
|
||||||
|
indexed, compact attributes moved over in header order); objects
|
||||||
|
already in dense storage (h5py- or clawhdf5-written): insertion,
|
||||||
|
same-size rewrites in place, other replacements by removal and
|
||||||
|
insertion. The heap is changed as `H5HF` changes it — best-fit free
|
||||||
|
sections from its free-space manager (kept as libhdf5 keeps `FSHD`/
|
||||||
|
`FSSE`), new direct blocks through the root indirect block (created,
|
||||||
|
doubled), blocks too small for an attribute skipped as libhdf5 skips
|
||||||
|
them (`H5HF__hdr_skip_blocks`: an indirect free section with its row
|
||||||
|
sections, serialized as libhdf5 serializes them, merged with the range
|
||||||
|
skipped just before it, and later attributes given skipped blocks from
|
||||||
|
either end or the middle of a range, which splits it), huge objects
|
||||||
|
through the huge-object B-tree (deleted with the last huge object),
|
||||||
|
removed objects' space merged back — with libhdf5's statistics: after
|
||||||
|
the same attribute workload the heap, its free space and both index
|
||||||
|
B-trees equal libhdf5's (`dense_skipped_blocks_match_libhdf5` covers
|
||||||
|
every way of skipping, with libhdf5 doing one edit per session as the
|
||||||
|
editor does). In a random attribute workload (1-4 KiB attributes among
|
||||||
|
small ones) 24% of `set_attr` calls were refused before skipping was
|
||||||
|
implemented; 2.2% are now, all replacements of the last attribute in a
|
||||||
|
heap block. Attributes are encoded as libhdf5
|
||||||
|
encodes them for a file h5py opens `r+` (message version 1, 3 for
|
||||||
|
non-ASCII names; simple dataspaces with their maximum dimensions).
|
||||||
|
Still refused: see `docs/known-issues.md`.
|
||||||
|
- **Freed space is reused within an editing session.** A `FileEditor`
|
||||||
|
reuses (best fit, zeroed) what its earlier edits freed — moved filtered
|
||||||
|
chunks, pruned chunks, merged B-tree nodes, replaced heap blocks — never
|
||||||
|
what the current edit frees, and writes reused blocks with the new space
|
||||||
|
before any existing byte changes. `FileEditor::reusable_bytes`. The
|
||||||
|
append workload of `measure_append_waste` leaks less (sizes in
|
||||||
|
`docs/known-issues.md`).
|
||||||
|
- **Reader: implicit chunk indexes below their maximum shape.** libhdf5
|
||||||
|
places an implicit index's chunks by their position in the *maximum*
|
||||||
|
chunk grid; the reader used the current grid and returned other chunks'
|
||||||
|
values from the second chunk row on (h5py early allocation with a fixed
|
||||||
|
`maxshape` larger than the shape).
|
||||||
|
`chunked_read::generate_implicit_chunks_in_grid` takes the maximum.
|
||||||
|
- **Reader: object headers with long continuation chains.** A version-1
|
||||||
|
header whose continuation chunks chain more than 32 deep (a header that
|
||||||
|
gains a chunk per attribute added when full, as libhdf5 and the editor
|
||||||
|
grow it) was refused with `NestingDepthExceeded`; version-2 headers
|
||||||
|
stopped at 256 chunks. Chunks are now read one at a time from a queue,
|
||||||
|
in the order their continuation messages are found (libhdf5's
|
||||||
|
`H5O_protect` order, which the editor already used; a version-1
|
||||||
|
chunk's messages used to be inserted at its continuation message), each
|
||||||
|
buffer released before the next is read; a chunk address seen twice (a
|
||||||
|
cycle), chunks adding up to more than the file (a crafted chain of
|
||||||
|
chunks nested in each other made storage with owned buffers read and
|
||||||
|
hold the square of the file's size), or more than 65 536 chunks are
|
||||||
|
refused, so a header's chunks read at most the file's size.
|
||||||
|
The first version allocated a queue and a set of chunk starts for every
|
||||||
|
header and made `ObjectHeader::parse` of 401 small headers 1.8x slower
|
||||||
|
(`local_metadata_bench`: 45.7 vs 24.8 µs on `main` `8f59b2e`). The first
|
||||||
|
8 chunks now live in an inline array (cycle check by scan; only a longer
|
||||||
|
header allocates), and the per-chunk message loop is its own function
|
||||||
|
instead of being inlined into the generic parser. Provisional (tank load
|
||||||
|
3–4; Criterion, separate binaries, 2 alternating rounds): 24.96–25.12 vs
|
||||||
|
24.85–25.15 µs on `main`.
|
||||||
|
- Tests: `crates/clawhdf5-tools/tests/edit_coverage_interop.rs` (h5py
|
||||||
|
`earliest`/`v110`/`latest` and clawhdf5-written files; structure
|
||||||
|
comparisons with libhdf5 for version-2 B-trees, shrink on every index,
|
||||||
|
and dense attribute heaps); the random-operation property test in
|
||||||
|
`edit_interop.rs` now shrinks, grows two unlimited dimensions and moves
|
||||||
|
attributes to dense storage (`CLAWHDF5_EDIT_SEED` for other seeds).
|
||||||
|
|
||||||
|
### Name lookups through the name index (2026-09-26)
|
||||||
|
- **Finding one link or attribute by name reads the name index, not every
|
||||||
|
entry.** In a dense group (links in a fractal heap) the v2 B-tree name
|
||||||
|
index (record type 5, lookup3 hash of the name) is descended to the
|
||||||
|
records with the name's hash and only their links are read — O(log n)
|
||||||
|
instead of all n. Path resolution (`File::dataset`, `resolve_path_any`,
|
||||||
|
soft-link targets) and `Group::dataset`/`Group::group` (on `File`,
|
||||||
|
`MmapFile` and `LazyFile`, which listed the whole group per call) use it;
|
||||||
|
names whose hashes collide are all compared, so the order libhdf5 gives
|
||||||
|
them does not matter. New `clawhdf5_format::group_v2::resolve_child`,
|
||||||
|
`btree_v2::find_btree_v2_records` (records in one key range), and a
|
||||||
|
`lookup-stats` feature counting heap objects read, for tests. Huge heap
|
||||||
|
objects are found through their index the same way.
|
||||||
|
- **`attr(name)`** on the facade's groups and datasets (all three file
|
||||||
|
types): one attribute, found in dense storage through its name index
|
||||||
|
(record type 8) instead of reading every attribute
|
||||||
|
(`clawhdf5_format::attribute::find_attribute_in_file`).
|
||||||
|
- **Two links of one name: the first wins everywhere.** A group cannot
|
||||||
|
validly hold two links of one name, but a damaged or hand-made one can.
|
||||||
|
The listing, `resolve_child` (`Group::dataset`/`group`) and path
|
||||||
|
resolution now all use only the first link of a name (header message
|
||||||
|
order in a compact group, name index order in a dense one) and ignore
|
||||||
|
the rest, even if the first dangles — libhdf5's rule for compact groups
|
||||||
|
(h5py fails to open a dangling first link although a later one
|
||||||
|
resolves). Before, the listing skipped a dangling first link and listed
|
||||||
|
the name via a later one that lookup did not follow, and path resolution
|
||||||
|
followed the last.
|
||||||
|
- **B-tree v2 internal nodes are checksum-verified.** Lookups prune
|
||||||
|
children by internal-node keys, so a corrupted internal node could hide
|
||||||
|
a name with no error; a mismatch is now `ChecksumMismatch`, as in
|
||||||
|
libhdf5, for lookups and listings alike.
|
||||||
|
- **`Group::entries()` and `File::group_at(address)`**: a listing's
|
||||||
|
`(name, address)` pairs, to open children without looking names up again.
|
||||||
|
- Test: `crates/clawhdf5/tests/indexed_lookup_interop.rs` — every child of
|
||||||
|
an h5py-written 35 001-link group (with colliding hashes) opened by name
|
||||||
|
reads at most two links per lookup (before: 35 001), matches h5py, and
|
||||||
|
every link kind (soft, relative, dangling, external) resolves as h5py
|
||||||
|
resolves it in dense and compact groups.
|
||||||
|
|
||||||
|
### Checked address conversion (2026-09-26)
|
||||||
|
- **No 64-bit file value is truncated on a 32-bit target.** All 119
|
||||||
|
truncating `u64 as usize` casts in `clawhdf5-format` that clippy's
|
||||||
|
`cast_possible_truncation` reports, under every feature the crate is
|
||||||
|
built with in CI except `szip` (115 with default features and
|
||||||
|
`plugin-filters`, 4 more behind `parallel`), are gone: file addresses,
|
||||||
|
lengths and counts go through `addr::to_usize`, which fails with
|
||||||
|
`FormatError::Overflow` where the value does not fit (wasm32 and other
|
||||||
|
32-bit targets; it used to wrap onto another part of the file), and
|
||||||
|
in-memory counts through `addr::saturating_usize`. On 64-bit targets
|
||||||
|
nothing changes. `scripts/check-32bit-casts.sh` (run by `ci-test.sh`)
|
||||||
|
lints the crate with no default features, with default features, and
|
||||||
|
with every optional feature but `szip` (for wasm32; the set with `zstd`,
|
||||||
|
which does not build for wasm32, for the host), and fails on any new
|
||||||
|
truncating cast. The facade, `clawhdf5-io` and `clawhdf5-ann` are not
|
||||||
|
covered.
|
||||||
|
|
||||||
|
### Range reads, milestone M1: the `Storage` trait (2026-09-26)
|
||||||
|
- **Breaking: `clawhdf5_format::error::FormatError` and `clawhdf5::Error`
|
||||||
|
are now `#[non_exhaustive]`.** An exhaustive `match` on either needs a
|
||||||
|
wildcard arm. `FormatError` has two new variants: `Storage(String)` (a
|
||||||
|
storage backend failed to serve a read) and
|
||||||
|
`ContiguousStorageRequired(&'static str)` (an operation not yet converted
|
||||||
|
to range reads was asked of a backend without the whole file in memory).
|
||||||
|
- New `clawhdf5_format::storage::Storage`, the synchronous, `no_std` read
|
||||||
|
interface of `docs/design/range-reads.md` option (a): `read_at(offset:
|
||||||
|
u64, len) -> Cow<[u8]>`, `read_ranges`, `len()` and an `as_contiguous()`
|
||||||
|
fast path; implemented for `[u8]`, `Vec<u8>`, and references, `Box`es
|
||||||
|
and (with `std`) `Arc`s of a `Storage`. Slices and `Vec`s serve borrowed
|
||||||
|
bytes, so parsing an in-memory file costs no copy.
|
||||||
|
- **The metadata parsers read through `Storage`.** Each converted parser has
|
||||||
|
an `*_in<S: Storage + ?Sized>(&S, ..)` core (a `&dyn Storage` works too),
|
||||||
|
and its `&[u8]` function is now a thin wrapper over it, so no caller
|
||||||
|
changes. The wrappers compile to a `[u8]` instance of the same code, so a
|
||||||
|
structure read in memory is a bounds check and a borrowed slice, with no
|
||||||
|
indirect call and no copy. Converted: the superblock
|
||||||
|
(`Superblock::parse_in`), its extension and cache image
|
||||||
|
(`read_superblock_extension_in`, `cache_image_state_in`), object headers
|
||||||
|
with their continuation chunks (`ObjectHeader::parse_in`), local and
|
||||||
|
global heaps, symbol-table nodes and the group B-tree (v1), fractal heaps,
|
||||||
|
fixed and extensible array chunk indexes, shared messages and the SOHM
|
||||||
|
table (`message_data_in`, `message_data_with_sohm_in`,
|
||||||
|
`load_sohm_table_in`, …), attributes (`extract_attributes_full_in`,
|
||||||
|
`extract_attributes_tolerant_in`, `AttributeMessage::parse_in_storage`),
|
||||||
|
fill values (`dataset_fill_value_from_storage`) and virtual-dataset
|
||||||
|
mappings (`DataLayout::resolve_vds_mappings_in`); also
|
||||||
|
`signature::find_signature_in`. Each structure is read with bounded reads
|
||||||
|
(a prefix, then the structure) instead of slicing the whole file; the
|
||||||
|
open-ended `&file_data[addr..]` slices in these modules are gone. Bounds
|
||||||
|
errors keep their values (absolute position, file length).
|
||||||
|
- Reads sized by untrusted fields are bounded by what the parser uses, so
|
||||||
|
a crafted size cannot turn one structure into a read of the rest of the
|
||||||
|
file on a range backend: local-heap names are read in growing pieces
|
||||||
|
(64 bytes first) rather than to the end of the data segment; a fractal
|
||||||
|
heap indirect block is read up to the entry covering the object (the
|
||||||
|
whole block only when that entry is unallocated); paged fixed and
|
||||||
|
extensible array data blocks over 1 MiB are read page by page, only the
|
||||||
|
pages in use; and a block under one checksum whose claimed size runs
|
||||||
|
past the end of the file fails its bounds check before any read (with
|
||||||
|
the `checksum` feature). An object header's prefix is one read (was
|
||||||
|
two).
|
||||||
|
- Structures still indexed by a v2 B-tree — dense attribute storage, a SOHM
|
||||||
|
B-tree index and huge fractal-heap objects found through their B-tree —
|
||||||
|
are not converted yet (the
|
||||||
|
v2 B-tree and dense groups come with milestone M3); over a backend without
|
||||||
|
the whole file in memory they are the clean `ContiguousStorageRequired`
|
||||||
|
error, never a partial result. Raw data, chunk B-tree (v1) indexes and VL
|
||||||
|
data are milestone M2. (All of them read through `Storage` since M2,
|
||||||
|
above.)
|
||||||
|
- **No behaviour change**, checked three ways (2026-09-26, tank): every
|
||||||
|
existing test passes unchanged; the conformance sweep
|
||||||
|
(`conformance/run.sh --no-fetch`) gives a byte-identical `results.json`
|
||||||
|
at `f2ff2c4` and on this branch, and identical per-file probe output for
|
||||||
|
696 of the 697 files — the exception, `cve-2025-2310.h5`, reports one of
|
||||||
|
two errors depending on which parallel chunk decode fails first, at
|
||||||
|
`f2ff2c4` as on this branch; and a transcript of every converted `&[u8]`
|
||||||
|
function's result over the fixtures, the conformance corpus and the
|
||||||
|
h5py-written files below (748 files, 7 603 object headers) is
|
||||||
|
byte-identical between the two builds.
|
||||||
|
- **Speed on local files** (provisional: tank was shared with other jobs;
|
||||||
|
both builds linked into one binary and timed alternately, 200 rounds;
|
||||||
|
new Criterion bench `clawhdf5/benches/local_metadata_bench.rs` over a
|
||||||
|
400-group version-1 file, `clawhdf5-format/tests/fixtures/v1_groups_400.h5`):
|
||||||
|
against `f2ff2c4`, listing the file through the facade is 2.7% faster,
|
||||||
|
`ObjectHeader::parse` is within ±1%, symbol-table nodes and the group
|
||||||
|
B-tree walk are about 19% faster (their entry loops were tightened),
|
||||||
|
local-heap names and `resolve_group_children` 1.5–3% faster. The same
|
||||||
|
harness run on two copies of the old code differs by up to 2%. The
|
||||||
|
Criterion bench itself, old and new as separate binaries run alternately
|
||||||
|
(3 rounds), agrees except for `ObjectHeader::parse`, which it puts about
|
||||||
|
7% slower (25.9 vs 24.1 µs for 401 headers) while the listing that
|
||||||
|
parses those headers is 5–8% faster. **Rechecked on an idle tank
|
||||||
|
(2026-09-26, load 1.95 at the start, 3–4 during; Criterion, `main`
|
||||||
|
`479d8b4` vs this branch `b49ec39` as separate binaries, 2 alternating
|
||||||
|
rounds):** `ObjectHeader::parse` for 401 headers 24.0–24.1 µs → 25.5–25.8
|
||||||
|
µs (+6–7%, about 4 ns per header — real, not noise); symbol-table nodes
|
||||||
|
2.39 → 1.94–2.04 µs (−17%); group B-tree walk 405–411 → 322–351 ns
|
||||||
|
(−16%); listing the 400-group file through the facade 8.56–8.71 →
|
||||||
|
8.40–8.42 ms (−2.4%). Net, local metadata reads are slightly faster; the
|
||||||
|
per-header cost is a known, small regression. Data reads are unchanged:
|
||||||
|
`concurrent_read --decode-threads 1` (the `BENCHMARKS.md` "Concurrent
|
||||||
|
reads" workload), `main` and this branch alternating, 2 rounds each, on
|
||||||
|
the same idle start: every deflate and contiguous full-read row within
|
||||||
|
±2.4% of `main` (the cache-bound contiguous hyperslab rows vary by up to
|
||||||
|
±40% between `main`'s own rounds and are not comparable).
|
||||||
|
- New equivalence harness `clawhdf5-format/tests/storage_equivalence.rs`:
|
||||||
|
every converted parser runs over the file as a slice and over
|
||||||
|
`storage::CountingStorage` — a `Storage` that serves an in-memory buffer
|
||||||
|
through `read_at` only (`as_contiguous()` is `None`), copying what it
|
||||||
|
serves and counting reads — and must give identical results. It walks
|
||||||
|
the fixtures, files h5py writes for it (extensible arrays with super
|
||||||
|
blocks and paged data blocks, paged fixed arrays, large v1 and dense
|
||||||
|
groups, a user block, SOHM list and B-tree indexes, dense, shared and
|
||||||
|
committed-type attributes, fixed and extensible array data blocks over
|
||||||
|
1 MiB, whole and truncated), and with `CLAWHDF5_STORAGE_CORPUS=<dir>` a
|
||||||
|
corpus (all 653 HDF5 files of the conformance corpus pass). Only the
|
||||||
|
structures listed above as not converted may answer
|
||||||
|
`ContiguousStorageRequired`, and only in the checks that reach them; a
|
||||||
|
converted parser falling back to the whole file fails it. Milestones M2
|
||||||
|
and M3 extend it.
|
||||||
|
|
||||||
|
### ZFP (2026-09-26)
|
||||||
|
- **ZFP (filter 32013, H5Z-ZFP) reads, in pure Rust.** It was the last
|
||||||
|
filter in the conformance corpus that failed with `UnsupportedFilter`.
|
||||||
|
New feature `zfp` (`clawhdf5-format` and `clawhdf5`, included in
|
||||||
|
`plugin-filters`, no dependencies) ports the zfp 1.0.1 decoder and the
|
||||||
|
decompression half of H5Z-ZFP 1.1.1: every mode (fixed rate, fixed
|
||||||
|
precision, fixed accuracy, reversible, expert), int32, int64, float and
|
||||||
|
double, 1-4-D fields with partial blocks, and headers written by
|
||||||
|
big-endian machines (values byte-swapped as H5Z-ZFP does). Read only:
|
||||||
|
there is no ZFP encoder. The decoder is deterministic, so lossy modes have
|
||||||
|
one right answer, and clawhdf5 returns exactly libzfp's values.
|
||||||
|
- Tests: `crates/clawhdf5/tests/zfp_interop.rs` has h5py + hdf5plugin 7.1
|
||||||
|
(H5Z-ZFP 1.1.1, zfp 1.0.1) write 2205 datasets over 16 mode settings
|
||||||
|
(including expert parameters at their edges) x the four types x 1-4-D
|
||||||
|
shapes with partial edge chunks, partial blocks and unit chunk
|
||||||
|
dimensions x smooth, noisy, wide-range, zero and inf/NaN data; each must
|
||||||
|
read byte for byte as h5py reads it. `tests/zfp_alloc_bounds.rs` fuzzes
|
||||||
|
headers and streams under a counting allocator: no panics, and the output
|
||||||
|
is allocated only when it matches the chunk's size and the stream holds at
|
||||||
|
least a bit per block. A stream that ends before the decoder is done is an
|
||||||
|
error (libzfp reads past its buffer).
|
||||||
|
- Conformance on tank (2026-09-26, `conformance/run.sh --no-fetch`): 600 of
|
||||||
|
697 files ok (599 before); `h5ex_d_zfp.h5` now reads.
|
||||||
|
|
||||||
|
### In-place modification (2026-09-26)
|
||||||
|
- **`clawhdf5::FileEditor` modifies an existing file where it lies.**
|
||||||
|
`FileBuilder` builds whole files in memory; the editor opens a file
|
||||||
|
written by libhdf5 (any `libver`, including HDF5 2.0's own format) or by
|
||||||
|
clawhdf5 and changes only what an edit touches, recomputing the checksum
|
||||||
|
of every structure it changes. It takes an exclusive `flock` on the file
|
||||||
|
(the lock libhdf5 takes), so a second editor gets `Error::Locked`.
|
||||||
|
- `write_selection` / `write_all` / `write_values`: overwrite values of a
|
||||||
|
compact, contiguous (also never-written, late-allocated) or chunked
|
||||||
|
dataset, in its own datatype, under any selection. Chunks are decoded,
|
||||||
|
updated and re-encoded through the dataset's filters; a chunk that no
|
||||||
|
longer fits moves to the end of the file. New chunks are added to
|
||||||
|
version-1 B-tree (every chunked dataset of h5py's default `libver`),
|
||||||
|
Extensible Array, Fixed Array and single-chunk indexes — creating the
|
||||||
|
index, its data blocks, super blocks and pages, and splitting B-tree
|
||||||
|
nodes, as libhdf5 does: after the same sequence of writes the B-tree has
|
||||||
|
the same number of nodes per level and the Extensible Array header the
|
||||||
|
same block statistics as libhdf5's (tested). Filters run as libhdf5's
|
||||||
|
`H5Z_pipeline` runs them (new
|
||||||
|
`clawhdf5_format::filters::compress_chunk_masked`): an optional filter
|
||||||
|
that fails — LZF or Blosc output no smaller than the chunk — is skipped
|
||||||
|
and its filter-mask bit set, so the chunk is stored exactly as h5py
|
||||||
|
stores it; a mandatory filter that fails fails the edit. (Storing such
|
||||||
|
a chunk LZF-encoded at the raw size with a clear mask let a later
|
||||||
|
libhdf5 rewrite of it keep the stale mask, and h5py could no longer
|
||||||
|
read the dataset.)
|
||||||
|
- `resize`: grow a chunked dataset up to its maximum dimensions (h5py's
|
||||||
|
`Dataset.resize`).
|
||||||
|
- `set_attr`: add or replace an attribute in an object header, in free
|
||||||
|
space or in a new continuation chunk at the end of the file. A
|
||||||
|
version-2 header (h5py `libver='v110'` and later) without an Attribute
|
||||||
|
Info message gets one, as libhdf5's `H5O__attr_create` adds it: libhdf5
|
||||||
|
counts such a header's attributes through that message, and without it
|
||||||
|
h5py reported `len(obj.attrs) == 0` while listing them.
|
||||||
|
- Each edit is planned in memory and refused as a whole
|
||||||
|
(`Error::Unsupported`, file untouched) when any part is not supported:
|
||||||
|
new chunks in a version-2 B-tree index (two or more unlimited
|
||||||
|
dimensions) or an implicit index, shrinking, variable-length and
|
||||||
|
reference data, chunks through a filter this build cannot encode
|
||||||
|
(scale-offset, N-Bit, SZIP), attributes in dense storage, past an
|
||||||
|
object's compact limit or with tracked creation order, files with a
|
||||||
|
metadata cache
|
||||||
|
image, paged or persistent free space, or marked open by another
|
||||||
|
writer. New error variants `Error::Unsupported`,
|
||||||
|
`Error::InvalidArgument`, `Error::Locked`, and `clawhdf5::Error` is now
|
||||||
|
`#[non_exhaustive]` — a breaking change for code that matches it
|
||||||
|
exhaustively (the Python bindings map the new variants to
|
||||||
|
`NotImplementedError`, `ValueError` and `OSError`).
|
||||||
|
- Durability: the new space (chunks, index blocks) is written and synced
|
||||||
|
before any existing byte changes, then the metadata that links it in,
|
||||||
|
then a second sync. There is no journal: a crash during the second
|
||||||
|
phase can leave the file inconsistent (as with libhdf5 without SWMR).
|
||||||
|
Freed space is not reused (see `docs/known-issues.md`; since reused
|
||||||
|
within an editing session, above).
|
||||||
|
- Tests: `crates/clawhdf5-tools/tests/edit_interop.rs` (h5py `earliest`,
|
||||||
|
`v114` and `latest` files and clawhdf5 files; after every round h5py
|
||||||
|
reads the expected values, h5dump and `h5rs check --data` accept the
|
||||||
|
file, and h5py `r+` modifies it further; random operations against a
|
||||||
|
model) and `crates/clawhdf5/tests/edit_tests.rs`.
|
||||||
|
- `clawhdf5_format::type_builders::build_attr_message` is public.
|
||||||
|
|
||||||
|
### Chunked full reads (2026-09-26)
|
||||||
|
- **Chunks are decoded straight into the output, into reused buffers.** A
|
||||||
|
full read of a chunked dataset faulted in about three times its size in
|
||||||
|
fresh pages: every chunk was decoded into a new buffer per filter stage
|
||||||
|
(the cached reader behind `read_*` decoded 128 chunks at a time before
|
||||||
|
placing any; the uncached one behind `MmapFile`, `LazyFile` and
|
||||||
|
`verify_provenance` decoded the whole dataset first), then assembled into
|
||||||
|
a byte buffer, which the typed readers copied once more. Now each chunk
|
||||||
|
is decoded into buffers the thread keeps between chunks and reads
|
||||||
|
(`clawhdf5_format::filters::DecodeScratch`,
|
||||||
|
`decompress_chunk_exact_with`: deflate inflates into a kept buffer with a
|
||||||
|
reset inflater, shuffle into the other one, Fletcher32 is checked in
|
||||||
|
place; other filters go through the registry as before) and copied
|
||||||
|
directly to its place in the output. Chunks still go into the file's
|
||||||
|
chunk cache when the whole dataset fits. Selection reads decode the
|
||||||
|
chunks they touch the same way.
|
||||||
|
- **Typed full reads of chunked data skip the byte buffer.** `read_f32`,
|
||||||
|
`read_f64`, `read_i32`, `read_i64` and `read_u64` (on `File`, `MmapFile`
|
||||||
|
and `LazyFile`) of a chunked dataset stored as that type in native byte
|
||||||
|
order decode every chunk into the returned `Vec` (huge-page backed when
|
||||||
|
large, like the byte readers' output); other types and byte orders
|
||||||
|
convert as before. New public
|
||||||
|
`clawhdf5_format::data_read::read_chunked_native`.
|
||||||
|
- **Reading threads no longer wait for a busy rayon pool.** A full read
|
||||||
|
handed its chunks to rayon and the calling thread slept until the pool had
|
||||||
|
decoded them, so with a small pool (2-4 threads) readers outside it queued
|
||||||
|
behind its workers. The calling thread now decodes too, and pool workers
|
||||||
|
join in only when free; a helper the pool starts after the read has
|
||||||
|
finished returns at once. A single read still spreads over the default
|
||||||
|
pool. This replaces the one-thread-pool special case below for full
|
||||||
|
reads. Chunks are placed from several threads only when the chunk index
|
||||||
|
puts them on the chunk grid at distinct places; a corrupt index is read
|
||||||
|
one chunk at a time, and the error reported is still the first failing
|
||||||
|
chunk's. New test `crates/clawhdf5/tests/busy_decode_pool.rs`.
|
||||||
|
- **Fixed:** in a filtered dataset, a chunk stored with every filter skipped
|
||||||
|
(filter mask) and shorter than a chunk read with zeros in place of its
|
||||||
|
missing part through `File`'s `read_*`; it is now an error naming the
|
||||||
|
chunk, as `MmapFile`/`LazyFile` already made it.
|
||||||
|
- New h5py comparison `crates/clawhdf5/tests/chunked_read_paths_interop.rs`:
|
||||||
|
every chunked read path (cached and uncached full reads, `MmapFile`,
|
||||||
|
`LazyFile`, small, strided and point selections, with and without the
|
||||||
|
`parallel` feature) for 1-8-byte integers and 2-8-byte floats in both
|
||||||
|
byte orders, through deflate, shuffle, Fletcher32, LZF, SZIP and Blosc,
|
||||||
|
with partial edge chunks, sparse datasets and fill values, and datasets
|
||||||
|
larger than the chunk cache.
|
||||||
|
|
||||||
|
### Writer: large dense indexes (2026-09-26)
|
||||||
|
- **`track_order` orders attributes too, as h5py's `track_order=True`
|
||||||
|
does.** It tracked link creation order only, so h5py listed a tracked
|
||||||
|
object's attributes by name. A tracking object's header now has the
|
||||||
|
attribute creation order tracked and indexed flags, an Attribute Info
|
||||||
|
message with the next creation order (also for inline attributes), and a
|
||||||
|
creation order on each inline attribute message; dense attribute storage
|
||||||
|
gets a creation-order index (B-tree type 9). `FileWriter::track_order` /
|
||||||
|
`FileBuilder::track_order` now apply to datasets' attributes as well, and
|
||||||
|
`DatasetBuilder::track_order` sets it per dataset. Groups and datasets
|
||||||
|
that track order are written differently from before; others are
|
||||||
|
unchanged. More than 65 535 attributes on a tracking object is an error
|
||||||
|
(libhdf5's creation order counter is 2 bytes). The reader
|
||||||
|
(`attribute::extract_attributes*`) lists a tracking object's attributes
|
||||||
|
in creation order. Test `track_order_lists_attributes_in_creation_order`
|
||||||
|
(h5py lists, reads and extends them in "r+" mode, including libhdf5's
|
||||||
|
move from inline to dense storage).
|
||||||
|
- **No more 65 535-record limit on the writer's v2 B-trees.** Dense link
|
||||||
|
storage (name index and creation-order index), dense attribute storage
|
||||||
|
and the chunk index of datasets with more than one unlimited dimension
|
||||||
|
were written as a single leaf, so a group with more than 65 535 links, an
|
||||||
|
object with more than 65 535 dense attributes, or such a dataset with more
|
||||||
|
than 65 535 chunks was an error. The writer now builds internal nodes to
|
||||||
|
any depth (`clawhdf5_format::btree_v2_write`), with node capacities and
|
||||||
|
child-pointer widths from the same arithmetic as libhdf5's
|
||||||
|
`H5B2__hdr_init` (shared with the reader, `btree_v2::node_info`) and
|
||||||
|
libhdf5's node sizes (512 bytes for dense storage, 2048 for chunks).
|
||||||
|
Indexes that fit the old one-leaf layout are written byte for byte as
|
||||||
|
before. New tests `crates/clawhdf5/tests/deep_btree_interop.rs` (100 000
|
||||||
|
links, 70 000 attributes, 200 000 chunks; h5py, h5dump, clawhdf5, and
|
||||||
|
h5py "r+" edits) and `check_files_with_deep_btrees` (`h5rs check`).
|
||||||
|
- **Links and attributes whose name hashes collide are found by name.** The
|
||||||
|
dense name indexes (a group's links: B-tree v2 type 5; an object's
|
||||||
|
attributes: type 8) are ordered by the name's lookup3 hash and, when two
|
||||||
|
hashes are equal, by the name itself, as libhdf5 compares them. The writer
|
||||||
|
broke ties by insertion order, so libhdf5 could not open one of two
|
||||||
|
colliding names (`"k69209"` and `"k155448"` share hash `0x3a0b13e6`;
|
||||||
|
collisions are likely from about 77 000 names). Regression test
|
||||||
|
`names_whose_hashes_collide_are_found_by_name` in
|
||||||
|
`crates/clawhdf5/tests/writer_groups_interop.rs`.
|
||||||
|
|
||||||
|
### Blosc2 (2026-09-26)
|
||||||
|
- **Blosc2 (filter 32026) reads, in pure Rust.** Files written with
|
||||||
|
hdf5plugin's `Blosc2` failed with `UnsupportedFilter`. New feature
|
||||||
|
`blosc2` (`clawhdf5-format` and `clawhdf5`, included in `plugin-filters`)
|
||||||
|
decodes the Blosc2 contiguous frame hdf5-blosc2 stores per chunk, the
|
||||||
|
B2ND arrays it uses for chunks of 2 or more dimensions (blocks gathered
|
||||||
|
back into C order), Blosc2 chunks with their special values (zeros, NaN,
|
||||||
|
uninitialised, one repeated value), and the shuffle, bit-shuffle, delta
|
||||||
|
and truncate-precision filters, over the BloscLZ, LZ4/LZ4HC, Zlib and
|
||||||
|
Zstandard codecs shared with Blosc 1. Read only: there is no Blosc2
|
||||||
|
encoder. Dictionaries, lazy chunks, variable-length blocks, user-defined
|
||||||
|
codecs and registered Blosc2 filters (e.g. bytedelta) are errors;
|
||||||
|
uninitialised chunks read as zeros. Tested against h5py 3.16 +
|
||||||
|
hdf5plugin 7.1 (every codec, filter and level 0-9; 1- to 5-D chunks with
|
||||||
|
partial edge chunks; every integer width and f4/f8; datasets of zeros,
|
||||||
|
one value and NaN; Fletcher32 before Blosc2) and against frames from
|
||||||
|
python-blosc2 4.13.1 for what hdf5plugin never writes
|
||||||
|
(`crates/clawhdf5-format/tests/fixtures/blosc2/`); the decoder is
|
||||||
|
fuzzed. Conformance: 576 of 697 files ok (was 575) — h5ex_d_blosc2.
|
||||||
|
- **A crafted Blosc2 chunk cannot allocate more than a few times its HDF5
|
||||||
|
chunk size.** Found in review before release: the sizes a frame declares
|
||||||
|
sized the decoder's buffers. A 173-byte frame whose offsets chunk claimed
|
||||||
|
2 GiB was decoded in full for a 1 MiB chunk; an empty chunk allocated its
|
||||||
|
declared block size twice (about 1 GiB); a B2ND chunk was decoded whole
|
||||||
|
with its padding (up to 16x the chunk); and a Zstandard stream's declared
|
||||||
|
window (up to 100 MiB) was reserved as the decoder was reused, which also
|
||||||
|
affected Blosc 1 and bitshuffle with Zstandard. Now the offsets chunk is
|
||||||
|
capped at the chunk size, block sizes are clamped to the chunk, B2ND
|
||||||
|
blocks are placed as they are decoded (padding is never held), and the
|
||||||
|
Zstandard window is capped at twice the stream's output (at least
|
||||||
|
128 KiB). A B2ND chunk may no longer be larger than its array, which
|
||||||
|
hdf5-blosc2 never writes. `tests/blosc2_alloc_bounds.rs` measures peak
|
||||||
|
allocation for these frames and for 45,000 fuzzed ones: at most 6x the
|
||||||
|
chunk size, twice the input and 2 MiB of Zstandard state.
|
||||||
|
|
||||||
|
### Remaining conformance errors (2026-09-26)
|
||||||
|
Conformance on tank, `conformance/run.sh --no-fetch`: 598 of 697 files
|
||||||
|
ok (575 before). Of the 5 our-errors left, 3 are corrupt data HDF5 2.0
|
||||||
|
reads only through a bug (listed in `CONFORMANCE.md`), 2 are the Blosc2 and
|
||||||
|
ZFP filters; the 2 mismatches are the known h5py big-endian VL bug. The
|
||||||
|
five files whose cache image libhdf5 cannot load (`cve-2025-6269-*`,
|
||||||
|
`cve-2025-6516`) count as ok because the library, like libhdf5, opens them
|
||||||
|
and fails their objects (see below).
|
||||||
|
- **Metadata cache images are read.** A file written with a metadata cache
|
||||||
|
image keeps its metadata cache entries in an image block the superblock
|
||||||
|
extension points at, and libhdf5 reads them in place of the file's own
|
||||||
|
bytes; in `h5clear_mdc_image.h5` the root group exists only there, and
|
||||||
|
every reader failed with `InvalidObjectHeaderVersion(0)`. `File`,
|
||||||
|
`MmapFile` and `LazyFile` (and `h5rs`) now apply the image at open
|
||||||
|
(`clawhdf5_format::superblock_ext::CacheImage`), with libhdf5's checks.
|
||||||
|
The file is not copied to do it: a mapped file gets the image's entries
|
||||||
|
written into a private copy-on-write mapping
|
||||||
|
(`clawhdf5_io::HDF5Read::private_copy`, `MAP_PRIVATE`), so only the pages
|
||||||
|
they land on are copied, and a buffer the opener owns (`File::from_bytes`,
|
||||||
|
`open_buffered`) is patched in place; files without an image are read
|
||||||
|
from the mapping exactly as before. (An interim version copied the whole
|
||||||
|
file onto the heap: 2 GB of memory to open a 1 GiB sparse file with an
|
||||||
|
image, and an abort for an 8 GiB one; `tests/cache_image_memory.rs`
|
||||||
|
guards it.) An image entry that runs past the end of file is refused
|
||||||
|
(libhdf5 checks only its start; the images it writes never do this). A
|
||||||
|
file whose image libhdf5 cannot load (`cve-2025-6269-*`, `cve-2025-6516`)
|
||||||
|
opens, as in libhdf5, and every object lookup fails with the image's
|
||||||
|
error (`File`, `MmapFile`; `LazyFile` reads the root group at open, so
|
||||||
|
its open fails). libhdf5 fails only its first metadata read and then
|
||||||
|
reads the file's own, possibly stale, bytes; those are never read here.
|
||||||
|
An interim version refused such a file at `File::open` while the
|
||||||
|
conformance probe reported it as libhdf5 does, so the gate counted five
|
||||||
|
files as agreeing with h5py that the library did not open; probe and
|
||||||
|
library now take the decision from the same
|
||||||
|
`superblock_ext::cache_image_state`.
|
||||||
|
- **Every other opener applies the superblock extension and the cache
|
||||||
|
image too** (`superblock_ext::apply_cache_image_in_place`, writing into
|
||||||
|
the buffer each already owns): `clawhdf5_io`'s `NativeVol` (at `open`,
|
||||||
|
and on read for `from_bytes`), `AsyncHDF5File`, `MpiVol` (a minimal edit
|
||||||
|
through the same `vol::load_hdf5`; the `mpi-io` feature cannot be built
|
||||||
|
without an MPI installation, so it was not compiled), and the external
|
||||||
|
source files of a virtual dataset. They read a file with an image from
|
||||||
|
its own bytes — stale metadata, or none (`h5clear_mdc_image.h5` failed
|
||||||
|
with `InvalidObjectHeaderVersion(0)`) — and skipped the extension checks
|
||||||
|
`File::open` makes. These readers cannot open a file and fail each
|
||||||
|
object, so an image libhdf5 cannot load is refused with the image's
|
||||||
|
error.
|
||||||
|
- **The superblock extension is decoded at open, as libhdf5 does:** a File
|
||||||
|
Space Info or Metadata Cache Image message libhdf5 cannot decode makes the
|
||||||
|
open fail (`cve-2020-10810`, `cve-2020-10812` were opened).
|
||||||
|
`FormatError::InvalidSuperblockExtension`, `InvalidCacheImage`.
|
||||||
|
- **Dataset storage libhdf5 refuses at open is refused at open**
|
||||||
|
(`FormatError::InvalidDatasetStorage`, `data_read::check_dataset_storage`):
|
||||||
|
an element count times element size that overflows (`cve-2024-32624`
|
||||||
|
`/Dset_OBJREF` opened and reported its shape), contiguous storage past the
|
||||||
|
end of the file, compact data of the wrong size. An empty contiguous
|
||||||
|
dataset at a defined address, which clawhdf5 up to v2.7.0 wrote, still
|
||||||
|
opens.
|
||||||
|
- **Wrong or missing data fixed:**
|
||||||
|
- a simple dataspace of rank 0 holds one element (it held 0;
|
||||||
|
`cve-2020-18494`), and contiguous storage larger than the dataset reads
|
||||||
|
(`cve-2024-32623`, `cve-2025-2309`; libhdf5 ignores the excess);
|
||||||
|
- scale-offset returned wrong values for ordinary h5py files — see
|
||||||
|
*Correctness* below; E-scale is refused, as in libhdf5; codes past the
|
||||||
|
end of the chunk stay an error (`cve-2025-2308`, where HDF5 2.0 reads
|
||||||
|
past its buffer);
|
||||||
|
- shuffle uses its own parameter as the element size, as libhdf5 does
|
||||||
|
(`cve-2025-44905`);
|
||||||
|
- an unfiltered chunk the index records at other than the chunk's size is
|
||||||
|
refused (it read with zeros for the missing bytes; `cve-2025-44904`);
|
||||||
|
- a v1 B-tree chunk index is read as libhdf5 reads it: each chunk is
|
||||||
|
looked up the way `H5B_find` / `H5D__btree_cmp3` / `H5D__btree_found`
|
||||||
|
look it up, and a chunk that lookup does not find reads as fill values.
|
||||||
|
A key with a non-zero element-size coordinate is found in a 1-D dataset
|
||||||
|
and not in one of rank 2 or more (`cve-2025-44905`
|
||||||
|
`/Shuffle_float_data_le`, which read the chunk's data where h5py reads
|
||||||
|
fill values); an interim fix refused every such key, including 1-D
|
||||||
|
files libhdf5 reads correctly.
|
||||||
|
- **Refused as libhdf5 refuses them:** a v1 group with an empty link name
|
||||||
|
fails its listing (`FormatError::InvalidLinkName`; lookups still work,
|
||||||
|
`cve-2021-46244`); dataspaces with more than 32 dimensions, a rank on a
|
||||||
|
scalar or null dataspace, or a dimension over its maximum
|
||||||
|
(`FormatError::InvalidDataspace`).
|
||||||
|
- `ObjectHeader::object_class` classifies a header as libhdf5 does (a
|
||||||
|
dataset needs a datatype *and* a dataspace).
|
||||||
|
- Conformance harness: user-defined links were listed as objects by the
|
||||||
|
reference, unopenable objects were not deduplicated, nested array types
|
||||||
|
were hashed wrong (`tarray3.h5`), and the attributes of objects h5py
|
||||||
|
cannot open were compared; all fixed. `CONFORMANCE.md` lists the corrupt
|
||||||
|
objects HDF5 2.0 reads through a bug (`bad_nbit_parms_walk.h5` among
|
||||||
|
them: libhdf5's own test now requires that read to fail).
|
||||||
|
|
||||||
### Concurrent reads (2026-09-26)
|
### Concurrent reads (2026-09-26)
|
||||||
- **Full reads of chunked datasets scale with threads again when rayon's
|
- **Full reads of chunked datasets scale with threads again when rayon's
|
||||||
pool has one thread.** Each full read handed its chunks to rayon to
|
pool has one thread.** Each full read handed its chunks to rayon to
|
||||||
@@ -372,6 +1165,7 @@
|
|||||||
missing feature ("unsupported filter: 32026 (Blosc2, not implemented by
|
missing feature ("unsupported filter: 32026 (Blosc2, not implemented by
|
||||||
clawhdf5)").
|
clawhdf5)").
|
||||||
- **Not implemented:** Blosc2 (32026) and ZFP (32013) remain a clear error.
|
- **Not implemented:** Blosc2 (32026) and ZFP (32013) remain a clear error.
|
||||||
|
(Blosc2 reads since the `blosc2` feature, see above.)
|
||||||
- **Wrong data: a chunk that decodes short read as zeros** (pre-existing, every
|
- **Wrong data: a chunk that decodes short read as zeros** (pre-existing, every
|
||||||
filter). HDF5 stores every chunk at the full chunk size, so a filter
|
filter). HDF5 stores every chunk at the full chunk size, so a filter
|
||||||
pipeline that decodes to fewer bytes means a corrupt chunk; every chunk
|
pipeline that decodes to fewer bytes means a corrupt chunk; every chunk
|
||||||
@@ -787,6 +1581,59 @@
|
|||||||
- CI keeps zlib-ng building and tested; the arm64 job no longer needs cmake.
|
- CI keeps zlib-ng building and tested; the arm64 job no longer needs cmake.
|
||||||
|
|
||||||
### Correctness
|
### Correctness
|
||||||
|
- **`FileBuilder` stored LZF and Blosc chunks with filter mask 0 even when
|
||||||
|
the filter had not shrunk them** (fixed 2026-09-26). Latent in the
|
||||||
|
unreleased LZF/Blosc writer only (added 2026-09-26, "Plugin filters"):
|
||||||
|
no tagged release writes LZF or Blosc, so v2.7.0 and earlier are
|
||||||
|
unaffected. libhdf5 treats LZF and Blosc output no smaller than the chunk
|
||||||
|
as a filter failure and, both being optional filters, stores such a chunk
|
||||||
|
unfiltered with the filter's mask bit set. clawhdf5 stored the filter's
|
||||||
|
output with a clear mask. For an LZF chunk whose stream was exactly the
|
||||||
|
chunk's size (h5py stores `[182, 0, 0, 0, 0]` in a 5-byte chunk raw),
|
||||||
|
the first libhdf5 rewrite of that chunk stored the new data raw at the
|
||||||
|
same size and, the size being unchanged, kept the stale mask 0: h5py
|
||||||
|
then failed to read the dataset ("filter returned failure during read").
|
||||||
|
The whole-file writer now runs chunks through the pipeline as libhdf5
|
||||||
|
does (`clawhdf5_format::filters::compress_chunk_masked`, as `FileEditor`
|
||||||
|
already did) and records each chunk's real mask in every chunk index it
|
||||||
|
builds (single chunk, Fixed Array, Extensible Array, version-2 B-tree;
|
||||||
|
it builds no version-1 B-tree or implicit index), in the sequential and
|
||||||
|
`parallel` paths and `create_datasets_parallel`. Files whose chunks all
|
||||||
|
compress are byte-identical to before. `PrecompressedChunks::chunks` is
|
||||||
|
now `(raw size, stored bytes, filter mask)` (**breaking** for code that
|
||||||
|
reads it). Files written before the fix read correctly; rewrite them
|
||||||
|
(with this build or `h5repack`) before modifying them with libhdf5.
|
||||||
|
Tests: `plugin_filters_interop`
|
||||||
|
`skipped_optional_filters_are_masked_as_libhdf5_masks_them` (LZF,
|
||||||
|
shuffle+LZF+fletcher32 and Blosc, random, compressible and alternating
|
||||||
|
chunks, every index: masks equal an h5py-written twin's; after h5py r+
|
||||||
|
rewrites and extends the datasets, h5py, h5dump and our reader read every
|
||||||
|
value — before the fix 20 of 24 datasets had other masks than h5py's,
|
||||||
|
and h5py could not read the rewritten `[x, 0, 0, 0, 0]` datasets) and
|
||||||
|
`files_whose_chunks_all_compress_are_unchanged`; `chunked_write`
|
||||||
|
`skipped_lzf_chunks_are_masked_in_every_index`.
|
||||||
|
- **Scale-offset data read wrong values in every release that decoded it
|
||||||
|
(v2.2.0 to v2.7.0), silently, on ordinary h5py files** (fixed
|
||||||
|
2026-09-26). Of 1480 scale-offset datasets h5py writes across every
|
||||||
|
integer type (`i1` .. `u8`), `f4` and `f8`, both byte orders, with and
|
||||||
|
without a fill value, and `scaleoffset` from 0 to the full width, 332 did
|
||||||
|
not read as h5py reads them: **151 returned wrong values with no error**
|
||||||
|
and 181 failed to read. The common cause was a chunk libhdf5 stores at
|
||||||
|
full width (`minbits` equal to the type's width), which it does for any
|
||||||
|
full-width `scaleoffset` and on its own whenever a chunk's values span
|
||||||
|
most of the type's range: `scaleoffset=0` integer data with a wide range
|
||||||
|
(82 datasets, all wrong values), full-width `u4`/`i4`/`u8`/`i8` (51 wrong
|
||||||
|
values; the narrower types and the rest failed with "truncated minval" or
|
||||||
|
"implausible minbits"), and `f4` D-scale data with a large range (18,
|
||||||
|
wrong values). Such a chunk holds the elements as they are; they were
|
||||||
|
decoded as offsets from `minval`. Also fixed, found on crafted files: the
|
||||||
|
packed codes start at byte 21 whatever size the chunk records for
|
||||||
|
`minval` (`cve-2025-44905` `/Scale_offset_short_data_be`), and a chunk
|
||||||
|
with `minbits` 0 and a fill value is all fill values (it read as
|
||||||
|
`minval`). The whole matrix is now an interop test
|
||||||
|
(`crates/clawhdf5/tests/scaleoffset_interop.rs`, generated by h5py at
|
||||||
|
test time, every dataset compared); on v2.7.0's decoder it reports the
|
||||||
|
332. See `docs/known-issues.md`.
|
||||||
- **Corrupt files libhdf5 refuses are now refused instead of read.** On the
|
- **Corrupt files libhdf5 refuses are now refused instead of read.** On the
|
||||||
HDF Group's CVE reproducers, 18 objects that libhdf5 (HDF5 2.0, through
|
HDF Group's CVE reproducers, 18 objects that libhdf5 (HDF5 2.0, through
|
||||||
h5py) refuses to open were read by clawhdf5, some as wrong data (a chunk
|
h5py) refuses to open were read by clawhdf5, some as wrong data (a chunk
|
||||||
|
|||||||
@@ -5,13 +5,13 @@ Pure-Rust HDF5 format implementation with HNSW vector search, WAL-backed persist
|
|||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
Cargo workspace with 18 crates under `crates/` (plus `libaec-sys`, an internal FFI bindings crate for the optional `szip` feature):
|
Cargo workspace with 19 crates under `crates/` (plus `libaec-sys`, an internal FFI bindings crate for the optional `szip` feature):
|
||||||
|
|
||||||
| Crate | Role |
|
| Crate | Role |
|
||||||
|-------|------|
|
|-------|------|
|
||||||
| `clawhdf5-format` | HDF5 binary spec parser (superblock, B-tree, heap) — also holds shared type definitions and physical constants |
|
| `clawhdf5-format` | HDF5 binary spec parser (superblock, B-tree, heap) — also holds shared type definitions and physical constants |
|
||||||
| `clawhdf5-io` | Read/write implementation |
|
| `clawhdf5-io` | Read/write implementation |
|
||||||
| `clawhdf5-filters` | Deflate backends (zlib-rs, zlib-ng, Apple Compression); the HDF5 filter pipeline, the filter registry (`clawhdf5_format::filter_registry`) and the other codecs (LZ4, Zstd, SZIP, N-Bit, scale-offset, pcodec, and the pure-Rust plugin filters LZF, bitshuffle, bzip2, Blosc 1) live in `clawhdf5-format`. No Blosc2 or ZFP. |
|
| `clawhdf5-filters` | Deflate backends (zlib-rs, zlib-ng, Apple Compression); the HDF5 filter pipeline, the filter registry (`clawhdf5_format::filter_registry`) and the other codecs (LZ4, Zstd, SZIP, N-Bit, scale-offset, pcodec, and the pure-Rust plugin filters LZF, bitshuffle, bzip2, Blosc 1, and Blosc2 and ZFP read-only) live in `clawhdf5-format`. |
|
||||||
| `clawhdf5-derive` | Proc-macro derive for HDF5-serializable structs |
|
| `clawhdf5-derive` | Proc-macro derive for HDF5-serializable structs |
|
||||||
| `clawhdf5` | Main facade crate |
|
| `clawhdf5` | Main facade crate |
|
||||||
| `clawhdf5-netcdf4` | NetCDF-4 compatibility layer |
|
| `clawhdf5-netcdf4` | NetCDF-4 compatibility layer |
|
||||||
@@ -26,6 +26,7 @@ Cargo workspace with 18 crates under `crates/` (plus `libaec-sys`, an internal F
|
|||||||
| `clawhdf5-napi` | Node.js native addon bindings |
|
| `clawhdf5-napi` | Node.js native addon bindings |
|
||||||
| `clawhdf5-py` | PyO3 Python bindings |
|
| `clawhdf5-py` | PyO3 Python bindings |
|
||||||
| `clawhdf5-wasm` | WebAssembly (wasm-bindgen) reader for the browser; demo in `examples/wasm-viewer/` |
|
| `clawhdf5-wasm` | WebAssembly (wasm-bindgen) reader for the browser; demo in `examples/wasm-viewer/` |
|
||||||
|
| `clawhdf5-remote` | Remote files: `open_url` over HTTP(S) range requests and object stores (`object_store`: S3, GCS, Azure) through a mandatory block cache (`BlockCache`) |
|
||||||
| `clawhdf5-bench` | Benchmark suite |
|
| `clawhdf5-bench` | Benchmark suite |
|
||||||
|
|
||||||
## Key Features
|
## Key Features
|
||||||
@@ -150,6 +151,35 @@ Cargo workspace with 18 crates under `crates/` (plus `libaec-sys`, an internal F
|
|||||||
Alerts never block a save — drain them with `HDF5Memory::take_anomaly_alerts`.
|
Alerts never block a save — drain them with `HDF5Memory::take_anomaly_alerts`.
|
||||||
`MemorySource` for this bookkeeping is inferred from the caller-supplied
|
`MemorySource` for this bookkeeping is inferred from the caller-supplied
|
||||||
`source_channel` string (a heuristic, not an authenticated trust boundary).
|
`source_channel` string (a heuristic, not an authenticated trust boundary).
|
||||||
|
- In-place modification: `clawhdf5::FileEditor` (`crates/clawhdf5/src/edit/`)
|
||||||
|
overwrites values, grows and shrinks chunked datasets (every chunk index,
|
||||||
|
version-2 B-trees included) and sets attributes (compact and dense
|
||||||
|
storage) in existing files (h5py- or clawhdf5-written) without rewriting
|
||||||
|
them, changing indexes and heaps as libhdf5 does (index shapes and heap
|
||||||
|
bookkeeping are compared with libhdf5's in the tests); space an edit
|
||||||
|
frees is reused by later edits of the same editor. Anything it cannot do
|
||||||
|
safely is `Error::Unsupported` before any write (limits in
|
||||||
|
`docs/known-issues.md`). Test changes with
|
||||||
|
`cargo test -p clawhdf5-tools --test edit_interop --test
|
||||||
|
edit_coverage_interop` (h5py, h5dump, `h5rs check`, structure comparisons
|
||||||
|
with libhdf5; libhdf5 sources for the algorithms are at
|
||||||
|
github.com/HDFGroup/hdf5, tag `hdf5_1_14_6`).
|
||||||
|
- Remote files (`clawhdf5-remote`, range-read milestone M3 of
|
||||||
|
`docs/design/range-reads.md`): `open_url("http://…")` gives a
|
||||||
|
`clawhdf5::File` over `File::open_storage`, read through `BlockCache`
|
||||||
|
(1 MiB blocks, LRU byte budget, per-block in-flight dedup across threads,
|
||||||
|
runs coalesced into parallel requests). `HttpStorage` pins the file by
|
||||||
|
ETag/Last-Modified and length (a change is `RemoteError::FileChanged`),
|
||||||
|
refuses servers that ignore `Range` unless a full download is allowed,
|
||||||
|
and retries transient failures. `ObjectStoreStorage` (feature
|
||||||
|
`object-store`, pure Rust) runs each read on a small owned tokio
|
||||||
|
runtime and waits on a channel, so it works from any thread, including
|
||||||
|
inside `spawn_blocking` or another runtime. Default build is plain HTTP with
|
||||||
|
no C; `https` (rustls + ring) and `s3`/`gcs`/`azure` (aws-lc-rs) are
|
||||||
|
opt-in. Tests run a std-only HTTP server
|
||||||
|
(`tests/common/server.rs`, also the `range_server` example);
|
||||||
|
`CLAWHDF5_REMOTE_CORPUS=conformance/.cache/corpus` compares every corpus
|
||||||
|
file over HTTP with `File::open`.
|
||||||
- GPU-accelerated vector distance computation (`clawhdf5-gpu`, wgpu); HDF5 I/O itself is CPU-only
|
- GPU-accelerated vector distance computation (`clawhdf5-gpu`, wgpu); HDF5 I/O itself is CPU-only
|
||||||
- Browser: `clawhdf5-wasm` (wasm-bindgen, read-only, file held in memory;
|
- Browser: `clawhdf5-wasm` (wasm-bindgen, read-only, file held in memory;
|
||||||
no Zstd/SZIP since they link C) and the `examples/wasm-viewer/` page.
|
no Zstd/SZIP since they link C) and the `examples/wasm-viewer/` page.
|
||||||
|
|||||||
+50
-57
@@ -13,15 +13,15 @@ fatal. This file is generated by `conformance/run.sh`; do not edit it by hand.
|
|||||||
|
|
||||||
| | |
|
| | |
|
||||||
|---|---|
|
|---|---|
|
||||||
| date | 2026-09-26 14:18 UTC |
|
| date | 2026-09-27 00:34 UTC |
|
||||||
| clawhdf5 commit | `73a01f1256fb9bf1b1e7601f755af9e8273cec4e` |
|
| clawhdf5 commit | `f37e7ae3263277319dba4bc39be5397194eb00c3` |
|
||||||
| machine | `tank`: AMD Ryzen 7 7800X3D 8-Core Processor, 16 CPUs, 61 GiB, Linux 7.0.0-34-generic x86_64 |
|
| machine | `tank`: AMD Ryzen 7 7800X3D 8-Core Processor, 16 CPUs, 61 GiB, Linux 7.0.0-34-generic x86_64 |
|
||||||
| command | `conformance/run.sh --no-fetch --update-baseline` |
|
| command | `conformance/run.sh --no-fetch --update-baseline` |
|
||||||
| rustc | rustc 1.98.1 (48a229cea 2026-09-01) |
|
| rustc | rustc 1.98.1 (48a229cea 2026-09-01) |
|
||||||
| reference | h5py 3.16.0, HDF5 2.0.0, numpy 2.5.3, hdf5plugin 7.1.0, Python 3.14.4 |
|
| reference | h5py 3.16.0, HDF5 2.0.0, numpy 2.5.3, hdf5plugin 7.1.0, Python 3.14.4 |
|
||||||
| h5dump | Version 1.14.6 (CVE corpus only) |
|
| h5dump | Version 1.14.6 (CVE corpus only) |
|
||||||
| limits | 20 s timeout (SIGKILL), 4096 MiB address space, per process; 16 files in parallel |
|
| limits | 20 s timeout (SIGKILL), 4096 MiB address space, per process; 16 files in parallel |
|
||||||
| runtime | 23 s probing + comparing (0 s fetch/build before it) |
|
| runtime | 21 s probing + comparing (0 s fetch/build before it) |
|
||||||
|
|
||||||
## Results
|
## Results
|
||||||
|
|
||||||
@@ -36,16 +36,18 @@ A file's class is the first that applies:
|
|||||||
| corpus | files | ok | our-error | mismatch | h5py-cannot-read | panic | hang | crash | oom |
|
| corpus | files | ok | our-error | mismatch | h5py-cannot-read | panic | hang | crash | oom |
|
||||||
|---|---|---|---|---|---|---|---|---|---|
|
|---|---|---|---|---|---|---|---|---|---|
|
||||||
| NCAS-CMS_pyfive | 33 | 32 | 0 | 1 | 0 | 0 | 0 | 0 | 0 |
|
| NCAS-CMS_pyfive | 33 | 32 | 0 | 1 | 0 | 0 | 0 | 0 | 0 |
|
||||||
| cve_hdf5 | 147 | 100 | 6 | 9 | 32 | 0 | 0 | 0 | 0 |
|
| cve_hdf5 | 147 | 113 | 2 | 0 | 32 | 0 | 0 | 0 | 0 |
|
||||||
| h5py_data | 4 | 4 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
|
| h5py_data | 4 | 4 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
|
||||||
| hdf5 | 466 | 392 | 4 | 10 | 60 | 0 | 0 | 0 | 0 |
|
| hdf5 | 466 | 404 | 1 | 1 | 60 | 0 | 0 | 0 | 0 |
|
||||||
| netcdf-c | 20 | 20 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
|
| netcdf-c | 20 | 20 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
|
||||||
| netcdf4-python | 18 | 18 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
|
| netcdf4-python | 18 | 18 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
|
||||||
| usnistgov_h5wasm | 5 | 5 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
|
| usnistgov_h5wasm | 5 | 5 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
|
||||||
| xarray-data | 4 | 4 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
|
| xarray-data | 4 | 4 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
|
||||||
| **all** | **697** | **575** | **10** | **20** | **92** | **0** | **0** | **0** | **0** |
|
| **all** | **697** | **600** | **3** | **2** | **92** | **0** | **0** | **0** | **0** |
|
||||||
|
|
||||||
2 of the 20 mismatches are a known h5py bug, not ours (see *Known not-our-bug*).
|
2 of the 2 mismatches are a known h5py bug, not ours (see *Known not-our-bug*).
|
||||||
|
|
||||||
|
3 of the 3 our-errors are corrupt data that HDF5 2.0 reads only through a bug and clawhdf5 refuses (see *Known not-our-bug*).
|
||||||
|
|
||||||
Corpora (fetched by `conformance/fetch-corpus.sh` into the gitignored `conformance/.cache/`):
|
Corpora (fetched by `conformance/fetch-corpus.sh` into the gitignored `conformance/.cache/`):
|
||||||
|
|
||||||
@@ -70,26 +72,13 @@ Grouped by normalised error message. *files* counts files whose class this cause
|
|||||||
|
|
||||||
| files | objects | error | examples |
|
| files | objects | error | examples |
|
||||||
|---:|---:|---|---|
|
|---:|---:|---|---|
|
||||||
| 3 | 3 | `DataSizeMismatch { expected: N, actual: N }` | `cve_hdf5/cvefiles/cve-2020-18494.h5`, `cve_hdf5/cvefiles/cve-2024-32623.h5`, `cve_hdf5/cvefiles/cve-2025-2309.h5` |
|
| 3 | 3 | `ChunkedReadError("…")` | `cve_hdf5/cvefiles/cve-2025-2308.h5`, `cve_hdf5/cvefiles/cve-2025-44904.h5`, `hdf5/test/testfiles/bad_nbit_parms_walk.h5` |
|
||||||
| 2 | 2 | `ChunkedReadError("…")` | `cve_hdf5/cvefiles/cve-2025-2308.h5`, `hdf5/test/testfiles/bad_nbit_parms_walk.h5` |
|
|
||||||
| 2 | 2 | `UnsupportedFilter(N)` | `hdf5/HDF5Examples/C/H5FLT/tfiles/h5ex_d_blosc2.h5`, `hdf5/HDF5Examples/C/H5FLT/tfiles/h5ex_d_zfp.h5` |
|
|
||||||
| 1 | 1 | `UnexpectedEof { expected: N, available: N }` | `cve_hdf5/cvefiles/cve-2019-9151.h5` |
|
|
||||||
| 1 | 1 | `MissingMessage(Dataspace)` | `cve_hdf5/cvefiles/cve-2024-33874.h5` |
|
|
||||||
| 1 | 1 | `InvalidObjectHeaderVersion(N)` | `hdf5/tools/test/testfiles/h5clear_mdc_image.h5` |
|
|
||||||
|
|
||||||
## Mismatch root causes
|
## Mismatch root causes
|
||||||
|
|
||||||
| files | objects | cause | examples |
|
| files | objects | cause | examples |
|
||||||
|---:|---:|---|---|
|
|---:|---:|---|---|
|
||||||
| 13 | 14 | `missing-object` | `cve_hdf5/cvefiles/cve-2019-8397.h5`, `cve_hdf5/cvefiles/cve-2019-8398.h5`, `cve_hdf5/cvefiles/cve-2021-46243.h5` (+10 more) |
|
|
||||||
| 2 | 6 | `extra-attr` | `cve_hdf5/cvefiles/cve-2018-17438`, `cve_hdf5/cvefiles/cve-2018-17439` |
|
|
||||||
| 1 | 1 | `attr-values: ours=vlen(>u8) h5py=object layout=- filters=-` | `NCAS-CMS_pyfive/tests/data/attr_datatypes.hdf5` |
|
| 1 | 1 | `attr-values: ours=vlen(>u8) h5py=object layout=- filters=-` | `NCAS-CMS_pyfive/tests/data/attr_datatypes.hdf5` |
|
||||||
| 1 | 4 | `extra-object` | `cve_hdf5/cvefiles/cve-2021-46244.h5` |
|
|
||||||
| 1 | 1 | `values: ours=<f4 h5py=float32 layout=chunked filters=-` | `cve_hdf5/cvefiles/cve-2025-44904.h5` |
|
|
||||||
| 1 | 1 | `values: ours=>i2 h5py=>i2 layout=chunked filters=[6]` | `cve_hdf5/cvefiles/cve-2025-44905.h5` |
|
|
||||||
| 1 | 1 | `values: ours=>f4 h5py=>f4 layout=chunked filters=[2]` | `cve_hdf5/cvefiles/cve-2025-44905.h5` |
|
|
||||||
| 1 | 1 | `values: ours=<f4 h5py=float32 layout=chunked filters=[2]` | `cve_hdf5/cvefiles/cve-2025-44905.h5` |
|
|
||||||
| 1 | 1 | `values: ours=((<i4)[6, 3])[4] h5py=(('<i4', (6, 3)), (4,)) layout=contiguous filters=-` | `hdf5/tools/test/testfiles/tarray3.h5` |
|
|
||||||
| 1 | 1 | `values: ours=vlen({r:>f4,i:>f4}8) h5py=object layout=contiguous filters=-` | `hdf5/tools/test/testfiles/tcomplex_be.h5` |
|
| 1 | 1 | `values: ours=vlen({r:>f4,i:>f4}8) h5py=object layout=contiguous filters=-` | `hdf5/tools/test/testfiles/tcomplex_be.h5` |
|
||||||
|
|
||||||
## CVE corpus: clawhdf5 vs h5dump vs h5py
|
## CVE corpus: clawhdf5 vs h5dump vs h5py
|
||||||
@@ -102,7 +91,7 @@ columns are.
|
|||||||
|
|
||||||
| tool | read | error | panic | crash | hang | oom |
|
| tool | read | error | panic | crash | hang | oom |
|
||||||
|---|---:|---:|---:|---:|---:|---:|
|
|---|---:|---:|---:|---:|---:|---:|
|
||||||
| clawhdf5 | 140 | 7 | 0 | 0 | 0 | 0 |
|
| clawhdf5 | 121 | 26 | 0 | 0 | 0 | 0 |
|
||||||
| h5dump 1.14.6 | 16 | 129 | 0 | 2 | 0 | 0 |
|
| h5dump 1.14.6 | 16 | 129 | 0 | 2 | 0 | 0 |
|
||||||
| h5py 3.16.0 / HDF5 2.0.0 | 115 | 31 | 0 | 1 | 0 | 0 |
|
| h5py 3.16.0 / HDF5 2.0.0 | 115 | 31 | 0 | 1 | 0 | 0 |
|
||||||
|
|
||||||
@@ -156,27 +145,27 @@ columns are.
|
|||||||
| cvefiles/cve-2018-17435.h5 | error exit | read 3 obj, 1 errors | read 3 obj, 1 errors | ok |
|
| cvefiles/cve-2018-17435.h5 | error exit | read 3 obj, 1 errors | read 3 obj, 1 errors | ok |
|
||||||
| cvefiles/cve-2018-17436 | error exit | open error | open error | h5py-cannot-read |
|
| cvefiles/cve-2018-17436 | error exit | open error | open error | h5py-cannot-read |
|
||||||
| cvefiles/cve-2018-17437.h5 | error exit | read 3 obj, 1 errors | read 3 obj, 1 errors | ok |
|
| cvefiles/cve-2018-17437.h5 | error exit | read 3 obj, 1 errors | read 3 obj, 1 errors | ok |
|
||||||
| cvefiles/cve-2018-17438 | error exit | read 3 obj, 1 errors | read 3 obj, 1 errors | mismatch |
|
| cvefiles/cve-2018-17438 | error exit | read 3 obj, 1 errors | read 3 obj, 1 errors | ok |
|
||||||
| cvefiles/cve-2018-17439 | error exit | read 3 obj, 1 errors | read 3 obj, 1 errors | mismatch |
|
| cvefiles/cve-2018-17439 | error exit | read 3 obj, 1 errors | read 3 obj, 1 errors | ok |
|
||||||
| cvefiles/cve-2019-8396.h5 | error exit | read 3 obj, 2 errors | read 3 obj, 2 errors | ok |
|
| cvefiles/cve-2019-8396.h5 | error exit | read 3 obj, 2 errors | read 3 obj, 2 errors | ok |
|
||||||
| cvefiles/cve-2019-8397.h5 | error exit | read 3 obj, 2 errors | read 2 obj, 1 errors | mismatch |
|
| cvefiles/cve-2019-8397.h5 | error exit | read 2 obj, 1 errors | read 2 obj, 1 errors | ok |
|
||||||
| cvefiles/cve-2019-8398.h5 | error exit | read 3 obj, 2 errors | read 2 obj, 1 errors | mismatch |
|
| cvefiles/cve-2019-8398.h5 | error exit | read 2 obj, 1 errors | read 2 obj, 1 errors | ok |
|
||||||
| cvefiles/cve-2019-9151.h5 | error exit | read 3 obj, 1 errors | read 3 obj, 2 errors | our-error |
|
| cvefiles/cve-2019-9151.h5 | error exit | read 3 obj, 1 errors | read 3 obj, 2 errors | ok |
|
||||||
| cvefiles/cve-2019-9152.h5 | error exit | read 3 obj, 1 errors | read 3 obj, 1 errors | ok |
|
| cvefiles/cve-2019-9152.h5 | error exit | read 3 obj, 1 errors | read 3 obj, 1 errors | ok |
|
||||||
| cvefiles/cve-2020-10809 | error exit | open error | open error | h5py-cannot-read |
|
| cvefiles/cve-2020-10809 | error exit | open error | open error | h5py-cannot-read |
|
||||||
| cvefiles/cve-2020-10810.h5 | error exit | open error | read 2 obj | h5py-cannot-read |
|
| cvefiles/cve-2020-10810.h5 | error exit | open error | open error | h5py-cannot-read |
|
||||||
| cvefiles/cve-2020-10811.h5 | error exit | read 25 obj, 1 errors | read 25 obj, 1 errors | ok |
|
| cvefiles/cve-2020-10811.h5 | error exit | read 25 obj, 1 errors | read 25 obj, 1 errors | ok |
|
||||||
| cvefiles/cve-2020-10812.h5 | error exit | open error | read 2 obj | h5py-cannot-read |
|
| cvefiles/cve-2020-10812.h5 | error exit | open error | open error | h5py-cannot-read |
|
||||||
| cvefiles/cve-2020-18232.h5 | error exit | read 3 obj, 2 errors | read 3 obj, 2 errors | ok |
|
| cvefiles/cve-2020-18232.h5 | error exit | read 3 obj, 2 errors | read 3 obj, 2 errors | ok |
|
||||||
| cvefiles/cve-2020-18494.h5 | ok | read 2 obj | read 2 obj, 1 errors | our-error |
|
| cvefiles/cve-2020-18494.h5 | ok | read 2 obj | read 2 obj | ok |
|
||||||
| cvefiles/cve-2021-36977.h5 | error exit | read 1 obj, 1 errors | read 1 obj, 1 errors | ok |
|
| cvefiles/cve-2021-36977.h5 | error exit | read 1 obj, 1 errors | read 1 obj, 1 errors | ok |
|
||||||
| cvefiles/cve-2021-37501.h5 | error exit | read 18 obj, 1 errors | read 18 obj, 1 errors | ok |
|
| cvefiles/cve-2021-37501.h5 | error exit | read 18 obj, 1 errors | read 18 obj, 1 errors | ok |
|
||||||
| cvefiles/cve-2021-45829.h5 | error exit | read 1 obj, 2 errors | read 1 obj, 1 errors | ok |
|
| cvefiles/cve-2021-45829.h5 | error exit | read 1 obj, 2 errors | read 1 obj, 1 errors | ok |
|
||||||
| cvefiles/cve-2021-45830.h5 | error exit | open error | read 1 obj, 1 errors | h5py-cannot-read |
|
| cvefiles/cve-2021-45830.h5 | error exit | open error | open error | h5py-cannot-read |
|
||||||
| cvefiles/cve-2021-45833.h5 | error exit | read 2 obj, 1 errors | read 2 obj, 1 errors | ok |
|
| cvefiles/cve-2021-45833.h5 | error exit | read 2 obj, 1 errors | read 2 obj, 1 errors | ok |
|
||||||
| cvefiles/cve-2021-46242.h5 | error exit | open error | read 1 obj, 1 errors | h5py-cannot-read |
|
| cvefiles/cve-2021-46242.h5 | error exit | open error | read 1 obj, 1 errors | h5py-cannot-read |
|
||||||
| cvefiles/cve-2021-46243.h5 | error exit | read 3 obj, 2 errors | read 2 obj, 1 errors | mismatch |
|
| cvefiles/cve-2021-46243.h5 | error exit | read 2 obj, 1 errors | read 2 obj, 1 errors | ok |
|
||||||
| cvefiles/cve-2021-46244.h5 | error exit | read 2 obj, 1 errors | read 6 obj, 4 errors | mismatch |
|
| cvefiles/cve-2021-46244.h5 | error exit | read 2 obj, 1 errors | read 2 obj, 1 errors | ok |
|
||||||
| cvefiles/cve-2024-29157.h5 | error exit | read 4 obj, 7 errors | read 4 obj, 7 errors | ok |
|
| cvefiles/cve-2024-29157.h5 | error exit | read 4 obj, 7 errors | read 4 obj, 7 errors | ok |
|
||||||
| cvefiles/cve-2024-29158.h5 | ok | read 3 obj, 1 errors | read 3 obj, 1 errors | ok |
|
| cvefiles/cve-2024-29158.h5 | ok | read 3 obj, 1 errors | read 3 obj, 1 errors | ok |
|
||||||
| cvefiles/cve-2024-29159.h5 | error exit | read 2 obj, 1 errors | read 2 obj, 1 errors | ok |
|
| cvefiles/cve-2024-29159.h5 | error exit | read 2 obj, 1 errors | read 2 obj, 1 errors | ok |
|
||||||
@@ -201,50 +190,50 @@ columns are.
|
|||||||
| cvefiles/cve-2024-32615.h5 | error exit | read 4 obj, 1 errors | read 4 obj, 1 errors | ok |
|
| cvefiles/cve-2024-32615.h5 | error exit | read 4 obj, 1 errors | read 4 obj, 1 errors | ok |
|
||||||
| cvefiles/cve-2024-32616.h5 | error exit | read 10 obj, 7 errors | read 10 obj, 6 errors | ok |
|
| cvefiles/cve-2024-32616.h5 | error exit | read 10 obj, 7 errors | read 10 obj, 6 errors | ok |
|
||||||
| cvefiles/cve-2024-32617.h5 | error exit | read 1 obj, 1 errors | read 1 obj, 1 errors | ok |
|
| cvefiles/cve-2024-32617.h5 | error exit | read 1 obj, 1 errors | read 1 obj, 1 errors | ok |
|
||||||
| cvefiles/cve-2024-32618.h5 | error exit | read 4 obj, 2 errors | read 3 obj, 1 errors | mismatch |
|
| cvefiles/cve-2024-32618.h5 | error exit | read 3 obj, 1 errors | read 3 obj, 1 errors | ok |
|
||||||
| cvefiles/cve-2024-32619.h5 | error exit | read 3 obj, 2 errors | read 3 obj, 2 errors | ok |
|
| cvefiles/cve-2024-32619.h5 | error exit | read 3 obj, 2 errors | read 3 obj, 2 errors | ok |
|
||||||
| cvefiles/cve-2024-32620.h5 | error exit | read 1 obj, 1 errors | read 1 obj, 1 errors | ok |
|
| cvefiles/cve-2024-32620.h5 | error exit | read 1 obj, 1 errors | read 1 obj, 1 errors | ok |
|
||||||
| cvefiles/cve-2024-32621.h5 | ok | read 3 obj, 1 errors | read 3 obj, 1 errors | ok |
|
| cvefiles/cve-2024-32621.h5 | ok | read 3 obj, 1 errors | read 3 obj, 1 errors | ok |
|
||||||
| cvefiles/cve-2024-32622.h5 | ok | read 3 obj, 1 errors | read 3 obj, 1 errors | ok |
|
| cvefiles/cve-2024-32622.h5 | ok | read 3 obj, 1 errors | read 3 obj, 1 errors | ok |
|
||||||
| cvefiles/cve-2024-32623.h5 | ok | read 6 obj | read 6 obj, 1 errors | our-error |
|
| cvefiles/cve-2024-32623.h5 | ok | read 6 obj | read 6 obj | ok |
|
||||||
| cvefiles/cve-2024-32624.h5 | error exit | read 6 obj, 1 errors | read 6 obj | ok |
|
| cvefiles/cve-2024-32624.h5 | error exit | read 6 obj, 1 errors | read 6 obj, 1 errors | ok |
|
||||||
| cvefiles/cve-2024-33873.h5 | error exit | read 4 obj, 1 errors | read 4 obj, 1 errors | ok |
|
| cvefiles/cve-2024-33873.h5 | error exit | read 4 obj, 1 errors | read 4 obj, 1 errors | ok |
|
||||||
| cvefiles/cve-2024-33874.h5 | ok | read 6 obj, 1 errors | read 6 obj, 2 errors | our-error |
|
| cvefiles/cve-2024-33874.h5 | ok | read 6 obj, 1 errors | read 6 obj, 1 errors | ok |
|
||||||
| cvefiles/cve-2024-33875.h5 | ok | read 2 obj | read 2 obj | ok |
|
| cvefiles/cve-2024-33875.h5 | ok | read 2 obj | read 2 obj | ok |
|
||||||
| cvefiles/cve-2024-33876.h5 | ok | read 3 obj, 1 errors | read 3 obj, 1 errors | ok |
|
| cvefiles/cve-2024-33876.h5 | ok | read 3 obj, 1 errors | read 3 obj, 1 errors | ok |
|
||||||
| cvefiles/cve-2024-33877.h5 | error exit | read 8 obj, 1 errors | read 8 obj, 1 errors | ok |
|
| cvefiles/cve-2024-33877.h5 | error exit | read 8 obj, 1 errors | read 8 obj, 1 errors | ok |
|
||||||
| cvefiles/cve-2025-2153.h5 | error exit | open error | read 1 obj, 1 errors | h5py-cannot-read |
|
| cvefiles/cve-2025-2153.h5 | error exit | open error | read 1 obj, 1 errors | h5py-cannot-read |
|
||||||
| cvefiles/cve-2025-2308.h5 | error exit | read 25 obj, 1 errors | read 25 obj, 2 errors | our-error |
|
| cvefiles/cve-2025-2308.h5 | error exit | read 25 obj, 1 errors | read 25 obj, 2 errors | our-error |
|
||||||
| cvefiles/cve-2025-2309.h5 | ok | read 6 obj, 1 errors | read 6 obj, 1 errors | our-error |
|
| cvefiles/cve-2025-2309.h5 | ok | read 6 obj, 1 errors | read 6 obj | ok |
|
||||||
| cvefiles/cve-2025-2310.h5 | error exit | read 24 obj, 8 errors | read 24 obj, 8 errors | ok |
|
| cvefiles/cve-2025-2310.h5 | error exit | read 24 obj, 8 errors | read 24 obj, 8 errors | ok |
|
||||||
| cvefiles/cve-2025-2912.h5 | error exit | open error | read 1 obj, 1 errors | h5py-cannot-read |
|
| cvefiles/cve-2025-2912.h5 | error exit | open error | open error | h5py-cannot-read |
|
||||||
| cvefiles/cve-2025-2913.h5 | error exit | open error | read 1 obj, 1 errors | h5py-cannot-read |
|
| cvefiles/cve-2025-2913.h5 | error exit | open error | open error | h5py-cannot-read |
|
||||||
| cvefiles/cve-2025-2914.h5 | error exit | open error | read 1 obj, 1 errors | h5py-cannot-read |
|
| cvefiles/cve-2025-2914.h5 | error exit | open error | open error | h5py-cannot-read |
|
||||||
| cvefiles/cve-2025-2915.h5 | error exit | open error | read 1 obj, 1 errors | h5py-cannot-read |
|
| cvefiles/cve-2025-2915.h5 | error exit | open error | open error | h5py-cannot-read |
|
||||||
| cvefiles/cve-2025-2923.h5 | error exit | open error | read 1 obj, 1 errors | h5py-cannot-read |
|
| cvefiles/cve-2025-2923.h5 | error exit | open error | open error | h5py-cannot-read |
|
||||||
| cvefiles/cve-2025-2924.h5 | error exit | read 1 obj, 1 errors | read 1 obj, 1 errors | ok |
|
| cvefiles/cve-2025-2924.h5 | error exit | read 1 obj, 1 errors | read 1 obj, 1 errors | ok |
|
||||||
| cvefiles/cve-2025-2925.h5 | error exit | read 1 obj, 1 errors | read 1 obj, 1 errors | ok |
|
| cvefiles/cve-2025-2925.h5 | error exit | read 1 obj, 1 errors | read 1 obj, 1 errors | ok |
|
||||||
| cvefiles/cve-2025-2926.h5 | error exit | open error | read 1 obj, 1 errors | h5py-cannot-read |
|
| cvefiles/cve-2025-2926.h5 | error exit | open error | open error | h5py-cannot-read |
|
||||||
| cvefiles/cve-2025-44904.h5 | error exit | read 25 obj, 1 errors | read 25 obj, 1 errors | mismatch |
|
| cvefiles/cve-2025-44904.h5 | error exit | read 25 obj, 1 errors | read 25 obj, 2 errors | our-error |
|
||||||
| cvefiles/cve-2025-44905.h5 | error exit | read 25 obj, 3 errors | read 25 obj, 3 errors | mismatch |
|
| cvefiles/cve-2025-44905.h5 | error exit | read 25 obj, 3 errors | read 25 obj, 3 errors | ok |
|
||||||
| cvefiles/cve-2025-6269-1.h5 | error exit | read 1 obj, 1 errors | read 1 obj, 1 errors | ok |
|
| cvefiles/cve-2025-6269-1.h5 | error exit | read 1 obj, 1 errors | read 1 obj, 1 errors | ok |
|
||||||
| cvefiles/cve-2025-6269-2.h5 | error exit | read 1 obj, 1 errors | read 1 obj, 1 errors | ok |
|
| cvefiles/cve-2025-6269-2.h5 | error exit | read 1 obj, 1 errors | read 1 obj, 1 errors | ok |
|
||||||
| cvefiles/cve-2025-6269-3.h5 | error exit | read 1 obj, 1 errors | read 1 obj, 1 errors | ok |
|
| cvefiles/cve-2025-6269-3.h5 | error exit | read 1 obj, 1 errors | read 1 obj, 1 errors | ok |
|
||||||
| cvefiles/cve-2025-6269-4.h5 | error exit | read 1 obj, 1 errors | read 1 obj, 1 errors | ok |
|
| cvefiles/cve-2025-6269-4.h5 | error exit | read 1 obj, 1 errors | read 1 obj, 1 errors | ok |
|
||||||
| cvefiles/cve-2025-6270-1.h5 | error exit | open error | read 1 obj, 1 errors | h5py-cannot-read |
|
| cvefiles/cve-2025-6270-1.h5 | error exit | open error | open error | h5py-cannot-read |
|
||||||
| cvefiles/cve-2025-6270-2.h5 | error exit | open error | read 1 obj, 1 errors | h5py-cannot-read |
|
| cvefiles/cve-2025-6270-2.h5 | error exit | open error | open error | h5py-cannot-read |
|
||||||
| cvefiles/cve-2025-6270-3.h5 | error exit | open error | read 1 obj, 1 errors | h5py-cannot-read |
|
| cvefiles/cve-2025-6270-3.h5 | error exit | open error | open error | h5py-cannot-read |
|
||||||
| cvefiles/cve-2025-6516.h5 | error exit | read 1 obj, 1 errors | read 1 obj, 1 errors | ok |
|
| cvefiles/cve-2025-6516.h5 | error exit | read 1 obj, 1 errors | read 1 obj, 1 errors | ok |
|
||||||
| cvefiles/cve-2025-6750.h5 | error exit | open error | read 1 obj, 1 errors | h5py-cannot-read |
|
| cvefiles/cve-2025-6750.h5 | error exit | open error | read 1 obj, 1 errors | h5py-cannot-read |
|
||||||
| cvefiles/cve-2025-6816.h5 | error exit | open error | read 1 obj, 1 errors | h5py-cannot-read |
|
| cvefiles/cve-2025-6816.h5 | error exit | open error | open error | h5py-cannot-read |
|
||||||
| cvefiles/cve-2025-6817.h5 | error exit | open error | read 1 obj | h5py-cannot-read |
|
| cvefiles/cve-2025-6817.h5 | error exit | open error | open error | h5py-cannot-read |
|
||||||
| cvefiles/cve-2025-6818.h5 | error exit | open error | read 1 obj | h5py-cannot-read |
|
| cvefiles/cve-2025-6818.h5 | error exit | open error | open error | h5py-cannot-read |
|
||||||
| cvefiles/cve-2025-6856.h5 | error exit | open error | read 1 obj, 1 errors | h5py-cannot-read |
|
| cvefiles/cve-2025-6856.h5 | error exit | open error | open error | h5py-cannot-read |
|
||||||
| cvefiles/cve-2025-6857.h5 | error exit | read 1 obj, 1 errors | read 1 obj, 1 errors | ok |
|
| cvefiles/cve-2025-6857.h5 | error exit | read 1 obj, 1 errors | read 1 obj, 1 errors | ok |
|
||||||
| cvefiles/cve-2025-6858.h5 | SIGSEGV | open error | read 1 obj, 1 errors | h5py-cannot-read |
|
| cvefiles/cve-2025-6858.h5 | SIGSEGV | open error | open error | h5py-cannot-read |
|
||||||
| cvefiles/cve-2025-7067.h5 | error exit | read 1 obj, 1 errors | read 1 obj, 1 errors | ok |
|
| cvefiles/cve-2025-7067.h5 | error exit | read 1 obj, 1 errors | read 1 obj, 1 errors | ok |
|
||||||
| cvefiles/cve-2025-7068.h5 | error exit | open error | read 1 obj, 1 errors | h5py-cannot-read |
|
| cvefiles/cve-2025-7068.h5 | error exit | open error | open error | h5py-cannot-read |
|
||||||
| cvefiles/cve-2025-7069.h5 | error exit | open error | read 1 obj, 1 errors | h5py-cannot-read |
|
| cvefiles/cve-2025-7069.h5 | error exit | open error | open error | h5py-cannot-read |
|
||||||
| cvefiles/cve-2026-26200.h5 | error exit | read 1 obj, 1 errors | read 1 obj, 1 errors | ok |
|
| cvefiles/cve-2026-26200.h5 | error exit | read 1 obj, 1 errors | read 1 obj, 1 errors | ok |
|
||||||
| cvefiles/cve-2026-34734.h5 | error exit | read 2 obj, 1 errors | read 2 obj, 1 errors | ok |
|
| cvefiles/cve-2026-34734.h5 | error exit | read 2 obj, 1 errors | read 2 obj, 1 errors | ok |
|
||||||
| cvefiles/cve-2026-92627.h5 | error exit | read 2 obj, 1 errors | read 2 obj, 1 errors | ok |
|
| cvefiles/cve-2026-92627.h5 | error exit | read 2 obj, 1 errors | read 2 obj, 1 errors | ok |
|
||||||
@@ -275,6 +264,11 @@ columns are.
|
|||||||
- **Types h5py widens.** Where h5py reads a type into a numpy type of a different size
|
- **Types h5py widens.** Where h5py reads a type into a numpy type of a different size
|
||||||
(FP8 -> float16, bfloat16 -> float32, x87 long double -> float128) the values are not
|
(FP8 -> float16, bfloat16 -> float32, x87 long double -> float128) the values are not
|
||||||
compared (shape and presence still are): dataset file type size 1 -> numpy float16 (2) (15x), attr file type size 1 -> numpy float16 (2) (15x), dataset file type size 2 -> numpy float32 (4) (2x), dataset file type size 8 -> numpy float128 (16) (1x), dataset file type size 12 -> numpy float128 (16) (1x), attr file type size 2 -> numpy float32 (4) (1x), dataset file type size 2 -> numpy >f4 (4) (1x), attr file type size 2 -> numpy >f4 (4) (1x).
|
compared (shape and presence still are): dataset file type size 1 -> numpy float16 (2) (15x), attr file type size 1 -> numpy float16 (2) (15x), dataset file type size 2 -> numpy float32 (4) (2x), dataset file type size 8 -> numpy float128 (16) (1x), dataset file type size 12 -> numpy float128 (16) (1x), attr file type size 2 -> numpy float32 (4) (1x), dataset file type size 2 -> numpy >f4 (4) (1x), attr file type size 2 -> numpy >f4 (4) (1x).
|
||||||
|
- **Corrupt data HDF5 2.0 reads through a bug.** clawhdf5 refuses these objects; h5py 3.16 /
|
||||||
|
HDF5 2.0 returns values for them that the file does not hold:
|
||||||
|
- `cve_hdf5/cvefiles/cve-2025-2308.h5` `/Scale_offset_long_long_data_le`: scale-offset codes run past the end of the chunk: HDF5 2.0 reads past its buffer; libhdf5's develop branch refuses the chunk ("Buffer too short").
|
||||||
|
- `cve_hdf5/cvefiles/cve-2025-44904.h5` `/Scale_offset_float_data_le`: unfiltered chunks of 38 and 37 bytes for 48-byte chunks: HDF5 2.0 fills the rest with whatever its buffer held; libhdf5's develop branch refuses them ("incorrect chunk size returned from index for unfiltered chunk").
|
||||||
|
- `hdf5/test/testfiles/bad_nbit_parms_walk.h5` `/Nbit_int_data_le`: an N-Bit parameter list one value short: HDF5 2.0 reads past the list; libhdf5's own test (`test_filter_bad_params`, test/dsets.c) now requires the read to fail.
|
||||||
- **References** are compared by presence only (`R`), not by target.
|
- **References** are compared by presence only (`R`), not by target.
|
||||||
|
|
||||||
## Objects h5py fails on but clawhdf5 reads
|
## Objects h5py fails on but clawhdf5 reads
|
||||||
@@ -282,7 +276,6 @@ columns are.
|
|||||||
- 19 x `OSError: Can't synchronously read data (no appropriate function for conversion path)`
|
- 19 x `OSError: Can't synchronously read data (no appropriate function for conversion path)`
|
||||||
- 1 x `TypeError: unhandled dtype kind M (dtype('…'))`
|
- 1 x `TypeError: unhandled dtype kind M (dtype('…'))`
|
||||||
- 1 x `TypeError: No NumPy equivalent for TypeTimeID exists`
|
- 1 x `TypeError: No NumPy equivalent for TypeTimeID exists`
|
||||||
- 1 x `KeyError: "…"`
|
|
||||||
- 1 x `ValueError: Insufficient precision in available types to represent (N, N, N, N, N)`
|
- 1 x `ValueError: Insufficient precision in available types to represent (N, N, N, N, N)`
|
||||||
|
|
||||||
## Reproduce
|
## Reproduce
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ members = [
|
|||||||
"crates/clawhdf5-bench",
|
"crates/clawhdf5-bench",
|
||||||
"crates/clawhdf5-tools",
|
"crates/clawhdf5-tools",
|
||||||
"crates/clawhdf5-wasm",
|
"crates/clawhdf5-wasm",
|
||||||
|
"crates/clawhdf5-remote",
|
||||||
"crates/libaec-sys",
|
"crates/libaec-sys",
|
||||||
]
|
]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|||||||
@@ -96,6 +96,12 @@ breaking change, are in [CHANGELOG.md](CHANGELOG.md).
|
|||||||
filtered top-k, never slower than unfiltered), and opt-in re-ranking and
|
filtered top-k, never slower than unfiltered), and opt-in re-ranking and
|
||||||
confidence rejection, which used to be reachable only through `ClawhdfBackend`.
|
confidence rejection, which used to be reachable only through `ClawhdfBackend`.
|
||||||
|
|
||||||
|
**Remote files (unreleased)**
|
||||||
|
- New crate `clawhdf5-remote`: `open_url("http://…")` reads a file on an
|
||||||
|
HTTP server (or in S3/GCS/Azure, opt-in) by range requests through a
|
||||||
|
block cache, without downloading it; `h5rs` takes URLs with its `remote`
|
||||||
|
feature. See [Reading remote files](#reading-remote-files).
|
||||||
|
|
||||||
**Tooling**
|
**Tooling**
|
||||||
- CI now runs the h5py/netCDF4 interop suites for real (they had been skipping
|
- CI now runs the h5py/netCDF4 interop suites for real (they had been skipping
|
||||||
silently) and runs an aarch64 job for the NEON kernels.
|
silently) and runs an aarch64 job for the NEON kernels.
|
||||||
@@ -433,6 +439,71 @@ b.write("groups.h5")?;
|
|||||||
A group holds at most 65 535 links; more is an error, as is a link over
|
A group holds at most 65 535 links; more is an error, as is a link over
|
||||||
65 515 bytes (a very long soft-link target) in a group of more than 8 links.
|
65 515 bytes (a very long soft-link target) in a group of more than 8 links.
|
||||||
|
|
||||||
|
### Modifying an existing file
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use clawhdf5::{AttrValue, FileEditor, Selection};
|
||||||
|
|
||||||
|
// A file from h5py or clawhdf5, dataset "x" chunked with maxshape=(None,).
|
||||||
|
let mut ed = FileEditor::open("data.h5")?; // exclusive lock, like libhdf5
|
||||||
|
ed.resize("x", &[1100])?; // h5py: ds.resize((1100,))
|
||||||
|
let sel = Selection::Hyperslab { start: vec![1000], stride: vec![1], count: vec![100], block: vec![1] };
|
||||||
|
ed.write_values("x", &sel, &[0.5f64; 100])?; // ds[1000:1100] = 0.5
|
||||||
|
ed.set_attr("x", "units", &AttrValue::String("m/s".into()))?;
|
||||||
|
ed.resize("x", &[900])?; // shrinking prunes chunks, like h5py
|
||||||
|
```
|
||||||
|
|
||||||
|
Each call changes the file in place (no rewrite) and syncs it. Any chunk
|
||||||
|
index (version-2 B-trees for several unlimited dimensions included) and
|
||||||
|
attributes in compact or dense storage are handled as libhdf5 handles
|
||||||
|
them; space an edit frees is reused by later edits of the same editor. What it
|
||||||
|
cannot change safely is refused before anything is written; see
|
||||||
|
[known issues](docs/known-issues.md) for the limits.
|
||||||
|
|
||||||
|
### Reading remote files
|
||||||
|
|
||||||
|
[`clawhdf5-remote`](crates/clawhdf5-remote/README.md) opens a file on an
|
||||||
|
HTTP server (or, with its `s3`/`gcs`/`azure` features, in an object store)
|
||||||
|
without downloading it: the read API is the same `clawhdf5::File`, and
|
||||||
|
only the bytes an operation needs are fetched, by `Range` requests through
|
||||||
|
a block cache (1 MiB blocks; opening fetches the first one). A file that
|
||||||
|
changes on the server while it is open is an error, never a mix of old and
|
||||||
|
new bytes.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let file = clawhdf5_remote::open_url("http://127.0.0.1:8000/tall.h5")?;
|
||||||
|
let values = file.dataset("/g2/dset2.1")?.read_f64()?;
|
||||||
|
```
|
||||||
|
|
||||||
|
To try it without a server of your own, the crate's test server serves a
|
||||||
|
directory with range support:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo run -p clawhdf5-remote --example range_server -- crates/clawhdf5/tests/fixtures 127.0.0.1:8000
|
||||||
|
# in another shell: list the file, read one dataset, print what it cost
|
||||||
|
cargo run -p clawhdf5-remote --example read_url -- http://127.0.0.1:8000/tall.h5 /g2/dset2.1
|
||||||
|
```
|
||||||
|
|
||||||
|
```text
|
||||||
|
/g1 group
|
||||||
|
/g2 group
|
||||||
|
/g2/dset2.1 dataset [10] F32
|
||||||
|
/g2/dset2.2 dataset [3, 5] F32
|
||||||
|
/g1/g1.1 group
|
||||||
|
/g1/g1.2 group
|
||||||
|
/g1/g1.2/g1.2.1 group
|
||||||
|
/g1/g1.1/dset1.1.1 dataset [10, 10] I32
|
||||||
|
/g1/g1.1/dset1.1.2 dataset [20] I32
|
||||||
|
/g2/dset2.1: 10 values, first [1.0, 1.100000023841858, 1.2000000476837158, ...]
|
||||||
|
1 range requests (the one at open included), 9968 bytes fetched, 9968 bytes cached
|
||||||
|
```
|
||||||
|
|
||||||
|
(`tall.h5` is 9 968 bytes, so the first block holds all of it.) `h5rs`
|
||||||
|
built with `--features remote` takes the same URLs:
|
||||||
|
`h5rs ls -r http://127.0.0.1:8000/tall.h5`. Plain HTTP builds no C;
|
||||||
|
`https://` is the `https` feature (rustls with ring, which compiles C).
|
||||||
|
Limits are in [known issues](docs/known-issues.md).
|
||||||
|
|
||||||
### Python
|
### Python
|
||||||
|
|
||||||
`crates/clawhdf5-py` is a Python package (PyO3 + numpy) that reads HDF5 with
|
`crates/clawhdf5-py` is a Python package (PyO3 + numpy) that reads HDF5 with
|
||||||
@@ -661,19 +732,20 @@ let exported = backend.export_markdown("MEMORY.md")?;
|
|||||||
## Crate Map
|
## Crate Map
|
||||||
|
|
||||||
```
|
```
|
||||||
clawhdf5 workspace (17 crates, ~86K lines of Rust in src/, ~104K with tests
|
clawhdf5 workspace (19 crates, ~86K lines of Rust in src/, ~104K with tests
|
||||||
and benches; plus libaec-sys, an internal FFI bindings
|
and benches; plus libaec-sys, an internal FFI bindings
|
||||||
crate for the optional szip feature)
|
crate for the optional szip feature)
|
||||||
│
|
│
|
||||||
├── Core HDF5
|
├── Core HDF5
|
||||||
│ ├── clawhdf5-format — Binary parser/writer (no_std-capable), shared type definitions
|
│ ├── clawhdf5-format — Binary parser/writer (no_std-capable), shared type definitions
|
||||||
│ ├── clawhdf5-io — I/O abstraction (file/memory readers; optional mmap, async, HSDS, MPI)
|
│ ├── clawhdf5-io — I/O abstraction (file/memory readers; optional mmap, async, HSDS, MPI)
|
||||||
│ ├── clawhdf5-filters — Fast deflate path (zlib-ng); the filter registry and the lz4/zstd/pcodec/szip/LZF/bitshuffle/bzip2/Blosc filters live in clawhdf5-format
|
│ ├── clawhdf5-filters — Fast deflate path (zlib-ng); the filter registry and the lz4/zstd/pcodec/szip/LZF/bitshuffle/bzip2/Blosc/Blosc2 filters live in clawhdf5-format
|
||||||
│ ├── clawhdf5-derive — Proc macros
|
│ ├── clawhdf5-derive — Proc macros
|
||||||
│ ├── clawhdf5 — High-level API
|
│ ├── clawhdf5 — High-level API
|
||||||
│ ├── clawhdf5-netcdf4 — NetCDF-4 support
|
│ ├── clawhdf5-netcdf4 — NetCDF-4 support
|
||||||
│ ├── clawhdf5-accel — SIMD (AVX2, NEON incl. SDOT int8; AVX-512 behind `avx512`)
|
│ ├── clawhdf5-accel — SIMD (AVX2, NEON incl. SDOT int8; AVX-512 behind `avx512`)
|
||||||
│ └── clawhdf5-gpu — GPU compute (wgpu, hand-written WGSL compute shaders)
|
│ ├── clawhdf5-gpu — GPU compute (wgpu, hand-written WGSL compute shaders)
|
||||||
|
│ └── clawhdf5-remote — Remote files: HTTP(S) range requests, object stores, block cache
|
||||||
│
|
│
|
||||||
├── Agent Memory
|
├── Agent Memory
|
||||||
│ ├── clawhdf5-agent — Memory engine (24.7K lines, 32 modules; chained-CRC WAL)
|
│ ├── clawhdf5-agent — Memory engine (24.7K lines, 32 modules; chained-CRC WAL)
|
||||||
@@ -688,6 +760,7 @@ clawhdf5 workspace (17 crates, ~86K lines of Rust in src/, ~104K with tests
|
|||||||
│ └── clawhdf5-wasm — Browser (WebAssembly, wasm-bindgen; read-only)
|
│ └── clawhdf5-wasm — Browser (WebAssembly, wasm-bindgen; read-only)
|
||||||
│
|
│
|
||||||
└── Tooling
|
└── Tooling
|
||||||
|
├── clawhdf5-tools — h5rs: ls, dump, stat, diff, check
|
||||||
└── clawhdf5-bench — Benchmark suite
|
└── clawhdf5-bench — Benchmark suite
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -779,13 +852,15 @@ stores keep their setting. Opt out with `float16 = false` or
|
|||||||
| `bitshuffle` | no | Bitshuffle filter (id 32008) with its LZ4 and Zstandard modes: read and write. Pure Rust (lz4_flex, ruzstd) |
|
| `bitshuffle` | no | Bitshuffle filter (id 32008) with its LZ4 and Zstandard modes: read and write. Pure Rust (lz4_flex, ruzstd) |
|
||||||
| `bzip2` | no | bzip2 filter (id 307): read and write. Pure Rust (the `bzip2` crate's libbz2-rs-sys backend compiles no C) |
|
| `bzip2` | no | bzip2 filter (id 307): read and write. Pure Rust (the `bzip2` crate's libbz2-rs-sys backend compiles no C) |
|
||||||
| `blosc` | no | Blosc 1 filter (id 32001): reads BloscLZ, LZ4/LZ4HC, Snappy, Zlib and Zstandard frames with byte or bit shuffle; writes LZ4, Snappy, Zlib or Zstandard (not BloscLZ). Pure Rust |
|
| `blosc` | no | Blosc 1 filter (id 32001): reads BloscLZ, LZ4/LZ4HC, Snappy, Zlib and Zstandard frames with byte or bit shuffle; writes LZ4, Snappy, Zlib or Zstandard (not BloscLZ). Pure Rust |
|
||||||
| `plugin-filters` | no | All four above |
|
| `blosc2` | no | Blosc2 filter (id 32026), read only: hdf5plugin's frames and B2ND (n-D) chunks, BloscLZ, LZ4/LZ4HC, Zlib and Zstandard, with shuffle, bit shuffle, delta or truncated precision. Pure Rust |
|
||||||
|
| `zfp` | no | ZFP filter (id 32013, H5Z-ZFP), read only: every mode (rate, precision, accuracy, reversible, expert) for int32, int64, float and double, 1-4-D, returning exactly libzfp's values. Pure Rust, no dependencies |
|
||||||
|
| `plugin-filters` | no | All six above |
|
||||||
|
|
||||||
Blosc2 (32026) and ZFP (32013) are not implemented: reading them fails with
|
clawhdf5 cannot write Blosc2 or ZFP. Any other
|
||||||
`UnsupportedFilter`, whose message names the filter. Any other filter can be
|
filter can be supplied at run time with `filter_registry::register_filter` (a
|
||||||
supplied at run time with `filter_registry::register_filter` (a decoder
|
decoder closure, or a `FilterCodec` that also encodes). The facade
|
||||||
closure, or a `FilterCodec` that also encodes). The facade (`clawhdf5`)
|
(`clawhdf5`) forwards `lzf`, `bitshuffle`, `bzip2`, `blosc`, `blosc2`, `zfp`
|
||||||
forwards `lzf`, `bitshuffle`, `bzip2`, `blosc` and `plugin-filters`. Write
|
and `plugin-filters`. Write
|
||||||
with `DatasetBuilder::with_lzf()`, `with_bitshuffle(..)`, `with_bzip2(..)`
|
with `DatasetBuilder::with_lzf()`, `with_bitshuffle(..)`, `with_bzip2(..)`
|
||||||
and `with_blosc(..)`; h5py + hdf5plugin read the result (tested both ways in
|
and `with_blosc(..)`; h5py + hdf5plugin read the result (tested both ways in
|
||||||
`crates/clawhdf5/tests/plugin_filters_interop.rs`). The pure-Rust Zstandard
|
`crates/clawhdf5/tests/plugin_filters_interop.rs`). The pure-Rust Zstandard
|
||||||
|
|||||||
+36
-12
@@ -1,15 +1,15 @@
|
|||||||
{
|
{
|
||||||
"comment": "conformance/run.sh fails if the ok count drops below `ok` or a file in `ok_files` stops being ok. Regenerate with `conformance/run.sh --update-baseline` after an intended change.",
|
"comment": "conformance/run.sh fails if the ok count drops below `ok` or a file in `ok_files` stops being ok. Regenerate with `conformance/run.sh --update-baseline` after an intended change.",
|
||||||
"commit": "73a01f1256fb9bf1b1e7601f755af9e8273cec4e",
|
"commit": "f37e7ae3263277319dba4bc39be5397194eb00c3",
|
||||||
"date": "2026-09-26 14:18 UTC",
|
"date": "2026-09-27 00:34 UTC",
|
||||||
"reference": "h5py 3.16.0 / HDF5 2.0.0",
|
"reference": "h5py 3.16.0 / HDF5 2.0.0",
|
||||||
"files": 697,
|
"files": 697,
|
||||||
"ok": 575,
|
"ok": 600,
|
||||||
"counts": {
|
"counts": {
|
||||||
"h5py-cannot-read": 92,
|
"h5py-cannot-read": 92,
|
||||||
"mismatch": 20,
|
"mismatch": 2,
|
||||||
"ok": 575,
|
"ok": 600,
|
||||||
"our-error": 10
|
"our-error": 3
|
||||||
},
|
},
|
||||||
"per_corpus": {
|
"per_corpus": {
|
||||||
"NCAS-CMS_pyfive": {
|
"NCAS-CMS_pyfive": {
|
||||||
@@ -18,18 +18,17 @@
|
|||||||
},
|
},
|
||||||
"cve_hdf5": {
|
"cve_hdf5": {
|
||||||
"h5py-cannot-read": 32,
|
"h5py-cannot-read": 32,
|
||||||
"mismatch": 9,
|
"ok": 113,
|
||||||
"ok": 100,
|
"our-error": 2
|
||||||
"our-error": 6
|
|
||||||
},
|
},
|
||||||
"h5py_data": {
|
"h5py_data": {
|
||||||
"ok": 4
|
"ok": 4
|
||||||
},
|
},
|
||||||
"hdf5": {
|
"hdf5": {
|
||||||
"h5py-cannot-read": 60,
|
"h5py-cannot-read": 60,
|
||||||
"mismatch": 10,
|
"mismatch": 1,
|
||||||
"ok": 392,
|
"ok": 404,
|
||||||
"our-error": 4
|
"our-error": 1
|
||||||
},
|
},
|
||||||
"netcdf-c": {
|
"netcdf-c": {
|
||||||
"ok": 20
|
"ok": 20
|
||||||
@@ -117,14 +116,22 @@
|
|||||||
"cve_hdf5/cvefiles/cve-2018-17434.h5",
|
"cve_hdf5/cvefiles/cve-2018-17434.h5",
|
||||||
"cve_hdf5/cvefiles/cve-2018-17435.h5",
|
"cve_hdf5/cvefiles/cve-2018-17435.h5",
|
||||||
"cve_hdf5/cvefiles/cve-2018-17437.h5",
|
"cve_hdf5/cvefiles/cve-2018-17437.h5",
|
||||||
|
"cve_hdf5/cvefiles/cve-2018-17438",
|
||||||
|
"cve_hdf5/cvefiles/cve-2018-17439",
|
||||||
"cve_hdf5/cvefiles/cve-2019-8396.h5",
|
"cve_hdf5/cvefiles/cve-2019-8396.h5",
|
||||||
|
"cve_hdf5/cvefiles/cve-2019-8397.h5",
|
||||||
|
"cve_hdf5/cvefiles/cve-2019-8398.h5",
|
||||||
|
"cve_hdf5/cvefiles/cve-2019-9151.h5",
|
||||||
"cve_hdf5/cvefiles/cve-2019-9152.h5",
|
"cve_hdf5/cvefiles/cve-2019-9152.h5",
|
||||||
"cve_hdf5/cvefiles/cve-2020-10811.h5",
|
"cve_hdf5/cvefiles/cve-2020-10811.h5",
|
||||||
"cve_hdf5/cvefiles/cve-2020-18232.h5",
|
"cve_hdf5/cvefiles/cve-2020-18232.h5",
|
||||||
|
"cve_hdf5/cvefiles/cve-2020-18494.h5",
|
||||||
"cve_hdf5/cvefiles/cve-2021-36977.h5",
|
"cve_hdf5/cvefiles/cve-2021-36977.h5",
|
||||||
"cve_hdf5/cvefiles/cve-2021-37501.h5",
|
"cve_hdf5/cvefiles/cve-2021-37501.h5",
|
||||||
"cve_hdf5/cvefiles/cve-2021-45829.h5",
|
"cve_hdf5/cvefiles/cve-2021-45829.h5",
|
||||||
"cve_hdf5/cvefiles/cve-2021-45833.h5",
|
"cve_hdf5/cvefiles/cve-2021-45833.h5",
|
||||||
|
"cve_hdf5/cvefiles/cve-2021-46243.h5",
|
||||||
|
"cve_hdf5/cvefiles/cve-2021-46244.h5",
|
||||||
"cve_hdf5/cvefiles/cve-2024-29157.h5",
|
"cve_hdf5/cvefiles/cve-2024-29157.h5",
|
||||||
"cve_hdf5/cvefiles/cve-2024-29158.h5",
|
"cve_hdf5/cvefiles/cve-2024-29158.h5",
|
||||||
"cve_hdf5/cvefiles/cve-2024-29159.h5",
|
"cve_hdf5/cvefiles/cve-2024-29159.h5",
|
||||||
@@ -148,18 +155,23 @@
|
|||||||
"cve_hdf5/cvefiles/cve-2024-32615.h5",
|
"cve_hdf5/cvefiles/cve-2024-32615.h5",
|
||||||
"cve_hdf5/cvefiles/cve-2024-32616.h5",
|
"cve_hdf5/cvefiles/cve-2024-32616.h5",
|
||||||
"cve_hdf5/cvefiles/cve-2024-32617.h5",
|
"cve_hdf5/cvefiles/cve-2024-32617.h5",
|
||||||
|
"cve_hdf5/cvefiles/cve-2024-32618.h5",
|
||||||
"cve_hdf5/cvefiles/cve-2024-32619.h5",
|
"cve_hdf5/cvefiles/cve-2024-32619.h5",
|
||||||
"cve_hdf5/cvefiles/cve-2024-32620.h5",
|
"cve_hdf5/cvefiles/cve-2024-32620.h5",
|
||||||
"cve_hdf5/cvefiles/cve-2024-32621.h5",
|
"cve_hdf5/cvefiles/cve-2024-32621.h5",
|
||||||
"cve_hdf5/cvefiles/cve-2024-32622.h5",
|
"cve_hdf5/cvefiles/cve-2024-32622.h5",
|
||||||
|
"cve_hdf5/cvefiles/cve-2024-32623.h5",
|
||||||
"cve_hdf5/cvefiles/cve-2024-32624.h5",
|
"cve_hdf5/cvefiles/cve-2024-32624.h5",
|
||||||
"cve_hdf5/cvefiles/cve-2024-33873.h5",
|
"cve_hdf5/cvefiles/cve-2024-33873.h5",
|
||||||
|
"cve_hdf5/cvefiles/cve-2024-33874.h5",
|
||||||
"cve_hdf5/cvefiles/cve-2024-33875.h5",
|
"cve_hdf5/cvefiles/cve-2024-33875.h5",
|
||||||
"cve_hdf5/cvefiles/cve-2024-33876.h5",
|
"cve_hdf5/cvefiles/cve-2024-33876.h5",
|
||||||
"cve_hdf5/cvefiles/cve-2024-33877.h5",
|
"cve_hdf5/cvefiles/cve-2024-33877.h5",
|
||||||
|
"cve_hdf5/cvefiles/cve-2025-2309.h5",
|
||||||
"cve_hdf5/cvefiles/cve-2025-2310.h5",
|
"cve_hdf5/cvefiles/cve-2025-2310.h5",
|
||||||
"cve_hdf5/cvefiles/cve-2025-2924.h5",
|
"cve_hdf5/cvefiles/cve-2025-2924.h5",
|
||||||
"cve_hdf5/cvefiles/cve-2025-2925.h5",
|
"cve_hdf5/cvefiles/cve-2025-2925.h5",
|
||||||
|
"cve_hdf5/cvefiles/cve-2025-44905.h5",
|
||||||
"cve_hdf5/cvefiles/cve-2025-6269-1.h5",
|
"cve_hdf5/cvefiles/cve-2025-6269-1.h5",
|
||||||
"cve_hdf5/cvefiles/cve-2025-6269-2.h5",
|
"cve_hdf5/cvefiles/cve-2025-6269-2.h5",
|
||||||
"cve_hdf5/cvefiles/cve-2025-6269-3.h5",
|
"cve_hdf5/cvefiles/cve-2025-6269-3.h5",
|
||||||
@@ -183,12 +195,14 @@
|
|||||||
"h5py_data/vlen_string_s390x.h5",
|
"h5py_data/vlen_string_s390x.h5",
|
||||||
"hdf5/HDF5Examples/C/H5FLT/tfiles/h5ex_d_bitgroom.h5",
|
"hdf5/HDF5Examples/C/H5FLT/tfiles/h5ex_d_bitgroom.h5",
|
||||||
"hdf5/HDF5Examples/C/H5FLT/tfiles/h5ex_d_blosc.h5",
|
"hdf5/HDF5Examples/C/H5FLT/tfiles/h5ex_d_blosc.h5",
|
||||||
|
"hdf5/HDF5Examples/C/H5FLT/tfiles/h5ex_d_blosc2.h5",
|
||||||
"hdf5/HDF5Examples/C/H5FLT/tfiles/h5ex_d_bshuf.h5",
|
"hdf5/HDF5Examples/C/H5FLT/tfiles/h5ex_d_bshuf.h5",
|
||||||
"hdf5/HDF5Examples/C/H5FLT/tfiles/h5ex_d_bzip2.h5",
|
"hdf5/HDF5Examples/C/H5FLT/tfiles/h5ex_d_bzip2.h5",
|
||||||
"hdf5/HDF5Examples/C/H5FLT/tfiles/h5ex_d_granularbr.h5",
|
"hdf5/HDF5Examples/C/H5FLT/tfiles/h5ex_d_granularbr.h5",
|
||||||
"hdf5/HDF5Examples/C/H5FLT/tfiles/h5ex_d_jpeg.h5",
|
"hdf5/HDF5Examples/C/H5FLT/tfiles/h5ex_d_jpeg.h5",
|
||||||
"hdf5/HDF5Examples/C/H5FLT/tfiles/h5ex_d_lz4.h5",
|
"hdf5/HDF5Examples/C/H5FLT/tfiles/h5ex_d_lz4.h5",
|
||||||
"hdf5/HDF5Examples/C/H5FLT/tfiles/h5ex_d_lzf.h5",
|
"hdf5/HDF5Examples/C/H5FLT/tfiles/h5ex_d_lzf.h5",
|
||||||
|
"hdf5/HDF5Examples/C/H5FLT/tfiles/h5ex_d_zfp.h5",
|
||||||
"hdf5/HDF5Examples/C/H5FLT/tfiles/h5ex_d_zstd.h5",
|
"hdf5/HDF5Examples/C/H5FLT/tfiles/h5ex_d_zstd.h5",
|
||||||
"hdf5/HDF5Examples/C/H5G/16/h5ex_g_iterate.h5",
|
"hdf5/HDF5Examples/C/H5G/16/h5ex_g_iterate.h5",
|
||||||
"hdf5/HDF5Examples/C/H5G/16/h5ex_g_traverse.h5",
|
"hdf5/HDF5Examples/C/H5G/16/h5ex_g_traverse.h5",
|
||||||
@@ -264,6 +278,7 @@
|
|||||||
"hdf5/test/testfiles/tmtimeo.h5",
|
"hdf5/test/testfiles/tmtimeo.h5",
|
||||||
"hdf5/test/testfiles/tnullspace.h5",
|
"hdf5/test/testfiles/tnullspace.h5",
|
||||||
"hdf5/test/testfiles/tsizeslheap.h5",
|
"hdf5/test/testfiles/tsizeslheap.h5",
|
||||||
|
"hdf5/tools/test/testfiles/bigendian/tall.h5",
|
||||||
"hdf5/tools/test/testfiles/bigendian/tdset2.h5",
|
"hdf5/tools/test/testfiles/bigendian/tdset2.h5",
|
||||||
"hdf5/tools/test/testfiles/binfp64.h5",
|
"hdf5/tools/test/testfiles/binfp64.h5",
|
||||||
"hdf5/tools/test/testfiles/binin16.h5",
|
"hdf5/tools/test/testfiles/binin16.h5",
|
||||||
@@ -284,6 +299,7 @@
|
|||||||
"hdf5/tools/test/testfiles/h5clear_fsm_persist_noclose.h5",
|
"hdf5/tools/test/testfiles/h5clear_fsm_persist_noclose.h5",
|
||||||
"hdf5/tools/test/testfiles/h5clear_fsm_persist_user_equal.h5",
|
"hdf5/tools/test/testfiles/h5clear_fsm_persist_user_equal.h5",
|
||||||
"hdf5/tools/test/testfiles/h5clear_fsm_persist_user_less.h5",
|
"hdf5/tools/test/testfiles/h5clear_fsm_persist_user_less.h5",
|
||||||
|
"hdf5/tools/test/testfiles/h5clear_mdc_image.h5",
|
||||||
"hdf5/tools/test/testfiles/h5clear_sec2_v0.h5",
|
"hdf5/tools/test/testfiles/h5clear_sec2_v0.h5",
|
||||||
"hdf5/tools/test/testfiles/h5clear_sec2_v2.h5",
|
"hdf5/tools/test/testfiles/h5clear_sec2_v2.h5",
|
||||||
"hdf5/tools/test/testfiles/h5copy_extlinks_src.h5",
|
"hdf5/tools/test/testfiles/h5copy_extlinks_src.h5",
|
||||||
@@ -337,6 +353,7 @@
|
|||||||
"hdf5/tools/test/testfiles/h5diff_softlinks.h5",
|
"hdf5/tools/test/testfiles/h5diff_softlinks.h5",
|
||||||
"hdf5/tools/test/testfiles/h5diff_strings1.h5",
|
"hdf5/tools/test/testfiles/h5diff_strings1.h5",
|
||||||
"hdf5/tools/test/testfiles/h5diff_strings2.h5",
|
"hdf5/tools/test/testfiles/h5diff_strings2.h5",
|
||||||
|
"hdf5/tools/test/testfiles/h5diff_types.h5",
|
||||||
"hdf5/tools/test/testfiles/h5fc_edge_v3.h5",
|
"hdf5/tools/test/testfiles/h5fc_edge_v3.h5",
|
||||||
"hdf5/tools/test/testfiles/h5fc_err_level.h5",
|
"hdf5/tools/test/testfiles/h5fc_err_level.h5",
|
||||||
"hdf5/tools/test/testfiles/h5fc_ext1_f.h5",
|
"hdf5/tools/test/testfiles/h5fc_ext1_f.h5",
|
||||||
@@ -414,9 +431,11 @@
|
|||||||
"hdf5/tools/test/testfiles/tCVE_2018_11206_fill_new.h5",
|
"hdf5/tools/test/testfiles/tCVE_2018_11206_fill_new.h5",
|
||||||
"hdf5/tools/test/testfiles/tCVE_2018_11206_fill_old.h5",
|
"hdf5/tools/test/testfiles/tCVE_2018_11206_fill_old.h5",
|
||||||
"hdf5/tools/test/testfiles/taindices.h5",
|
"hdf5/tools/test/testfiles/taindices.h5",
|
||||||
|
"hdf5/tools/test/testfiles/tall.h5",
|
||||||
"hdf5/tools/test/testfiles/tarray1.h5",
|
"hdf5/tools/test/testfiles/tarray1.h5",
|
||||||
"hdf5/tools/test/testfiles/tarray1_big.h5",
|
"hdf5/tools/test/testfiles/tarray1_big.h5",
|
||||||
"hdf5/tools/test/testfiles/tarray2.h5",
|
"hdf5/tools/test/testfiles/tarray2.h5",
|
||||||
|
"hdf5/tools/test/testfiles/tarray3.h5",
|
||||||
"hdf5/tools/test/testfiles/tarray4.h5",
|
"hdf5/tools/test/testfiles/tarray4.h5",
|
||||||
"hdf5/tools/test/testfiles/tarray5.h5",
|
"hdf5/tools/test/testfiles/tarray5.h5",
|
||||||
"hdf5/tools/test/testfiles/tarray8.h5",
|
"hdf5/tools/test/testfiles/tarray8.h5",
|
||||||
@@ -449,6 +468,7 @@
|
|||||||
"hdf5/tools/test/testfiles/textlinksrc.h5",
|
"hdf5/tools/test/testfiles/textlinksrc.h5",
|
||||||
"hdf5/tools/test/testfiles/textlinktar.h5",
|
"hdf5/tools/test/testfiles/textlinktar.h5",
|
||||||
"hdf5/tools/test/testfiles/textpfe.h5",
|
"hdf5/tools/test/testfiles/textpfe.h5",
|
||||||
|
"hdf5/tools/test/testfiles/tfcontents1.h5",
|
||||||
"hdf5/tools/test/testfiles/tfcontents2.h5",
|
"hdf5/tools/test/testfiles/tfcontents2.h5",
|
||||||
"hdf5/tools/test/testfiles/tfilters.h5",
|
"hdf5/tools/test/testfiles/tfilters.h5",
|
||||||
"hdf5/tools/test/testfiles/tfloat16.h5",
|
"hdf5/tools/test/testfiles/tfloat16.h5",
|
||||||
@@ -505,6 +525,7 @@
|
|||||||
"hdf5/tools/test/testfiles/tstr3.h5",
|
"hdf5/tools/test/testfiles/tstr3.h5",
|
||||||
"hdf5/tools/test/testfiles/tudfilter.h5",
|
"hdf5/tools/test/testfiles/tudfilter.h5",
|
||||||
"hdf5/tools/test/testfiles/tudfilter2.h5",
|
"hdf5/tools/test/testfiles/tudfilter2.h5",
|
||||||
|
"hdf5/tools/test/testfiles/tudlink.h5",
|
||||||
"hdf5/tools/test/testfiles/tvldtypes1.h5",
|
"hdf5/tools/test/testfiles/tvldtypes1.h5",
|
||||||
"hdf5/tools/test/testfiles/tvldtypes2.h5",
|
"hdf5/tools/test/testfiles/tvldtypes2.h5",
|
||||||
"hdf5/tools/test/testfiles/tvldtypes3.h5",
|
"hdf5/tools/test/testfiles/tvldtypes3.h5",
|
||||||
@@ -513,6 +534,8 @@
|
|||||||
"hdf5/tools/test/testfiles/tvlenstr_array.h5",
|
"hdf5/tools/test/testfiles/tvlenstr_array.h5",
|
||||||
"hdf5/tools/test/testfiles/tvlstr.h5",
|
"hdf5/tools/test/testfiles/tvlstr.h5",
|
||||||
"hdf5/tools/test/testfiles/tvms.h5",
|
"hdf5/tools/test/testfiles/tvms.h5",
|
||||||
|
"hdf5/tools/test/testfiles/twithub.h5",
|
||||||
|
"hdf5/tools/test/testfiles/twithub513.h5",
|
||||||
"hdf5/tools/test/testfiles/txtfp32.h5",
|
"hdf5/tools/test/testfiles/txtfp32.h5",
|
||||||
"hdf5/tools/test/testfiles/txtfp64.h5",
|
"hdf5/tools/test/testfiles/txtfp64.h5",
|
||||||
"hdf5/tools/test/testfiles/txtin16.h5",
|
"hdf5/tools/test/testfiles/txtin16.h5",
|
||||||
@@ -558,6 +581,7 @@
|
|||||||
"hdf5/tools/test/testfiles/xml/tenum.h5",
|
"hdf5/tools/test/testfiles/xml/tenum.h5",
|
||||||
"hdf5/tools/test/testfiles/xml/test35.nc",
|
"hdf5/tools/test/testfiles/xml/test35.nc",
|
||||||
"hdf5/tools/test/testfiles/xml/tloop2.h5",
|
"hdf5/tools/test/testfiles/xml/tloop2.h5",
|
||||||
|
"hdf5/tools/test/testfiles/xml/tmany.h5",
|
||||||
"hdf5/tools/test/testfiles/xml/tname-amp.h5",
|
"hdf5/tools/test/testfiles/xml/tname-amp.h5",
|
||||||
"hdf5/tools/test/testfiles/xml/tname-apos.h5",
|
"hdf5/tools/test/testfiles/xml/tname-apos.h5",
|
||||||
"hdf5/tools/test/testfiles/xml/tname-gt.h5",
|
"hdf5/tools/test/testfiles/xml/tname-gt.h5",
|
||||||
|
|||||||
@@ -146,7 +146,13 @@ for rel in files:
|
|||||||
if a.get("kind") != b.get("kind") and "error" not in b and "error" not in a:
|
if a.get("kind") != b.get("kind") and "error" not in b and "error" not in a:
|
||||||
issues.append(("mismatch", f"{p}: kind {a.get('kind')} vs ours {b.get('kind')}", "kind", b))
|
issues.append(("mismatch", f"{p}: kind {a.get('kind')} vs ours {b.get('kind')}", "kind", b))
|
||||||
ok = False
|
ok = False
|
||||||
|
# h5py could not open the object at all: it read none of its
|
||||||
|
# attributes or links, so there is nothing to compare ours with
|
||||||
|
# (the object's own error is compared above and below).
|
||||||
|
ref_unopened = a.get("kind") == "unknown" and "error" in a
|
||||||
for k in ("error", "list_error", "attrs_error"):
|
for k in ("error", "list_error", "attrs_error"):
|
||||||
|
if ref_unopened and k != "error":
|
||||||
|
continue
|
||||||
if k in b and k not in a:
|
if k in b and k not in a:
|
||||||
issues.append(("our-error", f"{p}: {k}: {b[k]}", b[k], b))
|
issues.append(("our-error", f"{p}: {k}: {b[k]}", b[k], b))
|
||||||
ok = False
|
ok = False
|
||||||
@@ -164,7 +170,7 @@ for rel in files:
|
|||||||
issues.append(("mismatch", f"{p}: values differ (h5py {a.get('dtype')} vs ours {b.get('dtype')})", "values", b | {"ref_head": a.get("head"), "ref_dtype": a.get("dtype")}))
|
issues.append(("mismatch", f"{p}: values differ (h5py {a.get('dtype')} vs ours {b.get('dtype')})", "values", b | {"ref_head": a.get("head"), "ref_dtype": a.get("dtype")}))
|
||||||
ok = False
|
ok = False
|
||||||
ra, oa = a.get("attrs") or {}, b.get("attrs") or {}
|
ra, oa = a.get("attrs") or {}, b.get("attrs") or {}
|
||||||
if "attrs_error" not in b and "attrs_error" not in a:
|
if "attrs_error" not in b and "attrs_error" not in a and not ref_unopened:
|
||||||
for an in sorted(set(ra) | set(oa)):
|
for an in sorted(set(ra) | set(oa)):
|
||||||
x, y = ra.get(an), oa.get(an)
|
x, y = ra.get(an), oa.get(an)
|
||||||
if x is None:
|
if x is None:
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ use clawhdf5_format::filter_pipeline::FilterPipeline;
|
|||||||
use clawhdf5_format::group_v1::{self, GroupEntry};
|
use clawhdf5_format::group_v1::{self, GroupEntry};
|
||||||
use clawhdf5_format::group_v2;
|
use clawhdf5_format::group_v2;
|
||||||
use clawhdf5_format::message_type::MessageType;
|
use clawhdf5_format::message_type::MessageType;
|
||||||
use clawhdf5_format::object_header::ObjectHeader;
|
use clawhdf5_format::object_header::{ObjectClass, ObjectHeader};
|
||||||
use clawhdf5_format::signature;
|
use clawhdf5_format::signature;
|
||||||
use clawhdf5_format::superblock::Superblock;
|
use clawhdf5_format::superblock::Superblock;
|
||||||
use clawhdf5_format::symbol_table::SymbolTableMessage;
|
use clawhdf5_format::symbol_table::SymbolTableMessage;
|
||||||
@@ -308,18 +308,20 @@ impl<'a> Ctx<'a> {
|
|||||||
)
|
)
|
||||||
.map_err(e)?;
|
.map_err(e)?;
|
||||||
}
|
}
|
||||||
let (shape, n) = Self::shape(&ds);
|
|
||||||
rec.insert("shape".into(), shape);
|
|
||||||
if n.saturating_mul(dt.type_size() as u64) > MAX_BYTES {
|
|
||||||
rec.insert("skipped".into(), Value::String("too large".into()));
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
let lm = h
|
let lm = h
|
||||||
.messages
|
.messages
|
||||||
.iter()
|
.iter()
|
||||||
.find(|m| m.msg_type == MessageType::DataLayout)
|
.find(|m| m.msg_type == MessageType::DataLayout)
|
||||||
.ok_or("MissingMessage(DataLayout)")?;
|
.ok_or("MissingMessage(DataLayout)")?;
|
||||||
let dl = DataLayout::parse(&lm.data, self.os, self.ls).map_err(e)?;
|
let dl = DataLayout::parse(&lm.data, self.os, self.ls).map_err(e)?;
|
||||||
|
// What libhdf5 checks when it opens the dataset (as File::dataset).
|
||||||
|
data_read::check_dataset_storage(&dl, &ds, &dt, self.data.len() as u64).map_err(e)?;
|
||||||
|
let (shape, n) = Self::shape(&ds);
|
||||||
|
rec.insert("shape".into(), shape);
|
||||||
|
if n.saturating_mul(dt.type_size() as u64) > MAX_BYTES {
|
||||||
|
rec.insert("skipped".into(), Value::String("too large".into()));
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
rec.insert(
|
rec.insert(
|
||||||
"layout".into(),
|
"layout".into(),
|
||||||
Value::String(
|
Value::String(
|
||||||
@@ -657,6 +659,20 @@ fn is_group(h: &ObjectHeader) -> bool {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The probe's kind for an object header: libhdf5's object class
|
||||||
|
/// ([`ObjectHeader::object_class`]: group, then dataset — a datatype *and* a
|
||||||
|
/// dataspace — then named datatype), which is what h5py opens the object as.
|
||||||
|
/// The root group, and a header with only link messages, count as groups.
|
||||||
|
fn kind_of(h: &ObjectHeader, is_root: bool) -> &'static str {
|
||||||
|
match h.object_class() {
|
||||||
|
Some(ObjectClass::Group) => "group",
|
||||||
|
Some(ObjectClass::Dataset) => "dataset",
|
||||||
|
_ if is_root || is_group(h) => "group",
|
||||||
|
Some(ObjectClass::NamedDatatype) => "datatype",
|
||||||
|
None => "unknown",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
install_hook();
|
install_hook();
|
||||||
let path = std::env::args().nth(1).expect("usage: probe <file>");
|
let path = std::env::args().nth(1).expect("usage: probe <file>");
|
||||||
@@ -696,6 +712,42 @@ fn main() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
// libhdf5 decodes the superblock extension at open (an error refuses
|
||||||
|
// the file), and loads a metadata cache image over the file's own
|
||||||
|
// metadata. It loads the image only when it first reads metadata — the
|
||||||
|
// root group — so a file whose image it cannot load still opens and
|
||||||
|
// that read fails. The library decides all three cases with the same
|
||||||
|
// `cache_image_state`: `File` and `MmapFile` open such a file and fail
|
||||||
|
// every object lookup with the image's error, which is what the probe
|
||||||
|
// records here (on the root group, where libhdf5 reports it).
|
||||||
|
use clawhdf5_format::superblock_ext::{self, CacheImageState};
|
||||||
|
let state = match guarded(|| superblock_ext::cache_image_state(hdf5, &sb).map_err(e)) {
|
||||||
|
Ok(x) => x,
|
||||||
|
Err(msg) => {
|
||||||
|
top.insert("open_error".into(), Value::String(msg));
|
||||||
|
println!("{}", Value::Object(top));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let mut image_error = None;
|
||||||
|
let view = match state {
|
||||||
|
CacheImageState::Absent => None,
|
||||||
|
CacheImageState::Unloadable(err) => {
|
||||||
|
image_error = Some(e(err));
|
||||||
|
None
|
||||||
|
}
|
||||||
|
CacheImageState::Loaded(image) => {
|
||||||
|
let mut v = hdf5.to_vec();
|
||||||
|
match image.block(hdf5).and_then(|b| image.apply(b, &mut v)) {
|
||||||
|
Ok(()) => Some(v),
|
||||||
|
Err(err) => {
|
||||||
|
image_error = Some(e(err));
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let hdf5: &[u8] = view.as_deref().unwrap_or(hdf5);
|
||||||
top.insert("superblock_version".into(), json!(sb.version));
|
top.insert("superblock_version".into(), json!(sb.version));
|
||||||
let ctx = Ctx {
|
let ctx = Ctx {
|
||||||
data: hdf5,
|
data: hdf5,
|
||||||
@@ -723,6 +775,9 @@ fn main() {
|
|||||||
let mut rec = Map::new();
|
let mut rec = Map::new();
|
||||||
rec.insert("path".into(), Value::String(p.clone()));
|
rec.insert("path".into(), Value::String(p.clone()));
|
||||||
let r = guarded(|| {
|
let r = guarded(|| {
|
||||||
|
if let Some(msg) = &image_error {
|
||||||
|
return Err(msg.clone());
|
||||||
|
}
|
||||||
let h = ctx.header(addr)?;
|
let h = ctx.header(addr)?;
|
||||||
Ok(h)
|
Ok(h)
|
||||||
});
|
});
|
||||||
@@ -735,23 +790,7 @@ fn main() {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let is_ds = h
|
let kind = kind_of(&h, addr == sb.root_group_address);
|
||||||
.messages
|
|
||||||
.iter()
|
|
||||||
.any(|m| m.msg_type == MessageType::DataLayout);
|
|
||||||
let kind = if is_ds {
|
|
||||||
"dataset"
|
|
||||||
} else if is_group(&h) || addr == sb.root_group_address {
|
|
||||||
"group"
|
|
||||||
} else if h
|
|
||||||
.messages
|
|
||||||
.iter()
|
|
||||||
.any(|m| m.msg_type == MessageType::Datatype)
|
|
||||||
{
|
|
||||||
"datatype"
|
|
||||||
} else {
|
|
||||||
"unknown"
|
|
||||||
};
|
|
||||||
rec.insert("kind".into(), Value::String(kind.into()));
|
rec.insert("kind".into(), Value::String(kind.into()));
|
||||||
if kind == "dataset"
|
if kind == "dataset"
|
||||||
&& let Err(msg) = guarded(|| ctx.read_dataset(&h, &mut rec))
|
&& let Err(msg) = guarded(|| ctx.read_dataset(&h, &mut rec))
|
||||||
@@ -867,6 +906,42 @@ mod tests {
|
|||||||
assert!(ieee_layout(&f32le));
|
assert!(ieee_layout(&f32le));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn kind_follows_libhdf5_object_class() {
|
||||||
|
use clawhdf5_format::object_header::HeaderMessage;
|
||||||
|
let header = |types: &[MessageType]| ObjectHeader {
|
||||||
|
version: 2,
|
||||||
|
messages: types
|
||||||
|
.iter()
|
||||||
|
.map(|&msg_type| HeaderMessage {
|
||||||
|
msg_type,
|
||||||
|
size: 0,
|
||||||
|
flags: 0,
|
||||||
|
creation_order: None,
|
||||||
|
data: Vec::new(),
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
reference_count: None,
|
||||||
|
flags: 0,
|
||||||
|
access_time: None,
|
||||||
|
modification_time: None,
|
||||||
|
change_time: None,
|
||||||
|
birth_time: None,
|
||||||
|
};
|
||||||
|
use MessageType::*;
|
||||||
|
// cve-2024-33874 `/Dset1`: a datatype and a layout but no dataspace
|
||||||
|
// is a named datatype to libhdf5 (h5py opens it as one).
|
||||||
|
assert_eq!(kind_of(&header(&[Datatype, DataLayout]), false), "datatype");
|
||||||
|
assert_eq!(
|
||||||
|
kind_of(&header(&[Datatype, Dataspace, DataLayout]), false),
|
||||||
|
"dataset"
|
||||||
|
);
|
||||||
|
assert_eq!(kind_of(&header(&[SymbolTable]), false), "group");
|
||||||
|
assert_eq!(kind_of(&header(&[Link]), false), "group");
|
||||||
|
assert_eq!(kind_of(&header(&[]), true), "group");
|
||||||
|
assert_eq!(kind_of(&header(&[]), false), "unknown");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn partial_precision_int_is_shifted_and_sign_extended() {
|
fn partial_precision_int_is_shifted_and_sign_extended() {
|
||||||
let dt = Datatype::FixedPoint {
|
let dt = Datatype::FixedPoint {
|
||||||
|
|||||||
+24
-10
@@ -111,8 +111,11 @@ def note_conversion(tid, dt, rec):
|
|||||||
|
|
||||||
|
|
||||||
def hash_values(arr, dt, rec):
|
def hash_values(arr, dt, rec):
|
||||||
if dt.subdtype is not None:
|
# h5py expands an HDF5 array element type into trailing array dims, a
|
||||||
# h5py expands an HDF5 array element type into trailing array dims
|
# nested array type (an array of arrays) into all of them. Converting the
|
||||||
|
# expanded array back to the inner subarray type would broadcast every
|
||||||
|
# element into a whole subarray, so strip every level.
|
||||||
|
while dt.subdtype is not None:
|
||||||
dt = dt.subdtype[0]
|
dt = dt.subdtype[0]
|
||||||
arr = np.asarray(arr, dtype=dt)
|
arr = np.asarray(arr, dtype=dt)
|
||||||
if simple(dt):
|
if simple(dt):
|
||||||
@@ -173,9 +176,13 @@ def main(path):
|
|||||||
return
|
return
|
||||||
objects = []
|
objects = []
|
||||||
seen = set()
|
seen = set()
|
||||||
stack = [("/", None)]
|
# Objects h5py cannot open have no ObjectID to deduplicate by; they are
|
||||||
|
# deduplicated by the address their hard link points at instead, as the
|
||||||
|
# probe deduplicates every object by header address.
|
||||||
|
seen_unopenable = set()
|
||||||
|
stack = [("/", None, None)]
|
||||||
while stack:
|
while stack:
|
||||||
p, obj = stack.pop()
|
p, obj, link_addr = stack.pop()
|
||||||
if len(objects) >= MAX_OBJECTS:
|
if len(objects) >= MAX_OBJECTS:
|
||||||
top["truncated"] = True
|
top["truncated"] = True
|
||||||
break
|
break
|
||||||
@@ -185,6 +192,10 @@ def main(path):
|
|||||||
obj = f[p]
|
obj = f[p]
|
||||||
key = hash(obj.id) # h5py ObjectID hash = (fileno, object address/token)
|
key = hash(obj.id) # h5py ObjectID hash = (fileno, object address/token)
|
||||||
except Exception as e: # noqa: BLE001
|
except Exception as e: # noqa: BLE001
|
||||||
|
if link_addr is not None:
|
||||||
|
if link_addr in seen_unopenable:
|
||||||
|
continue
|
||||||
|
seen_unopenable.add(link_addr)
|
||||||
rec["kind"] = "unknown"
|
rec["kind"] = "unknown"
|
||||||
rec["error"] = err(e)
|
rec["error"] = err(e)
|
||||||
objects.append(rec)
|
objects.append(rec)
|
||||||
@@ -232,15 +243,18 @@ def main(path):
|
|||||||
base = "" if p == "/" else p
|
base = "" if p == "/" else p
|
||||||
kids = []
|
kids = []
|
||||||
for n in names:
|
for n in names:
|
||||||
|
# The link's own type: `obj.get(n, getlink=True)` reports
|
||||||
|
# a user-defined link (type 64-255) as a HardLink.
|
||||||
try:
|
try:
|
||||||
link = obj.get(n, getlink=True)
|
info = obj.id.links.get_info(n.encode("utf-8", "surrogateescape"))
|
||||||
except Exception: # noqa: BLE001
|
except Exception: # noqa: BLE001
|
||||||
link = None
|
info = None
|
||||||
if link is not None and not isinstance(link, h5py.HardLink):
|
if info is not None and info.type != h5py.h5l.TYPE_HARD:
|
||||||
continue
|
continue
|
||||||
kids.append(f"{base}/{n}")
|
addr = info.u if info is not None else None
|
||||||
for k in reversed(kids):
|
kids.append((f"{base}/{n}", addr))
|
||||||
stack.append((k, None))
|
for k, addr in reversed(kids):
|
||||||
|
stack.append((k, None, addr))
|
||||||
except Exception as e: # noqa: BLE001
|
except Exception as e: # noqa: BLE001
|
||||||
rec["list_error"] = err(e)
|
rec["list_error"] = err(e)
|
||||||
objects.append(rec)
|
objects.append(rec)
|
||||||
|
|||||||
+39
-6
@@ -98,13 +98,37 @@ def is_h5py_be_vlen(i):
|
|||||||
and ">" in (i.get("ours_dtype") or ""))
|
and ">" in (i.get("ours_dtype") or ""))
|
||||||
|
|
||||||
|
|
||||||
|
# Objects the reference (h5py 3.16 / HDF5 2.0) reads only because of an
|
||||||
|
# HDF5 2.0 bug, and that clawhdf5 refuses: each one reads past a buffer or
|
||||||
|
# returns bytes the file does not hold, and libhdf5's develop branch refuses all
|
||||||
|
# three. (file, object) -> why. Checked 2026-09-26 against HDF5 2.0.0
|
||||||
|
# and HDFGroup/hdf5 develop sources; see docs/known-issues.md.
|
||||||
|
LIBHDF5_BUGS = {
|
||||||
|
("cve_hdf5/cvefiles/cve-2025-2308.h5", "/Scale_offset_long_long_data_le"):
|
||||||
|
"scale-offset codes run past the end of the chunk: HDF5 2.0 reads past its buffer; "
|
||||||
|
"libhdf5's develop branch refuses the chunk (\"Buffer too short\")",
|
||||||
|
("cve_hdf5/cvefiles/cve-2025-44904.h5", "/Scale_offset_float_data_le"):
|
||||||
|
"unfiltered chunks of 38 and 37 bytes for 48-byte chunks: HDF5 2.0 fills the rest with "
|
||||||
|
"whatever its buffer held; libhdf5's develop branch refuses them (\"incorrect chunk size returned "
|
||||||
|
"from index for unfiltered chunk\")",
|
||||||
|
("hdf5/test/testfiles/bad_nbit_parms_walk.h5", "/Nbit_int_data_le"):
|
||||||
|
"an N-Bit parameter list one value short: HDF5 2.0 reads past the list; libhdf5's own "
|
||||||
|
"test (`test_filter_bad_params`, test/dsets.c) now requires the read to fail",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def is_libhdf5_bug(rel, i):
|
||||||
|
return i["kind"] == "our-error" and any(
|
||||||
|
f == rel and i["detail"].startswith(obj + ":") for (f, obj) in LIBHDF5_BUGS)
|
||||||
|
|
||||||
|
|
||||||
known = collections.defaultdict(list)
|
known = collections.defaultdict(list)
|
||||||
for r in rows:
|
for r in rows:
|
||||||
if r["class"] != "mismatch":
|
|
||||||
continue
|
|
||||||
iss = issues.get(r["file"], [])
|
iss = issues.get(r["file"], [])
|
||||||
if iss and all(is_h5py_be_vlen(i) for i in iss):
|
if r["class"] == "mismatch" and iss and all(is_h5py_be_vlen(i) for i in iss):
|
||||||
known["h5py-be-vlen"].append(r["file"])
|
known["h5py-be-vlen"].append(r["file"])
|
||||||
|
if r["class"] == "our-error" and iss and all(is_libhdf5_bug(r["file"], i) for i in iss):
|
||||||
|
known["libhdf5-2.0"].append(r["file"])
|
||||||
|
|
||||||
|
|
||||||
# --- the CVE corpus: clawhdf5 vs h5dump vs h5py ------------------------------
|
# --- the CVE corpus: clawhdf5 vs h5dump vs h5py ------------------------------
|
||||||
@@ -230,9 +254,13 @@ for c in sorted(by_corpus):
|
|||||||
w(f"| {c} | {sum(cnt.values())} | " + " | ".join(str(cnt.get(k, 0)) for k in CLASSES) + " |")
|
w(f"| {c} | {sum(cnt.values())} | " + " | ".join(str(cnt.get(k, 0)) for k in CLASSES) + " |")
|
||||||
w(f"| **all** | **{len(rows)}** | " + " | ".join(f"**{total.get(k, 0)}**" for k in CLASSES) + " |")
|
w(f"| **all** | **{len(rows)}** | " + " | ".join(f"**{total.get(k, 0)}**" for k in CLASSES) + " |")
|
||||||
w("")
|
w("")
|
||||||
n_known = sum(len(v) for v in known.values())
|
if known["h5py-be-vlen"]:
|
||||||
if n_known:
|
w(f"{len(known['h5py-be-vlen'])} of the {total.get('mismatch', 0)} mismatches are a known h5py bug, "
|
||||||
w(f"{n_known} of the {total.get('mismatch', 0)} mismatches are a known h5py bug, not ours (see *Known not-our-bug*).")
|
"not ours (see *Known not-our-bug*).")
|
||||||
|
w("")
|
||||||
|
if known["libhdf5-2.0"]:
|
||||||
|
w(f"{len(known['libhdf5-2.0'])} of the {total.get('our-error', 0)} our-errors are corrupt data that "
|
||||||
|
"HDF5 2.0 reads only through a bug and clawhdf5 refuses (see *Known not-our-bug*).")
|
||||||
w("")
|
w("")
|
||||||
w("Corpora (fetched by `conformance/fetch-corpus.sh` into the gitignored `conformance/.cache/`):")
|
w("Corpora (fetched by `conformance/fetch-corpus.sh` into the gitignored `conformance/.cache/`):")
|
||||||
w("")
|
w("")
|
||||||
@@ -311,6 +339,11 @@ if res["incomparable"]:
|
|||||||
w(" (FP8 -> float16, bfloat16 -> float32, x87 long double -> float128) the values are not")
|
w(" (FP8 -> float16, bfloat16 -> float32, x87 long double -> float128) the values are not")
|
||||||
w(" compared (shape and presence still are): "
|
w(" compared (shape and presence still are): "
|
||||||
+ ", ".join(f"{k} ({n}x)" for k, n in res["incomparable"]) + ".")
|
+ ", ".join(f"{k} ({n}x)" for k, n in res["incomparable"]) + ".")
|
||||||
|
w("- **Corrupt data HDF5 2.0 reads through a bug.** clawhdf5 refuses these objects; h5py 3.16 /")
|
||||||
|
w(" HDF5 2.0 returns values for them that the file does not hold:")
|
||||||
|
for (f, obj), why in sorted(LIBHDF5_BUGS.items()):
|
||||||
|
here = "" if f in known["libhdf5-2.0"] else " (not an our-error in this run)"
|
||||||
|
w(f" - `{f}` `{obj}`: {why}{here}.")
|
||||||
w("- **References** are compared by presence only (`R`), not by target.")
|
w("- **References** are compared by presence only (`R`), not by target.")
|
||||||
w("")
|
w("")
|
||||||
if res.get("ref_only_errors"):
|
if res.get("ref_only_errors"):
|
||||||
|
|||||||
@@ -237,7 +237,10 @@ pub fn f16_to_f32_batch(input: &[u16], output: &mut [f32]) {
|
|||||||
convert::f16_to_f32_batch(input, output);
|
convert::f16_to_f32_batch(input, output);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Compute Fletcher-32 checksum.
|
/// Compute a textbook Fletcher-32 checksum (both sums start at 0xffff).
|
||||||
|
///
|
||||||
|
/// This is not HDF5's checksum; the Fletcher-32 I/O filter uses
|
||||||
|
/// `clawhdf5_format::checksum::fletcher32`.
|
||||||
pub fn checksum_fletcher32(data: &[u8]) -> u32 {
|
pub fn checksum_fletcher32(data: &[u8]) -> u32 {
|
||||||
checksum::checksum_fletcher32(data)
|
checksum::checksum_fletcher32(data)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,8 +76,16 @@ bitshuffle = ["lz4_flex", "ruzstd"]
|
|||||||
bzip2 = ["dep:bzip2", "std"]
|
bzip2 = ["dep:bzip2", "std"]
|
||||||
# Blosc 1 (32001) with its BloscLZ, LZ4, Snappy, Zlib and Zstandard codecs.
|
# Blosc 1 (32001) with its BloscLZ, LZ4, Snappy, Zlib and Zstandard codecs.
|
||||||
blosc = ["lz4_flex", "ruzstd", "snap", "deflate", "std"]
|
blosc = ["lz4_flex", "ruzstd", "snap", "deflate", "std"]
|
||||||
|
# Blosc2 (32026), read-only: frames, B2ND arrays, and the Blosc codecs above.
|
||||||
|
blosc2 = ["blosc"]
|
||||||
|
# ZFP (32013, H5Z-ZFP), read-only: every mode, for int32, int64, float and
|
||||||
|
# double fields of 1 to 4 dimensions.
|
||||||
|
zfp = []
|
||||||
# Every plugin filter above.
|
# Every plugin filter above.
|
||||||
plugin-filters = ["lzf", "bitshuffle", "bzip2", "blosc"]
|
plugin-filters = ["lzf", "bitshuffle", "bzip2", "blosc", "blosc2", "zfp"]
|
||||||
|
# Test instrumentation: per-thread counts of heap objects read (see
|
||||||
|
# `lookup_stats`), so tests can bound the cost of a name lookup.
|
||||||
|
lookup-stats = ["std"]
|
||||||
|
|
||||||
[[bench]]
|
[[bench]]
|
||||||
name = "parallel_decompress_bench"
|
name = "parallel_decompress_bench"
|
||||||
|
|||||||
@@ -0,0 +1,121 @@
|
|||||||
|
//! File address and length → in-memory index conversion.
|
||||||
|
//!
|
||||||
|
//! HDF5 addresses and lengths are 64-bit; the file is parsed through a
|
||||||
|
//! `&[u8]` indexed by `usize`. On a 64-bit target every `u64` fits, but on a
|
||||||
|
//! 32-bit one (`wasm32`, `i686`, `thumbv7em`) an address past `usize::MAX`
|
||||||
|
//! used to be truncated by an `as usize` cast — silently pointing at another
|
||||||
|
//! part of the file — or to panic. [`to_usize`] is the one conversion the
|
||||||
|
//! parsers use instead: such an address is a clean
|
||||||
|
//! [`FormatError::Overflow`]. It cannot be inside the data anyway: no slice
|
||||||
|
//! is longer than `isize::MAX` bytes.
|
||||||
|
|
||||||
|
#[cfg(not(feature = "std"))]
|
||||||
|
use alloc::format;
|
||||||
|
|
||||||
|
use crate::error::FormatError;
|
||||||
|
|
||||||
|
/// A file address, offset or length from the file as a `usize` index.
|
||||||
|
///
|
||||||
|
/// Fails with [`FormatError::Overflow`] when the value does not fit this
|
||||||
|
/// platform's `usize` (only possible on targets narrower than 64 bits).
|
||||||
|
#[inline]
|
||||||
|
pub fn to_usize(value: u64) -> Result<usize, FormatError> {
|
||||||
|
to_index::<usize>(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A file address for a [`crate::storage::Storage`] read, checked as
|
||||||
|
/// [`to_usize`] checks it: the parsers read through 64-bit offsets, but an
|
||||||
|
/// address that could not index an in-memory file on this platform is the
|
||||||
|
/// same [`FormatError::Overflow`] the slice parsers gave for it.
|
||||||
|
#[inline]
|
||||||
|
pub fn checked_addr(value: u64) -> Result<u64, FormatError> {
|
||||||
|
to_usize(value).map(|_| value)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`to_usize`] for an index type of any width. `usize` is 64 bits wide on
|
||||||
|
/// the hosts CI tests on, where the error path cannot be reached through
|
||||||
|
/// `usize`; tests run the same code with `u32` in its place, as on a 32-bit
|
||||||
|
/// target.
|
||||||
|
#[inline]
|
||||||
|
fn to_index<T: TryFrom<u64>>(value: u64) -> Result<T, FormatError> {
|
||||||
|
T::try_from(value).map_err(|_| too_large(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A count or offset into an in-memory buffer (a codec's progress counter,
|
||||||
|
/// a size the writer computed from data it holds) as a `usize`, saturating
|
||||||
|
/// at `usize::MAX` instead of truncating.
|
||||||
|
///
|
||||||
|
/// For values that are bounded by the length of something in memory, so
|
||||||
|
/// always fit; if one ever did not, a saturated index fails its bounds check
|
||||||
|
/// or allocation instead of silently addressing the wrong bytes. A value
|
||||||
|
/// read from the file uses [`to_usize`].
|
||||||
|
#[inline]
|
||||||
|
pub fn saturating_usize(value: u64) -> usize {
|
||||||
|
saturating_index(value, usize::MAX)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`saturating_usize`] for an index type of any width, whose largest
|
||||||
|
/// value is `max` (see [`to_index`]).
|
||||||
|
#[inline]
|
||||||
|
fn saturating_index<T: TryFrom<u64>>(value: u64, max: T) -> T {
|
||||||
|
T::try_from(value).unwrap_or(max)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cold]
|
||||||
|
#[inline(never)]
|
||||||
|
fn too_large(value: u64) -> FormatError {
|
||||||
|
FormatError::Overflow(format!(
|
||||||
|
"file address or length {value:#x} exceeds this platform's address space"
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn values_that_fit_convert_exactly() {
|
||||||
|
assert_eq!(to_usize(0), Ok(0));
|
||||||
|
assert_eq!(to_usize(0x1234), Ok(0x1234));
|
||||||
|
assert_eq!(to_usize(usize::MAX as u64), Ok(usize::MAX));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn saturating_conversion_never_wraps() {
|
||||||
|
assert_eq!(saturating_usize(0), 0);
|
||||||
|
assert_eq!(saturating_usize(0x1234), 0x1234);
|
||||||
|
assert_eq!(saturating_usize(usize::MAX as u64), usize::MAX);
|
||||||
|
// Past usize::MAX (32-bit targets) or at u64::MAX: saturates.
|
||||||
|
assert_eq!(saturating_usize(u64::MAX), usize::MAX);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn values_past_usize_max_are_an_error_not_truncated() {
|
||||||
|
// Reachable through `usize` only where it is narrower than u64 (no
|
||||||
|
// such target runs tests in CI), so the same conversion is run with
|
||||||
|
// u32 standing in for a 32-bit usize.
|
||||||
|
let max = u64::from(u32::MAX);
|
||||||
|
assert_eq!(to_index::<u32>(max), Ok(u32::MAX));
|
||||||
|
for past in [max + 1, max + 0x10, 0x1_0000_1234, u64::MAX] {
|
||||||
|
let err = to_index::<u32>(past).unwrap_err();
|
||||||
|
assert!(
|
||||||
|
matches!(err, FormatError::Overflow(_)),
|
||||||
|
"{past:#x}: {err:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Where an `as` cast would have wrapped to a small, valid-looking
|
||||||
|
// index, it is not returned.
|
||||||
|
assert_eq!(0x1_0000_1234_u64 as u32, 0x1234);
|
||||||
|
assert!(to_index::<u32>(0x1_0000_1234).is_err());
|
||||||
|
|
||||||
|
assert_eq!(saturating_index(max + 1, u32::MAX), u32::MAX);
|
||||||
|
assert_eq!(saturating_index(0x1_0000_1234, u32::MAX), u32::MAX);
|
||||||
|
assert_eq!(saturating_index(0x1234, u32::MAX), 0x1234);
|
||||||
|
|
||||||
|
// And through `usize` itself, whichever width it has here.
|
||||||
|
match (usize::MAX as u64).checked_add(1) {
|
||||||
|
Some(past) => assert!(matches!(to_usize(past), Err(FormatError::Overflow(_)))),
|
||||||
|
None => assert_eq!(to_usize(u64::MAX), Ok(usize::MAX)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,8 +5,10 @@ use alloc::{borrow::Cow, string::String, vec::Vec};
|
|||||||
#[cfg(feature = "std")]
|
#[cfg(feature = "std")]
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
|
|
||||||
|
use crate::addr::to_usize;
|
||||||
use crate::attribute_info::AttributeInfoMessage;
|
use crate::attribute_info::AttributeInfoMessage;
|
||||||
use crate::btree_v2::{BTreeV2Header, collect_btree_v2_records};
|
use crate::btree_v2::{BTreeV2Header, collect_btree_v2_records_in, find_btree_v2_records_in};
|
||||||
|
use crate::checksum::jenkins_lookup3;
|
||||||
use crate::data_read;
|
use crate::data_read;
|
||||||
use crate::dataspace::Dataspace;
|
use crate::dataspace::Dataspace;
|
||||||
use crate::datatype::Datatype;
|
use crate::datatype::Datatype;
|
||||||
@@ -15,6 +17,7 @@ use crate::fractal_heap::FractalHeapHeader;
|
|||||||
use crate::message_type::MessageType;
|
use crate::message_type::MessageType;
|
||||||
use crate::object_header::ObjectHeader;
|
use crate::object_header::ObjectHeader;
|
||||||
use crate::shared_message;
|
use crate::shared_message;
|
||||||
|
use crate::storage::Storage;
|
||||||
use crate::vl_data;
|
use crate::vl_data;
|
||||||
|
|
||||||
/// A parsed HDF5 attribute message.
|
/// A parsed HDF5 attribute message.
|
||||||
@@ -50,7 +53,7 @@ impl AttributeMessage {
|
|||||||
///
|
///
|
||||||
/// `length_size` is needed for dataspace dimension parsing.
|
/// `length_size` is needed for dataspace dimension parsing.
|
||||||
pub fn parse(data: &[u8], length_size: u8) -> Result<AttributeMessage, FormatError> {
|
pub fn parse(data: &[u8], length_size: u8) -> Result<AttributeMessage, FormatError> {
|
||||||
Self::parse_impl(data, length_size, None)
|
Self::parse_impl(data, length_size, None::<(&[u8], u8)>)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// [`AttributeMessage::parse`] with access to the rest of the file, which
|
/// [`AttributeMessage::parse`] with access to the rest of the file, which
|
||||||
@@ -65,13 +68,24 @@ impl AttributeMessage {
|
|||||||
offset_size: u8,
|
offset_size: u8,
|
||||||
length_size: u8,
|
length_size: u8,
|
||||||
) -> Result<AttributeMessage, FormatError> {
|
) -> Result<AttributeMessage, FormatError> {
|
||||||
Self::parse_impl(data, length_size, Some((file_data, offset_size)))
|
Self::parse_in_storage(data, file_data, offset_size, length_size)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_impl(
|
/// [`AttributeMessage::parse_in_file`] with the file behind any
|
||||||
|
/// [`Storage`].
|
||||||
|
pub fn parse_in_storage<S: Storage + ?Sized>(
|
||||||
|
data: &[u8],
|
||||||
|
file: &S,
|
||||||
|
offset_size: u8,
|
||||||
|
length_size: u8,
|
||||||
|
) -> Result<AttributeMessage, FormatError> {
|
||||||
|
Self::parse_impl(data, length_size, Some((file, offset_size)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_impl<S: Storage + ?Sized>(
|
||||||
data: &[u8],
|
data: &[u8],
|
||||||
length_size: u8,
|
length_size: u8,
|
||||||
file: Option<(&[u8], u8)>,
|
file: Option<(&S, u8)>,
|
||||||
) -> Result<AttributeMessage, FormatError> {
|
) -> Result<AttributeMessage, FormatError> {
|
||||||
ensure_len(data, 0, 2)?;
|
ensure_len(data, 0, 2)?;
|
||||||
let version = data[0];
|
let version = data[0];
|
||||||
@@ -86,19 +100,19 @@ impl AttributeMessage {
|
|||||||
|
|
||||||
/// The bytes of an embedded datatype/dataspace message, following the
|
/// The bytes of an embedded datatype/dataspace message, following the
|
||||||
/// shared-message reference when `shared` is set.
|
/// shared-message reference when `shared` is set.
|
||||||
fn embedded_message<'a>(
|
fn embedded_message<'a, S: Storage + ?Sized>(
|
||||||
bytes: &'a [u8],
|
bytes: &'a [u8],
|
||||||
shared: bool,
|
shared: bool,
|
||||||
msg_type: MessageType,
|
msg_type: MessageType,
|
||||||
length_size: u8,
|
length_size: u8,
|
||||||
file: Option<(&[u8], u8)>,
|
file: Option<(&S, u8)>,
|
||||||
) -> Result<Cow<'a, [u8]>, FormatError> {
|
) -> Result<Cow<'a, [u8]>, FormatError> {
|
||||||
if !shared {
|
if !shared {
|
||||||
return Ok(Cow::Borrowed(bytes));
|
return Ok(Cow::Borrowed(bytes));
|
||||||
}
|
}
|
||||||
let (file_data, offset_size) = file.ok_or(FormatError::UnresolvedSharedMessage)?;
|
let (file_data, offset_size) = file.ok_or(FormatError::UnresolvedSharedMessage)?;
|
||||||
let shared_ref = shared_message::parse_shared_ref_sized(bytes, offset_size, length_size)?;
|
let shared_ref = shared_message::parse_shared_ref_sized(bytes, offset_size, length_size)?;
|
||||||
shared_message::resolve_shared_message(
|
shared_message::resolve_shared_message_in(
|
||||||
file_data,
|
file_data,
|
||||||
&shared_ref,
|
&shared_ref,
|
||||||
msg_type,
|
msg_type,
|
||||||
@@ -143,10 +157,10 @@ impl AttributeMessage {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_v2(
|
fn parse_v2<S: Storage + ?Sized>(
|
||||||
data: &[u8],
|
data: &[u8],
|
||||||
length_size: u8,
|
length_size: u8,
|
||||||
file: Option<(&[u8], u8)>,
|
file: Option<(&S, u8)>,
|
||||||
) -> Result<AttributeMessage, FormatError> {
|
) -> Result<AttributeMessage, FormatError> {
|
||||||
// Flags: bit 0 = datatype is shared, bit 1 = dataspace is shared.
|
// Flags: bit 0 = datatype is shared, bit 1 = dataspace is shared.
|
||||||
let flags = data.get(1).copied().unwrap_or(0);
|
let flags = data.get(1).copied().unwrap_or(0);
|
||||||
@@ -197,10 +211,10 @@ impl AttributeMessage {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_v3(
|
fn parse_v3<S: Storage + ?Sized>(
|
||||||
data: &[u8],
|
data: &[u8],
|
||||||
length_size: u8,
|
length_size: u8,
|
||||||
file: Option<(&[u8], u8)>,
|
file: Option<(&S, u8)>,
|
||||||
) -> Result<AttributeMessage, FormatError> {
|
) -> Result<AttributeMessage, FormatError> {
|
||||||
// Flags: bit 0 = datatype is shared, bit 1 = dataspace is shared.
|
// Flags: bit 0 = datatype is shared, bit 1 = dataspace is shared.
|
||||||
let flags = data.get(1).copied().unwrap_or(0);
|
let flags = data.get(1).copied().unwrap_or(0);
|
||||||
@@ -322,9 +336,19 @@ impl AttributeMessage {
|
|||||||
file_data: &[u8],
|
file_data: &[u8],
|
||||||
offset_size: u8,
|
offset_size: u8,
|
||||||
length_size: u8,
|
length_size: u8,
|
||||||
|
) -> Result<Vec<String>, FormatError> {
|
||||||
|
self.read_vl_strings_in(file_data, offset_size, length_size)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`Self::read_vl_strings`] over any [`Storage`].
|
||||||
|
pub fn read_vl_strings_in<S: Storage + ?Sized>(
|
||||||
|
&self,
|
||||||
|
file_data: &S,
|
||||||
|
offset_size: u8,
|
||||||
|
length_size: u8,
|
||||||
) -> Result<Vec<String>, FormatError> {
|
) -> Result<Vec<String>, FormatError> {
|
||||||
let num_elements = self.dataspace.num_elements();
|
let num_elements = self.dataspace.num_elements();
|
||||||
vl_data::read_vl_strings(
|
vl_data::read_vl_strings_in(
|
||||||
file_data,
|
file_data,
|
||||||
&self.raw_data,
|
&self.raw_data,
|
||||||
num_elements,
|
num_elements,
|
||||||
@@ -341,7 +365,8 @@ fn compute_raw_data(
|
|||||||
dataspace: &Dataspace,
|
dataspace: &Dataspace,
|
||||||
datatype: &Datatype,
|
datatype: &Datatype,
|
||||||
) -> Vec<u8> {
|
) -> Vec<u8> {
|
||||||
let num_elements = dataspace.num_elements() as usize;
|
// Saturating, like the product: the size is capped at what is there.
|
||||||
|
let num_elements = usize::try_from(dataspace.num_elements()).unwrap_or(usize::MAX);
|
||||||
let elem_size = datatype.type_size() as usize;
|
let elem_size = datatype.type_size() as usize;
|
||||||
let expected_size = num_elements.saturating_mul(elem_size);
|
let expected_size = num_elements.saturating_mul(elem_size);
|
||||||
let available = data.len().saturating_sub(pos);
|
let available = data.len().saturating_sub(pos);
|
||||||
@@ -415,7 +440,20 @@ pub fn extract_attributes_full(
|
|||||||
offset_size: u8,
|
offset_size: u8,
|
||||||
length_size: u8,
|
length_size: u8,
|
||||||
) -> Result<Vec<AttributeMessage>, FormatError> {
|
) -> Result<Vec<AttributeMessage>, FormatError> {
|
||||||
extract_attributes_with(file_data, header, offset_size, length_size, &mut Err)
|
extract_attributes_full_in(file_data, header, offset_size, length_size)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`extract_attributes_full`] over any [`Storage`]. Dense attribute
|
||||||
|
/// storage is indexed by a v2 B-tree, which is not read over [`Storage`]
|
||||||
|
/// yet: on a backend without the whole file in memory an object with dense
|
||||||
|
/// attributes is [`FormatError::ContiguousStorageRequired`].
|
||||||
|
pub fn extract_attributes_full_in<S: Storage + ?Sized>(
|
||||||
|
file: &S,
|
||||||
|
header: &ObjectHeader,
|
||||||
|
offset_size: u8,
|
||||||
|
length_size: u8,
|
||||||
|
) -> Result<Vec<AttributeMessage>, FormatError> {
|
||||||
|
extract_attributes_with(file, header, offset_size, length_size, &mut Err)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Like [`extract_attributes_full`], but an attribute that cannot be read
|
/// Like [`extract_attributes_full`], but an attribute that cannot be read
|
||||||
@@ -431,6 +469,32 @@ pub fn extract_attributes_tolerant(
|
|||||||
header: &ObjectHeader,
|
header: &ObjectHeader,
|
||||||
offset_size: u8,
|
offset_size: u8,
|
||||||
length_size: u8,
|
length_size: u8,
|
||||||
|
) -> Result<(Vec<AttributeMessage>, Vec<FormatError>), FormatError> {
|
||||||
|
extract_attributes_tolerant_core(file_data, header, offset_size, length_size)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`extract_attributes_tolerant`] over any [`Storage`] (see
|
||||||
|
/// [`extract_attributes_full_in`] for dense storage). One with the whole
|
||||||
|
/// file in memory is read as the slice, by code compiled in this crate (see
|
||||||
|
/// [`crate::storage`], "Slice entry points").
|
||||||
|
#[inline]
|
||||||
|
pub fn extract_attributes_tolerant_in<S: Storage + ?Sized>(
|
||||||
|
file_data: &S,
|
||||||
|
header: &ObjectHeader,
|
||||||
|
offset_size: u8,
|
||||||
|
length_size: u8,
|
||||||
|
) -> Result<(Vec<AttributeMessage>, Vec<FormatError>), FormatError> {
|
||||||
|
match file_data.as_contiguous() {
|
||||||
|
Some(all) => extract_attributes_tolerant(all, header, offset_size, length_size),
|
||||||
|
None => extract_attributes_tolerant_core(file_data, header, offset_size, length_size),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extract_attributes_tolerant_core<S: Storage + ?Sized>(
|
||||||
|
file_data: &S,
|
||||||
|
header: &ObjectHeader,
|
||||||
|
offset_size: u8,
|
||||||
|
length_size: u8,
|
||||||
) -> Result<(Vec<AttributeMessage>, Vec<FormatError>), FormatError> {
|
) -> Result<(Vec<AttributeMessage>, Vec<FormatError>), FormatError> {
|
||||||
let mut errors = Vec::new();
|
let mut errors = Vec::new();
|
||||||
let attrs = extract_attributes_with(file_data, header, offset_size, length_size, &mut |e| {
|
let attrs = extract_attributes_with(file_data, header, offset_size, length_size, &mut |e| {
|
||||||
@@ -442,23 +506,198 @@ pub fn extract_attributes_tolerant(
|
|||||||
|
|
||||||
/// Read every attribute; each one that fails goes to `on_error`, which
|
/// Read every attribute; each one that fails goes to `on_error`, which
|
||||||
/// either stops the read (returns the error) or skips that attribute.
|
/// either stops the read (returns the error) or skips that attribute.
|
||||||
fn extract_attributes_with(
|
fn extract_attributes_with<S: Storage + ?Sized>(
|
||||||
file_data: &[u8],
|
file_data: &S,
|
||||||
header: &ObjectHeader,
|
header: &ObjectHeader,
|
||||||
offset_size: u8,
|
offset_size: u8,
|
||||||
length_size: u8,
|
length_size: u8,
|
||||||
on_error: &mut dyn FnMut(FormatError) -> Result<(), FormatError>,
|
on_error: &mut dyn FnMut(FormatError) -> Result<(), FormatError>,
|
||||||
) -> Result<Vec<AttributeMessage>, FormatError> {
|
) -> Result<Vec<AttributeMessage>, FormatError> {
|
||||||
let mut attrs = Vec::new();
|
let mut attrs = Vec::new();
|
||||||
|
// Each attribute's creation order, where the file records one.
|
||||||
|
let mut orders: Vec<u32> = Vec::new();
|
||||||
|
|
||||||
// Collect compact attributes (inline in OH)
|
extract_compact_attributes(
|
||||||
|
file_data,
|
||||||
|
header,
|
||||||
|
offset_size,
|
||||||
|
length_size,
|
||||||
|
&mut attrs,
|
||||||
|
&mut orders,
|
||||||
|
on_error,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// Check for dense attributes via AttributeInfo message
|
||||||
|
let attr_info = find_attribute_info(header, offset_size)?;
|
||||||
|
if let Some(info) = &attr_info
|
||||||
|
&& let Some(fh_addr) = info.fractal_heap_address
|
||||||
|
{
|
||||||
|
extract_dense_attributes(
|
||||||
|
file_data,
|
||||||
|
info,
|
||||||
|
fh_addr,
|
||||||
|
offset_size,
|
||||||
|
length_size,
|
||||||
|
&mut attrs,
|
||||||
|
&mut orders,
|
||||||
|
on_error,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// An object that tracks attribute creation order lists its attributes
|
||||||
|
// in that order (h5py's `track_order=True`), as libhdf5 does; otherwise
|
||||||
|
// they come in storage order.
|
||||||
|
if attr_info.is_some_and(|i| i.max_creation_index.is_some()) {
|
||||||
|
let mut paired: Vec<(u32, AttributeMessage)> = orders.into_iter().zip(attrs).collect();
|
||||||
|
paired.sort_by_key(|(o, _)| *o);
|
||||||
|
attrs = paired.into_iter().map(|(_, a)| a).collect();
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(attrs)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// B-tree v2 record type of dense attribute storage's name index.
|
||||||
|
const ATTRIBUTE_NAME_INDEX: u8 = 8;
|
||||||
|
|
||||||
|
/// The attribute called `name` on the object with header `header`: the
|
||||||
|
/// first one [`extract_attributes_tolerant`] returns under that name, or
|
||||||
|
/// `None` if it returns none (an attribute that cannot be read is not
|
||||||
|
/// returned there either).
|
||||||
|
///
|
||||||
|
/// Compact attributes are in the header and are scanned. Dense attributes
|
||||||
|
/// are found through the name index (a v2 B-tree of lookup3 name hashes,
|
||||||
|
/// record type 8): only the attributes whose names hash like `name` are read
|
||||||
|
/// from the heap, O(log n) instead of all of them. Errors in the structures
|
||||||
|
/// that index the attributes fail the call, as they fail a listing.
|
||||||
|
pub fn find_attribute_in_file(
|
||||||
|
file_data: &[u8],
|
||||||
|
header: &ObjectHeader,
|
||||||
|
name: &str,
|
||||||
|
offset_size: u8,
|
||||||
|
length_size: u8,
|
||||||
|
) -> Result<Option<AttributeMessage>, FormatError> {
|
||||||
|
find_attribute_core(file_data, header, name, offset_size, length_size)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`find_attribute_in_file`] over any [`Storage`] (see
|
||||||
|
/// [`extract_attributes_full_in`] for dense storage, whose name index still
|
||||||
|
/// needs the whole file in memory). One with the whole file in memory is
|
||||||
|
/// read as the slice, by code compiled in this crate (see
|
||||||
|
/// [`crate::storage`], "Slice entry points").
|
||||||
|
#[inline]
|
||||||
|
pub fn find_attribute_in<S: Storage + ?Sized>(
|
||||||
|
file_data: &S,
|
||||||
|
header: &ObjectHeader,
|
||||||
|
name: &str,
|
||||||
|
offset_size: u8,
|
||||||
|
length_size: u8,
|
||||||
|
) -> Result<Option<AttributeMessage>, FormatError> {
|
||||||
|
match file_data.as_contiguous() {
|
||||||
|
Some(all) => find_attribute_in_file(all, header, name, offset_size, length_size),
|
||||||
|
None => find_attribute_core(file_data, header, name, offset_size, length_size),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_attribute_core<S: Storage + ?Sized>(
|
||||||
|
file_data: &S,
|
||||||
|
header: &ObjectHeader,
|
||||||
|
name: &str,
|
||||||
|
offset_size: u8,
|
||||||
|
length_size: u8,
|
||||||
|
) -> Result<Option<AttributeMessage>, FormatError> {
|
||||||
|
let attr_info = find_attribute_info(header, offset_size)?;
|
||||||
|
let dense = attr_info
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|i| Some((i.fractal_heap_address?, i.btree_name_index_address?)));
|
||||||
|
let Some((fh_addr, btree_addr)) = dense else {
|
||||||
|
// Compact only (or dense storage without a name index, which a
|
||||||
|
// listing reports): as a listing finds it.
|
||||||
|
return Ok(
|
||||||
|
extract_attributes_tolerant_in(file_data, header, offset_size, length_size)?
|
||||||
|
.0
|
||||||
|
.into_iter()
|
||||||
|
.find(|a| a.name == name),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
let btree_hdr = BTreeV2Header::parse_in(
|
||||||
|
file_data,
|
||||||
|
to_usize(btree_addr)? as u64,
|
||||||
|
offset_size,
|
||||||
|
length_size,
|
||||||
|
)?;
|
||||||
|
let fh = FractalHeapHeader::parse_in(file_data, fh_addr, offset_size, length_size)?;
|
||||||
|
if btree_hdr.tree_type != ATTRIBUTE_NAME_INDEX || btree_hdr.record_size < 4 {
|
||||||
|
return Ok(
|
||||||
|
extract_attributes_tolerant_in(file_data, header, offset_size, length_size)?
|
||||||
|
.0
|
||||||
|
.into_iter()
|
||||||
|
.find(|a| a.name == name),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// A listing has the compact attributes first.
|
||||||
|
let mut compact = Vec::new();
|
||||||
|
extract_compact_attributes(
|
||||||
|
file_data,
|
||||||
|
header,
|
||||||
|
offset_size,
|
||||||
|
length_size,
|
||||||
|
&mut compact,
|
||||||
|
&mut Vec::new(),
|
||||||
|
&mut |_| Ok(()),
|
||||||
|
)?;
|
||||||
|
if let Some(a) = compact.into_iter().find(|a| a.name == name) {
|
||||||
|
return Ok(Some(a));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Record: heap ID + message flags(1) + creation order(4) + hash(4); the
|
||||||
|
// hash is the last field.
|
||||||
|
let hash = jenkins_lookup3(name.as_bytes());
|
||||||
|
let hash_at = usize::from(btree_hdr.record_size) - 4;
|
||||||
|
let records = find_btree_v2_records_in(file_data, &btree_hdr, offset_size, &mut |r| match r
|
||||||
|
.get(hash_at..hash_at + 4)
|
||||||
|
{
|
||||||
|
Some(h) => u32::from_le_bytes([h[0], h[1], h[2], h[3]]).cmp(&hash),
|
||||||
|
None => core::cmp::Ordering::Less,
|
||||||
|
})?;
|
||||||
|
let id_len = usize::from(fh.heap_id_length);
|
||||||
|
for record in &records {
|
||||||
|
let Some(id_bytes) = record.data.get(..id_len) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let attr = fh
|
||||||
|
.read_managed_object_in(file_data, id_bytes, offset_size)
|
||||||
|
.and_then(|d| {
|
||||||
|
AttributeMessage::parse_in_storage(&d, file_data, offset_size, length_size)
|
||||||
|
});
|
||||||
|
// One that cannot be read is left out, as from a listing.
|
||||||
|
if let Ok(attr) = attr
|
||||||
|
&& attr.name == name
|
||||||
|
{
|
||||||
|
return Ok(Some(attr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The attributes stored in the object header itself (compact storage), and
|
||||||
|
/// each one's creation order into `orders`.
|
||||||
|
fn extract_compact_attributes<S: Storage + ?Sized>(
|
||||||
|
file_data: &S,
|
||||||
|
header: &ObjectHeader,
|
||||||
|
offset_size: u8,
|
||||||
|
length_size: u8,
|
||||||
|
attrs: &mut Vec<AttributeMessage>,
|
||||||
|
orders: &mut Vec<u32>,
|
||||||
|
on_error: &mut dyn FnMut(FormatError) -> Result<(), FormatError>,
|
||||||
|
) -> Result<(), FormatError> {
|
||||||
for msg in &header.messages {
|
for msg in &header.messages {
|
||||||
if msg.msg_type == MessageType::Attribute {
|
if msg.msg_type == MessageType::Attribute {
|
||||||
let attr = if shared_message::is_shared(msg.flags) {
|
let attr = if shared_message::is_shared(msg.flags) {
|
||||||
// Shared attribute: resolve the reference to get actual attribute data
|
// Shared attribute: resolve the reference to get actual attribute data
|
||||||
shared_message::parse_shared_ref_sized(&msg.data, offset_size, length_size)
|
shared_message::parse_shared_ref_sized(&msg.data, offset_size, length_size)
|
||||||
.and_then(|shared_ref| {
|
.and_then(|shared_ref| {
|
||||||
shared_message::resolve_shared_message(
|
shared_message::resolve_shared_message_in(
|
||||||
file_data,
|
file_data,
|
||||||
&shared_ref,
|
&shared_ref,
|
||||||
MessageType::Attribute,
|
MessageType::Attribute,
|
||||||
@@ -467,7 +706,7 @@ fn extract_attributes_with(
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
.and_then(|resolved| {
|
.and_then(|resolved| {
|
||||||
AttributeMessage::parse_in_file(
|
AttributeMessage::parse_in_storage(
|
||||||
&resolved,
|
&resolved,
|
||||||
file_data,
|
file_data,
|
||||||
offset_size,
|
offset_size,
|
||||||
@@ -475,33 +714,19 @@ fn extract_attributes_with(
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
AttributeMessage::parse_in_file(&msg.data, file_data, offset_size, length_size)
|
AttributeMessage::parse_in_storage(&msg.data, file_data, offset_size, length_size)
|
||||||
};
|
};
|
||||||
let attr = attr.and_then(|a| check_in_header(a, header));
|
let attr = attr.and_then(|a| check_in_header(a, header));
|
||||||
match attr {
|
match attr {
|
||||||
Ok(attr) => attrs.push(attr),
|
Ok(attr) => {
|
||||||
|
attrs.push(attr);
|
||||||
|
orders.push(msg.creation_order.map_or(0, u32::from));
|
||||||
|
}
|
||||||
Err(e) => on_error(e)?,
|
Err(e) => on_error(e)?,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Ok(())
|
||||||
// Check for dense attributes via AttributeInfo message
|
|
||||||
let attr_info = find_attribute_info(header, offset_size)?;
|
|
||||||
if let Some(info) = attr_info
|
|
||||||
&& let Some(fh_addr) = info.fractal_heap_address
|
|
||||||
{
|
|
||||||
extract_dense_attributes(
|
|
||||||
file_data,
|
|
||||||
&info,
|
|
||||||
fh_addr,
|
|
||||||
offset_size,
|
|
||||||
length_size,
|
|
||||||
&mut attrs,
|
|
||||||
on_error,
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(attrs)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Find and parse the Attribute Info message from an object header.
|
/// Find and parse the Attribute Info message from an object header.
|
||||||
@@ -518,18 +743,21 @@ fn find_attribute_info(
|
|||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extract attributes from dense storage (fractal heap + B-tree v2).
|
/// Extract attributes from dense storage (fractal heap + B-tree v2), and
|
||||||
fn extract_dense_attributes(
|
/// each one's creation order into `orders`.
|
||||||
file_data: &[u8],
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
fn extract_dense_attributes<S: Storage + ?Sized>(
|
||||||
|
file_data: &S,
|
||||||
attr_info: &AttributeInfoMessage,
|
attr_info: &AttributeInfoMessage,
|
||||||
fh_addr: u64,
|
fh_addr: u64,
|
||||||
offset_size: u8,
|
offset_size: u8,
|
||||||
length_size: u8,
|
length_size: u8,
|
||||||
attrs: &mut Vec<AttributeMessage>,
|
attrs: &mut Vec<AttributeMessage>,
|
||||||
|
orders: &mut Vec<u32>,
|
||||||
on_error: &mut dyn FnMut(FormatError) -> Result<(), FormatError>,
|
on_error: &mut dyn FnMut(FormatError) -> Result<(), FormatError>,
|
||||||
) -> Result<(), FormatError> {
|
) -> Result<(), FormatError> {
|
||||||
// Parse fractal heap
|
// Parse fractal heap
|
||||||
let fh = FractalHeapHeader::parse(file_data, fh_addr as usize, offset_size, length_size)?;
|
let fh = FractalHeapHeader::parse_in(file_data, fh_addr, offset_size, length_size)?;
|
||||||
|
|
||||||
// Parse B-tree v2 for name index (type 8)
|
// Parse B-tree v2 for name index (type 8)
|
||||||
let btree_addr = attr_info
|
let btree_addr = attr_info
|
||||||
@@ -538,8 +766,13 @@ fn extract_dense_attributes(
|
|||||||
expected: 1,
|
expected: 1,
|
||||||
available: 0,
|
available: 0,
|
||||||
})?;
|
})?;
|
||||||
let btree_hdr = BTreeV2Header::parse(file_data, btree_addr as usize, offset_size, length_size)?;
|
let btree_hdr = BTreeV2Header::parse_in(
|
||||||
let records = collect_btree_v2_records(file_data, &btree_hdr, offset_size, length_size)?;
|
file_data,
|
||||||
|
to_usize(btree_addr)? as u64,
|
||||||
|
offset_size,
|
||||||
|
length_size,
|
||||||
|
)?;
|
||||||
|
let records = collect_btree_v2_records_in(file_data, &btree_hdr, offset_size, length_size)?;
|
||||||
|
|
||||||
for record in &records {
|
for record in &records {
|
||||||
// Per HDF5 spec, both type 8 and type 9 records start with heap_id:
|
// Per HDF5 spec, both type 8 and type 9 records start with heap_id:
|
||||||
@@ -556,12 +789,19 @@ fn extract_dense_attributes(
|
|||||||
|
|
||||||
// The data in the heap is a complete attribute message
|
// The data in the heap is a complete attribute message
|
||||||
let attr = fh
|
let attr = fh
|
||||||
.read_managed_object(file_data, id_bytes, offset_size)
|
.read_managed_object_in(file_data, id_bytes, offset_size)
|
||||||
.and_then(|attr_data| {
|
.and_then(|attr_data| {
|
||||||
AttributeMessage::parse_in_file(&attr_data, file_data, offset_size, length_size)
|
AttributeMessage::parse_in_storage(&attr_data, file_data, offset_size, length_size)
|
||||||
});
|
});
|
||||||
match attr {
|
match attr {
|
||||||
Ok(attr) => attrs.push(attr),
|
Ok(attr) => {
|
||||||
|
attrs.push(attr);
|
||||||
|
let order = record
|
||||||
|
.data
|
||||||
|
.get(id_len + 1..id_len + 5)
|
||||||
|
.map_or(0, |b| u32::from_le_bytes([b[0], b[1], b[2], b[3]]));
|
||||||
|
orders.push(order);
|
||||||
|
}
|
||||||
Err(e) => on_error(e)?,
|
Err(e) => on_error(e)?,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -961,4 +1201,73 @@ mod tests {
|
|||||||
let strs = attr.read_as_strings().unwrap();
|
let strs = attr.read_as_strings().unwrap();
|
||||||
assert_eq!(strs, vec!["abcd", "EFGH"]);
|
assert_eq!(strs, vec!["abcd", "EFGH"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Every object's attributes in h5py-written files read identically
|
||||||
|
/// through a read_at-only CountingStorage — compact ones, shared ones,
|
||||||
|
/// those behind an Attribute Info message and dense storage (its v2
|
||||||
|
/// B-tree name index included) — and through a slice as Storage.
|
||||||
|
#[test]
|
||||||
|
fn storage_reads_match_slice_reads() {
|
||||||
|
use crate::storage::CountingStorage;
|
||||||
|
let files: [(&str, &[u8]); 5] = [
|
||||||
|
("attrs", include_bytes!("../tests/fixtures/attrs.h5")),
|
||||||
|
(
|
||||||
|
"mixed_attrs",
|
||||||
|
include_bytes!("../tests/fixtures/mixed_attrs.h5"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"dense_attrs",
|
||||||
|
include_bytes!("../tests/fixtures/dense_attrs.h5"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"dense_attrs_root",
|
||||||
|
include_bytes!("../tests/fixtures/dense_attrs_root.h5"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"shared_fill_value",
|
||||||
|
include_bytes!("../tests/fixtures/shared_fill_value.h5"),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
let (mut same, mut dense, mut attrs) = (0, 0, 0);
|
||||||
|
for (name, file) in files {
|
||||||
|
let sb = crate::superblock::Superblock::parse(file, 0).unwrap();
|
||||||
|
let (os, ls) = (sb.offset_size, sb.length_size);
|
||||||
|
let mut addrs = vec![sb.root_group_address];
|
||||||
|
addrs.extend(
|
||||||
|
crate::group_v2::resolve_group_children(file, &sb, sb.root_group_address)
|
||||||
|
.unwrap()
|
||||||
|
.iter()
|
||||||
|
.map(|e| e.object_header_address),
|
||||||
|
);
|
||||||
|
let storage = CountingStorage::new(file.to_vec());
|
||||||
|
for addr in addrs {
|
||||||
|
let header = ObjectHeader::parse(file, addr as usize, os, ls).unwrap();
|
||||||
|
let want = extract_attributes_full(file, &header, os, ls);
|
||||||
|
let slice_storage = extract_attributes_full_in(&file, &header, os, ls);
|
||||||
|
assert_eq!(format!("{slice_storage:?}"), format!("{want:?}"));
|
||||||
|
let got = extract_attributes_full_in(&storage, &header, os, ls);
|
||||||
|
let got_t = extract_attributes_tolerant_in(&storage, &header, os, ls);
|
||||||
|
let is_dense = find_attribute_info(&header, os)
|
||||||
|
.unwrap()
|
||||||
|
.is_some_and(|i| i.fractal_heap_address.is_some());
|
||||||
|
if is_dense {
|
||||||
|
dense += 1;
|
||||||
|
}
|
||||||
|
attrs += want.as_ref().map_or(0, Vec::len);
|
||||||
|
assert_eq!(format!("{got:?}"), format!("{want:?}"), "{name}");
|
||||||
|
let want_t = extract_attributes_tolerant(file, &header, os, ls);
|
||||||
|
assert_eq!(format!("{got_t:?}"), format!("{want_t:?}"), "{name}");
|
||||||
|
same += 1;
|
||||||
|
for a in want.iter().flatten() {
|
||||||
|
let one = find_attribute_in(&storage, &header, &a.name, os, ls);
|
||||||
|
let want_one = find_attribute_in_file(file, &header, &a.name, os, ls);
|
||||||
|
assert_eq!(format!("{one:?}"), format!("{want_one:?}"), "{name}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert!(
|
||||||
|
same >= 5 && dense >= 2 && attrs >= 5,
|
||||||
|
"{same} {dense} {attrs}"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
use alloc::vec::Vec;
|
use alloc::vec::Vec;
|
||||||
|
|
||||||
use crate::error::FormatError;
|
use crate::error::FormatError;
|
||||||
|
use crate::storage::{Storage, read_exact_at};
|
||||||
|
|
||||||
/// A parsed B-tree v1 node.
|
/// A parsed B-tree v1 node.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@@ -74,13 +75,28 @@ impl BTreeV1Node {
|
|||||||
file_data: &[u8],
|
file_data: &[u8],
|
||||||
offset: usize,
|
offset: usize,
|
||||||
offset_size: u8,
|
offset_size: u8,
|
||||||
|
length_size: u8,
|
||||||
|
) -> Result<BTreeV1Node, FormatError> {
|
||||||
|
Self::parse_in(file_data, offset as u64, offset_size, length_size)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`Self::parse`] over any [`Storage`]: one read of the node's header,
|
||||||
|
/// one of its keys and children.
|
||||||
|
pub fn parse_in<S: Storage + ?Sized>(
|
||||||
|
file: &S,
|
||||||
|
offset: u64,
|
||||||
|
offset_size: u8,
|
||||||
_length_size: u8,
|
_length_size: u8,
|
||||||
) -> Result<BTreeV1Node, FormatError> {
|
) -> Result<BTreeV1Node, FormatError> {
|
||||||
// signature(4) + node_type(1) + node_level(1) + entries_used(2) = 8
|
// signature(4) + node_type(1) + node_level(1) + entries_used(2) = 8
|
||||||
// + left_sibling(offset_size) + right_sibling(offset_size)
|
// + left_sibling(offset_size) + right_sibling(offset_size)
|
||||||
let os = offset_size as usize;
|
let os = offset_size as usize;
|
||||||
let header_size = 8 + os * 2;
|
let header_size = 8 + os * 2;
|
||||||
ensure_len(file_data, offset, header_size)?;
|
let header = read_exact_at(file, offset, header_size)?;
|
||||||
|
let file_data: &[u8] = &header;
|
||||||
|
// The header's read checked that `offset + header_size` fits.
|
||||||
|
let body_start = offset + header_size as u64;
|
||||||
|
let offset = 0usize;
|
||||||
|
|
||||||
if &file_data[offset..offset + 4] != b"TREE" {
|
if &file_data[offset..offset + 4] != b"TREE" {
|
||||||
return Err(FormatError::InvalidBTreeSignature);
|
return Err(FormatError::InvalidBTreeSignature);
|
||||||
@@ -102,31 +118,30 @@ impl BTreeV1Node {
|
|||||||
} else {
|
} else {
|
||||||
Some(read_offset(file_data, pos, offset_size)?)
|
Some(read_offset(file_data, pos, offset_size)?)
|
||||||
};
|
};
|
||||||
pos += os;
|
|
||||||
|
|
||||||
// For type 0: keys are offset_size bytes, children are offset_size bytes
|
// For type 0: keys are offset_size bytes, children are offset_size bytes
|
||||||
// Layout: key[0], child[0], key[1], child[1], ..., key[N-1], child[N-1], key[N]
|
// Layout: key[0], child[0], key[1], child[1], ..., key[N-1], child[N-1], key[N]
|
||||||
let eu = entries_used as usize;
|
let eu = entries_used as usize;
|
||||||
let key_size = os; // For type 0, key = offset_size
|
let key_size = os; // For type 0, key = offset_size
|
||||||
let needed = eu * (key_size + os) + key_size; // eu children + (eu+1) keys
|
let needed = eu * (key_size + os) + key_size; // eu children + (eu+1) keys
|
||||||
ensure_len(file_data, pos, needed)?;
|
let body = read_exact_at(file, body_start, needed)?;
|
||||||
|
let file_data: &[u8] = &body;
|
||||||
|
|
||||||
let mut keys = Vec::with_capacity(eu + 1);
|
let mut keys = Vec::with_capacity(eu + 1);
|
||||||
let mut children = Vec::with_capacity(eu);
|
let mut children = Vec::with_capacity(eu);
|
||||||
|
|
||||||
for _i in 0..eu {
|
if os == 0 {
|
||||||
// key[i]
|
// What reading the first key reports (and keeps `chunks_exact`
|
||||||
let key = read_offset(file_data, pos, offset_size)?;
|
// below from being given a zero size).
|
||||||
keys.push(key);
|
return Err(FormatError::InvalidOffsetSize(offset_size));
|
||||||
pos += key_size;
|
|
||||||
// child[i]
|
|
||||||
let child = read_offset(file_data, pos, offset_size)?;
|
|
||||||
children.push(child);
|
|
||||||
pos += os;
|
|
||||||
}
|
}
|
||||||
// final key
|
// `needed` bytes: key[0], child[0], ..., child[eu - 1], key[eu].
|
||||||
let key = read_offset(file_data, pos, offset_size)?;
|
let (pairs, last) = file_data.split_at(eu * (key_size + os));
|
||||||
keys.push(key);
|
for pair in pairs.chunks_exact(key_size + os) {
|
||||||
|
keys.push(read_offset(pair, 0, offset_size)?);
|
||||||
|
children.push(read_offset(pair, key_size, offset_size)?);
|
||||||
|
}
|
||||||
|
keys.push(read_offset(last, 0, offset_size)?);
|
||||||
|
|
||||||
Ok(BTreeV1Node {
|
Ok(BTreeV1Node {
|
||||||
node_type,
|
node_type,
|
||||||
@@ -150,11 +165,21 @@ pub fn collect_symbol_table_nodes(
|
|||||||
offset_size: u8,
|
offset_size: u8,
|
||||||
length_size: u8,
|
length_size: u8,
|
||||||
) -> Result<Vec<u64>, FormatError> {
|
) -> Result<Vec<u64>, FormatError> {
|
||||||
collect_symbol_table_nodes_inner(file_data, btree_address, offset_size, length_size, 0)
|
collect_symbol_table_nodes_in(file_data, btree_address, offset_size, length_size)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn collect_symbol_table_nodes_inner(
|
/// [`collect_symbol_table_nodes`] over any [`Storage`]: two reads per node.
|
||||||
file_data: &[u8],
|
pub fn collect_symbol_table_nodes_in<S: Storage + ?Sized>(
|
||||||
|
file: &S,
|
||||||
|
btree_address: u64,
|
||||||
|
offset_size: u8,
|
||||||
|
length_size: u8,
|
||||||
|
) -> Result<Vec<u64>, FormatError> {
|
||||||
|
collect_symbol_table_nodes_inner(file, btree_address, offset_size, length_size, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn collect_symbol_table_nodes_inner<S: Storage + ?Sized>(
|
||||||
|
file: &S,
|
||||||
btree_address: u64,
|
btree_address: u64,
|
||||||
offset_size: u8,
|
offset_size: u8,
|
||||||
length_size: u8,
|
length_size: u8,
|
||||||
@@ -164,7 +189,7 @@ fn collect_symbol_table_nodes_inner(
|
|||||||
return Err(FormatError::NestingDepthExceeded);
|
return Err(FormatError::NestingDepthExceeded);
|
||||||
}
|
}
|
||||||
|
|
||||||
let node = BTreeV1Node::parse(file_data, btree_address as usize, offset_size, length_size)?;
|
let node = BTreeV1Node::parse_in(file, btree_address, offset_size, length_size)?;
|
||||||
|
|
||||||
if node.node_type != 0 {
|
if node.node_type != 0 {
|
||||||
return Err(FormatError::InvalidBTreeNodeType(node.node_type));
|
return Err(FormatError::InvalidBTreeNodeType(node.node_type));
|
||||||
@@ -178,7 +203,7 @@ fn collect_symbol_table_nodes_inner(
|
|||||||
let mut result = Vec::new();
|
let mut result = Vec::new();
|
||||||
for &child_addr in &node.children {
|
for &child_addr in &node.children {
|
||||||
let child_snods = collect_symbol_table_nodes_inner(
|
let child_snods = collect_symbol_table_nodes_inner(
|
||||||
file_data,
|
file,
|
||||||
child_addr,
|
child_addr,
|
||||||
offset_size,
|
offset_size,
|
||||||
length_size,
|
length_size,
|
||||||
@@ -317,4 +342,48 @@ mod tests {
|
|||||||
assert_eq!(node.entries_used, 1);
|
assert_eq!(node.entries_used, 1);
|
||||||
assert_eq!(node.children, vec![0x50]);
|
assert_eq!(node.children, vec![0x50]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Nodes and trees, cut at every length, parse identically through a
|
||||||
|
/// `read_at`-only storage.
|
||||||
|
#[test]
|
||||||
|
fn storage_parse_matches_slice_parse() {
|
||||||
|
use crate::storage::CountingStorage;
|
||||||
|
let nodes = [
|
||||||
|
build_btree_node(0, 0, &[0, 5, 10], &[0x100, 0x200], None, None, 8),
|
||||||
|
build_btree_node(0, 0, &[0, 5], &[0x100], Some(0x40), Some(0x80), 4),
|
||||||
|
build_btree_node(1, 2, &[0, 5], &[0x100], None, Some(0x80), 8),
|
||||||
|
];
|
||||||
|
for (n, node) in nodes.iter().enumerate() {
|
||||||
|
let os = if n == 1 { 4 } else { 8 };
|
||||||
|
for cut in 0..=node.len() {
|
||||||
|
let f = &node[..cut];
|
||||||
|
let storage = CountingStorage::new(f.to_vec());
|
||||||
|
let want = BTreeV1Node::parse(f, 0, os, 8);
|
||||||
|
let got = BTreeV1Node::parse_in(&storage, 0, os, 8);
|
||||||
|
assert_eq!(format!("{got:?}"), format!("{want:?}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let leaf1 = build_btree_node(0, 0, &[0, 5], &[0xA00], None, None, 8);
|
||||||
|
let leaf2 = build_btree_node(0, 0, &[5, 10], &[0xB00], None, None, 8);
|
||||||
|
let internal = build_btree_node(0, 1, &[0, 5, 10], &[0, 256], None, None, 8);
|
||||||
|
let mut file = vec![0u8; 512 + internal.len()];
|
||||||
|
file[..leaf1.len()].copy_from_slice(&leaf1);
|
||||||
|
file[256..256 + leaf2.len()].copy_from_slice(&leaf2);
|
||||||
|
file[512..].copy_from_slice(&internal);
|
||||||
|
for cut in [file.len(), 300, 260, 100, 10] {
|
||||||
|
let mut f = file.clone();
|
||||||
|
if cut < 512 {
|
||||||
|
// Truncate the leaves, keep the root.
|
||||||
|
f[cut..512].fill(0);
|
||||||
|
}
|
||||||
|
let storage = CountingStorage::new(f.clone());
|
||||||
|
assert_eq!(
|
||||||
|
collect_symbol_table_nodes_in(&storage, 512, 8, 8),
|
||||||
|
collect_symbol_table_nodes(&f, 512, 8, 8)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let storage = CountingStorage::new(file);
|
||||||
|
collect_symbol_table_nodes_in(&storage, 512, 8, 8).unwrap();
|
||||||
|
assert_eq!(storage.reads(), 6);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,11 +2,14 @@
|
|||||||
|
|
||||||
#[cfg(not(feature = "std"))]
|
#[cfg(not(feature = "std"))]
|
||||||
use alloc::vec::Vec;
|
use alloc::vec::Vec;
|
||||||
|
use core::cmp::Ordering;
|
||||||
|
|
||||||
#[cfg(feature = "checksum")]
|
#[cfg(feature = "checksum")]
|
||||||
use byteorder::{ByteOrder, LittleEndian};
|
use byteorder::{ByteOrder, LittleEndian};
|
||||||
|
|
||||||
|
use crate::addr::to_usize;
|
||||||
use crate::error::FormatError;
|
use crate::error::FormatError;
|
||||||
|
use crate::storage::{Storage, Window, len_usize};
|
||||||
|
|
||||||
/// Parsed B-tree v2 header (signature "BTHD").
|
/// Parsed B-tree v2 header (signature "BTHD").
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@@ -71,7 +74,7 @@ fn ensure_len(data: &[u8], pos: usize, needed: usize) -> Result<(), FormatError>
|
|||||||
|
|
||||||
/// Compute the number of bytes needed to represent a count, using variable-width encoding.
|
/// Compute the number of bytes needed to represent a count, using variable-width encoding.
|
||||||
/// B-tree v2 uses this for the number of records fields in internal nodes.
|
/// B-tree v2 uses this for the number of records fields in internal nodes.
|
||||||
fn bytes_for_max_records(max_nrec: u64) -> usize {
|
pub(crate) fn bytes_for_max_records(max_nrec: u64) -> usize {
|
||||||
if max_nrec == 0 {
|
if max_nrec == 0 {
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
@@ -97,38 +100,52 @@ impl BTreeV2Header {
|
|||||||
offset_size: u8,
|
offset_size: u8,
|
||||||
length_size: u8,
|
length_size: u8,
|
||||||
) -> Result<BTreeV2Header, FormatError> {
|
) -> Result<BTreeV2Header, FormatError> {
|
||||||
ensure_len(file_data, offset, 4)?;
|
Self::parse_in(file_data, offset as u64, offset_size, length_size)
|
||||||
if &file_data[offset..offset + 4] != b"BTHD" {
|
}
|
||||||
|
|
||||||
|
/// [`Self::parse`] over any [`Storage`]: one bounded read of the
|
||||||
|
/// header.
|
||||||
|
pub fn parse_in<S: Storage + ?Sized>(
|
||||||
|
file: &S,
|
||||||
|
offset: u64,
|
||||||
|
offset_size: u8,
|
||||||
|
length_size: u8,
|
||||||
|
) -> Result<BTreeV2Header, FormatError> {
|
||||||
|
// Every field and the checksum; the window holds all of it or ends
|
||||||
|
// at the end of the file, so its bounds checks are the whole-file
|
||||||
|
// ones.
|
||||||
|
let full = 16 + usize::from(offset_size) + 2 + usize::from(length_size) + 4;
|
||||||
|
let w = Window::read(file, offset, full)?;
|
||||||
|
let d = &w.bytes;
|
||||||
|
w.ensure(0, 4)?;
|
||||||
|
if &d[..4] != b"BTHD" {
|
||||||
return Err(FormatError::InvalidBTreeV2Signature);
|
return Err(FormatError::InvalidBTreeV2Signature);
|
||||||
}
|
}
|
||||||
|
|
||||||
ensure_len(file_data, offset, 4 + 1 + 1 + 4 + 2 + 2 + 1 + 1)?;
|
w.ensure(0, 4 + 1 + 1 + 4 + 2 + 2 + 1 + 1)?;
|
||||||
let version = file_data[offset + 4];
|
let version = d[4];
|
||||||
if version != 0 {
|
if version != 0 {
|
||||||
return Err(FormatError::InvalidBTreeV2Version(version));
|
return Err(FormatError::InvalidBTreeV2Version(version));
|
||||||
}
|
}
|
||||||
|
|
||||||
let tree_type = file_data[offset + 5];
|
let tree_type = d[5];
|
||||||
let node_size = u32::from_le_bytes([
|
let node_size = u32::from_le_bytes([d[6], d[7], d[8], d[9]]);
|
||||||
file_data[offset + 6],
|
let record_size = u16::from_le_bytes([d[10], d[11]]);
|
||||||
file_data[offset + 7],
|
let depth = u16::from_le_bytes([d[12], d[13]]);
|
||||||
file_data[offset + 8],
|
let _split_percent = d[14];
|
||||||
file_data[offset + 9],
|
let _merge_percent = d[15];
|
||||||
]);
|
|
||||||
let record_size = u16::from_le_bytes([file_data[offset + 10], file_data[offset + 11]]);
|
|
||||||
let depth = u16::from_le_bytes([file_data[offset + 12], file_data[offset + 13]]);
|
|
||||||
let _split_percent = file_data[offset + 14];
|
|
||||||
let _merge_percent = file_data[offset + 15];
|
|
||||||
|
|
||||||
let mut pos = offset + 16;
|
let mut pos = 16;
|
||||||
let root_node_address = read_offset(file_data, pos, offset_size)?;
|
w.ensure(pos, usize::from(offset_size))?;
|
||||||
|
let root_node_address = read_offset(d, pos, offset_size)?;
|
||||||
pos += offset_size as usize;
|
pos += offset_size as usize;
|
||||||
|
|
||||||
ensure_len(file_data, pos, 2)?;
|
w.ensure(pos, 2)?;
|
||||||
let num_records_in_root = u16::from_le_bytes([file_data[pos], file_data[pos + 1]]);
|
let num_records_in_root = u16::from_le_bytes([d[pos], d[pos + 1]]);
|
||||||
pos += 2;
|
pos += 2;
|
||||||
|
|
||||||
let total_records = read_offset(file_data, pos, length_size)?;
|
w.ensure(pos, usize::from(length_size))?;
|
||||||
|
let total_records = read_offset(d, pos, length_size)?;
|
||||||
#[allow(unused_assignments)]
|
#[allow(unused_assignments)]
|
||||||
{
|
{
|
||||||
pos += length_size as usize;
|
pos += length_size as usize;
|
||||||
@@ -137,9 +154,9 @@ impl BTreeV2Header {
|
|||||||
// Validate header checksum
|
// Validate header checksum
|
||||||
#[cfg(feature = "checksum")]
|
#[cfg(feature = "checksum")]
|
||||||
{
|
{
|
||||||
ensure_len(file_data, pos, 4)?;
|
w.ensure(pos, 4)?;
|
||||||
let stored = LittleEndian::read_u32(&file_data[pos..pos + 4]);
|
let stored = LittleEndian::read_u32(&d[pos..pos + 4]);
|
||||||
let computed = crate::checksum::jenkins_lookup3(&file_data[offset..pos]);
|
let computed = crate::checksum::jenkins_lookup3(&d[..pos]);
|
||||||
if computed != stored {
|
if computed != stored {
|
||||||
return Err(FormatError::ChecksumMismatch {
|
return Err(FormatError::ChecksumMismatch {
|
||||||
expected: stored,
|
expected: stored,
|
||||||
@@ -163,7 +180,7 @@ impl BTreeV2Header {
|
|||||||
/// Compute maximum records per node for a given depth level.
|
/// Compute maximum records per node for a given depth level.
|
||||||
/// leaf: (node_size - overhead) / record_size
|
/// leaf: (node_size - overhead) / record_size
|
||||||
/// internal: depends on pointers
|
/// internal: depends on pointers
|
||||||
fn max_records_leaf(node_size: u32, record_size: u16) -> u64 {
|
pub(crate) fn max_records_leaf(node_size: u32, record_size: u16) -> u64 {
|
||||||
// Leaf overhead: signature(4) + version(1) + type(1) + checksum(4) = 10
|
// Leaf overhead: signature(4) + version(1) + type(1) + checksum(4) = 10
|
||||||
let overhead = 10u32;
|
let overhead = 10u32;
|
||||||
if node_size <= overhead || record_size == 0 {
|
if node_size <= overhead || record_size == 0 {
|
||||||
@@ -189,6 +206,17 @@ pub fn collect_btree_v2_records(
|
|||||||
header: &BTreeV2Header,
|
header: &BTreeV2Header,
|
||||||
offset_size: u8,
|
offset_size: u8,
|
||||||
length_size: u8,
|
length_size: u8,
|
||||||
|
) -> Result<Vec<BTreeV2Record>, FormatError> {
|
||||||
|
collect_btree_v2_records_in(file_data, header, offset_size, length_size)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`collect_btree_v2_records`] over any [`Storage`]: one bounded read per
|
||||||
|
/// node.
|
||||||
|
pub fn collect_btree_v2_records_in<S: Storage + ?Sized>(
|
||||||
|
file: &S,
|
||||||
|
header: &BTreeV2Header,
|
||||||
|
offset_size: u8,
|
||||||
|
length_size: u8,
|
||||||
) -> Result<Vec<BTreeV2Record>, FormatError> {
|
) -> Result<Vec<BTreeV2Record>, FormatError> {
|
||||||
if header.total_records == 0 || header.num_records_in_root == 0 {
|
if header.total_records == 0 || header.num_records_in_root == 0 {
|
||||||
return Ok(Vec::new());
|
return Ok(Vec::new());
|
||||||
@@ -208,24 +236,25 @@ pub fn collect_btree_v2_records(
|
|||||||
// millions of records from a few kilobytes. Counting against what the
|
// millions of records from a few kilobytes. Counting against what the
|
||||||
// file could physically contain bounds that without trusting the
|
// file could physically contain bounds that without trusting the
|
||||||
// header's own `total_records`.
|
// header's own `total_records`.
|
||||||
let mut budget = file_data.len() / usize::from(header.record_size.max(1));
|
let mut budget = len_usize(file) / usize::from(header.record_size.max(1));
|
||||||
|
|
||||||
let max_leaf_nrec = max_records_leaf(header.node_size, header.record_size);
|
let max_leaf_nrec = max_records_leaf(header.node_size, header.record_size);
|
||||||
|
|
||||||
if header.depth == 0 {
|
if header.depth == 0 {
|
||||||
// Root is a leaf
|
// Root is a leaf
|
||||||
parse_leaf_records(
|
parse_leaf_records(
|
||||||
file_data,
|
file,
|
||||||
header.root_node_address as usize,
|
to_usize(header.root_node_address)?,
|
||||||
header.num_records_in_root,
|
header.num_records_in_root,
|
||||||
header.record_size,
|
header.record_size,
|
||||||
|
header.node_size,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
// Root is internal; traverse recursively
|
// Root is internal; traverse recursively
|
||||||
let mut records = Vec::new();
|
let mut records = Vec::new();
|
||||||
collect_internal_records(
|
collect_internal_records(
|
||||||
file_data,
|
file,
|
||||||
header.root_node_address as usize,
|
to_usize(header.root_node_address)?,
|
||||||
header.num_records_in_root,
|
header.num_records_in_root,
|
||||||
header.depth,
|
header.depth,
|
||||||
header.record_size,
|
header.record_size,
|
||||||
@@ -240,36 +269,72 @@ pub fn collect_btree_v2_records(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A node's bytes: `want` bytes at `offset` (fewer only at the end of the
|
||||||
|
/// file), after checking its 4-byte signature. A node is read in one piece
|
||||||
|
/// when it fits in `node_size` (every valid node does); a larger claimed
|
||||||
|
/// extent — record counts from a damaged parent — is first checked against
|
||||||
|
/// the end of the file, so it costs a read only of bytes the file has.
|
||||||
|
/// Bounds errors are the whole-file ones: the signature check needs the
|
||||||
|
/// first 6 bytes, then `checks` — `(position, length)` pairs relative to
|
||||||
|
/// the node, in the order the parser checks them — must lie in the file.
|
||||||
|
fn read_node<'a, S: Storage + ?Sized>(
|
||||||
|
file: &'a S,
|
||||||
|
offset: usize,
|
||||||
|
want: usize,
|
||||||
|
node_size: u32,
|
||||||
|
signature: &[u8; 4],
|
||||||
|
checks: &[(usize, usize)],
|
||||||
|
) -> Result<Window<'a>, FormatError> {
|
||||||
|
let one_read = usize::try_from(node_size).unwrap_or(usize::MAX).max(6);
|
||||||
|
let w = Window::read(file, offset as u64, want.min(one_read))?;
|
||||||
|
w.ensure(0, 6)?;
|
||||||
|
if &w.bytes[..4] != signature {
|
||||||
|
return Err(FormatError::InvalidBTreeV2Signature);
|
||||||
|
}
|
||||||
|
if want <= one_read {
|
||||||
|
return Ok(w);
|
||||||
|
}
|
||||||
|
for &(rel, len) in checks {
|
||||||
|
Window::check_extent(file, offset as u64, rel, len)?;
|
||||||
|
}
|
||||||
|
Window::read(file, offset as u64, want)
|
||||||
|
}
|
||||||
|
|
||||||
/// Parse records from a leaf node (signature "BTLF").
|
/// Parse records from a leaf node (signature "BTLF").
|
||||||
fn parse_leaf_records(
|
fn parse_leaf_records<S: Storage + ?Sized>(
|
||||||
file_data: &[u8],
|
file: &S,
|
||||||
offset: usize,
|
offset: usize,
|
||||||
num_records: u16,
|
num_records: u16,
|
||||||
record_size: u16,
|
record_size: u16,
|
||||||
|
node_size: u32,
|
||||||
) -> Result<Vec<BTreeV2Record>, FormatError> {
|
) -> Result<Vec<BTreeV2Record>, FormatError> {
|
||||||
// signature(4) + version(1) + type(1) = 6 bytes header
|
// signature(4) + version(1) + type(1) = 6 bytes header
|
||||||
ensure_len(file_data, offset, 6)?;
|
let pos = 6;
|
||||||
if &file_data[offset..offset + 4] != b"BTLF" {
|
|
||||||
return Err(FormatError::InvalidBTreeV2Signature);
|
|
||||||
}
|
|
||||||
|
|
||||||
let pos = offset + 6;
|
|
||||||
let rs = record_size as usize;
|
let rs = record_size as usize;
|
||||||
let total = (num_records as usize)
|
let total = (num_records as usize)
|
||||||
.checked_mul(rs)
|
.checked_mul(rs)
|
||||||
.ok_or(FormatError::UnexpectedEof {
|
.ok_or(FormatError::UnexpectedEof {
|
||||||
expected: usize::MAX,
|
expected: usize::MAX,
|
||||||
available: file_data.len(),
|
available: len_usize(file),
|
||||||
})?;
|
})?;
|
||||||
ensure_len(file_data, pos, total)?;
|
let w = read_node(
|
||||||
|
file,
|
||||||
|
offset,
|
||||||
|
pos + total + 4,
|
||||||
|
node_size,
|
||||||
|
b"BTLF",
|
||||||
|
&[(pos, total)],
|
||||||
|
)?;
|
||||||
|
let d = &w.bytes;
|
||||||
|
w.ensure(pos, total)?;
|
||||||
|
|
||||||
// Validate checksum: 4 bytes after records + padding
|
// Validate checksum: 4 bytes after records + padding
|
||||||
#[cfg(feature = "checksum")]
|
#[cfg(feature = "checksum")]
|
||||||
{
|
{
|
||||||
let checksum_pos = pos + total;
|
let checksum_pos = pos + total;
|
||||||
if file_data.len() >= checksum_pos + 4 {
|
if d.len() >= checksum_pos + 4 {
|
||||||
let stored = LittleEndian::read_u32(&file_data[checksum_pos..checksum_pos + 4]);
|
let stored = LittleEndian::read_u32(&d[checksum_pos..checksum_pos + 4]);
|
||||||
let computed = crate::checksum::jenkins_lookup3(&file_data[offset..checksum_pos]);
|
let computed = crate::checksum::jenkins_lookup3(&d[..checksum_pos]);
|
||||||
if computed != stored {
|
if computed != stored {
|
||||||
return Err(FormatError::ChecksumMismatch {
|
return Err(FormatError::ChecksumMismatch {
|
||||||
expected: stored,
|
expected: stored,
|
||||||
@@ -283,45 +348,57 @@ fn parse_leaf_records(
|
|||||||
for i in 0..num_records as usize {
|
for i in 0..num_records as usize {
|
||||||
let start = pos + i * rs;
|
let start = pos + i * rs;
|
||||||
records.push(BTreeV2Record {
|
records.push(BTreeV2Record {
|
||||||
data: file_data[start..start + rs].to_vec(),
|
data: d[start..start + rs].to_vec(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Ok(records)
|
Ok(records)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Recursively collect records from an internal node.
|
/// An internal node read from the file: its bytes (from the signature on),
|
||||||
#[allow(clippy::too_many_arguments, clippy::only_used_in_recursion)]
|
/// where its records start, and its children as `(address, record count)`.
|
||||||
fn collect_internal_records(
|
struct InternalNode<'a> {
|
||||||
file_data: &[u8],
|
node: Window<'a>,
|
||||||
|
records_start: usize,
|
||||||
|
children: Vec<(u64, u16)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InternalNode<'_> {
|
||||||
|
/// Record `i`, `rs` bytes long.
|
||||||
|
fn record(&self, i: usize, rs: usize) -> Result<&[u8], FormatError> {
|
||||||
|
let overflow = || FormatError::UnexpectedEof {
|
||||||
|
expected: usize::MAX,
|
||||||
|
available: usize::MAX,
|
||||||
|
};
|
||||||
|
let rec_start = i
|
||||||
|
.checked_mul(rs)
|
||||||
|
.and_then(|o| self.records_start.checked_add(o))
|
||||||
|
.ok_or_else(overflow)?;
|
||||||
|
self.node.ensure(rec_start, rs)?;
|
||||||
|
Ok(&self.node.bytes[rec_start..rec_start + rs])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An internal node's layout: where its records start, and its children as
|
||||||
|
/// `(address, record count)`.
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
fn read_internal_node<S: Storage + ?Sized>(
|
||||||
|
file: &S,
|
||||||
offset: usize,
|
offset: usize,
|
||||||
num_records: u16,
|
num_records: u16,
|
||||||
depth: u16,
|
depth: u16,
|
||||||
record_size: u16,
|
record_size: u16,
|
||||||
node_size: u32,
|
node_size: u32,
|
||||||
offset_size: u8,
|
offset_size: u8,
|
||||||
length_size: u8,
|
|
||||||
max_leaf_nrec: u64,
|
max_leaf_nrec: u64,
|
||||||
budget: &mut usize,
|
) -> Result<InternalNode<'_>, FormatError> {
|
||||||
out: &mut Vec<BTreeV2Record>,
|
|
||||||
) -> Result<(), FormatError> {
|
|
||||||
// signature(4) + version(1) + type(1) = 6
|
|
||||||
ensure_len(file_data, offset, 6)?;
|
|
||||||
if &file_data[offset..offset + 4] != b"BTIN" {
|
|
||||||
return Err(FormatError::InvalidBTreeV2Signature);
|
|
||||||
}
|
|
||||||
|
|
||||||
let nr = num_records as usize;
|
let nr = num_records as usize;
|
||||||
let rs = record_size as usize;
|
let rs = record_size as usize;
|
||||||
let mut pos = offset + 6;
|
|
||||||
|
|
||||||
// Read all records first
|
// Records first
|
||||||
let records_total = nr.checked_mul(rs).ok_or(FormatError::UnexpectedEof {
|
let records_total = nr.checked_mul(rs).ok_or(FormatError::UnexpectedEof {
|
||||||
expected: usize::MAX,
|
expected: usize::MAX,
|
||||||
available: file_data.len(),
|
available: len_usize(file),
|
||||||
})?;
|
})?;
|
||||||
ensure_len(file_data, pos, records_total)?;
|
|
||||||
let records_start = pos;
|
|
||||||
pos += records_total;
|
|
||||||
|
|
||||||
// Child pointer layout, as libhdf5 computes it (H5B2__hdr_init): the
|
// Child pointer layout, as libhdf5 computes it (H5B2__hdr_init): the
|
||||||
// child's record count is always encoded in the width needed for a
|
// child's record count is always encoded in the width needed for a
|
||||||
@@ -344,32 +421,105 @@ fn collect_internal_records(
|
|||||||
|
|
||||||
let num_children = nr + 1;
|
let num_children = nr + 1;
|
||||||
let child_ptr_size = offset_size as usize + nrec_width + total_nrec_width;
|
let child_ptr_size = offset_size as usize + nrec_width + total_nrec_width;
|
||||||
ensure_len(file_data, pos, num_children * child_ptr_size)?;
|
let pointers = num_children * child_ptr_size;
|
||||||
|
|
||||||
|
// signature(4) + version(1) + type(1) = 6, records, pointers, checksum.
|
||||||
|
let w = read_node(
|
||||||
|
file,
|
||||||
|
offset,
|
||||||
|
6 + records_total + pointers + 4,
|
||||||
|
node_size,
|
||||||
|
b"BTIN",
|
||||||
|
&[(6, records_total), (6 + records_total, pointers)],
|
||||||
|
)?;
|
||||||
|
let d = &w.bytes;
|
||||||
|
let mut pos = 6;
|
||||||
|
w.ensure(pos, records_total)?;
|
||||||
|
let records_start = pos;
|
||||||
|
pos += records_total;
|
||||||
|
|
||||||
|
w.ensure(pos, pointers)?;
|
||||||
|
|
||||||
// Read child pointers
|
|
||||||
let mut children = Vec::with_capacity(num_children);
|
let mut children = Vec::with_capacity(num_children);
|
||||||
for _ in 0..num_children {
|
for _ in 0..num_children {
|
||||||
let addr = read_offset(file_data, pos, offset_size)?;
|
let addr = read_offset(d, pos, offset_size)?;
|
||||||
pos += offset_size as usize;
|
pos += offset_size as usize;
|
||||||
let child_nrec = read_var_uint(file_data, pos, nrec_width)? as u16;
|
let child_nrec = read_var_uint(d, pos, nrec_width)? as u16;
|
||||||
pos += nrec_width;
|
pos += nrec_width;
|
||||||
pos += total_nrec_width; // skip total records in subtree
|
pos += total_nrec_width; // skip total records in subtree
|
||||||
children.push((addr, child_nrec));
|
children.push((addr, child_nrec));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The checksum follows the child pointers and covers the node up to it.
|
||||||
|
// Lookups prune children by the keys in this node, so an unverified
|
||||||
|
// internal node could hide a record without any error: libhdf5 refuses
|
||||||
|
// a mismatch here, and so does this.
|
||||||
|
#[cfg(feature = "checksum")]
|
||||||
|
{
|
||||||
|
w.ensure(pos, 4)?;
|
||||||
|
let stored = LittleEndian::read_u32(&d[pos..pos + 4]);
|
||||||
|
let computed = crate::checksum::jenkins_lookup3(&d[..pos]);
|
||||||
|
if computed != stored {
|
||||||
|
return Err(FormatError::ChecksumMismatch {
|
||||||
|
expected: stored,
|
||||||
|
computed,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(InternalNode {
|
||||||
|
node: w,
|
||||||
|
records_start,
|
||||||
|
children,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Recursively collect records from an internal node.
|
||||||
|
#[allow(clippy::too_many_arguments, clippy::only_used_in_recursion)]
|
||||||
|
fn collect_internal_records<S: Storage + ?Sized>(
|
||||||
|
file: &S,
|
||||||
|
offset: usize,
|
||||||
|
num_records: u16,
|
||||||
|
depth: u16,
|
||||||
|
record_size: u16,
|
||||||
|
node_size: u32,
|
||||||
|
offset_size: u8,
|
||||||
|
length_size: u8,
|
||||||
|
max_leaf_nrec: u64,
|
||||||
|
budget: &mut usize,
|
||||||
|
out: &mut Vec<BTreeV2Record>,
|
||||||
|
) -> Result<(), FormatError> {
|
||||||
|
let nr = num_records as usize;
|
||||||
|
let rs = record_size as usize;
|
||||||
|
let node = read_internal_node(
|
||||||
|
file,
|
||||||
|
offset,
|
||||||
|
num_records,
|
||||||
|
depth,
|
||||||
|
record_size,
|
||||||
|
node_size,
|
||||||
|
offset_size,
|
||||||
|
max_leaf_nrec,
|
||||||
|
)?;
|
||||||
|
let child_depth = depth - 1;
|
||||||
|
|
||||||
// Interleave: child[0], record[0], child[1], record[1], ..., child[nr]
|
// Interleave: child[0], record[0], child[1], record[1], ..., child[nr]
|
||||||
// We collect child[0] records, then record[0], then child[1], etc.
|
// We collect child[0] records, then record[0], then child[1], etc.
|
||||||
for (i, &(child_addr, child_nrec)) in children.iter().enumerate() {
|
for (i, &(child_addr, child_nrec)) in node.children.iter().enumerate() {
|
||||||
if child_depth == 0 {
|
if child_depth == 0 {
|
||||||
// Before parsing, so a refused tree is not also a large allocation.
|
// Before parsing, so a refused tree is not also a large allocation.
|
||||||
spend(budget, usize::from(child_nrec))?;
|
spend(budget, usize::from(child_nrec))?;
|
||||||
let leaf_recs =
|
let leaf_recs = parse_leaf_records(
|
||||||
parse_leaf_records(file_data, child_addr as usize, child_nrec, record_size)?;
|
file,
|
||||||
|
to_usize(child_addr)?,
|
||||||
|
child_nrec,
|
||||||
|
record_size,
|
||||||
|
node_size,
|
||||||
|
)?;
|
||||||
out.extend(leaf_recs);
|
out.extend(leaf_recs);
|
||||||
} else {
|
} else {
|
||||||
collect_internal_records(
|
collect_internal_records(
|
||||||
file_data,
|
file,
|
||||||
child_addr as usize,
|
to_usize(child_addr)?,
|
||||||
child_nrec,
|
child_nrec,
|
||||||
child_depth,
|
child_depth,
|
||||||
record_size,
|
record_size,
|
||||||
@@ -384,32 +534,10 @@ fn collect_internal_records(
|
|||||||
|
|
||||||
// Add record[i] (except after the last child)
|
// Add record[i] (except after the last child)
|
||||||
if i < nr {
|
if i < nr {
|
||||||
let rec_offset = i.checked_mul(rs).ok_or(FormatError::UnexpectedEof {
|
let data = node.record(i, rs)?;
|
||||||
expected: usize::MAX,
|
|
||||||
available: file_data.len(),
|
|
||||||
})?;
|
|
||||||
let rec_start =
|
|
||||||
records_start
|
|
||||||
.checked_add(rec_offset)
|
|
||||||
.ok_or(FormatError::UnexpectedEof {
|
|
||||||
expected: usize::MAX,
|
|
||||||
available: file_data.len(),
|
|
||||||
})?;
|
|
||||||
let rec_end = rec_start
|
|
||||||
.checked_add(rs)
|
|
||||||
.ok_or(FormatError::UnexpectedEof {
|
|
||||||
expected: usize::MAX,
|
|
||||||
available: file_data.len(),
|
|
||||||
})?;
|
|
||||||
if rec_end > file_data.len() {
|
|
||||||
return Err(FormatError::UnexpectedEof {
|
|
||||||
expected: rec_end,
|
|
||||||
available: file_data.len(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
spend(budget, 1)?;
|
spend(budget, 1)?;
|
||||||
out.push(BTreeV2Record {
|
out.push(BTreeV2Record {
|
||||||
data: file_data[rec_start..rec_end].to_vec(),
|
data: data.to_vec(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -417,11 +545,135 @@ fn collect_internal_records(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The records of a B-tree v2 that fall in one key range, found by
|
||||||
|
/// descending the tree instead of reading all of it.
|
||||||
|
///
|
||||||
|
/// `cmp` places a record relative to the range: `Less` if the record sorts
|
||||||
|
/// before it, `Greater` if after, `Equal` if the record is in it. The tree
|
||||||
|
/// must be ordered consistently with `cmp`, as libhdf5 orders it (a link or
|
||||||
|
/// attribute name index by name hash, so all records with one hash form a
|
||||||
|
/// range whatever order their names are in). Only the nodes whose key
|
||||||
|
/// interval overlaps the range are read: O(depth) nodes plus those holding
|
||||||
|
/// the matches. Matches come in tree order.
|
||||||
|
pub fn find_btree_v2_records(
|
||||||
|
file_data: &[u8],
|
||||||
|
header: &BTreeV2Header,
|
||||||
|
offset_size: u8,
|
||||||
|
cmp: &mut dyn FnMut(&[u8]) -> Ordering,
|
||||||
|
) -> Result<Vec<BTreeV2Record>, FormatError> {
|
||||||
|
find_btree_v2_records_in(file_data, header, offset_size, cmp)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`find_btree_v2_records`] over any [`Storage`]: one bounded read per
|
||||||
|
/// node visited.
|
||||||
|
pub fn find_btree_v2_records_in<S: Storage + ?Sized>(
|
||||||
|
file: &S,
|
||||||
|
header: &BTreeV2Header,
|
||||||
|
offset_size: u8,
|
||||||
|
cmp: &mut dyn FnMut(&[u8]) -> Ordering,
|
||||||
|
) -> Result<Vec<BTreeV2Record>, FormatError> {
|
||||||
|
if header.total_records == 0 || header.num_records_in_root == 0 {
|
||||||
|
return Ok(Vec::new());
|
||||||
|
}
|
||||||
|
if header.depth > MAX_DEPTH {
|
||||||
|
return Err(FormatError::NestingDepthExceeded);
|
||||||
|
}
|
||||||
|
// As in `collect_btree_v2_records`: a valid tree cannot hold more
|
||||||
|
// records than the file has room for, however its children are shared.
|
||||||
|
let mut budget = len_usize(file) / usize::from(header.record_size.max(1));
|
||||||
|
let max_leaf_nrec = max_records_leaf(header.node_size, header.record_size);
|
||||||
|
let mut out = Vec::new();
|
||||||
|
find_in_node(
|
||||||
|
file,
|
||||||
|
header,
|
||||||
|
to_usize(header.root_node_address)?,
|
||||||
|
header.num_records_in_root,
|
||||||
|
header.depth,
|
||||||
|
offset_size,
|
||||||
|
max_leaf_nrec,
|
||||||
|
cmp,
|
||||||
|
&mut budget,
|
||||||
|
&mut out,
|
||||||
|
)?;
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
fn find_in_node<S: Storage + ?Sized>(
|
||||||
|
file: &S,
|
||||||
|
header: &BTreeV2Header,
|
||||||
|
offset: usize,
|
||||||
|
num_records: u16,
|
||||||
|
depth: u16,
|
||||||
|
offset_size: u8,
|
||||||
|
max_leaf_nrec: u64,
|
||||||
|
cmp: &mut dyn FnMut(&[u8]) -> Ordering,
|
||||||
|
budget: &mut usize,
|
||||||
|
out: &mut Vec<BTreeV2Record>,
|
||||||
|
) -> Result<(), FormatError> {
|
||||||
|
spend(budget, usize::from(num_records))?;
|
||||||
|
if depth == 0 {
|
||||||
|
let records = parse_leaf_records(
|
||||||
|
file,
|
||||||
|
offset,
|
||||||
|
num_records,
|
||||||
|
header.record_size,
|
||||||
|
header.node_size,
|
||||||
|
)?;
|
||||||
|
out.extend(
|
||||||
|
records
|
||||||
|
.into_iter()
|
||||||
|
.filter(|r| cmp(&r.data) == Ordering::Equal),
|
||||||
|
);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let rs = usize::from(header.record_size);
|
||||||
|
let node = read_internal_node(
|
||||||
|
file,
|
||||||
|
offset,
|
||||||
|
num_records,
|
||||||
|
depth,
|
||||||
|
header.record_size,
|
||||||
|
header.node_size,
|
||||||
|
offset_size,
|
||||||
|
max_leaf_nrec,
|
||||||
|
)?;
|
||||||
|
let nr = usize::from(num_records);
|
||||||
|
let mut order = Vec::with_capacity(nr);
|
||||||
|
for i in 0..nr {
|
||||||
|
order.push(cmp(node.record(i, rs)?));
|
||||||
|
}
|
||||||
|
// Child `i` holds the keys between record `i - 1` and record `i`: it can
|
||||||
|
// hold a match unless the record before it is already past the range or
|
||||||
|
// the record after it is still before it.
|
||||||
|
for (i, &(child_addr, child_nrec)) in node.children.iter().enumerate() {
|
||||||
|
let after_left = i == 0 || order[i - 1] != Ordering::Greater;
|
||||||
|
let before_right = i == nr || order[i] != Ordering::Less;
|
||||||
|
if after_left && before_right {
|
||||||
|
find_in_node(
|
||||||
|
file,
|
||||||
|
header,
|
||||||
|
to_usize(child_addr)?,
|
||||||
|
child_nrec,
|
||||||
|
depth - 1,
|
||||||
|
offset_size,
|
||||||
|
max_leaf_nrec,
|
||||||
|
cmp,
|
||||||
|
budget,
|
||||||
|
out,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
if i < nr && order[i] == Ordering::Equal {
|
||||||
|
out.push(BTreeV2Record {
|
||||||
|
data: node.record(i, rs)?.to_vec(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Most records a subtree whose root is at `depth` can hold (libhdf5's
|
/// Most records a subtree whose root is at `depth` can hold (libhdf5's
|
||||||
/// `cum_max_nrec`): a leaf holds `max_leaf_nrec`; an internal node at depth
|
/// `cum_max_nrec`). See [`node_info`].
|
||||||
/// `d` holds `max_nrec(d)` records and `max_nrec(d) + 1` subtrees of depth
|
|
||||||
/// `d - 1`, where `max_nrec(d)` is what fits in a node once each record is
|
|
||||||
/// paired with a child pointer of the width depth `d` needs.
|
|
||||||
fn cum_max_records(
|
fn cum_max_records(
|
||||||
node_size: u32,
|
node_size: u32,
|
||||||
record_size: u16,
|
record_size: u16,
|
||||||
@@ -429,24 +681,82 @@ fn cum_max_records(
|
|||||||
max_leaf_nrec: u64,
|
max_leaf_nrec: u64,
|
||||||
depth: u16,
|
depth: u16,
|
||||||
) -> u64 {
|
) -> u64 {
|
||||||
|
node_info_from_leaf(node_size, record_size, offset_size, max_leaf_nrec, depth)
|
||||||
|
.last()
|
||||||
|
.map_or(max_leaf_nrec, |n| n.cum_max_nrec)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Capacity of a B-tree v2 node at one depth, as libhdf5 computes it
|
||||||
|
/// (`H5B2__hdr_init`'s `node_info`).
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub(crate) struct NodeInfo {
|
||||||
|
/// Most records one node at this depth holds.
|
||||||
|
pub(crate) max_nrec: u64,
|
||||||
|
/// Most records a subtree rooted at this depth holds.
|
||||||
|
pub(crate) cum_max_nrec: u64,
|
||||||
|
/// Bytes a subtree's total record count takes in a pointer to a node
|
||||||
|
/// at this depth (0 for a leaf, whose count is its own).
|
||||||
|
pub(crate) cum_max_nrec_size: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Node capacities for depths `0..=depth` (entry `d` for depth `d`): a leaf
|
||||||
|
/// holds `max_nrec(0)` records; an internal node at depth `d` holds
|
||||||
|
/// `max_nrec(d)` records and `max_nrec(d) + 1` subtrees of depth `d - 1`,
|
||||||
|
/// where `max_nrec(d)` is what fits in a node once each record is paired
|
||||||
|
/// with a child pointer of the width depth `d` needs (address, the child's
|
||||||
|
/// record count in the width a *leaf's* maximum needs, and below the first
|
||||||
|
/// internal level the child subtree's total in the width its maximum
|
||||||
|
/// needs), with one pointer more than records.
|
||||||
|
pub(crate) fn node_info(
|
||||||
|
node_size: u32,
|
||||||
|
record_size: u16,
|
||||||
|
offset_size: u8,
|
||||||
|
depth: u16,
|
||||||
|
) -> Vec<NodeInfo> {
|
||||||
|
let max_leaf = max_records_leaf(node_size, record_size);
|
||||||
|
node_info_from_leaf(node_size, record_size, offset_size, max_leaf, depth)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn node_info_from_leaf(
|
||||||
|
node_size: u32,
|
||||||
|
record_size: u16,
|
||||||
|
offset_size: u8,
|
||||||
|
max_leaf_nrec: u64,
|
||||||
|
depth: u16,
|
||||||
|
) -> Vec<NodeInfo> {
|
||||||
// Internal node overhead: signature(4) + version(1) + type(1) + checksum(4).
|
// Internal node overhead: signature(4) + version(1) + type(1) + checksum(4).
|
||||||
const PREFIX: u64 = 10;
|
const PREFIX: u64 = 10;
|
||||||
let nrec_width = bytes_for_max_records(max_leaf_nrec) as u64;
|
let nrec_width = bytes_for_max_records(max_leaf_nrec) as u64;
|
||||||
let mut cum = max_leaf_nrec;
|
let mut info = Vec::with_capacity(usize::from(depth) + 1);
|
||||||
let mut cum_width = 0u64;
|
info.push(NodeInfo {
|
||||||
|
max_nrec: max_leaf_nrec,
|
||||||
|
cum_max_nrec: max_leaf_nrec,
|
||||||
|
cum_max_nrec_size: 0,
|
||||||
|
});
|
||||||
for d in 1..=depth {
|
for d in 1..=depth {
|
||||||
let ptr = u64::from(offset_size) + nrec_width + if d > 1 { cum_width } else { 0 };
|
let below = info[usize::from(d) - 1];
|
||||||
|
let ptr = u64::from(offset_size)
|
||||||
|
+ nrec_width
|
||||||
|
+ if d > 1 {
|
||||||
|
below.cum_max_nrec_size as u64
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
let max_nrec = u64::from(node_size)
|
let max_nrec = u64::from(node_size)
|
||||||
.saturating_sub(PREFIX)
|
.saturating_sub(PREFIX)
|
||||||
.saturating_sub(ptr)
|
.saturating_sub(ptr)
|
||||||
/ (u64::from(record_size) + ptr).max(1);
|
/ (u64::from(record_size) + ptr).max(1);
|
||||||
cum = max_nrec
|
let cum = max_nrec
|
||||||
.saturating_add(1)
|
.saturating_add(1)
|
||||||
.saturating_mul(cum)
|
.saturating_mul(below.cum_max_nrec)
|
||||||
.saturating_add(max_nrec);
|
.saturating_add(max_nrec);
|
||||||
cum_width = bytes_for_max_records(cum) as u64;
|
info.push(NodeInfo {
|
||||||
|
max_nrec,
|
||||||
|
cum_max_nrec: cum,
|
||||||
|
cum_max_nrec_size: bytes_for_max_records(cum),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
cum
|
info
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -536,6 +846,8 @@ mod tests {
|
|||||||
buf.extend_from_slice(&child_nrec.to_le_bytes()[..nrec_width]);
|
buf.extend_from_slice(&child_nrec.to_le_bytes()[..nrec_width]);
|
||||||
buf.resize(buf.len() + total_width, 0);
|
buf.resize(buf.len() + total_width, 0);
|
||||||
}
|
}
|
||||||
|
let sum = crate::checksum::jenkins_lookup3(&buf);
|
||||||
|
buf.extend_from_slice(&sum.to_le_bytes());
|
||||||
buf
|
buf
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,503 @@
|
|||||||
|
//! Writing version-2 B-trees: a header (`BTHD`) and its nodes, leaves
|
||||||
|
//! (`BTLF`) and, for more records than one leaf holds, internal nodes
|
||||||
|
//! (`BTIN`) to any depth.
|
||||||
|
//!
|
||||||
|
//! Node capacities come from [`crate::btree_v2::node_info`], the arithmetic
|
||||||
|
//! libhdf5 uses (`H5B2__hdr_init`) and the reader decodes pointers with, so
|
||||||
|
//! the pointer widths the writer encodes are the ones every reader expects.
|
||||||
|
|
||||||
|
use crate::addr::saturating_usize;
|
||||||
|
#[cfg(not(feature = "std"))]
|
||||||
|
use alloc::{format, vec, vec::Vec};
|
||||||
|
|
||||||
|
use crate::btree_v2::{NodeInfo, bytes_for_max_records, node_info};
|
||||||
|
use crate::checksum::jenkins_lookup3;
|
||||||
|
use crate::error::FormatError;
|
||||||
|
|
||||||
|
/// How a B-tree is laid out: its record type and node geometry, as the
|
||||||
|
/// header records them.
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub(crate) struct BTreeV2Params {
|
||||||
|
/// Record type (5: link names, 6: link creation order, 8: attribute
|
||||||
|
/// names, 9: attribute creation order, 10/11: chunks).
|
||||||
|
pub(crate) tree_type: u8,
|
||||||
|
/// Bytes per node.
|
||||||
|
pub(crate) node_size: u32,
|
||||||
|
/// Bytes per record.
|
||||||
|
pub(crate) record_size: u16,
|
||||||
|
/// Split and merge percentages. The writer fills nodes itself; these
|
||||||
|
/// only tell libhdf5 when to split and merge as it modifies the tree.
|
||||||
|
pub(crate) split_percent: u8,
|
||||||
|
pub(crate) merge_percent: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Size of a B-tree v2 header.
|
||||||
|
pub(crate) fn header_size(offset_size: u8, length_size: u8) -> usize {
|
||||||
|
4 + 1 + 1 + 4 + 2 + 2 + 1 + 1 + offset_size as usize + 2 + length_size as usize + 4
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deepest tree the writer builds. Even at the smallest fan-out libhdf5's
|
||||||
|
/// arithmetic allows, a few levels hold more records than any file could.
|
||||||
|
const MAX_WRITE_DEPTH: u16 = 32;
|
||||||
|
|
||||||
|
/// Write a B-tree v2 holding `records` (`record_size` bytes each,
|
||||||
|
/// concatenated, already in the tree's key order) at `addr`: the header,
|
||||||
|
/// then its nodes, each `node_size` bytes. No records gives a header with
|
||||||
|
/// an undefined root.
|
||||||
|
///
|
||||||
|
/// The tree is as shallow as the node size allows: a single leaf when the
|
||||||
|
/// records fit one, otherwise internal nodes above leaves. Records are
|
||||||
|
/// spread evenly over each node's children, so every node but the root is
|
||||||
|
/// at least about half full (above libhdf5's merge threshold, which is below
|
||||||
|
/// half), and each node holds at most its depth's maximum.
|
||||||
|
pub(crate) fn build_btree_v2(
|
||||||
|
p: BTreeV2Params,
|
||||||
|
records: &[u8],
|
||||||
|
addr: u64,
|
||||||
|
offset_size: u8,
|
||||||
|
length_size: u8,
|
||||||
|
) -> Result<Vec<u8>, FormatError> {
|
||||||
|
let rs = usize::from(p.record_size);
|
||||||
|
if rs == 0 || !records.len().is_multiple_of(rs) {
|
||||||
|
return Err(FormatError::SerializationError(format!(
|
||||||
|
"B-tree v2 records are {} bytes, not a multiple of the record size {rs}",
|
||||||
|
records.len()
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
let n = (records.len() / rs) as u64;
|
||||||
|
let hdr_len = header_size(offset_size, length_size);
|
||||||
|
|
||||||
|
// The shallowest depth whose subtree can hold every record.
|
||||||
|
let mut info = node_info(p.node_size, p.record_size, offset_size, 0);
|
||||||
|
let max_leaf = info[0].max_nrec;
|
||||||
|
if max_leaf == 0 || max_leaf > u64::from(u16::MAX) {
|
||||||
|
return Err(FormatError::SerializationError(format!(
|
||||||
|
"a {}-byte B-tree v2 node holds {max_leaf} {}-byte records; \
|
||||||
|
a node holds 1 to 65535",
|
||||||
|
p.node_size, p.record_size
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
let mut depth = 0u16;
|
||||||
|
while info[usize::from(depth)].cum_max_nrec < n {
|
||||||
|
depth += 1;
|
||||||
|
if depth > MAX_WRITE_DEPTH {
|
||||||
|
return Err(FormatError::SerializationError(format!(
|
||||||
|
"{n} records do not fit a B-tree v2 of {}-byte nodes",
|
||||||
|
p.node_size
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
info = node_info(p.node_size, p.record_size, offset_size, depth);
|
||||||
|
let max = info[usize::from(depth)].max_nrec;
|
||||||
|
if max == 0 || max > u64::from(u16::MAX) {
|
||||||
|
return Err(FormatError::SerializationError(format!(
|
||||||
|
"a {}-byte B-tree v2 internal node holds {max} records; \
|
||||||
|
a node holds 1 to 65535",
|
||||||
|
p.node_size
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut w = TreeWriter {
|
||||||
|
p,
|
||||||
|
records,
|
||||||
|
info: &info,
|
||||||
|
nrec_width: bytes_for_max_records(max_leaf),
|
||||||
|
offset_size,
|
||||||
|
first_node: addr + hdr_len as u64,
|
||||||
|
nodes: Vec::new(),
|
||||||
|
};
|
||||||
|
let root = (n > 0)
|
||||||
|
.then(|| w.node(depth, 0, saturating_usize(n)))
|
||||||
|
.transpose()?;
|
||||||
|
|
||||||
|
let mut out = Vec::with_capacity(hdr_len + w.nodes.len() * p.node_size as usize);
|
||||||
|
out.extend_from_slice(b"BTHD");
|
||||||
|
out.push(0); // version
|
||||||
|
out.push(p.tree_type);
|
||||||
|
out.extend_from_slice(&p.node_size.to_le_bytes());
|
||||||
|
out.extend_from_slice(&p.record_size.to_le_bytes());
|
||||||
|
out.extend_from_slice(&depth.to_le_bytes());
|
||||||
|
out.push(p.split_percent);
|
||||||
|
out.push(p.merge_percent);
|
||||||
|
match root {
|
||||||
|
Some(r) => push_uint(&mut out, r.addr, offset_size as usize),
|
||||||
|
None => out.extend(core::iter::repeat_n(0xFF, offset_size as usize)),
|
||||||
|
}
|
||||||
|
let root_nrec = root.map_or(0, |r| r.nrec);
|
||||||
|
out.extend_from_slice(&(root_nrec as u16).to_le_bytes());
|
||||||
|
push_uint(&mut out, n, length_size as usize);
|
||||||
|
let sum = jenkins_lookup3(&out);
|
||||||
|
out.extend_from_slice(&sum.to_le_bytes());
|
||||||
|
debug_assert_eq!(out.len(), hdr_len);
|
||||||
|
for node in &w.nodes {
|
||||||
|
out.extend_from_slice(node);
|
||||||
|
}
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A written node, as its parent points at it.
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
struct NodeRef {
|
||||||
|
addr: u64,
|
||||||
|
/// Records in the node itself.
|
||||||
|
nrec: u64,
|
||||||
|
/// Records in the subtree it roots.
|
||||||
|
all_nrec: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TreeWriter<'a> {
|
||||||
|
p: BTreeV2Params,
|
||||||
|
records: &'a [u8],
|
||||||
|
info: &'a [NodeInfo],
|
||||||
|
/// Width of a child's record count: what a leaf's maximum needs.
|
||||||
|
nrec_width: usize,
|
||||||
|
offset_size: u8,
|
||||||
|
/// Address of the first node (right after the header).
|
||||||
|
first_node: u64,
|
||||||
|
/// Nodes in file order (children before their parent).
|
||||||
|
nodes: Vec<Vec<u8>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TreeWriter<'_> {
|
||||||
|
fn record(&self, i: usize) -> &[u8] {
|
||||||
|
let rs = usize::from(self.p.record_size);
|
||||||
|
&self.records[i * rs..(i + 1) * rs]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push_node(&mut self, mut node: Vec<u8>) -> u64 {
|
||||||
|
// The checksum covers the node up to it, not the padding after.
|
||||||
|
let sum = jenkins_lookup3(&node);
|
||||||
|
node.extend_from_slice(&sum.to_le_bytes());
|
||||||
|
debug_assert!(node.len() <= self.p.node_size as usize);
|
||||||
|
node.resize(self.p.node_size as usize, 0);
|
||||||
|
let addr = self.first_node + self.nodes.len() as u64 * u64::from(self.p.node_size);
|
||||||
|
self.nodes.push(node);
|
||||||
|
addr
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write the subtree of `depth` holding records `first..first + n`.
|
||||||
|
fn node(&mut self, depth: u16, first: usize, n: usize) -> Result<NodeRef, FormatError> {
|
||||||
|
let rs = usize::from(self.p.record_size);
|
||||||
|
let mut node = Vec::with_capacity(self.p.node_size as usize);
|
||||||
|
if depth == 0 {
|
||||||
|
debug_assert!(n as u64 <= self.info[0].max_nrec);
|
||||||
|
node.extend_from_slice(b"BTLF");
|
||||||
|
node.push(0); // version
|
||||||
|
node.push(self.p.tree_type);
|
||||||
|
node.extend_from_slice(&self.records[first * rs..(first + n) * rs]);
|
||||||
|
let addr = self.push_node(node);
|
||||||
|
return Ok(NodeRef {
|
||||||
|
addr,
|
||||||
|
nrec: n as u64,
|
||||||
|
all_nrec: n as u64,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// As few children as hold the records, at least two, with the
|
||||||
|
// records spread evenly: `k` children and `k - 1` records between
|
||||||
|
// them.
|
||||||
|
let below = self.info[usize::from(depth) - 1].cum_max_nrec;
|
||||||
|
let k = (n as u64 + 1).div_ceil(below + 1).max(2);
|
||||||
|
let max = self.info[usize::from(depth)].max_nrec;
|
||||||
|
if k - 1 > max || (n as u64) < k - 1 + k {
|
||||||
|
return Err(FormatError::SerializationError(format!(
|
||||||
|
"cannot spread {n} B-tree v2 records over {k} children at depth {depth}"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
let k = saturating_usize(k);
|
||||||
|
let in_children = n - (k - 1);
|
||||||
|
let (base, extra) = (in_children / k, in_children % k);
|
||||||
|
|
||||||
|
let mut children = Vec::with_capacity(k);
|
||||||
|
let mut separators = Vec::with_capacity(k - 1);
|
||||||
|
let mut next = first;
|
||||||
|
for c in 0..k {
|
||||||
|
let m = base + usize::from(c < extra);
|
||||||
|
children.push(self.node(depth - 1, next, m)?);
|
||||||
|
next += m;
|
||||||
|
if c + 1 < k {
|
||||||
|
separators.push(next);
|
||||||
|
next += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
debug_assert_eq!(next, first + n);
|
||||||
|
|
||||||
|
node.extend_from_slice(b"BTIN");
|
||||||
|
node.push(0); // version
|
||||||
|
node.push(self.p.tree_type);
|
||||||
|
for &s in &separators {
|
||||||
|
node.extend_from_slice(self.record(s));
|
||||||
|
}
|
||||||
|
let total_width = if depth > 1 {
|
||||||
|
self.info[usize::from(depth) - 1].cum_max_nrec_size
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
|
for c in &children {
|
||||||
|
push_uint(&mut node, c.addr, self.offset_size as usize);
|
||||||
|
push_uint(&mut node, c.nrec, self.nrec_width);
|
||||||
|
if depth > 1 {
|
||||||
|
push_uint(&mut node, c.all_nrec, total_width);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let addr = self.push_node(node);
|
||||||
|
Ok(NodeRef {
|
||||||
|
addr,
|
||||||
|
nrec: (k - 1) as u64,
|
||||||
|
all_nrec: n as u64,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Append `v` as a `width`-byte little-endian integer.
|
||||||
|
fn push_uint(buf: &mut Vec<u8>, v: u64, width: usize) {
|
||||||
|
let bytes = v.to_le_bytes();
|
||||||
|
buf.extend_from_slice(&bytes[..width.min(8)]);
|
||||||
|
buf.extend(vec![0u8; width.saturating_sub(8)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::btree_v2::{BTreeV2Header, collect_btree_v2_records};
|
||||||
|
|
||||||
|
fn params(node_size: u32, record_size: u16) -> BTreeV2Params {
|
||||||
|
BTreeV2Params {
|
||||||
|
tree_type: 5,
|
||||||
|
node_size,
|
||||||
|
record_size,
|
||||||
|
split_percent: 100,
|
||||||
|
merge_percent: 40,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `n` 11-byte records: a big-endian counter, so byte order is key order.
|
||||||
|
fn records(n: usize, rs: usize) -> Vec<u8> {
|
||||||
|
let mut out = Vec::with_capacity(n * rs);
|
||||||
|
for i in 0..n {
|
||||||
|
let mut r = vec![0u8; rs];
|
||||||
|
r[..8].copy_from_slice(&(i as u64).to_be_bytes());
|
||||||
|
out.extend_from_slice(&r);
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
fn roundtrip(node_size: u32, rs: u16, n: usize, os: u8, ls: u8) -> BTreeV2Header {
|
||||||
|
let recs = records(n, usize::from(rs));
|
||||||
|
let base = 4096u64;
|
||||||
|
let tree = build_btree_v2(params(node_size, rs), &recs, base, os, ls).unwrap();
|
||||||
|
let mut file = vec![0u8; base as usize];
|
||||||
|
file.extend_from_slice(&tree);
|
||||||
|
let hdr = BTreeV2Header::parse(&file, base as usize, os, ls).unwrap();
|
||||||
|
assert_eq!(hdr.total_records, n as u64);
|
||||||
|
let got = collect_btree_v2_records(&file, &hdr, os, ls).unwrap();
|
||||||
|
assert_eq!(got.len(), n);
|
||||||
|
let flat: Vec<u8> = got.into_iter().flat_map(|r| r.data).collect();
|
||||||
|
assert_eq!(flat, recs, "node {node_size} rs {rs} n {n}");
|
||||||
|
hdr
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn one_leaf_then_deeper_trees_read_back_in_order() {
|
||||||
|
// 512-byte nodes of 11-byte records: 45 per leaf, 1149 at depth 1,
|
||||||
|
// 26 449 at depth 2.
|
||||||
|
let info = node_info(512, 11, 8, 3);
|
||||||
|
assert_eq!(
|
||||||
|
info.iter().map(|i| i.cum_max_nrec).collect::<Vec<_>>(),
|
||||||
|
[45, 1149, 26_449, 608_349]
|
||||||
|
);
|
||||||
|
for (n, depth) in [
|
||||||
|
(0, 0),
|
||||||
|
(1, 0),
|
||||||
|
(45, 0),
|
||||||
|
(46, 1),
|
||||||
|
(1149, 1),
|
||||||
|
(1150, 2),
|
||||||
|
(26_449, 2),
|
||||||
|
(26_450, 3),
|
||||||
|
(100_000, 3),
|
||||||
|
] {
|
||||||
|
let hdr = roundtrip(512, 11, n, 8, 8);
|
||||||
|
assert_eq!(hdr.depth, depth, "{n} records");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pointer_widths_follow_the_offset_and_length_sizes() {
|
||||||
|
for (os, ls) in [(4, 4), (8, 4), (4, 8), (2, 2)] {
|
||||||
|
roundtrip(512, 11, 5000, os, ls);
|
||||||
|
}
|
||||||
|
// Wide counts: a leaf of 2048 bytes / 9-byte records (226, one byte)
|
||||||
|
// and deeper subtree totals of three bytes.
|
||||||
|
roundtrip(2048, 9, 300_000, 8, 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn every_node_is_within_its_capacity_and_above_the_merge_threshold() {
|
||||||
|
let rs = 17u16;
|
||||||
|
let n = 70_000usize;
|
||||||
|
let info = node_info(512, rs, 8, 3);
|
||||||
|
let recs = records(n, usize::from(rs));
|
||||||
|
let tree = build_btree_v2(params(512, rs), &recs, 0, 8, 8).unwrap();
|
||||||
|
let hdr_len = header_size(8, 8);
|
||||||
|
let nodes = (tree.len() - hdr_len) / 512;
|
||||||
|
for i in 0..nodes {
|
||||||
|
let node = &tree[hdr_len + i * 512..hdr_len + (i + 1) * 512];
|
||||||
|
let sig = &node[..4];
|
||||||
|
if sig == b"BTLF" {
|
||||||
|
continue; // counts checked through the parents below
|
||||||
|
}
|
||||||
|
assert_eq!(sig, b"BTIN");
|
||||||
|
}
|
||||||
|
// Walk from the header: each child's count within [40%, 100%].
|
||||||
|
let hdr = BTreeV2Header::parse(&tree, 0, 8, 8).unwrap();
|
||||||
|
assert_eq!(hdr.depth, 3);
|
||||||
|
assert!(u64::from(hdr.num_records_in_root) <= info[3].max_nrec);
|
||||||
|
fn walk(tree: &[u8], addr: usize, nrec: usize, depth: usize, info: &[NodeInfo], rs: usize) {
|
||||||
|
if depth == 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let nrec_w = bytes_for_max_records(info[0].max_nrec);
|
||||||
|
let tot_w = if depth > 1 {
|
||||||
|
info[depth - 1].cum_max_nrec_size
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
|
let mut pos = addr + 6 + nrec * rs;
|
||||||
|
for _ in 0..=nrec {
|
||||||
|
let a = u64::from_le_bytes(tree[pos..pos + 8].try_into().unwrap()) as usize;
|
||||||
|
pos += 8;
|
||||||
|
let mut c = 0usize;
|
||||||
|
for b in 0..nrec_w {
|
||||||
|
c |= usize::from(tree[pos + b]) << (8 * b);
|
||||||
|
}
|
||||||
|
pos += nrec_w + tot_w;
|
||||||
|
let max = info[depth - 1].max_nrec as usize;
|
||||||
|
assert!(c <= max && c * 100 > max * 40, "{c} of {max}");
|
||||||
|
walk(tree, a, c, depth - 1, info, rs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
walk(
|
||||||
|
&tree,
|
||||||
|
hdr.root_node_address as usize,
|
||||||
|
usize::from(hdr.num_records_in_root),
|
||||||
|
3,
|
||||||
|
&info,
|
||||||
|
usize::from(rs),
|
||||||
|
);
|
||||||
|
assert!(nodes > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Descending to a key range finds exactly the records a full read
|
||||||
|
/// holds in it — runs of equal keys that straddle node boundaries
|
||||||
|
/// included — at every depth, and nothing for keys not in the tree.
|
||||||
|
#[test]
|
||||||
|
fn a_key_range_search_matches_a_full_scan() {
|
||||||
|
use crate::btree_v2::find_btree_v2_records;
|
||||||
|
use core::cmp::Ordering;
|
||||||
|
let rs = 11usize;
|
||||||
|
// Keys 0, 0, 0, 2, 2, 2, 4, ...: runs of three, odd keys missing.
|
||||||
|
for n in [1usize, 45, 46, 1150, 30_000] {
|
||||||
|
let mut recs = Vec::with_capacity(n * rs);
|
||||||
|
for i in 0..n {
|
||||||
|
let mut r = vec![0u8; rs];
|
||||||
|
r[..8].copy_from_slice(&((i / 3 * 2) as u64).to_be_bytes());
|
||||||
|
r[8..].copy_from_slice(&[(i % 3) as u8, 0, 0]);
|
||||||
|
recs.extend_from_slice(&r);
|
||||||
|
}
|
||||||
|
let base = 4096u64;
|
||||||
|
let tree = build_btree_v2(params(512, 11), &recs, base, 8, 8).unwrap();
|
||||||
|
let mut file = vec![0u8; base as usize];
|
||||||
|
file.extend_from_slice(&tree);
|
||||||
|
let hdr = BTreeV2Header::parse(&file, base as usize, 8, 8).unwrap();
|
||||||
|
let all = collect_btree_v2_records(&file, &hdr, 8, 8).unwrap();
|
||||||
|
let key = |r: &[u8]| u64::from_be_bytes(r[..8].try_into().unwrap());
|
||||||
|
let last = key(&all[n - 1].data);
|
||||||
|
let probes = (0..=last + 1).step_by(if n > 1000 { 37 } else { 1 });
|
||||||
|
for k in probes.chain([last, last + 1, u64::MAX]) {
|
||||||
|
let found =
|
||||||
|
find_btree_v2_records(&file, &hdr, 8, &mut |r: &[u8]| key(r).cmp(&k)).unwrap();
|
||||||
|
let want: Vec<&[u8]> = all
|
||||||
|
.iter()
|
||||||
|
.map(|r| r.data.as_slice())
|
||||||
|
.filter(|r| key(r) == k)
|
||||||
|
.collect();
|
||||||
|
let got: Vec<&[u8]> = found.iter().map(|r| r.data.as_slice()).collect();
|
||||||
|
assert_eq!(got, want, "n {n} key {k}");
|
||||||
|
assert_eq!(
|
||||||
|
got.len(),
|
||||||
|
if k % 2 == 0 && k <= last {
|
||||||
|
want.len()
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Every record, or none, when the whole tree is in or out of range.
|
||||||
|
let every = find_btree_v2_records(&file, &hdr, 8, &mut |_| Ordering::Equal).unwrap();
|
||||||
|
assert_eq!(every.len(), n);
|
||||||
|
let none = find_btree_v2_records(&file, &hdr, 8, &mut |_| Ordering::Less).unwrap();
|
||||||
|
assert!(none.is_empty());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A two-level tree read through a `read_at`-only storage gives what
|
||||||
|
/// the slice gives — records, descents and errors — whole, truncated
|
||||||
|
/// at every length, and with each byte of its nodes flipped, and each
|
||||||
|
/// node costs one read.
|
||||||
|
#[test]
|
||||||
|
fn storage_reads_match_slice_reads() {
|
||||||
|
use crate::btree_v2::{
|
||||||
|
collect_btree_v2_records_in, find_btree_v2_records, find_btree_v2_records_in,
|
||||||
|
};
|
||||||
|
use crate::storage::CountingStorage;
|
||||||
|
let (rs, n, base) = (11usize, 120usize, 64usize);
|
||||||
|
let recs = records(n, rs);
|
||||||
|
let tree = build_btree_v2(params(128, 11), &recs, base as u64, 8, 8).unwrap();
|
||||||
|
let mut whole = vec![0u8; base];
|
||||||
|
whole.extend_from_slice(&tree);
|
||||||
|
let hdr = BTreeV2Header::parse(&whole, base, 8, 8).unwrap();
|
||||||
|
assert!(hdr.depth >= 1, "{hdr:?}");
|
||||||
|
let key = |r: &[u8]| u64::from_be_bytes(r[..8].try_into().unwrap());
|
||||||
|
let mut files = Vec::new();
|
||||||
|
for cut in base..=whole.len() {
|
||||||
|
files.push(whole[..cut].to_vec());
|
||||||
|
}
|
||||||
|
for at in base..whole.len() {
|
||||||
|
let mut bad = whole.clone();
|
||||||
|
bad[at] ^= 0x5a;
|
||||||
|
files.push(bad);
|
||||||
|
}
|
||||||
|
let mut ok = 0;
|
||||||
|
for f in &files {
|
||||||
|
let st = CountingStorage::new(f.clone());
|
||||||
|
let want_h = BTreeV2Header::parse(f, base, 8, 8);
|
||||||
|
let got_h = BTreeV2Header::parse_in(&st, base as u64, 8, 8);
|
||||||
|
assert_eq!(format!("{got_h:?}"), format!("{want_h:?}"));
|
||||||
|
// The nodes of the intact header, over each damaged file.
|
||||||
|
let want = collect_btree_v2_records(f, &hdr, 8, 8);
|
||||||
|
st.reset();
|
||||||
|
let got = collect_btree_v2_records_in(&st, &hdr, 8, 8);
|
||||||
|
assert_eq!(format!("{got:?}"), format!("{want:?}"));
|
||||||
|
if want.is_ok() {
|
||||||
|
ok += 1;
|
||||||
|
assert!(st.reads() <= 1 + n as u64 / 3, "{} reads", st.reads());
|
||||||
|
}
|
||||||
|
for k in [0u64, 7, 60, 119, 500] {
|
||||||
|
let want = find_btree_v2_records(f, &hdr, 8, &mut |r: &[u8]| key(r).cmp(&k));
|
||||||
|
let got = find_btree_v2_records_in(&st, &hdr, 8, &mut |r: &[u8]| key(r).cmp(&k));
|
||||||
|
assert_eq!(format!("{got:?}"), format!("{want:?}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert!(ok > 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_node_too_small_or_too_big_is_an_error() {
|
||||||
|
assert!(build_btree_v2(params(16, 11), &records(1, 11), 0, 8, 8).is_err());
|
||||||
|
// A leaf with room for more than 65 535 records.
|
||||||
|
assert!(build_btree_v2(params(1 << 20, 11), &records(1, 11), 0, 8, 8).is_err());
|
||||||
|
// Records that are not whole.
|
||||||
|
assert!(build_btree_v2(params(512, 11), &[0u8; 12], 0, 8, 8).is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,6 +14,45 @@ pub fn jenkins_lookup3(data: &[u8]) -> u32 {
|
|||||||
hashlittle(data, 0)
|
hashlittle(data, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// HDF5's Fletcher-32 checksum, as the Fletcher-32 I/O filter (filter id 3)
|
||||||
|
/// stores it after each chunk.
|
||||||
|
///
|
||||||
|
/// A line-for-line port of `H5_checksum_fletcher32` (H5checksum.c, libhdf5
|
||||||
|
/// 1.8 through 1.14): big-endian 16-bit words summed in blocks of 360, each
|
||||||
|
/// sum reduced after a block by the ones'-complement fold
|
||||||
|
/// `(s & 0xffff) + (s >> 16)` rather than `% 65535`, an odd trailing byte
|
||||||
|
/// taken as the high byte of a last word, and a final fold of both sums.
|
||||||
|
/// The fold and `% 65535` differ whenever a sum is a non-zero multiple of
|
||||||
|
/// 65535: the fold leaves 0xffff where the modulo gives 0, so the two
|
||||||
|
/// disagree on about one chunk in 32768 and libhdf5 rejects the other's
|
||||||
|
/// checksum. This must stay the only implementation.
|
||||||
|
pub fn fletcher32(data: &[u8]) -> u32 {
|
||||||
|
let mut sum1: u32 = 0;
|
||||||
|
let mut sum2: u32 = 0;
|
||||||
|
// 360 words keep both sums inside 32 bits between folds (the bound
|
||||||
|
// libhdf5 uses: after a fold sum1 < 0x10200, so sum2 stays below
|
||||||
|
// 360 * 361 / 2 * 0xffff + 360 * 0x10200 + 0x1fffe < 2^32). The adds wrap
|
||||||
|
// like the C unsigned arithmetic all the same.
|
||||||
|
let (words, odd) = data.as_chunks::<2>();
|
||||||
|
for block in words.chunks(360) {
|
||||||
|
for w in block {
|
||||||
|
sum1 = sum1.wrapping_add((u32::from(w[0]) << 8) | u32::from(w[1]));
|
||||||
|
sum2 = sum2.wrapping_add(sum1);
|
||||||
|
}
|
||||||
|
sum1 = (sum1 & 0xffff) + (sum1 >> 16);
|
||||||
|
sum2 = (sum2 & 0xffff) + (sum2 >> 16);
|
||||||
|
}
|
||||||
|
if let [last] = odd {
|
||||||
|
sum1 = sum1.wrapping_add(u32::from(*last) << 8);
|
||||||
|
sum2 = sum2.wrapping_add(sum1);
|
||||||
|
sum1 = (sum1 & 0xffff) + (sum1 >> 16);
|
||||||
|
sum2 = (sum2 & 0xffff) + (sum2 >> 16);
|
||||||
|
}
|
||||||
|
sum1 = (sum1 & 0xffff) + (sum1 >> 16);
|
||||||
|
sum2 = (sum2 & 0xffff) + (sum2 >> 16);
|
||||||
|
(sum2 << 16) | sum1
|
||||||
|
}
|
||||||
|
|
||||||
/// Compute CRC32 (IEEE / ISO 3309) over data.
|
/// Compute CRC32 (IEEE / ISO 3309) over data.
|
||||||
///
|
///
|
||||||
/// When the `fast-checksum` feature is enabled, this uses hardware CRC32
|
/// When the `fast-checksum` feature is enabled, this uses hardware CRC32
|
||||||
@@ -207,6 +246,19 @@ fn hashlittle(data: &[u8], initval: u32) -> u32 {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
/// Values of libhdf5's `H5_checksum_fletcher32` (h5py 3.x's bundled
|
||||||
|
/// libhdf5, called through ctypes). The first three are sums that are
|
||||||
|
/// multiples of 65535, where `% 65535` gave 0 instead of 0xffff.
|
||||||
|
#[test]
|
||||||
|
fn fletcher32_matches_libhdf5() {
|
||||||
|
assert_eq!(fletcher32(&[0x00, 0x01, 0xff, 0xfe]), 0x0001_ffff);
|
||||||
|
assert_eq!(fletcher32(&[0xff; 720]), 0xffff_ffff);
|
||||||
|
assert_eq!(fletcher32(&[0xff; 721]), 0xff00_ff00);
|
||||||
|
assert_eq!(fletcher32(&[0xff; 1441]), 0xff00_ff00);
|
||||||
|
assert_eq!(fletcher32(&[]), 0);
|
||||||
|
assert_eq!(fletcher32(&[7]), 0x0700_0700);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn empty_input() {
|
fn empty_input() {
|
||||||
// Empty input should return the initial state after no mixing
|
// Empty input should return the initial state after no mixing
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ use alloc::collections::BTreeMap;
|
|||||||
#[cfg(feature = "std")]
|
#[cfg(feature = "std")]
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use crate::addr::to_usize;
|
||||||
use crate::chunk_cache::ChunkCoord;
|
use crate::chunk_cache::ChunkCoord;
|
||||||
use crate::chunked_read::ChunkInfo;
|
use crate::chunked_read::ChunkInfo;
|
||||||
|
|
||||||
@@ -167,7 +168,15 @@ impl ChunkLayout {
|
|||||||
|
|
||||||
for (_coord, ci) in index.iter() {
|
for (_coord, ci) in index.iter() {
|
||||||
let coord: ChunkCoord = ci.offsets.iter().take(rank).copied().collect();
|
let coord: ChunkCoord = ci.offsets.iter().take(rank).copied().collect();
|
||||||
let chunk_offsets: Vec<usize> = coord.iter().map(|&o| o as usize).collect();
|
// `ds_dims` are `usize`: a chunk at an offset past `usize::MAX`
|
||||||
|
// (only on a 32-bit target) lies outside the dataset.
|
||||||
|
let Ok(chunk_offsets) = coord
|
||||||
|
.iter()
|
||||||
|
.map(|&o| to_usize(o))
|
||||||
|
.collect::<Result<Vec<usize>, _>>()
|
||||||
|
else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
let copies = if rank == 0 {
|
let copies = if rank == 0 {
|
||||||
// Scalar dataset — single copy
|
// Scalar dataset — single copy
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -3,9 +3,11 @@
|
|||||||
#[cfg(not(feature = "std"))]
|
#[cfg(not(feature = "std"))]
|
||||||
extern crate alloc;
|
extern crate alloc;
|
||||||
|
|
||||||
|
use crate::addr::saturating_usize;
|
||||||
#[cfg(not(feature = "std"))]
|
#[cfg(not(feature = "std"))]
|
||||||
use alloc::{format, vec, vec::Vec};
|
use alloc::{format, vec, vec::Vec};
|
||||||
|
|
||||||
|
use crate::btree_v2_write::{BTreeV2Params, build_btree_v2};
|
||||||
use crate::checksum::jenkins_lookup3;
|
use crate::checksum::jenkins_lookup3;
|
||||||
use crate::chunk_cache::{CACHE_LINE_SIZE, align_to_cache_line};
|
use crate::chunk_cache::{CACHE_LINE_SIZE, align_to_cache_line};
|
||||||
use crate::chunk_grid::ChunkGrid;
|
use crate::chunk_grid::ChunkGrid;
|
||||||
@@ -16,7 +18,7 @@ use crate::filter_pipeline::{
|
|||||||
FILTER_LZF, FILTER_PCODEC, FILTER_PCODEC_NAME, FILTER_SHUFFLE, FILTER_ZSTD, FilterDescription,
|
FILTER_LZF, FILTER_PCODEC, FILTER_PCODEC_NAME, FILTER_SHUFFLE, FILTER_ZSTD, FilterDescription,
|
||||||
FilterPipeline,
|
FilterPipeline,
|
||||||
};
|
};
|
||||||
use crate::filters::compress_chunk;
|
use crate::filters::compress_chunk_masked;
|
||||||
/// Round a file offset up to the next cache-line boundary.
|
/// Round a file offset up to the next cache-line boundary.
|
||||||
///
|
///
|
||||||
/// This ensures chunk data starts at an address that is a multiple of the
|
/// This ensures chunk data starts at an address that is a multiple of the
|
||||||
@@ -413,18 +415,18 @@ pub fn split_into_chunks(
|
|||||||
// Dataset strides (row-major)
|
// Dataset strides (row-major)
|
||||||
let mut ds_strides = vec![1usize; rank];
|
let mut ds_strides = vec![1usize; rank];
|
||||||
for i in (0..rank.saturating_sub(1)).rev() {
|
for i in (0..rank.saturating_sub(1)).rev() {
|
||||||
ds_strides[i] = ds_strides[i + 1] * shape[i + 1] as usize;
|
ds_strides[i] = ds_strides[i + 1] * saturating_usize(shape[i + 1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Chunk strides
|
// Chunk strides
|
||||||
let mut chunk_strides = vec![1usize; rank];
|
let mut chunk_strides = vec![1usize; rank];
|
||||||
for i in (0..rank.saturating_sub(1)).rev() {
|
for i in (0..rank.saturating_sub(1)).rev() {
|
||||||
chunk_strides[i] = chunk_strides[i + 1] * chunk_dims[i + 1] as usize;
|
chunk_strides[i] = chunk_strides[i + 1] * saturating_usize(chunk_dims[i + 1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
let chunk_total_elements: usize = chunk_dims.iter().map(|&d| d as usize).product();
|
let chunk_total_elements: usize = chunk_dims.iter().map(|&d| saturating_usize(d)).product();
|
||||||
|
|
||||||
let mut result = Vec::with_capacity(total_chunks as usize);
|
let mut result = Vec::with_capacity(saturating_usize(total_chunks));
|
||||||
|
|
||||||
for linear_idx in 0..total_chunks {
|
for linear_idx in 0..total_chunks {
|
||||||
// Convert linear index to chunk grid coordinates
|
// Convert linear index to chunk grid coordinates
|
||||||
@@ -452,8 +454,8 @@ pub fn split_into_chunks(
|
|||||||
let coord_in_chunk = remaining_idx / chunk_strides[d];
|
let coord_in_chunk = remaining_idx / chunk_strides[d];
|
||||||
remaining_idx %= chunk_strides[d];
|
remaining_idx %= chunk_strides[d];
|
||||||
|
|
||||||
let global_coord = offsets[d] as usize + coord_in_chunk;
|
let global_coord = saturating_usize(offsets[d]) + coord_in_chunk;
|
||||||
if global_coord >= shape[d] as usize {
|
if global_coord >= saturating_usize(shape[d]) {
|
||||||
out_of_bounds = true;
|
out_of_bounds = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -488,7 +490,12 @@ pub fn split_into_chunks(
|
|||||||
#[cfg(feature = "parallel")]
|
#[cfg(feature = "parallel")]
|
||||||
const PARALLEL_COMPRESS_THRESHOLD: usize = 2;
|
const PARALLEL_COMPRESS_THRESHOLD: usize = 2;
|
||||||
|
|
||||||
/// Compress all chunks, using parallel compression when beneficial.
|
/// Compress all chunks, using parallel compression when beneficial, and
|
||||||
|
/// return each chunk's stored bytes with its filter mask.
|
||||||
|
///
|
||||||
|
/// Chunks run through the pipeline as libhdf5 runs them
|
||||||
|
/// ([`compress_chunk_masked`]): an optional filter that fails — LZF or Blosc
|
||||||
|
/// output no smaller than its input — is skipped and its mask bit set.
|
||||||
///
|
///
|
||||||
/// With the `parallel` feature and more than [`PARALLEL_COMPRESS_THRESHOLD`]
|
/// With the `parallel` feature and more than [`PARALLEL_COMPRESS_THRESHOLD`]
|
||||||
/// filtered chunks, compression runs across rayon threads; otherwise it is
|
/// filtered chunks, compression runs across rayon threads; otherwise it is
|
||||||
@@ -498,7 +505,7 @@ fn compress_all_chunks(
|
|||||||
chunks: &[(Vec<u64>, Vec<u8>)],
|
chunks: &[(Vec<u64>, Vec<u8>)],
|
||||||
pipeline: &Option<FilterPipeline>,
|
pipeline: &Option<FilterPipeline>,
|
||||||
element_size: u32,
|
element_size: u32,
|
||||||
) -> Result<Vec<Vec<u8>>, FormatError> {
|
) -> Result<Vec<(Vec<u8>, u32)>, FormatError> {
|
||||||
#[cfg(feature = "parallel")]
|
#[cfg(feature = "parallel")]
|
||||||
{
|
{
|
||||||
if let Some(pl) = pipeline
|
if let Some(pl) = pipeline
|
||||||
@@ -507,7 +514,7 @@ fn compress_all_chunks(
|
|||||||
use rayon::prelude::*;
|
use rayon::prelude::*;
|
||||||
return chunks
|
return chunks
|
||||||
.par_iter()
|
.par_iter()
|
||||||
.map(|(_offsets, chunk_bytes)| compress_chunk(chunk_bytes, pl, element_size))
|
.map(|(_offsets, chunk_bytes)| compress_chunk_masked(chunk_bytes, pl, element_size))
|
||||||
.collect();
|
.collect();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -517,9 +524,9 @@ fn compress_all_chunks(
|
|||||||
.iter()
|
.iter()
|
||||||
.map(|(_offsets, chunk_bytes)| {
|
.map(|(_offsets, chunk_bytes)| {
|
||||||
if let Some(pl) = pipeline {
|
if let Some(pl) = pipeline {
|
||||||
compress_chunk(chunk_bytes, pl, element_size)
|
compress_chunk_masked(chunk_bytes, pl, element_size)
|
||||||
} else {
|
} else {
|
||||||
Ok(chunk_bytes.clone())
|
Ok((chunk_bytes.clone(), 0))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
@@ -797,8 +804,10 @@ pub fn build_fixed_array_at(
|
|||||||
/// writer passes eliminates the double-compression that the two-pass layout
|
/// writer passes eliminates the double-compression that the two-pass layout
|
||||||
/// algorithm previously performed.
|
/// algorithm previously performed.
|
||||||
pub struct PrecompressedChunks {
|
pub struct PrecompressedChunks {
|
||||||
/// Per-chunk: (raw_size_bytes, compressed_bytes).
|
/// Per-chunk: (raw_size_bytes, stored_bytes, filter_mask). Bit `i` of
|
||||||
pub chunks: Vec<(u64, Vec<u8>)>,
|
/// the mask is set when filter `i` was skipped (an optional filter that
|
||||||
|
/// failed); 0 for every chunk of an unfiltered dataset.
|
||||||
|
pub chunks: Vec<(u64, Vec<u8>, u32)>,
|
||||||
pub has_filters: bool,
|
pub has_filters: bool,
|
||||||
pub element_size: usize,
|
pub element_size: usize,
|
||||||
pub shape: Vec<u64>,
|
pub shape: Vec<u64>,
|
||||||
@@ -833,7 +842,7 @@ pub fn precompress_chunks(
|
|||||||
let chunks = raw_chunks
|
let chunks = raw_chunks
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.zip(compressed)
|
.zip(compressed)
|
||||||
.map(|((_offsets, raw_bytes), c)| (raw_bytes.len() as u64, c))
|
.map(|((_offsets, raw_bytes), (c, mask))| (raw_bytes.len() as u64, c, mask))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
Ok(PrecompressedChunks {
|
Ok(PrecompressedChunks {
|
||||||
@@ -866,7 +875,7 @@ pub fn build_chunked_data_from_precompressed(
|
|||||||
let mut data_buf = Vec::new();
|
let mut data_buf = Vec::new();
|
||||||
let mut written_chunks = Vec::with_capacity(num_chunks);
|
let mut written_chunks = Vec::with_capacity(num_chunks);
|
||||||
|
|
||||||
for (raw_size, compressed) in &pre.chunks {
|
for (raw_size, compressed, filter_mask) in &pre.chunks {
|
||||||
let aligned_offset = align_to_cache_line(data_buf.len());
|
let aligned_offset = align_to_cache_line(data_buf.len());
|
||||||
if aligned_offset > data_buf.len() {
|
if aligned_offset > data_buf.len() {
|
||||||
data_buf.resize(aligned_offset, 0u8);
|
data_buf.resize(aligned_offset, 0u8);
|
||||||
@@ -878,7 +887,7 @@ pub fn build_chunked_data_from_precompressed(
|
|||||||
address,
|
address,
|
||||||
compressed_size,
|
compressed_size,
|
||||||
raw_size: *raw_size,
|
raw_size: *raw_size,
|
||||||
filter_mask: 0,
|
filter_mask: *filter_mask,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -915,7 +924,7 @@ pub fn build_chunked_data_from_precompressed(
|
|||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
let filter_mask = if pre.has_filters { Some(0u32) } else { None };
|
let filter_mask = pre.has_filters.then_some(written_chunks[0].filter_mask);
|
||||||
serialize_v4_single_chunk(
|
serialize_v4_single_chunk(
|
||||||
&chunk_dims_u32,
|
&chunk_dims_u32,
|
||||||
chunk_addr,
|
chunk_addr,
|
||||||
@@ -1035,7 +1044,7 @@ impl ChunkIndexPlan {
|
|||||||
Ok(Self::SingleChunk)
|
Ok(Self::SingleChunk)
|
||||||
} else {
|
} else {
|
||||||
let grid = ChunkGrid::fixed_array(shape, Some(max), chunk_dims)?;
|
let grid = ChunkGrid::fixed_array(shape, Some(max), chunk_dims)?;
|
||||||
Ok(Self::FixedArray(grid, nslots as usize))
|
Ok(Self::FixedArray(grid, saturating_usize(nslots)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
1 => Ok(Self::ExtensibleArray(ChunkGrid::extensible_array(
|
1 => Ok(Self::ExtensibleArray(ChunkGrid::extensible_array(
|
||||||
@@ -1105,11 +1114,12 @@ const BT2_CHUNK_FILTERED: u8 = 11;
|
|||||||
///
|
///
|
||||||
/// `records` are `(scaled coordinates, chunk)` in lexicographic order of the
|
/// `records` are `(scaled coordinates, chunk)` in lexicographic order of the
|
||||||
/// coordinates, which is the order the library's comparator
|
/// coordinates, which is the order the library's comparator
|
||||||
/// (`H5VM_vector_cmp_u`) keeps them in. The tree is a single leaf: the
|
/// (`H5VM_vector_cmp_u`) keeps them in. Up to 65 535 chunks go in a single
|
||||||
/// library's 2048-byte node when the records fit, otherwise a leaf node
|
/// leaf: the library's 2048-byte node when the records fit, otherwise a leaf
|
||||||
/// sized to hold them all (the root's record count is 16-bit, so at most
|
/// node sized to hold them all (the layout the writer has always used, kept
|
||||||
/// 65535 chunks). Returns the bytes and the node size the layout message
|
/// so those files do not change). More chunks get the library's 2048-byte
|
||||||
/// must record.
|
/// nodes with internal nodes above the leaves. Returns the bytes and the
|
||||||
|
/// node size the layout message must record.
|
||||||
fn build_btree_v2_chunk_index_at(
|
fn build_btree_v2_chunk_index_at(
|
||||||
rank: usize,
|
rank: usize,
|
||||||
records: &[(Vec<u64>, &WrittenChunk)],
|
records: &[(Vec<u64>, &WrittenChunk)],
|
||||||
@@ -1119,73 +1129,49 @@ fn build_btree_v2_chunk_index_at(
|
|||||||
base_address: u64,
|
base_address: u64,
|
||||||
) -> Result<(Vec<u8>, u32), FormatError> {
|
) -> Result<(Vec<u8>, u32), FormatError> {
|
||||||
let os = offset_size as usize;
|
let os = offset_size as usize;
|
||||||
let nrec = u16::try_from(records.len()).map_err(|_| {
|
|
||||||
FormatError::ChunkedReadError(
|
|
||||||
"more than 65535 chunks with more than one unlimited dimension: \
|
|
||||||
use larger chunks"
|
|
||||||
.into(),
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
let chunk_size_bytes = has_filters.then(|| {
|
let chunk_size_bytes = has_filters.then(|| {
|
||||||
let slots: Vec<Option<WrittenChunk>> =
|
let slots: Vec<Option<WrittenChunk>> =
|
||||||
records.iter().map(|(_, c)| Some((*c).clone())).collect();
|
records.iter().map(|(_, c)| Some((*c).clone())).collect();
|
||||||
filtered_chunk_size_len(&slots)
|
filtered_chunk_size_len(&slots)
|
||||||
});
|
});
|
||||||
let record_size = os + chunk_size_bytes.map_or(0, |n| n + 4) + 8 * rank;
|
let record_size = os + chunk_size_bytes.map_or(0, |n| n + 4) + 8 * rank;
|
||||||
|
let record_size_u16 = u16::try_from(record_size)
|
||||||
|
.map_err(|_| FormatError::Overflow("B-tree v2 record size".into()))?;
|
||||||
|
let node_size = if records.len() <= usize::from(u16::MAX) {
|
||||||
// Leaf: signature, version, type, records, checksum.
|
// Leaf: signature, version, type, records, checksum.
|
||||||
let leaf_len = 4 + 1 + 1 + records.len() * record_size + 4;
|
let leaf_len = 4 + 1 + 1 + records.len() * record_size + 4;
|
||||||
let node_size = u32::try_from(leaf_len)
|
u32::try_from(leaf_len)
|
||||||
.map_err(|_| FormatError::Overflow("B-tree v2 leaf size".into()))?
|
.map_err(|_| FormatError::Overflow("B-tree v2 leaf size".into()))?
|
||||||
.max(BT2_NODE_SIZE);
|
.max(BT2_NODE_SIZE)
|
||||||
|
} else {
|
||||||
|
BT2_NODE_SIZE
|
||||||
|
};
|
||||||
let tree_type = if has_filters {
|
let tree_type = if has_filters {
|
||||||
BT2_CHUNK_FILTERED
|
BT2_CHUNK_FILTERED
|
||||||
} else {
|
} else {
|
||||||
BT2_CHUNK_UNFILTERED
|
BT2_CHUNK_UNFILTERED
|
||||||
};
|
};
|
||||||
|
|
||||||
let hdr_len = 4 + 1 + 1 + 4 + 2 + 2 + 1 + 1 + os + 2 + length_size as usize + 4;
|
let mut flat = Vec::with_capacity(records.len() * record_size);
|
||||||
let leaf_address = base_address + hdr_len as u64;
|
|
||||||
|
|
||||||
let mut out = Vec::with_capacity(hdr_len + node_size as usize);
|
|
||||||
out.extend_from_slice(b"BTHD");
|
|
||||||
out.push(0); // version
|
|
||||||
out.push(tree_type);
|
|
||||||
out.extend_from_slice(&node_size.to_le_bytes());
|
|
||||||
out.extend_from_slice(&(record_size as u16).to_le_bytes());
|
|
||||||
out.extend_from_slice(&0u16.to_le_bytes()); // depth
|
|
||||||
out.push(BT2_SPLIT_PERCENT);
|
|
||||||
out.push(BT2_MERGE_PERCENT);
|
|
||||||
if records.is_empty() {
|
|
||||||
out.extend(core::iter::repeat_n(0xFF, os));
|
|
||||||
} else {
|
|
||||||
push_addr(&mut out, leaf_address, offset_size);
|
|
||||||
}
|
|
||||||
out.extend_from_slice(&nrec.to_le_bytes());
|
|
||||||
match length_size {
|
|
||||||
4 => out.extend_from_slice(&(records.len() as u32).to_le_bytes()),
|
|
||||||
_ => out.extend_from_slice(&(records.len() as u64).to_le_bytes()),
|
|
||||||
}
|
|
||||||
let sum = jenkins_lookup3(&out);
|
|
||||||
out.extend_from_slice(&sum.to_le_bytes());
|
|
||||||
debug_assert_eq!(out.len(), hdr_len);
|
|
||||||
if records.is_empty() {
|
|
||||||
return Ok((out, node_size));
|
|
||||||
}
|
|
||||||
|
|
||||||
let leaf_start = out.len();
|
|
||||||
out.extend_from_slice(b"BTLF");
|
|
||||||
out.push(0); // version
|
|
||||||
out.push(tree_type);
|
|
||||||
for (scaled, chunk) in records {
|
for (scaled, chunk) in records {
|
||||||
push_index_element(&mut out, Some(chunk), offset_size, chunk_size_bytes);
|
push_index_element(&mut flat, Some(chunk), offset_size, chunk_size_bytes);
|
||||||
for &c in scaled {
|
for &c in scaled {
|
||||||
out.extend_from_slice(&c.to_le_bytes());
|
flat.extend_from_slice(&c.to_le_bytes());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let sum = jenkins_lookup3(&out[leaf_start..]);
|
let out = build_btree_v2(
|
||||||
out.extend_from_slice(&sum.to_le_bytes());
|
BTreeV2Params {
|
||||||
// The library reads whole nodes; pad the leaf out to the node size.
|
tree_type,
|
||||||
out.resize(leaf_start + node_size as usize, 0);
|
node_size,
|
||||||
|
record_size: record_size_u16,
|
||||||
|
split_percent: BT2_SPLIT_PERCENT,
|
||||||
|
merge_percent: BT2_MERGE_PERCENT,
|
||||||
|
},
|
||||||
|
&flat,
|
||||||
|
base_address,
|
||||||
|
offset_size,
|
||||||
|
length_size,
|
||||||
|
)?;
|
||||||
Ok((out, node_size))
|
Ok((out, node_size))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1273,7 +1259,7 @@ pub fn write_selection_to_buffer(
|
|||||||
let rank = dims.len();
|
let rank = dims.len();
|
||||||
let mut ds_strides = vec![1usize; rank];
|
let mut ds_strides = vec![1usize; rank];
|
||||||
for i in (0..rank.saturating_sub(1)).rev() {
|
for i in (0..rank.saturating_sub(1)).rev() {
|
||||||
ds_strides[i] = ds_strides[i + 1] * dims[i + 1] as usize;
|
ds_strides[i] = ds_strides[i + 1] * saturating_usize(dims[i + 1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut src_offset = 0usize;
|
let mut src_offset = 0usize;
|
||||||
@@ -1323,7 +1309,7 @@ pub fn write_selection_to_buffer(
|
|||||||
buffer,
|
buffer,
|
||||||
new_data,
|
new_data,
|
||||||
src_offset,
|
src_offset,
|
||||||
current_ds_offset + coord as usize * ds_strides[d],
|
current_ds_offset + saturating_usize(coord) * ds_strides[d],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1350,14 +1336,14 @@ pub fn write_selection_to_buffer(
|
|||||||
let rank = dims.len();
|
let rank = dims.len();
|
||||||
let mut ds_strides = vec![1usize; rank];
|
let mut ds_strides = vec![1usize; rank];
|
||||||
for i in (0..rank.saturating_sub(1)).rev() {
|
for i in (0..rank.saturating_sub(1)).rev() {
|
||||||
ds_strides[i] = ds_strides[i + 1] * dims[i + 1] as usize;
|
ds_strides[i] = ds_strides[i + 1] * saturating_usize(dims[i + 1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (pi, pt) in pts.iter().enumerate() {
|
for (pi, pt) in pts.iter().enumerate() {
|
||||||
let flat: usize = pt
|
let flat: usize = pt
|
||||||
.iter()
|
.iter()
|
||||||
.zip(ds_strides.iter())
|
.zip(ds_strides.iter())
|
||||||
.map(|(&p, &s)| p as usize * s)
|
.map(|(&p, &s)| saturating_usize(p) * s)
|
||||||
.sum();
|
.sum();
|
||||||
let dst = flat * elem_size;
|
let dst = flat * elem_size;
|
||||||
let src = pi * elem_size;
|
let src = pi * elem_size;
|
||||||
@@ -1965,6 +1951,98 @@ mod tests {
|
|||||||
bytes_to_f64(&output)
|
bytes_to_f64(&output)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Every chunk index the writer builds records each chunk's real filter
|
||||||
|
/// mask: LZF output no smaller than the chunk is skipped (bit 1, behind
|
||||||
|
/// shuffle) and the chunk stored shuffled only; compressible chunks keep
|
||||||
|
/// mask 0. The data reads back through both kinds of chunk.
|
||||||
|
#[cfg(feature = "lzf")]
|
||||||
|
#[test]
|
||||||
|
fn skipped_lzf_chunks_are_masked_in_every_index() {
|
||||||
|
let c = 64usize;
|
||||||
|
// Chunks alternate: random bytes (LZF cannot shrink them), then 7s.
|
||||||
|
let mut state = 0x1234_5678_u64;
|
||||||
|
let data: Vec<f64> = (0..4 * c)
|
||||||
|
.map(|i| {
|
||||||
|
if (i / c).is_multiple_of(2) {
|
||||||
|
state ^= state << 13;
|
||||||
|
state ^= state >> 7;
|
||||||
|
state ^= state << 17;
|
||||||
|
f64::from_bits(state)
|
||||||
|
} else {
|
||||||
|
7.0
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let raw = f64_to_bytes(&data);
|
||||||
|
let options = ChunkOptions {
|
||||||
|
plugin: Some(PluginFilter::Lzf),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let c64 = c as u64;
|
||||||
|
#[allow(clippy::type_complexity)]
|
||||||
|
let cases: [(&[u64], &[u64], Option<&[u64]>, u8, &[u32]); 4] = [
|
||||||
|
(&[c64], &[c64], None, 1, &[2]),
|
||||||
|
(&[4 * c64], &[c64], None, 3, &[2, 0, 2, 0]),
|
||||||
|
(&[4 * c64], &[c64], Some(&[u64::MAX]), 4, &[2, 0, 2, 0]),
|
||||||
|
(
|
||||||
|
&[2, 2 * c64],
|
||||||
|
&[1, c64],
|
||||||
|
Some(&[u64::MAX, u64::MAX]),
|
||||||
|
5,
|
||||||
|
&[2, 0, 2, 0],
|
||||||
|
),
|
||||||
|
];
|
||||||
|
let base = 0x1000u64;
|
||||||
|
for (shape, chunks, maxshape, index_type, want_masks) in cases {
|
||||||
|
let n: u64 = shape.iter().product();
|
||||||
|
let raw = &raw[..n as usize * 8];
|
||||||
|
let result =
|
||||||
|
build_chunked_data_at_ext(raw, shape, chunks, 8, &options, base, maxshape).unwrap();
|
||||||
|
let mut file = vec![0u8; base as usize];
|
||||||
|
file.extend_from_slice(&result.data_bytes);
|
||||||
|
let layout = DataLayout::parse(&result.layout_message, 8, 8).unwrap();
|
||||||
|
assert!(
|
||||||
|
matches!(&layout, DataLayout::Chunked { chunk_index_type, .. }
|
||||||
|
if *chunk_index_type == Some(index_type)),
|
||||||
|
"{layout:?}"
|
||||||
|
);
|
||||||
|
let dataspace = Dataspace {
|
||||||
|
space_type: DataspaceType::Simple,
|
||||||
|
rank: shape.len() as u8,
|
||||||
|
dimensions: shape.to_vec(),
|
||||||
|
max_dimensions: maxshape.map(<[u64]>::to_vec),
|
||||||
|
};
|
||||||
|
let (mut infos, _) =
|
||||||
|
crate::chunked_read::list_chunks(&file, &layout, &dataspace, 8, 8, 8).unwrap();
|
||||||
|
infos.sort_by(|a, b| a.offsets.cmp(&b.offsets));
|
||||||
|
let masks: Vec<u32> = infos.iter().map(|i| i.filter_mask).collect();
|
||||||
|
assert_eq!(masks, want_masks, "index type {index_type}");
|
||||||
|
for info in &infos {
|
||||||
|
// Skipped chunks are stored at the chunk's size (shuffled).
|
||||||
|
assert_eq!(
|
||||||
|
info.chunk_size == (c * 8) as u32,
|
||||||
|
info.filter_mask != 0,
|
||||||
|
"{info:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let pipeline = crate::filter_pipeline::FilterPipeline::parse(
|
||||||
|
result.pipeline_message.as_ref().unwrap(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let out = read_chunked_data(
|
||||||
|
&file,
|
||||||
|
&layout,
|
||||||
|
&dataspace,
|
||||||
|
&make_f64_type(),
|
||||||
|
Some(&pipeline),
|
||||||
|
8,
|
||||||
|
8,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(out, raw, "index type {index_type}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn ea_roundtrip_1d_inline_only() {
|
fn ea_roundtrip_1d_inline_only() {
|
||||||
let values: Vec<f64> = (0..10).map(|i| i as f64).collect();
|
let values: Vec<f64> = (0..10).map(|i| i as f64).collect();
|
||||||
|
|||||||
@@ -6,7 +6,9 @@ use alloc::{format, string::String, vec::Vec};
|
|||||||
#[cfg(feature = "std")]
|
#[cfg(feature = "std")]
|
||||||
use std::string::String;
|
use std::string::String;
|
||||||
|
|
||||||
|
use crate::addr::to_usize;
|
||||||
use crate::error::FormatError;
|
use crate::error::FormatError;
|
||||||
|
use crate::storage::Storage;
|
||||||
|
|
||||||
/// A single VDS (Virtual Dataset) source mapping.
|
/// A single VDS (Virtual Dataset) source mapping.
|
||||||
///
|
///
|
||||||
@@ -207,7 +209,7 @@ pub fn parse_vds_mappings(
|
|||||||
"VDS mapping shares a name with a later entry".into(),
|
"VDS mapping shares a name with a later entry".into(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
Ok(idx as usize)
|
to_usize(idx)
|
||||||
};
|
};
|
||||||
|
|
||||||
let source_file = if flags & VDS_SOURCE_SAME_FILE != 0 {
|
let source_file = if flags & VDS_SOURCE_SAME_FILE != 0 {
|
||||||
@@ -309,6 +311,16 @@ impl DataLayout {
|
|||||||
&mut self,
|
&mut self,
|
||||||
file_data: &[u8],
|
file_data: &[u8],
|
||||||
length_size: u8,
|
length_size: u8,
|
||||||
|
) -> Result<(), FormatError> {
|
||||||
|
self.resolve_vds_mappings_in(file_data, length_size)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`Self::resolve_vds_mappings`] over any [`Storage`]: one read of the
|
||||||
|
/// global heap collection holding the mappings.
|
||||||
|
pub fn resolve_vds_mappings_in<S: Storage + ?Sized>(
|
||||||
|
&mut self,
|
||||||
|
file_data: &S,
|
||||||
|
length_size: u8,
|
||||||
) -> Result<(), FormatError> {
|
) -> Result<(), FormatError> {
|
||||||
if let DataLayout::Virtual {
|
if let DataLayout::Virtual {
|
||||||
global_heap_address,
|
global_heap_address,
|
||||||
@@ -318,11 +330,8 @@ impl DataLayout {
|
|||||||
} = self
|
} = self
|
||||||
&& let Some(addr) = *global_heap_address
|
&& let Some(addr) = *global_heap_address
|
||||||
{
|
{
|
||||||
let coll = crate::global_heap::GlobalHeapCollection::parse(
|
let coll =
|
||||||
file_data,
|
crate::global_heap::GlobalHeapCollection::parse_in(file_data, addr, length_size)?;
|
||||||
addr as usize,
|
|
||||||
length_size,
|
|
||||||
)?;
|
|
||||||
let obj = coll.get_object(*global_heap_index as u16).ok_or(
|
let obj = coll.get_object(*global_heap_index as u16).ok_or(
|
||||||
FormatError::GlobalHeapObjectNotFound {
|
FormatError::GlobalHeapObjectNotFound {
|
||||||
collection_address: addr,
|
collection_address: addr,
|
||||||
@@ -1305,4 +1314,44 @@ mod tests {
|
|||||||
let blob = [0x01u8, 0, 0, 0, 0, 0, 0, 0, 0];
|
let blob = [0x01u8, 0, 0, 0, 0, 0, 0, 0, 0];
|
||||||
assert!(parse_vds_mappings(&blob, 8).unwrap().is_empty());
|
assert!(parse_vds_mappings(&blob, 8).unwrap().is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A virtual dataset's mappings resolve identically through a
|
||||||
|
/// read_at-only CountingStorage, in two reads of the global heap.
|
||||||
|
#[test]
|
||||||
|
fn vds_mappings_through_storage_match_slice() {
|
||||||
|
use crate::message_type::MessageType;
|
||||||
|
use crate::object_header::ObjectHeader;
|
||||||
|
use crate::storage::CountingStorage;
|
||||||
|
let file: &[u8] = include_bytes!("../tests/fixtures/vds_same_file.h5");
|
||||||
|
let sb = crate::superblock::Superblock::parse(file, 0).unwrap();
|
||||||
|
let (os, ls) = (sb.offset_size, sb.length_size);
|
||||||
|
let storage = CountingStorage::new(file.to_vec());
|
||||||
|
let mut virtuals = 0;
|
||||||
|
for child in
|
||||||
|
crate::group_v2::resolve_group_children(file, &sb, sb.root_group_address).unwrap()
|
||||||
|
{
|
||||||
|
let h =
|
||||||
|
ObjectHeader::parse(file, child.object_header_address as usize, os, ls).unwrap();
|
||||||
|
let Some(msg) = h
|
||||||
|
.messages
|
||||||
|
.iter()
|
||||||
|
.find(|m| m.msg_type == MessageType::DataLayout)
|
||||||
|
else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let mut want = DataLayout::parse(&msg.data, os, ls).unwrap();
|
||||||
|
if !matches!(want, DataLayout::Virtual { .. }) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let mut got = want.clone();
|
||||||
|
want.resolve_vds_mappings(file, ls).unwrap();
|
||||||
|
storage.reset();
|
||||||
|
got.resolve_vds_mappings_in(&storage, ls).unwrap();
|
||||||
|
assert_eq!(format!("{got:?}"), format!("{want:?}"));
|
||||||
|
assert!(matches!(&got, DataLayout::Virtual { mappings, .. } if !mappings.is_empty()));
|
||||||
|
assert_eq!(storage.reads(), 2);
|
||||||
|
virtuals += 1;
|
||||||
|
}
|
||||||
|
assert!(virtuals >= 1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,25 @@
|
|||||||
//! Raw data reading and typed conversion for HDF5 datasets.
|
//! Raw data reading and typed conversion for HDF5 datasets.
|
||||||
|
|
||||||
#[cfg(not(feature = "std"))]
|
#[cfg(not(feature = "std"))]
|
||||||
use alloc::{collections::BTreeMap, format, string::String, vec, vec::Vec};
|
use alloc::{borrow::Cow, collections::BTreeMap, format, string::String, vec, vec::Vec};
|
||||||
|
#[cfg(feature = "std")]
|
||||||
|
use std::borrow::Cow;
|
||||||
|
|
||||||
#[cfg(feature = "std")]
|
#[cfg(feature = "std")]
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
|
use crate::addr::to_usize;
|
||||||
#[cfg(feature = "std")]
|
#[cfg(feature = "std")]
|
||||||
use crate::chunk_cache::ChunkCache;
|
use crate::chunk_cache::ChunkCache;
|
||||||
use crate::chunked_read::read_chunked_data;
|
use crate::chunked_read::read_chunked_data_in;
|
||||||
#[cfg(feature = "std")]
|
#[cfg(feature = "std")]
|
||||||
use crate::chunked_read::{read_chunked_data_cached, read_chunked_data_indexed};
|
use crate::chunked_read::{read_chunked_data_cached_in, read_chunked_data_indexed_in};
|
||||||
use crate::data_layout::DataLayout;
|
use crate::data_layout::DataLayout;
|
||||||
use crate::dataspace::Dataspace;
|
use crate::dataspace::Dataspace;
|
||||||
use crate::datatype::{Datatype, DatatypeByteOrder};
|
use crate::datatype::{Datatype, DatatypeByteOrder};
|
||||||
use crate::error::FormatError;
|
use crate::error::FormatError;
|
||||||
use crate::filter_pipeline::FilterPipeline;
|
use crate::filter_pipeline::FilterPipeline;
|
||||||
|
use crate::storage::{Storage, read_exact_at};
|
||||||
|
|
||||||
/// Checks that `[offset, offset + needed)` fits within `data`, guarding the
|
/// Checks that `[offset, offset + needed)` fits within `data`, guarding the
|
||||||
/// addition against `usize` overflow from a crafted near-`usize::MAX` offset.
|
/// addition against `usize` overflow from a crafted near-`usize::MAX` offset.
|
||||||
@@ -32,6 +36,80 @@ fn ensure_len(data: &[u8], offset: usize, needed: usize) -> Result<(), FormatErr
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The storage checks libhdf5 makes when it opens a dataset, before any
|
||||||
|
/// data is read (`H5D__contig_check`, `H5D__compact_init`), so a dataset
|
||||||
|
/// they refuse fails to open, as in libhdf5, instead of opening and
|
||||||
|
/// reporting a shape nothing can be read from:
|
||||||
|
///
|
||||||
|
/// - the element count times the element size must not overflow 64 bits
|
||||||
|
/// ("size of dataset's storage overflowed" — `cve-2024-32624`
|
||||||
|
/// `/Dset_OBJREF`, 2^62 references of 8 bytes);
|
||||||
|
/// - contiguous storage at a defined address must end within the file's
|
||||||
|
/// `file_len` bytes (the HDF5 data up to the end of file the superblock
|
||||||
|
/// records);
|
||||||
|
/// - compact data must be exactly the dataset's size.
|
||||||
|
///
|
||||||
|
/// Deliberately not refused, unlike libhdf5: an empty contiguous dataset at
|
||||||
|
/// a defined address (libhdf5's overflow test `addr + 0 <= addr` refuses
|
||||||
|
/// it), which clawhdf5 up to v2.7.0 wrote. Chunked and virtual layouts are
|
||||||
|
/// checked when their data is read.
|
||||||
|
pub fn check_dataset_storage(
|
||||||
|
layout: &DataLayout,
|
||||||
|
dataspace: &Dataspace,
|
||||||
|
datatype: &Datatype,
|
||||||
|
file_len: u64,
|
||||||
|
) -> Result<(), FormatError> {
|
||||||
|
if !matches!(
|
||||||
|
layout,
|
||||||
|
DataLayout::Contiguous { .. } | DataLayout::Compact { .. }
|
||||||
|
) {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
const OVERFLOWED: &str = "size of dataset's storage overflowed";
|
||||||
|
let n = dataspace
|
||||||
|
.checked_num_elements()
|
||||||
|
.map_err(|_| FormatError::InvalidDatasetStorage(OVERFLOWED))?;
|
||||||
|
let data_size = n
|
||||||
|
.checked_mul(u64::from(datatype.type_size()))
|
||||||
|
.ok_or(FormatError::InvalidDatasetStorage(OVERFLOWED))?;
|
||||||
|
match layout {
|
||||||
|
DataLayout::Contiguous {
|
||||||
|
address: Some(address),
|
||||||
|
..
|
||||||
|
} if address
|
||||||
|
.checked_add(data_size)
|
||||||
|
.is_none_or(|end| end > file_len) =>
|
||||||
|
{
|
||||||
|
Err(FormatError::InvalidDatasetStorage(
|
||||||
|
"invalid dataset size, likely file corruption",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
DataLayout::Compact { data } if data.len() as u64 != data_size => {
|
||||||
|
Err(FormatError::InvalidDatasetStorage(
|
||||||
|
"bad value from dataset header - size of compact dataset's data buffer \
|
||||||
|
doesn't match size of dataset data",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
_ => Ok(()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// How many bytes to read from a contiguous dataset's storage of
|
||||||
|
/// `storage_size` bytes (the layout message's size) holding `needed` bytes
|
||||||
|
/// of elements. libhdf5 reads the elements' bytes from the start of the
|
||||||
|
/// storage and ignores storage past them (`H5D__contig_check` checks only
|
||||||
|
/// that the elements fit in the file), so a larger storage reads; one too
|
||||||
|
/// small to hold the elements is an error.
|
||||||
|
pub fn contiguous_read_len(storage_size: u64, needed: usize) -> Result<usize, FormatError> {
|
||||||
|
if storage_size < needed as u64 {
|
||||||
|
return Err(FormatError::DataSizeMismatch {
|
||||||
|
expected: needed,
|
||||||
|
actual: usize::try_from(storage_size).unwrap_or(usize::MAX),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok(needed)
|
||||||
|
}
|
||||||
|
|
||||||
/// Zero-copy read of contiguous raw data, returning a borrowed slice.
|
/// Zero-copy read of contiguous raw data, returning a borrowed slice.
|
||||||
///
|
///
|
||||||
/// For contiguous layouts, returns a direct `&[u8]` slice into `file_data`.
|
/// For contiguous layouts, returns a direct `&[u8]` slice into `file_data`.
|
||||||
@@ -43,7 +121,7 @@ pub fn read_raw_data_zerocopy<'a>(
|
|||||||
dataspace: &Dataspace,
|
dataspace: &Dataspace,
|
||||||
datatype: &Datatype,
|
datatype: &Datatype,
|
||||||
) -> Result<Option<&'a [u8]>, FormatError> {
|
) -> Result<Option<&'a [u8]>, FormatError> {
|
||||||
let num_elements = dataspace.num_elements() as usize;
|
let num_elements = to_usize(dataspace.num_elements())?;
|
||||||
let elem_size = datatype.type_size() as usize;
|
let elem_size = datatype.type_size() as usize;
|
||||||
let expected_size = num_elements.checked_mul(elem_size).ok_or_else(|| {
|
let expected_size = num_elements.checked_mul(elem_size).ok_or_else(|| {
|
||||||
FormatError::Overflow(format!(
|
FormatError::Overflow(format!(
|
||||||
@@ -54,14 +132,8 @@ pub fn read_raw_data_zerocopy<'a>(
|
|||||||
match layout {
|
match layout {
|
||||||
DataLayout::Contiguous { address, size } => {
|
DataLayout::Contiguous { address, size } => {
|
||||||
let addr = address.ok_or(FormatError::NoDataAllocated)?;
|
let addr = address.ok_or(FormatError::NoDataAllocated)?;
|
||||||
let addr = addr as usize;
|
let addr = to_usize(addr)?;
|
||||||
let sz = *size as usize;
|
let sz = contiguous_read_len(*size, expected_size)?;
|
||||||
if sz != expected_size {
|
|
||||||
return Err(FormatError::DataSizeMismatch {
|
|
||||||
expected: expected_size,
|
|
||||||
actual: sz,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
ensure_len(file_data, addr, sz)?;
|
ensure_len(file_data, addr, sz)?;
|
||||||
Ok(Some(&file_data[addr..addr + sz]))
|
Ok(Some(&file_data[addr..addr + sz]))
|
||||||
}
|
}
|
||||||
@@ -80,7 +152,17 @@ pub fn read_raw_data(
|
|||||||
dataspace: &Dataspace,
|
dataspace: &Dataspace,
|
||||||
datatype: &Datatype,
|
datatype: &Datatype,
|
||||||
) -> Result<Vec<u8>, FormatError> {
|
) -> Result<Vec<u8>, FormatError> {
|
||||||
read_raw_data_full(file_data, layout, dataspace, datatype, None, 8, 8)
|
read_raw_data_in(file_data, layout, dataspace, datatype)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`read_raw_data`] over any [`Storage`].
|
||||||
|
pub fn read_raw_data_in<S: Storage + ?Sized>(
|
||||||
|
file_data: &S,
|
||||||
|
layout: &DataLayout,
|
||||||
|
dataspace: &Dataspace,
|
||||||
|
datatype: &Datatype,
|
||||||
|
) -> Result<Vec<u8>, FormatError> {
|
||||||
|
read_raw_data_full_in(file_data, layout, dataspace, datatype, None, 8, 8)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resolves a Virtual Dataset source **file name** (as stored in the mapping,
|
/// Resolves a Virtual Dataset source **file name** (as stored in the mapping,
|
||||||
@@ -102,6 +184,27 @@ pub fn read_raw_data_full(
|
|||||||
pipeline: Option<&FilterPipeline>,
|
pipeline: Option<&FilterPipeline>,
|
||||||
offset_size: u8,
|
offset_size: u8,
|
||||||
length_size: u8,
|
length_size: u8,
|
||||||
|
) -> Result<Vec<u8>, FormatError> {
|
||||||
|
read_raw_data_full_in(
|
||||||
|
file_data,
|
||||||
|
layout,
|
||||||
|
dataspace,
|
||||||
|
datatype,
|
||||||
|
pipeline,
|
||||||
|
offset_size,
|
||||||
|
length_size,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`read_raw_data_full`] over any [`Storage`].
|
||||||
|
pub fn read_raw_data_full_in<S: Storage + ?Sized>(
|
||||||
|
file_data: &S,
|
||||||
|
layout: &DataLayout,
|
||||||
|
dataspace: &Dataspace,
|
||||||
|
datatype: &Datatype,
|
||||||
|
pipeline: Option<&FilterPipeline>,
|
||||||
|
offset_size: u8,
|
||||||
|
length_size: u8,
|
||||||
) -> Result<Vec<u8>, FormatError> {
|
) -> Result<Vec<u8>, FormatError> {
|
||||||
read_raw_data_full_impl(
|
read_raw_data_full_impl(
|
||||||
file_data,
|
file_data,
|
||||||
@@ -127,6 +230,30 @@ pub fn read_raw_data_full_with_resolver(
|
|||||||
offset_size: u8,
|
offset_size: u8,
|
||||||
length_size: u8,
|
length_size: u8,
|
||||||
resolver: Option<&VdsSourceResolver>,
|
resolver: Option<&VdsSourceResolver>,
|
||||||
|
) -> Result<Vec<u8>, FormatError> {
|
||||||
|
read_raw_data_full_with_resolver_in(
|
||||||
|
file_data,
|
||||||
|
layout,
|
||||||
|
dataspace,
|
||||||
|
datatype,
|
||||||
|
pipeline,
|
||||||
|
offset_size,
|
||||||
|
length_size,
|
||||||
|
resolver,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`read_raw_data_full_with_resolver`] over any [`Storage`].
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
pub fn read_raw_data_full_with_resolver_in<S: Storage + ?Sized>(
|
||||||
|
file_data: &S,
|
||||||
|
layout: &DataLayout,
|
||||||
|
dataspace: &Dataspace,
|
||||||
|
datatype: &Datatype,
|
||||||
|
pipeline: Option<&FilterPipeline>,
|
||||||
|
offset_size: u8,
|
||||||
|
length_size: u8,
|
||||||
|
resolver: Option<&VdsSourceResolver>,
|
||||||
) -> Result<Vec<u8>, FormatError> {
|
) -> Result<Vec<u8>, FormatError> {
|
||||||
read_raw_data_full_impl(
|
read_raw_data_full_impl(
|
||||||
file_data,
|
file_data,
|
||||||
@@ -141,8 +268,8 @@ pub fn read_raw_data_full_with_resolver(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
fn read_raw_data_full_impl(
|
fn read_raw_data_full_impl<S: Storage + ?Sized>(
|
||||||
file_data: &[u8],
|
file_data: &S,
|
||||||
layout: &DataLayout,
|
layout: &DataLayout,
|
||||||
dataspace: &Dataspace,
|
dataspace: &Dataspace,
|
||||||
datatype: &Datatype,
|
datatype: &Datatype,
|
||||||
@@ -151,7 +278,7 @@ fn read_raw_data_full_impl(
|
|||||||
length_size: u8,
|
length_size: u8,
|
||||||
resolver: Option<&VdsSourceResolver>,
|
resolver: Option<&VdsSourceResolver>,
|
||||||
) -> Result<Vec<u8>, FormatError> {
|
) -> Result<Vec<u8>, FormatError> {
|
||||||
let num_elements = dataspace.num_elements() as usize;
|
let num_elements = to_usize(dataspace.num_elements())?;
|
||||||
let elem_size = datatype.type_size() as usize;
|
let elem_size = datatype.type_size() as usize;
|
||||||
let expected_size = num_elements.checked_mul(elem_size).ok_or_else(|| {
|
let expected_size = num_elements.checked_mul(elem_size).ok_or_else(|| {
|
||||||
FormatError::Overflow(format!(
|
FormatError::Overflow(format!(
|
||||||
@@ -171,20 +298,19 @@ fn read_raw_data_full_impl(
|
|||||||
}
|
}
|
||||||
DataLayout::Contiguous { address, size } => {
|
DataLayout::Contiguous { address, size } => {
|
||||||
let addr = address.ok_or(FormatError::NoDataAllocated)?;
|
let addr = address.ok_or(FormatError::NoDataAllocated)?;
|
||||||
let addr = addr as usize;
|
let addr = to_usize(addr)?;
|
||||||
let sz = *size as usize;
|
let sz = contiguous_read_len(*size, expected_size)?;
|
||||||
if sz != expected_size {
|
match read_exact_at(file_data, addr as u64, sz)? {
|
||||||
return Err(FormatError::DataSizeMismatch {
|
Cow::Borrowed(bytes) => {
|
||||||
expected: expected_size,
|
|
||||||
actual: sz,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
ensure_len(file_data, addr, sz)?;
|
|
||||||
let mut out = crate::bulk_alloc::vec_for_bulk(sz);
|
let mut out = crate::bulk_alloc::vec_for_bulk(sz);
|
||||||
out.extend_from_slice(&file_data[addr..addr + sz]);
|
out.extend_from_slice(bytes);
|
||||||
Ok(out)
|
Ok(out)
|
||||||
}
|
}
|
||||||
DataLayout::Chunked { .. } => read_chunked_data(
|
// Fetched for this read: already the caller's copy.
|
||||||
|
Cow::Owned(out) => Ok(out),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DataLayout::Chunked { .. } => read_chunked_data_in(
|
||||||
file_data,
|
file_data,
|
||||||
layout,
|
layout,
|
||||||
dataspace,
|
dataspace,
|
||||||
@@ -221,9 +347,34 @@ pub fn read_raw_data_cached(
|
|||||||
offset_size: u8,
|
offset_size: u8,
|
||||||
length_size: u8,
|
length_size: u8,
|
||||||
cache: &ChunkCache,
|
cache: &ChunkCache,
|
||||||
|
) -> Result<Vec<u8>, FormatError> {
|
||||||
|
read_raw_data_cached_in(
|
||||||
|
file_data,
|
||||||
|
layout,
|
||||||
|
dataspace,
|
||||||
|
datatype,
|
||||||
|
pipeline,
|
||||||
|
offset_size,
|
||||||
|
length_size,
|
||||||
|
cache,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`read_raw_data_cached`] over any [`Storage`].
|
||||||
|
#[cfg(feature = "std")]
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
pub fn read_raw_data_cached_in<S: Storage + ?Sized>(
|
||||||
|
file_data: &S,
|
||||||
|
layout: &DataLayout,
|
||||||
|
dataspace: &Dataspace,
|
||||||
|
datatype: &Datatype,
|
||||||
|
pipeline: Option<&FilterPipeline>,
|
||||||
|
offset_size: u8,
|
||||||
|
length_size: u8,
|
||||||
|
cache: &ChunkCache,
|
||||||
) -> Result<Vec<u8>, FormatError> {
|
) -> Result<Vec<u8>, FormatError> {
|
||||||
match layout {
|
match layout {
|
||||||
DataLayout::Chunked { .. } => read_chunked_data_cached(
|
DataLayout::Chunked { .. } => read_chunked_data_cached_in(
|
||||||
file_data,
|
file_data,
|
||||||
layout,
|
layout,
|
||||||
dataspace,
|
dataspace,
|
||||||
@@ -233,7 +384,7 @@ pub fn read_raw_data_cached(
|
|||||||
length_size,
|
length_size,
|
||||||
cache,
|
cache,
|
||||||
),
|
),
|
||||||
_ => read_raw_data_full(
|
_ => read_raw_data_full_in(
|
||||||
file_data,
|
file_data,
|
||||||
layout,
|
layout,
|
||||||
dataspace,
|
dataspace,
|
||||||
@@ -262,9 +413,34 @@ pub fn read_raw_data_indexed(
|
|||||||
offset_size: u8,
|
offset_size: u8,
|
||||||
length_size: u8,
|
length_size: u8,
|
||||||
cache: &ChunkCache,
|
cache: &ChunkCache,
|
||||||
|
) -> Result<Vec<u8>, FormatError> {
|
||||||
|
read_raw_data_indexed_in(
|
||||||
|
file_data,
|
||||||
|
layout,
|
||||||
|
dataspace,
|
||||||
|
datatype,
|
||||||
|
pipeline,
|
||||||
|
offset_size,
|
||||||
|
length_size,
|
||||||
|
cache,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`read_raw_data_indexed`] over any [`Storage`].
|
||||||
|
#[cfg(feature = "std")]
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
pub fn read_raw_data_indexed_in<S: Storage + ?Sized>(
|
||||||
|
file_data: &S,
|
||||||
|
layout: &DataLayout,
|
||||||
|
dataspace: &Dataspace,
|
||||||
|
datatype: &Datatype,
|
||||||
|
pipeline: Option<&FilterPipeline>,
|
||||||
|
offset_size: u8,
|
||||||
|
length_size: u8,
|
||||||
|
cache: &ChunkCache,
|
||||||
) -> Result<Vec<u8>, FormatError> {
|
) -> Result<Vec<u8>, FormatError> {
|
||||||
match layout {
|
match layout {
|
||||||
DataLayout::Chunked { .. } => read_chunked_data_indexed(
|
DataLayout::Chunked { .. } => read_chunked_data_indexed_in(
|
||||||
file_data,
|
file_data,
|
||||||
layout,
|
layout,
|
||||||
dataspace,
|
dataspace,
|
||||||
@@ -274,7 +450,7 @@ pub fn read_raw_data_indexed(
|
|||||||
length_size,
|
length_size,
|
||||||
cache,
|
cache,
|
||||||
),
|
),
|
||||||
_ => read_raw_data_full(
|
_ => read_raw_data_full_in(
|
||||||
file_data,
|
file_data,
|
||||||
layout,
|
layout,
|
||||||
dataspace,
|
dataspace,
|
||||||
@@ -303,6 +479,30 @@ pub fn read_raw_data_selection(
|
|||||||
offset_size: u8,
|
offset_size: u8,
|
||||||
length_size: u8,
|
length_size: u8,
|
||||||
selection: &crate::selection::Selection,
|
selection: &crate::selection::Selection,
|
||||||
|
) -> Result<Vec<u8>, FormatError> {
|
||||||
|
read_raw_data_selection_in(
|
||||||
|
file_data,
|
||||||
|
layout,
|
||||||
|
dataspace,
|
||||||
|
datatype,
|
||||||
|
pipeline,
|
||||||
|
offset_size,
|
||||||
|
length_size,
|
||||||
|
selection,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`read_raw_data_selection`] over any [`Storage`].
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
pub fn read_raw_data_selection_in<S: Storage + ?Sized>(
|
||||||
|
file_data: &S,
|
||||||
|
layout: &DataLayout,
|
||||||
|
dataspace: &Dataspace,
|
||||||
|
datatype: &Datatype,
|
||||||
|
pipeline: Option<&FilterPipeline>,
|
||||||
|
offset_size: u8,
|
||||||
|
length_size: u8,
|
||||||
|
selection: &crate::selection::Selection,
|
||||||
) -> Result<Vec<u8>, FormatError> {
|
) -> Result<Vec<u8>, FormatError> {
|
||||||
use crate::selection::Selection;
|
use crate::selection::Selection;
|
||||||
|
|
||||||
@@ -312,7 +512,7 @@ pub fn read_raw_data_selection(
|
|||||||
// Read only what the selection's bounding box touches when that is
|
// Read only what the selection's bounding box touches when that is
|
||||||
// possible; everything below is the decode-everything-then-pick path,
|
// possible; everything below is the decode-everything-then-pick path,
|
||||||
// kept for the cases `partial_read` declines.
|
// kept for the cases `partial_read` declines.
|
||||||
if let Some(selected) = crate::partial_read::read_selection(
|
if let Some(selected) = crate::partial_read::read_selection_in(
|
||||||
file_data,
|
file_data,
|
||||||
layout,
|
layout,
|
||||||
dataspace,
|
dataspace,
|
||||||
@@ -327,7 +527,7 @@ pub fn read_raw_data_selection(
|
|||||||
|
|
||||||
match selection {
|
match selection {
|
||||||
Selection::All => {
|
Selection::All => {
|
||||||
return read_raw_data_full(
|
return read_raw_data_full_in(
|
||||||
file_data,
|
file_data,
|
||||||
layout,
|
layout,
|
||||||
dataspace,
|
dataspace,
|
||||||
@@ -347,7 +547,7 @@ pub fn read_raw_data_selection(
|
|||||||
match layout {
|
match layout {
|
||||||
DataLayout::Compact { .. } | DataLayout::Contiguous { .. } => {
|
DataLayout::Compact { .. } | DataLayout::Contiguous { .. } => {
|
||||||
// Read all data, then extract the selection
|
// Read all data, then extract the selection
|
||||||
let full_data = read_raw_data_full(
|
let full_data = read_raw_data_full_in(
|
||||||
file_data,
|
file_data,
|
||||||
layout,
|
layout,
|
||||||
dataspace,
|
dataspace,
|
||||||
@@ -371,7 +571,7 @@ pub fn read_raw_data_selection(
|
|||||||
// implicit-index generator, which then indexed past the rank and
|
// implicit-index generator, which then indexed past the rank and
|
||||||
// panicked — only to decode the full dataset anyway.
|
// panicked — only to decode the full dataset anyway.
|
||||||
crate::chunked_read::chunk_geometry(chunk_dimensions, *version, dataspace, elem_size)?;
|
crate::chunked_read::chunk_geometry(chunk_dimensions, *version, dataspace, elem_size)?;
|
||||||
let full_data = read_raw_data_full(
|
let full_data = read_raw_data_full_in(
|
||||||
file_data,
|
file_data,
|
||||||
layout,
|
layout,
|
||||||
dataspace,
|
dataspace,
|
||||||
@@ -384,7 +584,7 @@ pub fn read_raw_data_selection(
|
|||||||
}
|
}
|
||||||
DataLayout::Virtual { .. } => {
|
DataLayout::Virtual { .. } => {
|
||||||
// Assemble the full virtual dataset, then apply the read selection.
|
// Assemble the full virtual dataset, then apply the read selection.
|
||||||
let full_data = read_raw_data_full(
|
let full_data = read_raw_data_full_in(
|
||||||
file_data,
|
file_data,
|
||||||
layout,
|
layout,
|
||||||
dataspace,
|
dataspace,
|
||||||
@@ -408,8 +608,8 @@ pub fn read_raw_data_selection(
|
|||||||
/// would report differently from the stored dataspace (unlimited mappings).
|
/// would report differently from the stored dataspace (unlimited mappings).
|
||||||
/// Use [`crate::vds::read_virtual_dataset`] to read those.
|
/// Use [`crate::vds::read_virtual_dataset`] to read those.
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
fn read_virtual_data(
|
fn read_virtual_data<S: Storage + ?Sized>(
|
||||||
file_data: &[u8],
|
file_data: &S,
|
||||||
layout: &DataLayout,
|
layout: &DataLayout,
|
||||||
dataspace: &Dataspace,
|
dataspace: &Dataspace,
|
||||||
datatype: &Datatype,
|
datatype: &Datatype,
|
||||||
@@ -420,7 +620,7 @@ fn read_virtual_data(
|
|||||||
let wrapped =
|
let wrapped =
|
||||||
resolver.map(|r| move |name: &str| -> Result<Option<Vec<u8>>, FormatError> { Ok(r(name)) });
|
resolver.map(|r| move |name: &str| -> Result<Option<Vec<u8>>, FormatError> { Ok(r(name)) });
|
||||||
let wrapped_ref = wrapped.as_ref().map(|w| w as &crate::vds::VdsFileResolver);
|
let wrapped_ref = wrapped.as_ref().map(|w| w as &crate::vds::VdsFileResolver);
|
||||||
let v = crate::vds::read_virtual_dataset(
|
let v = crate::vds::read_virtual_dataset_in(
|
||||||
file_data,
|
file_data,
|
||||||
layout,
|
layout,
|
||||||
dataspace,
|
dataspace,
|
||||||
@@ -520,7 +720,7 @@ pub fn extract_selection_from_buffer(
|
|||||||
let rank = dims.len();
|
let rank = dims.len();
|
||||||
let mut ds_strides = vec![1usize; rank];
|
let mut ds_strides = vec![1usize; rank];
|
||||||
for i in (0..rank.saturating_sub(1)).rev() {
|
for i in (0..rank.saturating_sub(1)).rev() {
|
||||||
ds_strides[i] = ds_strides[i + 1] * dims[i + 1] as usize;
|
ds_strides[i] = ds_strides[i + 1] * to_usize(dims[i + 1])?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut output = Vec::with_capacity(pts.len() * elem_size);
|
let mut output = Vec::with_capacity(pts.len() * elem_size);
|
||||||
@@ -528,8 +728,8 @@ pub fn extract_selection_from_buffer(
|
|||||||
let flat: usize = pt
|
let flat: usize = pt
|
||||||
.iter()
|
.iter()
|
||||||
.zip(ds_strides.iter())
|
.zip(ds_strides.iter())
|
||||||
.map(|(&p, &s)| p as usize * s)
|
.map(|(&p, &s)| Ok(to_usize(p)? * s))
|
||||||
.sum();
|
.sum::<Result<usize, FormatError>>()?;
|
||||||
let src = flat * elem_size;
|
let src = flat * elem_size;
|
||||||
if src + elem_size <= full_data.len() {
|
if src + elem_size <= full_data.len() {
|
||||||
output.extend_from_slice(&full_data[src..src + elem_size]);
|
output.extend_from_slice(&full_data[src..src + elem_size]);
|
||||||
@@ -756,6 +956,220 @@ pub fn read_selection_native<T: NativeElement>(
|
|||||||
crate::gather::gather::<T>(raw, dims, elem_size, selection).map(Some)
|
crate::gather::gather::<T>(raw, dims, elem_size, selection).map(Some)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// [`read_selection_native`] of a contiguous dataset in any [`Storage`],
|
||||||
|
/// reading only the selected elements' runs (adjacent ones merged, one
|
||||||
|
/// [`Storage::read_ranges`] call) instead of the whole dataset.
|
||||||
|
///
|
||||||
|
/// `Ok(None)` wherever the in-memory fast path does not apply and the
|
||||||
|
/// caller converts through the byte readers instead: `datatype` is not
|
||||||
|
/// `T`'s native representation, the layout is not contiguous, or the
|
||||||
|
/// dataset's bytes cannot be located in the file (no address, storage too
|
||||||
|
/// small, past the end of file: the cases [`read_raw_data_zerocopy`]
|
||||||
|
/// fails). Otherwise the result and errors are [`read_selection_native`]'s
|
||||||
|
/// over those bytes.
|
||||||
|
pub fn read_selection_native_in<T: NativeElement, S: Storage + ?Sized>(
|
||||||
|
file_data: &S,
|
||||||
|
layout: &DataLayout,
|
||||||
|
dataspace: &Dataspace,
|
||||||
|
datatype: &Datatype,
|
||||||
|
selection: &crate::selection::Selection,
|
||||||
|
) -> Result<Option<Vec<T>>, FormatError> {
|
||||||
|
if !T::is_native(datatype) {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
let DataLayout::Contiguous {
|
||||||
|
address: Some(address),
|
||||||
|
size,
|
||||||
|
} = layout
|
||||||
|
else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
// Where the dataset's bytes are, as `read_raw_data_zerocopy` finds them.
|
||||||
|
let located = to_usize(dataspace.num_elements())
|
||||||
|
.ok()
|
||||||
|
.and_then(|n| n.checked_mul(datatype.type_size() as usize))
|
||||||
|
.filter(|&len| contiguous_read_len(*size, len).is_ok())
|
||||||
|
.filter(|&len| {
|
||||||
|
address
|
||||||
|
.checked_add(len as u64)
|
||||||
|
.is_some_and(|end| end <= file_data.len())
|
||||||
|
});
|
||||||
|
let Some(len) = located else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
if let Some(all) = file_data.as_contiguous() {
|
||||||
|
let start = to_usize(*address)?;
|
||||||
|
return read_selection_native(
|
||||||
|
&all[start..start + len],
|
||||||
|
&dataspace.dimensions,
|
||||||
|
datatype,
|
||||||
|
selection,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let dims = &dataspace.dimensions;
|
||||||
|
let elem_size = core::mem::size_of::<T>();
|
||||||
|
let total = dims
|
||||||
|
.iter()
|
||||||
|
.try_fold(1u64, |acc, &d| acc.checked_mul(d))
|
||||||
|
.ok_or_else(|| FormatError::Overflow("dataset shape overflows".into()))?;
|
||||||
|
let expected = crate::chunked_read::checked_byte_len(total, elem_size)?;
|
||||||
|
if len != expected {
|
||||||
|
return Err(FormatError::DataSizeMismatch {
|
||||||
|
expected,
|
||||||
|
actual: len,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let bytes = if let crate::selection::Selection::All = selection {
|
||||||
|
read_exact_at(file_data, *address, len)?.into_owned()
|
||||||
|
} else {
|
||||||
|
crate::partial_read::validate(selection, dims)?;
|
||||||
|
crate::gather::gather_storage(file_data, *address, len, dims, elem_size, selection)?
|
||||||
|
};
|
||||||
|
Ok(Some(native_to_vec(&bytes, bytes.len() / elem_size)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The bytes of a slice of [`NativeElement`]s.
|
||||||
|
#[cfg(feature = "std")]
|
||||||
|
fn bytes_of_mut<T: NativeElement>(values: &mut [T]) -> &mut [u8] {
|
||||||
|
// SAFETY: `T: NativeElement` has no padding and every bit pattern is a
|
||||||
|
// valid value, so its storage may be viewed, and written, as bytes; the
|
||||||
|
// byte slice covers exactly the values' storage and borrows it
|
||||||
|
// exclusively for its lifetime.
|
||||||
|
unsafe {
|
||||||
|
core::slice::from_raw_parts_mut(
|
||||||
|
values.as_mut_ptr().cast::<u8>(),
|
||||||
|
core::mem::size_of_val(values),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `count` zeroed values of `T`, from zeroed pages where the allocator can
|
||||||
|
/// (see [`crate::chunked_read::alloc_output`]) and backed by huge pages when
|
||||||
|
/// large. A size taken from the file surfaces as an error, not an abort.
|
||||||
|
#[cfg(feature = "std")]
|
||||||
|
fn alloc_zeroed_values<T: NativeElement>(count: usize) -> Result<Vec<T>, FormatError> {
|
||||||
|
if count == 0 || core::mem::size_of::<T>() == 0 {
|
||||||
|
return Ok(Vec::new());
|
||||||
|
}
|
||||||
|
let failed = || {
|
||||||
|
FormatError::Overflow(format!(
|
||||||
|
"cannot allocate {count} values of {} bytes for dataset output",
|
||||||
|
core::mem::size_of::<T>()
|
||||||
|
))
|
||||||
|
};
|
||||||
|
let layout = core::alloc::Layout::array::<T>(count).map_err(|_| failed())?;
|
||||||
|
// SAFETY: `layout` has non-zero size (count > 0, T not zero-sized).
|
||||||
|
let ptr = unsafe { std::alloc::alloc_zeroed(layout) };
|
||||||
|
if ptr.is_null() {
|
||||||
|
return Err(failed());
|
||||||
|
}
|
||||||
|
crate::bulk_alloc::advise_huge_pages(ptr, layout.size());
|
||||||
|
// SAFETY: allocated by the global allocator with the layout of
|
||||||
|
// `[T; count]`, which is what `Vec<T>` with capacity `count` frees; all
|
||||||
|
// bytes are zero, a valid `T` (`NativeElement`: any bit pattern is).
|
||||||
|
Ok(unsafe { Vec::from_raw_parts(ptr.cast::<T>(), count, count) })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read a whole chunked dataset that stores `T` natively
|
||||||
|
/// ([`NativeElement::is_native`]) straight into a `Vec<T>`: each chunk is
|
||||||
|
/// decoded and copied to its place in the typed output, with no byte buffer
|
||||||
|
/// to convert from afterwards. Unallocated chunks read as the dataset's fill
|
||||||
|
/// value, as [`crate::fill_value::read_full_with_fill`] makes them.
|
||||||
|
///
|
||||||
|
/// `Ok(None)` when this does not apply — the datatype is not `T`'s native
|
||||||
|
/// representation (another type, another byte order: the caller converts
|
||||||
|
/// through the byte readers and the `read_as_*` functions), the layout is
|
||||||
|
/// not chunked, no storage is allocated, or the data lives in external
|
||||||
|
/// files. `cache` is the file's chunk cache, used as
|
||||||
|
/// [`crate::chunked_read::read_chunked_data_cached`] uses it.
|
||||||
|
#[cfg(feature = "std")]
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
pub fn read_chunked_native<T: NativeElement>(
|
||||||
|
messages: &[crate::object_header::HeaderMessage],
|
||||||
|
file_data: &[u8],
|
||||||
|
layout: &DataLayout,
|
||||||
|
dataspace: &Dataspace,
|
||||||
|
datatype: &Datatype,
|
||||||
|
pipeline: Option<&FilterPipeline>,
|
||||||
|
offset_size: u8,
|
||||||
|
length_size: u8,
|
||||||
|
cache: Option<&ChunkCache>,
|
||||||
|
) -> Result<Option<Vec<T>>, FormatError> {
|
||||||
|
read_chunked_native_in(
|
||||||
|
messages,
|
||||||
|
file_data,
|
||||||
|
layout,
|
||||||
|
dataspace,
|
||||||
|
datatype,
|
||||||
|
pipeline,
|
||||||
|
offset_size,
|
||||||
|
length_size,
|
||||||
|
cache,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`read_chunked_native`] over any [`Storage`].
|
||||||
|
#[cfg(feature = "std")]
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
pub fn read_chunked_native_in<T: NativeElement, S: Storage + ?Sized>(
|
||||||
|
messages: &[crate::object_header::HeaderMessage],
|
||||||
|
file_data: &S,
|
||||||
|
layout: &DataLayout,
|
||||||
|
dataspace: &Dataspace,
|
||||||
|
datatype: &Datatype,
|
||||||
|
pipeline: Option<&FilterPipeline>,
|
||||||
|
offset_size: u8,
|
||||||
|
length_size: u8,
|
||||||
|
cache: Option<&ChunkCache>,
|
||||||
|
) -> Result<Option<Vec<T>>, FormatError> {
|
||||||
|
use crate::fill_value;
|
||||||
|
use crate::message_type::MessageType;
|
||||||
|
|
||||||
|
if !T::is_native(datatype)
|
||||||
|
|| !matches!(layout, DataLayout::Chunked { .. })
|
||||||
|
|| !fill_value::has_storage(layout)
|
||||||
|
|| messages
|
||||||
|
.iter()
|
||||||
|
.any(|m| m.msg_type == MessageType::ExternalDataFiles)
|
||||||
|
{
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
let size = core::mem::size_of::<T>();
|
||||||
|
let mut values = crate::chunked_read::read_chunked_full(
|
||||||
|
file_data,
|
||||||
|
layout,
|
||||||
|
dataspace,
|
||||||
|
datatype,
|
||||||
|
pipeline,
|
||||||
|
offset_size,
|
||||||
|
length_size,
|
||||||
|
cache,
|
||||||
|
|total_bytes| {
|
||||||
|
if !total_bytes.is_multiple_of(size) {
|
||||||
|
return Err(FormatError::DataSizeMismatch {
|
||||||
|
expected: total_bytes.next_multiple_of(size),
|
||||||
|
actual: total_bytes,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
alloc_zeroed_values::<T>(total_bytes / size)
|
||||||
|
},
|
||||||
|
|values| bytes_of_mut(values),
|
||||||
|
)?;
|
||||||
|
let fill =
|
||||||
|
fill_value::dataset_fill_value_from_storage(file_data, messages, offset_size, length_size)?;
|
||||||
|
fill_value::apply_to_unallocated_chunks_in(
|
||||||
|
bytes_of_mut(&mut values),
|
||||||
|
file_data,
|
||||||
|
layout,
|
||||||
|
dataspace,
|
||||||
|
size,
|
||||||
|
fill.as_deref(),
|
||||||
|
offset_size,
|
||||||
|
length_size,
|
||||||
|
)?;
|
||||||
|
Ok(Some(values))
|
||||||
|
}
|
||||||
|
|
||||||
/// Convert raw bytes to `f64` values.
|
/// Convert raw bytes to `f64` values.
|
||||||
pub fn read_as_f64(raw: &[u8], datatype: &Datatype) -> Result<Vec<f64>, FormatError> {
|
pub fn read_as_f64(raw: &[u8], datatype: &Datatype) -> Result<Vec<f64>, FormatError> {
|
||||||
// Array datatypes read as a flat sequence of their base elements, and
|
// Array datatypes read as a flat sequence of their base elements, and
|
||||||
@@ -1165,7 +1579,7 @@ pub fn read_compound_fields(
|
|||||||
let mut fields = Vec::with_capacity(members.len());
|
let mut fields = Vec::with_capacity(members.len());
|
||||||
for m in members {
|
for m in members {
|
||||||
let field_size = m.datatype.type_size() as usize;
|
let field_size = m.datatype.type_size() as usize;
|
||||||
let offset = m.byte_offset as usize;
|
let offset = to_usize(m.byte_offset)?;
|
||||||
if offset
|
if offset
|
||||||
.checked_add(field_size)
|
.checked_add(field_size)
|
||||||
.is_none_or(|end| end > elem_size)
|
.is_none_or(|end| end > elem_size)
|
||||||
@@ -2632,6 +3046,71 @@ mod tests {
|
|||||||
assert_eq!(result.unwrap(), &[1.5f32, 2.5, 3.5]);
|
assert_eq!(result.unwrap(), &[1.5f32, 2.5, 3.5]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// libhdf5 reads a contiguous dataset's elements from the start of its
|
||||||
|
/// storage and ignores storage past them (cve-2024-32623's scalar
|
||||||
|
/// `/Dset1` has 240 bytes of storage for one 4-byte element). Storage too
|
||||||
|
/// small for the elements is still an error.
|
||||||
|
#[test]
|
||||||
|
fn contiguous_storage_larger_than_the_elements_reads() {
|
||||||
|
let dt = make_f64_le_type();
|
||||||
|
let ds = make_simple_dataspace(&[2]);
|
||||||
|
let mut file_data = vec![0u8; 64];
|
||||||
|
file_data[..8].copy_from_slice(&1.5f64.to_le_bytes());
|
||||||
|
file_data[8..16].copy_from_slice(&2.5f64.to_le_bytes());
|
||||||
|
file_data[16..24].copy_from_slice(&9.0f64.to_le_bytes());
|
||||||
|
let layout = DataLayout::Contiguous {
|
||||||
|
address: Some(0),
|
||||||
|
size: 40,
|
||||||
|
};
|
||||||
|
let raw = read_raw_data(&file_data, &layout, &ds, &dt).unwrap();
|
||||||
|
assert_eq!(raw, file_data[..16]);
|
||||||
|
let zc = read_raw_data_zerocopy(&file_data, &layout, &ds, &dt).unwrap();
|
||||||
|
assert_eq!(zc, Some(&file_data[..16]));
|
||||||
|
let small = DataLayout::Contiguous {
|
||||||
|
address: Some(0),
|
||||||
|
size: 8,
|
||||||
|
};
|
||||||
|
assert!(matches!(
|
||||||
|
read_raw_data(&file_data, &small, &ds, &dt),
|
||||||
|
Err(FormatError::DataSizeMismatch { .. })
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `H5D__contig_check` / `H5D__compact_init`, run when a dataset opens.
|
||||||
|
#[test]
|
||||||
|
fn dataset_storage_checks_at_open() {
|
||||||
|
let dt = make_f64_le_type();
|
||||||
|
let contiguous = |address| DataLayout::Contiguous { address, size: 0 };
|
||||||
|
// cve-2024-32624 `/Dset_OBJREF`: 2^62 + 2 elements of 8 bytes.
|
||||||
|
let huge = make_simple_dataspace(&[(1 << 62) + 2]);
|
||||||
|
assert_eq!(
|
||||||
|
check_dataset_storage(&contiguous(None), &huge, &dt, 1 << 20),
|
||||||
|
Err(FormatError::InvalidDatasetStorage(
|
||||||
|
"size of dataset's storage overflowed"
|
||||||
|
))
|
||||||
|
);
|
||||||
|
let ds = make_simple_dataspace(&[4]);
|
||||||
|
assert!(check_dataset_storage(&contiguous(Some(100)), &ds, &dt, 132).is_ok());
|
||||||
|
assert!(matches!(
|
||||||
|
check_dataset_storage(&contiguous(Some(100)), &ds, &dt, 131),
|
||||||
|
Err(FormatError::InvalidDatasetStorage(_))
|
||||||
|
));
|
||||||
|
assert!(matches!(
|
||||||
|
check_dataset_storage(&contiguous(Some(u64::MAX - 8)), &ds, &dt, u64::MAX),
|
||||||
|
Err(FormatError::InvalidDatasetStorage(_))
|
||||||
|
));
|
||||||
|
// Not allocated, and (unlike libhdf5) empty at a defined address.
|
||||||
|
assert!(check_dataset_storage(&contiguous(None), &ds, &dt, 0).is_ok());
|
||||||
|
let empty = make_simple_dataspace(&[0]);
|
||||||
|
assert!(check_dataset_storage(&contiguous(Some(64)), &empty, &dt, 64).is_ok());
|
||||||
|
let compact = |n: usize| DataLayout::Compact { data: vec![0; n] };
|
||||||
|
assert!(check_dataset_storage(&compact(32), &ds, &dt, 0).is_ok());
|
||||||
|
assert!(matches!(
|
||||||
|
check_dataset_storage(&compact(24), &ds, &dt, 0),
|
||||||
|
Err(FormatError::InvalidDatasetStorage(_))
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn zerocopy_size_mismatch() {
|
fn zerocopy_size_mismatch() {
|
||||||
let dt = make_f64_le_type();
|
let dt = make_f64_le_type();
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ use alloc::vec::Vec;
|
|||||||
|
|
||||||
use crate::error::FormatError;
|
use crate::error::FormatError;
|
||||||
|
|
||||||
|
/// Most dimensions a dataspace can have (`H5S_MAX_RANK`).
|
||||||
|
pub const MAX_RANK: u8 = 32;
|
||||||
|
|
||||||
/// Type of dataspace.
|
/// Type of dataspace.
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub enum DataspaceType {
|
pub enum DataspaceType {
|
||||||
@@ -67,6 +70,12 @@ impl Dataspace {
|
|||||||
let version = data[0];
|
let version = data[0];
|
||||||
let rank = data[1];
|
let rank = data[1];
|
||||||
let flags = data[2];
|
let flags = data[2];
|
||||||
|
// H5O__sdspace_decode's checks.
|
||||||
|
if rank > MAX_RANK {
|
||||||
|
return Err(FormatError::InvalidDataspace(
|
||||||
|
"simple dataspace dimensionality is too large",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
let (space_type, header_size) = match version {
|
let (space_type, header_size) = match version {
|
||||||
1 => {
|
1 => {
|
||||||
@@ -88,6 +97,11 @@ impl Dataspace {
|
|||||||
2 => DataspaceType::Null,
|
2 => DataspaceType::Null,
|
||||||
_ => return Err(FormatError::InvalidDataspaceType(type_byte)),
|
_ => return Err(FormatError::InvalidDataspaceType(type_byte)),
|
||||||
};
|
};
|
||||||
|
if st != DataspaceType::Simple && rank > 0 {
|
||||||
|
return Err(FormatError::InvalidDataspace(
|
||||||
|
"invalid rank for scalar or NULL dataspace",
|
||||||
|
));
|
||||||
|
}
|
||||||
(st, 4usize)
|
(st, 4usize)
|
||||||
}
|
}
|
||||||
_ => return Err(FormatError::InvalidDataspaceVersion(version)),
|
_ => return Err(FormatError::InvalidDataspaceVersion(version)),
|
||||||
@@ -107,8 +121,13 @@ impl Dataspace {
|
|||||||
// Read max dimensions if flags bit 0 is set
|
// Read max dimensions if flags bit 0 is set
|
||||||
let max_dimensions = if flags & 0x01 != 0 {
|
let max_dimensions = if flags & 0x01 != 0 {
|
||||||
let mut max_dims = Vec::with_capacity(rank as usize);
|
let mut max_dims = Vec::with_capacity(rank as usize);
|
||||||
for _ in 0..rank {
|
for &dim in &dimensions {
|
||||||
let val = read_length(data, pos, length_size)?;
|
let val = read_length(data, pos, length_size)?;
|
||||||
|
if dim > val {
|
||||||
|
return Err(FormatError::InvalidDataspace(
|
||||||
|
"dataspace dimension size is greater than its maximum size",
|
||||||
|
));
|
||||||
|
}
|
||||||
max_dims.push(val);
|
max_dims.push(val);
|
||||||
pos += ls;
|
pos += ls;
|
||||||
}
|
}
|
||||||
@@ -176,7 +195,6 @@ impl Dataspace {
|
|||||||
match self.space_type {
|
match self.space_type {
|
||||||
DataspaceType::Null => Ok(0),
|
DataspaceType::Null => Ok(0),
|
||||||
DataspaceType::Scalar => Ok(1),
|
DataspaceType::Scalar => Ok(1),
|
||||||
DataspaceType::Simple if self.dimensions.is_empty() => Ok(0),
|
|
||||||
DataspaceType::Simple => self
|
DataspaceType::Simple => self
|
||||||
.dimensions
|
.dimensions
|
||||||
.iter()
|
.iter()
|
||||||
@@ -195,18 +213,14 @@ impl Dataspace {
|
|||||||
match self.space_type {
|
match self.space_type {
|
||||||
DataspaceType::Null => 0,
|
DataspaceType::Null => 0,
|
||||||
DataspaceType::Scalar => 1,
|
DataspaceType::Scalar => 1,
|
||||||
DataspaceType::Simple => {
|
// A simple dataspace of rank 0 holds one element, as in libhdf5
|
||||||
if self.dimensions.is_empty() {
|
// (the product of no dimensions). Saturate rather than wrap: a
|
||||||
0
|
// wrapped product could under-size a buffer. Size-critical
|
||||||
} else {
|
// callers use `checked_num_elements`.
|
||||||
// Saturate rather than wrap: a wrapped product could
|
DataspaceType::Simple => self
|
||||||
// under-size a buffer. Size-critical callers use
|
.dimensions
|
||||||
// `checked_num_elements`.
|
|
||||||
self.dimensions
|
|
||||||
.iter()
|
.iter()
|
||||||
.fold(1u64, |acc, &d| acc.saturating_mul(d))
|
.fold(1u64, |acc, &d| acc.saturating_mul(d)),
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -352,4 +366,44 @@ mod tests {
|
|||||||
let ds = Dataspace::parse(&data, 8).unwrap();
|
let ds = Dataspace::parse(&data, 8).unwrap();
|
||||||
assert_eq!(ds.max_dimensions, Some(vec![10]));
|
assert_eq!(ds.max_dimensions, Some(vec![10]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A simple dataspace of rank 0 (cve-2020-18494's `/dset1`) holds one
|
||||||
|
/// element in libhdf5, which h5py reads as shape `()`. It was 0.
|
||||||
|
#[test]
|
||||||
|
fn simple_rank_zero_holds_one_element() {
|
||||||
|
let data = build_v2_dataspace(0, 0, 1, &[], None);
|
||||||
|
let ds = Dataspace::parse(&data, 8).unwrap();
|
||||||
|
assert_eq!(ds.space_type, DataspaceType::Simple);
|
||||||
|
assert_eq!(ds.num_elements(), 1);
|
||||||
|
assert_eq!(ds.checked_num_elements().unwrap(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `H5O__sdspace_decode`'s checks.
|
||||||
|
#[test]
|
||||||
|
fn refuses_what_libhdf5_refuses() {
|
||||||
|
let too_many = build_v2_dataspace(33, 0, 1, &[1; 33], None);
|
||||||
|
assert!(matches!(
|
||||||
|
Dataspace::parse(&too_many, 8),
|
||||||
|
Err(FormatError::InvalidDataspace(_))
|
||||||
|
));
|
||||||
|
let scalar_with_rank = build_v2_dataspace(1, 0, 0, &[4], None);
|
||||||
|
assert!(matches!(
|
||||||
|
Dataspace::parse(&scalar_with_rank, 8),
|
||||||
|
Err(FormatError::InvalidDataspace(_))
|
||||||
|
));
|
||||||
|
let null_with_rank = build_v2_dataspace(1, 0, 2, &[4], None);
|
||||||
|
assert!(matches!(
|
||||||
|
Dataspace::parse(&null_with_rank, 8),
|
||||||
|
Err(FormatError::InvalidDataspace(_))
|
||||||
|
));
|
||||||
|
let over_max = build_v1_dataspace(2, 0x01, &[5, 20], Some(&[10, 10]));
|
||||||
|
assert!(matches!(
|
||||||
|
Dataspace::parse(&over_max, 8),
|
||||||
|
Err(FormatError::InvalidDataspace(_))
|
||||||
|
));
|
||||||
|
// 32 dimensions, and a size equal to the maximum or unlimited, are fine.
|
||||||
|
assert!(Dataspace::parse(&build_v2_dataspace(32, 0, 1, &[1; 32], None), 8).is_ok());
|
||||||
|
let at_max = build_v1_dataspace(2, 0x01, &[10, 20], Some(&[10, u64::MAX]));
|
||||||
|
assert!(Dataspace::parse(&at_max, 8).is_ok());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
#[cfg(not(feature = "std"))]
|
#[cfg(not(feature = "std"))]
|
||||||
extern crate alloc;
|
extern crate alloc;
|
||||||
|
|
||||||
|
use crate::addr::saturating_usize;
|
||||||
#[cfg(not(feature = "std"))]
|
#[cfg(not(feature = "std"))]
|
||||||
use alloc::{vec, vec::Vec};
|
use alloc::{vec, vec::Vec};
|
||||||
|
|
||||||
@@ -247,7 +248,7 @@ pub fn build_extensible_array_at(
|
|||||||
|
|
||||||
// Header (EAHD). The six statistics are, in order: super blocks, their
|
// Header (EAHD). The six statistics are, in order: super blocks, their
|
||||||
// bytes, data blocks, their bytes, max index set, elements realised.
|
// bytes, data blocks, their bytes, max index set, elements realised.
|
||||||
let mut out = Vec::with_capacity((cursor - ea_base_address) as usize);
|
let mut out = Vec::with_capacity(saturating_usize(cursor - ea_base_address));
|
||||||
out.extend_from_slice(b"EAHD");
|
out.extend_from_slice(b"EAHD");
|
||||||
out.push(0); // version
|
out.push(0); // version
|
||||||
out.push(client_id);
|
out.push(client_id);
|
||||||
|
|||||||
@@ -12,7 +12,11 @@ use std::string::String;
|
|||||||
use core::fmt;
|
use core::fmt;
|
||||||
|
|
||||||
/// Errors that can occur when parsing HDF5 binary format structures.
|
/// Errors that can occur when parsing HDF5 binary format structures.
|
||||||
|
///
|
||||||
|
/// Non-exhaustive: new failure modes (new storage backends, new file
|
||||||
|
/// features) add variants, so a `match` needs a wildcard arm.
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
#[non_exhaustive]
|
||||||
pub enum FormatError {
|
pub enum FormatError {
|
||||||
/// The HDF5 magic signature was not found at any valid offset.
|
/// The HDF5 magic signature was not found at any valid offset.
|
||||||
SignatureNotFound,
|
SignatureNotFound,
|
||||||
@@ -223,6 +227,33 @@ pub enum FormatError {
|
|||||||
/// The file's actual length in bytes.
|
/// The file's actual length in bytes.
|
||||||
actual_len: u64,
|
actual_len: u64,
|
||||||
},
|
},
|
||||||
|
/// A link libhdf5 refuses to list: a symbol-table entry with an empty
|
||||||
|
/// name ("invalid link name"). Listing the group fails, as in libhdf5.
|
||||||
|
InvalidLinkName,
|
||||||
|
/// A dataspace message libhdf5 refuses to decode (the reason is
|
||||||
|
/// libhdf5's own error text): more than 32 dimensions, a rank on a
|
||||||
|
/// scalar or null dataspace, a dimension larger than its maximum.
|
||||||
|
InvalidDataspace(&'static str),
|
||||||
|
/// A dataset whose storage libhdf5 refuses when it opens the dataset
|
||||||
|
/// (the reason is libhdf5's own error text): an element count times
|
||||||
|
/// element size that overflows, contiguous storage past the end of the
|
||||||
|
/// file, compact data of the wrong size.
|
||||||
|
InvalidDatasetStorage(&'static str),
|
||||||
|
/// A superblock extension message libhdf5 refuses to decode when it
|
||||||
|
/// opens the file (the reason is libhdf5's own error text): a File Space
|
||||||
|
/// Info message that runs off its end or has a bad page size, a metadata
|
||||||
|
/// cache image outside the file, …
|
||||||
|
InvalidSuperblockExtension(&'static str),
|
||||||
|
/// A metadata cache image block libhdf5 refuses to load (the reason is
|
||||||
|
/// libhdf5's own error text).
|
||||||
|
InvalidCacheImage(&'static str),
|
||||||
|
/// The [`Storage`](crate::storage::Storage) backend failed to serve a
|
||||||
|
/// read (an I/O or network error, or a short read inside the file).
|
||||||
|
Storage(String),
|
||||||
|
/// The operation still needs the whole file as one slice and the
|
||||||
|
/// [`Storage`](crate::storage::Storage) backend has no contiguous view
|
||||||
|
/// (`as_contiguous()` is `None`); the text names the operation.
|
||||||
|
ContiguousStorageRequired(&'static str),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for FormatError {
|
impl fmt::Display for FormatError {
|
||||||
@@ -494,6 +525,31 @@ impl fmt::Display for FormatError {
|
|||||||
but the file is {actual_len} bytes"
|
but the file is {actual_len} bytes"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
FormatError::InvalidLinkName => {
|
||||||
|
write!(f, "invalid link name: a group entry has an empty name")
|
||||||
|
}
|
||||||
|
FormatError::InvalidDataspace(why) => {
|
||||||
|
write!(f, "invalid dataspace: {why}")
|
||||||
|
}
|
||||||
|
FormatError::InvalidDatasetStorage(why) => {
|
||||||
|
write!(f, "invalid dataset storage: {why}")
|
||||||
|
}
|
||||||
|
FormatError::InvalidSuperblockExtension(why) => {
|
||||||
|
write!(f, "invalid superblock extension: {why}")
|
||||||
|
}
|
||||||
|
FormatError::InvalidCacheImage(why) => {
|
||||||
|
write!(f, "invalid metadata cache image: {why}")
|
||||||
|
}
|
||||||
|
FormatError::Storage(why) => {
|
||||||
|
write!(f, "storage read failed: {why}")
|
||||||
|
}
|
||||||
|
FormatError::ContiguousStorageRequired(what) => {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"{what} needs the whole file in memory, which this storage backend does \
|
||||||
|
not provide"
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,19 +9,23 @@ extern crate alloc;
|
|||||||
#[cfg(not(feature = "std"))]
|
#[cfg(not(feature = "std"))]
|
||||||
use alloc::{format, vec, vec::Vec};
|
use alloc::{format, vec, vec::Vec};
|
||||||
|
|
||||||
|
use crate::addr::to_usize;
|
||||||
use crate::chunk_grid::ChunkGrid;
|
use crate::chunk_grid::ChunkGrid;
|
||||||
use crate::chunked_read::ChunkInfo;
|
use crate::chunked_read::ChunkInfo;
|
||||||
use crate::error::FormatError;
|
use crate::error::FormatError;
|
||||||
|
use crate::storage::{PAGED_BLOCK_ONE_READ_MAX, Storage, Window, read_exact_at};
|
||||||
|
|
||||||
/// Verify the Jenkins lookup3 checksum stored immediately after
|
/// Verify the Jenkins lookup3 checksum stored immediately after
|
||||||
/// `data[start..end]`, as every Extensible Array structure carries one.
|
/// `data[start..end]`, as every Extensible Array structure carries one. `w`
|
||||||
|
/// is a window of the file and `start`/`end` are relative to it.
|
||||||
///
|
///
|
||||||
/// A corrupt chunk index yields addresses pointing at the wrong bytes, so a
|
/// A corrupt chunk index yields addresses pointing at the wrong bytes, so a
|
||||||
/// mismatch is an error: otherwise the damage surfaces as plausible data read
|
/// mismatch is an error: otherwise the damage surfaces as plausible data read
|
||||||
/// from the wrong chunk.
|
/// from the wrong chunk.
|
||||||
#[cfg(feature = "checksum")]
|
#[cfg(feature = "checksum")]
|
||||||
fn verify_checksum(data: &[u8], start: usize, end: usize) -> Result<(), FormatError> {
|
fn verify_checksum(w: &Window<'_>, start: usize, end: usize) -> Result<(), FormatError> {
|
||||||
ensure_len(data, end, 4)?;
|
w.ensure(end, 4)?;
|
||||||
|
let data: &[u8] = &w.bytes;
|
||||||
let stored = u32::from_le_bytes([data[end], data[end + 1], data[end + 2], data[end + 3]]);
|
let stored = u32::from_le_bytes([data[end], data[end + 1], data[end + 2], data[end + 3]]);
|
||||||
let computed = crate::checksum::jenkins_lookup3(&data[start..end]);
|
let computed = crate::checksum::jenkins_lookup3(&data[start..end]);
|
||||||
if computed != stored {
|
if computed != stored {
|
||||||
@@ -34,7 +38,7 @@ fn verify_checksum(data: &[u8], start: usize, end: usize) -> Result<(), FormatEr
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(feature = "checksum"))]
|
#[cfg(not(feature = "checksum"))]
|
||||||
fn verify_checksum(_data: &[u8], _start: usize, _end: usize) -> Result<(), FormatError> {
|
fn verify_checksum(_w: &Window<'_>, _start: usize, _end: usize) -> Result<(), FormatError> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,19 +84,6 @@ fn read_offset(data: &[u8], pos: usize, size: u8) -> Result<u64, FormatError> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ensure_len(data: &[u8], offset: usize, needed: usize) -> Result<(), FormatError> {
|
|
||||||
if offset
|
|
||||||
.checked_add(needed)
|
|
||||||
.is_none_or(|end| end > data.len())
|
|
||||||
{
|
|
||||||
return Err(FormatError::UnexpectedEof {
|
|
||||||
expected: offset.saturating_add(needed),
|
|
||||||
available: data.len(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_undefined_addr(addr: u64, offset_size: u8) -> bool {
|
fn is_undefined_addr(addr: u64, offset_size: u8) -> bool {
|
||||||
match offset_size {
|
match offset_size {
|
||||||
2 => addr == 0xFFFF,
|
2 => addr == 0xFFFF,
|
||||||
@@ -130,6 +121,16 @@ impl ExtensibleArrayHeader {
|
|||||||
offset: usize,
|
offset: usize,
|
||||||
offset_size: u8,
|
offset_size: u8,
|
||||||
length_size: u8,
|
length_size: u8,
|
||||||
|
) -> Result<Self, FormatError> {
|
||||||
|
Self::parse_in(file_data, offset as u64, offset_size, length_size)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`Self::parse`] over any [`Storage`]: one read of the header.
|
||||||
|
pub fn parse_in<S: Storage + ?Sized>(
|
||||||
|
file: &S,
|
||||||
|
offset: u64,
|
||||||
|
offset_size: u8,
|
||||||
|
length_size: u8,
|
||||||
) -> Result<Self, FormatError> {
|
) -> Result<Self, FormatError> {
|
||||||
// EAHD: signature(4) + version(1) + client_id(1) + element_size(1) +
|
// EAHD: signature(4) + version(1) + client_id(1) + element_size(1) +
|
||||||
// max_nelmts_bits(1) + idx_blk_elmts(1) + min_dblk_nelmts(1) +
|
// max_nelmts_bits(1) + idx_blk_elmts(1) + min_dblk_nelmts(1) +
|
||||||
@@ -137,9 +138,10 @@ impl ExtensibleArrayHeader {
|
|||||||
// 6 stats fields (each length_size) + index_block_address(offset_size) + checksum(4)
|
// 6 stats fields (each length_size) + index_block_address(offset_size) + checksum(4)
|
||||||
let min_size =
|
let min_size =
|
||||||
4 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 6 * length_size as usize + offset_size as usize + 4;
|
4 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 6 * length_size as usize + offset_size as usize + 4;
|
||||||
ensure_len(file_data, offset, min_size)?;
|
let w = Window::read(file, offset, min_size)?;
|
||||||
|
w.ensure(0, min_size)?;
|
||||||
|
|
||||||
let d = &file_data[offset..];
|
let d: &[u8] = &w.bytes;
|
||||||
if &d[0..4] != b"EAHD" {
|
if &d[0..4] != b"EAHD" {
|
||||||
return Err(FormatError::ChunkedReadError(
|
return Err(FormatError::ChunkedReadError(
|
||||||
"invalid Extensible Array header signature".into(),
|
"invalid Extensible Array header signature".into(),
|
||||||
@@ -172,7 +174,7 @@ impl ExtensibleArrayHeader {
|
|||||||
pos += ls; // skip max_idx_set (6th stats field)
|
pos += ls; // skip max_idx_set (6th stats field)
|
||||||
let index_block_address = read_offset(d, pos, offset_size)?;
|
let index_block_address = read_offset(d, pos, offset_size)?;
|
||||||
pos += offset_size as usize;
|
pos += offset_size as usize;
|
||||||
verify_checksum(file_data, offset, offset + pos)?;
|
verify_checksum(&w, 0, pos)?;
|
||||||
|
|
||||||
Ok(ExtensibleArrayHeader {
|
Ok(ExtensibleArrayHeader {
|
||||||
client_id,
|
client_id,
|
||||||
@@ -193,11 +195,11 @@ impl ExtensibleArrayHeader {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Read a single element from the extensible array element data.
|
/// Read a single element at offset `pos` of the window `w`.
|
||||||
/// Returns (chunk_info, bytes_consumed) or None if unallocated.
|
/// Returns (chunk_info, bytes_consumed) or None if unallocated.
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
fn read_element(
|
fn read_element(
|
||||||
data: &[u8],
|
w: &Window<'_>,
|
||||||
pos: usize,
|
pos: usize,
|
||||||
client_id: u8,
|
client_id: u8,
|
||||||
element_size: u8,
|
element_size: u8,
|
||||||
@@ -207,15 +209,11 @@ fn read_element(
|
|||||||
grid: &ChunkGrid,
|
grid: &ChunkGrid,
|
||||||
) -> Result<(Option<ChunkInfo>, usize), FormatError> {
|
) -> Result<(Option<ChunkInfo>, usize), FormatError> {
|
||||||
let os = offset_size as usize;
|
let os = offset_size as usize;
|
||||||
|
let data: &[u8] = &w.bytes;
|
||||||
|
|
||||||
if client_id == 0 {
|
if client_id == 0 {
|
||||||
// Non-filtered: just address
|
// Non-filtered: just address
|
||||||
if pos + os > data.len() {
|
w.ensure(pos, os)?;
|
||||||
return Err(FormatError::UnexpectedEof {
|
|
||||||
expected: pos + os,
|
|
||||||
available: data.len(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if is_undefined(data, pos, offset_size) {
|
if is_undefined(data, pos, offset_size) {
|
||||||
return Ok((None, os));
|
return Ok((None, os));
|
||||||
}
|
}
|
||||||
@@ -243,15 +241,7 @@ fn read_element(
|
|||||||
}
|
}
|
||||||
let chunk_size_bytes = es - os - 4;
|
let chunk_size_bytes = es - os - 4;
|
||||||
let elem_total = os + chunk_size_bytes + 4;
|
let elem_total = os + chunk_size_bytes + 4;
|
||||||
if pos
|
w.ensure(pos, elem_total)?;
|
||||||
.checked_add(elem_total)
|
|
||||||
.is_none_or(|end| end > data.len())
|
|
||||||
{
|
|
||||||
return Err(FormatError::UnexpectedEof {
|
|
||||||
expected: pos.saturating_add(elem_total),
|
|
||||||
available: data.len(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if is_undefined(data, pos, offset_size) {
|
if is_undefined(data, pos, offset_size) {
|
||||||
return Ok((None, elem_total));
|
return Ok((None, elem_total));
|
||||||
}
|
}
|
||||||
@@ -315,9 +305,9 @@ fn page_nelmts(header: &ExtensibleArrayHeader) -> Option<usize> {
|
|||||||
/// paged. The bitmap lives in the super block, not here — a paged data block
|
/// paged. The bitmap lives in the super block, not here — a paged data block
|
||||||
/// stores only its prefix, then one slot per page.
|
/// stores only its prefix, then one slot per page.
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
fn read_data_block_elements(
|
fn read_data_block_elements<S: Storage + ?Sized>(
|
||||||
file_data: &[u8],
|
file: &S,
|
||||||
db_offset: usize,
|
db_offset: u64,
|
||||||
nelmts: usize,
|
nelmts: usize,
|
||||||
header: &ExtensibleArrayHeader,
|
header: &ExtensibleArrayHeader,
|
||||||
offset_size: u8,
|
offset_size: u8,
|
||||||
@@ -330,21 +320,28 @@ fn read_data_block_elements(
|
|||||||
// EADB: signature(4) + version(1) + client_id(1) + header_address(offset_size)
|
// EADB: signature(4) + version(1) + client_id(1) + header_address(offset_size)
|
||||||
// + block offset(arr_off_size)
|
// + block offset(arr_off_size)
|
||||||
let db_header_size = 4 + 1 + 1 + offset_size as usize + arr_off_size(header);
|
let db_header_size = 4 + 1 + 1 + offset_size as usize + arr_off_size(header);
|
||||||
ensure_len(file_data, db_offset, db_header_size)?;
|
let prefix = read_exact_at(file, db_offset, db_header_size)?;
|
||||||
|
|
||||||
if &file_data[db_offset..db_offset + 4] != b"EADB" {
|
if &prefix[0..4] != b"EADB" {
|
||||||
return Err(FormatError::ChunkedReadError(
|
return Err(FormatError::ChunkedReadError(
|
||||||
"invalid Extensible Array data block signature".into(),
|
"invalid Extensible Array data block signature".into(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut pos = db_offset + db_header_size;
|
// Positions below are relative to the data block.
|
||||||
|
let mut pos = db_header_size;
|
||||||
let page = page_nelmts(header).ok_or_else(|| {
|
let page = page_nelmts(header).ok_or_else(|| {
|
||||||
FormatError::Overflow("Extensible Array page element count overflows usize".into())
|
FormatError::Overflow("Extensible Array page element count overflows usize".into())
|
||||||
})?;
|
})?;
|
||||||
|
let elem_bytes = if header.client_id == 0 {
|
||||||
|
offset_size as usize
|
||||||
|
} else {
|
||||||
|
header.element_size as usize
|
||||||
|
};
|
||||||
|
|
||||||
let mut chunks = Vec::new();
|
let mut chunks = Vec::new();
|
||||||
let read_run = |from: usize,
|
let read_run = |w: &Window<'_>,
|
||||||
|
from: usize,
|
||||||
count: usize,
|
count: usize,
|
||||||
first_index: usize,
|
first_index: usize,
|
||||||
chunks: &mut Vec<ChunkInfo>|
|
chunks: &mut Vec<ChunkInfo>|
|
||||||
@@ -352,7 +349,7 @@ fn read_data_block_elements(
|
|||||||
let mut p = from;
|
let mut p = from;
|
||||||
for i in 0..count {
|
for i in 0..count {
|
||||||
let (info, consumed) = read_element(
|
let (info, consumed) = read_element(
|
||||||
file_data,
|
w,
|
||||||
p,
|
p,
|
||||||
header.client_id,
|
header.client_id,
|
||||||
header.element_size,
|
header.element_size,
|
||||||
@@ -370,18 +367,19 @@ fn read_data_block_elements(
|
|||||||
};
|
};
|
||||||
|
|
||||||
if nelmts <= page {
|
if nelmts <= page {
|
||||||
// Prefix and elements are covered by one checksum.
|
// Prefix and elements are covered by one checksum. One window holds
|
||||||
let elem_bytes = if header.client_id == 0 {
|
// all of it (or ends at the end of the file), so its bounds checks
|
||||||
offset_size as usize
|
// are the whole-file ones.
|
||||||
} else {
|
|
||||||
header.element_size as usize
|
|
||||||
};
|
|
||||||
let end = nelmts
|
let end = nelmts
|
||||||
.checked_mul(elem_bytes)
|
.checked_mul(elem_bytes)
|
||||||
.and_then(|b| pos.checked_add(b))
|
.and_then(|b| pos.checked_add(b))
|
||||||
.ok_or_else(|| FormatError::Overflow("Extensible Array data block span".into()))?;
|
.ok_or_else(|| FormatError::Overflow("Extensible Array data block span".into()))?;
|
||||||
verify_checksum(file_data, db_offset, end)?;
|
// The checksum's bounds check comes first: make it before reading.
|
||||||
read_run(pos, nelmts, start_index, &mut chunks)?;
|
#[cfg(feature = "checksum")]
|
||||||
|
Window::check_extent(file, db_offset, end, 4)?;
|
||||||
|
let w = Window::read(file, db_offset, end.saturating_add(4))?;
|
||||||
|
verify_checksum(&w, 0, end)?;
|
||||||
|
read_run(&w, pos, nelmts, start_index, &mut chunks)?;
|
||||||
return Ok(chunks);
|
return Ok(chunks);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -389,18 +387,32 @@ fn read_data_block_elements(
|
|||||||
// each holding `page` elements followed by a checksum. Pages whose bit is
|
// each holding `page` elements followed by a checksum. Pages whose bit is
|
||||||
// clear were never written; their slot still occupies the file, so stride
|
// clear were never written; their slot still occupies the file, so stride
|
||||||
// over it rather than reading zeros as addresses.
|
// over it rather than reading zeros as addresses.
|
||||||
verify_checksum(file_data, db_offset, pos)?;
|
let npages = nelmts.div_ceil(page);
|
||||||
pos += 4;
|
// The whole data block in one window when it is small: every position
|
||||||
let elem_bytes = if header.client_id == 0 {
|
// checked below lies inside it (or past the end of the file). A larger
|
||||||
offset_size as usize
|
// block is read as its prefix, then each page in use on its own.
|
||||||
|
let block_len = pos
|
||||||
|
.saturating_add(4)
|
||||||
|
.saturating_add(npages.saturating_mul(page.saturating_mul(elem_bytes).saturating_add(4)));
|
||||||
|
let whole = if block_len <= PAGED_BLOCK_ONE_READ_MAX {
|
||||||
|
Some(Window::read(file, db_offset, block_len)?)
|
||||||
} else {
|
} else {
|
||||||
header.element_size as usize
|
None
|
||||||
};
|
};
|
||||||
|
let head_w;
|
||||||
|
let head = match &whole {
|
||||||
|
Some(w) => w,
|
||||||
|
None => {
|
||||||
|
head_w = Window::read(file, db_offset, pos + 4)?;
|
||||||
|
&head_w
|
||||||
|
}
|
||||||
|
};
|
||||||
|
verify_checksum(head, 0, pos)?;
|
||||||
|
pos += 4;
|
||||||
let page_stride = page
|
let page_stride = page
|
||||||
.checked_mul(elem_bytes)
|
.checked_mul(elem_bytes)
|
||||||
.and_then(|b| b.checked_add(4))
|
.and_then(|b| b.checked_add(4))
|
||||||
.ok_or_else(|| FormatError::Overflow("Extensible Array page stride".into()))?;
|
.ok_or_else(|| FormatError::Overflow("Extensible Array page stride".into()))?;
|
||||||
let npages = nelmts.div_ceil(page);
|
|
||||||
for p in 0..npages {
|
for p in 0..npages {
|
||||||
// One bit per page across the whole super block, packed contiguously
|
// One bit per page across the whole super block, packed contiguously
|
||||||
// and MSB-first within each byte, as H5VM_bit_get reads it.
|
// and MSB-first within each byte, as H5VM_bit_get reads it.
|
||||||
@@ -410,10 +422,20 @@ fn read_data_block_elements(
|
|||||||
.is_some_and(|byte| byte & (0x80 >> (bit % 8)) != 0);
|
.is_some_and(|byte| byte & (0x80 >> (bit % 8)) != 0);
|
||||||
if initialised {
|
if initialised {
|
||||||
let count = core::cmp::min(page, nelmts - p * page);
|
let count = core::cmp::min(page, nelmts - p * page);
|
||||||
|
// `w` holds the page from `base` on (positions below are
|
||||||
|
// relative to it, and `pos` to the data block).
|
||||||
|
let page_w;
|
||||||
|
let (w, base) = match &whole {
|
||||||
|
Some(w) => (w, 0),
|
||||||
|
None => {
|
||||||
|
page_w = Window::read(file, db_offset.saturating_add(pos as u64), page_stride)?;
|
||||||
|
(&page_w, pos)
|
||||||
|
}
|
||||||
|
};
|
||||||
// Each page carries its own checksum, over a full page's worth of
|
// Each page carries its own checksum, over a full page's worth of
|
||||||
// slots even when the last one holds fewer live elements.
|
// slots even when the last one holds fewer live elements.
|
||||||
verify_checksum(file_data, pos, pos + page * elem_bytes)?;
|
verify_checksum(w, pos - base, pos - base + page * elem_bytes)?;
|
||||||
read_run(pos, count, start_index + p * page, &mut chunks)?;
|
read_run(w, pos - base, count, start_index + p * page, &mut chunks)?;
|
||||||
}
|
}
|
||||||
pos = pos
|
pos = pos
|
||||||
.checked_add(page_stride)
|
.checked_add(page_stride)
|
||||||
@@ -435,6 +457,32 @@ pub fn read_extensible_array_chunks(
|
|||||||
chunk_dimensions: &[u32],
|
chunk_dimensions: &[u32],
|
||||||
element_size: u32,
|
element_size: u32,
|
||||||
offset_size: u8,
|
offset_size: u8,
|
||||||
|
length_size: u8,
|
||||||
|
) -> Result<Vec<ChunkInfo>, FormatError> {
|
||||||
|
read_extensible_array_chunks_in(
|
||||||
|
&file_data,
|
||||||
|
header,
|
||||||
|
dataset_dims,
|
||||||
|
max_dims,
|
||||||
|
chunk_dimensions,
|
||||||
|
element_size,
|
||||||
|
offset_size,
|
||||||
|
length_size,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`read_extensible_array_chunks`] over any [`Storage`]: one read of the
|
||||||
|
/// index block's prefix, one of the whole index block, and the same for
|
||||||
|
/// every super block and data block it references.
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
pub fn read_extensible_array_chunks_in<S: Storage + ?Sized>(
|
||||||
|
file: &S,
|
||||||
|
header: &ExtensibleArrayHeader,
|
||||||
|
dataset_dims: &[u64],
|
||||||
|
max_dims: Option<&[u64]>,
|
||||||
|
chunk_dimensions: &[u32],
|
||||||
|
element_size: u32,
|
||||||
|
offset_size: u8,
|
||||||
_length_size: u8,
|
_length_size: u8,
|
||||||
) -> Result<Vec<ChunkInfo>, FormatError> {
|
) -> Result<Vec<ChunkInfo>, FormatError> {
|
||||||
let os = offset_size as usize;
|
let os = offset_size as usize;
|
||||||
@@ -451,19 +499,20 @@ pub fn read_extensible_array_chunks(
|
|||||||
// Parse index block (EAIB): signature(4) + version(1) + client_id(1)
|
// Parse index block (EAIB): signature(4) + version(1) + client_id(1)
|
||||||
// + header address(offset_size), then the inline elements, then the
|
// + header address(offset_size), then the inline elements, then the
|
||||||
// direct data block addresses, then the super block addresses.
|
// direct data block addresses, then the super block addresses.
|
||||||
let ib_offset = header.index_block_address as usize;
|
// Positions below are relative to the index block.
|
||||||
|
let ib_offset = header.index_block_address;
|
||||||
let ib_header_size = 4 + 1 + 1 + os;
|
let ib_header_size = 4 + 1 + 1 + os;
|
||||||
ensure_len(file_data, ib_offset, ib_header_size)?;
|
let prefix = read_exact_at(file, ib_offset, ib_header_size)?;
|
||||||
|
|
||||||
if &file_data[ib_offset..ib_offset + 4] != b"EAIB" {
|
if &prefix[0..4] != b"EAIB" {
|
||||||
return Err(FormatError::ChunkedReadError(
|
return Err(FormatError::ChunkedReadError(
|
||||||
"invalid Extensible Array index block signature".into(),
|
"invalid Extensible Array index block signature".into(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
let mut pos = ib_offset + ib_header_size;
|
let mut pos = ib_header_size;
|
||||||
|
|
||||||
let mut chunks = Vec::new();
|
let mut chunks = Vec::new();
|
||||||
let total_elements = header.num_elements as usize;
|
let total_elements = to_usize(header.num_elements)?;
|
||||||
|
|
||||||
let dmin = header.min_dblk_nelmts as usize;
|
let dmin = header.min_dblk_nelmts as usize;
|
||||||
if dmin == 0 || !dmin.is_power_of_two() {
|
if dmin == 0 || !dmin.is_power_of_two() {
|
||||||
@@ -520,13 +569,19 @@ pub fn read_extensible_array_chunks(
|
|||||||
.and_then(|n| n.checked_mul(os).and_then(|b| p.checked_add(b)))
|
.and_then(|n| n.checked_mul(os).and_then(|b| p.checked_add(b)))
|
||||||
})
|
})
|
||||||
.ok_or_else(|| FormatError::Overflow("Extensible Array index block span".into()))?;
|
.ok_or_else(|| FormatError::Overflow("Extensible Array index block span".into()))?;
|
||||||
verify_checksum(file_data, ib_offset, ib_end)?;
|
// The whole index block in one window: every position read below is
|
||||||
|
// before `ib_end`.
|
||||||
|
// The checksum's bounds check comes first: make it before reading.
|
||||||
|
#[cfg(feature = "checksum")]
|
||||||
|
Window::check_extent(file, ib_offset, ib_end, 4)?;
|
||||||
|
let w = Window::read(file, ib_offset, ib_end.saturating_add(4))?;
|
||||||
|
verify_checksum(&w, 0, ib_end)?;
|
||||||
|
|
||||||
// 1. Elements stored inline in the index block.
|
// 1. Elements stored inline in the index block.
|
||||||
let n_inline = (header.idx_blk_elmts as usize).min(total_elements);
|
let n_inline = (header.idx_blk_elmts as usize).min(total_elements);
|
||||||
for i in 0..n_inline {
|
for i in 0..n_inline {
|
||||||
let (info, consumed) = read_element(
|
let (info, consumed) = read_element(
|
||||||
file_data,
|
&w,
|
||||||
pos,
|
pos,
|
||||||
header.client_id,
|
header.client_id,
|
||||||
header.element_size,
|
header.element_size,
|
||||||
@@ -550,8 +605,8 @@ pub fn read_extensible_array_chunks(
|
|||||||
if global_index >= total_elements {
|
if global_index >= total_elements {
|
||||||
return Ok(chunks);
|
return Ok(chunks);
|
||||||
}
|
}
|
||||||
ensure_len(file_data, pos, os)?;
|
w.ensure(pos, os)?;
|
||||||
let addr = read_offset(file_data, pos, offset_size)?;
|
let addr = read_offset(&w.bytes, pos, offset_size)?;
|
||||||
pos += os;
|
pos += os;
|
||||||
if !is_undefined_addr(addr, offset_size) {
|
if !is_undefined_addr(addr, offset_size) {
|
||||||
if dblk_nelmts > page_nelmts(header).unwrap_or(usize::MAX) {
|
if dblk_nelmts > page_nelmts(header).unwrap_or(usize::MAX) {
|
||||||
@@ -562,8 +617,8 @@ pub fn read_extensible_array_chunks(
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
chunks.extend(read_data_block_elements(
|
chunks.extend(read_data_block_elements(
|
||||||
file_data,
|
file,
|
||||||
addr as usize,
|
addr,
|
||||||
dblk_nelmts,
|
dblk_nelmts,
|
||||||
header,
|
header,
|
||||||
offset_size,
|
offset_size,
|
||||||
@@ -583,16 +638,16 @@ pub fn read_extensible_array_chunks(
|
|||||||
if global_index >= total_elements {
|
if global_index >= total_elements {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
ensure_len(file_data, pos, os)?;
|
w.ensure(pos, os)?;
|
||||||
let sb_addr = read_offset(file_data, pos, offset_size)?;
|
let sb_addr = read_offset(&w.bytes, pos, offset_size)?;
|
||||||
pos += os;
|
pos += os;
|
||||||
let (ndblks, dblk_nelmts) = sblk_info(u, dmin).ok_or_else(|| {
|
let (ndblks, dblk_nelmts) = sblk_info(u, dmin).ok_or_else(|| {
|
||||||
FormatError::Overflow("Extensible Array super block layout overflows usize".into())
|
FormatError::Overflow("Extensible Array super block layout overflows usize".into())
|
||||||
})?;
|
})?;
|
||||||
if !is_undefined_addr(sb_addr, offset_size) {
|
if !is_undefined_addr(sb_addr, offset_size) {
|
||||||
chunks.extend(read_super_block(
|
chunks.extend(read_super_block(
|
||||||
file_data,
|
file,
|
||||||
sb_addr as usize,
|
sb_addr,
|
||||||
ndblks,
|
ndblks,
|
||||||
dblk_nelmts,
|
dblk_nelmts,
|
||||||
header,
|
header,
|
||||||
@@ -617,9 +672,9 @@ pub fn read_extensible_array_chunks(
|
|||||||
/// + block offset + the page-init bitmap for every data block it owns
|
/// + block offset + the page-init bitmap for every data block it owns
|
||||||
/// + one address per data block + checksum.
|
/// + one address per data block + checksum.
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
fn read_super_block(
|
fn read_super_block<S: Storage + ?Sized>(
|
||||||
file_data: &[u8],
|
file: &S,
|
||||||
sb_offset: usize,
|
sb_offset: u64,
|
||||||
ndblks: usize,
|
ndblks: usize,
|
||||||
dblk_nelmts: usize,
|
dblk_nelmts: usize,
|
||||||
header: &ExtensibleArrayHeader,
|
header: &ExtensibleArrayHeader,
|
||||||
@@ -630,9 +685,9 @@ fn read_super_block(
|
|||||||
) -> Result<Vec<ChunkInfo>, FormatError> {
|
) -> Result<Vec<ChunkInfo>, FormatError> {
|
||||||
let os = offset_size as usize;
|
let os = offset_size as usize;
|
||||||
let sb_header_size = 4 + 1 + 1 + os + arr_off_size(header);
|
let sb_header_size = 4 + 1 + 1 + os + arr_off_size(header);
|
||||||
ensure_len(file_data, sb_offset, sb_header_size)?;
|
let prefix = read_exact_at(file, sb_offset, sb_header_size)?;
|
||||||
|
|
||||||
if &file_data[sb_offset..sb_offset + 4] != b"EASB" {
|
if &prefix[0..4] != b"EASB" {
|
||||||
return Err(FormatError::ChunkedReadError(
|
return Err(FormatError::ChunkedReadError(
|
||||||
"invalid Extensible Array super block signature".into(),
|
"invalid Extensible Array super block signature".into(),
|
||||||
));
|
));
|
||||||
@@ -654,29 +709,38 @@ fn read_super_block(
|
|||||||
let bitmap_bytes = per_dblk_bitmap
|
let bitmap_bytes = per_dblk_bitmap
|
||||||
.checked_mul(ndblks)
|
.checked_mul(ndblks)
|
||||||
.ok_or_else(|| FormatError::Overflow("Extensible Array page bitmap size".into()))?;
|
.ok_or_else(|| FormatError::Overflow("Extensible Array page bitmap size".into()))?;
|
||||||
let bitmap_start = sb_offset + sb_header_size;
|
// Positions below are relative to the super block, whose bytes (up to
|
||||||
ensure_len(file_data, bitmap_start, bitmap_bytes)?;
|
// its checksum) are all in one window.
|
||||||
let bitmap = &file_data[bitmap_start..bitmap_start + bitmap_bytes];
|
let bitmap_start = sb_header_size;
|
||||||
|
// The bitmap's bounds check, then (with checksums) the checksum's, come
|
||||||
|
// before anything else is read from the block: make them before reading
|
||||||
|
// it, so size fields stretching it past the end of the file cost no read.
|
||||||
|
Window::check_extent(file, sb_offset, bitmap_start, bitmap_bytes)?;
|
||||||
let mut pos = bitmap_start + bitmap_bytes;
|
let mut pos = bitmap_start + bitmap_bytes;
|
||||||
let mut chunks = Vec::new();
|
|
||||||
let mut global_idx = start_index;
|
|
||||||
|
|
||||||
// One checksum covers the prefix, the bitmap and every data block address.
|
// One checksum covers the prefix, the bitmap and every data block address.
|
||||||
let sb_end = ndblks
|
let sb_end = ndblks
|
||||||
.checked_mul(os)
|
.checked_mul(os)
|
||||||
.and_then(|b| pos.checked_add(b))
|
.and_then(|b| pos.checked_add(b))
|
||||||
.ok_or_else(|| FormatError::Overflow("Extensible Array super block span".into()))?;
|
.ok_or_else(|| FormatError::Overflow("Extensible Array super block span".into()))?;
|
||||||
verify_checksum(file_data, sb_offset, sb_end)?;
|
#[cfg(feature = "checksum")]
|
||||||
|
Window::check_extent(file, sb_offset, sb_end, 4)?;
|
||||||
|
let w = Window::read(file, sb_offset, sb_end.saturating_add(4))?;
|
||||||
|
w.ensure(bitmap_start, bitmap_bytes)?;
|
||||||
|
let bitmap = &w.bytes[bitmap_start..bitmap_start + bitmap_bytes];
|
||||||
|
|
||||||
|
let mut chunks = Vec::new();
|
||||||
|
let mut global_idx = start_index;
|
||||||
|
verify_checksum(&w, 0, sb_end)?;
|
||||||
|
|
||||||
for i in 0..ndblks {
|
for i in 0..ndblks {
|
||||||
ensure_len(file_data, pos, os)?;
|
w.ensure(pos, os)?;
|
||||||
let addr = read_offset(file_data, pos, offset_size)?;
|
let addr = read_offset(&w.bytes, pos, offset_size)?;
|
||||||
pos += os;
|
pos += os;
|
||||||
if !is_undefined_addr(addr, offset_size) {
|
if !is_undefined_addr(addr, offset_size) {
|
||||||
chunks.extend(read_data_block_elements(
|
chunks.extend(read_data_block_elements(
|
||||||
file_data,
|
file,
|
||||||
addr as usize,
|
addr,
|
||||||
dblk_nelmts,
|
dblk_nelmts,
|
||||||
header,
|
header,
|
||||||
offset_size,
|
offset_size,
|
||||||
@@ -887,11 +951,11 @@ mod tests {
|
|||||||
assert_eq!(chunks[1].offsets, vec![20]);
|
assert_eq!(chunks[1].offsets, vec![20]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Build a synthetic EA with inline elements + one direct data block.
|
/// A synthetic EA with inline elements + one direct data block: the
|
||||||
#[test]
|
/// file, with the header at 0x100 (8-byte offsets and lengths, 4 chunks
|
||||||
fn read_inline_plus_data_blocks() {
|
/// of 10 elements from 0x1000 on).
|
||||||
|
fn build_inline_plus_data_blocks() -> Vec<u8> {
|
||||||
let os: u8 = 8;
|
let os: u8 = 8;
|
||||||
let ls: u8 = 8;
|
|
||||||
let osv = os as usize;
|
let osv = os as usize;
|
||||||
let chunk_byte_size = 10u64 * 8; // 10 elements × 8 bytes
|
let chunk_byte_size = 10u64 * 8; // 10 elements × 8 bytes
|
||||||
let idx_blk_elmts = 2u8;
|
let idx_blk_elmts = 2u8;
|
||||||
@@ -981,8 +1045,17 @@ mod tests {
|
|||||||
dbpos += osv;
|
dbpos += osv;
|
||||||
}
|
}
|
||||||
stamp_checksum(&mut file_data, aedb_offset, dbpos);
|
stamp_checksum(&mut file_data, aedb_offset, dbpos);
|
||||||
|
file_data
|
||||||
|
}
|
||||||
|
|
||||||
let header = ExtensibleArrayHeader::parse(&file_data, aehd_offset, os, ls).unwrap();
|
/// Build a synthetic EA with inline elements + one direct data block.
|
||||||
|
#[test]
|
||||||
|
fn read_inline_plus_data_blocks() {
|
||||||
|
let (os, ls) = (8u8, 8u8);
|
||||||
|
let chunk_byte_size = 10u64 * 8;
|
||||||
|
let base_addr = 0x1000u64;
|
||||||
|
let file_data = build_inline_plus_data_blocks();
|
||||||
|
let header = ExtensibleArrayHeader::parse(&file_data, 0x100, os, ls).unwrap();
|
||||||
let ds_dims = vec![40u64];
|
let ds_dims = vec![40u64];
|
||||||
let chunk_dims = vec![10u32];
|
let chunk_dims = vec![10u32];
|
||||||
let chunks = read_extensible_array_chunks(
|
let chunks = read_extensible_array_chunks(
|
||||||
@@ -1018,7 +1091,8 @@ mod tests {
|
|||||||
fn read_element_unallocated() {
|
fn read_element_unallocated() {
|
||||||
let data = vec![0xFFu8; 16];
|
let data = vec![0xFFu8; 16];
|
||||||
let grid = ChunkGrid::fixed_array(&[50], None, &[10]).unwrap();
|
let grid = ChunkGrid::fixed_array(&[50], None, &[10]).unwrap();
|
||||||
let (info, consumed) = read_element(&data, 0, 0, 8, 8, 80, 0, &grid).unwrap();
|
let (info, consumed) =
|
||||||
|
read_element(&Window::whole(&data), 0, 0, 8, 8, 80, 0, &grid).unwrap();
|
||||||
assert!(info.is_none());
|
assert!(info.is_none());
|
||||||
assert_eq!(consumed, 8);
|
assert_eq!(consumed, 8);
|
||||||
}
|
}
|
||||||
@@ -1038,8 +1112,17 @@ mod tests {
|
|||||||
data[12..16].copy_from_slice(&0u32.to_le_bytes());
|
data[12..16].copy_from_slice(&0u32.to_le_bytes());
|
||||||
|
|
||||||
let grid = ChunkGrid::fixed_array(&[50], None, &[10]).unwrap();
|
let grid = ChunkGrid::fixed_array(&[50], None, &[10]).unwrap();
|
||||||
let (info, consumed) =
|
let (info, consumed) = read_element(
|
||||||
read_element(&data, 0, 1, elem_size as u8, os, 80, 2, &grid).unwrap();
|
&Window::whole(&data),
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
elem_size as u8,
|
||||||
|
os,
|
||||||
|
80,
|
||||||
|
2,
|
||||||
|
&grid,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
let ci = info.unwrap();
|
let ci = info.unwrap();
|
||||||
assert_eq!(ci.address, 0x2000);
|
assert_eq!(ci.address, 0x2000);
|
||||||
assert_eq!(ci.chunk_size, 120);
|
assert_eq!(ci.chunk_size, 120);
|
||||||
@@ -1047,4 +1130,38 @@ mod tests {
|
|||||||
assert_eq!(ci.offsets, vec![20]);
|
assert_eq!(ci.offsets, vec![20]);
|
||||||
assert_eq!(consumed, elem_size);
|
assert_eq!(consumed, elem_size);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The Storage path reads exactly what the slice path reads: the array
|
||||||
|
/// whole, cut at every length through its structures, and with a byte
|
||||||
|
/// damaged in each of them, through a read_at-only CountingStorage.
|
||||||
|
#[test]
|
||||||
|
fn storage_reads_match_slice_reads() {
|
||||||
|
use crate::storage::CountingStorage;
|
||||||
|
let full = build_inline_plus_data_blocks();
|
||||||
|
let mut files = Vec::new();
|
||||||
|
for cut in 0x100..0x340 {
|
||||||
|
files.push(full[..cut].to_vec());
|
||||||
|
}
|
||||||
|
for at in [0x104, 0x150, 0x204, 0x216, 0x230, 0x304, 0x318] {
|
||||||
|
let mut damaged = full.clone();
|
||||||
|
damaged[at] ^= 1;
|
||||||
|
files.push(damaged);
|
||||||
|
}
|
||||||
|
files.push(full);
|
||||||
|
let mut compared = 0;
|
||||||
|
for f in files {
|
||||||
|
let storage = CountingStorage::new(f.clone());
|
||||||
|
let want = ExtensibleArrayHeader::parse(&f, 0x100, 8, 8);
|
||||||
|
let got = ExtensibleArrayHeader::parse_in(&storage, 0x100, 8, 8);
|
||||||
|
assert_eq!(format!("{got:?}"), format!("{want:?}"));
|
||||||
|
let Ok(h) = want else { continue };
|
||||||
|
for dims in [&[40u64][..], &[25]] {
|
||||||
|
let want = read_extensible_array_chunks(&f, &h, dims, None, &[10], 8, 8, 8);
|
||||||
|
let got = read_extensible_array_chunks_in(&storage, &h, dims, None, &[10], 8, 8, 8);
|
||||||
|
assert_eq!(format!("{got:?}"), format!("{want:?}"), "{} bytes", f.len());
|
||||||
|
compared += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert!(compared > 100);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,10 +3,12 @@
|
|||||||
//! Produces valid HDF5 files with v3 superblock, v2 object headers,
|
//! Produces valid HDF5 files with v3 superblock, v2 object headers,
|
||||||
//! link messages, contiguous datasets, inline and dense attributes.
|
//! link messages, contiguous datasets, inline and dense attributes.
|
||||||
|
|
||||||
|
use crate::addr::saturating_usize;
|
||||||
#[cfg(not(feature = "std"))]
|
#[cfg(not(feature = "std"))]
|
||||||
use alloc::{format, vec, vec::Vec};
|
use alloc::{format, vec, vec::Vec};
|
||||||
|
|
||||||
use crate::attribute::AttributeMessage;
|
use crate::attribute::AttributeMessage;
|
||||||
|
use crate::btree_v2_write::{BTreeV2Params, build_btree_v2};
|
||||||
use crate::chunked_write::{
|
use crate::chunked_write::{
|
||||||
ChunkOptions, PrecompressedChunks, build_chunked_data_from_precompressed, precompress_chunks,
|
ChunkOptions, PrecompressedChunks, build_chunked_data_from_precompressed, precompress_chunks,
|
||||||
};
|
};
|
||||||
@@ -74,14 +76,57 @@ const DENSE_LINK_THRESHOLD: usize = 8;
|
|||||||
|
|
||||||
// ---- OH builders ----
|
// ---- OH builders ----
|
||||||
|
|
||||||
|
/// An object's attributes as its header stores them: inline Attribute
|
||||||
|
/// messages, or (`dense`) the Attribute Info message of dense storage; with
|
||||||
|
/// `track_order`, their creation order tracked and indexed.
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub(crate) struct AttrStorage<'a> {
|
||||||
|
pub(crate) attrs: &'a [AttributeMessage],
|
||||||
|
pub(crate) dense: Option<&'a DenseAttrBlob>,
|
||||||
|
pub(crate) track_order: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AttrStorage<'_> {
|
||||||
|
/// Add the attribute messages to the header being built. Tracking
|
||||||
|
/// creation order, as libhdf5 does it: the header's flags say so, an
|
||||||
|
/// Attribute Info message is written even for inline attributes (it
|
||||||
|
/// holds the next creation order), and each inline attribute's message
|
||||||
|
/// carries its creation order.
|
||||||
|
fn add_to(&self, w: &mut ObjectHeaderWriter) {
|
||||||
|
if self.track_order {
|
||||||
|
w.track_attr_order();
|
||||||
|
}
|
||||||
|
if let Some(blob) = self.dense {
|
||||||
|
w.add_message(MessageType::AttributeInfo, blob.attr_info_message.clone());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if self.track_order {
|
||||||
|
w.add_message(
|
||||||
|
MessageType::AttributeInfo,
|
||||||
|
serialize_attribute_info(
|
||||||
|
u64::MAX,
|
||||||
|
u64::MAX,
|
||||||
|
Some((self.attrs.len() as u16, u64::MAX)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
for (i, attr) in self.attrs.iter().enumerate() {
|
||||||
|
w.add_message_with_order(
|
||||||
|
MessageType::Attribute,
|
||||||
|
attr.serialize(LENGTH_SIZE),
|
||||||
|
i as u16,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub(crate) fn build_chunked_dataset_oh(
|
pub(crate) fn build_chunked_dataset_oh(
|
||||||
dt: &Datatype,
|
dt: &Datatype,
|
||||||
ds: &Dataspace,
|
ds: &Dataspace,
|
||||||
layout_message: &[u8],
|
layout_message: &[u8],
|
||||||
pipeline_message: Option<&[u8]>,
|
pipeline_message: Option<&[u8]>,
|
||||||
attrs: &[AttributeMessage],
|
attrs: AttrStorage<'_>,
|
||||||
dense_blob: Option<&DenseAttrBlob>,
|
|
||||||
fill_message: &[u8],
|
fill_message: &[u8],
|
||||||
refcount: u32,
|
refcount: u32,
|
||||||
) -> Result<Vec<u8>, FormatError> {
|
) -> Result<Vec<u8>, FormatError> {
|
||||||
@@ -93,13 +138,7 @@ pub(crate) fn build_chunked_dataset_oh(
|
|||||||
if let Some(pm) = pipeline_message {
|
if let Some(pm) = pipeline_message {
|
||||||
w.add_message(MessageType::FilterPipeline, pm.to_vec());
|
w.add_message(MessageType::FilterPipeline, pm.to_vec());
|
||||||
}
|
}
|
||||||
if let Some(blob) = dense_blob {
|
attrs.add_to(&mut w);
|
||||||
w.add_message(MessageType::AttributeInfo, blob.attr_info_message.clone());
|
|
||||||
} else {
|
|
||||||
for attr in attrs {
|
|
||||||
w.add_message(MessageType::Attribute, attr.serialize(LENGTH_SIZE));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
add_refcount(&mut w, refcount);
|
add_refcount(&mut w, refcount);
|
||||||
w.serialize()
|
w.serialize()
|
||||||
}
|
}
|
||||||
@@ -110,8 +149,7 @@ pub(crate) fn build_dataset_oh(
|
|||||||
ds: &Dataspace,
|
ds: &Dataspace,
|
||||||
data_addr: u64,
|
data_addr: u64,
|
||||||
data_size: u64,
|
data_size: u64,
|
||||||
attrs: &[AttributeMessage],
|
attrs: AttrStorage<'_>,
|
||||||
dense_blob: Option<&DenseAttrBlob>,
|
|
||||||
fill_message: &[u8],
|
fill_message: &[u8],
|
||||||
refcount: u32,
|
refcount: u32,
|
||||||
) -> Result<Vec<u8>, FormatError> {
|
) -> Result<Vec<u8>, FormatError> {
|
||||||
@@ -131,13 +169,7 @@ pub(crate) fn build_dataset_oh(
|
|||||||
dl.extend_from_slice(&data_addr.to_le_bytes());
|
dl.extend_from_slice(&data_addr.to_le_bytes());
|
||||||
dl.extend_from_slice(&data_size.to_le_bytes());
|
dl.extend_from_slice(&data_size.to_le_bytes());
|
||||||
w.add_message(MessageType::DataLayout, dl);
|
w.add_message(MessageType::DataLayout, dl);
|
||||||
if let Some(blob) = dense_blob {
|
attrs.add_to(&mut w);
|
||||||
w.add_message(MessageType::AttributeInfo, blob.attr_info_message.clone());
|
|
||||||
} else {
|
|
||||||
for attr in attrs {
|
|
||||||
w.add_message(MessageType::Attribute, attr.serialize(LENGTH_SIZE));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
add_refcount(&mut w, refcount);
|
add_refcount(&mut w, refcount);
|
||||||
w.serialize()
|
w.serialize()
|
||||||
}
|
}
|
||||||
@@ -147,8 +179,7 @@ pub(crate) fn build_compact_dataset_oh(
|
|||||||
dt: &Datatype,
|
dt: &Datatype,
|
||||||
ds: &Dataspace,
|
ds: &Dataspace,
|
||||||
data: &[u8],
|
data: &[u8],
|
||||||
attrs: &[AttributeMessage],
|
attrs: AttrStorage<'_>,
|
||||||
dense_blob: Option<&DenseAttrBlob>,
|
|
||||||
fill_message: &[u8],
|
fill_message: &[u8],
|
||||||
refcount: u32,
|
refcount: u32,
|
||||||
) -> Result<Vec<u8>, FormatError> {
|
) -> Result<Vec<u8>, FormatError> {
|
||||||
@@ -163,13 +194,7 @@ pub(crate) fn build_compact_dataset_oh(
|
|||||||
dl.extend_from_slice(&(data.len() as u16).to_le_bytes());
|
dl.extend_from_slice(&(data.len() as u16).to_le_bytes());
|
||||||
dl.extend_from_slice(data);
|
dl.extend_from_slice(data);
|
||||||
w.add_message(MessageType::DataLayout, dl);
|
w.add_message(MessageType::DataLayout, dl);
|
||||||
if let Some(blob) = dense_blob {
|
attrs.add_to(&mut w);
|
||||||
w.add_message(MessageType::AttributeInfo, blob.attr_info_message.clone());
|
|
||||||
} else {
|
|
||||||
for attr in attrs {
|
|
||||||
w.add_message(MessageType::Attribute, attr.serialize(LENGTH_SIZE));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
add_refcount(&mut w, refcount);
|
add_refcount(&mut w, refcount);
|
||||||
w.serialize()
|
w.serialize()
|
||||||
}
|
}
|
||||||
@@ -181,8 +206,7 @@ pub(crate) fn build_group_oh(
|
|||||||
links: &[LinkMessage],
|
links: &[LinkMessage],
|
||||||
link_info: &[u8],
|
link_info: &[u8],
|
||||||
dense_links: bool,
|
dense_links: bool,
|
||||||
attrs: &[AttributeMessage],
|
attrs: AttrStorage<'_>,
|
||||||
dense_blob: Option<&DenseAttrBlob>,
|
|
||||||
refcount: u32,
|
refcount: u32,
|
||||||
) -> Result<Vec<u8>, FormatError> {
|
) -> Result<Vec<u8>, FormatError> {
|
||||||
let mut w = ObjectHeaderWriter::new();
|
let mut w = ObjectHeaderWriter::new();
|
||||||
@@ -197,13 +221,7 @@ pub(crate) fn build_group_oh(
|
|||||||
w.add_message(MessageType::Link, link.serialize(OFFSET_SIZE));
|
w.add_message(MessageType::Link, link.serialize(OFFSET_SIZE));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(blob) = dense_blob {
|
attrs.add_to(&mut w);
|
||||||
w.add_message(MessageType::AttributeInfo, blob.attr_info_message.clone());
|
|
||||||
} else {
|
|
||||||
for attr in attrs {
|
|
||||||
w.add_message(MessageType::Attribute, attr.serialize(LENGTH_SIZE));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
add_refcount(&mut w, refcount);
|
add_refcount(&mut w, refcount);
|
||||||
w.serialize()
|
w.serialize()
|
||||||
}
|
}
|
||||||
@@ -319,7 +337,7 @@ pub(crate) fn build_single_block_fractal_heap(
|
|||||||
|
|
||||||
// An object must fit one direct block: the writer has no huge-object
|
// An object must fit one direct block: the writer has no huge-object
|
||||||
// path, and libhdf5 cannot read an object that overruns its block.
|
// path, and libhdf5 cannot read an object that overruns its block.
|
||||||
let max_managed = max_direct_block_size as usize - dblock_header_size;
|
let max_managed = saturating_usize(max_direct_block_size) - dblock_header_size;
|
||||||
if let Some(big) = serialized.iter().find(|s| s.len() > max_managed) {
|
if let Some(big) = serialized.iter().find(|s| s.len() > max_managed) {
|
||||||
return Err(FormatError::SerializationError(format!(
|
return Err(FormatError::SerializationError(format!(
|
||||||
"a {}-byte message cannot go in dense storage: a fractal heap \
|
"a {}-byte message cannot go in dense storage: a fractal heap \
|
||||||
@@ -375,7 +393,7 @@ pub(crate) fn build_single_block_fractal_heap(
|
|||||||
let dblock_addr = frhp_addr + frhp_size as u64;
|
let dblock_addr = frhp_addr + frhp_size as u64;
|
||||||
let btree_addr = dblock_addr + starting_block_size;
|
let btree_addr = dblock_addr + starting_block_size;
|
||||||
|
|
||||||
let data_space = starting_block_size as usize - dblock_header_size;
|
let data_space = saturating_usize(starting_block_size) - dblock_header_size;
|
||||||
let free_space = data_space - total_data_size;
|
let free_space = data_space - total_data_size;
|
||||||
|
|
||||||
// Build fractal heap header
|
// Build fractal heap header
|
||||||
@@ -411,7 +429,7 @@ pub(crate) fn build_single_block_fractal_heap(
|
|||||||
debug_assert_eq!(frhp.len(), frhp_size);
|
debug_assert_eq!(frhp.len(), frhp_size);
|
||||||
|
|
||||||
// Build direct block: header (with checksum) + data + padding
|
// Build direct block: header (with checksum) + data + padding
|
||||||
let mut dblock = Vec::with_capacity(starting_block_size as usize);
|
let mut dblock = Vec::with_capacity(saturating_usize(starting_block_size));
|
||||||
dblock.extend_from_slice(b"FHDB");
|
dblock.extend_from_slice(b"FHDB");
|
||||||
dblock.push(0); // version
|
dblock.push(0); // version
|
||||||
write_offset(&mut dblock, frhp_addr, OFFSET_SIZE);
|
write_offset(&mut dblock, frhp_addr, OFFSET_SIZE);
|
||||||
@@ -429,12 +447,12 @@ pub(crate) fn build_single_block_fractal_heap(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Pad to full block size
|
// Pad to full block size
|
||||||
dblock.resize(starting_block_size as usize, 0);
|
dblock.resize(saturating_usize(starting_block_size), 0);
|
||||||
|
|
||||||
// Checksum: computed over entire block with checksum field zeroed
|
// Checksum: computed over entire block with checksum field zeroed
|
||||||
let dblock_checksum = crate::checksum::jenkins_lookup3(&dblock);
|
let dblock_checksum = crate::checksum::jenkins_lookup3(&dblock);
|
||||||
dblock[cksum_pos..cksum_pos + 4].copy_from_slice(&dblock_checksum.to_le_bytes());
|
dblock[cksum_pos..cksum_pos + 4].copy_from_slice(&dblock_checksum.to_le_bytes());
|
||||||
debug_assert_eq!(dblock.len(), starting_block_size as usize);
|
debug_assert_eq!(dblock.len(), saturating_usize(starting_block_size));
|
||||||
|
|
||||||
// Build heap IDs
|
// Build heap IDs
|
||||||
let heap_ids: Vec<Vec<u8>> = obj_offsets
|
let heap_ids: Vec<Vec<u8>> = obj_offsets
|
||||||
@@ -689,7 +707,7 @@ impl HeapIndirectBlock {
|
|||||||
let cksum_pos = out.len();
|
let cksum_pos = out.len();
|
||||||
out.extend_from_slice(&[0u8; 4]); // checksum placeholder
|
out.extend_from_slice(&[0u8; 4]); // checksum placeholder
|
||||||
out.extend_from_slice(&b.data);
|
out.extend_from_slice(&b.data);
|
||||||
out.resize(d + b.size as usize, 0);
|
out.resize(d + saturating_usize(b.size), 0);
|
||||||
let cksum = crate::checksum::jenkins_lookup3(&out[d..]);
|
let cksum = crate::checksum::jenkins_lookup3(&out[d..]);
|
||||||
out[cksum_pos..cksum_pos + 4].copy_from_slice(&cksum.to_le_bytes());
|
out[cksum_pos..cksum_pos + 4].copy_from_slice(&cksum.to_le_bytes());
|
||||||
child += b.size;
|
child += b.size;
|
||||||
@@ -723,7 +741,7 @@ impl HeapPacker<'_> {
|
|||||||
nrows: Option<usize>,
|
nrows: Option<usize>,
|
||||||
) -> Result<HeapIndirectBlock, FormatError> {
|
) -> Result<HeapIndirectBlock, FormatError> {
|
||||||
let geom = self.geom;
|
let geom = self.geom;
|
||||||
let width = geom.width as usize;
|
let width = saturating_usize(geom.width);
|
||||||
let mut slots = Vec::new();
|
let mut slots = Vec::new();
|
||||||
let mut off = heap_offset;
|
let mut off = heap_offset;
|
||||||
let mut row = 0usize;
|
let mut row = 0usize;
|
||||||
@@ -746,7 +764,8 @@ impl HeapPacker<'_> {
|
|||||||
// A child whose biggest direct block cannot hold the
|
// A child whose biggest direct block cannot hold the
|
||||||
// next object is skipped whole, not walked.
|
// next object is skipped whole, not walked.
|
||||||
let biggest = geom.row_size(child_rows.min(geom.max_direct_rows()) - 1);
|
let biggest = geom.row_size(child_rows.min(geom.max_direct_rows()) - 1);
|
||||||
if self.objects[self.next].len() > (biggest as usize - geom.dblock_header_size)
|
if self.objects[self.next].len()
|
||||||
|
> (saturating_usize(biggest) - geom.dblock_header_size)
|
||||||
{
|
{
|
||||||
slots.push(HeapSlot::Empty);
|
slots.push(HeapSlot::Empty);
|
||||||
off += size;
|
off += size;
|
||||||
@@ -777,7 +796,7 @@ impl HeapPacker<'_> {
|
|||||||
/// objects as fit; leave it unallocated if not even the next one does.
|
/// objects as fit; leave it unallocated if not even the next one does.
|
||||||
fn fill_direct(&mut self, heap_offset: u64, size: u64) -> HeapSlot {
|
fn fill_direct(&mut self, heap_offset: u64, size: u64) -> HeapSlot {
|
||||||
let header = self.geom.dblock_header_size;
|
let header = self.geom.dblock_header_size;
|
||||||
let capacity = size as usize - header;
|
let capacity = saturating_usize(size) - header;
|
||||||
let mut data = Vec::new();
|
let mut data = Vec::new();
|
||||||
while let Some(obj) = self.objects.get(self.next) {
|
while let Some(obj) = self.objects.get(self.next) {
|
||||||
if data.len() + obj.len() > capacity {
|
if data.len() + obj.len() > capacity {
|
||||||
@@ -890,11 +909,31 @@ fn write_frhp(p: WriteFrhp) -> Vec<u8> {
|
|||||||
frhp
|
frhp
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// libhdf5 numbers the attributes of an object that tracks their creation
|
||||||
|
/// order with a 2-byte counter.
|
||||||
|
fn check_tracked_attr_count(track_order: bool, n: usize) -> Result<(), FormatError> {
|
||||||
|
if track_order && n > usize::from(u16::MAX) {
|
||||||
|
return Err(FormatError::SerializationError(format!(
|
||||||
|
"{n} attributes on one object with creation order tracked: libhdf5 \
|
||||||
|
numbers at most {} (set fewer, or turn off track_order)",
|
||||||
|
u16::MAX
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Build dense attribute storage for a set of attributes.
|
/// Build dense attribute storage for a set of attributes.
|
||||||
|
///
|
||||||
|
/// With `track_order` the Attribute Info message tracks creation order (an
|
||||||
|
/// attribute's creation order is its position in `attrs`) and a type-9
|
||||||
|
/// creation-order index follows the name index, as libhdf5 writes for h5py's
|
||||||
|
/// `track_order=True`. libhdf5 numbers at most 65 535 attributes.
|
||||||
pub(crate) fn build_dense_attrs(
|
pub(crate) fn build_dense_attrs(
|
||||||
attrs: &[AttributeMessage],
|
attrs: &[AttributeMessage],
|
||||||
base_address: u64,
|
base_address: u64,
|
||||||
|
track_order: bool,
|
||||||
) -> Result<DenseAttrBlob, FormatError> {
|
) -> Result<DenseAttrBlob, FormatError> {
|
||||||
|
check_tracked_attr_count(track_order, attrs.len())?;
|
||||||
// Dense attrs use v3 attribute messages (adds character set encoding byte).
|
// Dense attrs use v3 attribute messages (adds character set encoding byte).
|
||||||
let serialized: Vec<Vec<u8>> = attrs.iter().map(|a| a.serialize_v3(LENGTH_SIZE)).collect();
|
let serialized: Vec<Vec<u8>> = attrs.iter().map(|a| a.serialize_v3(LENGTH_SIZE)).collect();
|
||||||
|
|
||||||
@@ -910,31 +949,55 @@ pub(crate) fn build_dense_attrs(
|
|||||||
let heap_id_length = heap.heap_id_length;
|
let heap_id_length = heap.heap_id_length;
|
||||||
let heap_ids = &heap.heap_ids;
|
let heap_ids = &heap.heap_ids;
|
||||||
|
|
||||||
// Build B-tree v2 type 8 records (17 bytes each)
|
// Build B-tree v2 type 8 records (17 bytes each), in the index's key
|
||||||
|
// order: libhdf5 compares the name hash, then — for names whose hashes
|
||||||
|
// collide — the names themselves (`strcmp`).
|
||||||
let record_size: u16 = heap_id_length + 1 + 4 + 4;
|
let record_size: u16 = heap_id_length + 1 + 4 + 4;
|
||||||
let mut records: Vec<(u32, u32, Vec<u8>)> = Vec::with_capacity(attrs.len());
|
let mut order: Vec<usize> = (0..attrs.len()).collect();
|
||||||
for (i, heap_id) in heap_ids.iter().enumerate() {
|
order.sort_by(|&a, &b| {
|
||||||
|
name_hashes[a]
|
||||||
|
.cmp(&name_hashes[b])
|
||||||
|
.then_with(|| attrs[a].name.as_bytes().cmp(attrs[b].name.as_bytes()))
|
||||||
|
});
|
||||||
|
let records: Vec<Vec<u8>> = order
|
||||||
|
.into_iter()
|
||||||
|
.map(|i| {
|
||||||
let mut rec = Vec::with_capacity(record_size as usize);
|
let mut rec = Vec::with_capacity(record_size as usize);
|
||||||
rec.extend_from_slice(heap_id);
|
rec.extend_from_slice(&heap_ids[i]);
|
||||||
rec.push(0); // msg_flags
|
rec.push(0); // msg_flags
|
||||||
rec.extend_from_slice(&(i as u32).to_le_bytes()); // creation_order
|
rec.extend_from_slice(&(i as u32).to_le_bytes()); // creation_order
|
||||||
rec.extend_from_slice(&name_hashes[i].to_le_bytes()); // hash
|
rec.extend_from_slice(&name_hashes[i].to_le_bytes()); // hash
|
||||||
records.push((name_hashes[i], i as u32, rec));
|
rec
|
||||||
}
|
})
|
||||||
records.sort_by(|a, b| a.0.cmp(&b.0).then(a.1.cmp(&b.1)));
|
.collect();
|
||||||
|
|
||||||
let records: Vec<Vec<u8>> = records.into_iter().map(|(_, _, rec)| rec).collect();
|
|
||||||
let bthd_addr = btree_addr;
|
let bthd_addr = btree_addr;
|
||||||
let mut blob = heap.blob;
|
let mut blob = heap.blob;
|
||||||
blob.extend_from_slice(&single_leaf_v2_btree(
|
blob.extend_from_slice(&dense_v2_btree(8, record_size, &records, bthd_addr)?);
|
||||||
8,
|
|
||||||
record_size,
|
|
||||||
&records,
|
|
||||||
bthd_addr,
|
|
||||||
"attributes on one object",
|
|
||||||
)?);
|
|
||||||
|
|
||||||
let attr_info = serialize_attribute_info(frhp_addr, bthd_addr);
|
let order = if track_order {
|
||||||
|
// Type 9 records: heap ID, message flags, creation order (the key).
|
||||||
|
let records: Vec<Vec<u8>> = heap_ids
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(i, heap_id)| {
|
||||||
|
let mut rec = heap_id.clone();
|
||||||
|
rec.push(0); // msg_flags
|
||||||
|
rec.extend_from_slice(&(i as u32).to_le_bytes());
|
||||||
|
rec
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let corder_addr = base_address + blob.len() as u64;
|
||||||
|
blob.extend_from_slice(&dense_v2_btree(
|
||||||
|
9,
|
||||||
|
heap_id_length + 1 + 4,
|
||||||
|
&records,
|
||||||
|
corder_addr,
|
||||||
|
)?);
|
||||||
|
Some((attrs.len() as u16, corder_addr))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
let attr_info = serialize_attribute_info(frhp_addr, bthd_addr, order);
|
||||||
|
|
||||||
Ok(DenseAttrBlob {
|
Ok(DenseAttrBlob {
|
||||||
attr_info_message: attr_info,
|
attr_info_message: attr_info,
|
||||||
@@ -953,69 +1016,57 @@ pub(crate) struct DenseLinkBlob {
|
|||||||
pub(crate) blob: Vec<u8>,
|
pub(crate) blob: Vec<u8>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A v2 B-tree of `btree_type` holding `records` (already in key order) in a
|
/// libhdf5's node size for the dense link and attribute indexes it creates
|
||||||
/// single leaf, laid out at `addr`: the header, then the leaf. `what` names
|
/// (`H5G_NAME_BT2_NODE_SIZE`, `H5A_NAME_BT2_NODE_SIZE`, and the
|
||||||
/// the records in the error for too many ("links in one group").
|
/// creation-order indexes'), with their split and merge percentages.
|
||||||
fn single_leaf_v2_btree(
|
const DENSE_BT2_NODE_SIZE: u32 = 512;
|
||||||
|
const DENSE_BT2_SPLIT_PERCENT: u8 = 100;
|
||||||
|
const DENSE_BT2_MERGE_PERCENT: u8 = 40;
|
||||||
|
|
||||||
|
/// A dense-storage v2 B-tree of `btree_type` holding `records` (already in
|
||||||
|
/// key order), laid out at `addr`: the header, then its nodes.
|
||||||
|
///
|
||||||
|
/// Up to 65 535 records go in one leaf node sized to hold them (the layout
|
||||||
|
/// the writer has always used, kept so those files do not change). A leaf's
|
||||||
|
/// record count is a 2-byte field, and libhdf5 sizes a leaf's capacity from
|
||||||
|
/// the node size: a node with room for more than 65 535 records makes it
|
||||||
|
/// overflow that count when it adds one, so the node is capped at a full
|
||||||
|
/// leaf. More records get libhdf5's own 512-byte nodes, with internal nodes
|
||||||
|
/// above the leaves.
|
||||||
|
fn dense_v2_btree(
|
||||||
btree_type: u8,
|
btree_type: u8,
|
||||||
record_size: u16,
|
record_size: u16,
|
||||||
records: &[Vec<u8>],
|
records: &[Vec<u8>],
|
||||||
addr: u64,
|
addr: u64,
|
||||||
what: &str,
|
|
||||||
) -> Result<Vec<u8>, FormatError> {
|
) -> Result<Vec<u8>, FormatError> {
|
||||||
let os = OFFSET_SIZE as usize;
|
let rs = usize::from(record_size);
|
||||||
let ls = LENGTH_SIZE as usize;
|
let n = records.len();
|
||||||
// The root node's record count is a 2-byte field; more records need
|
let node_size = if n <= usize::from(u16::MAX) {
|
||||||
// internal nodes, which the writer does not build.
|
let btlf_size = 4 + 1 + 1 + n * rs + 4;
|
||||||
let num_records = u16::try_from(records.len()).map_err(|_| {
|
let max_node = 4 + 1 + 1 + usize::from(u16::MAX) * rs + 4;
|
||||||
FormatError::SerializationError(format!(
|
u32::try_from(btlf_size.next_power_of_two().max(512).min(max_node))
|
||||||
"{} {what}: at most {} can be written \
|
.map_err(|_| FormatError::Overflow("B-tree v2 node size".into()))?
|
||||||
(a deeper B-tree index is not implemented)",
|
} else {
|
||||||
records.len(),
|
DENSE_BT2_NODE_SIZE
|
||||||
u16::MAX
|
};
|
||||||
))
|
let flat: Vec<u8> = records
|
||||||
})?;
|
.iter()
|
||||||
let bthd_size = 4 + 1 + 1 + 4 + 2 + 2 + 1 + 1 + os + 2 + ls + 4;
|
.inspect(|r| debug_assert_eq!(r.len(), rs))
|
||||||
let btlf_size = 4 + 1 + 1 + (records.len() * record_size as usize) + 4;
|
.flat_map(|r| r.iter().copied())
|
||||||
// libhdf5 sizes a leaf's capacity from the node size, and a leaf's
|
.collect();
|
||||||
// record count is a 2-byte field: a node with room for more than
|
build_btree_v2(
|
||||||
// 65 535 records makes it overflow that count when it adds one (the
|
BTreeV2Params {
|
||||||
// group can then no longer be listed). Cap the node at a full leaf.
|
tree_type: btree_type,
|
||||||
let max_node = btlf_size - records.len() * record_size as usize
|
node_size,
|
||||||
+ usize::from(u16::MAX) * record_size as usize;
|
record_size,
|
||||||
let node_size = btlf_size.next_power_of_two().max(512).min(max_node) as u32;
|
split_percent: DENSE_BT2_SPLIT_PERCENT,
|
||||||
let btlf_addr = addr + bthd_size as u64;
|
merge_percent: DENSE_BT2_MERGE_PERCENT,
|
||||||
|
},
|
||||||
let mut out = Vec::with_capacity(bthd_size + node_size as usize);
|
&flat,
|
||||||
out.extend_from_slice(b"BTHD");
|
addr,
|
||||||
out.push(0); // version
|
OFFSET_SIZE,
|
||||||
out.push(btree_type);
|
LENGTH_SIZE,
|
||||||
out.extend_from_slice(&node_size.to_le_bytes());
|
)
|
||||||
out.extend_from_slice(&record_size.to_le_bytes());
|
|
||||||
out.extend_from_slice(&0u16.to_le_bytes()); // depth = 0 (single leaf)
|
|
||||||
out.push(100); // split_percent
|
|
||||||
out.push(40); // merge_percent
|
|
||||||
write_offset(&mut out, btlf_addr, OFFSET_SIZE);
|
|
||||||
out.extend_from_slice(&num_records.to_le_bytes());
|
|
||||||
write_length(&mut out, records.len() as u64, LENGTH_SIZE);
|
|
||||||
let checksum = crate::checksum::jenkins_lookup3(&out);
|
|
||||||
out.extend_from_slice(&checksum.to_le_bytes());
|
|
||||||
debug_assert_eq!(out.len(), bthd_size);
|
|
||||||
|
|
||||||
let mut btlf = Vec::with_capacity(node_size as usize);
|
|
||||||
btlf.extend_from_slice(b"BTLF");
|
|
||||||
btlf.push(0); // version
|
|
||||||
btlf.push(btree_type);
|
|
||||||
for rec in records {
|
|
||||||
debug_assert_eq!(rec.len(), record_size as usize);
|
|
||||||
btlf.extend_from_slice(rec);
|
|
||||||
}
|
|
||||||
// The checksum follows the records, not the end of the node.
|
|
||||||
let checksum = crate::checksum::jenkins_lookup3(&btlf);
|
|
||||||
btlf.extend_from_slice(&checksum.to_le_bytes());
|
|
||||||
btlf.resize(node_size as usize, 0);
|
|
||||||
out.extend_from_slice(&btlf);
|
|
||||||
Ok(out)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Build dense link storage for a group's links, laid out at `base_address`.
|
/// Build dense link storage for a group's links, laid out at `base_address`.
|
||||||
@@ -1039,14 +1090,18 @@ pub(crate) fn build_dense_links(
|
|||||||
let heap = build_single_block_fractal_heap(&serialized, base_address, 32, 7)?;
|
let heap = build_single_block_fractal_heap(&serialized, base_address, 32, 7)?;
|
||||||
let heap_id_length = heap.heap_id_length;
|
let heap_id_length = heap.heap_id_length;
|
||||||
|
|
||||||
// Type 5 records: hash(4) + heap_id. The B-tree's key is the name hash,
|
// Type 5 records: hash(4) + heap_id. The B-tree's key is the name hash
|
||||||
// so records are sorted by (hash, order).
|
// and, for names whose hashes collide, the name (libhdf5 compares them
|
||||||
|
// with `strcmp`): records out of that order are not found by name.
|
||||||
let mut by_name: Vec<(u32, usize)> = links
|
let mut by_name: Vec<(u32, usize)> = links
|
||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(i, l)| (crate::checksum::jenkins_lookup3(l.name.as_bytes()), i))
|
.map(|(i, l)| (crate::checksum::jenkins_lookup3(l.name.as_bytes()), i))
|
||||||
.collect();
|
.collect();
|
||||||
by_name.sort_unstable();
|
by_name.sort_unstable_by(|&(ha, a), &(hb, b)| {
|
||||||
|
ha.cmp(&hb)
|
||||||
|
.then_with(|| links[a].name.as_bytes().cmp(links[b].name.as_bytes()))
|
||||||
|
});
|
||||||
let name_records: Vec<Vec<u8>> = by_name
|
let name_records: Vec<Vec<u8>> = by_name
|
||||||
.iter()
|
.iter()
|
||||||
.map(|&(hash, i)| {
|
.map(|&(hash, i)| {
|
||||||
@@ -1057,12 +1112,11 @@ pub(crate) fn build_dense_links(
|
|||||||
.collect();
|
.collect();
|
||||||
let name_bt_addr = heap.btree_addr;
|
let name_bt_addr = heap.btree_addr;
|
||||||
let mut blob = heap.blob;
|
let mut blob = heap.blob;
|
||||||
blob.extend_from_slice(&single_leaf_v2_btree(
|
blob.extend_from_slice(&dense_v2_btree(
|
||||||
5,
|
5,
|
||||||
4 + heap_id_length,
|
4 + heap_id_length,
|
||||||
&name_records,
|
&name_records,
|
||||||
name_bt_addr,
|
name_bt_addr,
|
||||||
"links in one group",
|
|
||||||
)?);
|
)?);
|
||||||
|
|
||||||
let link_info_message = if track_order {
|
let link_info_message = if track_order {
|
||||||
@@ -1082,12 +1136,11 @@ pub(crate) fn build_dense_links(
|
|||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
let order_bt_addr = base_address + blob.len() as u64;
|
let order_bt_addr = base_address + blob.len() as u64;
|
||||||
blob.extend_from_slice(&single_leaf_v2_btree(
|
blob.extend_from_slice(&dense_v2_btree(
|
||||||
6,
|
6,
|
||||||
8 + heap_id_length,
|
8 + heap_id_length,
|
||||||
&order_records,
|
&order_records,
|
||||||
order_bt_addr,
|
order_bt_addr,
|
||||||
"links in one group",
|
|
||||||
)?);
|
)?);
|
||||||
let next_order = by_order.last().map_or(0, |&(o, _)| o + 1);
|
let next_order = by_order.last().map_or(0, |&(o, _)| o + 1);
|
||||||
serialize_link_info(
|
serialize_link_info(
|
||||||
@@ -1147,12 +1200,25 @@ fn encode_managed_id(offset: u64, length: u64, max_heap_size: u16, id_length: u1
|
|||||||
id
|
id
|
||||||
}
|
}
|
||||||
|
|
||||||
fn serialize_attribute_info(fh_addr: u64, btree_name_addr: u64) -> Vec<u8> {
|
/// Serialize an Attribute Info message (version 0). `order` — the next
|
||||||
|
/// creation order to assign and the creation-order index's address — is
|
||||||
|
/// present when creation order is tracked and indexed.
|
||||||
|
fn serialize_attribute_info(
|
||||||
|
fh_addr: u64,
|
||||||
|
btree_name_addr: u64,
|
||||||
|
order: Option<(u16, u64)>,
|
||||||
|
) -> Vec<u8> {
|
||||||
let mut data = Vec::new();
|
let mut data = Vec::new();
|
||||||
data.push(0); // version
|
data.push(0); // version
|
||||||
data.push(0x00); // flags
|
data.push(if order.is_some() { 0x03 } else { 0x00 }); // flags: tracked, indexed
|
||||||
|
if let Some((next, _)) = order {
|
||||||
|
data.extend_from_slice(&next.to_le_bytes());
|
||||||
|
}
|
||||||
data.extend_from_slice(&fh_addr.to_le_bytes());
|
data.extend_from_slice(&fh_addr.to_le_bytes());
|
||||||
data.extend_from_slice(&btree_name_addr.to_le_bytes());
|
data.extend_from_slice(&btree_name_addr.to_le_bytes());
|
||||||
|
if let Some((_, corder_addr)) = order {
|
||||||
|
data.extend_from_slice(&corder_addr.to_le_bytes());
|
||||||
|
}
|
||||||
data
|
data
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1227,8 +1293,7 @@ pub(crate) fn build_vds_dataset_oh(
|
|||||||
dt: &Datatype,
|
dt: &Datatype,
|
||||||
ds: &Dataspace,
|
ds: &Dataspace,
|
||||||
global_heap_addr: u64,
|
global_heap_addr: u64,
|
||||||
attrs: &[AttributeMessage],
|
attrs: AttrStorage<'_>,
|
||||||
dense_blob: Option<&DenseAttrBlob>,
|
|
||||||
fill_message: &[u8],
|
fill_message: &[u8],
|
||||||
refcount: u32,
|
refcount: u32,
|
||||||
) -> Result<Vec<u8>, FormatError> {
|
) -> Result<Vec<u8>, FormatError> {
|
||||||
@@ -1243,13 +1308,7 @@ pub(crate) fn build_vds_dataset_oh(
|
|||||||
dl.extend_from_slice(&global_heap_addr.to_le_bytes());
|
dl.extend_from_slice(&global_heap_addr.to_le_bytes());
|
||||||
dl.extend_from_slice(&1u32.to_le_bytes()); // object index 1 in the collection
|
dl.extend_from_slice(&1u32.to_le_bytes()); // object index 1 in the collection
|
||||||
w.add_message(MessageType::DataLayout, dl);
|
w.add_message(MessageType::DataLayout, dl);
|
||||||
if let Some(blob) = dense_blob {
|
attrs.add_to(&mut w);
|
||||||
w.add_message(MessageType::AttributeInfo, blob.attr_info_message.clone());
|
|
||||||
} else {
|
|
||||||
for attr in attrs {
|
|
||||||
w.add_message(MessageType::Attribute, attr.serialize(LENGTH_SIZE));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
add_refcount(&mut w, refcount);
|
add_refcount(&mut w, refcount);
|
||||||
w.serialize()
|
w.serialize()
|
||||||
}
|
}
|
||||||
@@ -1284,7 +1343,8 @@ fn write_undef_offset(buf: &mut Vec<u8>, offset_size: u8) {
|
|||||||
pub struct FileWriter {
|
pub struct FileWriter {
|
||||||
/// The root group's contents (its name is unused).
|
/// The root group's contents (its name is unused).
|
||||||
root: GroupBuilder,
|
root: GroupBuilder,
|
||||||
/// Default for groups that do not call [`GroupBuilder::track_order`].
|
/// Default for groups and datasets that do not set their own
|
||||||
|
/// `track_order`.
|
||||||
track_order: bool,
|
track_order: bool,
|
||||||
/// Global alignment threshold: datasets with raw data >= this many bytes
|
/// Global alignment threshold: datasets with raw data >= this many bytes
|
||||||
/// will have their data aligned to `alignment_bytes`.
|
/// will have their data aligned to `alignment_bytes`.
|
||||||
@@ -1319,11 +1379,18 @@ struct DsFlat {
|
|||||||
virtual_sources: Option<Vec<VdsMapping>>,
|
virtual_sources: Option<Vec<VdsMapping>>,
|
||||||
/// Number of hard links to the dataset.
|
/// Number of hard links to the dataset.
|
||||||
refcount: u32,
|
refcount: u32,
|
||||||
|
/// Track (and index) attribute creation order.
|
||||||
|
track_order: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convert a DatasetBuilder into a DsFlat, handling VDS (which does not
|
/// Convert a DatasetBuilder into a DsFlat, handling VDS (which does not
|
||||||
/// require a `data` field).
|
/// require a `data` field).
|
||||||
fn flatten_ds(db: DatasetBuilder, refcount: u32) -> Result<DsFlat, FormatError> {
|
fn flatten_ds(
|
||||||
|
db: DatasetBuilder,
|
||||||
|
refcount: u32,
|
||||||
|
default_track_order: bool,
|
||||||
|
) -> Result<DsFlat, FormatError> {
|
||||||
|
let track_order = db.track_order.unwrap_or(default_track_order);
|
||||||
let dt = db.datatype.ok_or(FormatError::DatasetMissingData)?;
|
let dt = db.datatype.ok_or(FormatError::DatasetMissingData)?;
|
||||||
let shape = db.shape.ok_or(FormatError::DatasetMissingShape)?;
|
let shape = db.shape.ok_or(FormatError::DatasetMissingShape)?;
|
||||||
let is_vds = db.virtual_sources.is_some();
|
let is_vds = db.virtual_sources.is_some();
|
||||||
@@ -1373,6 +1440,7 @@ fn flatten_ds(db: DatasetBuilder, refcount: u32) -> Result<DsFlat, FormatError>
|
|||||||
alignment: db.alignment,
|
alignment: db.alignment,
|
||||||
virtual_sources: db.virtual_sources,
|
virtual_sources: db.virtual_sources,
|
||||||
refcount,
|
refcount,
|
||||||
|
track_order,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1429,10 +1497,12 @@ impl FileWriter {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Track (and index) link creation order in every group that does not
|
/// Track (and index) creation order — of links and attributes in every
|
||||||
/// set its own [`GroupBuilder::track_order`], the root included — as
|
/// group that does not set its own [`GroupBuilder::track_order`], the
|
||||||
/// h5py's `track_order=True`: libhdf5 then lists members in the order
|
/// root included, and of attributes on every dataset that does not set
|
||||||
/// they were added. Off by default (members are listed by name).
|
/// its own [`DatasetBuilder::track_order`] — as h5py's
|
||||||
|
/// `track_order=True`: libhdf5 then lists members and attributes in the
|
||||||
|
/// order they were added. Off by default (they are listed by name).
|
||||||
pub fn track_order(&mut self, track: bool) -> &mut Self {
|
pub fn track_order(&mut self, track: bool) -> &mut Self {
|
||||||
self.track_order = track;
|
self.track_order = track;
|
||||||
self
|
self
|
||||||
@@ -1503,7 +1573,7 @@ impl FileWriter {
|
|||||||
let all_ds: Vec<DsFlat> = tree
|
let all_ds: Vec<DsFlat> = tree
|
||||||
.datasets
|
.datasets
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(db, refcount)| flatten_ds(db, refcount))
|
.map(|(db, refcount)| flatten_ds(db, refcount, self.track_order))
|
||||||
.collect::<Result<_, _>>()?;
|
.collect::<Result<_, _>>()?;
|
||||||
let groups: Vec<GrpFlat> = tree
|
let groups: Vec<GrpFlat> = tree
|
||||||
.groups
|
.groups
|
||||||
@@ -1520,6 +1590,15 @@ impl FileWriter {
|
|||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
// Refuse up front what dense storage would refuse after the work.
|
||||||
|
let tracked = groups
|
||||||
|
.iter()
|
||||||
|
.map(|g| (g.track_order, g.attrs.len()))
|
||||||
|
.chain(all_ds.iter().map(|d| (d.track_order, d.attrs.len())));
|
||||||
|
for (track, n) in tracked {
|
||||||
|
check_tracked_attr_count(track, n)?;
|
||||||
|
}
|
||||||
|
|
||||||
// Every datatype must have an on-disk encoding before anything is laid
|
// Every datatype must have an on-disk encoding before anything is laid
|
||||||
// out: `Datatype::serialize` itself cannot report a failure.
|
// out: `Datatype::serialize` itself cannot report a failure.
|
||||||
let group_attrs = groups.iter().flat_map(|g| &g.attrs);
|
let group_attrs = groups.iter().flat_map(|g| &g.attrs);
|
||||||
@@ -1574,7 +1653,7 @@ impl FileWriter {
|
|||||||
.map(|(gi, g)| {
|
.map(|(gi, g)| {
|
||||||
let dummy_links = g.link_messages(&[], &[]);
|
let dummy_links = g.link_messages(&[], &[]);
|
||||||
let attr_blob = group_dense[gi]
|
let attr_blob = group_dense[gi]
|
||||||
.then(|| build_dense_attrs(&g.attrs, 0))
|
.then(|| build_dense_attrs(&g.attrs, 0, g.track_order))
|
||||||
.transpose()?;
|
.transpose()?;
|
||||||
let li = if group_links_dense[gi] {
|
let li = if group_links_dense[gi] {
|
||||||
serialize_link_info(
|
serialize_link_info(
|
||||||
@@ -1590,8 +1669,11 @@ impl FileWriter {
|
|||||||
&dummy_links,
|
&dummy_links,
|
||||||
&li,
|
&li,
|
||||||
group_links_dense[gi],
|
group_links_dense[gi],
|
||||||
&g.attrs,
|
AttrStorage {
|
||||||
attr_blob.as_ref(),
|
attrs: &g.attrs,
|
||||||
|
dense: attr_blob.as_ref(),
|
||||||
|
track_order: g.track_order,
|
||||||
|
},
|
||||||
g.refcount,
|
g.refcount,
|
||||||
)
|
)
|
||||||
.map(|oh| oh.len())
|
.map(|oh| oh.len())
|
||||||
@@ -1610,7 +1692,7 @@ impl FileWriter {
|
|||||||
let mut dummy_cursor = 0u64;
|
let mut dummy_cursor = 0u64;
|
||||||
for (i, d) in all_ds.iter().enumerate() {
|
for (i, d) in all_ds.iter().enumerate() {
|
||||||
let dense_blob = ds_dense[i]
|
let dense_blob = ds_dense[i]
|
||||||
.then(|| build_dense_attrs(&d.attrs, 0))
|
.then(|| build_dense_attrs(&d.attrs, 0, d.track_order))
|
||||||
.transpose()?;
|
.transpose()?;
|
||||||
if is_vds[i] {
|
if is_vds[i] {
|
||||||
// VDS: dummy OH with address 0 to get the OH size. The global
|
// VDS: dummy OH with address 0 to get the OH size. The global
|
||||||
@@ -1619,8 +1701,11 @@ impl FileWriter {
|
|||||||
&d.dt,
|
&d.dt,
|
||||||
&d.ds,
|
&d.ds,
|
||||||
0, // dummy address
|
0, // dummy address
|
||||||
&d.attrs,
|
AttrStorage {
|
||||||
dense_blob.as_ref(),
|
attrs: &d.attrs,
|
||||||
|
dense: dense_blob.as_ref(),
|
||||||
|
track_order: d.track_order,
|
||||||
|
},
|
||||||
&d.fill_message,
|
&d.fill_message,
|
||||||
d.refcount,
|
d.refcount,
|
||||||
)?;
|
)?;
|
||||||
@@ -1659,8 +1744,11 @@ impl FileWriter {
|
|||||||
&d.ds,
|
&d.ds,
|
||||||
&result.layout_message,
|
&result.layout_message,
|
||||||
result.pipeline_message.as_deref(),
|
result.pipeline_message.as_deref(),
|
||||||
&d.attrs,
|
AttrStorage {
|
||||||
dense_blob.as_ref(),
|
attrs: &d.attrs,
|
||||||
|
dense: dense_blob.as_ref(),
|
||||||
|
track_order: d.track_order,
|
||||||
|
},
|
||||||
&d.fill_message,
|
&d.fill_message,
|
||||||
d.refcount,
|
d.refcount,
|
||||||
)?;
|
)?;
|
||||||
@@ -1674,8 +1762,11 @@ impl FileWriter {
|
|||||||
&d.dt,
|
&d.dt,
|
||||||
&d.ds,
|
&d.ds,
|
||||||
&d.raw,
|
&d.raw,
|
||||||
&d.attrs,
|
AttrStorage {
|
||||||
dense_blob.as_ref(),
|
attrs: &d.attrs,
|
||||||
|
dense: dense_blob.as_ref(),
|
||||||
|
track_order: d.track_order,
|
||||||
|
},
|
||||||
&d.fill_message,
|
&d.fill_message,
|
||||||
d.refcount,
|
d.refcount,
|
||||||
)?;
|
)?;
|
||||||
@@ -1690,8 +1781,11 @@ impl FileWriter {
|
|||||||
&d.ds,
|
&d.ds,
|
||||||
0,
|
0,
|
||||||
d.raw.len() as u64,
|
d.raw.len() as u64,
|
||||||
&d.attrs,
|
AttrStorage {
|
||||||
dense_blob.as_ref(),
|
attrs: &d.attrs,
|
||||||
|
dense: dense_blob.as_ref(),
|
||||||
|
track_order: d.track_order,
|
||||||
|
},
|
||||||
&d.fill_message,
|
&d.fill_message,
|
||||||
d.refcount,
|
d.refcount,
|
||||||
)?;
|
)?;
|
||||||
@@ -1735,7 +1829,7 @@ impl FileWriter {
|
|||||||
group_link_blob_addrs.push(None);
|
group_link_blob_addrs.push(None);
|
||||||
}
|
}
|
||||||
if group_dense[gi] {
|
if group_dense[gi] {
|
||||||
let blob = build_dense_attrs(&g.attrs, cursor2 as u64)?;
|
let blob = build_dense_attrs(&g.attrs, cursor2 as u64, g.track_order)?;
|
||||||
cursor2 += blob.blob.len();
|
cursor2 += blob.blob.len();
|
||||||
group_dense_blobs.push(Some(blob));
|
group_dense_blobs.push(Some(blob));
|
||||||
} else {
|
} else {
|
||||||
@@ -1752,7 +1846,8 @@ impl FileWriter {
|
|||||||
let addr = cursor2 as u64;
|
let addr = cursor2 as u64;
|
||||||
cursor2 += sz;
|
cursor2 += sz;
|
||||||
if ds_dense[i] {
|
if ds_dense[i] {
|
||||||
let blob = build_dense_attrs(&all_ds[i].attrs, cursor2 as u64)?;
|
let blob =
|
||||||
|
build_dense_attrs(&all_ds[i].attrs, cursor2 as u64, all_ds[i].track_order)?;
|
||||||
cursor2 += blob.blob.len();
|
cursor2 += blob.blob.len();
|
||||||
ds_dense_blobs.push(Some(blob));
|
ds_dense_blobs.push(Some(blob));
|
||||||
} else {
|
} else {
|
||||||
@@ -1776,8 +1871,11 @@ impl FileWriter {
|
|||||||
&d.dt,
|
&d.dt,
|
||||||
&d.ds,
|
&d.ds,
|
||||||
heap_addr,
|
heap_addr,
|
||||||
&d.attrs,
|
AttrStorage {
|
||||||
ds_dense_blobs[i].as_ref(),
|
attrs: &d.attrs,
|
||||||
|
dense: ds_dense_blobs[i].as_ref(),
|
||||||
|
track_order: d.track_order,
|
||||||
|
},
|
||||||
&d.fill_message,
|
&d.fill_message,
|
||||||
d.refcount,
|
d.refcount,
|
||||||
)?;
|
)?;
|
||||||
@@ -1804,8 +1902,11 @@ impl FileWriter {
|
|||||||
&d.ds,
|
&d.ds,
|
||||||
&result.layout_message,
|
&result.layout_message,
|
||||||
result.pipeline_message.as_deref(),
|
result.pipeline_message.as_deref(),
|
||||||
&d.attrs,
|
AttrStorage {
|
||||||
ds_dense_blobs[i].as_ref(),
|
attrs: &d.attrs,
|
||||||
|
dense: ds_dense_blobs[i].as_ref(),
|
||||||
|
track_order: d.track_order,
|
||||||
|
},
|
||||||
&d.fill_message,
|
&d.fill_message,
|
||||||
d.refcount,
|
d.refcount,
|
||||||
)?;
|
)?;
|
||||||
@@ -1820,8 +1921,11 @@ impl FileWriter {
|
|||||||
&d.dt,
|
&d.dt,
|
||||||
&d.ds,
|
&d.ds,
|
||||||
&d.raw,
|
&d.raw,
|
||||||
&d.attrs,
|
AttrStorage {
|
||||||
ds_dense_blobs[i].as_ref(),
|
attrs: &d.attrs,
|
||||||
|
dense: ds_dense_blobs[i].as_ref(),
|
||||||
|
track_order: d.track_order,
|
||||||
|
},
|
||||||
&d.fill_message,
|
&d.fill_message,
|
||||||
d.refcount,
|
d.refcount,
|
||||||
)?;
|
)?;
|
||||||
@@ -1846,8 +1950,11 @@ impl FileWriter {
|
|||||||
&d.ds,
|
&d.ds,
|
||||||
cursor2 as u64,
|
cursor2 as u64,
|
||||||
d.raw.len() as u64,
|
d.raw.len() as u64,
|
||||||
&d.attrs,
|
AttrStorage {
|
||||||
ds_dense_blobs[i].as_ref(),
|
attrs: &d.attrs,
|
||||||
|
dense: ds_dense_blobs[i].as_ref(),
|
||||||
|
track_order: d.track_order,
|
||||||
|
},
|
||||||
&d.fill_message,
|
&d.fill_message,
|
||||||
d.refcount,
|
d.refcount,
|
||||||
)?;
|
)?;
|
||||||
@@ -1915,8 +2022,11 @@ impl FileWriter {
|
|||||||
&links,
|
&links,
|
||||||
&li,
|
&li,
|
||||||
link_blob.is_some(),
|
link_blob.is_some(),
|
||||||
&g.attrs,
|
AttrStorage {
|
||||||
group_dense_blobs[gi].as_ref(),
|
attrs: &g.attrs,
|
||||||
|
dense: group_dense_blobs[gi].as_ref(),
|
||||||
|
track_order: g.track_order,
|
||||||
|
},
|
||||||
g.refcount,
|
g.refcount,
|
||||||
)?;
|
)?;
|
||||||
debug_assert_eq!(oh.len(), group_oh_sizes[gi]);
|
debug_assert_eq!(oh.len(), group_oh_sizes[gi]);
|
||||||
@@ -2147,6 +2257,92 @@ mod tests {
|
|||||||
assert_eq!(read_dataset_f64(&bytes, "data"), vec![1.0, 2.0, 3.0]);
|
assert_eq!(read_dataset_f64(&bytes, "data"), vec![1.0, 2.0, 3.0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Attribute names of the object at `path`, in the order the reader
|
||||||
|
/// lists them.
|
||||||
|
fn attr_names(bytes: &[u8], path: &str) -> Vec<String> {
|
||||||
|
let sig = signature::find_signature(bytes).unwrap();
|
||||||
|
let sb = Superblock::parse(bytes, sig).unwrap();
|
||||||
|
let addr = if path == "/" {
|
||||||
|
sb.root_group_address
|
||||||
|
} else {
|
||||||
|
resolve_path_any(bytes, &sb, path).unwrap()
|
||||||
|
};
|
||||||
|
let hdr =
|
||||||
|
ObjectHeader::parse(bytes, addr as usize, sb.offset_size, sb.length_size).unwrap();
|
||||||
|
crate::attribute::extract_attributes_full(bytes, &hdr, sb.offset_size, sb.length_size)
|
||||||
|
.unwrap()
|
||||||
|
.into_iter()
|
||||||
|
.map(|a| a.name)
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tracked_attributes_are_read_in_creation_order() {
|
||||||
|
let set = |names: &[String]| -> Vec<(String, AttrValue)> {
|
||||||
|
names
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(i, n)| (n.clone(), AttrValue::I64(i as i64)))
|
||||||
|
.collect()
|
||||||
|
};
|
||||||
|
let compact: Vec<String> = ["zeta", "alpha", "mid"].map(String::from).to_vec();
|
||||||
|
let dense: Vec<String> = (0..30).rev().map(|i| format!("a{i:02}")).collect();
|
||||||
|
let mut fw = FileWriter::new();
|
||||||
|
fw.track_order(true);
|
||||||
|
for (n, v) in set(&compact) {
|
||||||
|
fw.set_root_attr(&n, v);
|
||||||
|
}
|
||||||
|
let ds = fw.create_dataset("dense");
|
||||||
|
ds.with_i32_data(&[1]);
|
||||||
|
for (n, v) in set(&dense) {
|
||||||
|
ds.set_attr(&n, v);
|
||||||
|
}
|
||||||
|
let ds = fw.create_dataset("untracked");
|
||||||
|
ds.with_i32_data(&[1]).track_order(false);
|
||||||
|
for (n, v) in set(&dense) {
|
||||||
|
ds.set_attr(&n, v);
|
||||||
|
}
|
||||||
|
let mut g = fw.create_group("g");
|
||||||
|
g.track_order(false);
|
||||||
|
for (n, v) in set(&compact) {
|
||||||
|
g.set_attr(&n, v);
|
||||||
|
}
|
||||||
|
fw.add_group(g.finish());
|
||||||
|
let bytes = fw.finish().unwrap();
|
||||||
|
assert_eq!(attr_names(&bytes, "/"), compact);
|
||||||
|
assert_eq!(attr_names(&bytes, "dense"), dense);
|
||||||
|
// Without tracking: storage order (inline: as added; dense: hash).
|
||||||
|
assert_eq!(attr_names(&bytes, "g"), compact);
|
||||||
|
let mut by_hash = dense.clone();
|
||||||
|
by_hash.sort_by_key(|n| crate::checksum::jenkins_lookup3(n.as_bytes()));
|
||||||
|
assert_eq!(attr_names(&bytes, "untracked"), by_hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn too_many_tracked_attributes_is_an_error() {
|
||||||
|
// libhdf5 numbers at most 65 535 attributes on an object that
|
||||||
|
// tracks their creation order (a 2-byte field). (`set_attr` looks
|
||||||
|
// for an earlier value, so 65 536 of them through the builder take
|
||||||
|
// a while; build the messages directly.)
|
||||||
|
let attrs: Vec<AttributeMessage> = (0..65_536)
|
||||||
|
.map(|i| build_attr_message(&format!("a{i}"), &AttrValue::I64(i)))
|
||||||
|
.collect();
|
||||||
|
let err = build_dense_attrs(&attrs, 0, true)
|
||||||
|
.err()
|
||||||
|
.unwrap()
|
||||||
|
.to_string();
|
||||||
|
assert!(err.contains("65536 attributes on one object"), "{err}");
|
||||||
|
assert!(build_dense_attrs(&attrs[1..], 0, true).is_ok());
|
||||||
|
assert!(build_dense_attrs(&attrs, 0, false).is_ok());
|
||||||
|
let mut fw = FileWriter::new();
|
||||||
|
let ds = fw.create_dataset("x");
|
||||||
|
ds.with_i32_data(&[1]).track_order(true);
|
||||||
|
for i in 0..20 {
|
||||||
|
ds.set_attr(&format!("a{i}"), AttrValue::I64(i));
|
||||||
|
}
|
||||||
|
assert!(fw.finish().is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn dense_attrs_root_group_self_roundtrip() {
|
fn dense_attrs_root_group_self_roundtrip() {
|
||||||
let mut fw = FileWriter::new();
|
let mut fw = FileWriter::new();
|
||||||
|
|||||||
@@ -12,7 +12,8 @@
|
|||||||
#[cfg(not(feature = "std"))]
|
#[cfg(not(feature = "std"))]
|
||||||
use alloc::{format, vec, vec::Vec};
|
use alloc::{format, vec, vec::Vec};
|
||||||
|
|
||||||
use crate::chunked_read::{alloc_output, checked_byte_len, list_chunks};
|
use crate::addr::to_usize;
|
||||||
|
use crate::chunked_read::{alloc_output, checked_byte_len, list_chunks_in};
|
||||||
use crate::data_layout::DataLayout;
|
use crate::data_layout::DataLayout;
|
||||||
use crate::dataspace::Dataspace;
|
use crate::dataspace::Dataspace;
|
||||||
use crate::error::FormatError;
|
use crate::error::FormatError;
|
||||||
@@ -116,9 +117,22 @@ pub fn dataset_fill_value_in(
|
|||||||
messages: &[HeaderMessage],
|
messages: &[HeaderMessage],
|
||||||
offset_size: u8,
|
offset_size: u8,
|
||||||
length_size: u8,
|
length_size: u8,
|
||||||
|
) -> Result<Option<Vec<u8>>, FormatError> {
|
||||||
|
dataset_fill_value_from_storage(&file_data, messages, offset_size, length_size)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`dataset_fill_value_in`] with the file behind any
|
||||||
|
/// [`Storage`](crate::storage::Storage) (a `&dyn Storage` too). (The trait
|
||||||
|
/// is not imported here: its `len` would shadow the slice method in this
|
||||||
|
/// module.)
|
||||||
|
pub fn dataset_fill_value_from_storage<S: crate::storage::Storage + ?Sized>(
|
||||||
|
file: &S,
|
||||||
|
messages: &[HeaderMessage],
|
||||||
|
offset_size: u8,
|
||||||
|
length_size: u8,
|
||||||
) -> Result<Option<Vec<u8>>, FormatError> {
|
) -> Result<Option<Vec<u8>>, FormatError> {
|
||||||
fill_value_from(messages, |msg| {
|
fill_value_from(messages, |msg| {
|
||||||
crate::shared_message::message_data_with_sohm(file_data, msg, offset_size, length_size)
|
crate::shared_message::message_data_with_sohm_in(file, msg, offset_size, length_size)
|
||||||
.map(|data| data.into_owned())
|
.map(|data| data.into_owned())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -199,6 +213,30 @@ pub fn read_full_with_fill<E: From<FormatError>>(
|
|||||||
offset_size: u8,
|
offset_size: u8,
|
||||||
length_size: u8,
|
length_size: u8,
|
||||||
read: impl FnOnce() -> Result<Vec<u8>, E>,
|
read: impl FnOnce() -> Result<Vec<u8>, E>,
|
||||||
|
) -> Result<Vec<u8>, E> {
|
||||||
|
read_full_with_fill_in(
|
||||||
|
messages,
|
||||||
|
file_data,
|
||||||
|
layout,
|
||||||
|
dataspace,
|
||||||
|
elem_size,
|
||||||
|
offset_size,
|
||||||
|
length_size,
|
||||||
|
read,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`read_full_with_fill`] over any [`Storage`](crate::storage::Storage).
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
pub fn read_full_with_fill_in<E: From<FormatError>, S: crate::storage::Storage + ?Sized>(
|
||||||
|
messages: &[HeaderMessage],
|
||||||
|
file_data: &S,
|
||||||
|
layout: &DataLayout,
|
||||||
|
dataspace: &Dataspace,
|
||||||
|
elem_size: usize,
|
||||||
|
offset_size: u8,
|
||||||
|
length_size: u8,
|
||||||
|
read: impl FnOnce() -> Result<Vec<u8>, E>,
|
||||||
) -> Result<Vec<u8>, E> {
|
) -> Result<Vec<u8>, E> {
|
||||||
// A dataset with external raw data also has no data address in this
|
// A dataset with external raw data also has no data address in this
|
||||||
// file. It is NOT unallocated — its values live elsewhere — so it must
|
// file. It is NOT unallocated — its values live elsewhere — so it must
|
||||||
@@ -209,12 +247,12 @@ pub fn read_full_with_fill<E: From<FormatError>>(
|
|||||||
{
|
{
|
||||||
return Err(FormatError::ExternalDataFilesUnsupported.into());
|
return Err(FormatError::ExternalDataFilesUnsupported.into());
|
||||||
}
|
}
|
||||||
let fill = dataset_fill_value_in(file_data, messages, offset_size, length_size)?;
|
let fill = dataset_fill_value_from_storage(file_data, messages, offset_size, length_size)?;
|
||||||
if !has_storage(layout) {
|
if !has_storage(layout) {
|
||||||
return Ok(filled_dataset(dataspace, elem_size, fill.as_deref())?);
|
return Ok(filled_dataset(dataspace, elem_size, fill.as_deref())?);
|
||||||
}
|
}
|
||||||
let mut output = read()?;
|
let mut output = read()?;
|
||||||
apply_to_unallocated_chunks(
|
apply_to_unallocated_chunks_in(
|
||||||
&mut output,
|
&mut output,
|
||||||
file_data,
|
file_data,
|
||||||
layout,
|
layout,
|
||||||
@@ -240,6 +278,30 @@ pub fn apply_to_unallocated_chunks(
|
|||||||
fill: Option<&[u8]>,
|
fill: Option<&[u8]>,
|
||||||
offset_size: u8,
|
offset_size: u8,
|
||||||
length_size: u8,
|
length_size: u8,
|
||||||
|
) -> Result<(), FormatError> {
|
||||||
|
apply_to_unallocated_chunks_in(
|
||||||
|
output,
|
||||||
|
file_data,
|
||||||
|
layout,
|
||||||
|
dataspace,
|
||||||
|
elem_size,
|
||||||
|
fill,
|
||||||
|
offset_size,
|
||||||
|
length_size,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`apply_to_unallocated_chunks`] over any [`Storage`](crate::storage::Storage).
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
pub fn apply_to_unallocated_chunks_in<S: crate::storage::Storage + ?Sized>(
|
||||||
|
output: &mut [u8],
|
||||||
|
file_data: &S,
|
||||||
|
layout: &DataLayout,
|
||||||
|
dataspace: &Dataspace,
|
||||||
|
elem_size: usize,
|
||||||
|
fill: Option<&[u8]>,
|
||||||
|
offset_size: u8,
|
||||||
|
length_size: u8,
|
||||||
) -> Result<(), FormatError> {
|
) -> Result<(), FormatError> {
|
||||||
let Some(fill) = fill.filter(|f| f.len() == elem_size && !is_default(Some(f))) else {
|
let Some(fill) = fill.filter(|f| f.len() == elem_size && !is_default(Some(f))) else {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
@@ -247,7 +309,7 @@ pub fn apply_to_unallocated_chunks(
|
|||||||
if !matches!(layout, DataLayout::Chunked { .. }) || elem_size == 0 {
|
if !matches!(layout, DataLayout::Chunked { .. }) || elem_size == 0 {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
let (chunks, chunk_dims) = list_chunks(
|
let (chunks, chunk_dims) = list_chunks_in(
|
||||||
file_data,
|
file_data,
|
||||||
layout,
|
layout,
|
||||||
dataspace,
|
dataspace,
|
||||||
@@ -256,7 +318,11 @@ pub fn apply_to_unallocated_chunks(
|
|||||||
length_size,
|
length_size,
|
||||||
)?;
|
)?;
|
||||||
let rank = chunk_dims.len();
|
let rank = chunk_dims.len();
|
||||||
let ds_dims: Vec<usize> = dataspace.dimensions.iter().map(|&d| d as usize).collect();
|
let ds_dims: Vec<usize> = dataspace
|
||||||
|
.dimensions
|
||||||
|
.iter()
|
||||||
|
.map(|&d| to_usize(d))
|
||||||
|
.collect::<Result<_, _>>()?;
|
||||||
if rank == 0 || ds_dims.len() != rank || chunk_dims.contains(&0) {
|
if rank == 0 || ds_dims.len() != rank || chunk_dims.contains(&0) {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
@@ -288,7 +354,7 @@ pub fn apply_to_unallocated_chunks(
|
|||||||
let mut cell = 0usize;
|
let mut cell = 0usize;
|
||||||
let mut in_range = true;
|
let mut in_range = true;
|
||||||
for d in 0..rank {
|
for d in 0..rank {
|
||||||
let coord = chunk.offsets[d] as usize / chunk_dims[d];
|
let coord = to_usize(chunk.offsets[d])? / chunk_dims[d];
|
||||||
if coord >= grid[d] {
|
if coord >= grid[d] {
|
||||||
in_range = false;
|
in_range = false;
|
||||||
break;
|
break;
|
||||||
@@ -439,4 +505,37 @@ mod tests {
|
|||||||
.collect();
|
.collect();
|
||||||
assert_eq!(filled, [2, 3, 7, 8]);
|
assert_eq!(filled, [2, 3, 7, 8]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Fill values, shared ones in the SOHM heap included, resolve
|
||||||
|
/// identically through a read_at-only CountingStorage.
|
||||||
|
#[test]
|
||||||
|
fn storage_reads_match_slice_reads() {
|
||||||
|
use crate::object_header::ObjectHeader;
|
||||||
|
use crate::storage::CountingStorage;
|
||||||
|
let file: &[u8] = include_bytes!("../tests/fixtures/shared_fill_value.h5");
|
||||||
|
let sb = crate::superblock::Superblock::parse(file, 0).unwrap();
|
||||||
|
let (os, ls) = (sb.offset_size, sb.length_size);
|
||||||
|
let storage = CountingStorage::new(file.to_vec());
|
||||||
|
let mut shared = 0;
|
||||||
|
let children =
|
||||||
|
crate::group_v2::resolve_group_children(file, &sb, sb.root_group_address).unwrap();
|
||||||
|
assert!(children.len() >= 3);
|
||||||
|
for child in children {
|
||||||
|
let h =
|
||||||
|
ObjectHeader::parse(file, child.object_header_address as usize, os, ls).unwrap();
|
||||||
|
shared += h
|
||||||
|
.messages
|
||||||
|
.iter()
|
||||||
|
.filter(|m| {
|
||||||
|
m.msg_type == MessageType::FillValue
|
||||||
|
&& crate::shared_message::is_shared(m.flags)
|
||||||
|
})
|
||||||
|
.count();
|
||||||
|
let want = dataset_fill_value_in(file, &h.messages, os, ls);
|
||||||
|
assert_eq!(want, Ok(Some((-7i32).to_le_bytes().to_vec())));
|
||||||
|
let got = dataset_fill_value_from_storage(&storage, &h.messages, os, ls);
|
||||||
|
assert_eq!(got, want, "{}", child.name);
|
||||||
|
}
|
||||||
|
assert!(shared >= 2);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,8 @@ pub const FILTER_LZF: u16 = 32000;
|
|||||||
pub const FILTER_BLOSC: u16 = 32001;
|
pub const FILTER_BLOSC: u16 = 32001;
|
||||||
/// Bitshuffle, optionally with LZ4 or Zstandard (hdf5plugin's `Bitshuffle`).
|
/// Bitshuffle, optionally with LZ4 or Zstandard (hdf5plugin's `Bitshuffle`).
|
||||||
pub const FILTER_BITSHUFFLE: u16 = 32008;
|
pub const FILTER_BITSHUFFLE: u16 = 32008;
|
||||||
/// ZFP lossy floating-point compression (hdf5plugin's `Zfp`). Not supported.
|
/// ZFP lossy (and lossless) compression of numeric arrays (H5Z-ZFP;
|
||||||
|
/// hdf5plugin's `Zfp`). Read-only, with the `zfp` feature.
|
||||||
pub const FILTER_ZFP: u16 = 32013;
|
pub const FILTER_ZFP: u16 = 32013;
|
||||||
/// Blosc 2 (hdf5plugin's `Blosc2`).
|
/// Blosc 2 (hdf5plugin's `Blosc2`).
|
||||||
pub const FILTER_BLOSC2: u16 = 32026;
|
pub const FILTER_BLOSC2: u16 = 32026;
|
||||||
|
|||||||
@@ -5,7 +5,8 @@
|
|||||||
//! * **Built-in filters** — a static table of the filters compiled into this
|
//! * **Built-in filters** — a static table of the filters compiled into this
|
||||||
//! build: the HDF5 standard filters (deflate, shuffle, Fletcher32, szip,
|
//! build: the HDF5 standard filters (deflate, shuffle, Fletcher32, szip,
|
||||||
//! N-Bit, scale-offset) and the plugin filters whose cargo features are
|
//! N-Bit, scale-offset) and the plugin filters whose cargo features are
|
||||||
//! enabled (LZ4, Zstandard, pcodec, LZF, bitshuffle, bzip2, blosc).
|
//! enabled (LZ4, Zstandard, pcodec, LZF, bitshuffle, bzip2, blosc,
|
||||||
|
//! blosc2, zfp).
|
||||||
//! [`builtin_filters`] lists them.
|
//! [`builtin_filters`] lists them.
|
||||||
//! * **Registered filters** (`std` only) — codecs the application supplies
|
//! * **Registered filters** (`std` only) — codecs the application supplies
|
||||||
//! for any other ID with [`register_filter`] (a [`FilterCodec`], or just a
|
//! for any other ID with [`register_filter`] (a [`FilterCodec`], or just a
|
||||||
@@ -161,12 +162,12 @@ pub fn known_filter(id: u16) -> Option<(&'static str, Option<&'static str>)> {
|
|||||||
32001 => ("Blosc", Some("blosc")),
|
32001 => ("Blosc", Some("blosc")),
|
||||||
32004 => ("LZ4", Some("lz4")),
|
32004 => ("LZ4", Some("lz4")),
|
||||||
32008 => ("bitshuffle", Some("bitshuffle")),
|
32008 => ("bitshuffle", Some("bitshuffle")),
|
||||||
32013 => ("ZFP", None),
|
32013 => ("ZFP", Some("zfp")),
|
||||||
32015 => ("Zstandard", Some("zstd")),
|
32015 => ("Zstandard", Some("zstd")),
|
||||||
32019 => ("JPEG", None),
|
32019 => ("JPEG", None),
|
||||||
32022 => ("BitGroom", None),
|
32022 => ("BitGroom", None),
|
||||||
32023 => ("Granular BitRound", None),
|
32023 => ("Granular BitRound", None),
|
||||||
32026 => ("Blosc2", None),
|
32026 => ("Blosc2", Some("blosc2")),
|
||||||
_ => return None,
|
_ => return None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -187,6 +188,22 @@ pub fn is_filter_available(id: u16) -> bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether chunks filtered with `id` may be decoded by a codec the
|
||||||
|
/// application registered (whose stored sizes this crate cannot bound).
|
||||||
|
pub(crate) fn may_be_registered(id: u16) -> bool {
|
||||||
|
if builtin_filter(id).is_some_and(|b| !b.is_shared()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
#[cfg(feature = "std")]
|
||||||
|
{
|
||||||
|
registered(id).is_some()
|
||||||
|
}
|
||||||
|
#[cfg(not(feature = "std"))]
|
||||||
|
{
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "std")]
|
#[cfg(feature = "std")]
|
||||||
mod custom {
|
mod custom {
|
||||||
use super::FilterCodec;
|
use super::FilterCodec;
|
||||||
@@ -452,12 +469,14 @@ pub(crate) mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn unsupported_filter_error_names_the_filter() {
|
fn unsupported_filter_error_names_the_filter() {
|
||||||
let msg = FormatError::UnsupportedFilter(32026).to_string();
|
let msg = FormatError::UnsupportedFilter(32026).to_string();
|
||||||
|
assert!(msg.contains("Blosc2") && msg.contains("`blosc2`"), "{msg}");
|
||||||
|
let msg = FormatError::UnsupportedFilter(32013).to_string();
|
||||||
|
assert!(msg.contains("ZFP") && msg.contains("`zfp`"), "{msg}");
|
||||||
|
let msg = FormatError::UnsupportedFilter(32019).to_string();
|
||||||
assert!(
|
assert!(
|
||||||
msg.contains("Blosc2") && msg.contains("not implemented"),
|
msg.contains("JPEG") && msg.contains("not implemented"),
|
||||||
"{msg}"
|
"{msg}"
|
||||||
);
|
);
|
||||||
let msg = FormatError::UnsupportedFilter(32013).to_string();
|
|
||||||
assert!(msg.contains("ZFP"), "{msg}");
|
|
||||||
let msg = FormatError::UnsupportedFilter(32000).to_string();
|
let msg = FormatError::UnsupportedFilter(32000).to_string();
|
||||||
assert!(msg.contains("LZF") && msg.contains("`lzf`"), "{msg}");
|
assert!(msg.contains("LZF") && msg.contains("`lzf`"), "{msg}");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -218,12 +218,20 @@ pub(crate) fn bitshuffle_decode(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Decode Zstandard frames into exactly `dst`, failing if they hold more.
|
/// Decode Zstandard frames into exactly `dst`, failing if they hold more.
|
||||||
|
///
|
||||||
|
/// ruzstd reserves a frame's declared window (by default up to 100 MiB)
|
||||||
|
/// before decoding it, so the window is capped at what the output could
|
||||||
|
/// need: twice `dst` (window sizes are rounded up), and at least 128 KiB.
|
||||||
|
/// The encoders behind these filters (c-blosc, c-blosc2, bitshuffle)
|
||||||
|
/// compress each block in one call with its size known, so libzstd's
|
||||||
|
/// window never exceeds the block.
|
||||||
#[cfg(any(feature = "bitshuffle", feature = "blosc"))]
|
#[cfg(any(feature = "bitshuffle", feature = "blosc"))]
|
||||||
pub(crate) fn zstd_decode_into(
|
pub(crate) fn zstd_decode_into(
|
||||||
decoder: &mut ruzstd::decoding::FrameDecoder,
|
decoder: &mut ruzstd::decoding::FrameDecoder,
|
||||||
frames: &[u8],
|
frames: &[u8],
|
||||||
dst: &mut [u8],
|
dst: &mut [u8],
|
||||||
) -> Result<usize, FormatError> {
|
) -> Result<usize, FormatError> {
|
||||||
|
decoder.set_max_window_size((2 * dst.len()).max(1 << 17) as u64);
|
||||||
decoder
|
decoder
|
||||||
.decode_all(frames, dst)
|
.decode_all(frames, dst)
|
||||||
.map_err(|e| FormatError::DecompressionError(format!("zstd: {e}")))
|
.map_err(|e| FormatError::DecompressionError(format!("zstd: {e}")))
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ fn le32(b: &[u8], at: usize) -> Result<usize, FormatError> {
|
|||||||
|
|
||||||
/// The codec inside a Blosc frame (flags bits 5-7).
|
/// The codec inside a Blosc frame (flags bits 5-7).
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
enum Codec {
|
pub(crate) enum Codec {
|
||||||
BloscLz,
|
BloscLz,
|
||||||
Lz4,
|
Lz4,
|
||||||
Snappy,
|
Snappy,
|
||||||
@@ -58,7 +58,7 @@ enum Codec {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Codec {
|
impl Codec {
|
||||||
fn from_flags(flags: u8) -> Result<Codec, FormatError> {
|
pub(crate) fn from_flags(flags: u8) -> Result<Codec, FormatError> {
|
||||||
match flags >> 5 {
|
match flags >> 5 {
|
||||||
0 => Ok(Codec::BloscLz),
|
0 => Ok(Codec::BloscLz),
|
||||||
1 => Ok(Codec::Lz4),
|
1 => Ok(Codec::Lz4),
|
||||||
@@ -71,7 +71,7 @@ impl Codec {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Decode one codec stream into exactly `dst`.
|
/// Decode one codec stream into exactly `dst`.
|
||||||
fn decode_stream(
|
pub(crate) fn decode_stream(
|
||||||
codec: Codec,
|
codec: Codec,
|
||||||
src: &[u8],
|
src: &[u8],
|
||||||
dst: &mut [u8],
|
dst: &mut [u8],
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -4,6 +4,7 @@
|
|||||||
//! the compression level). Decoded with the `bzip2` crate's default backend,
|
//! the compression level). Decoded with the `bzip2` crate's default backend,
|
||||||
//! `libbz2-rs-sys`, a pure-Rust port of libbzip2.
|
//! `libbz2-rs-sys`, a pure-Rust port of libbzip2.
|
||||||
|
|
||||||
|
use crate::addr::saturating_usize;
|
||||||
use crate::error::FormatError;
|
use crate::error::FormatError;
|
||||||
use crate::filter_registry::FilterContext;
|
use crate::filter_registry::FilterContext;
|
||||||
|
|
||||||
@@ -28,7 +29,7 @@ pub(crate) fn bzip2_decode(input: &[u8], ctx: &FilterContext<'_>) -> Result<Vec<
|
|||||||
loop {
|
loop {
|
||||||
let (in_before, out_before) = (dec.total_in(), dec.total_out());
|
let (in_before, out_before) = (dec.total_in(), dec.total_out());
|
||||||
let status = dec
|
let status = dec
|
||||||
.decompress_vec(&input[in_before as usize..], &mut out)
|
.decompress_vec(&input[saturating_usize(in_before)..], &mut out)
|
||||||
.map_err(|e| err(&e.to_string()))?;
|
.map_err(|e| err(&e.to_string()))?;
|
||||||
if out.len() > limit {
|
if out.len() > limit {
|
||||||
return Err(err("output exceeds the chunk size"));
|
return Err(err("output exceeds the chunk size"));
|
||||||
@@ -43,7 +44,7 @@ pub(crate) fn bzip2_decode(input: &[u8], ctx: &FilterContext<'_>) -> Result<Vec<
|
|||||||
.max(1);
|
.max(1);
|
||||||
out.try_reserve_exact(grow)
|
out.try_reserve_exact(grow)
|
||||||
.map_err(|_| err("cannot allocate the output buffer"))?;
|
.map_err(|_| err("cannot allocate the output buffer"))?;
|
||||||
} else if dec.total_in() as usize >= input.len()
|
} else if saturating_usize(dec.total_in()) >= input.len()
|
||||||
|| (dec.total_in(), dec.total_out()) == (in_before, out_before)
|
|| (dec.total_in(), dec.total_out()) == (in_before, out_before)
|
||||||
{
|
{
|
||||||
return Err(err("truncated stream"));
|
return Err(err("truncated stream"));
|
||||||
@@ -61,7 +62,7 @@ pub(crate) fn bzip2_encode(input: &[u8], ctx: &FilterContext<'_>) -> Result<Vec<
|
|||||||
// bzip2's worst case is about 1% + 600 bytes over the input.
|
// bzip2's worst case is about 1% + 600 bytes over the input.
|
||||||
let mut out = Vec::with_capacity(input.len() + input.len() / 100 + 600);
|
let mut out = Vec::with_capacity(input.len() + input.len() / 100 + 600);
|
||||||
loop {
|
loop {
|
||||||
let consumed = enc.total_in() as usize;
|
let consumed = saturating_usize(enc.total_in());
|
||||||
let status = enc
|
let status = enc
|
||||||
.compress_vec(&input[consumed..], &mut out, Action::Finish)
|
.compress_vec(&input[consumed..], &mut out, Action::Finish)
|
||||||
.map_err(|e| cerr(e.to_string()))?;
|
.map_err(|e| cerr(e.to_string()))?;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -6,19 +6,23 @@ extern crate alloc;
|
|||||||
#[cfg(not(feature = "std"))]
|
#[cfg(not(feature = "std"))]
|
||||||
use alloc::{format, vec, vec::Vec};
|
use alloc::{format, vec, vec::Vec};
|
||||||
|
|
||||||
|
use crate::addr::to_usize;
|
||||||
use crate::chunk_grid::ChunkGrid;
|
use crate::chunk_grid::ChunkGrid;
|
||||||
use crate::chunked_read::ChunkInfo;
|
use crate::chunked_read::ChunkInfo;
|
||||||
use crate::error::FormatError;
|
use crate::error::FormatError;
|
||||||
|
use crate::storage::{PAGED_BLOCK_ONE_READ_MAX, Storage, Window, len_usize, read_exact_at};
|
||||||
|
|
||||||
/// Verify the Jenkins lookup3 checksum stored immediately after
|
/// Verify the Jenkins lookup3 checksum stored immediately after
|
||||||
/// `data[start..end]`, as every Fixed Array structure carries one.
|
/// `data[start..end]`, as every Fixed Array structure carries one. `w` is
|
||||||
|
/// a window of the file and `start`/`end` are relative to it.
|
||||||
///
|
///
|
||||||
/// A corrupt chunk index silently yields addresses pointing at the wrong
|
/// A corrupt chunk index silently yields addresses pointing at the wrong
|
||||||
/// bytes, so a mismatch has to be an error rather than a shrug: without this
|
/// bytes, so a mismatch has to be an error rather than a shrug: without this
|
||||||
/// the damage surfaces as plausible-looking data from the wrong chunk.
|
/// the damage surfaces as plausible-looking data from the wrong chunk.
|
||||||
#[cfg(feature = "checksum")]
|
#[cfg(feature = "checksum")]
|
||||||
fn verify_checksum(data: &[u8], start: usize, end: usize) -> Result<(), FormatError> {
|
fn verify_checksum(w: &Window<'_>, start: usize, end: usize) -> Result<(), FormatError> {
|
||||||
ensure_len(data, end, 4)?;
|
w.ensure(end, 4)?;
|
||||||
|
let data = &w.bytes;
|
||||||
let stored = u32::from_le_bytes([data[end], data[end + 1], data[end + 2], data[end + 3]]);
|
let stored = u32::from_le_bytes([data[end], data[end + 1], data[end + 2], data[end + 3]]);
|
||||||
let computed = crate::checksum::jenkins_lookup3(&data[start..end]);
|
let computed = crate::checksum::jenkins_lookup3(&data[start..end]);
|
||||||
if computed != stored {
|
if computed != stored {
|
||||||
@@ -31,7 +35,7 @@ fn verify_checksum(data: &[u8], start: usize, end: usize) -> Result<(), FormatEr
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(feature = "checksum"))]
|
#[cfg(not(feature = "checksum"))]
|
||||||
fn verify_checksum(_data: &[u8], _start: usize, _end: usize) -> Result<(), FormatError> {
|
fn verify_checksum(_w: &Window<'_>, _start: usize, _end: usize) -> Result<(), FormatError> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,19 +77,6 @@ fn read_length(data: &[u8], pos: usize, size: u8) -> Result<u64, FormatError> {
|
|||||||
read_offset(data, pos, size)
|
read_offset(data, pos, size)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ensure_len(data: &[u8], offset: usize, needed: usize) -> Result<(), FormatError> {
|
|
||||||
if offset
|
|
||||||
.checked_add(needed)
|
|
||||||
.is_none_or(|end| end > data.len())
|
|
||||||
{
|
|
||||||
return Err(FormatError::UnexpectedEof {
|
|
||||||
expected: offset.saturating_add(needed),
|
|
||||||
available: data.len(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_undefined(data: &[u8], pos: usize, size: u8) -> bool {
|
fn is_undefined(data: &[u8], pos: usize, size: u8) -> bool {
|
||||||
let s = size as usize;
|
let s = size as usize;
|
||||||
if pos + s > data.len() {
|
if pos + s > data.len() {
|
||||||
@@ -101,13 +92,24 @@ impl FixedArrayHeader {
|
|||||||
offset: usize,
|
offset: usize,
|
||||||
offset_size: u8,
|
offset_size: u8,
|
||||||
length_size: u8,
|
length_size: u8,
|
||||||
|
) -> Result<Self, FormatError> {
|
||||||
|
Self::parse_in(file_data, offset as u64, offset_size, length_size)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`Self::parse`] over any [`Storage`]: one read of the header.
|
||||||
|
pub fn parse_in<S: Storage + ?Sized>(
|
||||||
|
file: &S,
|
||||||
|
offset: u64,
|
||||||
|
offset_size: u8,
|
||||||
|
length_size: u8,
|
||||||
) -> Result<Self, FormatError> {
|
) -> Result<Self, FormatError> {
|
||||||
// FAHD signature(4) + version(1) + client_id(1) + element_size(1) +
|
// FAHD signature(4) + version(1) + client_id(1) + element_size(1) +
|
||||||
// max_nelmts_bits(1) + num_elements(length_size) + data_block_addr(offset_size) + checksum(4)
|
// max_nelmts_bits(1) + num_elements(length_size) + data_block_addr(offset_size) + checksum(4)
|
||||||
let min_size = 4 + 1 + 1 + 1 + 1 + length_size as usize + offset_size as usize + 4;
|
let min_size = 4 + 1 + 1 + 1 + 1 + length_size as usize + offset_size as usize + 4;
|
||||||
ensure_len(file_data, offset, min_size)?;
|
let w = Window::read(file, offset, min_size)?;
|
||||||
|
w.ensure(0, min_size)?;
|
||||||
|
|
||||||
let d = &file_data[offset..];
|
let d: &[u8] = &w.bytes;
|
||||||
if &d[0..4] != b"FAHD" {
|
if &d[0..4] != b"FAHD" {
|
||||||
return Err(FormatError::ChunkedReadError(
|
return Err(FormatError::ChunkedReadError(
|
||||||
"invalid Fixed Array header signature".into(),
|
"invalid Fixed Array header signature".into(),
|
||||||
@@ -130,7 +132,7 @@ impl FixedArrayHeader {
|
|||||||
pos += length_size as usize;
|
pos += length_size as usize;
|
||||||
let data_block_address = read_offset(d, pos, offset_size)?;
|
let data_block_address = read_offset(d, pos, offset_size)?;
|
||||||
pos += offset_size as usize;
|
pos += offset_size as usize;
|
||||||
verify_checksum(file_data, offset, offset + pos)?;
|
verify_checksum(&w, 0, pos)?;
|
||||||
|
|
||||||
Ok(FixedArrayHeader {
|
Ok(FixedArrayHeader {
|
||||||
client_id,
|
client_id,
|
||||||
@@ -156,15 +158,39 @@ pub fn read_fixed_array_chunks(
|
|||||||
chunk_dimensions: &[u32],
|
chunk_dimensions: &[u32],
|
||||||
element_size: u32,
|
element_size: u32,
|
||||||
offset_size: u8,
|
offset_size: u8,
|
||||||
|
length_size: u8,
|
||||||
|
) -> Result<Vec<ChunkInfo>, FormatError> {
|
||||||
|
read_fixed_array_chunks_in(
|
||||||
|
&file_data,
|
||||||
|
header,
|
||||||
|
dataset_dims,
|
||||||
|
max_dims,
|
||||||
|
chunk_dimensions,
|
||||||
|
element_size,
|
||||||
|
offset_size,
|
||||||
|
length_size,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`read_fixed_array_chunks`] over any [`Storage`]: one read of the data
|
||||||
|
/// block's prefix, one of the whole data block (pages included).
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
pub fn read_fixed_array_chunks_in<S: Storage + ?Sized>(
|
||||||
|
file: &S,
|
||||||
|
header: &FixedArrayHeader,
|
||||||
|
dataset_dims: &[u64],
|
||||||
|
max_dims: Option<&[u64]>,
|
||||||
|
chunk_dimensions: &[u32],
|
||||||
|
element_size: u32,
|
||||||
|
offset_size: u8,
|
||||||
_length_size: u8,
|
_length_size: u8,
|
||||||
) -> Result<Vec<ChunkInfo>, FormatError> {
|
) -> Result<Vec<ChunkInfo>, FormatError> {
|
||||||
let db_offset = header.data_block_address as usize;
|
let file_len = len_usize(file);
|
||||||
|
let db_offset = to_usize(header.data_block_address)?;
|
||||||
|
|
||||||
// Parse data block header: FADB(4) + version(1) + client_id(1) + header_address(offset_size)
|
// Parse data block header: FADB(4) + version(1) + client_id(1) + header_address(offset_size)
|
||||||
let db_header_size = 4 + 1 + 1 + offset_size as usize;
|
let db_header_size = 4 + 1 + 1 + offset_size as usize;
|
||||||
ensure_len(file_data, db_offset, db_header_size)?;
|
let d = read_exact_at(file, db_offset as u64, db_header_size)?;
|
||||||
|
|
||||||
let d = &file_data[db_offset..];
|
|
||||||
if &d[0..4] != b"FADB" {
|
if &d[0..4] != b"FADB" {
|
||||||
return Err(FormatError::ChunkedReadError(
|
return Err(FormatError::ChunkedReadError(
|
||||||
"invalid Fixed Array data block signature".into(),
|
"invalid Fixed Array data block signature".into(),
|
||||||
@@ -174,11 +200,11 @@ pub fn read_fixed_array_chunks(
|
|||||||
// Elements start immediately after the data block prefix.
|
// Elements start immediately after the data block prefix.
|
||||||
let elements_start = db_offset + db_header_size;
|
let elements_start = db_offset + db_header_size;
|
||||||
|
|
||||||
let num_elements = header.num_elements as usize;
|
let num_elements = to_usize(header.num_elements)?;
|
||||||
// A chunk index cannot describe more elements than the file has bytes (each
|
// A chunk index cannot describe more elements than the file has bytes (each
|
||||||
// element occupies at least `offset_size` bytes). Reject a corrupt count
|
// element occupies at least `offset_size` bytes). Reject a corrupt count
|
||||||
// before it can drive a huge loop or overflow an offset computation.
|
// before it can drive a huge loop or overflow an offset computation.
|
||||||
if num_elements > file_data.len() {
|
if num_elements > file_len {
|
||||||
return Err(FormatError::ChunkedReadError(
|
return Err(FormatError::ChunkedReadError(
|
||||||
"Fixed Array element count exceeds file size".into(),
|
"Fixed Array element count exceeds file size".into(),
|
||||||
));
|
));
|
||||||
@@ -208,11 +234,15 @@ pub fn read_fixed_array_chunks(
|
|||||||
chunk_dimensions.iter().map(|&d| d as u64).product::<u64>() * element_size as u64;
|
chunk_dimensions.iter().map(|&d| d as u64).product::<u64>() * element_size as u64;
|
||||||
|
|
||||||
let mut chunks = Vec::new();
|
let mut chunks = Vec::new();
|
||||||
let push_element =
|
// `rel` is relative to the data block, whose bytes are in `w`.
|
||||||
|i: usize, abs: usize, chunks: &mut Vec<ChunkInfo>| -> Result<(), FormatError> {
|
let push_element = |w: &Window<'_>,
|
||||||
|
i: usize,
|
||||||
|
rel: usize,
|
||||||
|
chunks: &mut Vec<ChunkInfo>|
|
||||||
|
-> Result<(), FormatError> {
|
||||||
if let Some((address, chunk_size, filter_mask)) = parse_fa_element(
|
if let Some((address, chunk_size, filter_mask)) = parse_fa_element(
|
||||||
file_data,
|
w,
|
||||||
abs,
|
rel,
|
||||||
header.client_id,
|
header.client_id,
|
||||||
offset_size,
|
offset_size,
|
||||||
header.element_size,
|
header.element_size,
|
||||||
@@ -246,10 +276,16 @@ pub fn read_fixed_array_chunks(
|
|||||||
|
|
||||||
if !is_paged {
|
if !is_paged {
|
||||||
// Non-paged: prefix, then `num_elements` elements packed directly,
|
// Non-paged: prefix, then `num_elements` elements packed directly,
|
||||||
// then a checksum over both.
|
// then a checksum over both. One window holds all of it (or ends at
|
||||||
verify_checksum(file_data, db_offset, elem_at(elements_start, num_elements)?)?;
|
// the end of the file), so its bounds checks are the whole-file ones.
|
||||||
|
let end = elem_at(elements_start, num_elements)?;
|
||||||
|
// The checksum's bounds check comes first: make it before reading.
|
||||||
|
#[cfg(feature = "checksum")]
|
||||||
|
Window::check_extent(file, db_offset as u64, end - db_offset, 4)?;
|
||||||
|
let w = Window::read(file, db_offset as u64, end.saturating_add(4) - db_offset)?;
|
||||||
|
verify_checksum(&w, 0, end - db_offset)?;
|
||||||
for i in 0..num_elements {
|
for i in 0..num_elements {
|
||||||
push_element(i, elem_at(elements_start, i)?, &mut chunks)?;
|
push_element(&w, i, elem_at(elements_start, i)? - db_offset, &mut chunks)?;
|
||||||
}
|
}
|
||||||
return Ok(chunks);
|
return Ok(chunks);
|
||||||
}
|
}
|
||||||
@@ -272,22 +308,40 @@ pub fn read_fixed_array_chunks(
|
|||||||
.and_then(|x| x.checked_add(4))
|
.and_then(|x| x.checked_add(4))
|
||||||
.ok_or_else(stride_overflow)?;
|
.ok_or_else(stride_overflow)?;
|
||||||
|
|
||||||
if bitmap_start + bitmap_size > file_data.len() {
|
if bitmap_start + bitmap_size > file_len {
|
||||||
return Err(FormatError::UnexpectedEof {
|
return Err(FormatError::UnexpectedEof {
|
||||||
expected: bitmap_start + bitmap_size,
|
expected: bitmap_start + bitmap_size,
|
||||||
available: file_data.len(),
|
available: file_len,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
// The whole data block in one window when it is small: every page slot
|
||||||
|
// is at most `page_stride` bytes, so every position checked below lies
|
||||||
|
// inside it (or past the end of the file). A larger block is read as its
|
||||||
|
// prefix and bitmap, then each page in use on its own.
|
||||||
|
let block_len = (pages_start - db_offset).saturating_add(npages.saturating_mul(page_stride));
|
||||||
|
let whole = if block_len <= PAGED_BLOCK_ONE_READ_MAX {
|
||||||
|
Some(Window::read(file, db_offset as u64, block_len)?)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
let head_w;
|
||||||
|
let head = match &whole {
|
||||||
|
Some(w) => w,
|
||||||
|
None => {
|
||||||
|
head_w = Window::read(file, db_offset as u64, pages_start - db_offset)?;
|
||||||
|
&head_w
|
||||||
|
}
|
||||||
|
};
|
||||||
// The prefix and page bitmap are covered by their own checksum, and each
|
// The prefix and page bitmap are covered by their own checksum, and each
|
||||||
// initialised page by one of its own.
|
// initialised page by one of its own.
|
||||||
verify_checksum(file_data, db_offset, bitmap_start + bitmap_size)?;
|
verify_checksum(head, 0, bitmap_start + bitmap_size - db_offset)?;
|
||||||
|
|
||||||
for p in 0..npages {
|
for p in 0..npages {
|
||||||
let page_first = p * page_nelmts; // < num_elements, cannot overflow
|
let page_first = p * page_nelmts; // < num_elements, cannot overflow
|
||||||
let page_count = core::cmp::min(page_nelmts, num_elements - page_first);
|
let page_count = core::cmp::min(page_nelmts, num_elements - page_first);
|
||||||
|
|
||||||
// Check the page-init bit (MSB-first within each byte).
|
// Check the page-init bit (MSB-first within each byte).
|
||||||
let bit_byte = file_data[bitmap_start + p / 8];
|
let bit_byte = head.bytes[bitmap_start + p / 8 - db_offset];
|
||||||
let bit_mask = 1u8 << (7 - (p % 8));
|
let bit_mask = 1u8 << (7 - (p % 8));
|
||||||
if bit_byte & bit_mask == 0 {
|
if bit_byte & bit_mask == 0 {
|
||||||
continue; // entire page unallocated
|
continue; // entire page unallocated
|
||||||
@@ -297,21 +351,33 @@ pub fn read_fixed_array_chunks(
|
|||||||
.checked_mul(page_stride)
|
.checked_mul(page_stride)
|
||||||
.and_then(|o| pages_start.checked_add(o))
|
.and_then(|o| pages_start.checked_add(o))
|
||||||
.ok_or_else(stride_overflow)?;
|
.ok_or_else(stride_overflow)?;
|
||||||
verify_checksum(file_data, page_off, elem_at(page_off, page_count)?)?;
|
let page_end = elem_at(page_off, page_count)?;
|
||||||
|
// `w` holds the page from `base` on (positions below are relative
|
||||||
|
// to it).
|
||||||
|
let page_w;
|
||||||
|
let (w, base) = match &whole {
|
||||||
|
Some(w) => (w, db_offset),
|
||||||
|
None => {
|
||||||
|
page_w =
|
||||||
|
Window::read(file, page_off as u64, page_end.saturating_add(4) - page_off)?;
|
||||||
|
(&page_w, page_off)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
verify_checksum(w, page_off - base, page_end - base)?;
|
||||||
for e in 0..page_count {
|
for e in 0..page_count {
|
||||||
push_element(page_first + e, elem_at(page_off, e)?, &mut chunks)?;
|
push_element(w, page_first + e, elem_at(page_off, e)? - base, &mut chunks)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(chunks)
|
Ok(chunks)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse a single Fixed Array element at absolute file offset `abs`.
|
/// Parse a single Fixed Array element at offset `abs` of the window `w`.
|
||||||
///
|
///
|
||||||
/// Returns `Some((address, chunk_size, filter_mask))` for an allocated chunk, or
|
/// Returns `Some((address, chunk_size, filter_mask))` for an allocated chunk, or
|
||||||
/// `None` if the element is undefined (an unallocated chunk, address all-`0xFF`).
|
/// `None` if the element is undefined (an unallocated chunk, address all-`0xFF`).
|
||||||
fn parse_fa_element(
|
fn parse_fa_element(
|
||||||
file_data: &[u8],
|
w: &Window<'_>,
|
||||||
abs: usize,
|
abs: usize,
|
||||||
client_id: u8,
|
client_id: u8,
|
||||||
offset_size: u8,
|
offset_size: u8,
|
||||||
@@ -321,12 +387,8 @@ fn parse_fa_element(
|
|||||||
let os = offset_size as usize;
|
let os = offset_size as usize;
|
||||||
if client_id == 0 {
|
if client_id == 0 {
|
||||||
// Non-filtered: element is just the chunk address.
|
// Non-filtered: element is just the chunk address.
|
||||||
if abs + os > file_data.len() {
|
w.ensure(abs, os)?;
|
||||||
return Err(FormatError::UnexpectedEof {
|
let file_data: &[u8] = &w.bytes;
|
||||||
expected: abs + os,
|
|
||||||
available: file_data.len(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if is_undefined(file_data, abs, offset_size) {
|
if is_undefined(file_data, abs, offset_size) {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
@@ -341,17 +403,14 @@ fn parse_fa_element(
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
let chunk_size_bytes = es - os - 4;
|
let chunk_size_bytes = es - os - 4;
|
||||||
if abs + es > file_data.len() {
|
w.ensure(abs, es)?;
|
||||||
return Err(FormatError::UnexpectedEof {
|
let file_data: &[u8] = &w.bytes;
|
||||||
expected: abs + es,
|
|
||||||
available: file_data.len(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if is_undefined(file_data, abs, offset_size) {
|
if is_undefined(file_data, abs, offset_size) {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
let address = read_offset(file_data, abs, offset_size)?;
|
let address = read_offset(file_data, abs, offset_size)?;
|
||||||
let chunk_size = read_variable_length(&file_data[abs + os..], chunk_size_bytes)?;
|
let chunk_size =
|
||||||
|
read_variable_length(&file_data[abs + os..abs + es - 4], chunk_size_bytes)?;
|
||||||
let fm_off = abs + os + chunk_size_bytes;
|
let fm_off = abs + os + chunk_size_bytes;
|
||||||
let filter_mask = u32::from_le_bytes([
|
let filter_mask = u32::from_le_bytes([
|
||||||
file_data[fm_off],
|
file_data[fm_off],
|
||||||
@@ -813,4 +872,126 @@ mod tests {
|
|||||||
.collect();
|
.collect();
|
||||||
assert_eq!(got, expect);
|
assert_eq!(got, expect);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A fixed array (header at 0x100, data block at 0x200) of `n` chunks,
|
||||||
|
/// filtered or not, paged when `n` exceeds `1 << page_bits`; every
|
||||||
|
/// page initialised except page 1.
|
||||||
|
fn build_fixed_array(n: usize, filtered: bool, page_bits: u8) -> Vec<u8> {
|
||||||
|
let os = 8usize;
|
||||||
|
let es = if filtered { os + 4 + 4 } else { os };
|
||||||
|
let (fahd, db) = (0x100usize, 0x200usize);
|
||||||
|
let mut f = vec![0u8; 0x2000];
|
||||||
|
f[fahd..fahd + 4].copy_from_slice(b"FAHD");
|
||||||
|
f[fahd + 5] = u8::from(filtered);
|
||||||
|
f[fahd + 6] = es as u8;
|
||||||
|
f[fahd + 7] = page_bits;
|
||||||
|
f[fahd + 8..fahd + 16].copy_from_slice(&(n as u64).to_le_bytes());
|
||||||
|
f[fahd + 16..fahd + 24].copy_from_slice(&(db as u64).to_le_bytes());
|
||||||
|
stamp_checksum(&mut f, fahd, fahd + 24);
|
||||||
|
f[db..db + 4].copy_from_slice(b"FADB");
|
||||||
|
f[db + 5] = u8::from(filtered);
|
||||||
|
f[db + 6..db + 14].copy_from_slice(&(fahd as u64).to_le_bytes());
|
||||||
|
let elems = db + 6 + os;
|
||||||
|
let write = |f: &mut Vec<u8>, at: usize, i: usize| {
|
||||||
|
let addr = if i == 2 {
|
||||||
|
u64::MAX
|
||||||
|
} else {
|
||||||
|
0x1000 + i as u64 * 0x100
|
||||||
|
};
|
||||||
|
f[at..at + os].copy_from_slice(&addr.to_le_bytes());
|
||||||
|
if filtered {
|
||||||
|
f[at + os..at + os + 4].copy_from_slice(&(100 + i as u32).to_le_bytes());
|
||||||
|
f[at + os + 4..at + os + 8].copy_from_slice(&(i as u32 & 1).to_le_bytes());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let page = 1usize << page_bits;
|
||||||
|
if n <= page {
|
||||||
|
for i in 0..n {
|
||||||
|
write(&mut f, elems + i * es, i);
|
||||||
|
}
|
||||||
|
stamp_checksum(&mut f, db, elems + n * es);
|
||||||
|
} else {
|
||||||
|
let npages = n.div_ceil(page);
|
||||||
|
let bitmap = npages.div_ceil(8);
|
||||||
|
for p in 0..npages {
|
||||||
|
if p != 1 {
|
||||||
|
f[elems + p / 8] |= 0x80 >> (p % 8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stamp_checksum(&mut f, db, elems + bitmap);
|
||||||
|
let pages_start = elems + bitmap + 4;
|
||||||
|
for p in (0..npages).filter(|&p| p != 1) {
|
||||||
|
let at = pages_start + p * (page * es + 4);
|
||||||
|
let count = page.min(n - p * page);
|
||||||
|
for e in 0..count {
|
||||||
|
write(&mut f, at + e * es, p * page + e);
|
||||||
|
}
|
||||||
|
stamp_checksum(&mut f, at, at + count * es);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
f
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Non-paged and paged, filtered and unfiltered arrays, cut at every
|
||||||
|
/// length through the data block and with a damaged byte, read
|
||||||
|
/// identically through a `read_at`-only storage.
|
||||||
|
#[test]
|
||||||
|
fn storage_reads_match_slice_reads() {
|
||||||
|
use crate::storage::CountingStorage;
|
||||||
|
for (n, filtered, bits) in [(3, false, 10), (3, true, 10), (11, false, 2), (11, true, 2)] {
|
||||||
|
let full = build_fixed_array(n, filtered, bits);
|
||||||
|
let es = if filtered { 16 } else { 8 };
|
||||||
|
let dims = [n as u64 * 20];
|
||||||
|
let h = FixedArrayHeader::parse(&full, 0x100, 8, 8).unwrap();
|
||||||
|
let chunks = read_fixed_array_chunks(&full, &h, &dims, None, &[20], 8, 8, 8).unwrap();
|
||||||
|
// Chunk 2 is unallocated, and so is page 1 of a paged array.
|
||||||
|
let expect = if n > 4 { n - 1 - 4 } else { n - 1 };
|
||||||
|
assert_eq!(chunks.len(), expect);
|
||||||
|
let mut files = Vec::new();
|
||||||
|
for cut in (0x100..0x200 + 40 + n * (es + 4) + 16).step_by(3) {
|
||||||
|
files.push(full[..cut].to_vec());
|
||||||
|
}
|
||||||
|
for at in [0x104, 0x210, 0x21a, 0x230] {
|
||||||
|
let mut damaged = full.clone();
|
||||||
|
damaged[at] ^= 1;
|
||||||
|
files.push(damaged);
|
||||||
|
}
|
||||||
|
files.push(full);
|
||||||
|
for f in files {
|
||||||
|
let storage = CountingStorage::new(f.clone());
|
||||||
|
let want = FixedArrayHeader::parse(&f, 0x100, 8, 8);
|
||||||
|
let got = FixedArrayHeader::parse_in(&storage, 0x100, 8, 8);
|
||||||
|
assert_eq!(format!("{got:?}"), format!("{want:?}"));
|
||||||
|
let Ok(h) = want else { continue };
|
||||||
|
let want = read_fixed_array_chunks(&f, &h, &dims, None, &[20], 8, 8, 8);
|
||||||
|
let got = read_fixed_array_chunks_in(&storage, &h, &dims, None, &[20], 8, 8, 8);
|
||||||
|
assert_eq!(format!("{got:?}"), format!("{want:?}"), "{} bytes", f.len());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A header whose element count stretches its data block (one checksum
|
||||||
|
/// over the whole block) far past the end of a 16 MiB file: the
|
||||||
|
/// checksum's bounds check fails before the block is read, with the
|
||||||
|
/// slice read's error.
|
||||||
|
#[cfg(feature = "checksum")]
|
||||||
|
#[test]
|
||||||
|
fn oversized_block_fails_before_reading() {
|
||||||
|
use crate::storage::CountingStorage;
|
||||||
|
let mut f = build_fixed_array(3, false, 10);
|
||||||
|
f.resize(16 << 20, 0);
|
||||||
|
let mut h = FixedArrayHeader::parse(&f, 0x100, 8, 8).unwrap();
|
||||||
|
h.max_nelmts_bits = 30;
|
||||||
|
h.num_elements = 4 << 20;
|
||||||
|
let dims = [h.num_elements * 20];
|
||||||
|
let want = read_fixed_array_chunks(&f, &h, &dims, None, &[20], 8, 8, 8);
|
||||||
|
assert!(
|
||||||
|
matches!(want, Err(FormatError::UnexpectedEof { .. })),
|
||||||
|
"{want:?}"
|
||||||
|
);
|
||||||
|
let storage = CountingStorage::new(f);
|
||||||
|
let got = read_fixed_array_chunks_in(&storage, &h, &dims, None, &[20], 8, 8, 8);
|
||||||
|
assert_eq!(format!("{got:?}"), format!("{want:?}"));
|
||||||
|
assert!(storage.bytes_read() < 64, "{} bytes", storage.bytes_read());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,9 +6,11 @@ use alloc::{format, vec::Vec};
|
|||||||
#[cfg(feature = "checksum")]
|
#[cfg(feature = "checksum")]
|
||||||
use byteorder::{ByteOrder, LittleEndian};
|
use byteorder::{ByteOrder, LittleEndian};
|
||||||
|
|
||||||
use crate::btree_v2::{BTreeV2Header, collect_btree_v2_records};
|
use crate::addr::to_usize;
|
||||||
|
use crate::btree_v2::{BTreeV2Header, find_btree_v2_records_in};
|
||||||
use crate::error::FormatError;
|
use crate::error::FormatError;
|
||||||
use crate::filter_pipeline::FilterPipeline;
|
use crate::filter_pipeline::FilterPipeline;
|
||||||
|
use crate::storage::{Storage, Window, len_usize, read_exact_at};
|
||||||
|
|
||||||
/// Parsed fractal heap header (signature "FRHP").
|
/// Parsed fractal heap header (signature "FRHP").
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@@ -138,6 +140,36 @@ impl FractalHeapHeader {
|
|||||||
offset_size: u8,
|
offset_size: u8,
|
||||||
length_size: u8,
|
length_size: u8,
|
||||||
) -> Result<FractalHeapHeader, FormatError> {
|
) -> Result<FractalHeapHeader, FormatError> {
|
||||||
|
Self::parse_in(file_data, offset as u64, offset_size, length_size)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`Self::parse`] over any [`Storage`]: one read of the header (two
|
||||||
|
/// when it holds an I/O filter pipeline).
|
||||||
|
pub fn parse_in<S: Storage + ?Sized>(
|
||||||
|
file: &S,
|
||||||
|
offset: u64,
|
||||||
|
offset_size: u8,
|
||||||
|
length_size: u8,
|
||||||
|
) -> Result<FractalHeapHeader, FormatError> {
|
||||||
|
// Every field up to the checksum, without and with the filter
|
||||||
|
// information; the window holds all of it (or ends at the end of
|
||||||
|
// the file), so its bounds checks are the whole-file ones.
|
||||||
|
let (os, ls) = (usize::from(offset_size), usize::from(length_size));
|
||||||
|
let unfiltered_len = 26 + 12 * ls + 3 * os;
|
||||||
|
let mut w = Window::read(file, offset, unfiltered_len)?;
|
||||||
|
if w.bytes.len() == unfiltered_len {
|
||||||
|
let filter_len = usize::from(u16::from_le_bytes([w.bytes[7], w.bytes[8]]));
|
||||||
|
if filter_len > 0 {
|
||||||
|
w = Window::read(file, offset, unfiltered_len + ls + 4 + filter_len)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let ensure_len = |_: &[u8], pos: usize, needed: usize| w.ensure(pos, needed);
|
||||||
|
let read_offset = |_: &[u8], pos: usize, size: u8| {
|
||||||
|
w.ensure(pos, usize::from(size))?;
|
||||||
|
read_offset(&w.bytes, pos, size)
|
||||||
|
};
|
||||||
|
let file_data: &[u8] = &w.bytes;
|
||||||
|
let offset = 0usize;
|
||||||
ensure_len(file_data, offset, 5)?;
|
ensure_len(file_data, offset, 5)?;
|
||||||
if &file_data[offset..offset + 4] != b"FRHP" {
|
if &file_data[offset..offset + 4] != b"FRHP" {
|
||||||
return Err(FormatError::InvalidFractalHeapSignature);
|
return Err(FormatError::InvalidFractalHeapSignature);
|
||||||
@@ -148,9 +180,6 @@ impl FractalHeapHeader {
|
|||||||
return Err(FormatError::InvalidFractalHeapVersion(version));
|
return Err(FormatError::InvalidFractalHeapVersion(version));
|
||||||
}
|
}
|
||||||
|
|
||||||
let os = offset_size as usize;
|
|
||||||
let ls = length_size as usize;
|
|
||||||
|
|
||||||
let mut pos = offset + 5;
|
let mut pos = offset + 5;
|
||||||
ensure_len(file_data, pos, 2)?;
|
ensure_len(file_data, pos, 2)?;
|
||||||
let heap_id_length = u16::from_le_bytes([file_data[pos], file_data[pos + 1]]);
|
let heap_id_length = u16::from_le_bytes([file_data[pos], file_data[pos + 1]]);
|
||||||
@@ -354,6 +383,17 @@ impl FractalHeapHeader {
|
|||||||
id_bytes: &[u8],
|
id_bytes: &[u8],
|
||||||
offset_size: u8,
|
offset_size: u8,
|
||||||
) -> Result<Vec<u8>, FormatError> {
|
) -> Result<Vec<u8>, FormatError> {
|
||||||
|
self.read_managed_object_in(file_data, id_bytes, offset_size)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`Self::read_managed_object`] over any [`Storage`].
|
||||||
|
pub fn read_managed_object_in<S: Storage + ?Sized>(
|
||||||
|
&self,
|
||||||
|
file_data: &S,
|
||||||
|
id_bytes: &[u8],
|
||||||
|
offset_size: u8,
|
||||||
|
) -> Result<Vec<u8>, FormatError> {
|
||||||
|
crate::lookup_stats::heap_object_read();
|
||||||
let Some(&first) = id_bytes.first() else {
|
let Some(&first) = id_bytes.first() else {
|
||||||
return Err(FormatError::UnexpectedEof {
|
return Err(FormatError::UnexpectedEof {
|
||||||
expected: 1,
|
expected: 1,
|
||||||
@@ -383,7 +423,11 @@ impl FractalHeapHeader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Read a huge object (heap ID type 1).
|
/// Read a huge object (heap ID type 1).
|
||||||
fn read_huge_object(&self, file_data: &[u8], id: &[u8]) -> Result<Vec<u8>, FormatError> {
|
fn read_huge_object<S: Storage + ?Sized>(
|
||||||
|
&self,
|
||||||
|
file: &S,
|
||||||
|
id: &[u8],
|
||||||
|
) -> Result<Vec<u8>, FormatError> {
|
||||||
let os = usize::from(self.offset_size);
|
let os = usize::from(self.offset_size);
|
||||||
let ls = usize::from(self.length_size);
|
let ls = usize::from(self.length_size);
|
||||||
// (address, stored length, filter mask, decoded length); the last two
|
// (address, stored length, filter mask, decoded length); the last two
|
||||||
@@ -414,18 +458,17 @@ impl FractalHeapHeader {
|
|||||||
let key_len = (usize::from(self.heap_id_length).saturating_sub(1)).min(8);
|
let key_len = (usize::from(self.heap_id_length).saturating_sub(1)).min(8);
|
||||||
ensure_len(id, 1, key_len)?;
|
ensure_len(id, 1, key_len)?;
|
||||||
let key = le_uint(&id[1..1 + key_len]);
|
let key = le_uint(&id[1..1 + key_len]);
|
||||||
self.find_huge_record(file_data, key)?
|
self.find_huge_record(file, key)?
|
||||||
};
|
};
|
||||||
|
|
||||||
let start = usize::try_from(addr).map_err(|_| heap_error("huge object address"))?;
|
let start = usize::try_from(addr).map_err(|_| heap_error("huge object address"))?;
|
||||||
let len = usize::try_from(stored_len).map_err(|_| heap_error("huge object length"))?;
|
let len = usize::try_from(stored_len).map_err(|_| heap_error("huge object length"))?;
|
||||||
ensure_len(file_data, start, len)?;
|
let stored = read_exact_at(file, start as u64, len)?;
|
||||||
let stored = &file_data[start..start + len];
|
|
||||||
match &self.filter_pipeline {
|
match &self.filter_pipeline {
|
||||||
None => Ok(stored.to_vec()),
|
None => Ok(stored.into_owned()),
|
||||||
Some(pipeline) => {
|
Some(pipeline) => {
|
||||||
let mem = usize::try_from(mem_len).map_err(|_| heap_error("huge object size"))?;
|
let mem = usize::try_from(mem_len).map_err(|_| heap_error("huge object size"))?;
|
||||||
let out = crate::filters::decompress_chunk_masked(stored, pipeline, mem, 1, mask)?;
|
let out = crate::filters::decompress_chunk_masked(&stored, pipeline, mem, 1, mask)?;
|
||||||
if out.len() != mem {
|
if out.len() != mem {
|
||||||
return Err(heap_error("filtered huge object decoded to the wrong size"));
|
return Err(heap_error("filtered huge object decoded to the wrong size"));
|
||||||
}
|
}
|
||||||
@@ -436,9 +479,9 @@ impl FractalHeapHeader {
|
|||||||
|
|
||||||
/// Look up huge object `key` in the huge-object v2 B-tree, returning
|
/// Look up huge object `key` in the huge-object v2 B-tree, returning
|
||||||
/// (address, stored length, filter mask, decoded length).
|
/// (address, stored length, filter mask, decoded length).
|
||||||
fn find_huge_record(
|
fn find_huge_record<S: Storage + ?Sized>(
|
||||||
&self,
|
&self,
|
||||||
file_data: &[u8],
|
file: &S,
|
||||||
key: u64,
|
key: u64,
|
||||||
) -> Result<(u64, u64, u32, u64), FormatError> {
|
) -> Result<(u64, u64, u32, u64), FormatError> {
|
||||||
if is_undefined(self.huge_btree_address, self.offset_size) {
|
if is_undefined(self.huge_btree_address, self.offset_size) {
|
||||||
@@ -446,9 +489,9 @@ impl FractalHeapHeader {
|
|||||||
"huge object ID but the heap has no huge-object index",
|
"huge object ID but the heap has no huge-object index",
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
let hdr = BTreeV2Header::parse(
|
let hdr = BTreeV2Header::parse_in(
|
||||||
file_data,
|
file,
|
||||||
self.huge_btree_address as usize,
|
self.huge_btree_address,
|
||||||
self.offset_size,
|
self.offset_size,
|
||||||
self.length_size,
|
self.length_size,
|
||||||
)?;
|
)?;
|
||||||
@@ -463,8 +506,12 @@ impl FractalHeapHeader {
|
|||||||
if hdr.tree_type != expected_type || usize::from(hdr.record_size) < rec_len {
|
if hdr.tree_type != expected_type || usize::from(hdr.record_size) < rec_len {
|
||||||
return Err(heap_error("unexpected huge-object B-tree record type"));
|
return Err(heap_error("unexpected huge-object B-tree record type"));
|
||||||
}
|
}
|
||||||
let records =
|
// Records are ordered by ID (the last field): descend to the ones
|
||||||
collect_btree_v2_records(file_data, &hdr, self.offset_size, self.length_size)?;
|
// equal to `key` instead of reading the whole index.
|
||||||
|
let id_at = rec_len - ls;
|
||||||
|
let records = find_btree_v2_records_in(file, &hdr, self.offset_size, &mut |r| {
|
||||||
|
le_uint(&r[id_at..id_at + ls]).cmp(&key)
|
||||||
|
})?;
|
||||||
for rec in &records {
|
for rec in &records {
|
||||||
let d = &rec.data;
|
let d = &rec.data;
|
||||||
if d.len() < rec_len {
|
if d.len() < rec_len {
|
||||||
@@ -513,9 +560,9 @@ impl FractalHeapHeader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Read a managed object (heap ID type 0).
|
/// Read a managed object (heap ID type 0).
|
||||||
fn read_heap_managed(
|
fn read_heap_managed<S: Storage + ?Sized>(
|
||||||
&self,
|
&self,
|
||||||
file_data: &[u8],
|
file_data: &S,
|
||||||
id_bytes: &[u8],
|
id_bytes: &[u8],
|
||||||
offset_size: u8,
|
offset_size: u8,
|
||||||
) -> Result<Vec<u8>, FormatError> {
|
) -> Result<Vec<u8>, FormatError> {
|
||||||
@@ -533,24 +580,24 @@ impl FractalHeapHeader {
|
|||||||
self.read_from_direct_block(
|
self.read_from_direct_block(
|
||||||
file_data,
|
file_data,
|
||||||
DirectBlock {
|
DirectBlock {
|
||||||
addr: self.root_block_address as usize,
|
addr: to_usize(self.root_block_address)?,
|
||||||
size: self.starting_block_size,
|
size: self.starting_block_size,
|
||||||
heap_offset: 0,
|
heap_offset: 0,
|
||||||
filtered_size: self.root_direct_block_filtered_size,
|
filtered_size: self.root_direct_block_filtered_size,
|
||||||
filter_mask: self.root_direct_block_filter_mask,
|
filter_mask: self.root_direct_block_filter_mask,
|
||||||
},
|
},
|
||||||
heap_offset,
|
heap_offset,
|
||||||
obj_len as usize,
|
to_usize(obj_len)?,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
// Root is an indirect block — limit recursion to 64 levels
|
// Root is an indirect block — limit recursion to 64 levels
|
||||||
self.read_from_indirect_block(
|
self.read_from_indirect_block(
|
||||||
file_data,
|
file_data,
|
||||||
self.root_block_address as usize,
|
to_usize(self.root_block_address)?,
|
||||||
self.current_rows_in_root_indirect_block,
|
self.current_rows_in_root_indirect_block,
|
||||||
0, // block offset
|
0, // block offset
|
||||||
heap_offset,
|
heap_offset,
|
||||||
obj_len as usize,
|
to_usize(obj_len)?,
|
||||||
offset_size,
|
offset_size,
|
||||||
64, // max recursion depth
|
64, // max recursion depth
|
||||||
)
|
)
|
||||||
@@ -563,27 +610,27 @@ impl FractalHeapHeader {
|
|||||||
/// header), so we just add it to the block address minus the block's heap
|
/// header), so we just add it to the block address minus the block's heap
|
||||||
/// offset. A filtered heap stores each direct block (header included)
|
/// offset. A filtered heap stores each direct block (header included)
|
||||||
/// through its filter pipeline, so the block is decoded first.
|
/// through its filter pipeline, so the block is decoded first.
|
||||||
fn read_from_direct_block(
|
fn read_from_direct_block<S: Storage + ?Sized>(
|
||||||
&self,
|
&self,
|
||||||
file_data: &[u8],
|
file: &S,
|
||||||
block: DirectBlock,
|
block: DirectBlock,
|
||||||
target_offset: u64,
|
target_offset: u64,
|
||||||
length: usize,
|
length: usize,
|
||||||
) -> Result<Vec<u8>, FormatError> {
|
) -> Result<Vec<u8>, FormatError> {
|
||||||
if target_offset < block.heap_offset {
|
if target_offset < block.heap_offset {
|
||||||
return Err(FormatError::UnexpectedEof {
|
return Err(FormatError::UnexpectedEof {
|
||||||
expected: block.heap_offset as usize,
|
expected: to_usize(block.heap_offset)?,
|
||||||
available: target_offset as usize,
|
available: to_usize(target_offset)?,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
let local_offset = (target_offset - block.heap_offset) as usize;
|
let local_offset = to_usize(target_offset - block.heap_offset)?;
|
||||||
if let Some(pipeline) = &self.filter_pipeline {
|
if let Some(pipeline) = &self.filter_pipeline {
|
||||||
let stored_len = usize::try_from(block.filtered_size)
|
let stored_len = usize::try_from(block.filtered_size)
|
||||||
.map_err(|_| heap_error("direct block size"))?;
|
.map_err(|_| heap_error("direct block size"))?;
|
||||||
let size = usize::try_from(block.size).map_err(|_| heap_error("direct block size"))?;
|
let size = usize::try_from(block.size).map_err(|_| heap_error("direct block size"))?;
|
||||||
ensure_len(file_data, block.addr, stored_len)?;
|
let stored = read_exact_at(file, block.addr as u64, stored_len)?;
|
||||||
let decoded = crate::filters::decompress_chunk_masked(
|
let decoded = crate::filters::decompress_chunk_masked(
|
||||||
&file_data[block.addr..block.addr + stored_len],
|
&stored,
|
||||||
pipeline,
|
pipeline,
|
||||||
size,
|
size,
|
||||||
1,
|
1,
|
||||||
@@ -597,17 +644,16 @@ impl FractalHeapHeader {
|
|||||||
.checked_add(local_offset)
|
.checked_add(local_offset)
|
||||||
.ok_or(FormatError::UnexpectedEof {
|
.ok_or(FormatError::UnexpectedEof {
|
||||||
expected: usize::MAX,
|
expected: usize::MAX,
|
||||||
available: file_data.len(),
|
available: len_usize(file),
|
||||||
})?;
|
})?;
|
||||||
ensure_len(file_data, pos, length)?;
|
Ok(read_exact_at(file, pos as u64, length)?.into_owned())
|
||||||
Ok(file_data[pos..pos + length].to_vec())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Read an object by traversing an indirect block to find the right direct block.
|
/// Read an object by traversing an indirect block to find the right direct block.
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
fn read_from_indirect_block(
|
fn read_from_indirect_block<S: Storage + ?Sized>(
|
||||||
&self,
|
&self,
|
||||||
file_data: &[u8],
|
file: &S,
|
||||||
iblock_addr: usize,
|
iblock_addr: usize,
|
||||||
nrows: u16,
|
nrows: u16,
|
||||||
iblock_heap_offset: u64,
|
iblock_heap_offset: u64,
|
||||||
@@ -621,29 +667,169 @@ impl FractalHeapHeader {
|
|||||||
"fractal heap: maximum recursion depth exceeded".into(),
|
"fractal heap: maximum recursion depth exceeded".into(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
// Parse indirect block header
|
|
||||||
ensure_len(file_data, iblock_addr, 4)?;
|
|
||||||
if &file_data[iblock_addr..iblock_addr + 4] != b"FHIB" {
|
|
||||||
return Err(FormatError::InvalidFractalHeapSignature);
|
|
||||||
}
|
|
||||||
|
|
||||||
let block_offset_bytes = (self.max_heap_size as usize).div_ceil(8);
|
let block_offset_bytes = (self.max_heap_size as usize).div_ceil(8);
|
||||||
let iblock_header = 5 + offset_size as usize + block_offset_bytes;
|
let iblock_header = 5 + offset_size as usize + block_offset_bytes;
|
||||||
let mut pos = iblock_addr + iblock_header;
|
|
||||||
|
|
||||||
let tw = self.table_width as u64;
|
|
||||||
let nrows_usize = nrows as usize;
|
let nrows_usize = nrows as usize;
|
||||||
let mut current_heap_offset = iblock_heap_offset;
|
|
||||||
|
|
||||||
// Rows below max_direct_rows hold direct blocks; rows at/above hold
|
// Rows below max_direct_rows hold direct blocks; rows at/above hold
|
||||||
// child indirect blocks. (NOT the FRHP "starting rows" field.)
|
// child indirect blocks. (NOT the FRHP "starting rows" field.)
|
||||||
let start_indirect = self.max_direct_rows();
|
let start_indirect = self.max_direct_rows();
|
||||||
let max_direct_rows = nrows_usize.min(start_indirect);
|
let max_direct_rows = nrows_usize.min(start_indirect);
|
||||||
|
|
||||||
|
// The block up to its last child entry. The walk below reads
|
||||||
|
// entries in order and stops at the one covering the target, which
|
||||||
|
// the geometry alone locates, so the first window ends there: a
|
||||||
|
// header claiming a huge table costs a read of the entries in front
|
||||||
|
// of the target, not of the rest of the file. Only when that entry
|
||||||
|
// is unallocated (or none covers the target) does the walk go on,
|
||||||
|
// over the whole block. Either window holds what it was asked for or
|
||||||
|
// ends at the end of the file, so its bounds checks are the
|
||||||
|
// whole-file ones.
|
||||||
|
let direct_entry = usize::from(offset_size)
|
||||||
|
+ if self.filter_pipeline.is_some() {
|
||||||
|
usize::from(self.length_size) + 4
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
|
let direct_entries = max_direct_rows.saturating_mul(usize::from(self.table_width));
|
||||||
|
let entries_len = |n: usize| {
|
||||||
|
n.min(direct_entries)
|
||||||
|
.saturating_mul(direct_entry)
|
||||||
|
.saturating_add(
|
||||||
|
n.saturating_sub(direct_entries)
|
||||||
|
.saturating_mul(usize::from(offset_size)),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
let all_entries = direct_entries.saturating_add(
|
||||||
|
nrows_usize
|
||||||
|
.saturating_sub(start_indirect)
|
||||||
|
.saturating_mul(usize::from(self.table_width)),
|
||||||
|
);
|
||||||
|
let block_len = iblock_header.saturating_add(entries_len(all_entries));
|
||||||
|
let target_entry = self.indirect_entry_for(nrows_usize, iblock_heap_offset, target_offset);
|
||||||
|
let first_len = target_entry.map_or(block_len, |i| {
|
||||||
|
iblock_header
|
||||||
|
.saturating_add(entries_len(i.saturating_add(1)))
|
||||||
|
.min(block_len)
|
||||||
|
});
|
||||||
|
let mut next = self.walk_indirect_block(
|
||||||
|
&Window::read(file, iblock_addr as u64, first_len)?,
|
||||||
|
nrows_usize,
|
||||||
|
iblock_heap_offset,
|
||||||
|
target_offset,
|
||||||
|
offset_size,
|
||||||
|
target_entry.map_or(usize::MAX, |i| i.saturating_add(1)),
|
||||||
|
)?;
|
||||||
|
if next.is_none() && first_len < block_len {
|
||||||
|
next = self.walk_indirect_block(
|
||||||
|
&Window::read(file, iblock_addr as u64, block_len)?,
|
||||||
|
nrows_usize,
|
||||||
|
iblock_heap_offset,
|
||||||
|
target_offset,
|
||||||
|
offset_size,
|
||||||
|
usize::MAX,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
match next {
|
||||||
|
Some(IndirectChild::Direct(block)) => {
|
||||||
|
self.read_from_direct_block(file, block, target_offset, length)
|
||||||
|
}
|
||||||
|
Some(IndirectChild::Indirect {
|
||||||
|
addr,
|
||||||
|
nrows,
|
||||||
|
heap_offset,
|
||||||
|
}) => self.read_from_indirect_block(
|
||||||
|
file,
|
||||||
|
addr,
|
||||||
|
nrows,
|
||||||
|
heap_offset,
|
||||||
|
target_offset,
|
||||||
|
length,
|
||||||
|
offset_size,
|
||||||
|
depth_remaining - 1,
|
||||||
|
),
|
||||||
|
None => Err(FormatError::UnexpectedEof {
|
||||||
|
expected: to_usize(target_offset)?.saturating_add(length),
|
||||||
|
available: len_usize(file),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Which child entry of an indirect block (numbered in walk order:
|
||||||
|
/// direct rows, then indirect rows) covers `target_offset`, from the
|
||||||
|
/// doubling-table geometry alone — the entry
|
||||||
|
/// [`Self::walk_indirect_block`] stops at if it is allocated. `None`
|
||||||
|
/// when no entry does.
|
||||||
|
fn indirect_entry_for(&self, nrows: usize, heap_offset: u64, target: u64) -> Option<usize> {
|
||||||
|
// The walk adds block sizes with saturation; in u128 the same test
|
||||||
|
// is `cur <= target < cur + size` without it (a target of u64::MAX
|
||||||
|
// is never inside a saturated range).
|
||||||
|
if target == u64::MAX {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let (tw, target) = (u128::from(self.table_width), u128::from(target));
|
||||||
|
let mut cur = u128::from(heap_offset);
|
||||||
|
let mut before = 0usize;
|
||||||
|
for row in 0..nrows {
|
||||||
|
if target < cur {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
// Direct and indirect rows alike span this row's block size per
|
||||||
|
// entry.
|
||||||
|
let size = u128::from(self.block_size_for_row(row));
|
||||||
|
let span = size * tw;
|
||||||
|
if size > 0 && target < cur + span {
|
||||||
|
let col = usize::try_from((target - cur) / size).ok()?;
|
||||||
|
return before.checked_add(col);
|
||||||
|
}
|
||||||
|
cur += span;
|
||||||
|
before = before.saturating_add(self.table_width as usize);
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Walk an indirect block's child entries in order, in the window `w`
|
||||||
|
/// (the block from its signature on), and return the allocated child
|
||||||
|
/// covering `target_offset`, or `None` when no entry among the first
|
||||||
|
/// `limit` does.
|
||||||
|
fn walk_indirect_block(
|
||||||
|
&self,
|
||||||
|
w: &Window<'_>,
|
||||||
|
nrows: usize,
|
||||||
|
iblock_heap_offset: u64,
|
||||||
|
target_offset: u64,
|
||||||
|
offset_size: u8,
|
||||||
|
limit: usize,
|
||||||
|
) -> Result<Option<IndirectChild>, FormatError> {
|
||||||
|
let ensure_len = |_: &[u8], pos: usize, needed: usize| w.ensure(pos, needed);
|
||||||
|
let read_offset = |_: &[u8], pos: usize, size: u8| {
|
||||||
|
w.ensure(pos, usize::from(size))?;
|
||||||
|
read_offset(&w.bytes, pos, size)
|
||||||
|
};
|
||||||
|
let file_data: &[u8] = &w.bytes;
|
||||||
|
let block_offset_bytes = (self.max_heap_size as usize).div_ceil(8);
|
||||||
|
let iblock_header = 5 + offset_size as usize + block_offset_bytes;
|
||||||
|
let tw = self.table_width as u64;
|
||||||
|
let mut current_heap_offset = iblock_heap_offset;
|
||||||
|
let start_indirect = self.max_direct_rows();
|
||||||
|
let max_direct_rows = nrows.min(start_indirect);
|
||||||
|
let mut walked = 0usize;
|
||||||
|
|
||||||
|
// Parse indirect block header
|
||||||
|
ensure_len(file_data, 0, 4)?;
|
||||||
|
if &file_data[..4] != b"FHIB" {
|
||||||
|
return Err(FormatError::InvalidFractalHeapSignature);
|
||||||
|
}
|
||||||
|
let mut pos = iblock_header;
|
||||||
|
|
||||||
for row in 0..max_direct_rows {
|
for row in 0..max_direct_rows {
|
||||||
let block_size = self.block_size_for_row(row);
|
let block_size = self.block_size_for_row(row);
|
||||||
|
|
||||||
for _col in 0..tw {
|
for _col in 0..tw {
|
||||||
|
if walked == limit {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
walked += 1;
|
||||||
let child_addr = read_offset(file_data, pos, offset_size)?;
|
let child_addr = read_offset(file_data, pos, offset_size)?;
|
||||||
pos += offset_size as usize;
|
pos += offset_size as usize;
|
||||||
|
|
||||||
@@ -670,18 +856,13 @@ impl FractalHeapHeader {
|
|||||||
&& target_offset >= current_heap_offset
|
&& target_offset >= current_heap_offset
|
||||||
&& target_offset < block_end
|
&& target_offset < block_end
|
||||||
{
|
{
|
||||||
return self.read_from_direct_block(
|
return Ok(Some(IndirectChild::Direct(DirectBlock {
|
||||||
file_data,
|
addr: to_usize(child_addr)?,
|
||||||
DirectBlock {
|
|
||||||
addr: child_addr as usize,
|
|
||||||
size: block_size,
|
size: block_size,
|
||||||
heap_offset: current_heap_offset,
|
heap_offset: current_heap_offset,
|
||||||
filtered_size,
|
filtered_size,
|
||||||
filter_mask,
|
filter_mask,
|
||||||
},
|
})));
|
||||||
target_offset,
|
|
||||||
length,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
current_heap_offset = block_end;
|
current_heap_offset = block_end;
|
||||||
}
|
}
|
||||||
@@ -690,11 +871,15 @@ impl FractalHeapHeader {
|
|||||||
// Rows at and above `start_indirect` hold child indirect blocks. A
|
// Rows at and above `start_indirect` hold child indirect blocks. A
|
||||||
// child in row r spans exactly that row's block size of heap space,
|
// child in row r spans exactly that row's block size of heap space,
|
||||||
// so it has as many rows as a table of that total size needs.
|
// so it has as many rows as a table of that total size needs.
|
||||||
for row in start_indirect..nrows_usize {
|
for row in start_indirect..nrows {
|
||||||
let child_space = self.block_size_for_row(row);
|
let child_space = self.block_size_for_row(row);
|
||||||
let child_nrows = self.rows_for_size(child_space);
|
let child_nrows = self.rows_for_size(child_space);
|
||||||
|
|
||||||
for _col in 0..tw {
|
for _col in 0..tw {
|
||||||
|
if walked == limit {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
walked += 1;
|
||||||
let child_addr = read_offset(file_data, pos, offset_size)?;
|
let child_addr = read_offset(file_data, pos, offset_size)?;
|
||||||
pos += offset_size as usize;
|
pos += offset_size as usize;
|
||||||
|
|
||||||
@@ -703,25 +888,16 @@ impl FractalHeapHeader {
|
|||||||
&& target_offset >= current_heap_offset
|
&& target_offset >= current_heap_offset
|
||||||
&& target_offset < block_end
|
&& target_offset < block_end
|
||||||
{
|
{
|
||||||
return self.read_from_indirect_block(
|
return Ok(Some(IndirectChild::Indirect {
|
||||||
file_data,
|
addr: to_usize(child_addr)?,
|
||||||
child_addr as usize,
|
nrows: child_nrows,
|
||||||
child_nrows,
|
heap_offset: current_heap_offset,
|
||||||
current_heap_offset,
|
}));
|
||||||
target_offset,
|
|
||||||
length,
|
|
||||||
offset_size,
|
|
||||||
depth_remaining - 1,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
current_heap_offset = block_end;
|
current_heap_offset = block_end;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Ok(None)
|
||||||
Err(FormatError::UnexpectedEof {
|
|
||||||
expected: target_offset as usize + length,
|
|
||||||
available: file_data.len(),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Number of rows in the doubling table whose block size is at most the
|
/// Number of rows in the doubling table whose block size is at most the
|
||||||
@@ -765,6 +941,16 @@ impl FractalHeapHeader {
|
|||||||
|
|
||||||
/// A managed direct block's location, extent and (for a filtered heap) its
|
/// A managed direct block's location, extent and (for a filtered heap) its
|
||||||
/// stored size and filter mask.
|
/// stored size and filter mask.
|
||||||
|
/// The child of an indirect block that covers a heap offset.
|
||||||
|
enum IndirectChild {
|
||||||
|
Direct(DirectBlock),
|
||||||
|
Indirect {
|
||||||
|
addr: usize,
|
||||||
|
nrows: u16,
|
||||||
|
heap_offset: u64,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
struct DirectBlock {
|
struct DirectBlock {
|
||||||
addr: usize,
|
addr: usize,
|
||||||
size: u64,
|
size: u64,
|
||||||
@@ -1024,4 +1210,152 @@ mod tests {
|
|||||||
let id = [0x40u8, 0, 0, 0, 0, 0, 0];
|
let id = [0x40u8, 0, 0, 0, 0, 0, 0];
|
||||||
assert!(hdr.read_managed_object(&file_data, &id, 8).is_err());
|
assert!(hdr.read_managed_object(&file_data, &id, 8).is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Headers, and managed (in a direct root and through an indirect
|
||||||
|
/// root), huge and tiny objects read identically through a
|
||||||
|
/// `read_at`-only storage, for every truncation of the file.
|
||||||
|
#[test]
|
||||||
|
fn storage_reads_match_slice_reads() {
|
||||||
|
use crate::storage::CountingStorage;
|
||||||
|
let (mut file, header_end) = build_simple_heap(8, 8);
|
||||||
|
// An indirect root block at 600: row 0 holds the direct block at
|
||||||
|
// 256, then three undefined blocks.
|
||||||
|
file[600..604].copy_from_slice(b"FHIB");
|
||||||
|
let mut at = 600 + 5 + 8 + 2;
|
||||||
|
for addr in [256u64, u64::MAX, u64::MAX, u64::MAX] {
|
||||||
|
file[at..at + 8].copy_from_slice(&addr.to_le_bytes());
|
||||||
|
at += 8;
|
||||||
|
}
|
||||||
|
file[900..905].copy_from_slice(b"huge!");
|
||||||
|
let managed_id = |offset: u64, len: u64| {
|
||||||
|
let payload = offset | (len << 16);
|
||||||
|
let mut id = vec![0u8];
|
||||||
|
id.extend_from_slice(&payload.to_le_bytes()[..6]);
|
||||||
|
id
|
||||||
|
};
|
||||||
|
let mut huge = vec![0x10u8];
|
||||||
|
huge.extend_from_slice(&900u64.to_le_bytes());
|
||||||
|
huge.extend_from_slice(&5u64.to_le_bytes());
|
||||||
|
let ids = [
|
||||||
|
managed_id(15, 13),
|
||||||
|
managed_id(15, 200),
|
||||||
|
managed_id(130, 4),
|
||||||
|
huge,
|
||||||
|
vec![0x22, b'a', b'b', b'c', 0, 0, 0],
|
||||||
|
];
|
||||||
|
let mut cuts: Vec<usize> = (0..=header_end + 1).collect();
|
||||||
|
cuts.extend([256, 260, 271, 280, 600, 610, 620, 640, 900, 903, file.len()]);
|
||||||
|
for cut in cuts {
|
||||||
|
let f = &file[..cut];
|
||||||
|
let storage = CountingStorage::new(f.to_vec());
|
||||||
|
let want = FractalHeapHeader::parse(f, 0, 8, 8);
|
||||||
|
let got = FractalHeapHeader::parse_in(&storage, 0, 8, 8);
|
||||||
|
assert_eq!(format!("{got:?}"), format!("{want:?}"), "cut {cut}");
|
||||||
|
let Ok(direct) = want else { continue };
|
||||||
|
let mut indirect = direct.clone();
|
||||||
|
indirect.root_block_address = 600;
|
||||||
|
indirect.current_rows_in_root_indirect_block = 1;
|
||||||
|
let mut huge_ids = direct.clone();
|
||||||
|
huge_ids.heap_id_length = 17;
|
||||||
|
for hdr in [&direct, &indirect, &huge_ids] {
|
||||||
|
for id in &ids {
|
||||||
|
assert_eq!(
|
||||||
|
hdr.read_managed_object_in(&storage, id, 8),
|
||||||
|
hdr.read_managed_object(f, id, 8),
|
||||||
|
"cut {cut}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A header claiming a huge doubling table (width 0xFFFF, 0xFFFF rows in
|
||||||
|
/// the root indirect block) in a 16 MiB file: reading an object from the
|
||||||
|
/// table's first block reads the entries up to it, not the rest of the
|
||||||
|
/// file, and gives what the slice read gives. When the covering entry is
|
||||||
|
/// unallocated the walk goes on over the whole block, still identically.
|
||||||
|
#[test]
|
||||||
|
fn huge_table_claims_read_only_what_the_walk_needs() {
|
||||||
|
use crate::storage::CountingStorage;
|
||||||
|
let (mut file, _) = build_simple_heap(8, 8);
|
||||||
|
file.resize(16 << 20, 0);
|
||||||
|
file[600..604].copy_from_slice(b"FHIB");
|
||||||
|
let first_entry = 600 + 5 + 8 + 2;
|
||||||
|
file[first_entry..first_entry + 8].copy_from_slice(&256u64.to_le_bytes());
|
||||||
|
let mut hdr = FractalHeapHeader::parse(&file, 0, 8, 8).unwrap();
|
||||||
|
hdr.table_width = 0xFFFF;
|
||||||
|
hdr.root_block_address = 600;
|
||||||
|
hdr.current_rows_in_root_indirect_block = 0xFFFF;
|
||||||
|
let managed_id = |offset: u64, len: u64| {
|
||||||
|
let payload = offset | (len << 16);
|
||||||
|
let mut id = vec![0u8];
|
||||||
|
id.extend_from_slice(&payload.to_le_bytes()[..6]);
|
||||||
|
id
|
||||||
|
};
|
||||||
|
let storage = CountingStorage::new(file.clone());
|
||||||
|
let id = managed_id(15, 13);
|
||||||
|
let want = hdr.read_managed_object(&file, &id, 8);
|
||||||
|
assert!(want.is_ok(), "{want:?}");
|
||||||
|
storage.reset();
|
||||||
|
assert_eq!(hdr.read_managed_object_in(&storage, &id, 8), want);
|
||||||
|
assert!(
|
||||||
|
storage.bytes_read() < 1024,
|
||||||
|
"{} bytes in {} reads",
|
||||||
|
storage.bytes_read(),
|
||||||
|
storage.reads()
|
||||||
|
);
|
||||||
|
// The second entry (heap offsets 128..256) is unallocated (zero is
|
||||||
|
// not the undefined address, so make it all ones).
|
||||||
|
file[first_entry + 8..first_entry + 16].fill(0xFF);
|
||||||
|
let storage = CountingStorage::new(file.clone());
|
||||||
|
let id = managed_id(130, 4);
|
||||||
|
let want = hdr.read_managed_object(&file, &id, 8);
|
||||||
|
assert_eq!(hdr.read_managed_object_in(&storage, &id, 8), want);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A huge object found through the huge-object B-tree reads the
|
||||||
|
/// B-tree through Storage: the same result (here an error, there is no
|
||||||
|
/// B-tree at that address) as from the slice.
|
||||||
|
#[test]
|
||||||
|
fn huge_object_btree_reads_through_storage() {
|
||||||
|
use crate::storage::CountingStorage;
|
||||||
|
let (file, _) = build_simple_heap(8, 8);
|
||||||
|
let mut hdr = FractalHeapHeader::parse(&file, 0, 8, 8).unwrap();
|
||||||
|
hdr.huge_btree_address = 700;
|
||||||
|
let id = [0x10, 1, 0, 0, 0, 0, 0];
|
||||||
|
let want = hdr.read_managed_object(&file, &id, 8);
|
||||||
|
assert!(want.is_err());
|
||||||
|
let storage = CountingStorage::new(file);
|
||||||
|
assert_eq!(hdr.read_managed_object_in(&storage, &id, 8), want);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A header with an I/O filter pipeline (read in a second, longer
|
||||||
|
/// window) parses identically through a `read_at`-only storage, for
|
||||||
|
/// every truncation.
|
||||||
|
#[test]
|
||||||
|
fn filtered_header_parses_identically_through_storage() {
|
||||||
|
use crate::storage::CountingStorage;
|
||||||
|
let (simple, header_end) = build_simple_heap(8, 8);
|
||||||
|
let pipeline = [2u8, 1, 1, 0, 0, 0, 1, 0, 6, 0, 0, 0]; // deflate, level 6
|
||||||
|
let mut header = simple[..header_end - 4].to_vec();
|
||||||
|
header[7..9].copy_from_slice(&(pipeline.len() as u16).to_le_bytes());
|
||||||
|
header.extend_from_slice(&100u64.to_le_bytes()); // root block's stored size
|
||||||
|
header.extend_from_slice(&0u32.to_le_bytes()); // its filter mask
|
||||||
|
header.extend_from_slice(&pipeline);
|
||||||
|
let sum = crate::checksum::jenkins_lookup3(&header);
|
||||||
|
header.extend_from_slice(&sum.to_le_bytes());
|
||||||
|
let mut file = header.clone();
|
||||||
|
file.resize(256, 0);
|
||||||
|
let hdr = FractalHeapHeader::parse(&file, 0, 8, 8).unwrap();
|
||||||
|
assert!(hdr.filter_pipeline.is_some());
|
||||||
|
for cut in 0..=file.len() {
|
||||||
|
let f = &file[..cut];
|
||||||
|
let storage = CountingStorage::new(f.to_vec());
|
||||||
|
assert_eq!(
|
||||||
|
format!("{:?}", FractalHeapHeader::parse_in(&storage, 0, 8, 8)),
|
||||||
|
format!("{:?}", FractalHeapHeader::parse(f, 0, 8, 8)),
|
||||||
|
"cut {cut}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ use alloc::{vec, vec::Vec};
|
|||||||
use crate::data_read::NativeElement;
|
use crate::data_read::NativeElement;
|
||||||
use crate::error::FormatError;
|
use crate::error::FormatError;
|
||||||
use crate::selection::Selection;
|
use crate::selection::Selection;
|
||||||
|
use crate::storage::{ExtentBytes, ExtentReq, Storage, raw_batches};
|
||||||
|
|
||||||
/// Row-major element strides of `dims` (the last dimension has stride 1).
|
/// Row-major element strides of `dims` (the last dimension has stride 1).
|
||||||
fn strides(dims: &[u64]) -> Vec<u64> {
|
fn strides(dims: &[u64]) -> Vec<u64> {
|
||||||
@@ -261,6 +262,259 @@ pub(crate) fn gather<T: NativeElement>(
|
|||||||
Ok(out)
|
Ok(out)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Largest gap between two of a selection's runs that [`gather_storage`]
|
||||||
|
/// reads through rather than asking for the runs separately: skipping a
|
||||||
|
/// few KiB costs a remote backend far less than another request (and a
|
||||||
|
/// local one less than another call and allocation).
|
||||||
|
pub(crate) const GATHER_GAP_BYTES: usize = 4 << 10;
|
||||||
|
|
||||||
|
/// Largest single read [`gather_storage`] makes of a selection's runs: runs
|
||||||
|
/// are merged into reads up to this size, and a longer run is split.
|
||||||
|
pub(crate) const GATHER_SPAN_BYTES: usize = 8 << 20;
|
||||||
|
|
||||||
|
/// Call `emit(first_element, element_count)` for each run of a validated
|
||||||
|
/// hyperslab or point selection (in output order; see [`hyperslab_runs`]),
|
||||||
|
/// or the error for a hyperslab of the wrong rank or a point outside `dims`
|
||||||
|
/// (runs before that point have been emitted).
|
||||||
|
fn selection_runs(
|
||||||
|
dims: &[u64],
|
||||||
|
selection: &Selection,
|
||||||
|
emit: &mut dyn FnMut(u64, u64),
|
||||||
|
) -> Result<(), FormatError> {
|
||||||
|
match selection {
|
||||||
|
Selection::Hyperslab {
|
||||||
|
start,
|
||||||
|
stride,
|
||||||
|
count,
|
||||||
|
block,
|
||||||
|
} => {
|
||||||
|
let rank = dims.len();
|
||||||
|
if [start.len(), stride.len(), count.len(), block.len()] != [rank; 4] {
|
||||||
|
return Err(FormatError::SelectionOutOfBounds(
|
||||||
|
"hyperslab rank does not match dataset rank".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
hyperslab_runs(dims, start, stride, count, block, emit);
|
||||||
|
}
|
||||||
|
Selection::Points(points) => {
|
||||||
|
let strides = strides(dims);
|
||||||
|
let mut coalesce = Coalesce {
|
||||||
|
start: 0,
|
||||||
|
len: 0,
|
||||||
|
emit,
|
||||||
|
};
|
||||||
|
for p in points {
|
||||||
|
if p.len() != dims.len() || p.iter().zip(dims).any(|(c, n)| c >= n) {
|
||||||
|
return Err(FormatError::SelectionOutOfBounds(
|
||||||
|
"selection addresses elements outside the dataset".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let at = p
|
||||||
|
.iter()
|
||||||
|
.zip(&strides)
|
||||||
|
.fold(0u64, |acc, (c, s)| acc.wrapping_add(c.wrapping_mul(*s)));
|
||||||
|
coalesce.push(at, 1);
|
||||||
|
}
|
||||||
|
coalesce.flush();
|
||||||
|
}
|
||||||
|
Selection::None | Selection::All => {}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One read of [`gather_storage`]: bytes `[start, end)` of the dataset,
|
||||||
|
/// which hold the output's bytes up to `out_end` (from where the previous
|
||||||
|
/// span's end left off).
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
struct Span {
|
||||||
|
start: usize,
|
||||||
|
end: usize,
|
||||||
|
out_end: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`gather`] of bytes (`T = u8`) from a dataset that is not in memory: the
|
||||||
|
/// dataset's `src_len` bytes start at `base` in `file`, which must hold all
|
||||||
|
/// of them (the caller checks). Same checks and errors as [`gather`].
|
||||||
|
///
|
||||||
|
/// The selection's runs are walked twice. The first walk checks them and
|
||||||
|
/// plans the reads: runs in increasing order with at most
|
||||||
|
/// [`GATHER_GAP_BYTES`] between them are read as one span (the gap is read
|
||||||
|
/// and dropped), up to [`GATHER_SPAN_BYTES`] per span. So a strided
|
||||||
|
/// selection is a few large reads, not one per element, and nothing is
|
||||||
|
/// allocated per run. The spans are fetched batch by batch (one
|
||||||
|
/// [`Storage::read_ranges`] call per [`crate::storage::RAW_BATCH_BYTES`])
|
||||||
|
/// while the second walk copies each run out of its span.
|
||||||
|
pub(crate) fn gather_storage<S: Storage + ?Sized>(
|
||||||
|
file: &S,
|
||||||
|
base: u64,
|
||||||
|
src_len: usize,
|
||||||
|
dims: &[u64],
|
||||||
|
elem_size: usize,
|
||||||
|
selection: &Selection,
|
||||||
|
) -> Result<Vec<u8>, FormatError> {
|
||||||
|
if elem_size == 0 {
|
||||||
|
return Err(FormatError::DataSizeMismatch {
|
||||||
|
expected: 1,
|
||||||
|
actual: elem_size,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let n_elements = match selection {
|
||||||
|
Selection::None => 0,
|
||||||
|
Selection::Hyperslab { count, block, .. } => count
|
||||||
|
.iter()
|
||||||
|
.zip(block)
|
||||||
|
.try_fold(1u64, |acc, (&c, &b)| acc.checked_mul(c.checked_mul(b)?))
|
||||||
|
.ok_or_else(|| FormatError::Overflow("hyperslab count x block overflows".into()))?,
|
||||||
|
Selection::Points(points) => points.len() as u64,
|
||||||
|
Selection::All => {
|
||||||
|
return Err(FormatError::SelectionOutOfBounds(
|
||||||
|
"gather does not take Selection::All".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let out_bytes = crate::chunked_read::checked_byte_len(n_elements, elem_size)?;
|
||||||
|
let outside = || {
|
||||||
|
FormatError::SelectionOutOfBounds("selection addresses elements outside the dataset".into())
|
||||||
|
};
|
||||||
|
|
||||||
|
// First walk: check every run and plan the spans.
|
||||||
|
let mut spans: Vec<Span> = Vec::new();
|
||||||
|
let mut total = 0usize;
|
||||||
|
let mut failed = false;
|
||||||
|
selection_runs(dims, selection, &mut |first: u64, n: u64| {
|
||||||
|
if failed {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let range = usize::try_from(first)
|
||||||
|
.ok()
|
||||||
|
.and_then(|f| f.checked_mul(elem_size))
|
||||||
|
.zip(
|
||||||
|
usize::try_from(n)
|
||||||
|
.ok()
|
||||||
|
.and_then(|n| n.checked_mul(elem_size)),
|
||||||
|
)
|
||||||
|
.and_then(|(at, len)| Some((at, len, at.checked_add(len)?)));
|
||||||
|
let Some((mut at, mut len)) = range
|
||||||
|
.filter(|&(_, len, end)| end <= src_len && len <= out_bytes - total)
|
||||||
|
.map(|(at, len, _)| (at, len))
|
||||||
|
else {
|
||||||
|
failed = true;
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
while len > 0 {
|
||||||
|
let room = match spans.last_mut() {
|
||||||
|
Some(s)
|
||||||
|
if at >= s.end
|
||||||
|
&& at - s.end <= GATHER_GAP_BYTES
|
||||||
|
&& at - s.start < GATHER_SPAN_BYTES =>
|
||||||
|
{
|
||||||
|
let take = len.min(GATHER_SPAN_BYTES - (at - s.start));
|
||||||
|
s.end = at + take;
|
||||||
|
s.out_end += take;
|
||||||
|
take
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
let take = len.min(GATHER_SPAN_BYTES);
|
||||||
|
spans.push(Span {
|
||||||
|
start: at,
|
||||||
|
end: at + take,
|
||||||
|
out_end: total + take,
|
||||||
|
});
|
||||||
|
take
|
||||||
|
}
|
||||||
|
};
|
||||||
|
total += room;
|
||||||
|
at += room;
|
||||||
|
len -= room;
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
if failed || total != out_bytes {
|
||||||
|
return Err(outside());
|
||||||
|
}
|
||||||
|
|
||||||
|
// The spans' reads, and the batches they are fetched in.
|
||||||
|
let reqs: Vec<ExtentReq> = spans
|
||||||
|
.iter()
|
||||||
|
.map(|s| ExtentReq {
|
||||||
|
addr: base + s.start as u64,
|
||||||
|
len: s.end - s.start,
|
||||||
|
fetch: Some(s.end - s.start),
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let batches = raw_batches(reqs.len(), false, |i| reqs[i].len);
|
||||||
|
|
||||||
|
// Second walk: copy each run out of its span, fetching each batch of
|
||||||
|
// spans when the walk reaches it (and dropping the previous one).
|
||||||
|
let mut out = crate::bulk_alloc::vec_for_bulk(out_bytes);
|
||||||
|
let mut span = 0usize;
|
||||||
|
let mut batch = 0usize;
|
||||||
|
let mut fetched: Option<ExtentBytes<'_>> = None;
|
||||||
|
let mut error: Option<FormatError> = None;
|
||||||
|
selection_runs(dims, selection, &mut |first: u64, n: u64| {
|
||||||
|
if error.is_some() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Checked by the first walk (these cannot saturate or wrap).
|
||||||
|
let mut at = crate::addr::saturating_usize(first).wrapping_mul(elem_size);
|
||||||
|
let mut len = crate::addr::saturating_usize(n).wrapping_mul(elem_size);
|
||||||
|
while len > 0 {
|
||||||
|
while spans.get(span).is_some_and(|s| s.out_end <= out.len()) {
|
||||||
|
span += 1;
|
||||||
|
}
|
||||||
|
if fetched.is_none() || span >= batches[batch].end {
|
||||||
|
fetched = None;
|
||||||
|
while batches.get(batch).is_some_and(|b| span >= b.end) {
|
||||||
|
batch += 1;
|
||||||
|
}
|
||||||
|
let (Some(b), Some(_)) = (batches.get(batch).cloned(), spans.get(span)) else {
|
||||||
|
// The second walk emitted more than the first.
|
||||||
|
error = Some(outside());
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
match ExtentBytes::fetch(file, &reqs[b.clone()], b.start) {
|
||||||
|
Ok(f) => fetched = Some(f),
|
||||||
|
Err(e) => {
|
||||||
|
error = Some(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let s = spans[span];
|
||||||
|
let take = len.min(s.out_end - out.len());
|
||||||
|
let bytes = match fetched
|
||||||
|
.as_ref()
|
||||||
|
.map(|f| f.get(span, &reqs[span]))
|
||||||
|
.unwrap_or_else(|| Err(outside()))
|
||||||
|
{
|
||||||
|
Ok(b) => b,
|
||||||
|
Err(e) => {
|
||||||
|
error = Some(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
match at
|
||||||
|
.checked_sub(s.start)
|
||||||
|
.and_then(|o| bytes.get(o..o.checked_add(take)?))
|
||||||
|
{
|
||||||
|
Some(b) => out.extend_from_slice(b),
|
||||||
|
None => {
|
||||||
|
error = Some(outside());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
at += take;
|
||||||
|
len -= take;
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
if let Some(e) = error {
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
if out.len() != out_bytes {
|
||||||
|
return Err(outside());
|
||||||
|
}
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
//! HDF5 Global Heap collection parsing.
|
//! HDF5 Global Heap collection parsing.
|
||||||
|
|
||||||
#[cfg(not(feature = "std"))]
|
#[cfg(not(feature = "std"))]
|
||||||
use alloc::{format, string::String, vec::Vec};
|
use alloc::{borrow::Cow, format, string::String, vec::Vec};
|
||||||
|
#[cfg(feature = "std")]
|
||||||
|
use std::borrow::Cow;
|
||||||
|
|
||||||
use crate::error::FormatError;
|
use crate::error::FormatError;
|
||||||
|
use crate::storage::{Storage, len_usize, read_exact_at};
|
||||||
|
|
||||||
/// Magic signature for global heap collections.
|
/// Magic signature for global heap collections.
|
||||||
const GCOL_SIGNATURE: [u8; 4] = *b"GCOL";
|
const GCOL_SIGNATURE: [u8; 4] = *b"GCOL";
|
||||||
@@ -28,19 +31,20 @@ pub struct GlobalHeapObject {
|
|||||||
pub data: Vec<u8>,
|
pub data: Vec<u8>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ensure_len(data: &[u8], offset: usize, needed: usize) -> Result<(), FormatError> {
|
/// Checks that `[offset, offset + needed)` ends by `data_len`.
|
||||||
|
fn ensure_len(data_len: usize, offset: usize, needed: usize) -> Result<(), FormatError> {
|
||||||
match offset.checked_add(needed) {
|
match offset.checked_add(needed) {
|
||||||
Some(end) if end <= data.len() => Ok(()),
|
Some(end) if end <= data_len => Ok(()),
|
||||||
_ => Err(FormatError::UnexpectedEof {
|
_ => Err(FormatError::UnexpectedEof {
|
||||||
expected: offset.saturating_add(needed),
|
expected: offset.saturating_add(needed),
|
||||||
available: data.len(),
|
available: data_len,
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read_length(data: &[u8], offset: usize, length_size: u8) -> Result<u64, FormatError> {
|
fn read_length(data: &[u8], offset: usize, length_size: u8) -> Result<u64, FormatError> {
|
||||||
let s = length_size as usize;
|
let s = length_size as usize;
|
||||||
ensure_len(data, offset, s)?;
|
ensure_len(data.len(), offset, s)?;
|
||||||
let slice = &data[offset..offset + s];
|
let slice = &data[offset..offset + s];
|
||||||
Ok(match length_size {
|
Ok(match length_size {
|
||||||
2 => u16::from_le_bytes([slice[0], slice[1]]) as u64,
|
2 => u16::from_le_bytes([slice[0], slice[1]]) as u64,
|
||||||
@@ -95,7 +99,17 @@ impl GlobalHeapCollection {
|
|||||||
offset: usize,
|
offset: usize,
|
||||||
length_size: u8,
|
length_size: u8,
|
||||||
) -> Result<GlobalHeapCollection, FormatError> {
|
) -> Result<GlobalHeapCollection, FormatError> {
|
||||||
let index = Self::parse_index(file_data, offset, length_size)?;
|
Self::parse_in(file_data, offset as u64, length_size)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`Self::parse`] over any [`Storage`]: one read of the header, one of
|
||||||
|
/// the collection.
|
||||||
|
pub fn parse_in<S: Storage + ?Sized>(
|
||||||
|
file: &S,
|
||||||
|
offset: u64,
|
||||||
|
length_size: u8,
|
||||||
|
) -> Result<GlobalHeapCollection, FormatError> {
|
||||||
|
let (bytes, base, index) = Self::read_collection(file, offset, length_size)?;
|
||||||
Ok(GlobalHeapCollection {
|
Ok(GlobalHeapCollection {
|
||||||
collection_size: index.collection_size,
|
collection_size: index.collection_size,
|
||||||
objects: index
|
objects: index
|
||||||
@@ -104,7 +118,7 @@ impl GlobalHeapCollection {
|
|||||||
.map(|o| GlobalHeapObject {
|
.map(|o| GlobalHeapObject {
|
||||||
index: o.index,
|
index: o.index,
|
||||||
reference_count: o.reference_count,
|
reference_count: o.reference_count,
|
||||||
data: file_data[o.offset..o.offset + o.size].to_vec(),
|
data: bytes[o.offset - base..o.offset - base + o.size].to_vec(),
|
||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
})
|
})
|
||||||
@@ -122,43 +136,72 @@ impl GlobalHeapCollection {
|
|||||||
offset: usize,
|
offset: usize,
|
||||||
length_size: u8,
|
length_size: u8,
|
||||||
) -> Result<GlobalHeapIndex, FormatError> {
|
) -> Result<GlobalHeapIndex, FormatError> {
|
||||||
|
Self::parse_index_in(file_data, offset as u64, length_size)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`Self::parse_index`] over any [`Storage`]: one read of the header,
|
||||||
|
/// one of the collection. The object offsets are file offsets.
|
||||||
|
pub fn parse_index_in<S: Storage + ?Sized>(
|
||||||
|
file: &S,
|
||||||
|
offset: u64,
|
||||||
|
length_size: u8,
|
||||||
|
) -> Result<GlobalHeapIndex, FormatError> {
|
||||||
|
Ok(Self::read_collection(file, offset, length_size)?.2)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read the collection at `offset` and index its objects: the
|
||||||
|
/// collection's bytes, its offset as a `usize`, and the index (with
|
||||||
|
/// file offsets).
|
||||||
|
pub(crate) fn read_collection<S: Storage + ?Sized>(
|
||||||
|
file: &S,
|
||||||
|
offset: u64,
|
||||||
|
length_size: u8,
|
||||||
|
) -> Result<(Cow<'_, [u8]>, usize, GlobalHeapIndex), FormatError> {
|
||||||
|
let file_len = len_usize(file);
|
||||||
// signature(4) + version(1) + reserved(3) + collection_size(length_size),
|
// signature(4) + version(1) + reserved(3) + collection_size(length_size),
|
||||||
// padded to a multiple of 8 as libhdf5 lays it out (`H5HG_SIZEOF_HDR`).
|
// padded to a multiple of 8 as libhdf5 lays it out (`H5HG_SIZEOF_HDR`).
|
||||||
// With 8-byte lengths the padding is 0; with 4-byte lengths it is 4,
|
// With 8-byte lengths the padding is 0; with 4-byte lengths it is 4,
|
||||||
// and reading without it put every object 4 bytes early.
|
// and reading without it put every object 4 bytes early.
|
||||||
let header_size = pad8(8 + length_size as usize);
|
let header_size = pad8(8 + length_size as usize);
|
||||||
ensure_len(file_data, offset, header_size)?;
|
let header = read_exact_at(file, offset, header_size)?;
|
||||||
|
let offset = usize::try_from(offset).map_err(|_| FormatError::UnexpectedEof {
|
||||||
|
expected: usize::MAX,
|
||||||
|
available: file_len,
|
||||||
|
})?;
|
||||||
|
|
||||||
if file_data[offset..offset + 4] != GCOL_SIGNATURE {
|
if header[..4] != GCOL_SIGNATURE {
|
||||||
return Err(FormatError::InvalidGlobalHeapSignature);
|
return Err(FormatError::InvalidGlobalHeapSignature);
|
||||||
}
|
}
|
||||||
|
|
||||||
let version = file_data[offset + 4];
|
let version = header[4];
|
||||||
if version != 1 {
|
if version != 1 {
|
||||||
return Err(FormatError::InvalidGlobalHeapVersion(version));
|
return Err(FormatError::InvalidGlobalHeapVersion(version));
|
||||||
}
|
}
|
||||||
|
|
||||||
let collection_size = read_length(file_data, offset + 8, length_size)?;
|
let collection_size = read_length(&header, 8, length_size)?;
|
||||||
let collection_end = usize::try_from(collection_size)
|
let collection_end = usize::try_from(collection_size)
|
||||||
.ok()
|
.ok()
|
||||||
.and_then(|size| offset.checked_add(size))
|
.and_then(|size| offset.checked_add(size))
|
||||||
.ok_or(FormatError::UnexpectedEof {
|
.ok_or(FormatError::UnexpectedEof {
|
||||||
expected: usize::MAX,
|
expected: usize::MAX,
|
||||||
available: file_data.len(),
|
available: file_len,
|
||||||
})?;
|
})?;
|
||||||
if collection_end > file_data.len() {
|
if collection_end > file_len {
|
||||||
return Err(FormatError::UnexpectedEof {
|
return Err(FormatError::UnexpectedEof {
|
||||||
expected: collection_end,
|
expected: collection_end,
|
||||||
available: file_data.len(),
|
available: file_len,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
let collection = read_exact_at(file, offset as u64, collection_end - offset)?;
|
||||||
|
// Positions below are file offsets; `file_data(p)` is the byte at `p`.
|
||||||
|
let file_data = |p: usize| collection[p - offset];
|
||||||
|
|
||||||
let mut pos = offset + header_size;
|
let mut pos = offset + header_size;
|
||||||
let mut objects = Vec::new();
|
let mut objects = Vec::new();
|
||||||
|
|
||||||
// Parse objects until we hit index 0 (free space) or run out of space
|
// Parse objects until we hit index 0 (free space) or run out of space
|
||||||
while pos + 2 <= collection_end {
|
while pos + 2 <= collection_end {
|
||||||
let object_index = u16::from_le_bytes([file_data[pos], file_data[pos + 1]]);
|
let object_index = u16::from_le_bytes([file_data(pos), file_data(pos + 1)]);
|
||||||
|
|
||||||
if object_index == 0 {
|
if object_index == 0 {
|
||||||
// Free space marker — done
|
// Free space marker — done
|
||||||
@@ -168,10 +211,11 @@ impl GlobalHeapCollection {
|
|||||||
// object_index(2) + reference_count(2) + reserved(4) +
|
// object_index(2) + reference_count(2) + reserved(4) +
|
||||||
// object_size(length_size), padded to 8 (`H5HG_SIZEOF_OBJHDR`).
|
// object_size(length_size), padded to 8 (`H5HG_SIZEOF_OBJHDR`).
|
||||||
let obj_header_size = pad8(8 + length_size as usize);
|
let obj_header_size = pad8(8 + length_size as usize);
|
||||||
ensure_len(&file_data[..collection_end], pos, obj_header_size)?;
|
ensure_len(collection_end, pos, obj_header_size)?;
|
||||||
|
|
||||||
let reference_count = u16::from_le_bytes([file_data[pos + 2], file_data[pos + 3]]);
|
let reference_count = u16::from_le_bytes([file_data(pos + 2), file_data(pos + 3)]);
|
||||||
let object_size = usize::try_from(read_length(file_data, pos + 8, length_size)?)
|
let object_size =
|
||||||
|
usize::try_from(read_length(&collection[pos - offset..], 8, length_size)?)
|
||||||
.map_err(|_| FormatError::Overflow("global heap object size".into()))?;
|
.map_err(|_| FormatError::Overflow("global heap object size".into()))?;
|
||||||
|
|
||||||
pos += obj_header_size;
|
pos += obj_header_size;
|
||||||
@@ -197,10 +241,11 @@ impl GlobalHeapCollection {
|
|||||||
pos = pos.saturating_add(pad8(object_size));
|
pos = pos.saturating_add(pad8(object_size));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(GlobalHeapIndex {
|
let index = GlobalHeapIndex {
|
||||||
collection_size,
|
collection_size,
|
||||||
objects,
|
objects,
|
||||||
})
|
};
|
||||||
|
Ok((collection, offset, index))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get an object by its index.
|
/// Get an object by its index.
|
||||||
@@ -226,7 +271,7 @@ mod tests {
|
|||||||
let mut obj_size_total = 0usize;
|
let mut obj_size_total = 0usize;
|
||||||
for (_, _, data) in objects {
|
for (_, _, data) in objects {
|
||||||
let obj_header = pad8(8 + ls);
|
let obj_header = pad8(8 + ls);
|
||||||
obj_size_total += obj_header + pad8(data.len());
|
obj_size_total += obj_header + pad8(<[u8]>::len(data));
|
||||||
}
|
}
|
||||||
// Free space marker (2 bytes for index 0)
|
// Free space marker (2 bytes for index 0)
|
||||||
obj_size_total += 2;
|
obj_size_total += 2;
|
||||||
@@ -251,15 +296,17 @@ mod tests {
|
|||||||
buf.extend_from_slice(&ref_count.to_le_bytes());
|
buf.extend_from_slice(&ref_count.to_le_bytes());
|
||||||
buf.extend_from_slice(&[0u8; 4]); // reserved
|
buf.extend_from_slice(&[0u8; 4]); // reserved
|
||||||
match length_size {
|
match length_size {
|
||||||
4 => buf.extend_from_slice(&(data.len() as u32).to_le_bytes()),
|
// `<[u8]>::len`: with `Storage` in scope `data.len()` on a
|
||||||
8 => buf.extend_from_slice(&(data.len() as u64).to_le_bytes()),
|
// `&&[u8]` resolves to `Storage::len` (a `u64`).
|
||||||
|
4 => buf.extend_from_slice(&(<[u8]>::len(data) as u32).to_le_bytes()),
|
||||||
|
8 => buf.extend_from_slice(&(<[u8]>::len(data) as u64).to_le_bytes()),
|
||||||
_ => panic!("unsupported"),
|
_ => panic!("unsupported"),
|
||||||
}
|
}
|
||||||
buf.resize(buf.len() + (pad8(8 + ls) - (8 + ls)), 0);
|
buf.resize(buf.len() + (pad8(8 + ls) - (8 + ls)), 0);
|
||||||
buf.extend_from_slice(data);
|
buf.extend_from_slice(data);
|
||||||
// Pad to 8 bytes
|
// Pad to 8 bytes
|
||||||
let padded = pad8(data.len());
|
let padded = pad8(<[u8]>::len(data));
|
||||||
buf.resize(buf.len() + (padded - data.len()), 0);
|
buf.resize(buf.len() + (padded - <[u8]>::len(data)), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Free space marker
|
// Free space marker
|
||||||
@@ -327,4 +374,44 @@ mod tests {
|
|||||||
assert_eq!(coll.objects.len(), 1);
|
assert_eq!(coll.objects.len(), 1);
|
||||||
assert_eq!(coll.objects[0].data, b"test");
|
assert_eq!(coll.objects[0].data, b"test");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Collections, and every truncation of them, index and parse
|
||||||
|
/// identically through a `read_at`-only storage: two reads each.
|
||||||
|
#[test]
|
||||||
|
fn storage_parse_matches_slice_parse() {
|
||||||
|
use crate::storage::CountingStorage;
|
||||||
|
let objs: &[(u16, u16, &[u8])] = &[(1, 1, b"hello"), (2, 3, b"a longer object")];
|
||||||
|
for ls in [4u8, 8] {
|
||||||
|
let coll = build_collection(objs, ls);
|
||||||
|
let mut corrupt = coll.clone();
|
||||||
|
corrupt[8] = 200; // collection size past the end of the file
|
||||||
|
let mut overrun = coll.clone();
|
||||||
|
let size_at = pad8(8 + ls as usize) + 8;
|
||||||
|
overrun[size_at] = 250; // first object runs past the collection
|
||||||
|
for full in [coll, corrupt, overrun] {
|
||||||
|
for at in [0usize, 5] {
|
||||||
|
for cut in 0..=full.len() {
|
||||||
|
let mut f = vec![0u8; at];
|
||||||
|
f.extend_from_slice(&full[..cut]);
|
||||||
|
let storage = CountingStorage::new(f.clone());
|
||||||
|
let want = GlobalHeapCollection::parse(&f, at, ls);
|
||||||
|
let got = GlobalHeapCollection::parse_in(&storage, at as u64, ls);
|
||||||
|
assert_eq!(format!("{got:?}"), format!("{want:?}"));
|
||||||
|
let want = GlobalHeapCollection::parse_index(&f, at, ls);
|
||||||
|
let got = GlobalHeapCollection::parse_index_in(&storage, at as u64, ls);
|
||||||
|
assert_eq!(format!("{got:?}"), format!("{want:?}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let storage = CountingStorage::new(build_collection(objs, 8));
|
||||||
|
assert_eq!(
|
||||||
|
GlobalHeapCollection::parse_in(&storage, 0, 8)
|
||||||
|
.unwrap()
|
||||||
|
.objects
|
||||||
|
.len(),
|
||||||
|
2
|
||||||
|
);
|
||||||
|
assert_eq!(storage.reads(), 2);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,11 +3,13 @@
|
|||||||
#[cfg(not(feature = "std"))]
|
#[cfg(not(feature = "std"))]
|
||||||
use alloc::{string::String, vec::Vec};
|
use alloc::{string::String, vec::Vec};
|
||||||
|
|
||||||
use crate::btree_v1::collect_symbol_table_nodes;
|
use crate::addr::checked_addr;
|
||||||
|
use crate::btree_v1::collect_symbol_table_nodes_in;
|
||||||
use crate::error::FormatError;
|
use crate::error::FormatError;
|
||||||
use crate::local_heap::LocalHeap;
|
use crate::local_heap::LocalHeap;
|
||||||
use crate::message_type::MessageType;
|
use crate::message_type::MessageType;
|
||||||
use crate::object_header::ObjectHeader;
|
use crate::object_header::ObjectHeader;
|
||||||
|
use crate::storage::Storage;
|
||||||
use crate::symbol_table::{SymbolTableMessage, SymbolTableNode};
|
use crate::symbol_table::{SymbolTableMessage, SymbolTableNode};
|
||||||
|
|
||||||
/// A resolved group entry (child name + object header address).
|
/// A resolved group entry (child name + object header address).
|
||||||
@@ -21,23 +23,56 @@ pub struct GroupEntry {
|
|||||||
pub cache_type: u32,
|
pub cache_type: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Given a SymbolTableMessage, resolve all group children.
|
/// Given a SymbolTableMessage, resolve all group children: the group's
|
||||||
|
/// listing.
|
||||||
|
///
|
||||||
|
/// An entry with an empty name fails the listing with
|
||||||
|
/// [`FormatError::InvalidLinkName`], as it fails libhdf5's link iteration
|
||||||
|
/// (`H5G__ent_to_link`: "invalid link name"). Looking a name up
|
||||||
|
/// ([`resolve_path`], and the path resolution in
|
||||||
|
/// [`crate::group_v2::resolve_path_any`]) still works in such a group, as it
|
||||||
|
/// does in libhdf5.
|
||||||
pub fn resolve_v1_group_entries(
|
pub fn resolve_v1_group_entries(
|
||||||
file_data: &[u8],
|
file_data: &[u8],
|
||||||
sym_table_msg: &SymbolTableMessage,
|
sym_table_msg: &SymbolTableMessage,
|
||||||
offset_size: u8,
|
offset_size: u8,
|
||||||
length_size: u8,
|
length_size: u8,
|
||||||
|
) -> Result<Vec<GroupEntry>, FormatError> {
|
||||||
|
resolve_v1_group_entries_in(file_data, sym_table_msg, offset_size, length_size)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`resolve_v1_group_entries`] over any [`Storage`].
|
||||||
|
pub fn resolve_v1_group_entries_in<S: Storage + ?Sized>(
|
||||||
|
file_data: &S,
|
||||||
|
sym_table_msg: &SymbolTableMessage,
|
||||||
|
offset_size: u8,
|
||||||
|
length_size: u8,
|
||||||
|
) -> Result<Vec<GroupEntry>, FormatError> {
|
||||||
|
let entries = v1_group_entries(file_data, sym_table_msg, offset_size, length_size)?;
|
||||||
|
if entries.iter().any(|e| e.name.is_empty()) {
|
||||||
|
return Err(FormatError::InvalidLinkName);
|
||||||
|
}
|
||||||
|
Ok(entries)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Every entry of a v1 group, empty names included — for looking a name up,
|
||||||
|
/// which never matches an empty name.
|
||||||
|
pub(crate) fn v1_group_entries<S: Storage + ?Sized>(
|
||||||
|
file_data: &S,
|
||||||
|
sym_table_msg: &SymbolTableMessage,
|
||||||
|
offset_size: u8,
|
||||||
|
length_size: u8,
|
||||||
) -> Result<Vec<GroupEntry>, FormatError> {
|
) -> Result<Vec<GroupEntry>, FormatError> {
|
||||||
// Parse local heap
|
// Parse local heap
|
||||||
let heap = LocalHeap::parse(
|
let heap = LocalHeap::parse_in(
|
||||||
file_data,
|
file_data,
|
||||||
sym_table_msg.local_heap_address as usize,
|
checked_addr(sym_table_msg.local_heap_address)?,
|
||||||
offset_size,
|
offset_size,
|
||||||
length_size,
|
length_size,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
// Collect all SNOD addresses from B-tree
|
// Collect all SNOD addresses from B-tree
|
||||||
let snod_addrs = collect_symbol_table_nodes(
|
let snod_addrs = collect_symbol_table_nodes_in(
|
||||||
file_data,
|
file_data,
|
||||||
sym_table_msg.btree_address,
|
sym_table_msg.btree_address,
|
||||||
offset_size,
|
offset_size,
|
||||||
@@ -47,15 +82,15 @@ pub fn resolve_v1_group_entries(
|
|||||||
let mut entries = Vec::new();
|
let mut entries = Vec::new();
|
||||||
let mut heap_checked = false;
|
let mut heap_checked = false;
|
||||||
for snod_addr in snod_addrs {
|
for snod_addr in snod_addrs {
|
||||||
let snod = SymbolTableNode::parse(file_data, snod_addr as usize, offset_size)?;
|
let snod = SymbolTableNode::parse_in(file_data, checked_addr(snod_addr)?, offset_size)?;
|
||||||
for entry in &snod.entries {
|
for entry in &snod.entries {
|
||||||
// Like libhdf5, look at the heap's free list only once a name is
|
// Like libhdf5, look at the heap's free list only once a name is
|
||||||
// needed: an empty group with a damaged heap still lists.
|
// needed: an empty group with a damaged heap still lists.
|
||||||
if !heap_checked {
|
if !heap_checked {
|
||||||
heap.validate_free_list(file_data, length_size)?;
|
heap.validate_free_list_in(file_data, length_size)?;
|
||||||
heap_checked = true;
|
heap_checked = true;
|
||||||
}
|
}
|
||||||
let name = heap.read_string(file_data, entry.link_name_offset)?;
|
let name = heap.read_string_in(file_data, entry.link_name_offset)?;
|
||||||
entries.push(GroupEntry {
|
entries.push(GroupEntry {
|
||||||
name,
|
name,
|
||||||
object_header_address: entry.object_header_address,
|
object_header_address: entry.object_header_address,
|
||||||
@@ -79,6 +114,17 @@ pub fn find_v1_soft_link(
|
|||||||
name: &str,
|
name: &str,
|
||||||
offset_size: u8,
|
offset_size: u8,
|
||||||
length_size: u8,
|
length_size: u8,
|
||||||
|
) -> Result<Option<String>, FormatError> {
|
||||||
|
find_v1_soft_link_in(file_data, sym_table_msg, name, offset_size, length_size)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`find_v1_soft_link`] over any [`Storage`].
|
||||||
|
pub fn find_v1_soft_link_in<S: Storage + ?Sized>(
|
||||||
|
file_data: &S,
|
||||||
|
sym_table_msg: &SymbolTableMessage,
|
||||||
|
name: &str,
|
||||||
|
offset_size: u8,
|
||||||
|
length_size: u8,
|
||||||
) -> Result<Option<String>, FormatError> {
|
) -> Result<Option<String>, FormatError> {
|
||||||
let mut found = None;
|
let mut found = None;
|
||||||
for_each_v1_soft_link(
|
for_each_v1_soft_link(
|
||||||
@@ -101,6 +147,16 @@ pub fn v1_soft_links(
|
|||||||
sym_table_msg: &SymbolTableMessage,
|
sym_table_msg: &SymbolTableMessage,
|
||||||
offset_size: u8,
|
offset_size: u8,
|
||||||
length_size: u8,
|
length_size: u8,
|
||||||
|
) -> Result<Vec<(String, String)>, FormatError> {
|
||||||
|
v1_soft_links_in(file_data, sym_table_msg, offset_size, length_size)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`v1_soft_links`] over any [`Storage`].
|
||||||
|
pub fn v1_soft_links_in<S: Storage + ?Sized>(
|
||||||
|
file_data: &S,
|
||||||
|
sym_table_msg: &SymbolTableMessage,
|
||||||
|
offset_size: u8,
|
||||||
|
length_size: u8,
|
||||||
) -> Result<Vec<(String, String)>, FormatError> {
|
) -> Result<Vec<(String, String)>, FormatError> {
|
||||||
let mut links = Vec::new();
|
let mut links = Vec::new();
|
||||||
for_each_v1_soft_link(
|
for_each_v1_soft_link(
|
||||||
@@ -119,21 +175,21 @@ pub fn v1_soft_links(
|
|||||||
|
|
||||||
/// Visit the soft links of a v1 group whose name passes `wanted`, with their
|
/// Visit the soft links of a v1 group whose name passes `wanted`, with their
|
||||||
/// target paths, until `visit` returns false.
|
/// target paths, until `visit` returns false.
|
||||||
fn for_each_v1_soft_link(
|
fn for_each_v1_soft_link<S: Storage + ?Sized>(
|
||||||
file_data: &[u8],
|
file_data: &S,
|
||||||
sym_table_msg: &SymbolTableMessage,
|
sym_table_msg: &SymbolTableMessage,
|
||||||
offset_size: u8,
|
offset_size: u8,
|
||||||
length_size: u8,
|
length_size: u8,
|
||||||
wanted: impl Fn(&str) -> bool,
|
wanted: impl Fn(&str) -> bool,
|
||||||
mut visit: impl FnMut(&str, String) -> bool,
|
mut visit: impl FnMut(&str, String) -> bool,
|
||||||
) -> Result<(), FormatError> {
|
) -> Result<(), FormatError> {
|
||||||
let heap = LocalHeap::parse(
|
let heap = LocalHeap::parse_in(
|
||||||
file_data,
|
file_data,
|
||||||
sym_table_msg.local_heap_address as usize,
|
checked_addr(sym_table_msg.local_heap_address)?,
|
||||||
offset_size,
|
offset_size,
|
||||||
length_size,
|
length_size,
|
||||||
)?;
|
)?;
|
||||||
let snod_addrs = collect_symbol_table_nodes(
|
let snod_addrs = collect_symbol_table_nodes_in(
|
||||||
file_data,
|
file_data,
|
||||||
sym_table_msg.btree_address,
|
sym_table_msg.btree_address,
|
||||||
offset_size,
|
offset_size,
|
||||||
@@ -141,16 +197,16 @@ fn for_each_v1_soft_link(
|
|||||||
)?;
|
)?;
|
||||||
let mut heap_checked = false;
|
let mut heap_checked = false;
|
||||||
for snod_addr in snod_addrs {
|
for snod_addr in snod_addrs {
|
||||||
let snod = SymbolTableNode::parse(file_data, snod_addr as usize, offset_size)?;
|
let snod = SymbolTableNode::parse_in(file_data, checked_addr(snod_addr)?, offset_size)?;
|
||||||
for entry in &snod.entries {
|
for entry in &snod.entries {
|
||||||
if entry.cache_type != CACHE_TYPE_SOFT_LINK {
|
if entry.cache_type != CACHE_TYPE_SOFT_LINK {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if !heap_checked {
|
if !heap_checked {
|
||||||
heap.validate_free_list(file_data, length_size)?;
|
heap.validate_free_list_in(file_data, length_size)?;
|
||||||
heap_checked = true;
|
heap_checked = true;
|
||||||
}
|
}
|
||||||
let name = heap.read_string(file_data, entry.link_name_offset)?;
|
let name = heap.read_string_in(file_data, entry.link_name_offset)?;
|
||||||
if !wanted(&name) {
|
if !wanted(&name) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -160,7 +216,7 @@ fn for_each_v1_soft_link(
|
|||||||
entry.scratch_pad[2],
|
entry.scratch_pad[2],
|
||||||
entry.scratch_pad[3],
|
entry.scratch_pad[3],
|
||||||
]);
|
]);
|
||||||
let target = heap.read_string(file_data, u64::from(value_offset))?;
|
let target = heap.read_string_in(file_data, u64::from(value_offset))?;
|
||||||
if !visit(&name, target) {
|
if !visit(&name, target) {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
@@ -198,6 +254,17 @@ pub fn resolve_path(
|
|||||||
path: &str,
|
path: &str,
|
||||||
offset_size: u8,
|
offset_size: u8,
|
||||||
length_size: u8,
|
length_size: u8,
|
||||||
|
) -> Result<u64, FormatError> {
|
||||||
|
resolve_path_in(file_data, root_sym_table, path, offset_size, length_size)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`resolve_path`] over any [`Storage`].
|
||||||
|
pub fn resolve_path_in<S: Storage + ?Sized>(
|
||||||
|
file_data: &S,
|
||||||
|
root_sym_table: &SymbolTableMessage,
|
||||||
|
path: &str,
|
||||||
|
offset_size: u8,
|
||||||
|
length_size: u8,
|
||||||
) -> Result<u64, FormatError> {
|
) -> Result<u64, FormatError> {
|
||||||
let components: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
|
let components: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
|
||||||
if components.is_empty() {
|
if components.is_empty() {
|
||||||
@@ -207,8 +274,7 @@ pub fn resolve_path(
|
|||||||
let mut current_sym_table = root_sym_table.clone();
|
let mut current_sym_table = root_sym_table.clone();
|
||||||
|
|
||||||
for (i, component) in components.iter().enumerate() {
|
for (i, component) in components.iter().enumerate() {
|
||||||
let entries =
|
let entries = v1_group_entries(file_data, ¤t_sym_table, offset_size, length_size)?;
|
||||||
resolve_v1_group_entries(file_data, ¤t_sym_table, offset_size, length_size)?;
|
|
||||||
|
|
||||||
let found = entries.iter().find(|e| e.name == *component);
|
let found = entries.iter().find(|e| e.name == *component);
|
||||||
match found {
|
match found {
|
||||||
@@ -218,9 +284,9 @@ pub fn resolve_path(
|
|||||||
return Ok(entry.object_header_address);
|
return Ok(entry.object_header_address);
|
||||||
}
|
}
|
||||||
// Not last — must be a group, parse its object header to get symbol table
|
// Not last — must be a group, parse its object header to get symbol table
|
||||||
let obj_header = ObjectHeader::parse(
|
let obj_header = ObjectHeader::parse_in(
|
||||||
file_data,
|
file_data,
|
||||||
entry.object_header_address as usize,
|
checked_addr(entry.object_header_address)?,
|
||||||
offset_size,
|
offset_size,
|
||||||
length_size,
|
length_size,
|
||||||
)?;
|
)?;
|
||||||
@@ -425,6 +491,22 @@ mod tests {
|
|||||||
assert_eq!(entries[1].object_header_address, 0x2000);
|
assert_eq!(entries[1].object_header_address, 0x2000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// cve-2021-46244 `/BAG_root`: a symbol-table entry with an empty name.
|
||||||
|
/// libhdf5 fails the group's listing ("invalid link name"); a lookup of
|
||||||
|
/// the other names still works.
|
||||||
|
#[test]
|
||||||
|
fn empty_entry_name_fails_the_listing_not_a_lookup() {
|
||||||
|
let (file, msg) = build_synthetic_group(&[("", 0x1000, 0), ("elevation", 0x2000, 0)], 8, 8);
|
||||||
|
assert_eq!(
|
||||||
|
resolve_v1_group_entries(&file, &msg, 8, 8).unwrap_err(),
|
||||||
|
FormatError::InvalidLinkName
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
resolve_path(&file, &msg, "elevation", 8, 8).unwrap(),
|
||||||
|
0x2000
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn resolve_path_single_level() {
|
fn resolve_path_single_level() {
|
||||||
let (file, msg) =
|
let (file, msg) =
|
||||||
|
|||||||
@@ -6,7 +6,14 @@
|
|||||||
#[cfg(not(feature = "std"))]
|
#[cfg(not(feature = "std"))]
|
||||||
use alloc::{string::String, vec::Vec};
|
use alloc::{string::String, vec::Vec};
|
||||||
|
|
||||||
use crate::btree_v2::{BTreeV2Header, collect_btree_v2_records};
|
#[cfg(not(feature = "std"))]
|
||||||
|
use alloc::collections::BTreeSet;
|
||||||
|
#[cfg(feature = "std")]
|
||||||
|
use std::collections::BTreeSet;
|
||||||
|
|
||||||
|
use crate::addr::checked_addr;
|
||||||
|
use crate::btree_v2::{BTreeV2Header, collect_btree_v2_records_in, find_btree_v2_records_in};
|
||||||
|
use crate::checksum::jenkins_lookup3;
|
||||||
use crate::error::FormatError;
|
use crate::error::FormatError;
|
||||||
use crate::fractal_heap::FractalHeapHeader;
|
use crate::fractal_heap::FractalHeapHeader;
|
||||||
use crate::group_v1::{self, GroupEntry};
|
use crate::group_v1::{self, GroupEntry};
|
||||||
@@ -14,6 +21,7 @@ use crate::link_info::LinkInfoMessage;
|
|||||||
use crate::link_message::{LinkMessage, LinkTarget};
|
use crate::link_message::{LinkMessage, LinkTarget};
|
||||||
use crate::message_type::MessageType;
|
use crate::message_type::MessageType;
|
||||||
use crate::object_header::ObjectHeader;
|
use crate::object_header::ObjectHeader;
|
||||||
|
use crate::storage::Storage;
|
||||||
use crate::superblock::Superblock;
|
use crate::superblock::Superblock;
|
||||||
use crate::symbol_table::SymbolTableMessage;
|
use crate::symbol_table::SymbolTableMessage;
|
||||||
|
|
||||||
@@ -25,6 +33,16 @@ pub fn resolve_v2_group_entries(
|
|||||||
object_header: &ObjectHeader,
|
object_header: &ObjectHeader,
|
||||||
offset_size: u8,
|
offset_size: u8,
|
||||||
length_size: u8,
|
length_size: u8,
|
||||||
|
) -> Result<Vec<GroupEntry>, FormatError> {
|
||||||
|
resolve_v2_group_entries_in(file_data, object_header, offset_size, length_size)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`resolve_v2_group_entries`] over any [`Storage`].
|
||||||
|
pub fn resolve_v2_group_entries_in<S: Storage + ?Sized>(
|
||||||
|
file_data: &S,
|
||||||
|
object_header: &ObjectHeader,
|
||||||
|
offset_size: u8,
|
||||||
|
length_size: u8,
|
||||||
) -> Result<Vec<GroupEntry>, FormatError> {
|
) -> Result<Vec<GroupEntry>, FormatError> {
|
||||||
// Look for Link Info message to determine storage type
|
// Look for Link Info message to determine storage type
|
||||||
let link_info = find_link_info(object_header, offset_size)?;
|
let link_info = find_link_info(object_header, offset_size)?;
|
||||||
@@ -84,8 +102,8 @@ fn resolve_compact_entries(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Visit every link in dense storage (fractal heap + B-tree v2 name index).
|
/// Visit every link in dense storage (fractal heap + B-tree v2 name index).
|
||||||
fn for_each_dense_link(
|
fn for_each_dense_link<S: Storage + ?Sized>(
|
||||||
file_data: &[u8],
|
file_data: &S,
|
||||||
link_info: &LinkInfoMessage,
|
link_info: &LinkInfoMessage,
|
||||||
fh_addr: u64,
|
fh_addr: u64,
|
||||||
offset_size: u8,
|
offset_size: u8,
|
||||||
@@ -93,14 +111,20 @@ fn for_each_dense_link(
|
|||||||
mut visit: impl FnMut(LinkMessage),
|
mut visit: impl FnMut(LinkMessage),
|
||||||
) -> Result<(), FormatError> {
|
) -> Result<(), FormatError> {
|
||||||
// Parse fractal heap
|
// Parse fractal heap
|
||||||
let fh = FractalHeapHeader::parse(file_data, fh_addr as usize, offset_size, length_size)?;
|
let fh =
|
||||||
|
FractalHeapHeader::parse_in(file_data, checked_addr(fh_addr)?, offset_size, length_size)?;
|
||||||
|
|
||||||
// Parse B-tree v2 for name index
|
// Parse B-tree v2 for name index
|
||||||
let btree_addr = link_info
|
let btree_addr = link_info
|
||||||
.btree_name_index_address
|
.btree_name_index_address
|
||||||
.ok_or_else(|| FormatError::PathNotFound(String::from("no B-tree v2 name index")))?;
|
.ok_or_else(|| FormatError::PathNotFound(String::from("no B-tree v2 name index")))?;
|
||||||
let btree_hdr = BTreeV2Header::parse(file_data, btree_addr as usize, offset_size, length_size)?;
|
let btree_hdr = BTreeV2Header::parse_in(
|
||||||
let records = collect_btree_v2_records(file_data, &btree_hdr, offset_size, length_size)?;
|
file_data,
|
||||||
|
checked_addr(btree_addr)?,
|
||||||
|
offset_size,
|
||||||
|
length_size,
|
||||||
|
)?;
|
||||||
|
let records = collect_btree_v2_records_in(file_data, &btree_hdr, offset_size, length_size)?;
|
||||||
|
|
||||||
for record in &records {
|
for record in &records {
|
||||||
// For type 5 (name index): hash(4) + heap_id(heap_id_length)
|
// For type 5 (name index): hash(4) + heap_id(heap_id_length)
|
||||||
@@ -117,7 +141,7 @@ fn for_each_dense_link(
|
|||||||
let id_bytes = &record.data[id_offset..id_offset + fh.heap_id_length as usize];
|
let id_bytes = &record.data[id_offset..id_offset + fh.heap_id_length as usize];
|
||||||
|
|
||||||
// Read managed object from fractal heap
|
// Read managed object from fractal heap
|
||||||
let link_data = fh.read_managed_object(file_data, id_bytes, offset_size)?;
|
let link_data = fh.read_managed_object_in(file_data, id_bytes, offset_size)?;
|
||||||
if let Some(link) = parse_link(&link_data, offset_size)? {
|
if let Some(link) = parse_link(&link_data, offset_size)? {
|
||||||
visit(link);
|
visit(link);
|
||||||
}
|
}
|
||||||
@@ -126,8 +150,8 @@ fn for_each_dense_link(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Resolve entries from dense storage (fractal heap + B-tree v2).
|
/// Resolve entries from dense storage (fractal heap + B-tree v2).
|
||||||
fn resolve_dense_entries(
|
fn resolve_dense_entries<S: Storage + ?Sized>(
|
||||||
file_data: &[u8],
|
file_data: &S,
|
||||||
link_info: &LinkInfoMessage,
|
link_info: &LinkInfoMessage,
|
||||||
fh_addr: u64,
|
fh_addr: u64,
|
||||||
offset_size: u8,
|
offset_size: u8,
|
||||||
@@ -156,17 +180,16 @@ fn resolve_dense_entries(
|
|||||||
Ok(entries)
|
Ok(entries)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The soft or external link called `name` in this group, if there is one.
|
/// The soft link called `name` in a v1 (symbol table) group, if there is
|
||||||
/// Hard links are what `resolve_group_entries` returns; this is consulted only
|
/// one. Hard links are what `resolve_group_entries` returns; this is
|
||||||
/// when a path component isn't among them.
|
/// consulted only when a path component isn't among them.
|
||||||
fn find_symbolic_link(
|
fn find_v1_symbolic_link<S: Storage + ?Sized>(
|
||||||
file_data: &[u8],
|
file_data: &S,
|
||||||
object_header: &ObjectHeader,
|
object_header: &ObjectHeader,
|
||||||
name: &str,
|
name: &str,
|
||||||
offset_size: u8,
|
offset_size: u8,
|
||||||
length_size: u8,
|
length_size: u8,
|
||||||
) -> Result<Option<LinkTarget>, FormatError> {
|
) -> Result<Option<LinkTarget>, FormatError> {
|
||||||
if is_v1_group(object_header) {
|
|
||||||
let Some(sym_msg) = object_header
|
let Some(sym_msg) = object_header
|
||||||
.messages
|
.messages
|
||||||
.iter()
|
.iter()
|
||||||
@@ -175,16 +198,56 @@ fn find_symbolic_link(
|
|||||||
return Ok(None);
|
return Ok(None);
|
||||||
};
|
};
|
||||||
let stm = SymbolTableMessage::parse(&sym_msg.data, offset_size)?;
|
let stm = SymbolTableMessage::parse(&sym_msg.data, offset_size)?;
|
||||||
return group_v1::find_v1_soft_link(file_data, &stm, name, offset_size, length_size)
|
group_v1::find_v1_soft_link_in(file_data, &stm, name, offset_size, length_size)
|
||||||
.map(|target| target.map(|target_path| LinkTarget::Soft { target_path }));
|
.map(|target| target.map(|target_path| LinkTarget::Soft { target_path }))
|
||||||
}
|
}
|
||||||
if !is_v2_group(object_header) {
|
|
||||||
return Ok(None);
|
/// B-tree v2 record type of a dense group's link name index.
|
||||||
}
|
const LINK_NAME_INDEX: u8 = 5;
|
||||||
let is_symbolic = |t: &LinkTarget| !matches!(t, LinkTarget::Hard { .. });
|
|
||||||
|
/// The links called `name` in a v2 group (a valid group has at most one),
|
||||||
|
/// in storage order: header message order for a compact group, name index
|
||||||
|
/// order for a dense one.
|
||||||
|
///
|
||||||
|
/// In dense storage the link name index (a v2 B-tree of lookup3 name
|
||||||
|
/// hashes, record type 5) is descended to the records with the name's hash,
|
||||||
|
/// and only their links are read from the heap — O(log n) instead of every
|
||||||
|
/// link. libhdf5 orders records with equal hashes by name; all of them are
|
||||||
|
/// read and compared here, so that order does not matter. An index of
|
||||||
|
/// another type is scanned in full.
|
||||||
|
fn links_named<S: Storage + ?Sized>(
|
||||||
|
file_data: &S,
|
||||||
|
object_header: &ObjectHeader,
|
||||||
|
name: &str,
|
||||||
|
offset_size: u8,
|
||||||
|
length_size: u8,
|
||||||
|
) -> Result<Vec<LinkMessage>, FormatError> {
|
||||||
|
let mut found = Vec::new();
|
||||||
let link_info = find_link_info(object_header, offset_size)?;
|
let link_info = find_link_info(object_header, offset_size)?;
|
||||||
let mut found = None;
|
let Some(fh_addr) = link_info.fractal_heap_address else {
|
||||||
if let Some(fh_addr) = link_info.fractal_heap_address {
|
for msg in &object_header.messages {
|
||||||
|
if msg.msg_type == MessageType::Link
|
||||||
|
&& let Some(link) = parse_link(&msg.data, offset_size)?
|
||||||
|
&& link.name == name
|
||||||
|
{
|
||||||
|
found.push(link);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Ok(found);
|
||||||
|
};
|
||||||
|
|
||||||
|
let fh =
|
||||||
|
FractalHeapHeader::parse_in(file_data, checked_addr(fh_addr)?, offset_size, length_size)?;
|
||||||
|
let btree_addr = link_info
|
||||||
|
.btree_name_index_address
|
||||||
|
.ok_or_else(|| FormatError::PathNotFound(String::from("no B-tree v2 name index")))?;
|
||||||
|
let btree_hdr = BTreeV2Header::parse_in(
|
||||||
|
file_data,
|
||||||
|
checked_addr(btree_addr)?,
|
||||||
|
offset_size,
|
||||||
|
length_size,
|
||||||
|
)?;
|
||||||
|
if btree_hdr.tree_type != LINK_NAME_INDEX {
|
||||||
for_each_dense_link(
|
for_each_dense_link(
|
||||||
file_data,
|
file_data,
|
||||||
&link_info,
|
&link_info,
|
||||||
@@ -192,26 +255,176 @@ fn find_symbolic_link(
|
|||||||
offset_size,
|
offset_size,
|
||||||
length_size,
|
length_size,
|
||||||
|link| {
|
|link| {
|
||||||
if link.name == name && is_symbolic(&link.link_target) {
|
if link.name == name {
|
||||||
found = Some(link.link_target);
|
found.push(link);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)?;
|
)?;
|
||||||
} else {
|
return Ok(found);
|
||||||
for msg in &object_header.messages {
|
}
|
||||||
if msg.msg_type == MessageType::Link {
|
|
||||||
let Some(link) = parse_link(&msg.data, offset_size)? else {
|
// Record: hash(4) + heap ID.
|
||||||
|
let hash = jenkins_lookup3(name.as_bytes());
|
||||||
|
let records = find_btree_v2_records_in(file_data, &btree_hdr, offset_size, &mut |r| {
|
||||||
|
match r.get(..4) {
|
||||||
|
Some(h) => u32::from_le_bytes([h[0], h[1], h[2], h[3]]).cmp(&hash),
|
||||||
|
// Too short to hold a hash (a corrupt record size): never a match.
|
||||||
|
None => core::cmp::Ordering::Less,
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
let id_len = usize::from(fh.heap_id_length);
|
||||||
|
for record in &records {
|
||||||
|
let Some(id_bytes) = record.data.get(4..4 + id_len) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
if link.name == name && is_symbolic(&link.link_target) {
|
let link_data = fh.read_managed_object_in(file_data, id_bytes, offset_size)?;
|
||||||
found = Some(link.link_target);
|
if let Some(link) = parse_link(&link_data, offset_size)?
|
||||||
}
|
&& link.name == name
|
||||||
}
|
{
|
||||||
|
found.push(link);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(found)
|
Ok(found)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The link called `name` in a v2 group, if any.
|
||||||
|
///
|
||||||
|
/// A valid group has at most one; libhdf5 cannot create two. If a damaged
|
||||||
|
/// or hand-made group has several, the first wins and the rest are
|
||||||
|
/// ignored, whatever their kind and even if the first cannot be followed.
|
||||||
|
/// That is libhdf5's rule for a compact group (`H5G__compact_lookup` stops
|
||||||
|
/// at the first Link message of that name; h5py then fails to open a
|
||||||
|
/// dangling first link although a later one resolves). For a dense group
|
||||||
|
/// "first" is first in name index order; libhdf5 binary-searches the index
|
||||||
|
/// and may land on another of several exact duplicates. The listing
|
||||||
|
/// ([`resolve_group_children`]), [`resolve_child`] and path resolution all
|
||||||
|
/// apply this rule, so they agree.
|
||||||
|
fn first_link_named<S: Storage + ?Sized>(
|
||||||
|
file_data: &S,
|
||||||
|
object_header: &ObjectHeader,
|
||||||
|
name: &str,
|
||||||
|
offset_size: u8,
|
||||||
|
length_size: u8,
|
||||||
|
) -> Result<Option<LinkMessage>, FormatError> {
|
||||||
|
Ok(
|
||||||
|
links_named(file_data, object_header, name, offset_size, length_size)?
|
||||||
|
.into_iter()
|
||||||
|
.next(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The link [`resolve_path_any`] follows for one path component `name` of
|
||||||
|
/// the group with header `object_header`: a hard link (as `Hard`), else a
|
||||||
|
/// soft or external link of that name, else `None`. Fails with
|
||||||
|
/// `PathNotFound` if the object is not a group.
|
||||||
|
fn lookup_link<S: Storage + ?Sized>(
|
||||||
|
file_data: &S,
|
||||||
|
object_header: &ObjectHeader,
|
||||||
|
name: &str,
|
||||||
|
offset_size: u8,
|
||||||
|
length_size: u8,
|
||||||
|
) -> Result<Option<LinkTarget>, FormatError> {
|
||||||
|
if is_v1_group(object_header) {
|
||||||
|
let entries = resolve_group_entries(file_data, object_header, offset_size, length_size)?;
|
||||||
|
if let Some(e) = entries
|
||||||
|
.iter()
|
||||||
|
.find(|e| e.name == name && e.object_header_address != u64::MAX)
|
||||||
|
{
|
||||||
|
return Ok(Some(LinkTarget::Hard {
|
||||||
|
object_header_address: e.object_header_address,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
return find_v1_symbolic_link(file_data, object_header, name, offset_size, length_size);
|
||||||
|
}
|
||||||
|
if !is_v2_group(object_header) {
|
||||||
|
return Err(FormatError::PathNotFound(String::from(
|
||||||
|
"object header is not a group",
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
Ok(
|
||||||
|
first_link_named(file_data, object_header, name, offset_size, length_size)?
|
||||||
|
.map(|link| link.link_target)
|
||||||
|
.filter(|t| {
|
||||||
|
!matches!(
|
||||||
|
t,
|
||||||
|
LinkTarget::Hard {
|
||||||
|
object_header_address: u64::MAX
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The object header address of the child called `name` of the group at
|
||||||
|
/// `group_address`: the address [`resolve_group_children`] lists under that
|
||||||
|
/// name, or `PathNotFound` if it lists none.
|
||||||
|
///
|
||||||
|
/// A dense group's child is found through its link name index (see
|
||||||
|
/// [`links_named`]) and only the named link is read and, if it is a soft
|
||||||
|
/// link, followed — not every link in the group. A v1 group is listed.
|
||||||
|
pub fn resolve_child(
|
||||||
|
file_data: &[u8],
|
||||||
|
superblock: &Superblock,
|
||||||
|
group_address: u64,
|
||||||
|
name: &str,
|
||||||
|
) -> Result<u64, FormatError> {
|
||||||
|
resolve_child_core(file_data, superblock, group_address, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`resolve_child`] over any [`Storage`]. One with the whole file in memory
|
||||||
|
/// is read as the slice, by code compiled in this crate (see
|
||||||
|
/// [`crate::storage`], "Slice entry points").
|
||||||
|
#[inline]
|
||||||
|
pub fn resolve_child_in<S: Storage + ?Sized>(
|
||||||
|
file_data: &S,
|
||||||
|
superblock: &Superblock,
|
||||||
|
group_address: u64,
|
||||||
|
name: &str,
|
||||||
|
) -> Result<u64, FormatError> {
|
||||||
|
match file_data.as_contiguous() {
|
||||||
|
Some(all) => resolve_child(all, superblock, group_address, name),
|
||||||
|
None => resolve_child_core(file_data, superblock, group_address, name),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_child_core<S: Storage + ?Sized>(
|
||||||
|
file_data: &S,
|
||||||
|
superblock: &Superblock,
|
||||||
|
group_address: u64,
|
||||||
|
name: &str,
|
||||||
|
) -> Result<u64, FormatError> {
|
||||||
|
let os = superblock.offset_size;
|
||||||
|
let ls = superblock.length_size;
|
||||||
|
let not_found = || FormatError::PathNotFound(String::from(name));
|
||||||
|
let header = ObjectHeader::parse_in(file_data, checked_addr(group_address)?, os, ls)?;
|
||||||
|
if !is_v2_group(&header) || is_v1_group(&header) {
|
||||||
|
return resolve_group_children_in(file_data, superblock, group_address)?
|
||||||
|
.into_iter()
|
||||||
|
.find(|e| e.name == name)
|
||||||
|
.map(|e| e.object_header_address)
|
||||||
|
.ok_or_else(not_found);
|
||||||
|
}
|
||||||
|
// The first link of that name only, as the listing (see
|
||||||
|
// `first_link_named`).
|
||||||
|
match first_link_named(file_data, &header, name, os, ls)?.map(|l| l.link_target) {
|
||||||
|
Some(LinkTarget::Hard {
|
||||||
|
object_header_address,
|
||||||
|
}) => Ok(object_header_address),
|
||||||
|
Some(LinkTarget::Soft { target_path }) => {
|
||||||
|
match resolve_path_from_in(file_data, superblock, group_address, &target_path) {
|
||||||
|
// Left out of the listing: dangling, cyclic, or in another file.
|
||||||
|
Err(
|
||||||
|
FormatError::PathNotFound(_)
|
||||||
|
| FormatError::NestingDepthExceeded
|
||||||
|
| FormatError::ExternalLinkUnsupported { .. },
|
||||||
|
) => Err(not_found()),
|
||||||
|
other => other,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(LinkTarget::External { .. }) | None => Err(not_found()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Find and parse the Link Info message from an object header.
|
/// Find and parse the Link Info message from an object header.
|
||||||
fn find_link_info(
|
fn find_link_info(
|
||||||
object_header: &ObjectHeader,
|
object_header: &ObjectHeader,
|
||||||
@@ -254,6 +467,29 @@ pub fn resolve_path_any(
|
|||||||
file_data: &[u8],
|
file_data: &[u8],
|
||||||
superblock: &Superblock,
|
superblock: &Superblock,
|
||||||
path: &str,
|
path: &str,
|
||||||
|
) -> Result<u64, FormatError> {
|
||||||
|
resolve_path_any_core(file_data, superblock, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`resolve_path_any`] over any [`Storage`]. One with the whole file in memory
|
||||||
|
/// is read as the slice, by code compiled in this crate (see
|
||||||
|
/// [`crate::storage`], "Slice entry points").
|
||||||
|
#[inline]
|
||||||
|
pub fn resolve_path_any_in<S: Storage + ?Sized>(
|
||||||
|
file_data: &S,
|
||||||
|
superblock: &Superblock,
|
||||||
|
path: &str,
|
||||||
|
) -> Result<u64, FormatError> {
|
||||||
|
match file_data.as_contiguous() {
|
||||||
|
Some(all) => resolve_path_any(all, superblock, path),
|
||||||
|
None => resolve_path_any_core(file_data, superblock, path),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_path_any_core<S: Storage + ?Sized>(
|
||||||
|
file_data: &S,
|
||||||
|
superblock: &Superblock,
|
||||||
|
path: &str,
|
||||||
) -> Result<u64, FormatError> {
|
) -> Result<u64, FormatError> {
|
||||||
resolve_path_following_links(
|
resolve_path_following_links(
|
||||||
file_data,
|
file_data,
|
||||||
@@ -272,6 +508,16 @@ pub fn resolve_path_from(
|
|||||||
superblock: &Superblock,
|
superblock: &Superblock,
|
||||||
group_address: u64,
|
group_address: u64,
|
||||||
path: &str,
|
path: &str,
|
||||||
|
) -> Result<u64, FormatError> {
|
||||||
|
resolve_path_from_in(file_data, superblock, group_address, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`resolve_path_from`] over any [`Storage`].
|
||||||
|
pub fn resolve_path_from_in<S: Storage + ?Sized>(
|
||||||
|
file_data: &S,
|
||||||
|
superblock: &Superblock,
|
||||||
|
group_address: u64,
|
||||||
|
path: &str,
|
||||||
) -> Result<u64, FormatError> {
|
) -> Result<u64, FormatError> {
|
||||||
let start = if path.starts_with('/') {
|
let start = if path.starts_with('/') {
|
||||||
superblock.root_group_address
|
superblock.root_group_address
|
||||||
@@ -295,10 +541,33 @@ pub fn resolve_group_children(
|
|||||||
file_data: &[u8],
|
file_data: &[u8],
|
||||||
superblock: &Superblock,
|
superblock: &Superblock,
|
||||||
group_address: u64,
|
group_address: u64,
|
||||||
|
) -> Result<Vec<GroupEntry>, FormatError> {
|
||||||
|
resolve_group_children_core(file_data, superblock, group_address)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`resolve_group_children`] over any [`Storage`]. One with the whole file in memory
|
||||||
|
/// is read as the slice, by code compiled in this crate (see
|
||||||
|
/// [`crate::storage`], "Slice entry points").
|
||||||
|
#[inline]
|
||||||
|
pub fn resolve_group_children_in<S: Storage + ?Sized>(
|
||||||
|
file_data: &S,
|
||||||
|
superblock: &Superblock,
|
||||||
|
group_address: u64,
|
||||||
|
) -> Result<Vec<GroupEntry>, FormatError> {
|
||||||
|
match file_data.as_contiguous() {
|
||||||
|
Some(all) => resolve_group_children(all, superblock, group_address),
|
||||||
|
None => resolve_group_children_core(file_data, superblock, group_address),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_group_children_core<S: Storage + ?Sized>(
|
||||||
|
file_data: &S,
|
||||||
|
superblock: &Superblock,
|
||||||
|
group_address: u64,
|
||||||
) -> Result<Vec<GroupEntry>, FormatError> {
|
) -> Result<Vec<GroupEntry>, FormatError> {
|
||||||
let os = superblock.offset_size;
|
let os = superblock.offset_size;
|
||||||
let ls = superblock.length_size;
|
let ls = superblock.length_size;
|
||||||
let header = ObjectHeader::parse(file_data, group_address as usize, os, ls)?;
|
let header = ObjectHeader::parse_in(file_data, checked_addr(group_address)?, os, ls)?;
|
||||||
|
|
||||||
let mut entries = Vec::new();
|
let mut entries = Vec::new();
|
||||||
let mut soft = Vec::new();
|
let mut soft = Vec::new();
|
||||||
@@ -309,13 +578,19 @@ pub fn resolve_group_children(
|
|||||||
.find(|m| m.msg_type == MessageType::SymbolTable)
|
.find(|m| m.msg_type == MessageType::SymbolTable)
|
||||||
.ok_or_else(|| FormatError::PathNotFound(String::from("no symbol table message")))?;
|
.ok_or_else(|| FormatError::PathNotFound(String::from("no symbol table message")))?;
|
||||||
let stm = SymbolTableMessage::parse(&sym_msg.data, os)?;
|
let stm = SymbolTableMessage::parse(&sym_msg.data, os)?;
|
||||||
let all = group_v1::resolve_v1_group_entries(file_data, &stm, os, ls)?;
|
let all = group_v1::resolve_v1_group_entries_in(file_data, &stm, os, ls)?;
|
||||||
if all.iter().any(group_v1::is_v1_soft_link) {
|
if all.iter().any(group_v1::is_v1_soft_link) {
|
||||||
soft = group_v1::v1_soft_links(file_data, &stm, os, ls)?;
|
soft = group_v1::v1_soft_links_in(file_data, &stm, os, ls)?;
|
||||||
}
|
}
|
||||||
entries.extend(all.into_iter().filter(|e| !group_v1::is_v1_soft_link(e)));
|
entries.extend(all.into_iter().filter(|e| !group_v1::is_v1_soft_link(e)));
|
||||||
} else if is_v2_group(&header) {
|
} else if is_v2_group(&header) {
|
||||||
let mut visit = |link: LinkMessage| match link.link_target {
|
// Only the first link of each name counts (see `first_link_named`).
|
||||||
|
let mut seen = BTreeSet::new();
|
||||||
|
let mut visit = |link: LinkMessage| {
|
||||||
|
if !seen.insert(link.name.clone()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
match link.link_target {
|
||||||
LinkTarget::Hard {
|
LinkTarget::Hard {
|
||||||
object_header_address,
|
object_header_address,
|
||||||
} => entries.push(GroupEntry {
|
} => entries.push(GroupEntry {
|
||||||
@@ -325,6 +600,7 @@ pub fn resolve_group_children(
|
|||||||
}),
|
}),
|
||||||
LinkTarget::Soft { target_path } => soft.push((link.name, target_path)),
|
LinkTarget::Soft { target_path } => soft.push((link.name, target_path)),
|
||||||
LinkTarget::External { .. } => {}
|
LinkTarget::External { .. } => {}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
let link_info = find_link_info(&header, os)?;
|
let link_info = find_link_info(&header, os)?;
|
||||||
if let Some(fh_addr) = link_info.fractal_heap_address {
|
if let Some(fh_addr) = link_info.fractal_heap_address {
|
||||||
@@ -341,7 +617,7 @@ pub fn resolve_group_children(
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (name, target) in soft {
|
for (name, target) in soft {
|
||||||
match resolve_path_from(file_data, superblock, group_address, &target) {
|
match resolve_path_from_in(file_data, superblock, group_address, &target) {
|
||||||
Ok(object_header_address) => entries.push(GroupEntry {
|
Ok(object_header_address) => entries.push(GroupEntry {
|
||||||
name,
|
name,
|
||||||
object_header_address,
|
object_header_address,
|
||||||
@@ -364,8 +640,8 @@ pub fn resolve_group_children(
|
|||||||
const MAX_SOFT_LINK_DEPTH: u8 = 16;
|
const MAX_SOFT_LINK_DEPTH: u8 = 16;
|
||||||
|
|
||||||
/// Walk `path` from the group at `start`, following soft links.
|
/// Walk `path` from the group at `start`, following soft links.
|
||||||
fn resolve_path_following_links(
|
fn resolve_path_following_links<S: Storage + ?Sized>(
|
||||||
file_data: &[u8],
|
file_data: &S,
|
||||||
superblock: &Superblock,
|
superblock: &Superblock,
|
||||||
start: u64,
|
start: u64,
|
||||||
path: &str,
|
path: &str,
|
||||||
@@ -383,24 +659,22 @@ fn resolve_path_following_links(
|
|||||||
let ls = superblock.length_size;
|
let ls = superblock.length_size;
|
||||||
|
|
||||||
let mut current_addr = start;
|
let mut current_addr = start;
|
||||||
let mut current_header = ObjectHeader::parse(file_data, start as usize, os, ls)?;
|
let mut current_header = ObjectHeader::parse_in(file_data, checked_addr(start)?, os, ls)?;
|
||||||
|
|
||||||
for (i, component) in components.iter().enumerate() {
|
for (i, component) in components.iter().enumerate() {
|
||||||
let entries = resolve_group_entries(file_data, ¤t_header, os, ls)?;
|
match lookup_link(file_data, ¤t_header, component, os, ls)? {
|
||||||
|
Some(LinkTarget::Hard {
|
||||||
let found = entries
|
object_header_address,
|
||||||
.iter()
|
}) => {
|
||||||
.find(|e| e.name == *component && e.object_header_address != u64::MAX);
|
|
||||||
match found {
|
|
||||||
Some(entry) => {
|
|
||||||
if i == components.len() - 1 {
|
if i == components.len() - 1 {
|
||||||
return Ok(entry.object_header_address);
|
return Ok(object_header_address);
|
||||||
}
|
}
|
||||||
current_addr = entry.object_header_address;
|
current_addr = object_header_address;
|
||||||
current_header = ObjectHeader::parse(file_data, current_addr as usize, os, ls)?;
|
current_header =
|
||||||
|
ObjectHeader::parse_in(file_data, checked_addr(current_addr)?, os, ls)?;
|
||||||
}
|
}
|
||||||
None => {
|
found => {
|
||||||
return match find_symbolic_link(file_data, ¤t_header, component, os, ls)? {
|
return match found {
|
||||||
Some(LinkTarget::Soft { target_path }) => {
|
Some(LinkTarget::Soft { target_path }) => {
|
||||||
if depth >= MAX_SOFT_LINK_DEPTH {
|
if depth >= MAX_SOFT_LINK_DEPTH {
|
||||||
return Err(FormatError::NestingDepthExceeded);
|
return Err(FormatError::NestingDepthExceeded);
|
||||||
@@ -436,8 +710,8 @@ fn resolve_path_following_links(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Resolve group entries from an object header, auto-detecting v1 vs v2.
|
/// Resolve group entries from an object header, auto-detecting v1 vs v2.
|
||||||
fn resolve_group_entries(
|
fn resolve_group_entries<S: Storage + ?Sized>(
|
||||||
file_data: &[u8],
|
file_data: &S,
|
||||||
object_header: &ObjectHeader,
|
object_header: &ObjectHeader,
|
||||||
offset_size: u8,
|
offset_size: u8,
|
||||||
length_size: u8,
|
length_size: u8,
|
||||||
@@ -450,9 +724,11 @@ fn resolve_group_entries(
|
|||||||
.find(|m| m.msg_type == MessageType::SymbolTable)
|
.find(|m| m.msg_type == MessageType::SymbolTable)
|
||||||
.ok_or_else(|| FormatError::PathNotFound(String::from("no symbol table message")))?;
|
.ok_or_else(|| FormatError::PathNotFound(String::from("no symbol table message")))?;
|
||||||
let stm = SymbolTableMessage::parse(&sym_msg.data, offset_size)?;
|
let stm = SymbolTableMessage::parse(&sym_msg.data, offset_size)?;
|
||||||
group_v1::resolve_v1_group_entries(file_data, &stm, offset_size, length_size)
|
// A lookup: an entry with an empty name (which fails a listing) is
|
||||||
|
// skipped by the name comparison, as in libhdf5.
|
||||||
|
group_v1::v1_group_entries(file_data, &stm, offset_size, length_size)
|
||||||
} else if is_v2_group(object_header) {
|
} else if is_v2_group(object_header) {
|
||||||
resolve_v2_group_entries(file_data, object_header, offset_size, length_size)
|
resolve_v2_group_entries_in(file_data, object_header, offset_size, length_size)
|
||||||
} else {
|
} else {
|
||||||
Err(FormatError::PathNotFound(String::from(
|
Err(FormatError::PathNotFound(String::from(
|
||||||
"object header is not a group",
|
"object header is not a group",
|
||||||
|
|||||||
@@ -112,7 +112,8 @@ pub fn partition(
|
|||||||
|
|
||||||
for idx in 0..num_items {
|
for idx in 0..num_items {
|
||||||
let h = fxhash_combine(seed, idx as u64);
|
let h = fxhash_combine(seed, idx as u64);
|
||||||
let lane = (h % num_lanes as u64) as usize;
|
// Below `num_lanes`, so it fits.
|
||||||
|
let lane = crate::addr::saturating_usize(h % num_lanes as u64);
|
||||||
lanes[lane].push(idx);
|
lanes[lane].push(idx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -57,10 +57,12 @@
|
|||||||
#[cfg(not(feature = "std"))]
|
#[cfg(not(feature = "std"))]
|
||||||
extern crate alloc;
|
extern crate alloc;
|
||||||
|
|
||||||
|
pub mod addr;
|
||||||
pub mod attribute;
|
pub mod attribute;
|
||||||
pub mod attribute_info;
|
pub mod attribute_info;
|
||||||
pub mod btree_v1;
|
pub mod btree_v1;
|
||||||
pub mod btree_v2;
|
pub mod btree_v2;
|
||||||
|
mod btree_v2_write;
|
||||||
mod bulk_alloc;
|
mod bulk_alloc;
|
||||||
pub mod checksum;
|
pub mod checksum;
|
||||||
pub mod chunk_cache;
|
pub mod chunk_cache;
|
||||||
@@ -86,11 +88,15 @@ pub mod filters;
|
|||||||
mod filters_bitshuffle;
|
mod filters_bitshuffle;
|
||||||
#[cfg(feature = "blosc")]
|
#[cfg(feature = "blosc")]
|
||||||
pub mod filters_blosc;
|
pub mod filters_blosc;
|
||||||
|
#[cfg(feature = "blosc2")]
|
||||||
|
pub mod filters_blosc2;
|
||||||
#[cfg(feature = "bzip2")]
|
#[cfg(feature = "bzip2")]
|
||||||
mod filters_bzip2;
|
mod filters_bzip2;
|
||||||
#[cfg(feature = "lzf")]
|
#[cfg(feature = "lzf")]
|
||||||
pub mod filters_lzf;
|
pub mod filters_lzf;
|
||||||
mod filters_szip;
|
mod filters_szip;
|
||||||
|
#[cfg(feature = "zfp")]
|
||||||
|
pub mod filters_zfp;
|
||||||
pub mod fixed_array;
|
pub mod fixed_array;
|
||||||
pub mod float16;
|
pub mod float16;
|
||||||
pub mod fractal_heap;
|
pub mod fractal_heap;
|
||||||
@@ -104,6 +110,7 @@ pub mod lane_partition;
|
|||||||
pub mod link_info;
|
pub mod link_info;
|
||||||
pub mod link_message;
|
pub mod link_message;
|
||||||
pub mod local_heap;
|
pub mod local_heap;
|
||||||
|
pub mod lookup_stats;
|
||||||
pub mod message_type;
|
pub mod message_type;
|
||||||
pub mod metadata_cache;
|
pub mod metadata_cache;
|
||||||
pub mod metadata_index;
|
pub mod metadata_index;
|
||||||
@@ -117,7 +124,9 @@ pub mod property_list;
|
|||||||
pub mod selection;
|
pub mod selection;
|
||||||
pub mod shared_message;
|
pub mod shared_message;
|
||||||
pub mod signature;
|
pub mod signature;
|
||||||
|
pub mod storage;
|
||||||
pub mod superblock;
|
pub mod superblock;
|
||||||
|
pub mod superblock_ext;
|
||||||
pub mod symbol_table;
|
pub mod symbol_table;
|
||||||
#[cfg(all(
|
#[cfg(all(
|
||||||
test,
|
test,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
#[cfg(not(feature = "std"))]
|
#[cfg(not(feature = "std"))]
|
||||||
use alloc::{string::String, vec::Vec};
|
use alloc::{string::String, vec::Vec};
|
||||||
|
|
||||||
|
use crate::addr::to_usize;
|
||||||
use crate::datatype::CharacterSet;
|
use crate::datatype::CharacterSet;
|
||||||
use crate::error::FormatError;
|
use crate::error::FormatError;
|
||||||
|
|
||||||
@@ -247,7 +248,7 @@ impl LinkMessage {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Link name length
|
// Link name length
|
||||||
let name_len = read_offset(data, pos, name_size_field_width)? as usize;
|
let name_len = to_usize(read_offset(data, pos, name_size_field_width)?)?;
|
||||||
pos += name_size_field_width as usize;
|
pos += name_size_field_width as usize;
|
||||||
|
|
||||||
// Link name
|
// Link name
|
||||||
|
|||||||
@@ -3,7 +3,9 @@
|
|||||||
#[cfg(not(feature = "std"))]
|
#[cfg(not(feature = "std"))]
|
||||||
use alloc::string::String;
|
use alloc::string::String;
|
||||||
|
|
||||||
|
use crate::addr::to_usize;
|
||||||
use crate::error::FormatError;
|
use crate::error::FormatError;
|
||||||
|
use crate::storage::{Storage, len_usize, read_exact_at};
|
||||||
|
|
||||||
/// Parsed HDF5 Local Heap header.
|
/// Parsed HDF5 Local Heap header.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@@ -16,21 +18,6 @@ pub struct LocalHeap {
|
|||||||
pub data_segment_address: u64,
|
pub data_segment_address: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Checks that `[offset, offset + needed)` fits within `data`, guarding the
|
|
||||||
/// addition against `usize` overflow from a crafted near-`usize::MAX` offset.
|
|
||||||
fn ensure_len(data: &[u8], offset: usize, needed: usize) -> Result<(), FormatError> {
|
|
||||||
if offset
|
|
||||||
.checked_add(needed)
|
|
||||||
.is_none_or(|end| end > data.len())
|
|
||||||
{
|
|
||||||
return Err(FormatError::UnexpectedEof {
|
|
||||||
expected: offset.saturating_add(needed),
|
|
||||||
available: data.len(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn read_offset(data: &[u8], pos: usize, size: u8) -> Result<u64, FormatError> {
|
fn read_offset(data: &[u8], pos: usize, size: u8) -> Result<u64, FormatError> {
|
||||||
let s = size as usize;
|
let s = size as usize;
|
||||||
if pos.checked_add(s).is_none_or(|end| end > data.len()) {
|
if pos.checked_add(s).is_none_or(|end| end > data.len()) {
|
||||||
@@ -50,6 +37,10 @@ fn read_offset(data: &[u8], pos: usize, size: u8) -> Result<u64, FormatError> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// First read of a name on a backend without the file in memory: most link
|
||||||
|
/// names are shorter than this.
|
||||||
|
const NAME_READ_START: usize = 64;
|
||||||
|
|
||||||
impl LocalHeap {
|
impl LocalHeap {
|
||||||
/// Parse a local heap header at the given offset in the file data.
|
/// Parse a local heap header at the given offset in the file data.
|
||||||
pub fn parse(
|
pub fn parse(
|
||||||
@@ -57,12 +48,24 @@ impl LocalHeap {
|
|||||||
offset: usize,
|
offset: usize,
|
||||||
offset_size: u8,
|
offset_size: u8,
|
||||||
length_size: u8,
|
length_size: u8,
|
||||||
|
) -> Result<LocalHeap, FormatError> {
|
||||||
|
Self::parse_in(file_data, offset as u64, offset_size, length_size)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`Self::parse`] over any [`Storage`]: one read of the header.
|
||||||
|
pub fn parse_in<S: Storage + ?Sized>(
|
||||||
|
file: &S,
|
||||||
|
offset: u64,
|
||||||
|
offset_size: u8,
|
||||||
|
length_size: u8,
|
||||||
) -> Result<LocalHeap, FormatError> {
|
) -> Result<LocalHeap, FormatError> {
|
||||||
// signature(4) + version(1) + reserved(3) = 8, then length_size*2 + offset_size
|
// signature(4) + version(1) + reserved(3) = 8, then length_size*2 + offset_size
|
||||||
let ls = length_size as usize;
|
let ls = length_size as usize;
|
||||||
let os = offset_size as usize;
|
let os = offset_size as usize;
|
||||||
let total = 8 + ls * 2 + os;
|
let total = 8 + ls * 2 + os;
|
||||||
ensure_len(file_data, offset, total)?;
|
let header = read_exact_at(file, offset, total)?;
|
||||||
|
let file_data: &[u8] = &header;
|
||||||
|
let offset = 0usize;
|
||||||
|
|
||||||
if &file_data[offset..offset + 4] != b"HEAP" {
|
if &file_data[offset..offset + 4] != b"HEAP" {
|
||||||
return Err(FormatError::InvalidLocalHeapSignature);
|
return Err(FormatError::InvalidLocalHeapSignature);
|
||||||
@@ -99,6 +102,16 @@ impl LocalHeap {
|
|||||||
/// The end of the list is `H5HL_FREE_NULL` (1); an all-ones value (the
|
/// The end of the list is `H5HL_FREE_NULL` (1); an all-ones value (the
|
||||||
/// undefined address) is accepted as "no free list" too.
|
/// undefined address) is accepted as "no free list" too.
|
||||||
pub fn validate_free_list(&self, file_data: &[u8], length_size: u8) -> Result<(), FormatError> {
|
pub fn validate_free_list(&self, file_data: &[u8], length_size: u8) -> Result<(), FormatError> {
|
||||||
|
self.validate_free_list_in(file_data, length_size)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`Self::validate_free_list`] over any [`Storage`]: two small reads
|
||||||
|
/// per free block.
|
||||||
|
pub fn validate_free_list_in<S: Storage + ?Sized>(
|
||||||
|
&self,
|
||||||
|
file: &S,
|
||||||
|
length_size: u8,
|
||||||
|
) -> Result<(), FormatError> {
|
||||||
const FREE_NULL: u64 = 1;
|
const FREE_NULL: u64 = 1;
|
||||||
let ls = length_size as usize;
|
let ls = length_size as usize;
|
||||||
let undefined = if ls >= 8 {
|
let undefined = if ls >= 8 {
|
||||||
@@ -123,11 +136,12 @@ impl LocalHeap {
|
|||||||
.and_then(|a| usize::try_from(a).ok())
|
.and_then(|a| usize::try_from(a).ok())
|
||||||
.ok_or(FormatError::InvalidLocalHeapFreeList)?;
|
.ok_or(FormatError::InvalidLocalHeapFreeList)?;
|
||||||
let block_offset = next;
|
let block_offset = next;
|
||||||
next = read_offset(file_data, at, length_size)?;
|
next = read_offset(&read_exact_at(file, at as u64, ls)?, 0, length_size)?;
|
||||||
if next == 0 {
|
if next == 0 {
|
||||||
return Err(FormatError::InvalidLocalHeapFreeList);
|
return Err(FormatError::InvalidLocalHeapFreeList);
|
||||||
}
|
}
|
||||||
let block_size = read_offset(file_data, at + ls, length_size)?;
|
let block_size =
|
||||||
|
read_offset(&read_exact_at(file, (at + ls) as u64, ls)?, 0, length_size)?;
|
||||||
if block_offset
|
if block_offset
|
||||||
.checked_add(block_size)
|
.checked_add(block_size)
|
||||||
.is_none_or(|end| end > size)
|
.is_none_or(|end| end > size)
|
||||||
@@ -140,43 +154,65 @@ impl LocalHeap {
|
|||||||
|
|
||||||
/// Read a null-terminated string from the heap's data segment at the given byte offset.
|
/// Read a null-terminated string from the heap's data segment at the given byte offset.
|
||||||
pub fn read_string(&self, file_data: &[u8], string_offset: u64) -> Result<String, FormatError> {
|
pub fn read_string(&self, file_data: &[u8], string_offset: u64) -> Result<String, FormatError> {
|
||||||
let seg_addr = self.data_segment_address as usize;
|
self.read_string_in(file_data, string_offset)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`Self::read_string`] over any [`Storage`]: one read of up to 64
|
||||||
|
/// bytes for a short name, more (each four times the last) up to the end
|
||||||
|
/// of the data segment for a longer one.
|
||||||
|
pub fn read_string_in<S: Storage + ?Sized>(
|
||||||
|
&self,
|
||||||
|
file: &S,
|
||||||
|
string_offset: u64,
|
||||||
|
) -> Result<String, FormatError> {
|
||||||
|
let file_len = len_usize(file);
|
||||||
|
let seg_addr = to_usize(self.data_segment_address)?;
|
||||||
let str_start =
|
let str_start =
|
||||||
seg_addr
|
seg_addr
|
||||||
.checked_add(string_offset as usize)
|
.checked_add(to_usize(string_offset)?)
|
||||||
.ok_or(FormatError::Overflow(
|
.ok_or(FormatError::Overflow(
|
||||||
"local heap seg_addr + string_offset overflow".into(),
|
"local heap seg_addr + string_offset overflow".into(),
|
||||||
))?;
|
))?;
|
||||||
let seg_end = seg_addr
|
let seg_end = seg_addr
|
||||||
.checked_add(self.data_segment_size as usize)
|
.checked_add(to_usize(self.data_segment_size)?)
|
||||||
.ok_or(FormatError::Overflow(
|
.ok_or(FormatError::Overflow(
|
||||||
"local heap seg_addr + data_segment_size overflow".into(),
|
"local heap seg_addr + data_segment_size overflow".into(),
|
||||||
))?;
|
))?;
|
||||||
|
|
||||||
if str_start >= file_data.len() || str_start >= seg_end {
|
if str_start >= file_len || str_start >= seg_end {
|
||||||
return Err(FormatError::UnexpectedEof {
|
return Err(FormatError::UnexpectedEof {
|
||||||
expected: str_start + 1,
|
expected: str_start + 1,
|
||||||
available: file_data.len(),
|
available: file_len,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find null terminator
|
// Find the null terminator, which lies before the end of the data
|
||||||
let search_end = seg_end.min(file_data.len());
|
// segment (or of the file). In memory that is one borrowed slice;
|
||||||
let mut end = str_start;
|
// otherwise the bytes are read in growing pieces, so a name costs a
|
||||||
while end < search_end && file_data[end] != 0 {
|
// read of about its own length, not of the rest of the segment
|
||||||
end += 1;
|
// (whose size is an untrusted header field).
|
||||||
|
let search_end = seg_end.min(file_len);
|
||||||
|
let total = search_end - str_start;
|
||||||
|
let mut want = if file.as_contiguous().is_some() {
|
||||||
|
total
|
||||||
|
} else {
|
||||||
|
total.min(NAME_READ_START)
|
||||||
|
};
|
||||||
|
loop {
|
||||||
|
let rest = read_exact_at(file, str_start as u64, want)?;
|
||||||
|
if let Some(len) = rest.iter().position(|&b| b == 0) {
|
||||||
|
let s = core::str::from_utf8(&rest[..len])
|
||||||
|
.map_err(|_| FormatError::InvalidLocalHeapSignature)?;
|
||||||
|
return Ok(String::from(s));
|
||||||
}
|
}
|
||||||
|
if want == total {
|
||||||
if end >= search_end {
|
|
||||||
return Err(FormatError::UnexpectedEof {
|
return Err(FormatError::UnexpectedEof {
|
||||||
expected: end + 1,
|
expected: search_end + 1,
|
||||||
available: search_end,
|
available: search_end,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
want = want.saturating_mul(4).min(total);
|
||||||
let s = core::str::from_utf8(&file_data[str_start..end])
|
}
|
||||||
.map_err(|_| FormatError::InvalidLocalHeapSignature)?;
|
|
||||||
Ok(String::from(s))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -345,4 +381,77 @@ mod tests {
|
|||||||
let err = LocalHeap::parse(&file, 0, 8, 8).unwrap_err();
|
let err = LocalHeap::parse(&file, 0, 8, 8).unwrap_err();
|
||||||
assert_eq!(err, FormatError::InvalidLocalHeapVersion(1));
|
assert_eq!(err, FormatError::InvalidLocalHeapVersion(1));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Header, free list and strings read identically through a
|
||||||
|
/// `read_at`-only storage, for every truncation of the file.
|
||||||
|
#[test]
|
||||||
|
fn storage_reads_match_slice_reads() {
|
||||||
|
use crate::storage::CountingStorage;
|
||||||
|
let plain = build_heap_file(0, 64, &["", "alpha", "beta"], 8, 8);
|
||||||
|
// A free block of 16 bytes at segment offset 12, ending the list.
|
||||||
|
let mut free = build_heap_file(0, 64, &["", "alpha", "beta", &"x".repeat(20)], 8, 8);
|
||||||
|
free[16..24].copy_from_slice(&12u64.to_le_bytes());
|
||||||
|
free[64 + 12..64 + 20].copy_from_slice(&1u64.to_le_bytes());
|
||||||
|
free[64 + 20..64 + 28].copy_from_slice(&16u64.to_le_bytes());
|
||||||
|
let mut bad_free = free.clone();
|
||||||
|
bad_free[64 + 20..64 + 28].copy_from_slice(&99u64.to_le_bytes());
|
||||||
|
for full in [plain, free, bad_free] {
|
||||||
|
for cut in 0..=full.len() {
|
||||||
|
let f = &full[..cut];
|
||||||
|
let storage = CountingStorage::new(f.to_vec());
|
||||||
|
let want = LocalHeap::parse(f, 0, 8, 8);
|
||||||
|
let got = LocalHeap::parse_in(&storage, 0, 8, 8);
|
||||||
|
assert_eq!(format!("{got:?}"), format!("{want:?}"));
|
||||||
|
let Ok(heap) = want else { continue };
|
||||||
|
assert_eq!(
|
||||||
|
heap.validate_free_list_in(&storage, 8),
|
||||||
|
heap.validate_free_list(f, 8)
|
||||||
|
);
|
||||||
|
for off in [0u64, 1, 2, 6, 7, 11, 100] {
|
||||||
|
assert_eq!(heap.read_string_in(&storage, off), heap.read_string(f, off));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Names of every length around the first read's size, and one with no
|
||||||
|
/// terminator, read identically through a `read_at`-only storage; a
|
||||||
|
/// short name in a heap whose header claims a huge data segment costs
|
||||||
|
/// one small read, not a read of the rest of the file.
|
||||||
|
#[test]
|
||||||
|
fn long_names_and_hostile_segment_sizes() {
|
||||||
|
use crate::storage::CountingStorage;
|
||||||
|
let names: Vec<String> = [0usize, 1, 63, 64, 65, 255, 256, 257, 1000, 5000]
|
||||||
|
.iter()
|
||||||
|
.map(|&n| "n".repeat(n))
|
||||||
|
.collect();
|
||||||
|
let refs: Vec<&str> = names.iter().map(String::as_str).collect();
|
||||||
|
let mut file = build_heap_file(0, 64, &refs, 8, 8);
|
||||||
|
let heap = LocalHeap::parse(&file, 0, 8, 8).unwrap();
|
||||||
|
let storage = CountingStorage::new(file.clone());
|
||||||
|
let mut off = 0u64;
|
||||||
|
for name in &names {
|
||||||
|
let got = heap.read_string_in(&storage, off);
|
||||||
|
assert_eq!(got, heap.read_string(&file, off));
|
||||||
|
assert_eq!(got.unwrap(), *name);
|
||||||
|
off += name.len() as u64 + 1;
|
||||||
|
}
|
||||||
|
// The last name loses its terminator: both report the same error.
|
||||||
|
let seg_end = 64 + heap.data_segment_size as usize;
|
||||||
|
file[seg_end - 1] = b'n';
|
||||||
|
let storage = CountingStorage::new(file.clone());
|
||||||
|
let last = off - names[names.len() - 1].len() as u64 - 1;
|
||||||
|
let want = heap.read_string(&file, last);
|
||||||
|
assert!(want.is_err());
|
||||||
|
assert_eq!(heap.read_string_in(&storage, last), want);
|
||||||
|
|
||||||
|
// A 64 MiB file whose heap claims a data segment reaching its end.
|
||||||
|
let mut big = build_heap_file(0, 64, &["short", "names"], 8, 8);
|
||||||
|
big.resize(64 << 20, 0);
|
||||||
|
big[8..16].copy_from_slice(&((64u64 << 20) - 64).to_le_bytes());
|
||||||
|
let heap = LocalHeap::parse(&big, 0, 8, 8).unwrap();
|
||||||
|
let storage = CountingStorage::new(big.clone());
|
||||||
|
assert_eq!(heap.read_string_in(&storage, 6).unwrap(), "names");
|
||||||
|
assert_eq!((storage.reads(), storage.bytes_read()), (1, 64));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
//! Work counters for tests of lookup cost (feature `lookup-stats`).
|
||||||
|
//!
|
||||||
|
//! Counts fractal-heap objects read — each is one link or attribute message
|
||||||
|
//! decoded out of a dense group or dense attribute storage — so a test can
|
||||||
|
//! check that finding one name reads a handful of them, not the whole group.
|
||||||
|
//! Per thread, so tests running in parallel do not see each other's reads.
|
||||||
|
//! Without the feature the counting compiles to nothing.
|
||||||
|
|
||||||
|
#[cfg(feature = "lookup-stats")]
|
||||||
|
std::thread_local! {
|
||||||
|
static HEAP_OBJECTS: core::cell::Cell<u64> = const { core::cell::Cell::new(0) };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Record one heap object read.
|
||||||
|
#[inline(always)]
|
||||||
|
pub(crate) fn heap_object_read() {
|
||||||
|
#[cfg(feature = "lookup-stats")]
|
||||||
|
HEAP_OBJECTS.with(|c| c.set(c.get() + 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Heap objects read on this thread since the last [`reset`].
|
||||||
|
#[cfg(feature = "lookup-stats")]
|
||||||
|
pub fn heap_objects_read() -> u64 {
|
||||||
|
HEAP_OBJECTS.with(core::cell::Cell::get)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Zero this thread's counters.
|
||||||
|
#[cfg(feature = "lookup-stats")]
|
||||||
|
pub fn reset() {
|
||||||
|
HEAP_OBJECTS.with(|c| c.set(0));
|
||||||
|
}
|
||||||
@@ -1,12 +1,16 @@
|
|||||||
//! HDF5 Object Header parsing (v1 and v2).
|
//! HDF5 Object Header parsing (v1 and v2).
|
||||||
|
|
||||||
#[cfg(not(feature = "std"))]
|
#[cfg(not(feature = "std"))]
|
||||||
use alloc::vec::Vec;
|
use alloc::{boxed::Box, collections::BTreeSet, vec::Vec};
|
||||||
|
#[cfg(feature = "std")]
|
||||||
|
use std::collections::BTreeSet;
|
||||||
|
|
||||||
use byteorder::{ByteOrder, LittleEndian};
|
use byteorder::{ByteOrder, LittleEndian};
|
||||||
|
|
||||||
|
use crate::addr::to_usize;
|
||||||
use crate::error::FormatError;
|
use crate::error::FormatError;
|
||||||
use crate::message_type::MessageType;
|
use crate::message_type::MessageType;
|
||||||
|
use crate::storage::{Storage, Window, len_usize, read_exact_at};
|
||||||
|
|
||||||
/// OHDR signature for v2 object headers.
|
/// OHDR signature for v2 object headers.
|
||||||
const OHDR_SIGNATURE: [u8; 4] = *b"OHDR";
|
const OHDR_SIGNATURE: [u8; 4] = *b"OHDR";
|
||||||
@@ -75,42 +79,122 @@ fn read_offset(data: &[u8], pos: usize, size: u8) -> Result<u64, FormatError> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The kind of object an object header describes.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum ObjectClass {
|
||||||
|
/// A group: the header has a Symbol Table or a Link Info message.
|
||||||
|
Group,
|
||||||
|
/// A dataset: the header has a Datatype and a Dataspace message.
|
||||||
|
Dataset,
|
||||||
|
/// A committed (named) datatype: a Datatype message, no Dataspace.
|
||||||
|
NamedDatatype,
|
||||||
|
}
|
||||||
|
|
||||||
impl ObjectHeader {
|
impl ObjectHeader {
|
||||||
|
/// The kind of object this header describes, decided as libhdf5 decides
|
||||||
|
/// it (`H5O__obj_class_real`): group first (a Symbol Table or Link Info
|
||||||
|
/// message), then dataset (a Datatype *and* a Dataspace message — not a
|
||||||
|
/// Data Layout message), then named datatype (a Datatype message).
|
||||||
|
/// `None` when none applies; libhdf5 then cannot open the object
|
||||||
|
/// ("unable to determine object type").
|
||||||
|
///
|
||||||
|
/// A header with a Datatype and a Data Layout message but no Dataspace
|
||||||
|
/// is a named datatype to libhdf5, not a dataset.
|
||||||
|
pub fn object_class(&self) -> Option<ObjectClass> {
|
||||||
|
let has = |t: MessageType| self.messages.iter().any(|m| m.msg_type == t);
|
||||||
|
if has(MessageType::SymbolTable) || has(MessageType::LinkInfo) {
|
||||||
|
Some(ObjectClass::Group)
|
||||||
|
} else if has(MessageType::Datatype) && has(MessageType::Dataspace) {
|
||||||
|
Some(ObjectClass::Dataset)
|
||||||
|
} else if has(MessageType::Datatype) {
|
||||||
|
Some(ObjectClass::NamedDatatype)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Parse an object header at the given offset in the data buffer.
|
/// Parse an object header at the given offset in the data buffer.
|
||||||
///
|
///
|
||||||
/// `offset_size` and `length_size` come from the superblock.
|
/// `offset_size` and `length_size` come from the superblock.
|
||||||
|
#[inline]
|
||||||
pub fn parse(
|
pub fn parse(
|
||||||
data: &[u8],
|
data: &[u8],
|
||||||
offset: usize,
|
offset: usize,
|
||||||
offset_size: u8,
|
offset_size: u8,
|
||||||
length_size: u8,
|
length_size: u8,
|
||||||
) -> Result<ObjectHeader, FormatError> {
|
) -> Result<ObjectHeader, FormatError> {
|
||||||
ensure_len(data, offset, 4)?;
|
Self::parse_slice(data, offset as u64, offset_size, length_size)
|
||||||
if data[offset..offset + 4] == OHDR_SIGNATURE {
|
}
|
||||||
Self::parse_v2(data, offset, offset_size, length_size)
|
|
||||||
} else {
|
/// [`Self::parse`] over any [`Storage`].
|
||||||
Self::parse_v1(data, offset, offset_size, length_size)
|
///
|
||||||
|
/// Reads the prefix (at most [`V2_PREFIX_MAX`] bytes, signature
|
||||||
|
/// included), then each chunk as one bounded read, continuation chunks
|
||||||
|
/// included. A storage with the whole file in memory is parsed as its
|
||||||
|
/// slice, by code compiled in this crate (see
|
||||||
|
/// [`crate::storage`], "Slice entry points").
|
||||||
|
#[inline]
|
||||||
|
pub fn parse_in<S: Storage + ?Sized>(
|
||||||
|
file: &S,
|
||||||
|
offset: u64,
|
||||||
|
offset_size: u8,
|
||||||
|
length_size: u8,
|
||||||
|
) -> Result<ObjectHeader, FormatError> {
|
||||||
|
match file.as_contiguous() {
|
||||||
|
Some(all) => Self::parse_slice(all, offset, offset_size, length_size),
|
||||||
|
None => Self::parse_storage(file, offset, offset_size, length_size),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_v1(
|
/// [`Self::parse_storage`] for the slice, compiled in this crate: the
|
||||||
|
/// one copy [`Self::parse`] and [`Self::parse_in`] (in memory) call.
|
||||||
|
fn parse_slice(
|
||||||
data: &[u8],
|
data: &[u8],
|
||||||
offset: usize,
|
offset: u64,
|
||||||
|
offset_size: u8,
|
||||||
|
length_size: u8,
|
||||||
|
) -> Result<ObjectHeader, FormatError> {
|
||||||
|
Self::parse_storage(data, offset, offset_size, length_size)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_storage<S: Storage + ?Sized>(
|
||||||
|
file: &S,
|
||||||
|
offset: u64,
|
||||||
|
offset_size: u8,
|
||||||
|
length_size: u8,
|
||||||
|
) -> Result<ObjectHeader, FormatError> {
|
||||||
|
// The longest prefix of either version, in one read. It holds the
|
||||||
|
// whole prefix or ends at the end of the file, so its bounds checks
|
||||||
|
// are the whole-file ones.
|
||||||
|
let prefix = Window::read(file, offset, V2_PREFIX_MAX)?;
|
||||||
|
prefix.ensure(0, 4)?;
|
||||||
|
if prefix.bytes[..4] == OHDR_SIGNATURE {
|
||||||
|
Self::parse_v2(file, offset, &prefix, offset_size, length_size)
|
||||||
|
} else {
|
||||||
|
Self::parse_v1(file, offset, &prefix, offset_size, length_size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_v1<S: Storage + ?Sized>(
|
||||||
|
file: &S,
|
||||||
|
offset: u64,
|
||||||
|
prefix: &Window<'_>,
|
||||||
offset_size: u8,
|
offset_size: u8,
|
||||||
length_size: u8,
|
length_size: u8,
|
||||||
) -> Result<ObjectHeader, FormatError> {
|
) -> Result<ObjectHeader, FormatError> {
|
||||||
// version(1) + reserved(1) + num_messages(2) + ref_count(4) + header_size(4) = 12
|
// version(1) + reserved(1) + num_messages(2) + ref_count(4) + header_size(4) = 12
|
||||||
// then pad to 8-byte alignment from start of header
|
// then pad to 8-byte alignment from start of header
|
||||||
ensure_len(data, offset, 12)?;
|
prefix.ensure(0, 12)?;
|
||||||
|
let prefix = &prefix.bytes[..12];
|
||||||
|
|
||||||
let version = data[offset];
|
let version = prefix[0];
|
||||||
if version != 1 {
|
if version != 1 {
|
||||||
return Err(FormatError::InvalidObjectHeaderVersion(version));
|
return Err(FormatError::InvalidObjectHeaderVersion(version));
|
||||||
}
|
}
|
||||||
|
|
||||||
let num_messages = LittleEndian::read_u16(&data[offset + 2..offset + 4]) as usize;
|
let num_messages = LittleEndian::read_u16(&prefix[2..4]) as usize;
|
||||||
let reference_count = LittleEndian::read_u32(&data[offset + 4..offset + 8]);
|
let reference_count = LittleEndian::read_u32(&prefix[4..8]);
|
||||||
let header_data_size = LittleEndian::read_u32(&data[offset + 8..offset + 12]) as usize;
|
let header_data_size = LittleEndian::read_u32(&prefix[8..12]) as usize;
|
||||||
|
|
||||||
// libhdf5 (H5O__prefix_deserialize): a header with messages needs room
|
// libhdf5 (H5O__prefix_deserialize): a header with messages needs room
|
||||||
// for at least one message header, and one without has an empty chunk.
|
// for at least one message header, and one without has an empty chunk.
|
||||||
@@ -128,19 +212,19 @@ impl ObjectHeader {
|
|||||||
.checked_add(12 + padding)
|
.checked_add(12 + padding)
|
||||||
.ok_or(FormatError::UnexpectedEof {
|
.ok_or(FormatError::UnexpectedEof {
|
||||||
expected: usize::MAX,
|
expected: usize::MAX,
|
||||||
available: data.len(),
|
available: len_usize(file),
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
ensure_len(data, msg_start, header_data_size)?;
|
// parse_v1_chunk reads the chunk, with the bounds check that was here.
|
||||||
|
// The prefix's count (NIL messages included, capped: it is untrusted)
|
||||||
let mut messages = Vec::new();
|
// sizes the list once instead of growing it message by message.
|
||||||
|
let mut messages = Vec::with_capacity(num_messages.min(64));
|
||||||
let chunk0_count = Self::parse_v1_chunk(
|
let chunk0_count = Self::parse_v1_chunk(
|
||||||
data,
|
file,
|
||||||
msg_start,
|
msg_start,
|
||||||
header_data_size,
|
header_data_size,
|
||||||
offset_size,
|
offset_size,
|
||||||
length_size,
|
length_size,
|
||||||
MAX_V1_CONTINUATION_DEPTH,
|
|
||||||
&mut messages,
|
&mut messages,
|
||||||
)?;
|
)?;
|
||||||
// libhdf5 reads every message in the first chunk and refuses a header
|
// libhdf5 reads every message in the first chunk and refuses a header
|
||||||
@@ -174,24 +258,54 @@ impl ObjectHeader {
|
|||||||
/// 8; libhdf5 refuses a message that is not aligned, that runs past the
|
/// 8; libhdf5 refuses a message that is not aligned, that runs past the
|
||||||
/// end of the chunk, or leftover bytes too few for a message header (a
|
/// end of the chunk, or leftover bytes too few for a message header (a
|
||||||
/// "gap", which only version 2 allows).
|
/// "gap", which only version 2 allows).
|
||||||
#[allow(clippy::too_many_arguments)]
|
///
|
||||||
fn parse_v1_chunk(
|
/// Continuation chunks are read in the order their messages are found,
|
||||||
data: &[u8],
|
/// as `H5O_protect` loads them (so the messages keep libhdf5's order):
|
||||||
offset: usize,
|
/// a queue of (address, length) pairs, each chunk read, parsed and
|
||||||
|
/// released before the next, so only one chunk buffer is alive at a
|
||||||
|
/// time whatever the storage. Every chunk must start at a new address
|
||||||
|
/// (else a cycle), and the chunks together may be no larger than the
|
||||||
|
/// file, so the bytes read stay within the file's size; a header of
|
||||||
|
/// more than [`MAX_V1_CHUNKS`] chunks is refused.
|
||||||
|
fn parse_v1_chunk<S: Storage + ?Sized>(
|
||||||
|
file: &S,
|
||||||
|
offset: u64,
|
||||||
length: usize,
|
length: usize,
|
||||||
offset_size: u8,
|
offset_size: u8,
|
||||||
length_size: u8,
|
length_size: u8,
|
||||||
depth_remaining: u16,
|
|
||||||
messages: &mut Vec<HeaderMessage>,
|
messages: &mut Vec<HeaderMessage>,
|
||||||
) -> Result<usize, FormatError> {
|
) -> Result<usize, FormatError> {
|
||||||
if depth_remaining == 0 {
|
// The chunks found so far are also the queue of chunks to read.
|
||||||
return Err(FormatError::NestingDepthExceeded);
|
let mut spans = ChunkSpans::new(file.len(), offset, length)?;
|
||||||
|
let mut chunk0_count = 0usize;
|
||||||
|
let mut next = 0usize;
|
||||||
|
while let Some((chunk_offset, chunk_length)) = spans.get(next) {
|
||||||
|
let chunk = read_exact_at(file, chunk_offset, chunk_length)?;
|
||||||
|
let count =
|
||||||
|
Self::parse_v1_messages(&chunk, offset_size, length_size, messages, &mut spans)?;
|
||||||
|
// Only the first chunk's messages are held to the prefix count.
|
||||||
|
if next == 0 {
|
||||||
|
chunk0_count = count;
|
||||||
|
}
|
||||||
|
next += 1;
|
||||||
|
}
|
||||||
|
Ok(chunk0_count)
|
||||||
}
|
}
|
||||||
ensure_len(data, offset, length)?;
|
|
||||||
let end = offset + length;
|
|
||||||
let mut pos = offset;
|
|
||||||
let mut count = 0usize;
|
|
||||||
|
|
||||||
|
/// The messages of one version-1 chunk: each checked and appended to
|
||||||
|
/// `messages` (NIL ones dropped), each continuation added to `spans`.
|
||||||
|
/// Returns how many messages (NIL ones included) the chunk holds.
|
||||||
|
#[inline(never)]
|
||||||
|
fn parse_v1_messages(
|
||||||
|
data: &[u8],
|
||||||
|
offset_size: u8,
|
||||||
|
length_size: u8,
|
||||||
|
messages: &mut Vec<HeaderMessage>,
|
||||||
|
spans: &mut ChunkSpans,
|
||||||
|
) -> Result<usize, FormatError> {
|
||||||
|
let end = data.len();
|
||||||
|
let mut pos = 0usize;
|
||||||
|
let mut count = 0usize;
|
||||||
while pos < end {
|
while pos < end {
|
||||||
if end - pos < V1_MSG_HEADER_SIZE {
|
if end - pos < V1_MSG_HEADER_SIZE {
|
||||||
return Err(FormatError::InvalidObjectHeader(
|
return Err(FormatError::InvalidObjectHeader(
|
||||||
@@ -215,7 +329,6 @@ impl ObjectHeader {
|
|||||||
let body = &data[pos..pos + msg_data_size];
|
let body = &data[pos..pos + msg_data_size];
|
||||||
check_message(1, msg_type_raw, msg_flags, body, offset_size, length_size)?;
|
check_message(1, msg_type_raw, msg_flags, body, offset_size, length_size)?;
|
||||||
count += 1;
|
count += 1;
|
||||||
|
|
||||||
let msg_type = MessageType::from_u16(msg_type_raw);
|
let msg_type = MessageType::from_u16(msg_type_raw);
|
||||||
if msg_type != MessageType::Nil {
|
if msg_type != MessageType::Nil {
|
||||||
messages.push(HeaderMessage {
|
messages.push(HeaderMessage {
|
||||||
@@ -226,34 +339,34 @@ impl ObjectHeader {
|
|||||||
data: body.to_vec(),
|
data: body.to_vec(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
pos += msg_data_size;
|
// Queue continuations (v1 continuation chunks are just raw
|
||||||
|
|
||||||
// Follow continuations (v1 continuation chunks are just raw
|
|
||||||
// messages, no signature); check_message has checked the body.
|
// messages, no signature); check_message has checked the body.
|
||||||
if msg_type == MessageType::ObjectHeaderContinuation {
|
if msg_type == MessageType::ObjectHeaderContinuation {
|
||||||
let cont_offset = read_offset(body, 0, offset_size)? as usize;
|
let cont_offset = read_offset(body, 0, offset_size)?;
|
||||||
let cont_length = read_offset(body, offset_size as usize, length_size)? as usize;
|
let cont_length = to_usize(read_offset(body, offset_size as usize, length_size)?)?;
|
||||||
Self::parse_v1_chunk(
|
spans.add(cont_offset, cont_length)?;
|
||||||
data,
|
|
||||||
cont_offset,
|
|
||||||
cont_length,
|
|
||||||
offset_size,
|
|
||||||
length_size,
|
|
||||||
depth_remaining - 1,
|
|
||||||
messages,
|
|
||||||
)?;
|
|
||||||
}
|
}
|
||||||
|
pos += msg_data_size;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(count)
|
Ok(count)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_v2(
|
fn parse_v2<S: Storage + ?Sized>(
|
||||||
data: &[u8],
|
file: &S,
|
||||||
offset: usize,
|
offset: u64,
|
||||||
|
prefix: &Window<'_>,
|
||||||
offset_size: u8,
|
offset_size: u8,
|
||||||
length_size: u8,
|
length_size: u8,
|
||||||
) -> Result<ObjectHeader, FormatError> {
|
) -> Result<ObjectHeader, FormatError> {
|
||||||
|
// `ensure_len` checks positions relative to the header against the
|
||||||
|
// prefix window and reports them as the whole-file check did, with
|
||||||
|
// absolute positions and the file's length.
|
||||||
|
let data: &[u8] = &prefix.bytes;
|
||||||
|
let file_len = len_usize(file);
|
||||||
|
let base = usize::try_from(offset).unwrap_or(usize::MAX);
|
||||||
|
let abs = |rel: usize| base.saturating_add(rel);
|
||||||
|
let ensure_len = |_: &[u8], rel: usize, needed: usize| prefix.ensure(rel, needed);
|
||||||
|
let offset = 0usize;
|
||||||
// signature(4) + version(1) + flags(1) = 6
|
// signature(4) + version(1) + flags(1) = 6
|
||||||
ensure_len(data, offset, 6)?;
|
ensure_len(data, offset, 6)?;
|
||||||
|
|
||||||
@@ -306,7 +419,7 @@ impl ObjectHeader {
|
|||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
};
|
};
|
||||||
ensure_len(data, pos, chunk_size_width as usize)?;
|
ensure_len(data, pos, chunk_size_width as usize)?;
|
||||||
let chunk0_size = read_offset(data, pos, chunk_size_width)? as usize;
|
let chunk0_size = to_usize(read_offset(data, pos, chunk_size_width)?)?;
|
||||||
pos += chunk_size_width as usize;
|
pos += chunk_size_width as usize;
|
||||||
// Bit 2: attribute creation order tracked → messages include creation order field
|
// Bit 2: attribute creation order tracked → messages include creation order field
|
||||||
let has_creation_order = flags & 0x04 != 0;
|
let has_creation_order = flags & 0x04 != 0;
|
||||||
@@ -318,15 +431,20 @@ impl ObjectHeader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let chunk0_msg_start = pos;
|
let chunk0_msg_start = pos;
|
||||||
let chunk0_msg_end = pos
|
let Some(chunk0_abs_end) = abs(pos).checked_add(chunk0_size) else {
|
||||||
.checked_add(chunk0_size)
|
return Err(FormatError::UnexpectedEof {
|
||||||
.ok_or(FormatError::UnexpectedEof {
|
|
||||||
expected: usize::MAX,
|
expected: usize::MAX,
|
||||||
available: data.len(),
|
available: file_len,
|
||||||
})?;
|
});
|
||||||
|
};
|
||||||
|
let chunk0_msg_end = chunk0_abs_end - base;
|
||||||
|
|
||||||
|
// The whole first chunk, prefix to checksum, in one read (its
|
||||||
|
// bounds check is the one on the checksum's 4 bytes).
|
||||||
|
let chunk0 = read_exact_at(file, base as u64, chunk0_msg_end.saturating_add(4))?;
|
||||||
|
let data: &[u8] = &chunk0;
|
||||||
|
|
||||||
// Validate checksum: from OHDR signature through all messages (before checksum)
|
// Validate checksum: from OHDR signature through all messages (before checksum)
|
||||||
ensure_len(data, chunk0_msg_end, 4)?;
|
|
||||||
#[cfg(feature = "checksum")]
|
#[cfg(feature = "checksum")]
|
||||||
{
|
{
|
||||||
let stored = LittleEndian::read_u32(&data[chunk0_msg_end..chunk0_msg_end + 4]);
|
let stored = LittleEndian::read_u32(&data[chunk0_msg_end..chunk0_msg_end + 4]);
|
||||||
@@ -353,16 +471,17 @@ impl ObjectHeader {
|
|||||||
&mut continuations,
|
&mut continuations,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
// Follow continuations (limit to prevent cycles in malformed data)
|
// Follow continuations, one chunk buffer at a time. A chunk address
|
||||||
let mut cont_remaining = 256u16;
|
// seen twice is a cycle in malformed data, and the chunks may add up
|
||||||
|
// to no more than the file; a valid header can have many chunks (libhdf5 adds one
|
||||||
|
// whenever a message no longer fits), up to the same bound as a
|
||||||
|
// version-1 header.
|
||||||
|
let mut spans = ChunkSpans::new(file.len(), base as u64, chunk0_msg_end.saturating_add(4))?;
|
||||||
while let Some((cont_offset, cont_length)) = continuations.pop() {
|
while let Some((cont_offset, cont_length)) = continuations.pop() {
|
||||||
if cont_remaining == 0 {
|
spans.add(cont_offset as u64, cont_length)?;
|
||||||
return Err(FormatError::NestingDepthExceeded);
|
|
||||||
}
|
|
||||||
cont_remaining -= 1;
|
|
||||||
Self::parse_v2_continuation(
|
Self::parse_v2_continuation(
|
||||||
data,
|
file,
|
||||||
cont_offset,
|
cont_offset as u64,
|
||||||
cont_length,
|
cont_length,
|
||||||
has_creation_order,
|
has_creation_order,
|
||||||
offset_size,
|
offset_size,
|
||||||
@@ -439,8 +558,8 @@ impl ObjectHeader {
|
|||||||
let msg_type = MessageType::from_u16(msg_type_raw);
|
let msg_type = MessageType::from_u16(msg_type_raw);
|
||||||
if msg_type == MessageType::ObjectHeaderContinuation {
|
if msg_type == MessageType::ObjectHeaderContinuation {
|
||||||
// check_message has checked the body holds both fields.
|
// check_message has checked the body holds both fields.
|
||||||
let cont_off = read_offset(body, 0, offset_size)? as usize;
|
let cont_off = to_usize(read_offset(body, 0, offset_size)?)?;
|
||||||
let cont_len = read_offset(body, offset_size as usize, length_size)? as usize;
|
let cont_len = to_usize(read_offset(body, offset_size as usize, length_size)?)?;
|
||||||
continuations.push((cont_off, cont_len));
|
continuations.push((cont_off, cont_len));
|
||||||
} else if msg_type == MessageType::Nil {
|
} else if msg_type == MessageType::Nil {
|
||||||
null_count += 1;
|
null_count += 1;
|
||||||
@@ -461,9 +580,9 @@ impl ObjectHeader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
fn parse_v2_continuation(
|
fn parse_v2_continuation<S: Storage + ?Sized>(
|
||||||
data: &[u8],
|
file: &S,
|
||||||
offset: usize,
|
offset: u64,
|
||||||
length: usize,
|
length: usize,
|
||||||
has_creation_order: bool,
|
has_creation_order: bool,
|
||||||
offset_size: u8,
|
offset_size: u8,
|
||||||
@@ -472,7 +591,9 @@ impl ObjectHeader {
|
|||||||
continuations: &mut Vec<(usize, usize)>,
|
continuations: &mut Vec<(usize, usize)>,
|
||||||
) -> Result<(), FormatError> {
|
) -> Result<(), FormatError> {
|
||||||
// OCHK signature(4) + messages + checksum(4)
|
// OCHK signature(4) + messages + checksum(4)
|
||||||
ensure_len(data, offset, length)?;
|
let chunk = read_exact_at(file, offset, length)?;
|
||||||
|
let data: &[u8] = &chunk;
|
||||||
|
let offset = 0usize;
|
||||||
if length < 8 {
|
if length < 8 {
|
||||||
return Err(FormatError::UnexpectedEof {
|
return Err(FormatError::UnexpectedEof {
|
||||||
expected: 8,
|
expected: 8,
|
||||||
@@ -513,11 +634,114 @@ impl ObjectHeader {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Longest version-2 object header prefix: signature(4) + version(1) +
|
||||||
|
/// flags(1) + times(16) + attribute phase change(4) + chunk-0 size(8).
|
||||||
|
const V2_PREFIX_MAX: usize = 34;
|
||||||
|
|
||||||
/// Size of a version-1 message header: type(2) + size(2) + flags(1) + reserved(3).
|
/// Size of a version-1 message header: type(2) + size(2) + flags(1) + reserved(3).
|
||||||
const V1_MSG_HEADER_SIZE: usize = 8;
|
const V1_MSG_HEADER_SIZE: usize = 8;
|
||||||
|
|
||||||
/// How deep version-1 continuation chunks may chain (malformed-data guard).
|
/// The chunks of one object header read so far, in the order they were
|
||||||
const MAX_V1_CONTINUATION_DEPTH: u16 = 32;
|
/// found (which is the order version-1 chunks are read in). A chunk starting
|
||||||
|
/// where another did is a cycle. Chunks of a valid header do not overlap, so
|
||||||
|
/// together they are no larger than the file; a header whose chunks add up
|
||||||
|
/// to more is refused, which bounds what its chunks can make a reader read
|
||||||
|
/// (a crafted chain of chunks each nested in the last would otherwise read
|
||||||
|
/// the file over and over). Overlap itself is not refused: libhdf5 reads
|
||||||
|
/// such headers (`cve-2025-7067.h5` has one).
|
||||||
|
///
|
||||||
|
/// Almost every header has at most a few chunks, and this runs once per
|
||||||
|
/// header, so the first [`INLINE_CHUNKS`] live in an inline array and are
|
||||||
|
/// checked for cycles by a scan; only a longer header allocates (the rest
|
||||||
|
/// of the list, and a set of starts). Allocating a queue and a set for
|
||||||
|
/// every header made parsing 401 small headers 1.8x slower.
|
||||||
|
struct ChunkSpans {
|
||||||
|
inline: [(u64, usize); INLINE_CHUNKS],
|
||||||
|
/// Chunks after the first [`INLINE_CHUNKS`], and every chunk start.
|
||||||
|
spill: Option<Box<SpilledSpans>>,
|
||||||
|
/// How many chunks there are.
|
||||||
|
len: usize,
|
||||||
|
/// Bytes of the chunks so far, and the most they may add up to.
|
||||||
|
total: u64,
|
||||||
|
budget: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The chunks of a [`ChunkSpans`] beyond its inline ones.
|
||||||
|
struct SpilledSpans {
|
||||||
|
chunks: Vec<(u64, usize)>,
|
||||||
|
starts: BTreeSet<u64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// How many chunks [`ChunkSpans`] holds without allocating.
|
||||||
|
const INLINE_CHUNKS: usize = 8;
|
||||||
|
|
||||||
|
impl ChunkSpans {
|
||||||
|
#[inline]
|
||||||
|
fn new(file_len: u64, start: u64, len: usize) -> Result<Self, FormatError> {
|
||||||
|
let mut s = Self {
|
||||||
|
inline: [(0, 0); INLINE_CHUNKS],
|
||||||
|
spill: None,
|
||||||
|
len: 0,
|
||||||
|
total: 0,
|
||||||
|
budget: file_len,
|
||||||
|
};
|
||||||
|
s.add(start, len)?;
|
||||||
|
Ok(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Record the chunk `len` bytes at `start`.
|
||||||
|
#[inline]
|
||||||
|
fn add(&mut self, start: u64, len: usize) -> Result<(), FormatError> {
|
||||||
|
self.total = self.total.saturating_add(len as u64);
|
||||||
|
if self.len < INLINE_CHUNKS {
|
||||||
|
if self.inline[..self.len].iter().any(|&(s, _)| s == start) {
|
||||||
|
return Err(FormatError::NestingDepthExceeded);
|
||||||
|
}
|
||||||
|
self.inline[self.len] = (start, len);
|
||||||
|
} else {
|
||||||
|
self.add_spilled(start, len)?;
|
||||||
|
}
|
||||||
|
self.len += 1;
|
||||||
|
if self.total > self.budget {
|
||||||
|
return Err(FormatError::InvalidObjectHeader(
|
||||||
|
"object header chunks larger than the file",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cold]
|
||||||
|
#[inline(never)]
|
||||||
|
fn add_spilled(&mut self, start: u64, len: usize) -> Result<(), FormatError> {
|
||||||
|
let inline = &self.inline;
|
||||||
|
let spill = self.spill.get_or_insert_with(|| {
|
||||||
|
Box::new(SpilledSpans {
|
||||||
|
chunks: Vec::new(),
|
||||||
|
starts: inline.iter().map(|&(s, _)| s).collect(),
|
||||||
|
})
|
||||||
|
});
|
||||||
|
if !spill.starts.insert(start) || self.len >= MAX_V1_CHUNKS {
|
||||||
|
return Err(FormatError::NestingDepthExceeded);
|
||||||
|
}
|
||||||
|
spill.chunks.push((start, len));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The `i`th chunk recorded.
|
||||||
|
#[inline]
|
||||||
|
fn get(&self, i: usize) -> Option<(u64, usize)> {
|
||||||
|
if i < INLINE_CHUNKS {
|
||||||
|
(i < self.len).then(|| self.inline[i])
|
||||||
|
} else {
|
||||||
|
self.spill.as_ref()?.chunks.get(i - INLINE_CHUNKS).copied()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Most chunks a version-1 object header may have (malformed-data guard;
|
||||||
|
/// libhdf5 has no limit, and a header that gains one continuation chunk per
|
||||||
|
/// attribute added can have many).
|
||||||
|
const MAX_V1_CHUNKS: usize = 1 << 16;
|
||||||
|
|
||||||
/// Every defined version-2 object header status flag (libhdf5
|
/// Every defined version-2 object header status flag (libhdf5
|
||||||
/// `H5O_HDR_ALL_FLAGS`): chunk-0 size width (bits 0-1), attribute creation
|
/// `H5O_HDR_ALL_FLAGS`): chunk-0 size width (bits 0-1), attribute creation
|
||||||
@@ -662,6 +886,54 @@ fn check_message(
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
fn header_with(types: &[MessageType]) -> ObjectHeader {
|
||||||
|
ObjectHeader {
|
||||||
|
version: 2,
|
||||||
|
messages: types
|
||||||
|
.iter()
|
||||||
|
.map(|&msg_type| HeaderMessage {
|
||||||
|
msg_type,
|
||||||
|
size: 0,
|
||||||
|
flags: 0,
|
||||||
|
creation_order: None,
|
||||||
|
data: Vec::new(),
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
reference_count: None,
|
||||||
|
flags: 0,
|
||||||
|
access_time: None,
|
||||||
|
modification_time: None,
|
||||||
|
change_time: None,
|
||||||
|
birth_time: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn object_class_follows_libhdf5() {
|
||||||
|
use MessageType::*;
|
||||||
|
let class = |t: &[MessageType]| header_with(t).object_class();
|
||||||
|
assert_eq!(
|
||||||
|
class(&[Datatype, Dataspace, DataLayout]),
|
||||||
|
Some(ObjectClass::Dataset)
|
||||||
|
);
|
||||||
|
// A Data Layout message does not make a dataset without a dataspace
|
||||||
|
// (cve-2024-33874 `/Dset1`: h5py opens it as a named datatype).
|
||||||
|
assert_eq!(
|
||||||
|
class(&[Datatype, DataLayout]),
|
||||||
|
Some(ObjectClass::NamedDatatype)
|
||||||
|
);
|
||||||
|
assert_eq!(class(&[Datatype]), Some(ObjectClass::NamedDatatype));
|
||||||
|
// Group messages win over dataset messages.
|
||||||
|
assert_eq!(
|
||||||
|
class(&[Datatype, Dataspace, SymbolTable]),
|
||||||
|
Some(ObjectClass::Group)
|
||||||
|
);
|
||||||
|
assert_eq!(class(&[LinkInfo]), Some(ObjectClass::Group));
|
||||||
|
// Link messages alone are not a group; nothing is not an object.
|
||||||
|
assert_eq!(class(&[Link]), None);
|
||||||
|
assert_eq!(class(&[]), None);
|
||||||
|
}
|
||||||
|
|
||||||
// Helper: build a v1 object header with given messages
|
// Helper: build a v1 object header with given messages
|
||||||
fn build_v1_header(
|
fn build_v1_header(
|
||||||
messages: &[(u16, &[u8], u8)], // (type, data, flags)
|
messages: &[(u16, &[u8], u8)], // (type, data, flags)
|
||||||
@@ -673,13 +945,13 @@ mod tests {
|
|||||||
let mut msg_bytes = Vec::new();
|
let mut msg_bytes = Vec::new();
|
||||||
for (mtype, mdata, mflags) in messages {
|
for (mtype, mdata, mflags) in messages {
|
||||||
// v1 message sizes are multiples of 8 (the data is zero-padded).
|
// v1 message sizes are multiples of 8 (the data is zero-padded).
|
||||||
let padded = mdata.len().div_ceil(8) * 8;
|
let padded = <[u8]>::len(mdata).div_ceil(8) * 8;
|
||||||
msg_bytes.extend_from_slice(&mtype.to_le_bytes()); // type(2)
|
msg_bytes.extend_from_slice(&mtype.to_le_bytes()); // type(2)
|
||||||
msg_bytes.extend_from_slice(&(padded as u16).to_le_bytes()); // size(2)
|
msg_bytes.extend_from_slice(&(padded as u16).to_le_bytes()); // size(2)
|
||||||
msg_bytes.push(*mflags); // flags(1)
|
msg_bytes.push(*mflags); // flags(1)
|
||||||
msg_bytes.extend_from_slice(&[0u8; 3]); // reserved(3)
|
msg_bytes.extend_from_slice(&[0u8; 3]); // reserved(3)
|
||||||
msg_bytes.extend_from_slice(mdata); // data
|
msg_bytes.extend_from_slice(mdata); // data
|
||||||
msg_bytes.resize(msg_bytes.len() + padded - mdata.len(), 0);
|
msg_bytes.resize(msg_bytes.len() + padded - <[u8]>::len(mdata), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut buf = Vec::new();
|
let mut buf = Vec::new();
|
||||||
@@ -775,6 +1047,176 @@ mod tests {
|
|||||||
assert_eq!(hdr.messages[1].data[..2], [5, 6]);
|
assert_eq!(hdr.messages[1].data[..2], [5, 6]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A version-1 header whose continuation chunks form a chain: chunk k
|
||||||
|
/// holds a Dataspace message `[k]` and the continuation to chunk k + 1.
|
||||||
|
/// With `cycle`, the last chunk points back at the first continuation
|
||||||
|
/// chunk.
|
||||||
|
fn v1_chain(n: usize, cycle: bool) -> Vec<u8> {
|
||||||
|
// Each continuation chunk: dataspace (8 + 8) + continuation (8 + 16).
|
||||||
|
let chunk_len = 40u64;
|
||||||
|
let first = 64u64;
|
||||||
|
let cont = |addr: u64| {
|
||||||
|
let mut b = addr.to_le_bytes().to_vec();
|
||||||
|
b.extend_from_slice(&chunk_len.to_le_bytes());
|
||||||
|
b
|
||||||
|
};
|
||||||
|
let mut data = build_v1_header(&[(0x0010, &cont(first)[..], 0)], 8, 8);
|
||||||
|
data.resize(first as usize, 0);
|
||||||
|
for k in 0..n {
|
||||||
|
let mut c = Vec::new();
|
||||||
|
c.extend_from_slice(&1u16.to_le_bytes());
|
||||||
|
c.extend_from_slice(&8u16.to_le_bytes());
|
||||||
|
c.extend_from_slice(&[0; 4]);
|
||||||
|
c.extend_from_slice(&(k as u64).to_le_bytes());
|
||||||
|
let next = if k + 1 < n {
|
||||||
|
first + (k as u64 + 1) * chunk_len
|
||||||
|
} else if cycle {
|
||||||
|
first
|
||||||
|
} else {
|
||||||
|
// The last chunk ends in a NIL message instead.
|
||||||
|
c.extend_from_slice(&[0, 0, 16, 0, 0, 0, 0, 0]);
|
||||||
|
c.extend_from_slice(&[0; 16]);
|
||||||
|
data.extend_from_slice(&c);
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
c.extend_from_slice(&0x10u16.to_le_bytes());
|
||||||
|
c.extend_from_slice(&16u16.to_le_bytes());
|
||||||
|
c.extend_from_slice(&[0; 4]);
|
||||||
|
c.extend_from_slice(&cont(next));
|
||||||
|
data.extend_from_slice(&c);
|
||||||
|
}
|
||||||
|
data
|
||||||
|
}
|
||||||
|
|
||||||
|
/// libhdf5 reads any chain of continuation chunks (a header grows one
|
||||||
|
/// per attribute added when full); the reader used to stop at 32.
|
||||||
|
#[test]
|
||||||
|
fn long_v1_continuation_chains_are_read() {
|
||||||
|
let data = v1_chain(200, false);
|
||||||
|
let hdr = ObjectHeader::parse(&data, 0, 8, 8).unwrap();
|
||||||
|
let spaces: Vec<u8> = hdr
|
||||||
|
.messages
|
||||||
|
.iter()
|
||||||
|
.filter(|m| m.msg_type == MessageType::Dataspace)
|
||||||
|
.map(|m| m.data[0])
|
||||||
|
.collect();
|
||||||
|
assert_eq!(spaces, (0..200).map(|k| k as u8).collect::<Vec<_>>());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A crafted version-1 header whose continuation chunks nest: each
|
||||||
|
/// chunk's continuation message points at the rest of that chunk. Read
|
||||||
|
/// depth-first with every enclosing chunk kept alive, from storage that
|
||||||
|
/// hands out owned buffers, it read n^2 bytes and held them all at once
|
||||||
|
/// (a 192 KB file read 768 MB). Chunks adding up to more than the file
|
||||||
|
/// are refused, and the bytes read stay within the file's size.
|
||||||
|
#[test]
|
||||||
|
fn nested_v1_continuation_chunks_are_bounded() {
|
||||||
|
use crate::storage::CountingStorage;
|
||||||
|
let n = 2000u64;
|
||||||
|
let a = 64u64;
|
||||||
|
let cont = |addr: u64, len: u64| {
|
||||||
|
let mut m = vec![0x10, 0, 16, 0, 0, 0, 0, 0];
|
||||||
|
m.extend_from_slice(&addr.to_le_bytes());
|
||||||
|
m.extend_from_slice(&len.to_le_bytes());
|
||||||
|
m
|
||||||
|
};
|
||||||
|
// Prefix: version 1, one message, reference count 1, 24 bytes.
|
||||||
|
let mut buf = vec![1, 0, 1, 0, 1, 0, 0, 0, 24, 0, 0, 0, 0, 0, 0, 0];
|
||||||
|
buf.extend_from_slice(&cont(a, 24 * n));
|
||||||
|
buf.resize(a as usize, 0);
|
||||||
|
for k in 0..n {
|
||||||
|
if k + 1 < n {
|
||||||
|
buf.extend_from_slice(&cont(a + 24 * (k + 1), 24 * (n - k - 1)));
|
||||||
|
} else {
|
||||||
|
buf.extend_from_slice(&[0, 0, 16, 0, 0, 0, 0, 0]);
|
||||||
|
buf.extend_from_slice(&[0; 16]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let len = buf.len() as u64;
|
||||||
|
let s = CountingStorage::new(buf);
|
||||||
|
assert!(matches!(
|
||||||
|
ObjectHeader::parse_in(&s, 0, 8, 8),
|
||||||
|
Err(FormatError::InvalidObjectHeader(
|
||||||
|
"object header chunks larger than the file"
|
||||||
|
))
|
||||||
|
));
|
||||||
|
assert!(
|
||||||
|
s.bytes_read() <= 2 * len,
|
||||||
|
"read {} of {len}",
|
||||||
|
s.bytes_read()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// libhdf5 reads a continuation chunk that overlaps the chunk holding
|
||||||
|
/// its message (`cve-2025-7067.h5` has one), and so does this reader.
|
||||||
|
#[test]
|
||||||
|
fn overlapping_v1_continuation_chunk_is_read() {
|
||||||
|
// Chunk 0 (at 16): continuation (24 bytes), then a NIL message at
|
||||||
|
// 40; the continuation chunk is that NIL message's 8-byte header.
|
||||||
|
let mut cont = 40u64.to_le_bytes().to_vec();
|
||||||
|
cont.extend_from_slice(&8u64.to_le_bytes());
|
||||||
|
let data = build_v1_header(&[(0x0010, &cont[..], 0), (0x0000, &[][..], 0)], 8, 8);
|
||||||
|
let hdr = ObjectHeader::parse(&data, 0, 8, 8).unwrap();
|
||||||
|
assert_eq!(hdr.messages.len(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A valid chain over owned-buffer storage reads each chunk once.
|
||||||
|
#[test]
|
||||||
|
fn long_v1_chain_reads_each_chunk_once() {
|
||||||
|
use crate::storage::CountingStorage;
|
||||||
|
let data = v1_chain(3000, false);
|
||||||
|
let len = data.len() as u64;
|
||||||
|
let s = CountingStorage::new(data);
|
||||||
|
let hdr = ObjectHeader::parse_in(&s, 0, 8, 8).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
hdr.messages
|
||||||
|
.iter()
|
||||||
|
.filter(|m| m.msg_type == MessageType::Dataspace)
|
||||||
|
.count(),
|
||||||
|
3000
|
||||||
|
);
|
||||||
|
assert!(s.bytes_read() <= len, "read {} of {len}", s.bytes_read());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Continuation chunks are read in the order their messages are found
|
||||||
|
/// (libhdf5's `H5O_protect`), so a chunk's messages follow every
|
||||||
|
/// message of the chunk before, not the continuation message.
|
||||||
|
#[test]
|
||||||
|
fn v1_continuation_messages_keep_libhdf5_order() {
|
||||||
|
// Chunk 0: continuation to A, dataspace [1]; A: dataspace [2].
|
||||||
|
let a = 64u64;
|
||||||
|
let mut cont = a.to_le_bytes().to_vec();
|
||||||
|
cont.extend_from_slice(&16u64.to_le_bytes());
|
||||||
|
let mut data = build_v1_header(&[(0x0010, &cont[..], 0), (0x0001, &[1; 8][..], 0)], 8, 8);
|
||||||
|
data.resize(a as usize, 0);
|
||||||
|
data.extend_from_slice(&[1, 0, 8, 0, 0, 0, 0, 0]);
|
||||||
|
data.extend_from_slice(&[2; 8]);
|
||||||
|
let hdr = ObjectHeader::parse(&data, 0, 8, 8).unwrap();
|
||||||
|
let spaces: Vec<u8> = hdr
|
||||||
|
.messages
|
||||||
|
.iter()
|
||||||
|
.filter(|m| m.msg_type == MessageType::Dataspace)
|
||||||
|
.map(|m| m.data[0])
|
||||||
|
.collect();
|
||||||
|
assert_eq!(spaces, [1, 2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn v1_continuation_cycles_are_refused() {
|
||||||
|
// Within the inline chunk list, and past it (the cycle returns to
|
||||||
|
// an inline chunk once the list has spilled).
|
||||||
|
for n in [5, 7, 8, 9, 40] {
|
||||||
|
let data = v1_chain(n, true);
|
||||||
|
assert!(
|
||||||
|
matches!(
|
||||||
|
ObjectHeader::parse(&data, 0, 8, 8),
|
||||||
|
Err(FormatError::NestingDepthExceeded)
|
||||||
|
),
|
||||||
|
"{n} chunks"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_v1_unknown_message_ok() {
|
fn parse_v1_unknown_message_ok() {
|
||||||
let messages = [(0x00FFu16, &[0xAA, 0xBB][..], 0u8)];
|
let messages = [(0x00FFu16, &[0xAA, 0xBB][..], 0u8)];
|
||||||
@@ -1199,4 +1641,56 @@ mod tests {
|
|||||||
let err = ObjectHeader::parse(&data, 0, 8, 8).unwrap_err();
|
let err = ObjectHeader::parse(&data, 0, 8, 8).unwrap_err();
|
||||||
assert!(matches!(err, FormatError::UnexpectedEof { .. }));
|
assert!(matches!(err, FormatError::UnexpectedEof { .. }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Every header, and every truncation of it, parses to the same result
|
||||||
|
/// (or the same error) through a `read_at`-only storage as from a slice;
|
||||||
|
/// a header in one chunk takes two reads (prefix, chunk).
|
||||||
|
#[test]
|
||||||
|
fn parse_in_matches_slice_parse() {
|
||||||
|
use crate::storage::CountingStorage;
|
||||||
|
let mut headers = vec![
|
||||||
|
build_v1_header(&[], 8, 8),
|
||||||
|
build_v1_header(&[(0x0001, &[1, 2, 3], 0), (0x0003, &[9; 8], 0)], 8, 8),
|
||||||
|
build_v2_header(0x00, &[(0x01, &[42], 0)], None),
|
||||||
|
build_v2_header(0x03, &[(0x01, &[1, 2], 0), (0x03, &[3], 0)], None),
|
||||||
|
build_v2_header(0x24, &[(0x01, &[1], 0)], Some((1, 2, 3, 4))),
|
||||||
|
build_v2_header(0x35, &[(0x01, &[1], 0)], Some((5, 6, 7, 8))),
|
||||||
|
];
|
||||||
|
// A v2 header with a continuation chunk at 256.
|
||||||
|
let mut ochk = OCHK_SIGNATURE.to_vec();
|
||||||
|
ochk.extend_from_slice(&[0x03, 2, 0, 0, 0xDE, 0xAD]);
|
||||||
|
let sum = crate::checksum::jenkins_lookup3(&ochk);
|
||||||
|
ochk.extend_from_slice(&sum.to_le_bytes());
|
||||||
|
let mut cont = 256u64.to_le_bytes().to_vec();
|
||||||
|
cont.extend_from_slice(&(ochk.len() as u64).to_le_bytes());
|
||||||
|
let main = build_v2_header(0x00, &[(0x01, &[42], 0), (0x10, &cont, 0)], None);
|
||||||
|
let mut with_cont = vec![0u8; 256 + ochk.len()];
|
||||||
|
with_cont[..main.len()].copy_from_slice(&main);
|
||||||
|
with_cont[256..].copy_from_slice(&ochk);
|
||||||
|
headers.push(with_cont);
|
||||||
|
|
||||||
|
for h in headers {
|
||||||
|
for at in [0usize, 3] {
|
||||||
|
for cut in 0..=h.len() {
|
||||||
|
let mut f = vec![0u8; at];
|
||||||
|
f.extend_from_slice(&h[..cut]);
|
||||||
|
if at == 0 && cut == h.len() {
|
||||||
|
f.resize(f.len() + 64, 0);
|
||||||
|
}
|
||||||
|
let want = ObjectHeader::parse(&f, at, 8, 8);
|
||||||
|
let storage = CountingStorage::new(f.clone());
|
||||||
|
let got = ObjectHeader::parse_in(&storage, at as u64, 8, 8);
|
||||||
|
assert_eq!(
|
||||||
|
format!("{got:?}"),
|
||||||
|
format!("{want:?}"),
|
||||||
|
"at {at}, cut {cut}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let one_chunk = build_v2_header(0x00, &[(0x01, &[42], 0)], None);
|
||||||
|
let storage = CountingStorage::new(one_chunk);
|
||||||
|
ObjectHeader::parse_in(&storage, 0, 8, 8).unwrap();
|
||||||
|
assert_eq!(storage.reads(), 2);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,9 +12,16 @@ use crate::message_type::MessageType;
|
|||||||
/// its size truncated to 16 bits produced files libhdf5 refuses.
|
/// its size truncated to 16 bits produced files libhdf5 refuses.
|
||||||
pub const MAX_MESSAGE_SIZE: usize = u16::MAX as usize;
|
pub const MAX_MESSAGE_SIZE: usize = u16::MAX as usize;
|
||||||
|
|
||||||
|
/// Object header flags: attribute creation order tracked (each message
|
||||||
|
/// then carries a 2-byte creation order) and indexed.
|
||||||
|
const OHDR_ATTR_CRT_ORDER_TRACKED: u8 = 0x04;
|
||||||
|
const OHDR_ATTR_CRT_ORDER_INDEXED: u8 = 0x08;
|
||||||
|
|
||||||
/// Writer for v2 object headers with proper checksums.
|
/// Writer for v2 object headers with proper checksums.
|
||||||
pub struct ObjectHeaderWriter {
|
pub struct ObjectHeaderWriter {
|
||||||
messages: Vec<(MessageType, Vec<u8>, u8)>, // (type, data, msg_flags)
|
messages: Vec<(MessageType, Vec<u8>, u8, u16)>, // (type, data, msg_flags, creation order)
|
||||||
|
/// Attribute creation order tracked and indexed.
|
||||||
|
attr_order: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ObjectHeaderWriter {
|
impl ObjectHeaderWriter {
|
||||||
@@ -22,17 +29,33 @@ impl ObjectHeaderWriter {
|
|||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
messages: Vec::new(),
|
messages: Vec::new(),
|
||||||
|
attr_order: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Track and index attribute creation order, as libhdf5 does for an
|
||||||
|
/// object created with `H5P_CRT_ORDER_TRACKED | H5P_CRT_ORDER_INDEXED`
|
||||||
|
/// (h5py's `track_order=True`): the header's flags say so, and every
|
||||||
|
/// message carries a creation order (an attribute's own; 0 for the
|
||||||
|
/// others). libhdf5 reads the setting back from these flags.
|
||||||
|
pub fn track_attr_order(&mut self) {
|
||||||
|
self.attr_order = true;
|
||||||
|
}
|
||||||
|
|
||||||
/// Add a message to the header with default flags (0).
|
/// Add a message to the header with default flags (0).
|
||||||
pub fn add_message(&mut self, msg_type: MessageType, data: Vec<u8>) {
|
pub fn add_message(&mut self, msg_type: MessageType, data: Vec<u8>) {
|
||||||
self.messages.push((msg_type, data, 0));
|
self.messages.push((msg_type, data, 0, 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add a message with specific flags.
|
/// Add a message with specific flags.
|
||||||
pub fn add_message_with_flags(&mut self, msg_type: MessageType, data: Vec<u8>, flags: u8) {
|
pub fn add_message_with_flags(&mut self, msg_type: MessageType, data: Vec<u8>, flags: u8) {
|
||||||
self.messages.push((msg_type, data, flags));
|
self.messages.push((msg_type, data, flags, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a message with its creation order, which is written only when
|
||||||
|
/// attribute creation order is tracked ([`Self::track_attr_order`]).
|
||||||
|
pub fn add_message_with_order(&mut self, msg_type: MessageType, data: Vec<u8>, order: u16) {
|
||||||
|
self.messages.push((msg_type, data, 0, order));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Serialize the complete v2 object header (OHDR + messages + checksum).
|
/// Serialize the complete v2 object header (OHDR + messages + checksum).
|
||||||
@@ -41,10 +64,10 @@ impl ObjectHeaderWriter {
|
|||||||
/// than [`MAX_MESSAGE_SIZE`] (e.g. an attribute over ~64 KiB, which would
|
/// than [`MAX_MESSAGE_SIZE`] (e.g. an attribute over ~64 KiB, which would
|
||||||
/// need dense attribute storage), rather than writing a corrupt header.
|
/// need dense attribute storage), rather than writing a corrupt header.
|
||||||
pub fn serialize(&self) -> Result<Vec<u8>, FormatError> {
|
pub fn serialize(&self) -> Result<Vec<u8>, FormatError> {
|
||||||
if let Some((msg_type, data, _)) = self
|
if let Some((msg_type, data, _, _)) = self
|
||||||
.messages
|
.messages
|
||||||
.iter()
|
.iter()
|
||||||
.find(|(_, data, _)| data.len() > MAX_MESSAGE_SIZE)
|
.find(|(_, data, _, _)| data.len() > MAX_MESSAGE_SIZE)
|
||||||
{
|
{
|
||||||
return Err(FormatError::SerializationError(format!(
|
return Err(FormatError::SerializationError(format!(
|
||||||
"{msg_type:?} message is {} bytes; an object header message holds at most \
|
"{msg_type:?} message is {} bytes; an object header message holds at most \
|
||||||
@@ -52,11 +75,13 @@ impl ObjectHeaderWriter {
|
|||||||
data.len()
|
data.len()
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
// Calculate total message bytes: each message has type(1) + size(2) + flags(1) + data
|
// Calculate total message bytes: each message has type(1) + size(2) +
|
||||||
|
// flags(1) [+ creation order(2)] + data
|
||||||
|
let msg_header = if self.attr_order { 6 } else { 4 };
|
||||||
let msg_bytes_total: usize = self
|
let msg_bytes_total: usize = self
|
||||||
.messages
|
.messages
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(_, data, _)| 4 + data.len())
|
.map(|(_, data, _, _)| msg_header + data.len())
|
||||||
.sum();
|
.sum();
|
||||||
|
|
||||||
// Determine chunk size field width based on msg_bytes_total
|
// Determine chunk size field width based on msg_bytes_total
|
||||||
@@ -68,6 +93,12 @@ impl ObjectHeaderWriter {
|
|||||||
(0x02u8, 4)
|
(0x02u8, 4)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let flags = if self.attr_order {
|
||||||
|
flags | OHDR_ATTR_CRT_ORDER_TRACKED | OHDR_ATTR_CRT_ORDER_INDEXED
|
||||||
|
} else {
|
||||||
|
flags
|
||||||
|
};
|
||||||
|
|
||||||
let mut buf = Vec::new();
|
let mut buf = Vec::new();
|
||||||
|
|
||||||
// OHDR signature
|
// OHDR signature
|
||||||
@@ -85,7 +116,7 @@ impl ObjectHeaderWriter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Messages
|
// Messages
|
||||||
for (msg_type, data, msg_flags) in &self.messages {
|
for (msg_type, data, msg_flags, order) in &self.messages {
|
||||||
let type_id = msg_type.to_u16();
|
let type_id = msg_type.to_u16();
|
||||||
assert!(
|
assert!(
|
||||||
type_id <= 255,
|
type_id <= 255,
|
||||||
@@ -94,6 +125,9 @@ impl ObjectHeaderWriter {
|
|||||||
buf.push(type_id as u8); // type (1 byte in v2)
|
buf.push(type_id as u8); // type (1 byte in v2)
|
||||||
buf.extend_from_slice(&(data.len() as u16).to_le_bytes()); // size (2 bytes)
|
buf.extend_from_slice(&(data.len() as u16).to_le_bytes()); // size (2 bytes)
|
||||||
buf.push(*msg_flags); // flags
|
buf.push(*msg_flags); // flags
|
||||||
|
if self.attr_order {
|
||||||
|
buf.extend_from_slice(&order.to_le_bytes()); // creation order
|
||||||
|
}
|
||||||
buf.extend_from_slice(data);
|
buf.extend_from_slice(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,6 +227,21 @@ mod tests {
|
|||||||
assert_eq!(hdr.messages.len(), 0);
|
assert_eq!(hdr.messages.len(), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tracked_attribute_order_is_in_the_flags_and_every_message() {
|
||||||
|
let mut writer = ObjectHeaderWriter::new();
|
||||||
|
writer.track_attr_order();
|
||||||
|
writer.add_message(MessageType::Dataspace, vec![1, 2, 3, 4]);
|
||||||
|
writer.add_message_with_order(MessageType::Attribute, vec![5, 6], 7);
|
||||||
|
let bytes = writer.serialize().unwrap();
|
||||||
|
assert_eq!(bytes[5] & 0x0C, 0x0C);
|
||||||
|
let hdr = ObjectHeader::parse(&bytes, 0, 8, 8).unwrap();
|
||||||
|
assert_eq!(hdr.messages.len(), 2);
|
||||||
|
assert_eq!(hdr.messages[0].creation_order, Some(0));
|
||||||
|
assert_eq!(hdr.messages[1].creation_order, Some(7));
|
||||||
|
assert_eq!(hdr.messages[1].data, vec![5, 6]);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn two_messages_roundtrip() {
|
fn two_messages_roundtrip() {
|
||||||
let mut writer = ObjectHeaderWriter::new();
|
let mut writer = ObjectHeaderWriter::new();
|
||||||
|
|||||||
@@ -12,6 +12,22 @@ use crate::error::FormatError;
|
|||||||
use crate::filter_pipeline::FilterPipeline;
|
use crate::filter_pipeline::FilterPipeline;
|
||||||
use crate::filters::decompress_chunk_exact;
|
use crate::filters::decompress_chunk_exact;
|
||||||
use crate::lane_partition::{self, LaneStats, PartitionStats};
|
use crate::lane_partition::{self, LaneStats, PartitionStats};
|
||||||
|
use crate::storage::{ExtentReq, Storage, for_each_extent_batch};
|
||||||
|
|
||||||
|
/// The extents of `chunks`' stored bytes (see
|
||||||
|
/// [`crate::chunked_read::chunk_req`]), fetched batch by batch with
|
||||||
|
/// [`for_each_extent_batch`] when the file is not in memory (each chunk's
|
||||||
|
/// bounds error is reported when that chunk is decoded, as before).
|
||||||
|
fn chunk_reqs(
|
||||||
|
chunks: &[ChunkInfo],
|
||||||
|
pipeline: Option<&FilterPipeline>,
|
||||||
|
chunk_total_bytes: usize,
|
||||||
|
) -> Vec<ExtentReq> {
|
||||||
|
chunks
|
||||||
|
.iter()
|
||||||
|
.map(|c| crate::chunked_read::chunk_req(c, pipeline, chunk_total_bytes, true))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
/// Threshold: only use parallel decompression when chunk count exceeds this.
|
/// Threshold: only use parallel decompression when chunk count exceeds this.
|
||||||
const PARALLEL_THRESHOLD: usize = 4;
|
const PARALLEL_THRESHOLD: usize = 4;
|
||||||
@@ -41,6 +57,132 @@ pub fn pool_can_parallelise() -> bool {
|
|||||||
rayon::current_num_threads() > 1
|
rayon::current_num_threads() > 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// How many rayon workers [`run_with_helpers`] should ask to help with
|
||||||
|
/// `items` work items, given that the calling thread works too: the pool's
|
||||||
|
/// other threads (all of them when the caller is not one), at most one per
|
||||||
|
/// item beyond the caller's first.
|
||||||
|
pub(crate) fn helper_count(items: usize) -> usize {
|
||||||
|
let pool = rayon::current_num_threads();
|
||||||
|
// A one-thread pool means "decode on the calling thread" (the setting
|
||||||
|
// benchmarks use to compare with h5py, where each call decodes on its
|
||||||
|
// caller): no helper, so one read never uses two cores.
|
||||||
|
if pool <= 1 {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
let others = if rayon::current_thread_index().is_some() {
|
||||||
|
pool.saturating_sub(1)
|
||||||
|
} else {
|
||||||
|
pool
|
||||||
|
};
|
||||||
|
others.min(items.saturating_sub(1))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Run `body` on the calling thread and on up to `helpers` rayon workers at
|
||||||
|
/// once, returning when the caller's call has finished and every worker that
|
||||||
|
/// started one has too. `body` shares its work out itself (typically by
|
||||||
|
/// claiming items from an atomic counter until none are left).
|
||||||
|
///
|
||||||
|
/// The caller never waits for a worker to *become* free: helpers are queued
|
||||||
|
/// on the pool, and one that only gets to run after the caller has finished
|
||||||
|
/// returns without calling `body`. So a busy or small pool can only fail to
|
||||||
|
/// speed a read up, never hold it back — with `par_iter`, the calling thread
|
||||||
|
/// (not a pool worker) handed all the work to the pool and slept, and N
|
||||||
|
/// threads reading through a 2-worker pool decoded on 2 cores.
|
||||||
|
///
|
||||||
|
/// A panic in `body`, on any thread, is resumed on the caller once every
|
||||||
|
/// helper that started has stopped.
|
||||||
|
pub(crate) fn run_with_helpers(helpers: usize, body: &(dyn Fn() + Sync)) {
|
||||||
|
use std::panic::{AssertUnwindSafe, catch_unwind, resume_unwind};
|
||||||
|
use std::sync::{Arc, Condvar, Mutex, PoisonError};
|
||||||
|
|
||||||
|
if helpers == 0 {
|
||||||
|
body();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
type Body = dyn Fn() + Sync + 'static;
|
||||||
|
struct Shared {
|
||||||
|
/// `body`, its lifetime erased. Only dereferenced by a helper that
|
||||||
|
/// registered in `state` while it was open (see below).
|
||||||
|
body: *const Body,
|
||||||
|
/// (closed, helpers inside `body`).
|
||||||
|
state: Mutex<(bool, usize)>,
|
||||||
|
idle: Condvar,
|
||||||
|
panic: Mutex<Option<Box<dyn core::any::Any + Send>>>,
|
||||||
|
}
|
||||||
|
// SAFETY: `body` points to a `Sync` closure, so calling it from other
|
||||||
|
// threads is allowed; the pointer is only used under the protocol below,
|
||||||
|
// which keeps it from outliving the closure.
|
||||||
|
unsafe impl Send for Shared {}
|
||||||
|
unsafe impl Sync for Shared {}
|
||||||
|
|
||||||
|
fn help(shared: &Shared) {
|
||||||
|
{
|
||||||
|
let mut state = shared.state.lock().unwrap_or_else(PoisonError::into_inner);
|
||||||
|
if state.0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
state.1 += 1;
|
||||||
|
}
|
||||||
|
// SAFETY: registered while open, so the caller of `run_with_helpers`
|
||||||
|
// is still inside it (it closes, then waits until no helper is
|
||||||
|
// registered, before returning), and `body` is alive.
|
||||||
|
let body = unsafe { &*shared.body };
|
||||||
|
if let Err(payload) = catch_unwind(AssertUnwindSafe(body)) {
|
||||||
|
shared
|
||||||
|
.panic
|
||||||
|
.lock()
|
||||||
|
.unwrap_or_else(PoisonError::into_inner)
|
||||||
|
.get_or_insert(payload);
|
||||||
|
}
|
||||||
|
let mut state = shared.state.lock().unwrap_or_else(PoisonError::into_inner);
|
||||||
|
state.1 -= 1;
|
||||||
|
if state.1 == 0 {
|
||||||
|
shared.idle.notify_all();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let body_ptr: *const (dyn Fn() + Sync + '_) = body;
|
||||||
|
// SAFETY: only the lifetime changes (same fat-pointer layout). The
|
||||||
|
// pointer is dereferenced only while this function is running: see
|
||||||
|
// `help` and the wait below.
|
||||||
|
let body_ptr: *const Body = unsafe { core::mem::transmute(body_ptr) };
|
||||||
|
let shared = Arc::new(Shared {
|
||||||
|
body: body_ptr,
|
||||||
|
state: Mutex::new((false, 0)),
|
||||||
|
idle: Condvar::new(),
|
||||||
|
panic: Mutex::new(None),
|
||||||
|
});
|
||||||
|
for _ in 0..helpers {
|
||||||
|
let shared = Arc::clone(&shared);
|
||||||
|
rayon::spawn(move || help(&shared));
|
||||||
|
}
|
||||||
|
let caller = catch_unwind(AssertUnwindSafe(body));
|
||||||
|
{
|
||||||
|
// Close, then wait for the helpers inside `body`; later ones return
|
||||||
|
// at once. This must happen even if `body` panicked on this thread.
|
||||||
|
let mut state = shared.state.lock().unwrap_or_else(PoisonError::into_inner);
|
||||||
|
state.0 = true;
|
||||||
|
while state.1 > 0 {
|
||||||
|
state = shared
|
||||||
|
.idle
|
||||||
|
.wait(state)
|
||||||
|
.unwrap_or_else(PoisonError::into_inner);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Err(payload) = caller {
|
||||||
|
resume_unwind(payload);
|
||||||
|
}
|
||||||
|
let helper_panic = shared
|
||||||
|
.panic
|
||||||
|
.lock()
|
||||||
|
.unwrap_or_else(PoisonError::into_inner)
|
||||||
|
.take();
|
||||||
|
if let Some(payload) = helper_panic {
|
||||||
|
resume_unwind(payload);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Decompress chunks in parallel using lane-partitioned assignment.
|
/// Decompress chunks in parallel using lane-partitioned assignment.
|
||||||
///
|
///
|
||||||
/// Instead of naive `par_iter`, chunks are deterministically assigned to lanes
|
/// Instead of naive `par_iter`, chunks are deterministically assigned to lanes
|
||||||
@@ -63,6 +205,27 @@ pub fn decompress_chunks_lane_partitioned(
|
|||||||
element_size: u32,
|
element_size: u32,
|
||||||
seed: u64,
|
seed: u64,
|
||||||
num_lanes: Option<usize>,
|
num_lanes: Option<usize>,
|
||||||
|
) -> Result<(Vec<Vec<u8>>, PartitionStats), FormatError> {
|
||||||
|
decompress_chunks_lane_partitioned_in(
|
||||||
|
file_data,
|
||||||
|
chunks,
|
||||||
|
pipeline,
|
||||||
|
chunk_total_bytes,
|
||||||
|
element_size,
|
||||||
|
seed,
|
||||||
|
num_lanes,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`decompress_chunks_lane_partitioned`] over any [`Storage`].
|
||||||
|
pub fn decompress_chunks_lane_partitioned_in<S: Storage + ?Sized>(
|
||||||
|
file_data: &S,
|
||||||
|
chunks: &[ChunkInfo],
|
||||||
|
pipeline: &FilterPipeline,
|
||||||
|
chunk_total_bytes: usize,
|
||||||
|
element_size: u32,
|
||||||
|
seed: u64,
|
||||||
|
num_lanes: Option<usize>,
|
||||||
) -> Result<(Vec<Vec<u8>>, PartitionStats), FormatError> {
|
) -> Result<(Vec<Vec<u8>>, PartitionStats), FormatError> {
|
||||||
use rayon::prelude::*;
|
use rayon::prelude::*;
|
||||||
|
|
||||||
@@ -72,31 +235,28 @@ pub fn decompress_chunks_lane_partitioned(
|
|||||||
.unwrap_or(1)
|
.unwrap_or(1)
|
||||||
});
|
});
|
||||||
|
|
||||||
let assignments = lane_partition::partition_chunks(chunks.len(), lanes, seed);
|
let reqs = chunk_reqs(chunks, Some(pipeline), chunk_total_bytes);
|
||||||
let num_lanes = assignments.len();
|
let mut ordered: Vec<Vec<u8>> = Vec::with_capacity(chunks.len());
|
||||||
|
let mut partition_stats = PartitionStats::new(0);
|
||||||
|
partition_stats.total_chunks = chunks.len();
|
||||||
|
// Each batch of fetched chunks is partitioned into lanes and decoded
|
||||||
|
// before the next batch is fetched (with the file in memory there is
|
||||||
|
// one batch: all the chunks).
|
||||||
|
for_each_extent_batch(file_data, &reqs, |batch, raw_bytes| {
|
||||||
|
let assignments = lane_partition::partition_chunks(batch.len(), lanes, seed);
|
||||||
// Each lane processes its assigned chunks and returns results + stats.
|
// Each lane processes its assigned chunks and returns results + stats.
|
||||||
let lane_results: Result<Vec<(Vec<DecompressedChunk>, LaneStats)>, FormatError> = assignments
|
let lane_results: Result<Vec<(Vec<DecompressedChunk>, LaneStats)>, FormatError> =
|
||||||
|
assignments
|
||||||
.into_par_iter()
|
.into_par_iter()
|
||||||
.map(|indices| {
|
.map(|indices| {
|
||||||
let mut results = Vec::with_capacity(indices.len());
|
let mut results = Vec::with_capacity(indices.len());
|
||||||
let mut stats = LaneStats::default();
|
let mut stats = LaneStats::default();
|
||||||
|
|
||||||
for &index in &indices {
|
for &local in &indices {
|
||||||
|
let index = batch.start + local;
|
||||||
let chunk_info = &chunks[index];
|
let chunk_info = &chunks[index];
|
||||||
let c_addr = chunk_info.address as usize;
|
|
||||||
let size = chunk_info.chunk_size as usize;
|
let size = chunk_info.chunk_size as usize;
|
||||||
|
let raw_chunk = raw_bytes.get(index, &reqs[index])?;
|
||||||
if c_addr
|
|
||||||
.checked_add(size)
|
|
||||||
.is_none_or(|end| end > file_data.len())
|
|
||||||
{
|
|
||||||
return Err(FormatError::UnexpectedEof {
|
|
||||||
expected: c_addr.saturating_add(size),
|
|
||||||
available: file_data.len(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
let raw_chunk = &file_data[c_addr..c_addr + size];
|
|
||||||
|
|
||||||
let decompressed = decompress_chunk_exact(
|
let decompressed = decompress_chunk_exact(
|
||||||
raw_chunk,
|
raw_chunk,
|
||||||
@@ -120,14 +280,19 @@ pub fn decompress_chunks_lane_partitioned(
|
|||||||
Ok((results, stats))
|
Ok((results, stats))
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let lane_results = lane_results?;
|
let lane_results = lane_results?;
|
||||||
|
|
||||||
// Aggregate stats
|
// Aggregate stats
|
||||||
let mut partition_stats = PartitionStats::new(num_lanes);
|
if partition_stats.per_lane.len() < lane_results.len() {
|
||||||
partition_stats.total_chunks = chunks.len();
|
partition_stats
|
||||||
for (lane_idx, (_, stats)) in lane_results.iter().enumerate() {
|
.per_lane
|
||||||
partition_stats.per_lane[lane_idx] = stats.clone();
|
.resize_with(lane_results.len(), LaneStats::default);
|
||||||
|
partition_stats.num_lanes = lane_results.len();
|
||||||
|
}
|
||||||
|
for (lane, (_, stats)) in partition_stats.per_lane.iter_mut().zip(&lane_results) {
|
||||||
|
lane.chunks_processed += stats.chunks_processed;
|
||||||
|
lane.compressed_bytes += stats.compressed_bytes;
|
||||||
|
lane.decompressed_bytes += stats.decompressed_bytes;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Flatten and sort by original index to restore order
|
// Flatten and sort by original index to restore order
|
||||||
@@ -136,8 +301,9 @@ pub fn decompress_chunks_lane_partitioned(
|
|||||||
.flat_map(|(chunks, _)| chunks)
|
.flat_map(|(chunks, _)| chunks)
|
||||||
.collect();
|
.collect();
|
||||||
all_chunks.sort_by_key(|dc| dc.index);
|
all_chunks.sort_by_key(|dc| dc.index);
|
||||||
|
ordered.extend(all_chunks.into_iter().map(|dc| dc.data));
|
||||||
let ordered = all_chunks.into_iter().map(|dc| dc.data).collect();
|
Ok(())
|
||||||
|
})?;
|
||||||
Ok((ordered, partition_stats))
|
Ok((ordered, partition_stats))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,25 +321,29 @@ pub fn decompress_chunks_parallel(
|
|||||||
pipeline: &FilterPipeline,
|
pipeline: &FilterPipeline,
|
||||||
chunk_total_bytes: usize,
|
chunk_total_bytes: usize,
|
||||||
element_size: u32,
|
element_size: u32,
|
||||||
|
) -> Result<Vec<Vec<u8>>, FormatError> {
|
||||||
|
decompress_chunks_parallel_in(file_data, chunks, pipeline, chunk_total_bytes, element_size)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`decompress_chunks_parallel`] over any [`Storage`].
|
||||||
|
pub fn decompress_chunks_parallel_in<S: Storage + ?Sized>(
|
||||||
|
file_data: &S,
|
||||||
|
chunks: &[ChunkInfo],
|
||||||
|
pipeline: &FilterPipeline,
|
||||||
|
chunk_total_bytes: usize,
|
||||||
|
element_size: u32,
|
||||||
) -> Result<Vec<Vec<u8>>, FormatError> {
|
) -> Result<Vec<Vec<u8>>, FormatError> {
|
||||||
use rayon::prelude::*;
|
use rayon::prelude::*;
|
||||||
|
|
||||||
let results: Result<Vec<DecompressedChunk>, FormatError> = chunks
|
let reqs = chunk_reqs(chunks, Some(pipeline), chunk_total_bytes);
|
||||||
.par_iter()
|
let mut ordered: Vec<Vec<u8>> = Vec::with_capacity(chunks.len());
|
||||||
.enumerate()
|
for_each_extent_batch(file_data, &reqs, |batch, raw_bytes| {
|
||||||
.map(|(index, chunk_info)| {
|
let results: Result<Vec<DecompressedChunk>, FormatError> = batch
|
||||||
let c_addr = chunk_info.address as usize;
|
.clone()
|
||||||
let size = chunk_info.chunk_size as usize;
|
.into_par_iter()
|
||||||
if c_addr
|
.map(|index| {
|
||||||
.checked_add(size)
|
let chunk_info = &chunks[index];
|
||||||
.is_none_or(|end| end > file_data.len())
|
let raw_chunk = raw_bytes.get(index, &reqs[index])?;
|
||||||
{
|
|
||||||
return Err(FormatError::UnexpectedEof {
|
|
||||||
expected: c_addr.saturating_add(size),
|
|
||||||
available: file_data.len(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
let raw_chunk = &file_data[c_addr..c_addr + size];
|
|
||||||
|
|
||||||
let decompressed = decompress_chunk_exact(
|
let decompressed = decompress_chunk_exact(
|
||||||
raw_chunk,
|
raw_chunk,
|
||||||
@@ -193,7 +363,10 @@ pub fn decompress_chunks_parallel(
|
|||||||
|
|
||||||
let mut result_vec = results?;
|
let mut result_vec = results?;
|
||||||
result_vec.sort_by_key(|dc| dc.index);
|
result_vec.sort_by_key(|dc| dc.index);
|
||||||
Ok(result_vec.into_iter().map(|dc| dc.data).collect())
|
ordered.extend(result_vec.into_iter().map(|dc| dc.data));
|
||||||
|
Ok(())
|
||||||
|
})?;
|
||||||
|
Ok(ordered)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Decompress chunks sequentially (fallback when parallel is not warranted).
|
/// Decompress chunks sequentially (fallback when parallel is not warranted).
|
||||||
@@ -204,20 +377,23 @@ pub fn decompress_chunks_sequential(
|
|||||||
chunk_total_bytes: usize,
|
chunk_total_bytes: usize,
|
||||||
element_size: u32,
|
element_size: u32,
|
||||||
) -> Result<Vec<Vec<u8>>, FormatError> {
|
) -> Result<Vec<Vec<u8>>, FormatError> {
|
||||||
|
decompress_chunks_sequential_in(file_data, chunks, pipeline, chunk_total_bytes, element_size)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`decompress_chunks_sequential`] over any [`Storage`].
|
||||||
|
pub fn decompress_chunks_sequential_in<S: Storage + ?Sized>(
|
||||||
|
file_data: &S,
|
||||||
|
chunks: &[ChunkInfo],
|
||||||
|
pipeline: Option<&FilterPipeline>,
|
||||||
|
chunk_total_bytes: usize,
|
||||||
|
element_size: u32,
|
||||||
|
) -> Result<Vec<Vec<u8>>, FormatError> {
|
||||||
|
let reqs = chunk_reqs(chunks, pipeline, chunk_total_bytes);
|
||||||
let mut result = Vec::with_capacity(chunks.len());
|
let mut result = Vec::with_capacity(chunks.len());
|
||||||
for chunk_info in chunks {
|
for_each_extent_batch(file_data, &reqs, |batch, raw_bytes| {
|
||||||
let c_addr = chunk_info.address as usize;
|
for i in batch {
|
||||||
let size = chunk_info.chunk_size as usize;
|
let chunk_info = &chunks[i];
|
||||||
if c_addr
|
let raw_chunk = raw_bytes.get(i, &reqs[i])?;
|
||||||
.checked_add(size)
|
|
||||||
.is_none_or(|end| end > file_data.len())
|
|
||||||
{
|
|
||||||
return Err(FormatError::UnexpectedEof {
|
|
||||||
expected: c_addr.saturating_add(size),
|
|
||||||
available: file_data.len(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
let raw_chunk = &file_data[c_addr..c_addr + size];
|
|
||||||
|
|
||||||
let decompressed = if let Some(pl) = pipeline {
|
let decompressed = if let Some(pl) = pipeline {
|
||||||
decompress_chunk_exact(
|
decompress_chunk_exact(
|
||||||
@@ -233,6 +409,8 @@ pub fn decompress_chunks_sequential(
|
|||||||
};
|
};
|
||||||
result.push(decompressed);
|
result.push(decompressed);
|
||||||
}
|
}
|
||||||
|
Ok(())
|
||||||
|
})?;
|
||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -258,6 +436,60 @@ mod tests {
|
|||||||
(file, infos)
|
(file, infos)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Every item is processed exactly once, whatever mix of caller and
|
||||||
|
/// helpers ends up doing it.
|
||||||
|
#[test]
|
||||||
|
fn run_with_helpers_shares_all_work() {
|
||||||
|
use core::sync::atomic::{AtomicUsize, Ordering};
|
||||||
|
for helpers in [0, 1, 3, 16] {
|
||||||
|
let n = 1000;
|
||||||
|
let next = AtomicUsize::new(0);
|
||||||
|
let done: Vec<AtomicUsize> = (0..n).map(|_| AtomicUsize::new(0)).collect();
|
||||||
|
run_with_helpers(helpers, &|| {
|
||||||
|
loop {
|
||||||
|
let i = next.fetch_add(1, Ordering::Relaxed);
|
||||||
|
if i >= n {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
done[i].fetch_add(1, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
assert!(done.iter().all(|d| d.load(Ordering::Relaxed) == 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A panic in the shared body reaches the caller whichever thread it
|
||||||
|
/// happened on, and only after the helpers inside the body have left it
|
||||||
|
/// (they borrow the caller's stack).
|
||||||
|
#[test]
|
||||||
|
fn run_with_helpers_propagates_panics() {
|
||||||
|
use core::sync::atomic::{AtomicUsize, Ordering};
|
||||||
|
use std::panic::{AssertUnwindSafe, catch_unwind};
|
||||||
|
let caller = std::thread::current().id();
|
||||||
|
for panic_on_caller in [true, false] {
|
||||||
|
let inside = AtomicUsize::new(0);
|
||||||
|
let calls = AtomicUsize::new(0);
|
||||||
|
let result = catch_unwind(AssertUnwindSafe(|| {
|
||||||
|
run_with_helpers(4, &|| {
|
||||||
|
inside.fetch_add(1, Ordering::SeqCst);
|
||||||
|
calls.fetch_add(1, Ordering::SeqCst);
|
||||||
|
let on_caller = std::thread::current().id() == caller;
|
||||||
|
std::thread::sleep(std::time::Duration::from_millis(20));
|
||||||
|
inside.fetch_sub(1, Ordering::SeqCst);
|
||||||
|
if on_caller == panic_on_caller {
|
||||||
|
panic!("boom");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
// A helper may never have run (the pool was slow to start it),
|
||||||
|
// in which case nothing panicked when `panic_on_caller` is false.
|
||||||
|
if panic_on_caller || calls.load(Ordering::SeqCst) > 1 {
|
||||||
|
assert!(result.is_err());
|
||||||
|
}
|
||||||
|
assert_eq!(inside.load(Ordering::SeqCst), 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Every parallel decoder refuses a chunk that decodes short, naming it.
|
/// Every parallel decoder refuses a chunk that decodes short, naming it.
|
||||||
#[test]
|
#[test]
|
||||||
fn short_decoded_chunk_is_an_error() {
|
fn short_decoded_chunk_is_an_error() {
|
||||||
|
|||||||
@@ -18,14 +18,15 @@ use alloc::{format, vec, vec::Vec};
|
|||||||
#[cfg(feature = "std")]
|
#[cfg(feature = "std")]
|
||||||
use std::string as alloc_or_std;
|
use std::string as alloc_or_std;
|
||||||
|
|
||||||
use crate::chunked_read::{alloc_output, checked_byte_len, list_chunks};
|
use crate::chunked_read::{alloc_output, checked_byte_len, list_chunks_for_read_in};
|
||||||
use crate::data_layout::DataLayout;
|
use crate::data_layout::DataLayout;
|
||||||
use crate::data_read::extract_selection_from_buffer;
|
use crate::data_read::extract_selection_from_buffer;
|
||||||
use crate::dataspace::Dataspace;
|
use crate::dataspace::Dataspace;
|
||||||
use crate::error::FormatError;
|
use crate::error::FormatError;
|
||||||
use crate::filter_pipeline::FilterPipeline;
|
use crate::filter_pipeline::FilterPipeline;
|
||||||
use crate::filters::{all_filters_skipped, decompress_chunk_exact};
|
use crate::filters::{all_filters_skipped, decompress_chunk_exact_with};
|
||||||
use crate::selection::Selection;
|
use crate::selection::Selection;
|
||||||
|
use crate::storage::{ExtentReq, Storage, for_each_extent_batch};
|
||||||
|
|
||||||
/// The smallest axis-aligned box containing every selected element, as
|
/// The smallest axis-aligned box containing every selected element, as
|
||||||
/// `(start, extent)` per dimension. `None` when there is nothing to gain or
|
/// `(start, extent)` per dimension. `None` when there is nothing to gain or
|
||||||
@@ -203,7 +204,12 @@ fn copy_overlap(
|
|||||||
};
|
};
|
||||||
let (src_strides, out_strides) = (strides(src_shape), strides(box_extent));
|
let (src_strides, out_strides) = (strides(src_shape), strides(box_extent));
|
||||||
let last = rank - 1;
|
let last = rank - 1;
|
||||||
let run = ((hi[last] - lo[last]) as usize) * elem_size;
|
// Byte offsets into the in-memory buffers; one that does not fit `usize`
|
||||||
|
// (a 32-bit target) is out of both buffers, like one past their ends.
|
||||||
|
let bytes = |elements: u64| usize::try_from(elements).ok()?.checked_mul(elem_size);
|
||||||
|
let Some(run) = bytes(hi[last] - lo[last]) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
let mut idx = lo.clone();
|
let mut idx = lo.clone();
|
||||||
loop {
|
loop {
|
||||||
@@ -213,8 +219,12 @@ fn copy_overlap(
|
|||||||
let out_at: u64 = (0..rank)
|
let out_at: u64 = (0..rank)
|
||||||
.map(|d| (idx[d] - box_start[d]) * out_strides[d])
|
.map(|d| (idx[d] - box_start[d]) * out_strides[d])
|
||||||
.sum();
|
.sum();
|
||||||
let (s, o) = (src_at as usize * elem_size, out_at as usize * elem_size);
|
if let (Some(s), Some(o)) = (bytes(src_at), bytes(out_at))
|
||||||
if let (Some(from), Some(to)) = (src.get(s..s + run), out.get_mut(o..o + run)) {
|
&& let (Some(from), Some(to)) = (
|
||||||
|
src.get(s..s.saturating_add(run)),
|
||||||
|
out.get_mut(o..o.saturating_add(run)),
|
||||||
|
)
|
||||||
|
{
|
||||||
to.copy_from_slice(from);
|
to.copy_from_slice(from);
|
||||||
}
|
}
|
||||||
// Advance over every dimension but the last.
|
// Advance over every dimension but the last.
|
||||||
@@ -247,6 +257,30 @@ pub fn read_selection(
|
|||||||
offset_size: u8,
|
offset_size: u8,
|
||||||
length_size: u8,
|
length_size: u8,
|
||||||
selection: &Selection,
|
selection: &Selection,
|
||||||
|
) -> Result<Option<Vec<u8>>, FormatError> {
|
||||||
|
read_selection_in(
|
||||||
|
file_data,
|
||||||
|
layout,
|
||||||
|
dataspace,
|
||||||
|
elem_size,
|
||||||
|
pipeline,
|
||||||
|
offset_size,
|
||||||
|
length_size,
|
||||||
|
selection,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`read_selection`] over any [`Storage`].
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
pub fn read_selection_in<S: Storage + ?Sized>(
|
||||||
|
file_data: &S,
|
||||||
|
layout: &DataLayout,
|
||||||
|
dataspace: &Dataspace,
|
||||||
|
elem_size: usize,
|
||||||
|
pipeline: Option<&FilterPipeline>,
|
||||||
|
offset_size: u8,
|
||||||
|
length_size: u8,
|
||||||
|
selection: &Selection,
|
||||||
) -> Result<Option<Vec<u8>>, FormatError> {
|
) -> Result<Option<Vec<u8>>, FormatError> {
|
||||||
let dims = &dataspace.dimensions;
|
let dims = &dataspace.dimensions;
|
||||||
if dims.is_empty() || elem_size == 0 {
|
if dims.is_empty() || elem_size == 0 {
|
||||||
@@ -267,15 +301,34 @@ pub fn read_selection(
|
|||||||
validate(selection, dims)?;
|
validate(selection, dims)?;
|
||||||
let base = usize::try_from(*address)
|
let base = usize::try_from(*address)
|
||||||
.map_err(|_| FormatError::Overflow("data address exceeds usize".into()))?;
|
.map_err(|_| FormatError::Overflow("data address exceeds usize".into()))?;
|
||||||
let data = file_data
|
let file_len = crate::storage::len_usize(file_data);
|
||||||
|
let eof = FormatError::UnexpectedEof {
|
||||||
|
expected: base,
|
||||||
|
available: file_len,
|
||||||
|
};
|
||||||
|
if let Some(all) = file_data.as_contiguous() {
|
||||||
|
let data = all
|
||||||
.get(base..)
|
.get(base..)
|
||||||
.and_then(|d| d.get(..checked_byte_len(total, elem_size).ok()?))
|
.and_then(|d| d.get(..checked_byte_len(total, elem_size).ok()?))
|
||||||
.ok_or(FormatError::UnexpectedEof {
|
.ok_or(eof)?;
|
||||||
expected: base,
|
|
||||||
available: file_data.len(),
|
|
||||||
})?;
|
|
||||||
return crate::gather::gather::<u8>(data, dims, elem_size, selection).map(Some);
|
return crate::gather::gather::<u8>(data, dims, elem_size, selection).map(Some);
|
||||||
}
|
}
|
||||||
|
// Not in memory: the same bounds check, then only the selected runs
|
||||||
|
// are read.
|
||||||
|
let len = checked_byte_len(total, elem_size)
|
||||||
|
.ok()
|
||||||
|
.filter(|&len| base <= file_len && len <= file_len - base)
|
||||||
|
.ok_or(eof)?;
|
||||||
|
return crate::gather::gather_storage(
|
||||||
|
file_data,
|
||||||
|
base as u64,
|
||||||
|
len,
|
||||||
|
dims,
|
||||||
|
elem_size,
|
||||||
|
selection,
|
||||||
|
)
|
||||||
|
.map(Some);
|
||||||
|
}
|
||||||
let Some((box_start, box_extent)) = bounding_box(selection, dims) else {
|
let Some((box_start, box_extent)) = bounding_box(selection, dims) else {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
};
|
};
|
||||||
@@ -294,52 +347,61 @@ pub fn read_selection(
|
|||||||
btree_address: Some(_),
|
btree_address: Some(_),
|
||||||
..
|
..
|
||||||
} => {
|
} => {
|
||||||
let (chunks, chunk_dims) = list_chunks(
|
let (chunks, chunk_dims) = list_chunks_for_read_in(
|
||||||
file_data,
|
file_data,
|
||||||
layout,
|
layout,
|
||||||
dataspace,
|
dataspace,
|
||||||
elem_size,
|
elem_size,
|
||||||
|
pipeline,
|
||||||
offset_size,
|
offset_size,
|
||||||
length_size,
|
length_size,
|
||||||
)?;
|
)?;
|
||||||
let rank = dims.len();
|
let rank = dims.len();
|
||||||
let chunk_shape: Vec<u64> = chunk_dims.iter().map(|&d| d as u64).collect();
|
let chunk_shape: Vec<u64> = chunk_dims.iter().map(|&d| d as u64).collect();
|
||||||
let chunk_bytes = crate::chunked_read::checked_chunk_byte_len(&chunk_dims, elem_size)?;
|
let chunk_bytes = crate::chunked_read::checked_chunk_byte_len(&chunk_dims, elem_size)?;
|
||||||
for chunk in &chunks {
|
// The chunks overlapping the box, in index order.
|
||||||
|
let wanted: Vec<&crate::chunked_read::ChunkInfo> = chunks
|
||||||
|
.iter()
|
||||||
|
.filter(|chunk| {
|
||||||
if chunk.offsets.len() < rank || chunk.address == u64::MAX {
|
if chunk.offsets.len() < rank || chunk.address == u64::MAX {
|
||||||
continue;
|
return false;
|
||||||
}
|
}
|
||||||
let origin = &chunk.offsets[..rank];
|
let origin = &chunk.offsets[..rank];
|
||||||
let overlaps = (0..rank).all(|d| {
|
(0..rank).all(|d| {
|
||||||
origin[d] < box_start[d] + box_extent[d]
|
origin[d] < box_start[d] + box_extent[d]
|
||||||
&& origin[d].saturating_add(chunk_shape[d]) > box_start[d]
|
&& origin[d].saturating_add(chunk_shape[d]) > box_start[d]
|
||||||
});
|
})
|
||||||
if !overlaps {
|
})
|
||||||
continue;
|
.collect();
|
||||||
}
|
// Their stored bytes, batch by batch when the file is not in
|
||||||
let at = usize::try_from(chunk.address)
|
// memory; each batch's chunks are decoded into this thread's
|
||||||
.map_err(|_| FormatError::Overflow("chunk address exceeds usize".into()))?;
|
// reusable buffers before the next batch is fetched.
|
||||||
let raw = at
|
let reqs: Vec<ExtentReq> = wanted
|
||||||
.checked_add(chunk.chunk_size as usize)
|
.iter()
|
||||||
.and_then(|end| file_data.get(at..end))
|
.map(|c| crate::chunked_read::chunk_req(c, pipeline, chunk_bytes, true))
|
||||||
.ok_or(FormatError::UnexpectedEof {
|
.collect();
|
||||||
expected: at.saturating_add(chunk.chunk_size as usize),
|
for_each_extent_batch(file_data, &reqs, |batch, raw_bytes| {
|
||||||
available: file_data.len(),
|
crate::chunked_read::with_scratch(|scratch| -> Result<(), FormatError> {
|
||||||
|
for i in batch {
|
||||||
|
let chunk = wanted[i];
|
||||||
|
let origin = &chunk.offsets[..rank];
|
||||||
|
usize::try_from(chunk.address).map_err(|_| {
|
||||||
|
FormatError::Overflow("chunk address exceeds usize".into())
|
||||||
})?;
|
})?;
|
||||||
// Mirrors the full-read path: filter-mask bit i set means
|
let raw = raw_bytes.get(i, &reqs[i])?;
|
||||||
// filter i was not applied to this chunk.
|
// Mirrors the full-read path: filter-mask bit i set
|
||||||
let decoded;
|
// means filter i was not applied to this chunk.
|
||||||
let data: &[u8] = match pipeline {
|
let data: &[u8] = match pipeline {
|
||||||
Some(pl) if !all_filters_skipped(pl, chunk.filter_mask) => {
|
Some(pl) if !all_filters_skipped(pl, chunk.filter_mask) => {
|
||||||
decoded = decompress_chunk_exact(
|
decompress_chunk_exact_with(
|
||||||
raw,
|
raw,
|
||||||
pl,
|
pl,
|
||||||
chunk_bytes,
|
chunk_bytes,
|
||||||
elem_size as u32,
|
elem_size as u32,
|
||||||
chunk.filter_mask,
|
chunk.filter_mask,
|
||||||
&chunk.offsets[..rank],
|
&chunk.offsets[..rank],
|
||||||
)?;
|
scratch,
|
||||||
&decoded
|
)?
|
||||||
}
|
}
|
||||||
_ => raw,
|
_ => raw,
|
||||||
};
|
};
|
||||||
@@ -353,6 +415,9 @@ pub fn read_selection(
|
|||||||
elem_size,
|
elem_size,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
})?;
|
||||||
}
|
}
|
||||||
_ => return Ok(None),
|
_ => return Ok(None),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ use sha2::{Digest, Sha256};
|
|||||||
|
|
||||||
use crate::attribute::AttributeMessage;
|
use crate::attribute::AttributeMessage;
|
||||||
use crate::data_layout::DataLayout;
|
use crate::data_layout::DataLayout;
|
||||||
use crate::data_read::read_raw_data;
|
|
||||||
use crate::dataspace::Dataspace;
|
use crate::dataspace::Dataspace;
|
||||||
use crate::datatype::Datatype;
|
use crate::datatype::Datatype;
|
||||||
use crate::error::FormatError;
|
use crate::error::FormatError;
|
||||||
@@ -128,10 +127,20 @@ pub fn verify_dataset(
|
|||||||
header: &ObjectHeader,
|
header: &ObjectHeader,
|
||||||
offset_size: u8,
|
offset_size: u8,
|
||||||
length_size: u8,
|
length_size: u8,
|
||||||
|
) -> Result<VerifyResult, FormatError> {
|
||||||
|
verify_dataset_in(file_data, header, offset_size, length_size)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`verify_dataset`] over any [`Storage`](crate::storage::Storage).
|
||||||
|
pub fn verify_dataset_in<S: crate::storage::Storage + ?Sized>(
|
||||||
|
file_data: &S,
|
||||||
|
header: &ObjectHeader,
|
||||||
|
offset_size: u8,
|
||||||
|
length_size: u8,
|
||||||
) -> Result<VerifyResult, FormatError> {
|
) -> Result<VerifyResult, FormatError> {
|
||||||
// 1. Extract all attributes (compact + dense).
|
// 1. Extract all attributes (compact + dense).
|
||||||
let attrs =
|
let attrs =
|
||||||
crate::attribute::extract_attributes_full(file_data, header, offset_size, length_size)?;
|
crate::attribute::extract_attributes_full_in(file_data, header, offset_size, length_size)?;
|
||||||
|
|
||||||
// 2. Find the stored hash.
|
// 2. Find the stored hash.
|
||||||
let stored_hash = attrs
|
let stored_hash = attrs
|
||||||
@@ -174,7 +183,7 @@ pub fn verify_dataset(
|
|||||||
.transpose()?;
|
.transpose()?;
|
||||||
|
|
||||||
let raw = match &dl {
|
let raw = match &dl {
|
||||||
DataLayout::Chunked { .. } => crate::chunked_read::read_chunked_data(
|
DataLayout::Chunked { .. } => crate::chunked_read::read_chunked_data_in(
|
||||||
file_data,
|
file_data,
|
||||||
&dl,
|
&dl,
|
||||||
&ds,
|
&ds,
|
||||||
@@ -183,7 +192,7 @@ pub fn verify_dataset(
|
|||||||
offset_size,
|
offset_size,
|
||||||
length_size,
|
length_size,
|
||||||
)?,
|
)?,
|
||||||
_ => read_raw_data(file_data, &dl, &ds, &dt)?,
|
_ => crate::data_read::read_raw_data_in(file_data, &dl, &ds, &dt)?,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 4. Compare.
|
// 4. Compare.
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ use alloc::{vec, vec::Vec};
|
|||||||
|
|
||||||
use core::ops::Range;
|
use core::ops::Range;
|
||||||
|
|
||||||
|
use crate::addr::to_usize;
|
||||||
use crate::error::FormatError;
|
use crate::error::FormatError;
|
||||||
|
|
||||||
/// A selection describing which elements of a dataset to access.
|
/// A selection describing which elements of a dataset to access.
|
||||||
@@ -562,7 +563,7 @@ fn decode_hyperslab(r: &mut SelReader, version: u64) -> Result<SerializedSelecti
|
|||||||
if !matches!(enc_size, 2 | 4 | 8) {
|
if !matches!(enc_size, 2 | 4 | 8) {
|
||||||
return Err(sel_err("unsupported hyperslab coordinate encoding size"));
|
return Err(sel_err("unsupported hyperslab coordinate encoding size"));
|
||||||
}
|
}
|
||||||
let rank = r.uint(4)? as usize;
|
let rank = to_usize(r.uint(4)?)?;
|
||||||
// HDF5 caps dataspace rank at 32 (H5S_MAX_RANK). Reject anything else so a
|
// HDF5 caps dataspace rank at 32 (H5S_MAX_RANK). Reject anything else so a
|
||||||
// corrupt rank can't drive a huge allocation or read loop.
|
// corrupt rank can't drive a huge allocation or read loop.
|
||||||
if rank == 0 || rank > 32 {
|
if rank == 0 || rank > 32 {
|
||||||
@@ -625,11 +626,11 @@ fn decode_hyperslab(r: &mut SelReader, version: u64) -> Result<SerializedSelecti
|
|||||||
return Err(FormatError::UnexpectedEof {
|
return Err(FormatError::UnexpectedEof {
|
||||||
expected: r
|
expected: r
|
||||||
.pos
|
.pos
|
||||||
.saturating_add(nblocks.saturating_mul(per_block) as usize),
|
.saturating_add(to_usize(nblocks.saturating_mul(per_block))?),
|
||||||
available: r.data.len(),
|
available: r.data.len(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
let n = nblocks as usize * rank;
|
let n = to_usize(nblocks)? * rank;
|
||||||
let (mut starts, mut ends) = (Vec::with_capacity(n), Vec::with_capacity(n));
|
let (mut starts, mut ends) = (Vec::with_capacity(n), Vec::with_capacity(n));
|
||||||
for _ in 0..nblocks {
|
for _ in 0..nblocks {
|
||||||
for _ in 0..rank {
|
for _ in 0..rank {
|
||||||
@@ -662,7 +663,7 @@ fn blocks_union_coords(
|
|||||||
.filter(|&t| t <= MAX_EXPANDED_POINTS)
|
.filter(|&t| t <= MAX_EXPANDED_POINTS)
|
||||||
.ok_or_else(|| sel_err("irregular hyperslab selection is too large to expand"))?;
|
.ok_or_else(|| sel_err("irregular hyperslab selection is too large to expand"))?;
|
||||||
}
|
}
|
||||||
let mut out = Vec::with_capacity(total as usize);
|
let mut out = Vec::with_capacity(to_usize(total)?);
|
||||||
for (s, e) in starts.chunks_exact(rank).zip(ends.chunks_exact(rank)) {
|
for (s, e) in starts.chunks_exact(rank).zip(ends.chunks_exact(rank)) {
|
||||||
let mut cur = s.to_vec();
|
let mut cur = s.to_vec();
|
||||||
'block: loop {
|
'block: loop {
|
||||||
|
|||||||
@@ -23,11 +23,12 @@ use alloc::vec::Vec;
|
|||||||
#[cfg(feature = "std")]
|
#[cfg(feature = "std")]
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
|
|
||||||
use crate::btree_v2::{BTreeV2Header, collect_btree_v2_records};
|
use crate::btree_v2::{BTreeV2Header, collect_btree_v2_records_in};
|
||||||
use crate::error::FormatError;
|
use crate::error::FormatError;
|
||||||
use crate::fractal_heap::FractalHeapHeader;
|
use crate::fractal_heap::FractalHeapHeader;
|
||||||
use crate::message_type::MessageType;
|
use crate::message_type::MessageType;
|
||||||
use crate::object_header::ObjectHeader;
|
use crate::object_header::ObjectHeader;
|
||||||
|
use crate::storage::{Storage, Window, read_exact_at};
|
||||||
|
|
||||||
/// Fractal heap ID length for SOHM entries (fixed at 8 bytes).
|
/// Fractal heap ID length for SOHM entries (fixed at 8 bytes).
|
||||||
const FHEAP_ID_LEN: usize = 8;
|
const FHEAP_ID_LEN: usize = 8;
|
||||||
@@ -253,17 +254,31 @@ pub fn parse_sohm_table(
|
|||||||
nindexes: u8,
|
nindexes: u8,
|
||||||
offset_size: u8,
|
offset_size: u8,
|
||||||
) -> Result<SohmTable, FormatError> {
|
) -> Result<SohmTable, FormatError> {
|
||||||
ensure_len(file_data, table_addr, 4)?;
|
parse_sohm_table_in(file_data, table_addr as u64, nindexes, offset_size)
|
||||||
if &file_data[table_addr..table_addr + 4] != b"SMTB" {
|
}
|
||||||
|
|
||||||
|
/// [`parse_sohm_table`] over any [`Storage`]: one read of the signature,
|
||||||
|
/// one of every index entry.
|
||||||
|
pub fn parse_sohm_table_in<S: Storage + ?Sized>(
|
||||||
|
file: &S,
|
||||||
|
table_addr: u64,
|
||||||
|
nindexes: u8,
|
||||||
|
offset_size: u8,
|
||||||
|
) -> Result<SohmTable, FormatError> {
|
||||||
|
let sig = read_exact_at(file, table_addr, 4)?;
|
||||||
|
if *sig != *b"SMTB" {
|
||||||
return Err(FormatError::InvalidSohmTableSignature);
|
return Err(FormatError::InvalidSohmTableSignature);
|
||||||
}
|
}
|
||||||
let mut pos = table_addr + 4;
|
|
||||||
let os = offset_size as usize;
|
let os = offset_size as usize;
|
||||||
let entry_size = 1 + 1 + 2 + 4 + 2 + 2 + 2 + os + os; // 14 + 2*offset_size
|
let entry_size = 1 + 1 + 2 + 4 + 2 + 2 + 2 + os + os; // 14 + 2*offset_size
|
||||||
|
// Positions below are relative to the table.
|
||||||
|
let w = Window::read(file, table_addr, 4 + nindexes as usize * entry_size)?;
|
||||||
|
let file_data: &[u8] = &w.bytes;
|
||||||
|
let mut pos = 4;
|
||||||
|
|
||||||
let mut indexes = Vec::with_capacity(nindexes as usize);
|
let mut indexes = Vec::with_capacity(nindexes as usize);
|
||||||
for _ in 0..nindexes {
|
for _ in 0..nindexes {
|
||||||
ensure_len(file_data, pos, entry_size)?;
|
w.ensure(pos, entry_size)?;
|
||||||
let version = file_data[pos];
|
let version = file_data[pos];
|
||||||
if version != 0 {
|
if version != 0 {
|
||||||
return Err(FormatError::InvalidSohmTableVersion(version));
|
return Err(FormatError::InvalidSohmTableVersion(version));
|
||||||
@@ -369,16 +384,29 @@ pub fn parse_sohm_list(
|
|||||||
num_messages: u16,
|
num_messages: u16,
|
||||||
offset_size: u8,
|
offset_size: u8,
|
||||||
) -> Result<Vec<SohmEntry>, FormatError> {
|
) -> Result<Vec<SohmEntry>, FormatError> {
|
||||||
ensure_len(file_data, list_addr, 4)?;
|
parse_sohm_list_in(file_data, list_addr as u64, num_messages, offset_size)
|
||||||
if &file_data[list_addr..list_addr + 4] != b"SMLI" {
|
}
|
||||||
|
|
||||||
|
/// [`parse_sohm_list`] over any [`Storage`]: one read of the signature, one
|
||||||
|
/// of every entry.
|
||||||
|
pub fn parse_sohm_list_in<S: Storage + ?Sized>(
|
||||||
|
file: &S,
|
||||||
|
list_addr: u64,
|
||||||
|
num_messages: u16,
|
||||||
|
offset_size: u8,
|
||||||
|
) -> Result<Vec<SohmEntry>, FormatError> {
|
||||||
|
let sig = read_exact_at(file, list_addr, 4)?;
|
||||||
|
if *sig != *b"SMLI" {
|
||||||
return Err(FormatError::InvalidSohmListSignature);
|
return Err(FormatError::InvalidSohmListSignature);
|
||||||
}
|
}
|
||||||
let entry_sz = sohm_entry_size(offset_size);
|
let entry_sz = sohm_entry_size(offset_size);
|
||||||
let mut pos = list_addr + 4;
|
// Positions below are relative to the list.
|
||||||
|
let w = Window::read(file, list_addr, 4 + num_messages as usize * entry_sz)?;
|
||||||
|
let mut pos = 4;
|
||||||
let mut entries = Vec::with_capacity(num_messages as usize);
|
let mut entries = Vec::with_capacity(num_messages as usize);
|
||||||
for _ in 0..num_messages {
|
for _ in 0..num_messages {
|
||||||
ensure_len(file_data, pos, entry_sz)?;
|
w.ensure(pos, entry_sz)?;
|
||||||
let entry = parse_sohm_entry(&file_data[pos..], offset_size)?;
|
let entry = parse_sohm_entry(&w.bytes[pos..], offset_size)?;
|
||||||
entries.push(entry);
|
entries.push(entry);
|
||||||
pos += entry_sz;
|
pos += entry_sz;
|
||||||
}
|
}
|
||||||
@@ -392,8 +420,18 @@ pub fn parse_sohm_btree_entries(
|
|||||||
offset_size: u8,
|
offset_size: u8,
|
||||||
length_size: u8,
|
length_size: u8,
|
||||||
) -> Result<Vec<SohmEntry>, FormatError> {
|
) -> Result<Vec<SohmEntry>, FormatError> {
|
||||||
let header = BTreeV2Header::parse(file_data, btree_addr, offset_size, length_size)?;
|
parse_sohm_btree_entries_in(file_data, btree_addr as u64, offset_size, length_size)
|
||||||
let records = collect_btree_v2_records(file_data, &header, offset_size, length_size)?;
|
}
|
||||||
|
|
||||||
|
/// [`parse_sohm_btree_entries`] over any [`Storage`].
|
||||||
|
pub fn parse_sohm_btree_entries_in<S: Storage + ?Sized>(
|
||||||
|
file: &S,
|
||||||
|
btree_addr: u64,
|
||||||
|
offset_size: u8,
|
||||||
|
length_size: u8,
|
||||||
|
) -> Result<Vec<SohmEntry>, FormatError> {
|
||||||
|
let header = BTreeV2Header::parse_in(file, btree_addr, offset_size, length_size)?;
|
||||||
|
let records = collect_btree_v2_records_in(file, &header, offset_size, length_size)?;
|
||||||
let mut entries = Vec::with_capacity(records.len());
|
let mut entries = Vec::with_capacity(records.len());
|
||||||
for rec in &records {
|
for rec in &records {
|
||||||
let entry = parse_sohm_entry(&rec.data, offset_size)?;
|
let entry = parse_sohm_entry(&rec.data, offset_size)?;
|
||||||
@@ -413,15 +451,24 @@ pub fn load_sohm_table(
|
|||||||
offset_size: u8,
|
offset_size: u8,
|
||||||
length_size: u8,
|
length_size: u8,
|
||||||
) -> Result<Option<SohmTable>, FormatError> {
|
) -> Result<Option<SohmTable>, FormatError> {
|
||||||
let sig = crate::signature::find_signature(file_data)?;
|
load_sohm_table_in(file_data, offset_size, length_size)
|
||||||
let sb = crate::superblock::Superblock::parse(file_data, sig)?;
|
}
|
||||||
|
|
||||||
|
/// [`load_sohm_table`] over any [`Storage`].
|
||||||
|
pub fn load_sohm_table_in<S: Storage + ?Sized>(
|
||||||
|
file_data: &S,
|
||||||
|
offset_size: u8,
|
||||||
|
length_size: u8,
|
||||||
|
) -> Result<Option<SohmTable>, FormatError> {
|
||||||
|
let sig = crate::signature::find_signature_in(file_data)?;
|
||||||
|
let sb = crate::superblock::Superblock::parse_in(file_data, sig)?;
|
||||||
let Some(ext_addr) = sb
|
let Some(ext_addr) = sb
|
||||||
.superblock_extension_address
|
.superblock_extension_address
|
||||||
.filter(|&a| !is_undefined(a, offset_size))
|
.filter(|&a| !is_undefined(a, offset_size))
|
||||||
else {
|
else {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
};
|
};
|
||||||
let ext = ObjectHeader::parse(file_data, ext_addr as usize, offset_size, length_size)?;
|
let ext = ObjectHeader::parse_in(file_data, ext_addr, offset_size, length_size)?;
|
||||||
let Some(msg) = ext
|
let Some(msg) = ext
|
||||||
.messages
|
.messages
|
||||||
.iter()
|
.iter()
|
||||||
@@ -430,9 +477,9 @@ pub fn load_sohm_table(
|
|||||||
return Ok(None);
|
return Ok(None);
|
||||||
};
|
};
|
||||||
let table_msg = parse_sohm_table_message(&msg.data, offset_size)?;
|
let table_msg = parse_sohm_table_message(&msg.data, offset_size)?;
|
||||||
parse_sohm_table(
|
parse_sohm_table_in(
|
||||||
file_data,
|
file_data,
|
||||||
table_msg.table_address as usize,
|
table_msg.table_address,
|
||||||
table_msg.nindexes,
|
table_msg.nindexes,
|
||||||
offset_size,
|
offset_size,
|
||||||
)
|
)
|
||||||
@@ -446,17 +493,27 @@ pub fn message_data_with_sohm<'a>(
|
|||||||
msg: &'a crate::object_header::HeaderMessage,
|
msg: &'a crate::object_header::HeaderMessage,
|
||||||
offset_size: u8,
|
offset_size: u8,
|
||||||
length_size: u8,
|
length_size: u8,
|
||||||
|
) -> Result<Cow<'a, [u8]>, FormatError> {
|
||||||
|
message_data_with_sohm_in(file_data, msg, offset_size, length_size)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`message_data_with_sohm`] over any [`Storage`].
|
||||||
|
pub fn message_data_with_sohm_in<'a, S: Storage + ?Sized>(
|
||||||
|
file_data: &S,
|
||||||
|
msg: &'a crate::object_header::HeaderMessage,
|
||||||
|
offset_size: u8,
|
||||||
|
length_size: u8,
|
||||||
) -> Result<Cow<'a, [u8]>, FormatError> {
|
) -> Result<Cow<'a, [u8]>, FormatError> {
|
||||||
if !is_shared(msg.flags) {
|
if !is_shared(msg.flags) {
|
||||||
return Ok(Cow::Borrowed(&msg.data));
|
return Ok(Cow::Borrowed(&msg.data));
|
||||||
}
|
}
|
||||||
let shared_ref = parse_shared_ref_sized(&msg.data, offset_size, length_size)?;
|
let shared_ref = parse_shared_ref_sized(&msg.data, offset_size, length_size)?;
|
||||||
let table = if shared_ref.heap_id.is_some() {
|
let table = if shared_ref.heap_id.is_some() {
|
||||||
load_sohm_table(file_data, offset_size, length_size)?
|
load_sohm_table_in(file_data, offset_size, length_size)?
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
resolve_shared_message_with_sohm(
|
resolve_shared_message_with_sohm_in(
|
||||||
file_data,
|
file_data,
|
||||||
&shared_ref,
|
&shared_ref,
|
||||||
msg.msg_type,
|
msg.msg_type,
|
||||||
@@ -495,6 +552,25 @@ pub fn resolve_sohm_message(
|
|||||||
target_msg_type: MessageType,
|
target_msg_type: MessageType,
|
||||||
offset_size: u8,
|
offset_size: u8,
|
||||||
length_size: u8,
|
length_size: u8,
|
||||||
|
) -> Result<Vec<u8>, FormatError> {
|
||||||
|
resolve_sohm_message_in(
|
||||||
|
&file_data,
|
||||||
|
heap_id,
|
||||||
|
sohm_table,
|
||||||
|
target_msg_type,
|
||||||
|
offset_size,
|
||||||
|
length_size,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`resolve_sohm_message`] over any [`Storage`].
|
||||||
|
pub fn resolve_sohm_message_in<S: Storage + ?Sized>(
|
||||||
|
file_data: &S,
|
||||||
|
heap_id: &[u8; FHEAP_ID_LEN],
|
||||||
|
sohm_table: &SohmTable,
|
||||||
|
target_msg_type: MessageType,
|
||||||
|
offset_size: u8,
|
||||||
|
length_size: u8,
|
||||||
) -> Result<Vec<u8>, FormatError> {
|
) -> Result<Vec<u8>, FormatError> {
|
||||||
let index = find_index_for_msg_type(sohm_table, target_msg_type)
|
let index = find_index_for_msg_type(sohm_table, target_msg_type)
|
||||||
.ok_or(FormatError::InvalidSharedMessageVersion(2))?;
|
.ok_or(FormatError::InvalidSharedMessageVersion(2))?;
|
||||||
@@ -503,13 +579,9 @@ pub fn resolve_sohm_message(
|
|||||||
return Err(FormatError::InvalidSharedMessageVersion(2));
|
return Err(FormatError::InvalidSharedMessageVersion(2));
|
||||||
}
|
}
|
||||||
|
|
||||||
let fh_header = FractalHeapHeader::parse(
|
let fh_header =
|
||||||
file_data,
|
FractalHeapHeader::parse_in(file_data, index.heap_addr, offset_size, length_size)?;
|
||||||
index.heap_addr as usize,
|
fh_header.read_managed_object_in(file_data, heap_id, offset_size)
|
||||||
offset_size,
|
|
||||||
length_size,
|
|
||||||
)?;
|
|
||||||
fh_header.read_managed_object(file_data, heap_id, offset_size)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The payload of an object-header message, following the indirection if the
|
/// The payload of an object-header message, following the indirection if the
|
||||||
@@ -526,12 +598,22 @@ pub fn message_data<'a>(
|
|||||||
msg: &'a crate::object_header::HeaderMessage,
|
msg: &'a crate::object_header::HeaderMessage,
|
||||||
offset_size: u8,
|
offset_size: u8,
|
||||||
length_size: u8,
|
length_size: u8,
|
||||||
|
) -> Result<Cow<'a, [u8]>, FormatError> {
|
||||||
|
message_data_in(file_data, msg, offset_size, length_size)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`message_data`] over any [`Storage`].
|
||||||
|
pub fn message_data_in<'a, S: Storage + ?Sized>(
|
||||||
|
file_data: &S,
|
||||||
|
msg: &'a crate::object_header::HeaderMessage,
|
||||||
|
offset_size: u8,
|
||||||
|
length_size: u8,
|
||||||
) -> Result<Cow<'a, [u8]>, FormatError> {
|
) -> Result<Cow<'a, [u8]>, FormatError> {
|
||||||
if !is_shared(msg.flags) {
|
if !is_shared(msg.flags) {
|
||||||
return Ok(Cow::Borrowed(&msg.data));
|
return Ok(Cow::Borrowed(&msg.data));
|
||||||
}
|
}
|
||||||
let shared_ref = parse_shared_ref_sized(&msg.data, offset_size, length_size)?;
|
let shared_ref = parse_shared_ref_sized(&msg.data, offset_size, length_size)?;
|
||||||
resolve_shared_message(
|
resolve_shared_message_in(
|
||||||
file_data,
|
file_data,
|
||||||
&shared_ref,
|
&shared_ref,
|
||||||
msg.msg_type,
|
msg.msg_type,
|
||||||
@@ -553,13 +635,30 @@ pub fn resolve_shared_message(
|
|||||||
target_msg_type: MessageType,
|
target_msg_type: MessageType,
|
||||||
offset_size: u8,
|
offset_size: u8,
|
||||||
length_size: u8,
|
length_size: u8,
|
||||||
|
) -> Result<Vec<u8>, FormatError> {
|
||||||
|
resolve_shared_message_in(
|
||||||
|
&file_data,
|
||||||
|
shared_ref,
|
||||||
|
target_msg_type,
|
||||||
|
offset_size,
|
||||||
|
length_size,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`resolve_shared_message`] over any [`Storage`].
|
||||||
|
pub fn resolve_shared_message_in<S: Storage + ?Sized>(
|
||||||
|
file_data: &S,
|
||||||
|
shared_ref: &SharedMessageRef,
|
||||||
|
target_msg_type: MessageType,
|
||||||
|
offset_size: u8,
|
||||||
|
length_size: u8,
|
||||||
) -> Result<Vec<u8>, FormatError> {
|
) -> Result<Vec<u8>, FormatError> {
|
||||||
let table = if shared_ref.heap_id.is_some() {
|
let table = if shared_ref.heap_id.is_some() {
|
||||||
load_sohm_table(file_data, offset_size, length_size)?
|
load_sohm_table_in(file_data, offset_size, length_size)?
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
resolve_shared_message_with_sohm(
|
resolve_shared_message_with_sohm_in(
|
||||||
file_data,
|
file_data,
|
||||||
shared_ref,
|
shared_ref,
|
||||||
target_msg_type,
|
target_msg_type,
|
||||||
@@ -577,6 +676,25 @@ pub fn resolve_shared_message_with_sohm(
|
|||||||
offset_size: u8,
|
offset_size: u8,
|
||||||
length_size: u8,
|
length_size: u8,
|
||||||
sohm_table: Option<&SohmTable>,
|
sohm_table: Option<&SohmTable>,
|
||||||
|
) -> Result<Vec<u8>, FormatError> {
|
||||||
|
resolve_shared_message_with_sohm_in(
|
||||||
|
&file_data,
|
||||||
|
shared_ref,
|
||||||
|
target_msg_type,
|
||||||
|
offset_size,
|
||||||
|
length_size,
|
||||||
|
sohm_table,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`resolve_shared_message_with_sohm`] over any [`Storage`].
|
||||||
|
pub fn resolve_shared_message_with_sohm_in<S: Storage + ?Sized>(
|
||||||
|
file_data: &S,
|
||||||
|
shared_ref: &SharedMessageRef,
|
||||||
|
target_msg_type: MessageType,
|
||||||
|
offset_size: u8,
|
||||||
|
length_size: u8,
|
||||||
|
sohm_table: Option<&SohmTable>,
|
||||||
) -> Result<Vec<u8>, FormatError> {
|
) -> Result<Vec<u8>, FormatError> {
|
||||||
// Dispatch on what the reference carries rather than on `ref_type`: v1/v2
|
// Dispatch on what the reference carries rather than on `ref_type`: v1/v2
|
||||||
// references are always an object-header address whatever their type
|
// references are always an object-header address whatever their type
|
||||||
@@ -586,8 +704,7 @@ pub fn resolve_shared_message_with_sohm(
|
|||||||
shared_ref.heap_id.as_ref(),
|
shared_ref.heap_id.as_ref(),
|
||||||
) {
|
) {
|
||||||
(Some(addr), _) => {
|
(Some(addr), _) => {
|
||||||
let target_header =
|
let target_header = ObjectHeader::parse_in(file_data, addr, offset_size, length_size)?;
|
||||||
ObjectHeader::parse(file_data, addr as usize, offset_size, length_size)?;
|
|
||||||
for msg in &target_header.messages {
|
for msg in &target_header.messages {
|
||||||
if msg.msg_type == target_msg_type && !is_shared(msg.flags) {
|
if msg.msg_type == target_msg_type && !is_shared(msg.flags) {
|
||||||
return Ok(msg.data.clone());
|
return Ok(msg.data.clone());
|
||||||
@@ -614,7 +731,7 @@ pub fn resolve_shared_message_with_sohm(
|
|||||||
}
|
}
|
||||||
(None, Some(heap_id)) => {
|
(None, Some(heap_id)) => {
|
||||||
let table = sohm_table.ok_or(FormatError::InvalidSharedMessageVersion(2))?;
|
let table = sohm_table.ok_or(FormatError::InvalidSharedMessageVersion(2))?;
|
||||||
resolve_sohm_message(
|
resolve_sohm_message_in(
|
||||||
file_data,
|
file_data,
|
||||||
heap_id,
|
heap_id,
|
||||||
table,
|
table,
|
||||||
@@ -1052,4 +1169,82 @@ mod tests {
|
|||||||
// With 2-byte offsets: OH=2+2=4, heap=12, entry=1+4+12=17
|
// With 2-byte offsets: OH=2+2=4, heap=12, entry=1+4+12=17
|
||||||
assert_eq!(sohm_entry_size(2), 17);
|
assert_eq!(sohm_entry_size(2), 17);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// SOHM tables and lists parse identically through a read_at-only
|
||||||
|
/// CountingStorage: at two offsets, with 4- and 8-byte offsets, cut at
|
||||||
|
/// every length and with a bad signature.
|
||||||
|
#[test]
|
||||||
|
fn storage_reads_match_slice_reads() {
|
||||||
|
use crate::storage::CountingStorage;
|
||||||
|
let idx = |t: u8, n: u16| SohmIndex {
|
||||||
|
index_type: t,
|
||||||
|
mesg_types: 0x0008,
|
||||||
|
min_mesg_size: 50,
|
||||||
|
list_max: 50,
|
||||||
|
btree_min: 40,
|
||||||
|
num_messages: n,
|
||||||
|
index_addr: 0x3000,
|
||||||
|
heap_addr: 0x4000,
|
||||||
|
};
|
||||||
|
let heap_entry = |h: u32| SohmEntry {
|
||||||
|
location: 0,
|
||||||
|
hash: h,
|
||||||
|
heap_id: Some([1, 2, 3, 4, 5, 6, 7, h as u8]),
|
||||||
|
ref_count: Some(h),
|
||||||
|
mesg_index: None,
|
||||||
|
oh_addr: None,
|
||||||
|
};
|
||||||
|
let oh_entry = SohmEntry {
|
||||||
|
location: 1,
|
||||||
|
hash: 9,
|
||||||
|
heap_id: None,
|
||||||
|
ref_count: None,
|
||||||
|
mesg_index: Some(3),
|
||||||
|
oh_addr: Some(0x7000),
|
||||||
|
};
|
||||||
|
let mut compared = 0;
|
||||||
|
for os in [4u8, 8] {
|
||||||
|
let smtb = build_smtb(&[idx(0, 2), idx(1, 7)], os);
|
||||||
|
let smli = build_smli(&[heap_entry(1), oh_entry.clone(), heap_entry(2)], os);
|
||||||
|
for (body, n) in [(smtb, 2u16), (smli, 3)] {
|
||||||
|
let is_table = &body[..4] == b"SMTB";
|
||||||
|
for at in [0usize, 0x40] {
|
||||||
|
let mut full = vec![0u8; at];
|
||||||
|
full.extend_from_slice(&body);
|
||||||
|
let mut files = Vec::new();
|
||||||
|
for cut in at..=full.len() {
|
||||||
|
files.push(full[..cut].to_vec());
|
||||||
|
}
|
||||||
|
let mut bad = full.clone();
|
||||||
|
bad[at] = b'X';
|
||||||
|
files.push(bad);
|
||||||
|
for f in files {
|
||||||
|
let st = CountingStorage::new(f.clone());
|
||||||
|
let (want, got) = if is_table {
|
||||||
|
(
|
||||||
|
format!("{:?}", parse_sohm_table(&f, at, n as u8, os)),
|
||||||
|
format!("{:?}", parse_sohm_table_in(&st, at as u64, n as u8, os)),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
(
|
||||||
|
format!("{:?}", parse_sohm_list(&f, at, n, os)),
|
||||||
|
format!("{:?}", parse_sohm_list_in(&st, at as u64, n, os)),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
assert_eq!(got, want, "{} bytes", f.len());
|
||||||
|
assert!(st.reads() <= 2);
|
||||||
|
compared += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert!(compared > 200);
|
||||||
|
// The B-tree index reads through Storage too, errors included.
|
||||||
|
let junk = vec![0u8; 64];
|
||||||
|
let st = CountingStorage::new(junk.clone());
|
||||||
|
assert_eq!(
|
||||||
|
format!("{:?}", parse_sohm_btree_entries_in(&st, 0, 8, 8)),
|
||||||
|
format!("{:?}", parse_sohm_btree_entries(&junk, 0, 8, 8))
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
//! HDF5 file signature (magic bytes) detection.
|
//! HDF5 file signature (magic bytes) detection.
|
||||||
|
|
||||||
use crate::error::FormatError;
|
use crate::error::FormatError;
|
||||||
|
use crate::storage::{Storage, read_exact_at};
|
||||||
|
|
||||||
/// The 8-byte HDF5 magic signature.
|
/// The 8-byte HDF5 magic signature.
|
||||||
pub const HDF5_SIGNATURE: [u8; 8] = [0x89, b'H', b'D', b'F', b'\r', b'\n', 0x1A, b'\n'];
|
pub const HDF5_SIGNATURE: [u8; 8] = [0x89, b'H', b'D', b'F', b'\r', b'\n', 0x1A, b'\n'];
|
||||||
@@ -39,6 +40,20 @@ pub fn find_signature(data: &[u8]) -> Result<usize, FormatError> {
|
|||||||
Err(FormatError::SignatureNotFound)
|
Err(FormatError::SignatureNotFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// [`find_signature`] over any [`Storage`]: one 8-byte read per candidate
|
||||||
|
/// offset.
|
||||||
|
pub fn find_signature_in<S: Storage + ?Sized>(file: &S) -> Result<u64, FormatError> {
|
||||||
|
let len = file.len();
|
||||||
|
let mut offset = 0u64;
|
||||||
|
while offset.checked_add(8).is_some_and(|end| end <= len) {
|
||||||
|
if *read_exact_at(file, offset, 8)? == HDF5_SIGNATURE {
|
||||||
|
return Ok(offset);
|
||||||
|
}
|
||||||
|
offset = if offset == 0 { 512 } else { offset * 2 };
|
||||||
|
}
|
||||||
|
Err(FormatError::SignatureNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
/// Split a file into its user block and its HDF5 bytes.
|
/// Split a file into its user block and its HDF5 bytes.
|
||||||
///
|
///
|
||||||
/// Returns `(user_block, hdf5)`: `user_block` is everything before the
|
/// Returns `(user_block, hdf5)`: `user_block` is everything before the
|
||||||
@@ -132,4 +147,26 @@ mod tests {
|
|||||||
data[512..520].copy_from_slice(&HDF5_SIGNATURE);
|
data[512..520].copy_from_slice(&HDF5_SIGNATURE);
|
||||||
assert_eq!(find_signature(&data), Ok(0));
|
assert_eq!(find_signature(&data), Ok(0));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn find_signature_in_matches_slice_search() {
|
||||||
|
use crate::storage::CountingStorage;
|
||||||
|
for (len, at) in [
|
||||||
|
(0, None),
|
||||||
|
(7, None),
|
||||||
|
(8, Some(0)),
|
||||||
|
(600, Some(512)),
|
||||||
|
(5000, Some(4096)),
|
||||||
|
(3000, Some(2048)),
|
||||||
|
(3000, None),
|
||||||
|
] {
|
||||||
|
let mut data = vec![0u8; len];
|
||||||
|
if let Some(at) = at {
|
||||||
|
data[at..at + 8].copy_from_slice(&HDF5_SIGNATURE);
|
||||||
|
}
|
||||||
|
let want = find_signature(&data).map(|o| o as u64);
|
||||||
|
let got = find_signature_in(&CountingStorage::new(data));
|
||||||
|
assert_eq!(got, want, "{len} {at:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,723 @@
|
|||||||
|
//! Where the parsers read the file from: the [`Storage`] trait.
|
||||||
|
//!
|
||||||
|
//! Every parser used to take the whole file as one `&[u8]`. [`Storage`] is
|
||||||
|
//! the abstraction that replaces it (see `docs/design/range-reads.md`,
|
||||||
|
//! option (a)): a parser asks for the bytes it needs, `[offset, offset +
|
||||||
|
//! len)`, with 64-bit offsets, and gets them back as a [`Cow`] — borrowed
|
||||||
|
//! when the backend holds the file in memory (a `Vec`, an mmap), owned when
|
||||||
|
//! it had to fetch them (a range request, a block cache).
|
||||||
|
//!
|
||||||
|
//! `impl Storage for [u8]` serves the in-memory case with no copy, and
|
||||||
|
//! [`Storage::as_contiguous`] lets a hot loop borrow the whole file at once
|
||||||
|
//! when the backend has it. Modules are converted one at a time: a converted
|
||||||
|
//! parser has an `*_in<S: Storage + ?Sized>(file: &S, ..)` core and keeps
|
||||||
|
//! its old `&[u8]` signature as a thin wrapper, so callers do not change.
|
||||||
|
//!
|
||||||
|
//! The cores are generic rather than taking `&dyn Storage` so that the
|
||||||
|
//! wrappers monomorphise for `[u8]`: the bounds check of each structure read
|
||||||
|
//! inlines to what the slice code did, with no indirect call and no copy,
|
||||||
|
//! which keeps local files as fast as before the migration. A `&dyn Storage`
|
||||||
|
//! still works (`S = dyn Storage`), and a remote backend pays one indirect
|
||||||
|
//! call per structure read.
|
||||||
|
//!
|
||||||
|
//! # Slice entry points
|
||||||
|
//!
|
||||||
|
//! A generic core is instantiated in the crate that calls it, so a
|
||||||
|
//! downstream crate calling `parse_in::<[u8]>` gets its own copy of the
|
||||||
|
//! parser, compiled without this crate's private helpers inlined (there is
|
||||||
|
//! no cross-crate inlining of non-`#[inline]` functions without LTO): a
|
||||||
|
//! metadata walk through the facade ran about 6% slower that way than
|
||||||
|
//! through the `&[u8]` wrappers. The `*_in` entry points on the facade's hot
|
||||||
|
//! paths (object headers, group listing and lookup, attributes) therefore
|
||||||
|
//! check [`Storage::as_contiguous`] first and hand an in-memory file to
|
||||||
|
//! their non-generic `&[u8]` wrapper, compiled here; both run the one core.
|
||||||
|
//!
|
||||||
|
//! The trait is synchronous and `no_std`: parsing is CPU work, and a remote
|
||||||
|
//! backend bridges to its own I/O.
|
||||||
|
|
||||||
|
#[cfg(not(feature = "std"))]
|
||||||
|
use alloc::{borrow::Cow, boxed::Box, vec::Vec};
|
||||||
|
#[cfg(feature = "std")]
|
||||||
|
use std::{borrow::Cow, boxed::Box, vec::Vec};
|
||||||
|
|
||||||
|
use core::ops::Range;
|
||||||
|
|
||||||
|
use crate::error::FormatError;
|
||||||
|
|
||||||
|
/// A random-access source of file bytes.
|
||||||
|
///
|
||||||
|
/// Offsets are relative to the start of the HDF5 data (the superblock), like
|
||||||
|
/// every address in the file.
|
||||||
|
pub trait Storage {
|
||||||
|
/// Bytes `[offset, offset + len)`.
|
||||||
|
///
|
||||||
|
/// The result is shorter than `len` only when the range runs past the
|
||||||
|
/// end of the storage (and empty when `offset` is at or past the end);
|
||||||
|
/// a backend that cannot serve a range returns an error instead of a
|
||||||
|
/// short read.
|
||||||
|
/// It is never longer than `len`; the parsers cut a longer result to
|
||||||
|
/// `len` (see [`exact_len`]) rather than read bytes from outside the
|
||||||
|
/// range.
|
||||||
|
fn read_at(&self, offset: u64, len: usize) -> Result<Cow<'_, [u8]>, FormatError>;
|
||||||
|
|
||||||
|
/// Current length of the storage in bytes.
|
||||||
|
fn len(&self) -> u64;
|
||||||
|
|
||||||
|
/// Whether the storage holds no bytes.
|
||||||
|
fn is_empty(&self) -> bool {
|
||||||
|
self.len() == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Several reads at once, in the order given. Backends that talk to a
|
||||||
|
/// remote store coalesce and parallelise these; the default reads them
|
||||||
|
/// one by one with [`Storage::read_at`].
|
||||||
|
fn read_ranges(&self, ranges: &[Range<u64>]) -> Result<Vec<Cow<'_, [u8]>>, FormatError> {
|
||||||
|
ranges
|
||||||
|
.iter()
|
||||||
|
.map(|r| {
|
||||||
|
let len = usize::try_from(r.end.saturating_sub(r.start)).map_err(|_| {
|
||||||
|
FormatError::Overflow("read range longer than the address space".into())
|
||||||
|
})?;
|
||||||
|
self.read_at(r.start, len)
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The whole storage as one slice, when the backend has it in memory
|
||||||
|
/// (a `Vec`, an mmap). Hot loops use this to keep their zero-copy path;
|
||||||
|
/// `None` means every byte has to go through [`Storage::read_at`].
|
||||||
|
fn as_contiguous(&self) -> Option<&[u8]> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Storage for [u8] {
|
||||||
|
#[inline]
|
||||||
|
fn read_at(&self, offset: u64, len: usize) -> Result<Cow<'_, [u8]>, FormatError> {
|
||||||
|
let n = self.len();
|
||||||
|
let start = usize::try_from(offset).map_or(n, |o| o.min(n));
|
||||||
|
let end = start.saturating_add(len).min(n);
|
||||||
|
Ok(Cow::Borrowed(&self[start..end]))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn len(&self) -> u64 {
|
||||||
|
<[u8]>::len(self) as u64
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn as_contiguous(&self) -> Option<&[u8]> {
|
||||||
|
Some(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Storage for Vec<u8> {
|
||||||
|
#[inline]
|
||||||
|
fn read_at(&self, offset: u64, len: usize) -> Result<Cow<'_, [u8]>, FormatError> {
|
||||||
|
self.as_slice().read_at(offset, len)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn len(&self) -> u64 {
|
||||||
|
Vec::len(self) as u64
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn as_contiguous(&self) -> Option<&[u8]> {
|
||||||
|
Some(self.as_slice())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Storage + ?Sized> Storage for &T {
|
||||||
|
#[inline]
|
||||||
|
fn read_at(&self, offset: u64, len: usize) -> Result<Cow<'_, [u8]>, FormatError> {
|
||||||
|
(**self).read_at(offset, len)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn len(&self) -> u64 {
|
||||||
|
(**self).len()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn read_ranges(&self, ranges: &[Range<u64>]) -> Result<Vec<Cow<'_, [u8]>>, FormatError> {
|
||||||
|
(**self).read_ranges(ranges)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn as_contiguous(&self) -> Option<&[u8]> {
|
||||||
|
(**self).as_contiguous()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Storage + ?Sized> Storage for Box<T> {
|
||||||
|
#[inline]
|
||||||
|
fn read_at(&self, offset: u64, len: usize) -> Result<Cow<'_, [u8]>, FormatError> {
|
||||||
|
(**self).read_at(offset, len)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn len(&self) -> u64 {
|
||||||
|
(**self).len()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn read_ranges(&self, ranges: &[Range<u64>]) -> Result<Vec<Cow<'_, [u8]>>, FormatError> {
|
||||||
|
(**self).read_ranges(ranges)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn as_contiguous(&self) -> Option<&[u8]> {
|
||||||
|
(**self).as_contiguous()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "std")]
|
||||||
|
impl<T: Storage + ?Sized> Storage for std::sync::Arc<T> {
|
||||||
|
#[inline]
|
||||||
|
fn read_at(&self, offset: u64, len: usize) -> Result<Cow<'_, [u8]>, FormatError> {
|
||||||
|
(**self).read_at(offset, len)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn len(&self) -> u64 {
|
||||||
|
(**self).len()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn read_ranges(&self, ranges: &[Range<u64>]) -> Result<Vec<Cow<'_, [u8]>>, FormatError> {
|
||||||
|
(**self).read_ranges(ranges)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn as_contiguous(&self) -> Option<&[u8]> {
|
||||||
|
(**self).as_contiguous()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `storage.len()` as the `usize` the parsers' end-of-file errors report
|
||||||
|
/// (saturating on targets where the file is larger than the address space).
|
||||||
|
#[inline]
|
||||||
|
pub(crate) fn len_usize<S: Storage + ?Sized>(file: &S) -> usize {
|
||||||
|
usize::try_from(file.len()).unwrap_or(usize::MAX)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Bytes `[offset, offset + len)`, all of them.
|
||||||
|
///
|
||||||
|
/// A range that runs past the end of the storage is
|
||||||
|
/// [`FormatError::UnexpectedEof`] with `expected = offset + len` and
|
||||||
|
/// `available = storage length` — the error the `&[u8]` parsers give for
|
||||||
|
/// the same bounds check (`offset + len > file_data.len()`).
|
||||||
|
#[inline]
|
||||||
|
pub fn read_exact_at<S: Storage + ?Sized>(
|
||||||
|
file: &S,
|
||||||
|
offset: u64,
|
||||||
|
len: usize,
|
||||||
|
) -> Result<Cow<'_, [u8]>, FormatError> {
|
||||||
|
let eof = || FormatError::UnexpectedEof {
|
||||||
|
expected: usize::try_from(offset)
|
||||||
|
.unwrap_or(usize::MAX)
|
||||||
|
.saturating_add(len),
|
||||||
|
available: len_usize(file),
|
||||||
|
};
|
||||||
|
// In-memory fast path: plain slicing (for `S = [u8]` this inlines to
|
||||||
|
// the slice code's bounds check).
|
||||||
|
if let Some(all) = file.as_contiguous() {
|
||||||
|
return usize::try_from(offset)
|
||||||
|
.ok()
|
||||||
|
.and_then(|start| all.get(start..start.checked_add(len)?))
|
||||||
|
.map(Cow::Borrowed)
|
||||||
|
.ok_or_else(eof);
|
||||||
|
}
|
||||||
|
match offset.checked_add(len as u64) {
|
||||||
|
Some(end) if end <= file.len() => {}
|
||||||
|
_ => return Err(eof()),
|
||||||
|
}
|
||||||
|
// A short read (the storage shrank, or the backend served less inside
|
||||||
|
// the file) is an error: never parse a partial structure.
|
||||||
|
exact_len(file.read_at(offset, len)?, len)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `bytes`, the result of asking a [`Storage`] for `len` bytes, as exactly
|
||||||
|
/// `len` bytes: a longer result (a backend that broke
|
||||||
|
/// [`Storage::read_at`]'s contract) is cut to `len`, so bytes from outside
|
||||||
|
/// the range asked for are never parsed or returned; a shorter one is an
|
||||||
|
/// error (the storage shrank, or the backend failed), never a partial
|
||||||
|
/// structure.
|
||||||
|
#[inline]
|
||||||
|
pub fn exact_len(bytes: Cow<'_, [u8]>, len: usize) -> Result<Cow<'_, [u8]>, FormatError> {
|
||||||
|
match bytes.len().cmp(&len) {
|
||||||
|
core::cmp::Ordering::Equal => Ok(bytes),
|
||||||
|
core::cmp::Ordering::Less => Err(short_read()),
|
||||||
|
core::cmp::Ordering::Greater => Ok(match bytes {
|
||||||
|
Cow::Borrowed(b) => Cow::Borrowed(&b[..len]),
|
||||||
|
Cow::Owned(mut v) => {
|
||||||
|
v.truncate(len);
|
||||||
|
Cow::Owned(v)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cold]
|
||||||
|
#[inline(never)]
|
||||||
|
fn short_read() -> FormatError {
|
||||||
|
FormatError::Storage(
|
||||||
|
"short read inside the file (the storage shrank or the backend failed)".into(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Largest paged data block (fixed or extensible array) read in one piece.
|
||||||
|
/// A bigger one is read as its prefix and then page by page, only the pages
|
||||||
|
/// in use, so a block whose size fields claim more than the file holds
|
||||||
|
/// costs no more than the pages it really has.
|
||||||
|
pub(crate) const PAGED_BLOCK_ONE_READ_MAX: usize = 1 << 20;
|
||||||
|
|
||||||
|
/// A window of the file: up to `max` bytes read at `base`, fewer only at
|
||||||
|
/// the end of the file. Its [`Window::ensure`] reports a bounds failure
|
||||||
|
/// exactly as the whole-file check `ensure_len(file_data, base + rel, n)`
|
||||||
|
/// did — with the absolute position and the file's length — as long as
|
||||||
|
/// every position checked lies within the `max` bytes the window was asked
|
||||||
|
/// for: then a position past the window is past the end of the file.
|
||||||
|
pub(crate) struct Window<'a> {
|
||||||
|
/// The bytes, from `base` on.
|
||||||
|
pub bytes: Cow<'a, [u8]>,
|
||||||
|
base: usize,
|
||||||
|
file_len: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Window<'a> {
|
||||||
|
/// Read up to `max` bytes at `base`.
|
||||||
|
pub fn read<S: Storage + ?Sized>(
|
||||||
|
file: &'a S,
|
||||||
|
base: u64,
|
||||||
|
max: usize,
|
||||||
|
) -> Result<Self, FormatError> {
|
||||||
|
Ok(Window {
|
||||||
|
bytes: read_upto(file, base, max)?,
|
||||||
|
base: usize::try_from(base).unwrap_or(usize::MAX),
|
||||||
|
file_len: len_usize(file),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A whole in-memory file as one window (base 0).
|
||||||
|
#[cfg(test)]
|
||||||
|
pub fn whole(bytes: &'a [u8]) -> Self {
|
||||||
|
Window {
|
||||||
|
bytes: Cow::Borrowed(bytes),
|
||||||
|
base: 0,
|
||||||
|
file_len: bytes.len(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`Window::ensure`] for a window at `base` that has not been read:
|
||||||
|
/// whether `[rel, rel + needed)` lies in the file, with the same error.
|
||||||
|
/// Lets a parser whose first step is to check a structure's whole extent
|
||||||
|
/// (a checksum at its end) fail before reading a structure that a
|
||||||
|
/// hostile size field has stretched past the end of the file.
|
||||||
|
pub fn check_extent<S: Storage + ?Sized>(
|
||||||
|
file: &S,
|
||||||
|
base: u64,
|
||||||
|
rel: usize,
|
||||||
|
needed: usize,
|
||||||
|
) -> Result<(), FormatError> {
|
||||||
|
let base = usize::try_from(base).unwrap_or(usize::MAX);
|
||||||
|
let file_len = len_usize(file);
|
||||||
|
match base.checked_add(rel).and_then(|p| p.checked_add(needed)) {
|
||||||
|
Some(end) if end <= file_len => Ok(()),
|
||||||
|
_ => Err(FormatError::UnexpectedEof {
|
||||||
|
expected: base.saturating_add(rel).saturating_add(needed),
|
||||||
|
available: file_len,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check that `[rel, rel + needed)` (relative to `base`) is in the file.
|
||||||
|
#[inline]
|
||||||
|
pub fn ensure(&self, rel: usize, needed: usize) -> Result<(), FormatError> {
|
||||||
|
match rel.checked_add(needed) {
|
||||||
|
Some(end) if end <= self.bytes.len() => Ok(()),
|
||||||
|
_ => Err(FormatError::UnexpectedEof {
|
||||||
|
expected: self.base.saturating_add(rel).saturating_add(needed),
|
||||||
|
available: self.file_len,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Up to `max` bytes from `offset` on: fewer only at the end of the
|
||||||
|
/// storage. For structures whose size is only known once their prefix has
|
||||||
|
/// been parsed and whose parsers bound-check what they are given.
|
||||||
|
#[inline]
|
||||||
|
pub fn read_upto<S: Storage + ?Sized>(
|
||||||
|
file: &S,
|
||||||
|
offset: u64,
|
||||||
|
max: usize,
|
||||||
|
) -> Result<Cow<'_, [u8]>, FormatError> {
|
||||||
|
if let Some(all) = file.as_contiguous() {
|
||||||
|
let start = usize::try_from(offset).map_or(all.len(), |o| o.min(all.len()));
|
||||||
|
let end = start.saturating_add(max).min(all.len());
|
||||||
|
return Ok(Cow::Borrowed(&all[start..end]));
|
||||||
|
}
|
||||||
|
let avail = file.len().saturating_sub(offset);
|
||||||
|
let len = usize::try_from(avail).map_or(max, |a| a.min(max));
|
||||||
|
exact_len(file.read_at(offset, len)?, len)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Most stored bytes fetched by one [`Storage::read_ranges`] call when a
|
||||||
|
/// read gathers many extents (a chunked dataset's chunks, a selection's
|
||||||
|
/// runs): a larger read is fetched and decoded batch by batch, so a backend
|
||||||
|
/// without the file in memory never holds more than this much undecoded
|
||||||
|
/// data per read (or one extent, when a single one is larger — and every
|
||||||
|
/// chunk's extent is bounded by what the chunk can need, see
|
||||||
|
/// [`crate::filters::stored_chunk_limit`]).
|
||||||
|
pub const RAW_BATCH_BYTES: usize = 64 << 20;
|
||||||
|
|
||||||
|
/// One extent of a raw-data read: `len` bytes stored at `addr`, whose
|
||||||
|
/// bounds are checked against the file, of which the first `fetch` bytes
|
||||||
|
/// are read (`None`: only checked, not read — its bytes are not needed).
|
||||||
|
///
|
||||||
|
/// `fetch` below `len` bounds what a crafted size field can make a read
|
||||||
|
/// fetch: a chunk never needs more of its stored bytes than its decoded
|
||||||
|
/// size allows, however large its index entry says it is.
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub(crate) struct ExtentReq {
|
||||||
|
pub addr: u64,
|
||||||
|
pub len: usize,
|
||||||
|
pub fetch: Option<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ExtentReq {
|
||||||
|
/// How many bytes are read for this extent.
|
||||||
|
#[inline]
|
||||||
|
fn fetch_len(&self) -> usize {
|
||||||
|
self.fetch.map_or(0, |f| f.min(self.len))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One extent's bytes on their own (see [`ExtentReq`]): the whole extent's
|
||||||
|
/// bounds checked as [`read_exact_at`] checks them, and its first
|
||||||
|
/// `req.fetch` bytes read (none when `fetch` is `None`).
|
||||||
|
pub(crate) fn read_extent<'a, S: Storage + ?Sized>(
|
||||||
|
file: &'a S,
|
||||||
|
req: &ExtentReq,
|
||||||
|
) -> Result<Cow<'a, [u8]>, FormatError> {
|
||||||
|
let start = usize::try_from(req.addr).unwrap_or(usize::MAX);
|
||||||
|
match start.checked_add(req.len) {
|
||||||
|
Some(end) if end <= len_usize(file) => read_exact_at(file, req.addr, req.fetch_len()),
|
||||||
|
_ => Err(FormatError::UnexpectedEof {
|
||||||
|
expected: start.saturating_add(req.len),
|
||||||
|
available: len_usize(file),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The stored bytes of one batch of extents (chunks, contiguous runs),
|
||||||
|
/// fetched together: [`Storage::read_ranges`] is called once for the batch,
|
||||||
|
/// so a remote backend can coalesce and parallelise the requests. See
|
||||||
|
/// [`for_each_extent_batch`], which is how every raw-data read gets them.
|
||||||
|
///
|
||||||
|
/// With the whole file in memory nothing is fetched: [`Self::get`] slices
|
||||||
|
/// it, as the slice readers did. Either way an extent that does not lie in
|
||||||
|
/// the file is the error the slice readers gave for it
|
||||||
|
/// ([`FormatError::UnexpectedEof`] with its end and the file length, or
|
||||||
|
/// [`FormatError::Overflow`] for an address past this platform's `usize`),
|
||||||
|
/// reported when that extent is asked for — so a read reports the first
|
||||||
|
/// failing extent in its own order, whatever fails after it.
|
||||||
|
pub(crate) enum ExtentBytes<'a> {
|
||||||
|
/// The whole file.
|
||||||
|
Contiguous(&'a [u8]),
|
||||||
|
/// Each extent's bytes, or its bounds error; the first is extent
|
||||||
|
/// `base` of the read.
|
||||||
|
Fetched {
|
||||||
|
base: usize,
|
||||||
|
extents: Vec<Extent<'a>>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One extent of [`ExtentBytes::Fetched`].
|
||||||
|
pub(crate) enum Extent<'a> {
|
||||||
|
/// Its bytes.
|
||||||
|
Bytes(Cow<'a, [u8]>),
|
||||||
|
/// In the file, but not fetched (the caller did not want its bytes).
|
||||||
|
NotFetched,
|
||||||
|
/// The error reading it gives.
|
||||||
|
Err(FormatError),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> ExtentBytes<'a> {
|
||||||
|
/// Fetch `reqs`, extents `base..base + reqs.len()` of the read: the
|
||||||
|
/// bytes of those wanted, and the bounds check of all of them.
|
||||||
|
pub(crate) fn fetch<S: Storage + ?Sized>(
|
||||||
|
file: &'a S,
|
||||||
|
reqs: &[ExtentReq],
|
||||||
|
base: usize,
|
||||||
|
) -> Result<Self, FormatError> {
|
||||||
|
if let Some(all) = file.as_contiguous() {
|
||||||
|
return Ok(ExtentBytes::Contiguous(all));
|
||||||
|
}
|
||||||
|
let file_len = len_usize(file);
|
||||||
|
let mut ranges = Vec::new();
|
||||||
|
let mut out = Vec::with_capacity(reqs.len());
|
||||||
|
// Positions in `out` of the extents being read, in `ranges` order.
|
||||||
|
let mut slots = Vec::new();
|
||||||
|
for req in reqs {
|
||||||
|
let checked = crate::addr::to_usize(req.addr).and_then(|start| {
|
||||||
|
match start.checked_add(req.len) {
|
||||||
|
Some(end) if end <= file_len => Ok(()),
|
||||||
|
_ => Err(FormatError::UnexpectedEof {
|
||||||
|
expected: start.saturating_add(req.len),
|
||||||
|
available: file_len,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
match checked {
|
||||||
|
Ok(()) if req.fetch.is_some() => {
|
||||||
|
slots.push(out.len());
|
||||||
|
ranges.push(req.addr..req.addr + req.fetch_len() as u64);
|
||||||
|
out.push(Extent::NotFetched);
|
||||||
|
}
|
||||||
|
Ok(()) => out.push(Extent::NotFetched),
|
||||||
|
Err(e) => out.push(Extent::Err(e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !ranges.is_empty() {
|
||||||
|
let got = file.read_ranges(&ranges)?;
|
||||||
|
if got.len() != ranges.len() {
|
||||||
|
return Err(FormatError::Storage(
|
||||||
|
"read_ranges returned the wrong number of ranges".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
for ((slot, bytes), r) in slots.into_iter().zip(got).zip(&ranges) {
|
||||||
|
let len = crate::addr::saturating_usize(r.end - r.start);
|
||||||
|
out[slot] = Extent::Bytes(exact_len(bytes, len)?);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(ExtentBytes::Fetched { base, extents: out })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether extent `i` of the read (`req`) lies in the file: its bounds
|
||||||
|
/// error if not.
|
||||||
|
pub(crate) fn check(&self, i: usize, req: &ExtentReq) -> Result<(), FormatError> {
|
||||||
|
match self {
|
||||||
|
ExtentBytes::Contiguous(_) => self.get(i, req).map(|_| ()),
|
||||||
|
ExtentBytes::Fetched { base, extents } => {
|
||||||
|
match i.checked_sub(*base).and_then(|j| extents.get(j)) {
|
||||||
|
Some(Extent::Err(e)) => Err(e.clone()),
|
||||||
|
Some(_) => Ok(()),
|
||||||
|
None => Err(not_fetched()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extent `i` of the read (`req`): its first `req.fetch` bytes, the
|
||||||
|
/// same whether the file is in memory or not.
|
||||||
|
pub(crate) fn get(&self, i: usize, req: &ExtentReq) -> Result<&[u8], FormatError> {
|
||||||
|
match self {
|
||||||
|
ExtentBytes::Contiguous(all) => {
|
||||||
|
let start = crate::addr::to_usize(req.addr)?;
|
||||||
|
start
|
||||||
|
.checked_add(req.len)
|
||||||
|
.and_then(|end| all.get(start..end))
|
||||||
|
.map(|b| &b[..req.fetch_len()])
|
||||||
|
.ok_or(FormatError::UnexpectedEof {
|
||||||
|
expected: start.saturating_add(req.len),
|
||||||
|
available: <[u8]>::len(all),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
ExtentBytes::Fetched { base, extents } => {
|
||||||
|
match i.checked_sub(*base).and_then(|j| extents.get(j)) {
|
||||||
|
Some(Extent::Bytes(b)) => Ok(b),
|
||||||
|
Some(Extent::Err(e)) => Err(e.clone()),
|
||||||
|
_ => Err(not_fetched()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cold]
|
||||||
|
fn not_fetched() -> FormatError {
|
||||||
|
FormatError::Storage("an extent that was not fetched was asked for".into())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The one way raw-data reads fetch stored bytes: `reqs` are split into
|
||||||
|
/// consecutive batches of at most [`RAW_BATCH_BYTES`] of fetched bytes (at
|
||||||
|
/// least one extent each — and no extent fetches more than its
|
||||||
|
/// [`ExtentReq::fetch`]), and for each batch in turn its bytes are fetched
|
||||||
|
/// with one [`Storage::read_ranges`] call and `f(batch, &bytes)` is called,
|
||||||
|
/// with `bytes` indexed by the extent's position in `reqs`. A batch's bytes
|
||||||
|
/// are dropped before the next batch is fetched, and an error from `f`
|
||||||
|
/// stops the read before anything more is fetched.
|
||||||
|
///
|
||||||
|
/// With the whole file in memory there is nothing to fetch: one call, over
|
||||||
|
/// all of `reqs`, that slices the file.
|
||||||
|
pub(crate) fn for_each_extent_batch<'a, S: Storage + ?Sized>(
|
||||||
|
file: &'a S,
|
||||||
|
reqs: &[ExtentReq],
|
||||||
|
mut f: impl FnMut(Range<usize>, &ExtentBytes<'a>) -> Result<(), FormatError>,
|
||||||
|
) -> Result<(), FormatError> {
|
||||||
|
let contiguous = file.as_contiguous().is_some();
|
||||||
|
for batch in raw_batches(reqs.len(), contiguous, |i| reqs[i].fetch_len()) {
|
||||||
|
let bytes = ExtentBytes::fetch(file, &reqs[batch.clone()], batch.start)?;
|
||||||
|
f(batch, &bytes)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Split `n` extents, whose sizes `size(i)` gives, into consecutive batches
|
||||||
|
/// of at most [`RAW_BATCH_BYTES`] (at least one extent each): the ranges of
|
||||||
|
/// `0..n` to fetch together. With the whole file in memory (`contiguous`)
|
||||||
|
/// there is nothing to fetch, and one batch.
|
||||||
|
pub(crate) fn raw_batches(
|
||||||
|
n: usize,
|
||||||
|
contiguous: bool,
|
||||||
|
size: impl Fn(usize) -> usize,
|
||||||
|
) -> Vec<Range<usize>> {
|
||||||
|
if contiguous || n == 0 {
|
||||||
|
return core::iter::once(0..n).collect();
|
||||||
|
}
|
||||||
|
let mut out = Vec::new();
|
||||||
|
let (mut start, mut bytes) = (0, 0usize);
|
||||||
|
for i in 0..n {
|
||||||
|
let s = size(i);
|
||||||
|
if i > start && bytes.saturating_add(s) > RAW_BATCH_BYTES {
|
||||||
|
out.push(start..i);
|
||||||
|
start = i;
|
||||||
|
bytes = 0;
|
||||||
|
}
|
||||||
|
bytes = bytes.saturating_add(s);
|
||||||
|
}
|
||||||
|
out.push(start..n);
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Borrow the whole file for a code path that has not been converted to
|
||||||
|
/// [`Storage`] yet. On a backend without a contiguous view this is the
|
||||||
|
/// clean [`FormatError::ContiguousStorageRequired`] error, never a guess.
|
||||||
|
#[inline]
|
||||||
|
pub fn require_contiguous<'a, S: Storage + ?Sized>(
|
||||||
|
file: &'a S,
|
||||||
|
what: &'static str,
|
||||||
|
) -> Result<&'a [u8], FormatError> {
|
||||||
|
file.as_contiguous()
|
||||||
|
.ok_or(FormatError::ContiguousStorageRequired(what))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A [`Storage`] over an in-memory buffer that serves every byte through
|
||||||
|
/// [`Storage::read_at`] (its [`Storage::as_contiguous`] is `None`, so no
|
||||||
|
/// parser can take the whole-slice shortcut), copies what it serves (as a
|
||||||
|
/// remote backend would), and counts the reads and bytes.
|
||||||
|
///
|
||||||
|
/// It is the equivalence harness of the range-read migration: parsing a
|
||||||
|
/// file through it must give exactly what parsing the `&[u8]` gives, and
|
||||||
|
/// the counters are the request counts a cacheless range reader would make.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct CountingStorage {
|
||||||
|
data: Vec<u8>,
|
||||||
|
reads: portable_atomic::AtomicU64,
|
||||||
|
bytes: portable_atomic::AtomicU64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CountingStorage {
|
||||||
|
/// Serve `data` (the file from the superblock on).
|
||||||
|
pub fn new(data: Vec<u8>) -> Self {
|
||||||
|
CountingStorage {
|
||||||
|
data,
|
||||||
|
reads: portable_atomic::AtomicU64::new(0),
|
||||||
|
bytes: portable_atomic::AtomicU64::new(0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Number of `read_at` calls served so far.
|
||||||
|
pub fn reads(&self) -> u64 {
|
||||||
|
self.reads.load(portable_atomic::Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Number of bytes served so far.
|
||||||
|
pub fn bytes_read(&self) -> u64 {
|
||||||
|
self.bytes.load(portable_atomic::Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reset both counters.
|
||||||
|
pub fn reset(&self) {
|
||||||
|
self.reads.store(0, portable_atomic::Ordering::Relaxed);
|
||||||
|
self.bytes.store(0, portable_atomic::Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Storage for CountingStorage {
|
||||||
|
fn read_at(&self, offset: u64, len: usize) -> Result<Cow<'_, [u8]>, FormatError> {
|
||||||
|
let got = self.data.as_slice().read_at(offset, len)?;
|
||||||
|
self.reads.fetch_add(1, portable_atomic::Ordering::Relaxed);
|
||||||
|
self.bytes
|
||||||
|
.fetch_add(got.len() as u64, portable_atomic::Ordering::Relaxed);
|
||||||
|
Ok(Cow::Owned(got.into_owned()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn len(&self) -> u64 {
|
||||||
|
self.data.len() as u64
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn slice_reads_are_borrowed_and_clamped() {
|
||||||
|
let data: Vec<u8> = (0u8..10).collect();
|
||||||
|
let s: &[u8] = &data;
|
||||||
|
let dynamic: &dyn Storage = &s;
|
||||||
|
assert_eq!(dynamic.len(), 10);
|
||||||
|
let r = dynamic.read_at(2, 3).unwrap();
|
||||||
|
assert!(matches!(r, Cow::Borrowed(_)));
|
||||||
|
assert_eq!(&*r, &[2, 3, 4]);
|
||||||
|
assert_eq!(&*dynamic.read_at(8, 5).unwrap(), &[8, 9]);
|
||||||
|
assert!(dynamic.read_at(10, 5).unwrap().is_empty());
|
||||||
|
assert!(dynamic.read_at(u64::MAX, 5).unwrap().is_empty());
|
||||||
|
assert_eq!(dynamic.as_contiguous(), Some(&data[..]));
|
||||||
|
let v: &dyn Storage = &data;
|
||||||
|
assert_eq!(v.as_contiguous(), Some(&data[..]));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn read_exact_matches_slice_bounds_errors() {
|
||||||
|
let data = [0u8; 10];
|
||||||
|
let s: &[u8] = &data;
|
||||||
|
assert_eq!(&*read_exact_at(&s, 4, 6).unwrap(), &[0; 6]);
|
||||||
|
assert_eq!(
|
||||||
|
read_exact_at(&s, 4, 7).unwrap_err(),
|
||||||
|
FormatError::UnexpectedEof {
|
||||||
|
expected: 11,
|
||||||
|
available: 10
|
||||||
|
}
|
||||||
|
);
|
||||||
|
assert!(read_exact_at(&s, u64::MAX, 1).is_err());
|
||||||
|
assert_eq!(read_upto(&s, 7, 100).unwrap().len(), 3);
|
||||||
|
assert_eq!(read_upto(&s, 70, 100).unwrap().len(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn counting_storage_counts_and_hides_the_slice() {
|
||||||
|
let c = CountingStorage::new((0u8..10).collect());
|
||||||
|
assert!(c.as_contiguous().is_none());
|
||||||
|
let r = c.read_at(3, 4).unwrap();
|
||||||
|
assert!(matches!(r, Cow::Owned(_)));
|
||||||
|
assert_eq!(&*r, &[3, 4, 5, 6]);
|
||||||
|
c.read_at(8, 4).unwrap();
|
||||||
|
assert_eq!((c.reads(), c.bytes_read()), (2, 6));
|
||||||
|
c.reset();
|
||||||
|
assert_eq!((c.reads(), c.bytes_read()), (0, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn read_ranges_default_loops() {
|
||||||
|
let data: Vec<u8> = (0u8..10).collect();
|
||||||
|
let s: &[u8] = &data;
|
||||||
|
let got = s.read_ranges(&[1..3, 5..9]).unwrap();
|
||||||
|
assert_eq!(&*got[0], &[1, 2]);
|
||||||
|
assert_eq!(&*got[1], &[5, 6, 7, 8]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,11 @@ use byteorder::{ByteOrder, LittleEndian};
|
|||||||
|
|
||||||
use crate::error::FormatError;
|
use crate::error::FormatError;
|
||||||
use crate::signature::HDF5_SIGNATURE;
|
use crate::signature::HDF5_SIGNATURE;
|
||||||
|
use crate::storage::{Storage, read_upto};
|
||||||
|
|
||||||
|
/// Bytes read to parse a superblock: more than the largest one (version 1
|
||||||
|
/// with 8-byte offsets and lengths, 100 bytes).
|
||||||
|
const SUPERBLOCK_READ_LEN: usize = 128;
|
||||||
|
|
||||||
/// Parsed HDF5 superblock (all versions).
|
/// Parsed HDF5 superblock (all versions).
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
@@ -161,7 +166,16 @@ impl Superblock {
|
|||||||
file_data: &[u8],
|
file_data: &[u8],
|
||||||
signature_offset: usize,
|
signature_offset: usize,
|
||||||
) -> Result<u64, FormatError> {
|
) -> Result<u64, FormatError> {
|
||||||
let refreshed = Superblock::parse(file_data, signature_offset)?;
|
self.refresh_eof_in(file_data, signature_offset as u64)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`Self::refresh_eof`] over any [`Storage`].
|
||||||
|
pub fn refresh_eof_in<S: Storage + ?Sized>(
|
||||||
|
&mut self,
|
||||||
|
file: &S,
|
||||||
|
signature_offset: u64,
|
||||||
|
) -> Result<u64, FormatError> {
|
||||||
|
let refreshed = Superblock::parse_in(file, signature_offset)?;
|
||||||
self.eof_address = refreshed.eof_address;
|
self.eof_address = refreshed.eof_address;
|
||||||
self.consistency_flags = refreshed.consistency_flags;
|
self.consistency_flags = refreshed.consistency_flags;
|
||||||
Ok(self.eof_address)
|
Ok(self.eof_address)
|
||||||
@@ -219,15 +233,23 @@ impl Superblock {
|
|||||||
/// [`FormatError::UserBlockNotStripped`] because the addresses in the
|
/// [`FormatError::UserBlockNotStripped`] because the addresses in the
|
||||||
/// returned superblock would otherwise be applied to the wrong bytes.
|
/// returned superblock would otherwise be applied to the wrong bytes.
|
||||||
pub fn parse(data: &[u8], signature_offset: usize) -> Result<Superblock, FormatError> {
|
pub fn parse(data: &[u8], signature_offset: usize) -> Result<Superblock, FormatError> {
|
||||||
if signature_offset != 0 {
|
Self::parse_in(data, signature_offset as u64)
|
||||||
return Err(FormatError::UserBlockNotStripped(signature_offset as u64));
|
|
||||||
}
|
}
|
||||||
let d = data
|
|
||||||
.get(signature_offset..)
|
/// [`Self::parse`] over any [`Storage`]: one read of the first
|
||||||
.ok_or(FormatError::UnexpectedEof {
|
/// [`SUPERBLOCK_READ_LEN`] bytes (fewer when the file is shorter, which
|
||||||
expected: signature_offset + 1,
|
/// is then refused with the same end-of-file errors as a short slice).
|
||||||
available: data.len(),
|
pub fn parse_in<S: Storage + ?Sized>(
|
||||||
})?;
|
file: &S,
|
||||||
|
signature_offset: u64,
|
||||||
|
) -> Result<Superblock, FormatError> {
|
||||||
|
if signature_offset != 0 {
|
||||||
|
return Err(FormatError::UserBlockNotStripped(signature_offset));
|
||||||
|
}
|
||||||
|
// Every bounds check below needs at most 100 bytes, so on a longer
|
||||||
|
// file none of them can fail and the window's length does not show.
|
||||||
|
let window = read_upto(file, 0, SUPERBLOCK_READ_LEN)?;
|
||||||
|
let d: &[u8] = &window;
|
||||||
ensure_len(d, 9)?; // signature(8) + version(1)
|
ensure_len(d, 9)?; // signature(8) + version(1)
|
||||||
|
|
||||||
// Verify signature
|
// Verify signature
|
||||||
@@ -894,4 +916,34 @@ mod tests {
|
|||||||
assert_eq!(parsed.version, 3);
|
assert_eq!(parsed.version, 3);
|
||||||
assert_eq!(parsed.page_size, None);
|
assert_eq!(parsed.page_size, None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Through a storage that serves only `read_at`, every version parses
|
||||||
|
/// to the same superblock, and every truncation to the same error, as
|
||||||
|
/// from a slice — in one read.
|
||||||
|
#[test]
|
||||||
|
fn parse_in_matches_slice_parse() {
|
||||||
|
use crate::storage::CountingStorage;
|
||||||
|
let mut files = vec![
|
||||||
|
build_v0_bytes(8),
|
||||||
|
build_v0_bytes(4),
|
||||||
|
build_v1_bytes(8),
|
||||||
|
build_v1_bytes(4),
|
||||||
|
build_v2_bytes(8, 2),
|
||||||
|
build_v2_bytes(4, 3),
|
||||||
|
];
|
||||||
|
for f in files.clone() {
|
||||||
|
let mut long = f.clone();
|
||||||
|
long.resize(4096, 0xAB);
|
||||||
|
files.push(long);
|
||||||
|
for cut in [0, 5, 9, 13, 20, 30, f.len() - 1] {
|
||||||
|
files.push(f[..cut.min(f.len())].to_vec());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for f in files {
|
||||||
|
let want = Superblock::parse(&f, 0);
|
||||||
|
let storage = CountingStorage::new(f.clone());
|
||||||
|
assert_eq!(Superblock::parse_in(&storage, 0), want, "{} bytes", f.len());
|
||||||
|
assert_eq!(storage.reads(), 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,922 @@
|
|||||||
|
//! The superblock extension of a version 2 or 3 superblock, and the
|
||||||
|
//! metadata cache image it can point to.
|
||||||
|
//!
|
||||||
|
//! libhdf5 reads the extension when it opens a file (`H5F__super_read`) and
|
||||||
|
//! decodes the messages that configure the file: v1 B-tree "K" values, File
|
||||||
|
//! Space Info, and the Metadata Cache Image. A message that does not decode
|
||||||
|
//! makes the file fail to open, so [`read_superblock_extension`] decodes and
|
||||||
|
//! checks them the way libhdf5 does.
|
||||||
|
//!
|
||||||
|
//! A metadata cache image (written with `H5Pset_mdc_image_config`) is a
|
||||||
|
//! block holding serialized metadata cache entries — object headers, B-tree
|
||||||
|
//! nodes, heaps — each with its file address. libhdf5 loads it into its
|
||||||
|
//! cache before it reads any other metadata (`H5C__load_cache_image`,
|
||||||
|
//! `H5C__reconstruct_cache_contents`), and the entries take the place of
|
||||||
|
//! the file's bytes at their addresses: the file itself may hold stale or
|
||||||
|
//! no metadata there (in `h5clear_mdc_image.h5` the root group's header is
|
||||||
|
//! only in the image). [`CacheImage::apply`] does the same with bytes: it
|
||||||
|
//! writes every entry at its address, so every parser reads what libhdf5
|
||||||
|
//! reads. It writes into whatever the opener gives it — a private
|
||||||
|
//! copy-on-write mapping of the file, or a buffer the opener owns — so the
|
||||||
|
//! file is never copied whole.
|
||||||
|
|
||||||
|
#[cfg(not(feature = "std"))]
|
||||||
|
use alloc::{borrow::Cow, collections::BTreeSet, vec::Vec};
|
||||||
|
#[cfg(feature = "std")]
|
||||||
|
use std::{borrow::Cow, collections::BTreeSet};
|
||||||
|
|
||||||
|
use crate::error::FormatError;
|
||||||
|
use crate::message_type::MessageType;
|
||||||
|
use crate::object_header::ObjectHeader;
|
||||||
|
use crate::storage::{Storage, read_exact_at};
|
||||||
|
use crate::superblock::Superblock;
|
||||||
|
|
||||||
|
/// Message type of the File Space Info message.
|
||||||
|
const MSG_FSINFO: u16 = 0x0017;
|
||||||
|
/// Message type of the Metadata Cache Image message.
|
||||||
|
const MSG_MDCI: u16 = 0x0018;
|
||||||
|
/// Header message flag: the library did not know the message when it wrote
|
||||||
|
/// it back (`H5O_MSG_FLAG_WAS_UNKNOWN`); libhdf5 then ignores its contents.
|
||||||
|
const MSG_FLAG_WAS_UNKNOWN: u8 = 0x20;
|
||||||
|
|
||||||
|
/// `H5F_FILE_SPACE_PAGE_SIZE_MIN` / `_MAX`.
|
||||||
|
const PAGE_SIZE_MIN: u64 = 512;
|
||||||
|
const PAGE_SIZE_MAX: u64 = 1024 * 1024 * 1024;
|
||||||
|
/// libhdf5's default file space page size, used for a version 0 message.
|
||||||
|
const PAGE_SIZE_DEFAULT: u64 = 4096;
|
||||||
|
/// Free-space managers whose addresses a persisting version 1 File Space
|
||||||
|
/// Info message lists (`H5F_MEM_PAGE_SUPER` .. `H5F_MEM_PAGE_NTYPES`), and
|
||||||
|
/// a version 0 one (`H5FD_MEM_SUPER` .. `H5FD_MEM_NTYPES`).
|
||||||
|
const FSM_ADDRS_V1: usize = 12;
|
||||||
|
const FSM_ADDRS_V0: usize = 6;
|
||||||
|
|
||||||
|
/// Metadata cache image block limits (`H5Cimage.c`, `H5ACprivate.h`).
|
||||||
|
const MDCI_SIGNATURE: &[u8; 4] = b"MDCI";
|
||||||
|
const MDCI_HAVE_RESIZE_STATUS: u8 = 0x01;
|
||||||
|
const MDCI_ENTRY_IS_FD_PARENT: u8 = 0x04;
|
||||||
|
const MDCI_ENTRY_IS_FD_CHILD: u8 = 0x08;
|
||||||
|
/// `H5AC_NTYPES`: entry type ids are below this.
|
||||||
|
const MDCI_NTYPES: u8 = 30;
|
||||||
|
/// `H5C_RING_NTYPES`.
|
||||||
|
const MDCI_RING_NTYPES: u8 = 6;
|
||||||
|
/// `H5AC__CACHE_IMAGE__ENTRY_AGEOUT__MAX`.
|
||||||
|
const MDCI_AGE_MAX: u8 = 100;
|
||||||
|
|
||||||
|
/// A decoded File Space Info message (0x0017), mapped to version 1 as
|
||||||
|
/// libhdf5 maps a version 0 one.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct FileSpaceInfo {
|
||||||
|
/// Message version as stored (0 or 1).
|
||||||
|
pub version: u8,
|
||||||
|
/// File space strategy (`H5F_fspace_strategy_t`).
|
||||||
|
pub strategy: u8,
|
||||||
|
/// Whether free space is persisted.
|
||||||
|
pub persist: bool,
|
||||||
|
/// Free-space section threshold.
|
||||||
|
pub threshold: u64,
|
||||||
|
/// File space page size.
|
||||||
|
pub page_size: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Where a metadata cache image block is (Metadata Cache Image message,
|
||||||
|
/// 0x0018).
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub struct CacheImageLocation {
|
||||||
|
/// Address of the image block.
|
||||||
|
pub address: u64,
|
||||||
|
/// Length of the image block in bytes.
|
||||||
|
pub length: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The messages of a superblock extension that libhdf5 decodes at open.
|
||||||
|
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||||
|
pub struct SuperblockExtension {
|
||||||
|
/// v1 B-tree "K" values (chunk index, symbol table node, symbol table
|
||||||
|
/// leaf), when the extension overrides the defaults.
|
||||||
|
pub btree_k: Option<(u16, u16, u16)>,
|
||||||
|
/// The File Space Info message.
|
||||||
|
pub file_space_info: Option<FileSpaceInfo>,
|
||||||
|
/// The metadata cache image, when the file has one.
|
||||||
|
pub cache_image: Option<CacheImageLocation>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ext_err(why: &'static str) -> FormatError {
|
||||||
|
FormatError::InvalidSuperblockExtension(why)
|
||||||
|
}
|
||||||
|
|
||||||
|
const RAN_OFF: &str = "ran off end of input buffer while decoding";
|
||||||
|
|
||||||
|
/// A little-endian cursor over one message or block, failing with
|
||||||
|
/// `overrun` when it runs off the end.
|
||||||
|
struct Cursor<'a> {
|
||||||
|
data: &'a [u8],
|
||||||
|
pos: usize,
|
||||||
|
overrun: FormatError,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Cursor<'a> {
|
||||||
|
fn new(data: &'a [u8], overrun: FormatError) -> Self {
|
||||||
|
Cursor {
|
||||||
|
data,
|
||||||
|
pos: 0,
|
||||||
|
overrun,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn take(&mut self, n: usize) -> Result<&'a [u8], FormatError> {
|
||||||
|
let end = self
|
||||||
|
.pos
|
||||||
|
.checked_add(n)
|
||||||
|
.filter(|&e| e <= self.data.len())
|
||||||
|
.ok_or_else(|| self.overrun.clone())?;
|
||||||
|
let s = &self.data[self.pos..end];
|
||||||
|
self.pos = end;
|
||||||
|
Ok(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn u8(&mut self) -> Result<u8, FormatError> {
|
||||||
|
Ok(self.take(1)?[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn uint(&mut self, width: u8) -> Result<u64, FormatError> {
|
||||||
|
let b = self.take(width as usize)?;
|
||||||
|
Ok(b.iter()
|
||||||
|
.rev()
|
||||||
|
.fold(0u64, |acc, &x| (acc << 8) | u64::from(x)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An address of `width` bytes; `None` when undefined (all ones).
|
||||||
|
fn addr(&mut self, width: u8) -> Result<Option<u64>, FormatError> {
|
||||||
|
let v = self.uint(width)?;
|
||||||
|
let undef = if width >= 8 {
|
||||||
|
u64::MAX
|
||||||
|
} else {
|
||||||
|
(1u64 << (8 * u32::from(width))) - 1
|
||||||
|
};
|
||||||
|
Ok((v != undef).then_some(v))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decode and check the superblock extension of `sb`, as libhdf5 does when
|
||||||
|
/// it opens the file. `data` is the file from the superblock on, up to the
|
||||||
|
/// end of file the superblock records (its end is libhdf5's "eoa").
|
||||||
|
///
|
||||||
|
/// Returns `Ok(None)` for a superblock without an extension (versions 0
|
||||||
|
/// and 1 have none). A message libhdf5 fails to decode, or a cache image
|
||||||
|
/// that does not lie inside the file, is an error: libhdf5 refuses to open
|
||||||
|
/// such a file (`cve-2020-10810`: a File Space Info message too short for
|
||||||
|
/// the free-space manager addresses it announces; `cve-2020-10812`: a cache
|
||||||
|
/// image past the end of the file).
|
||||||
|
pub fn read_superblock_extension(
|
||||||
|
data: &[u8],
|
||||||
|
sb: &Superblock,
|
||||||
|
) -> Result<Option<SuperblockExtension>, FormatError> {
|
||||||
|
read_superblock_extension_in(data, sb)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`read_superblock_extension`] over any [`Storage`]; its length is the
|
||||||
|
/// end of file.
|
||||||
|
pub fn read_superblock_extension_in<S: Storage + ?Sized>(
|
||||||
|
file: &S,
|
||||||
|
sb: &Superblock,
|
||||||
|
) -> Result<Option<SuperblockExtension>, FormatError> {
|
||||||
|
let os = sb.offset_size;
|
||||||
|
let ls = sb.length_size;
|
||||||
|
let undef = if os >= 8 {
|
||||||
|
u64::MAX
|
||||||
|
} else {
|
||||||
|
(1u64 << (8 * u32::from(os))) - 1
|
||||||
|
};
|
||||||
|
let Some(addr) = sb.superblock_extension_address.filter(|&a| a != undef) else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
let addr = usize::try_from(addr).map_err(|_| ext_err("address out of range"))?;
|
||||||
|
let header = ObjectHeader::parse_in(file, addr as u64, os, ls)?;
|
||||||
|
let eoa = file.len();
|
||||||
|
|
||||||
|
let mut ext = SuperblockExtension::default();
|
||||||
|
for msg in &header.messages {
|
||||||
|
match msg.msg_type {
|
||||||
|
MessageType::BTreeKValues => {
|
||||||
|
let mut c = Cursor::new(&msg.data, ext_err(RAN_OFF));
|
||||||
|
if c.u8()? != 0 {
|
||||||
|
return Err(ext_err("bad version number for v1 B-tree 'K' message"));
|
||||||
|
}
|
||||||
|
let chunk = c.uint(2)? as u16;
|
||||||
|
let snode = c.uint(2)? as u16;
|
||||||
|
let leaf = c.uint(2)? as u16;
|
||||||
|
ext.btree_k = Some((chunk, snode, leaf));
|
||||||
|
}
|
||||||
|
MessageType::Unknown(MSG_FSINFO) if msg.flags & MSG_FLAG_WAS_UNKNOWN == 0 => {
|
||||||
|
ext.file_space_info = Some(decode_fsinfo(&msg.data, os, ls)?);
|
||||||
|
}
|
||||||
|
MessageType::Unknown(MSG_MDCI) => {
|
||||||
|
let mut c = Cursor::new(&msg.data, ext_err(RAN_OFF));
|
||||||
|
if c.u8()? != 0 {
|
||||||
|
return Err(ext_err(
|
||||||
|
"bad version number for metadata cache image message",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let address = c.addr(os)?;
|
||||||
|
let length = c.uint(ls)?;
|
||||||
|
let Some(address) = address else {
|
||||||
|
return Err(ext_err("metadata cache image address is undefined"));
|
||||||
|
};
|
||||||
|
if address.checked_add(length).is_none_or(|end| end > eoa) {
|
||||||
|
return Err(ext_err(
|
||||||
|
"metadata cache image: address plus size exceeds file eoa",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
ext.cache_image = Some(CacheImageLocation { address, length });
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(Some(ext))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `H5O__fsinfo_decode` plus the checks `H5F__super_read` makes on it.
|
||||||
|
fn decode_fsinfo(data: &[u8], os: u8, ls: u8) -> Result<FileSpaceInfo, FormatError> {
|
||||||
|
let mut c = Cursor::new(data, ext_err(RAN_OFF));
|
||||||
|
let version = c.u8()?;
|
||||||
|
let info = if version == 0 {
|
||||||
|
let old_strategy = c.u8()?;
|
||||||
|
let threshold = c.uint(ls)?;
|
||||||
|
// H5F_file_space_type_t: 1 ALL_PERSIST, 2 ALL, 3 AGGR_VFD, 4 VFD.
|
||||||
|
let (strategy, persist) = match old_strategy {
|
||||||
|
1 => {
|
||||||
|
for _ in 0..FSM_ADDRS_V0 {
|
||||||
|
c.addr(os)?;
|
||||||
|
}
|
||||||
|
(0, true)
|
||||||
|
}
|
||||||
|
2 => (0, false),
|
||||||
|
3 => (2, false),
|
||||||
|
4 => (3, false),
|
||||||
|
_ => return Err(ext_err("invalid file space strategy")),
|
||||||
|
};
|
||||||
|
FileSpaceInfo {
|
||||||
|
version,
|
||||||
|
strategy,
|
||||||
|
persist,
|
||||||
|
threshold,
|
||||||
|
page_size: PAGE_SIZE_DEFAULT,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if version > 1 {
|
||||||
|
return Err(ext_err("File space info message's version out of bounds"));
|
||||||
|
}
|
||||||
|
let strategy = c.u8()?;
|
||||||
|
let persist = c.u8()? != 0;
|
||||||
|
let threshold = c.uint(ls)?;
|
||||||
|
let page_size = c.uint(ls)?;
|
||||||
|
if page_size == 0 || page_size > PAGE_SIZE_MAX {
|
||||||
|
return Err(ext_err("invalid page size in file space info"));
|
||||||
|
}
|
||||||
|
c.uint(2)?; // page end metadata threshold
|
||||||
|
c.addr(os)?; // EOA before the free-space managers
|
||||||
|
if persist {
|
||||||
|
for _ in 0..FSM_ADDRS_V1 {
|
||||||
|
c.addr(os)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FileSpaceInfo {
|
||||||
|
version,
|
||||||
|
strategy,
|
||||||
|
persist,
|
||||||
|
threshold,
|
||||||
|
page_size,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if info.page_size < PAGE_SIZE_MIN {
|
||||||
|
return Err(ext_err("file space page size too small"));
|
||||||
|
}
|
||||||
|
Ok(info)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One entry of a metadata cache image: `len` bytes at `image_offset` in
|
||||||
|
/// the image block, belonging at file address `address`.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
struct ImageEntry {
|
||||||
|
address: u64,
|
||||||
|
image_offset: usize,
|
||||||
|
len: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A decoded metadata cache image: where its block is, and the entries it
|
||||||
|
/// holds. [`CacheImage::apply`] writes the entries over a file's bytes.
|
||||||
|
///
|
||||||
|
/// Only the entry list is kept, never a copy of the file: an opener that
|
||||||
|
/// maps the file applies the image to a private copy-on-write mapping, so
|
||||||
|
/// only the pages the entries land on are copied.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct CacheImage {
|
||||||
|
location: CacheImageLocation,
|
||||||
|
entries: Vec<ImageEntry>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// What an opener must do about a file's metadata cache image.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum CacheImageState {
|
||||||
|
/// The file has no image: its bytes are its metadata.
|
||||||
|
Absent,
|
||||||
|
/// The file has an image that loads: apply it with [`CacheImage::apply`].
|
||||||
|
Loaded(CacheImage),
|
||||||
|
/// The file has an image libhdf5 fails to load. libhdf5 still opens the
|
||||||
|
/// file (the image loads at the first metadata read), and that read
|
||||||
|
/// fails with this error.
|
||||||
|
Unloadable(FormatError),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CacheImage {
|
||||||
|
/// Decode the metadata cache image at `location` in `data` (the file
|
||||||
|
/// from the superblock on, up to its recorded end of file). The image is
|
||||||
|
/// checked as libhdf5 checks it (`H5C__decode_cache_image_header`,
|
||||||
|
/// `H5C__reconstruct_cache_entry`): signature and version, the image
|
||||||
|
/// length it records, entry types, rings and ages in range, entry
|
||||||
|
/// addresses inside the file and not repeated, flush-dependency parents
|
||||||
|
/// already in the cache.
|
||||||
|
///
|
||||||
|
/// One check is stricter than libhdf5's: an entry must end inside the
|
||||||
|
/// file. libhdf5 checks only that it starts there, and serves the rest
|
||||||
|
/// from the image; the images libhdf5 writes never do this (every entry
|
||||||
|
/// lies below the image block, which is written last), and the bytes an
|
||||||
|
/// entry would put past the end of file have nowhere to go in a view of
|
||||||
|
/// the file.
|
||||||
|
///
|
||||||
|
/// libhdf5 does not verify the block's trailing checksum when it loads
|
||||||
|
/// an image, so neither does this.
|
||||||
|
pub fn decode(
|
||||||
|
data: &[u8],
|
||||||
|
location: CacheImageLocation,
|
||||||
|
sb: &Superblock,
|
||||||
|
) -> Result<Self, FormatError> {
|
||||||
|
Self::decode_in(data, location, sb)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`Self::decode`] over any [`Storage`]: one read of the image block.
|
||||||
|
pub fn decode_in<S: Storage + ?Sized>(
|
||||||
|
file: &S,
|
||||||
|
location: CacheImageLocation,
|
||||||
|
sb: &Superblock,
|
||||||
|
) -> Result<Self, FormatError> {
|
||||||
|
let (offset_size, length_size) = (sb.offset_size, sb.length_size);
|
||||||
|
let bad = FormatError::InvalidCacheImage;
|
||||||
|
let block = image_block_in(file, location)?;
|
||||||
|
let eoa = file.len();
|
||||||
|
let mut c = Cursor::new(&block, bad(RAN_OFF));
|
||||||
|
|
||||||
|
// Header: signature, version, flags, image data length, entry count.
|
||||||
|
if c.take(4)? != MDCI_SIGNATURE {
|
||||||
|
return Err(bad("bad metadata cache image header signature"));
|
||||||
|
}
|
||||||
|
if c.u8()? != 0 {
|
||||||
|
return Err(bad("bad metadata cache image version"));
|
||||||
|
}
|
||||||
|
if c.u8()? & MDCI_HAVE_RESIZE_STATUS != 0 {
|
||||||
|
return Err(bad("MDC resize status not yet supported"));
|
||||||
|
}
|
||||||
|
if c.uint(length_size)? != location.length {
|
||||||
|
return Err(bad("bad metadata cache image data length"));
|
||||||
|
}
|
||||||
|
let n_entries = c.uint(4)?;
|
||||||
|
if n_entries == 0 {
|
||||||
|
return Err(bad("bad metadata cache entry count"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut entries = Vec::new();
|
||||||
|
// What is in libhdf5's cache when it loads the image: the superblock
|
||||||
|
// and the superblock extension's object header (read to find the
|
||||||
|
// image). Each entry's flush-dependency parents are looked up in the
|
||||||
|
// cache as the entry is inserted (`H5C__reconstruct_cache_contents`
|
||||||
|
// searches the index inside the loop that inserts the entries, in
|
||||||
|
// HDF5 1.14.6 and 2.0.0 alike), so a parent must be one of those or
|
||||||
|
// an earlier entry.
|
||||||
|
let mut cached = BTreeSet::new();
|
||||||
|
cached.insert(0);
|
||||||
|
if let Some(ext) = sb.superblock_extension_address {
|
||||||
|
cached.insert(ext);
|
||||||
|
}
|
||||||
|
let mut seen = BTreeSet::new();
|
||||||
|
for _ in 0..n_entries {
|
||||||
|
let type_id = c.u8()?;
|
||||||
|
if type_id >= MDCI_NTYPES {
|
||||||
|
return Err(bad("type id is out of valid range"));
|
||||||
|
}
|
||||||
|
let flags = c.u8()?;
|
||||||
|
if c.u8()? >= MDCI_RING_NTYPES {
|
||||||
|
return Err(bad("ring is out of valid range"));
|
||||||
|
}
|
||||||
|
if c.u8()? > MDCI_AGE_MAX {
|
||||||
|
return Err(bad("entry age is out of policy range"));
|
||||||
|
}
|
||||||
|
let children = c.uint(2)?;
|
||||||
|
// libhdf5 checks the parent flag against the child count only in
|
||||||
|
// debug builds (release builds refuse any entry with children);
|
||||||
|
// the image format's own rule is checked here.
|
||||||
|
if (flags & MDCI_ENTRY_IS_FD_PARENT != 0) != (children > 0) {
|
||||||
|
return Err(bad("flush dependency parent flag and child count disagree"));
|
||||||
|
}
|
||||||
|
c.uint(2)?; // dirty dependency children: reset for a read-only open
|
||||||
|
let parents = c.uint(2)?;
|
||||||
|
if (flags & MDCI_ENTRY_IS_FD_CHILD != 0) != (parents > 0) {
|
||||||
|
return Err(bad("flush dependency child flag and parent count disagree"));
|
||||||
|
}
|
||||||
|
c.uint(4)?; // LRU rank
|
||||||
|
let address = c
|
||||||
|
.addr(offset_size)?
|
||||||
|
.filter(|&a| a < eoa)
|
||||||
|
.ok_or(bad("invalid entry address range"))?;
|
||||||
|
let size = c.uint(length_size)?;
|
||||||
|
if size == 0 {
|
||||||
|
return Err(bad("invalid entry size"));
|
||||||
|
}
|
||||||
|
for _ in 0..parents {
|
||||||
|
let parent = c
|
||||||
|
.addr(offset_size)?
|
||||||
|
.ok_or(bad("invalid flush dependency parent offset"))?;
|
||||||
|
if !seen.contains(&parent) && !cached.contains(&parent) {
|
||||||
|
return Err(bad("fd parent not in cache"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let len = usize::try_from(size).map_err(|_| bad(RAN_OFF))?;
|
||||||
|
let image_offset = c.pos;
|
||||||
|
c.take(len)?;
|
||||||
|
if address.checked_add(size).is_none_or(|end| end > eoa) {
|
||||||
|
return Err(bad("entry extends past the end of file"));
|
||||||
|
}
|
||||||
|
if !seen.insert(address) {
|
||||||
|
return Err(bad("duplicate addresses in cache"));
|
||||||
|
}
|
||||||
|
entries.push(ImageEntry {
|
||||||
|
address,
|
||||||
|
image_offset,
|
||||||
|
len,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok(CacheImage { location, entries })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Where the image block is.
|
||||||
|
pub fn location(&self) -> CacheImageLocation {
|
||||||
|
self.location
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The number of entries in the image.
|
||||||
|
pub fn len(&self) -> usize {
|
||||||
|
self.entries.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether the image has no entries (a decoded image always has some).
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
self.entries.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The file ranges (address, length) the image's entries replace.
|
||||||
|
pub fn entry_ranges(&self) -> impl Iterator<Item = (u64, usize)> + '_ {
|
||||||
|
self.entries.iter().map(|e| (e.address, e.len))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The image block in `data`, the bytes [`Self::decode`] read it from.
|
||||||
|
pub fn block<'a>(&self, data: &'a [u8]) -> Result<&'a [u8], FormatError> {
|
||||||
|
image_block(data, self.location)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`Self::block`] over any [`Storage`].
|
||||||
|
pub fn block_in<'a, S: Storage + ?Sized>(
|
||||||
|
&self,
|
||||||
|
file: &'a S,
|
||||||
|
) -> Result<Cow<'a, [u8]>, FormatError> {
|
||||||
|
image_block_in(file, self.location)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Every entry as `(file address, its bytes)`, taken from `block` (the
|
||||||
|
/// image block, see [`Self::block_in`]), in the order [`Self::apply`]
|
||||||
|
/// writes them: for a reader that cannot write the image over the
|
||||||
|
/// file's bytes and lays the entries over each read instead.
|
||||||
|
pub fn entries<'b>(&self, block: &'b [u8]) -> Result<Vec<(u64, &'b [u8])>, FormatError> {
|
||||||
|
let short = || FormatError::InvalidCacheImage("image applied to the wrong file");
|
||||||
|
self.entries
|
||||||
|
.iter()
|
||||||
|
.map(|e| {
|
||||||
|
let src = block
|
||||||
|
.get(e.image_offset..e.image_offset + e.len)
|
||||||
|
.ok_or_else(short)?;
|
||||||
|
Ok((e.address, src))
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write every entry over `dst`, the file's bytes from the superblock
|
||||||
|
/// on (as long as the `data` the image was decoded from), taking the
|
||||||
|
/// entries from `block` (the image block, see [`Self::block`]). `block`
|
||||||
|
/// must not alias `dst`: an entry may land on the block itself.
|
||||||
|
pub fn apply(&self, block: &[u8], dst: &mut [u8]) -> Result<(), FormatError> {
|
||||||
|
let short = || FormatError::InvalidCacheImage("image applied to the wrong file");
|
||||||
|
for e in &self.entries {
|
||||||
|
let src = block
|
||||||
|
.get(e.image_offset..e.image_offset + e.len)
|
||||||
|
.ok_or_else(short)?;
|
||||||
|
let at = usize::try_from(e.address).map_err(|_| short())?;
|
||||||
|
dst.get_mut(at..at + e.len)
|
||||||
|
.ok_or_else(short)?
|
||||||
|
.copy_from_slice(src);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn image_block(data: &[u8], location: CacheImageLocation) -> Result<&[u8], FormatError> {
|
||||||
|
let (start, len) = image_block_range(data.len() as u64, location)?;
|
||||||
|
let start = crate::addr::to_usize(start)?;
|
||||||
|
Ok(&data[start..start + len])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn image_block_in<S: Storage + ?Sized>(
|
||||||
|
file: &S,
|
||||||
|
location: CacheImageLocation,
|
||||||
|
) -> Result<Cow<'_, [u8]>, FormatError> {
|
||||||
|
let (start, len) = image_block_range(file.len(), location)?;
|
||||||
|
read_exact_at(file, start, len)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Where the image block is, checked against a file of `file_len` bytes.
|
||||||
|
fn image_block_range(
|
||||||
|
file_len: u64,
|
||||||
|
location: CacheImageLocation,
|
||||||
|
) -> Result<(u64, usize), FormatError> {
|
||||||
|
let bad = FormatError::InvalidCacheImage;
|
||||||
|
let start = usize::try_from(location.address).map_err(|_| bad("address out of range"))?;
|
||||||
|
let len = usize::try_from(location.length).map_err(|_| bad("length out of range"))?;
|
||||||
|
start
|
||||||
|
.checked_add(len)
|
||||||
|
.filter(|&end| end as u64 <= file_len)
|
||||||
|
.ok_or(bad("image block extends past the end of the file"))?;
|
||||||
|
Ok((start as u64, len))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// What an opener must do before reading a file's metadata: check the
|
||||||
|
/// superblock extension ([`read_superblock_extension`]; an error means
|
||||||
|
/// libhdf5 refuses to open the file) and decode any metadata cache image
|
||||||
|
/// ([`CacheImage::decode`]). `data` is the file from the superblock on, up
|
||||||
|
/// to its recorded end of file.
|
||||||
|
pub fn cache_image_state(data: &[u8], sb: &Superblock) -> Result<CacheImageState, FormatError> {
|
||||||
|
cache_image_state_in(data, sb)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`cache_image_state`] over any [`Storage`].
|
||||||
|
pub fn cache_image_state_in<S: Storage + ?Sized>(
|
||||||
|
file: &S,
|
||||||
|
sb: &Superblock,
|
||||||
|
) -> Result<CacheImageState, FormatError> {
|
||||||
|
match read_superblock_extension_in(file, sb)? {
|
||||||
|
Some(SuperblockExtension {
|
||||||
|
cache_image: Some(location),
|
||||||
|
..
|
||||||
|
}) => Ok(match CacheImage::decode_in(file, location, sb) {
|
||||||
|
Ok(image) => CacheImageState::Loaded(image),
|
||||||
|
Err(e) => CacheImageState::Unloadable(e),
|
||||||
|
}),
|
||||||
|
_ => Ok(CacheImageState::Absent),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`cache_image_state`] for a reader that holds the file's bytes in a
|
||||||
|
/// buffer of its own: check the superblock extension and write any cache
|
||||||
|
/// image over `data` in place (only the image block is copied). An image
|
||||||
|
/// libhdf5 cannot load is an error here: such a reader has no way to open
|
||||||
|
/// the file and fail each object instead.
|
||||||
|
pub fn apply_cache_image_in_place(data: &mut [u8], sb: &Superblock) -> Result<(), FormatError> {
|
||||||
|
match cache_image_state(data, sb)? {
|
||||||
|
CacheImageState::Absent => Ok(()),
|
||||||
|
CacheImageState::Unloadable(e) => Err(e),
|
||||||
|
CacheImageState::Loaded(image) => {
|
||||||
|
let block = image.block(data)?.to_vec();
|
||||||
|
image.apply(&block, data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
/// The file's bytes with the image at `loc` applied.
|
||||||
|
fn apply_cache_image(
|
||||||
|
data: &[u8],
|
||||||
|
loc: CacheImageLocation,
|
||||||
|
sb: &Superblock,
|
||||||
|
) -> Result<Vec<u8>, FormatError> {
|
||||||
|
let image = CacheImage::decode(data, loc, sb)?;
|
||||||
|
let mut out = data.to_vec();
|
||||||
|
image.apply(image.block(data)?, &mut out)?;
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sb_v2(ext: u64) -> Superblock {
|
||||||
|
Superblock {
|
||||||
|
version: 2,
|
||||||
|
offset_size: 8,
|
||||||
|
length_size: 8,
|
||||||
|
base_address: 0,
|
||||||
|
eof_address: 0,
|
||||||
|
root_group_address: 0,
|
||||||
|
group_leaf_node_k: None,
|
||||||
|
group_internal_node_k: None,
|
||||||
|
indexed_storage_internal_node_k: None,
|
||||||
|
free_space_address: None,
|
||||||
|
driver_info_address: None,
|
||||||
|
consistency_flags: 0,
|
||||||
|
superblock_extension_address: Some(ext),
|
||||||
|
checksum: None,
|
||||||
|
page_size: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A file whose superblock extension (a version 1 object header at 48)
|
||||||
|
/// holds the given messages, padded to `len` bytes.
|
||||||
|
fn file_with_ext(messages: &[(u16, &[u8])], len: usize) -> Vec<u8> {
|
||||||
|
let mut body = Vec::new();
|
||||||
|
for &(t, d) in messages {
|
||||||
|
let padded = d.len().div_ceil(8) * 8;
|
||||||
|
body.extend_from_slice(&t.to_le_bytes());
|
||||||
|
body.extend_from_slice(&(padded as u16).to_le_bytes());
|
||||||
|
body.extend_from_slice(&[0x14, 0, 0, 0]);
|
||||||
|
body.extend_from_slice(d);
|
||||||
|
body.resize(body.len() + padded - d.len(), 0);
|
||||||
|
}
|
||||||
|
let mut f = vec![0u8; 48];
|
||||||
|
f.push(1);
|
||||||
|
f.push(0);
|
||||||
|
f.extend_from_slice(&(messages.len() as u16).to_le_bytes());
|
||||||
|
f.extend_from_slice(&1u32.to_le_bytes());
|
||||||
|
f.extend_from_slice(&(body.len() as u32).to_le_bytes());
|
||||||
|
f.extend_from_slice(&[0; 4]);
|
||||||
|
f.extend_from_slice(&body);
|
||||||
|
f.resize(len, 0);
|
||||||
|
f
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fsinfo_v1(page_size: u64, persist: bool, n_addrs: usize) -> Vec<u8> {
|
||||||
|
let mut m = vec![1, 1, u8::from(persist)];
|
||||||
|
m.extend_from_slice(&1u64.to_le_bytes());
|
||||||
|
m.extend_from_slice(&page_size.to_le_bytes());
|
||||||
|
m.extend_from_slice(&0u16.to_le_bytes());
|
||||||
|
m.extend_from_slice(&u64::MAX.to_le_bytes());
|
||||||
|
for _ in 0..n_addrs {
|
||||||
|
m.extend_from_slice(&u64::MAX.to_le_bytes());
|
||||||
|
}
|
||||||
|
m
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mdci(address: u64, length: u64) -> Vec<u8> {
|
||||||
|
let mut m = vec![0];
|
||||||
|
m.extend_from_slice(&address.to_le_bytes());
|
||||||
|
m.extend_from_slice(&length.to_le_bytes());
|
||||||
|
m
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn no_extension() {
|
||||||
|
assert_eq!(
|
||||||
|
read_superblock_extension(&[0; 64], &sb_v2(u64::MAX)).unwrap(),
|
||||||
|
None
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn file_space_info_as_libhdf5_decodes_it() {
|
||||||
|
// What FileWriter::with_page_size writes.
|
||||||
|
let f = file_with_ext(&[(MSG_FSINFO, &fsinfo_v1(4096, false, 0))], 256);
|
||||||
|
let ext = read_superblock_extension(&f, &sb_v2(48)).unwrap().unwrap();
|
||||||
|
assert_eq!(ext.file_space_info.unwrap().page_size, 4096);
|
||||||
|
let f = file_with_ext(&[(MSG_FSINFO, &fsinfo_v1(4096, true, 12))], 512);
|
||||||
|
assert!(read_superblock_extension(&f, &sb_v2(48)).is_ok());
|
||||||
|
|
||||||
|
let refused = |m: Vec<u8>| {
|
||||||
|
let f = file_with_ext(&[(MSG_FSINFO, &m)], 512);
|
||||||
|
read_superblock_extension(&f, &sb_v2(48)).unwrap_err()
|
||||||
|
};
|
||||||
|
// Persisting, but too short for the manager addresses.
|
||||||
|
let mut short = fsinfo_v1(4096, true, 12);
|
||||||
|
short.truncate(short.len() - 8);
|
||||||
|
assert_eq!(refused(short), ext_err(RAN_OFF));
|
||||||
|
assert!(matches!(
|
||||||
|
refused(fsinfo_v1(256, false, 0)),
|
||||||
|
FormatError::InvalidSuperblockExtension(_)
|
||||||
|
));
|
||||||
|
assert!(matches!(
|
||||||
|
refused(fsinfo_v1(0, false, 0)),
|
||||||
|
FormatError::InvalidSuperblockExtension(_)
|
||||||
|
));
|
||||||
|
let mut v2 = fsinfo_v1(4096, false, 0);
|
||||||
|
v2[0] = 2;
|
||||||
|
assert!(matches!(
|
||||||
|
refused(v2),
|
||||||
|
FormatError::InvalidSuperblockExtension(_)
|
||||||
|
));
|
||||||
|
// cve-2020-10810: version 0, strategy ALL_PERSIST, and a message of
|
||||||
|
// 32 bytes that cannot hold the six addresses that follow.
|
||||||
|
let mut v0 = vec![0u8, 1];
|
||||||
|
v0.extend_from_slice(&[0, 1, 0, 0, 0, 0, 0, 0]);
|
||||||
|
v0.resize(32, 0xff);
|
||||||
|
assert_eq!(refused(v0), ext_err(RAN_OFF));
|
||||||
|
// A version 0 message without persistence is fine.
|
||||||
|
let mut v0 = vec![0u8, 2];
|
||||||
|
v0.extend_from_slice(&[0; 8]);
|
||||||
|
let f = file_with_ext(&[(MSG_FSINFO, &v0)], 256);
|
||||||
|
assert!(read_superblock_extension(&f, &sb_v2(48)).is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cache_image_location_must_be_inside_the_file() {
|
||||||
|
let f = file_with_ext(&[(MSG_MDCI, &mdci(128, 64))], 192);
|
||||||
|
let ext = read_superblock_extension(&f, &sb_v2(48)).unwrap().unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
ext.cache_image,
|
||||||
|
Some(CacheImageLocation {
|
||||||
|
address: 128,
|
||||||
|
length: 64
|
||||||
|
})
|
||||||
|
);
|
||||||
|
// cve-2020-10812: 256 MiB at 0x10100 in a 2565-byte file.
|
||||||
|
let f = file_with_ext(&[(MSG_MDCI, &mdci(0x10100, 0x1000_0000))], 2565);
|
||||||
|
assert!(matches!(
|
||||||
|
read_superblock_extension(&f, &sb_v2(48)),
|
||||||
|
Err(FormatError::InvalidSuperblockExtension(_))
|
||||||
|
));
|
||||||
|
let f = file_with_ext(&[(MSG_MDCI, &mdci(u64::MAX, 8))], 256);
|
||||||
|
assert!(read_superblock_extension(&f, &sb_v2(48)).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A cache image block with `entries` of (address, bytes).
|
||||||
|
fn image(entries: &[(u64, &[u8])]) -> Vec<u8> {
|
||||||
|
let with_deps: Vec<_> = entries.iter().map(|&(a, b)| (a, b, 0, None)).collect();
|
||||||
|
image_with_deps(&with_deps)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A cache image block with `entries` of (address, bytes, flush
|
||||||
|
/// dependency children, flush dependency parent).
|
||||||
|
fn image_with_deps(entries: &[(u64, &[u8], u16, Option<u64>)]) -> Vec<u8> {
|
||||||
|
let mut b = Vec::new();
|
||||||
|
b.extend_from_slice(MDCI_SIGNATURE);
|
||||||
|
b.push(0);
|
||||||
|
b.push(0);
|
||||||
|
b.extend_from_slice(&0u64.to_le_bytes()); // length, patched below
|
||||||
|
b.extend_from_slice(&(entries.len() as u32).to_le_bytes());
|
||||||
|
for &(addr, bytes, children, parent) in entries {
|
||||||
|
let mut flags = 0x02; // in LRU
|
||||||
|
if children > 0 {
|
||||||
|
flags |= MDCI_ENTRY_IS_FD_PARENT;
|
||||||
|
}
|
||||||
|
if parent.is_some() {
|
||||||
|
flags |= MDCI_ENTRY_IS_FD_CHILD;
|
||||||
|
}
|
||||||
|
b.extend_from_slice(&[5, flags, 1, 0]); // type, flags, ring, age
|
||||||
|
b.extend_from_slice(&children.to_le_bytes());
|
||||||
|
b.extend_from_slice(&0u16.to_le_bytes()); // dirty children
|
||||||
|
b.extend_from_slice(&u16::from(parent.is_some()).to_le_bytes());
|
||||||
|
b.extend_from_slice(&0i32.to_le_bytes());
|
||||||
|
b.extend_from_slice(&addr.to_le_bytes());
|
||||||
|
b.extend_from_slice(&(bytes.len() as u64).to_le_bytes());
|
||||||
|
if let Some(p) = parent {
|
||||||
|
b.extend_from_slice(&p.to_le_bytes());
|
||||||
|
}
|
||||||
|
b.extend_from_slice(bytes);
|
||||||
|
}
|
||||||
|
b.extend_from_slice(&[0; 4]); // checksum (not verified, as in libhdf5)
|
||||||
|
let n = b.len() as u64;
|
||||||
|
b[6..14].copy_from_slice(&n.to_le_bytes());
|
||||||
|
b
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cache_image_entries_replace_the_file_bytes() {
|
||||||
|
let img = image(&[(16, b"HEADER"), (40, b"NODE")]);
|
||||||
|
let mut f = vec![0u8; 64];
|
||||||
|
let at = f.len() as u64;
|
||||||
|
f.extend_from_slice(&img);
|
||||||
|
let loc = CacheImageLocation {
|
||||||
|
address: at,
|
||||||
|
length: img.len() as u64,
|
||||||
|
};
|
||||||
|
let out = apply_cache_image(&f, loc, &sb_v2(u64::MAX)).unwrap();
|
||||||
|
assert_eq!(out.len(), f.len());
|
||||||
|
assert_eq!(&out[16..22], b"HEADER");
|
||||||
|
assert_eq!(&out[40..44], b"NODE");
|
||||||
|
assert_eq!(&out[..16], &f[..16]);
|
||||||
|
|
||||||
|
let bad = |img: Vec<u8>| {
|
||||||
|
let mut f = vec![0u8; 64];
|
||||||
|
f.extend_from_slice(&img);
|
||||||
|
let loc = CacheImageLocation {
|
||||||
|
address: 64,
|
||||||
|
length: img.len() as u64,
|
||||||
|
};
|
||||||
|
apply_cache_image(&f, loc, &sb_v2(u64::MAX)).unwrap_err()
|
||||||
|
};
|
||||||
|
let mut sig = image(&[(16, b"x")]);
|
||||||
|
sig[0] = b'X';
|
||||||
|
assert!(matches!(bad(sig), FormatError::InvalidCacheImage(_)));
|
||||||
|
assert!(matches!(
|
||||||
|
bad(image(&[(16, b"a"), (16, b"b")])),
|
||||||
|
FormatError::InvalidCacheImage("duplicate addresses in cache")
|
||||||
|
));
|
||||||
|
assert!(matches!(
|
||||||
|
bad(image(&[(1 << 20, b"far")])),
|
||||||
|
FormatError::InvalidCacheImage("invalid entry address range")
|
||||||
|
));
|
||||||
|
let mut len = image(&[(16, b"x")]);
|
||||||
|
len[6] ^= 1;
|
||||||
|
assert!(matches!(bad(len), FormatError::InvalidCacheImage(_)));
|
||||||
|
// An entry that starts inside the file (64 bytes, then a 60-byte
|
||||||
|
// image) but runs past its end.
|
||||||
|
assert!(matches!(
|
||||||
|
bad(image(&[(123, b"8 bytes!")])),
|
||||||
|
FormatError::InvalidCacheImage("entry extends past the end of file")
|
||||||
|
));
|
||||||
|
let mut cut = image(&[(16, b"abcdef")]);
|
||||||
|
let n = cut.len() as u64 - 8;
|
||||||
|
cut.truncate(cut.len() - 8);
|
||||||
|
cut[6..14].copy_from_slice(&n.to_le_bytes());
|
||||||
|
assert!(matches!(bad(cut), FormatError::InvalidCacheImage(_)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// libhdf5 resolves an entry's flush-dependency parents as it inserts
|
||||||
|
/// the entry (`H5C__reconstruct_cache_contents`): a parent must be an
|
||||||
|
/// earlier entry, or the superblock or its extension's object header,
|
||||||
|
/// which are cached before the image loads. A parent listed after its
|
||||||
|
/// child fails ("fd parent not in cache?!?").
|
||||||
|
#[test]
|
||||||
|
fn flush_dependency_parents_must_already_be_cached() {
|
||||||
|
let load = |img: Vec<u8>| {
|
||||||
|
let mut f = vec![0u8; 64];
|
||||||
|
f.extend_from_slice(&img);
|
||||||
|
let loc = CacheImageLocation {
|
||||||
|
address: 64,
|
||||||
|
length: img.len() as u64,
|
||||||
|
};
|
||||||
|
apply_cache_image(&f, loc, &sb_v2(48))
|
||||||
|
};
|
||||||
|
// Parent first, as libhdf5 writes images.
|
||||||
|
assert!(
|
||||||
|
load(image_with_deps(&[
|
||||||
|
(16, b"P", 1, None),
|
||||||
|
(40, b"C", 0, Some(16))
|
||||||
|
]))
|
||||||
|
.is_ok()
|
||||||
|
);
|
||||||
|
// Child first: libhdf5 does not find the parent.
|
||||||
|
assert_eq!(
|
||||||
|
load(image_with_deps(&[
|
||||||
|
(40, b"C", 0, Some(16)),
|
||||||
|
(16, b"P", 1, None)
|
||||||
|
]))
|
||||||
|
.unwrap_err(),
|
||||||
|
FormatError::InvalidCacheImage("fd parent not in cache")
|
||||||
|
);
|
||||||
|
// The superblock extension's header (at 48 here) is in the cache.
|
||||||
|
assert!(load(image_with_deps(&[(40, b"C", 0, Some(48))])).is_ok());
|
||||||
|
// An entry cannot be its own parent.
|
||||||
|
assert!(load(image_with_deps(&[(40, b"C", 1, Some(40))])).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The extension and cache image decode identically through a
|
||||||
|
/// `read_at`-only storage, errors included.
|
||||||
|
#[test]
|
||||||
|
fn storage_parse_matches_slice_parse() {
|
||||||
|
use crate::storage::CountingStorage;
|
||||||
|
let img = image(&[(16, b"HEADER"), (40, b"NODE")]);
|
||||||
|
let mut with_image = file_with_ext(&[(MSG_MDCI, &mdci(256, img.len() as u64))], 256);
|
||||||
|
with_image.extend_from_slice(&img);
|
||||||
|
let mut bad_image = with_image.clone();
|
||||||
|
bad_image[256] = b'X';
|
||||||
|
let files = [
|
||||||
|
file_with_ext(&[(MSG_FSINFO, &fsinfo_v1(4096, false, 0))], 256),
|
||||||
|
file_with_ext(&[(MSG_FSINFO, &fsinfo_v1(256, false, 0))], 256),
|
||||||
|
file_with_ext(&[(MSG_MDCI, &mdci(128, 64))], 192),
|
||||||
|
file_with_ext(&[(MSG_MDCI, &mdci(0x10100, 0x1000_0000))], 2565),
|
||||||
|
file_with_ext(&[(MSG_FSINFO, &fsinfo_v1(4096, true, 12))], 60),
|
||||||
|
with_image,
|
||||||
|
bad_image,
|
||||||
|
];
|
||||||
|
for f in files {
|
||||||
|
let storage = CountingStorage::new(f.clone());
|
||||||
|
let sb = sb_v2(48);
|
||||||
|
assert_eq!(
|
||||||
|
read_superblock_extension_in(&storage, &sb),
|
||||||
|
read_superblock_extension(&f, &sb)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
cache_image_state_in(&storage, &sb),
|
||||||
|
cache_image_state(&f, &sb)
|
||||||
|
);
|
||||||
|
if let Ok(CacheImageState::Loaded(image)) = cache_image_state(&f, &sb) {
|
||||||
|
assert_eq!(
|
||||||
|
&*image.block_in(&storage).unwrap(),
|
||||||
|
image.block(&f).unwrap()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
use alloc::vec::Vec;
|
use alloc::vec::Vec;
|
||||||
|
|
||||||
use crate::error::FormatError;
|
use crate::error::FormatError;
|
||||||
|
use crate::storage::{Storage, read_exact_at};
|
||||||
|
|
||||||
/// Symbol Table message (type 0x0011) found in v1 group object headers.
|
/// Symbol Table message (type 0x0011) found in v1 group object headers.
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
@@ -79,65 +80,49 @@ impl SymbolTableNode {
|
|||||||
offset: usize,
|
offset: usize,
|
||||||
offset_size: u8,
|
offset_size: u8,
|
||||||
) -> Result<SymbolTableNode, FormatError> {
|
) -> Result<SymbolTableNode, FormatError> {
|
||||||
// signature(4) + version(1) + reserved(1) + number_of_symbols(2) = 8
|
Self::parse_in(file_data, offset as u64, offset_size)
|
||||||
if offset
|
|
||||||
.checked_add(8)
|
|
||||||
.is_none_or(|end| end > file_data.len())
|
|
||||||
{
|
|
||||||
return Err(FormatError::UnexpectedEof {
|
|
||||||
expected: offset.saturating_add(8),
|
|
||||||
available: file_data.len(),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if &file_data[offset..offset + 4] != b"SNOD" {
|
/// [`Self::parse`] over any [`Storage`]: one read of the node's header,
|
||||||
|
/// one of its entries.
|
||||||
|
pub fn parse_in<S: Storage + ?Sized>(
|
||||||
|
file: &S,
|
||||||
|
offset: u64,
|
||||||
|
offset_size: u8,
|
||||||
|
) -> Result<SymbolTableNode, FormatError> {
|
||||||
|
// signature(4) + version(1) + reserved(1) + number_of_symbols(2) = 8
|
||||||
|
let header = read_exact_at(file, offset, 8)?;
|
||||||
|
|
||||||
|
if &header[..4] != b"SNOD" {
|
||||||
return Err(FormatError::InvalidSymbolTableNodeSignature);
|
return Err(FormatError::InvalidSymbolTableNodeSignature);
|
||||||
}
|
}
|
||||||
|
|
||||||
let version = file_data[offset + 4];
|
let version = header[4];
|
||||||
if version != 1 {
|
if version != 1 {
|
||||||
return Err(FormatError::InvalidSymbolTableNodeVersion(version));
|
return Err(FormatError::InvalidSymbolTableNodeVersion(version));
|
||||||
}
|
}
|
||||||
|
|
||||||
let num_symbols =
|
let num_symbols = u16::from_le_bytes([header[6], header[7]]) as usize;
|
||||||
u16::from_le_bytes([file_data[offset + 6], file_data[offset + 7]]) as usize;
|
|
||||||
|
|
||||||
let os = offset_size as usize;
|
let os = offset_size as usize;
|
||||||
// Each entry: link_name_offset(os) + obj_hdr_addr(os) + cache_type(4) + reserved(4) + scratch(16)
|
// Each entry: link_name_offset(os) + obj_hdr_addr(os) + cache_type(4) + reserved(4) + scratch(16)
|
||||||
let entry_size = os + os + 4 + 4 + 16;
|
let entry_size = os + os + 4 + 4 + 16;
|
||||||
let entries_start = offset + 8;
|
// `offset + 8` fits: the header's read checked it. The entries'
|
||||||
let needed = entries_start.checked_add(num_symbols * entry_size).ok_or(
|
// read is the bounds check (`offset + 8 + entries > file length`,
|
||||||
FormatError::UnexpectedEof {
|
// which cannot overflow: at most 65535 entries of 40 bytes).
|
||||||
expected: usize::MAX,
|
let body = read_exact_at(file, offset + 8, num_symbols * entry_size)?;
|
||||||
available: file_data.len(),
|
let file_data: &[u8] = &body;
|
||||||
},
|
|
||||||
)?;
|
|
||||||
if needed > file_data.len() {
|
|
||||||
return Err(FormatError::UnexpectedEof {
|
|
||||||
expected: needed,
|
|
||||||
available: file_data.len(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut entries = Vec::with_capacity(num_symbols);
|
let mut entries = Vec::with_capacity(num_symbols);
|
||||||
let mut pos = entries_start;
|
for entry in file_data.chunks_exact(entry_size) {
|
||||||
for _ in 0..num_symbols {
|
let link_name_offset = read_offset(entry, 0, offset_size)?;
|
||||||
let link_name_offset = read_offset(file_data, pos, offset_size)?;
|
let object_header_address = read_offset(entry, os, offset_size)?;
|
||||||
pos += os;
|
let pos = 2 * os;
|
||||||
let object_header_address = read_offset(file_data, pos, offset_size)?;
|
let cache_type =
|
||||||
pos += os;
|
u32::from_le_bytes([entry[pos], entry[pos + 1], entry[pos + 2], entry[pos + 3]]);
|
||||||
let cache_type = u32::from_le_bytes([
|
|
||||||
file_data[pos],
|
|
||||||
file_data[pos + 1],
|
|
||||||
file_data[pos + 2],
|
|
||||||
file_data[pos + 3],
|
|
||||||
]);
|
|
||||||
pos += 4;
|
|
||||||
// reserved 4 bytes
|
// reserved 4 bytes
|
||||||
pos += 4;
|
|
||||||
let mut scratch_pad = [0u8; 16];
|
let mut scratch_pad = [0u8; 16];
|
||||||
scratch_pad.copy_from_slice(&file_data[pos..pos + 16]);
|
scratch_pad.copy_from_slice(&entry[pos + 8..pos + 24]);
|
||||||
pos += 16;
|
|
||||||
|
|
||||||
entries.push(SymbolTableEntry {
|
entries.push(SymbolTableEntry {
|
||||||
link_name_offset,
|
link_name_offset,
|
||||||
@@ -256,4 +241,28 @@ mod tests {
|
|||||||
let result = SymbolTableNode::parse(&data, usize::MAX / 2, 8);
|
let result = SymbolTableNode::parse(&data, usize::MAX / 2, 8);
|
||||||
assert!(result.is_err());
|
assert!(result.is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Nodes, cut at every length and at an offset, parse identically
|
||||||
|
/// through a `read_at`-only storage.
|
||||||
|
#[test]
|
||||||
|
fn storage_parse_matches_slice_parse() {
|
||||||
|
use crate::storage::CountingStorage;
|
||||||
|
for os in [4u8, 8] {
|
||||||
|
let node = build_snod(&[(0, 0x100, 0), (8, 0x200, 1), (16, 0x300, 2)], os);
|
||||||
|
let mut bad = node.clone();
|
||||||
|
bad[4] = 2;
|
||||||
|
for full in [node, bad] {
|
||||||
|
for at in [0usize, 7] {
|
||||||
|
for cut in 0..=full.len() {
|
||||||
|
let mut f = vec![0u8; at];
|
||||||
|
f.extend_from_slice(&full[..cut]);
|
||||||
|
let storage = CountingStorage::new(f.clone());
|
||||||
|
let want = SymbolTableNode::parse(&f, at, os);
|
||||||
|
let got = SymbolTableNode::parse_in(&storage, at as u64, os);
|
||||||
|
assert_eq!(format!("{got:?}"), format!("{want:?}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -296,7 +296,8 @@ impl EnumTypeBuilder {
|
|||||||
|
|
||||||
// ---- Attribute helper ----
|
// ---- Attribute helper ----
|
||||||
|
|
||||||
pub(crate) fn build_attr_message(name: &str, value: &AttrValue) -> AttributeMessage {
|
/// The attribute message the writers store for `value` under `name`.
|
||||||
|
pub fn build_attr_message(name: &str, value: &AttrValue) -> AttributeMessage {
|
||||||
match value {
|
match value {
|
||||||
AttrValue::F64(v) => AttributeMessage {
|
AttrValue::F64(v) => AttributeMessage {
|
||||||
name: name.to_string(),
|
name: name.to_string(),
|
||||||
@@ -503,6 +504,9 @@ pub struct DatasetBuilder {
|
|||||||
/// `data` field is ignored; instead the global heap blob is built from
|
/// `data` field is ignored; instead the global heap blob is built from
|
||||||
/// these mappings and a VDS layout message is emitted.
|
/// these mappings and a VDS layout message is emitted.
|
||||||
pub(crate) virtual_sources: Option<Vec<VdsMapping>>,
|
pub(crate) virtual_sources: Option<Vec<VdsMapping>>,
|
||||||
|
/// Track (and index) attribute creation order; `None` follows the
|
||||||
|
/// file's default (`FileWriter::track_order`).
|
||||||
|
pub(crate) track_order: Option<bool>,
|
||||||
#[cfg(feature = "provenance")]
|
#[cfg(feature = "provenance")]
|
||||||
pub(crate) provenance: Option<ProvenanceConfig>,
|
pub(crate) provenance: Option<ProvenanceConfig>,
|
||||||
}
|
}
|
||||||
@@ -522,11 +526,22 @@ impl DatasetBuilder {
|
|||||||
compact: false,
|
compact: false,
|
||||||
alignment: 0,
|
alignment: 0,
|
||||||
virtual_sources: None,
|
virtual_sources: None,
|
||||||
|
track_order: None,
|
||||||
#[cfg(feature = "provenance")]
|
#[cfg(feature = "provenance")]
|
||||||
provenance: None,
|
provenance: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Track the creation order of this dataset's attributes, and index it,
|
||||||
|
/// as h5py's `create_dataset(..., track_order=True)` does: libhdf5 (and
|
||||||
|
/// h5py) then list the attributes in the order they were set rather
|
||||||
|
/// than by name. libhdf5 numbers at most 65 535 attributes on an object
|
||||||
|
/// that tracks their order; more is an error when the file is written.
|
||||||
|
pub fn track_order(&mut self, track: bool) -> &mut Self {
|
||||||
|
self.track_order = Some(track);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub fn with_f64_data(&mut self, data: &[f64]) -> &mut Self {
|
pub fn with_f64_data(&mut self, data: &[f64]) -> &mut Self {
|
||||||
self.datatype = Some(make_f64_type());
|
self.datatype = Some(make_f64_type());
|
||||||
let mut b = Vec::with_capacity(data.len() * 8);
|
let mut b = Vec::with_capacity(data.len() * 8);
|
||||||
@@ -986,10 +1001,11 @@ impl GroupBuilder {
|
|||||||
self.attrs.push((name.to_string(), value));
|
self.attrs.push((name.to_string(), value));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Track the creation order of this group's links, and index it, as
|
/// Track the creation order of this group's links and attributes, and
|
||||||
/// h5py's `track_order=True` does: libhdf5 (and h5py) then list the
|
/// index it, as h5py's `track_order=True` does: libhdf5 (and h5py) then
|
||||||
/// group's members in the order they were added rather than by name.
|
/// list the group's members, and its attributes, in the order they were
|
||||||
/// Applies to links only, not to attributes.
|
/// added rather than by name. libhdf5 numbers at most 65 535 attributes
|
||||||
|
/// on an object that tracks their order.
|
||||||
pub fn track_order(&mut self, track: bool) -> &mut Self {
|
pub fn track_order(&mut self, track: bool) -> &mut Self {
|
||||||
self.track_order = Some(track);
|
self.track_order = Some(track);
|
||||||
self
|
self
|
||||||
|
|||||||
@@ -15,11 +15,13 @@
|
|||||||
#[cfg(not(feature = "std"))]
|
#[cfg(not(feature = "std"))]
|
||||||
use alloc::{format, string::String, vec, vec::Vec};
|
use alloc::{format, string::String, vec, vec::Vec};
|
||||||
|
|
||||||
|
use crate::addr::{checked_addr, to_usize};
|
||||||
use crate::data_layout::{DataLayout, VdsMapping, parse_vds_mappings};
|
use crate::data_layout::{DataLayout, VdsMapping, parse_vds_mappings};
|
||||||
use crate::dataspace::Dataspace;
|
use crate::dataspace::Dataspace;
|
||||||
use crate::datatype::Datatype;
|
use crate::datatype::Datatype;
|
||||||
use crate::error::FormatError;
|
use crate::error::FormatError;
|
||||||
use crate::selection::{SerializedSelection, UNLIMITED};
|
use crate::selection::{SerializedSelection, UNLIMITED};
|
||||||
|
use crate::storage::Storage;
|
||||||
|
|
||||||
/// Resolves the name of an external VDS source file, as stored in the
|
/// Resolves the name of an external VDS source file, as stored in the
|
||||||
/// mapping, to that file's bytes.
|
/// mapping, to that file's bytes.
|
||||||
@@ -191,8 +193,8 @@ fn non_unlimited_elements(sel: &SerializedSelection, skip: usize) -> Option<u64>
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Load and decode the mapping list of a virtual layout.
|
/// Load and decode the mapping list of a virtual layout.
|
||||||
fn load_mappings(
|
fn load_mappings<S: Storage + ?Sized>(
|
||||||
file_data: &[u8],
|
file_data: &S,
|
||||||
layout: &DataLayout,
|
layout: &DataLayout,
|
||||||
length_size: u8,
|
length_size: u8,
|
||||||
) -> Result<Vec<Mapping>, FormatError> {
|
) -> Result<Vec<Mapping>, FormatError> {
|
||||||
@@ -207,8 +209,11 @@ fn load_mappings(
|
|||||||
let Some(addr) = *global_heap_address else {
|
let Some(addr) = *global_heap_address else {
|
||||||
return Ok(Vec::new());
|
return Ok(Vec::new());
|
||||||
};
|
};
|
||||||
let coll =
|
let coll = crate::global_heap::GlobalHeapCollection::parse_in(
|
||||||
crate::global_heap::GlobalHeapCollection::parse(file_data, addr as usize, length_size)?;
|
file_data,
|
||||||
|
checked_addr(addr)?,
|
||||||
|
length_size,
|
||||||
|
)?;
|
||||||
let index = u16::try_from(*global_heap_index)
|
let index = u16::try_from(*global_heap_index)
|
||||||
.map_err(|_| vds_err("VDS mapping heap index out of range"))?;
|
.map_err(|_| vds_err("VDS mapping heap index out of range"))?;
|
||||||
let obj = coll
|
let obj = coll
|
||||||
@@ -329,7 +334,11 @@ enum Step {
|
|||||||
/// Work out the extent libhdf5 gives the virtual dataset
|
/// Work out the extent libhdf5 gives the virtual dataset
|
||||||
/// (`H5D__virtual_set_extent_unlim`, default view `H5D_VDS_LAST_AVAILABLE`
|
/// (`H5D__virtual_set_extent_unlim`, default view `H5D_VDS_LAST_AVAILABLE`
|
||||||
/// with a printf gap of 0) and how much of each unlimited mapping is read.
|
/// with a printf gap of 0) and how much of each unlimited mapping is read.
|
||||||
fn plan(mappings: &[Mapping], stored: &[u64], sources: &mut Sources) -> Result<Plan, FormatError> {
|
fn plan<S: Storage + ?Sized>(
|
||||||
|
mappings: &[Mapping],
|
||||||
|
stored: &[u64],
|
||||||
|
sources: &mut Sources<'_, '_, S>,
|
||||||
|
) -> Result<Plan, FormatError> {
|
||||||
let overflow = || FormatError::Overflow("VDS extent overflow".into());
|
let overflow = || FormatError::Overflow("VDS extent overflow".into());
|
||||||
let rank = stored.len();
|
let rank = stored.len();
|
||||||
let mut new_dims: Vec<Option<u64>> = vec![None; rank];
|
let mut new_dims: Vec<Option<u64>> = vec![None; rank];
|
||||||
@@ -471,6 +480,25 @@ pub fn virtual_dataset_extent(
|
|||||||
_offset_size: u8,
|
_offset_size: u8,
|
||||||
length_size: u8,
|
length_size: u8,
|
||||||
resolver: Option<&VdsFileResolver>,
|
resolver: Option<&VdsFileResolver>,
|
||||||
|
) -> Result<Vec<u64>, FormatError> {
|
||||||
|
virtual_dataset_extent_in(
|
||||||
|
file_data,
|
||||||
|
layout,
|
||||||
|
dataspace,
|
||||||
|
_offset_size,
|
||||||
|
length_size,
|
||||||
|
resolver,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`virtual_dataset_extent`] over any [`Storage`].
|
||||||
|
pub fn virtual_dataset_extent_in<S: Storage + ?Sized>(
|
||||||
|
file_data: &S,
|
||||||
|
layout: &DataLayout,
|
||||||
|
dataspace: &Dataspace,
|
||||||
|
_offset_size: u8,
|
||||||
|
length_size: u8,
|
||||||
|
resolver: Option<&VdsFileResolver>,
|
||||||
) -> Result<Vec<u64>, FormatError> {
|
) -> Result<Vec<u64>, FormatError> {
|
||||||
let mappings = load_mappings(file_data, layout, length_size)?;
|
let mappings = load_mappings(file_data, layout, length_size)?;
|
||||||
if mappings.iter().all(|m| m.kind == Kind::Fixed) {
|
if mappings.iter().all(|m| m.kind == Kind::Fixed) {
|
||||||
@@ -497,6 +525,30 @@ pub fn read_virtual_dataset(
|
|||||||
_offset_size: u8,
|
_offset_size: u8,
|
||||||
length_size: u8,
|
length_size: u8,
|
||||||
resolver: Option<&VdsFileResolver>,
|
resolver: Option<&VdsFileResolver>,
|
||||||
|
) -> Result<VirtualData, FormatError> {
|
||||||
|
read_virtual_dataset_in(
|
||||||
|
file_data,
|
||||||
|
layout,
|
||||||
|
dataspace,
|
||||||
|
datatype,
|
||||||
|
fill,
|
||||||
|
_offset_size,
|
||||||
|
length_size,
|
||||||
|
resolver,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`read_virtual_dataset`] over any [`Storage`].
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
pub fn read_virtual_dataset_in<S: Storage + ?Sized>(
|
||||||
|
file_data: &S,
|
||||||
|
layout: &DataLayout,
|
||||||
|
dataspace: &Dataspace,
|
||||||
|
datatype: &Datatype,
|
||||||
|
fill: Option<&[u8]>,
|
||||||
|
_offset_size: u8,
|
||||||
|
length_size: u8,
|
||||||
|
resolver: Option<&VdsFileResolver>,
|
||||||
) -> Result<VirtualData, FormatError> {
|
) -> Result<VirtualData, FormatError> {
|
||||||
let mappings = load_mappings(file_data, layout, length_size)?;
|
let mappings = load_mappings(file_data, layout, length_size)?;
|
||||||
let mut sources = Sources::new(file_data, resolver);
|
let mut sources = Sources::new(file_data, resolver);
|
||||||
@@ -510,7 +562,7 @@ pub fn read_virtual_dataset(
|
|||||||
let mut data = crate::chunked_read::alloc_output(crate::chunked_read::checked_byte_len(
|
let mut data = crate::chunked_read::alloc_output(crate::chunked_read::checked_byte_len(
|
||||||
total, elem_size,
|
total, elem_size,
|
||||||
)?)?;
|
)?)?;
|
||||||
if let Some(fill) = fill.filter(|f| f.len() == elem_size && f.iter().any(|&b| b != 0)) {
|
if let Some(fill) = fill.filter(|f| <[u8]>::len(f) == elem_size && f.iter().any(|&b| b != 0)) {
|
||||||
for element in data.chunks_exact_mut(elem_size) {
|
for element in data.chunks_exact_mut(elem_size) {
|
||||||
element.copy_from_slice(fill);
|
element.copy_from_slice(fill);
|
||||||
}
|
}
|
||||||
@@ -611,12 +663,12 @@ fn scatter(
|
|||||||
return Err(vds_err("virtual/source selection element counts differ"));
|
return Err(vds_err("virtual/source selection element counts differ"));
|
||||||
}
|
}
|
||||||
for (&v, &s) in vidx.iter().zip(sidx) {
|
for (&v, &s) in vidx.iter().zip(sidx) {
|
||||||
let (vo, so) = (v as usize * elem_size, s as usize * elem_size);
|
let (vo, so) = (to_usize(v)? * elem_size, to_usize(s)? * elem_size);
|
||||||
if vo + elem_size > out.len() || so + elem_size > src.len() {
|
if vo + elem_size > out.len() || so + elem_size > src.len() {
|
||||||
return Err(vds_err("virtual dataset selection out of bounds"));
|
return Err(vds_err("virtual dataset selection out of bounds"));
|
||||||
}
|
}
|
||||||
out[vo..vo + elem_size].copy_from_slice(&src[so..so + elem_size]);
|
out[vo..vo + elem_size].copy_from_slice(&src[so..so + elem_size]);
|
||||||
mapped[v as usize] = true;
|
mapped[to_usize(v)?] = true;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -747,7 +799,7 @@ fn selection_indices(
|
|||||||
return Err(vds_err("VDS selection blocks overlap"));
|
return Err(vds_err("VDS selection blocks overlap"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let mut out = Vec::with_capacity(volume as usize);
|
let mut out = Vec::with_capacity(to_usize(volume)?);
|
||||||
for (s, e) in starts.chunks_exact(rank).zip(ends.chunks_exact(rank)) {
|
for (s, e) in starts.chunks_exact(rank).zip(ends.chunks_exact(rank)) {
|
||||||
let mut cur = s.to_vec();
|
let mut cur = s.to_vec();
|
||||||
'block: loop {
|
'block: loop {
|
||||||
@@ -779,14 +831,17 @@ struct SourceData {
|
|||||||
|
|
||||||
/// Source files and datasets, fetched on demand. The most recently used
|
/// Source files and datasets, fetched on demand. The most recently used
|
||||||
/// external file is kept, since consecutive mappings usually share one.
|
/// external file is kept, since consecutive mappings usually share one.
|
||||||
struct Sources<'a, 'r> {
|
///
|
||||||
file_data: &'a [u8],
|
/// The virtual dataset's own file (`"."`) is read through its [`Storage`];
|
||||||
|
/// an external source file is loaded whole, through the resolver.
|
||||||
|
struct Sources<'a, 'r, S: Storage + ?Sized> {
|
||||||
|
file_data: &'a S,
|
||||||
resolver: Option<&'r VdsFileResolver<'r>>,
|
resolver: Option<&'r VdsFileResolver<'r>>,
|
||||||
cached_file: Option<(String, Option<Vec<u8>>)>,
|
cached_file: Option<(String, Option<Vec<u8>>)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, 'r> Sources<'a, 'r> {
|
impl<'a, 'r, S: Storage + ?Sized> Sources<'a, 'r, S> {
|
||||||
fn new(file_data: &'a [u8], resolver: Option<&'r VdsFileResolver<'r>>) -> Self {
|
fn new(file_data: &'a S, resolver: Option<&'r VdsFileResolver<'r>>) -> Self {
|
||||||
Sources {
|
Sources {
|
||||||
file_data,
|
file_data,
|
||||||
resolver,
|
resolver,
|
||||||
@@ -794,16 +849,18 @@ impl<'a, 'r> Sources<'a, 'r> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The bytes of source file `name`, or `None` if it does not exist.
|
/// The bytes of external source file `name` (not `"."`), or `None` if it
|
||||||
fn file(&mut self, name: &str) -> Result<Option<&[u8]>, FormatError> {
|
/// does not exist.
|
||||||
if name == "." {
|
fn external(&mut self, name: &str) -> Result<Option<&[u8]>, FormatError> {
|
||||||
return Ok(Some(self.file_data));
|
|
||||||
}
|
|
||||||
if self.cached_file.as_ref().is_none_or(|(n, _)| n != name) {
|
if self.cached_file.as_ref().is_none_or(|(n, _)| n != name) {
|
||||||
let resolver = self.resolver.ok_or_else(|| {
|
let resolver = self.resolver.ok_or_else(|| {
|
||||||
vds_err("external-file virtual dataset sources require a file resolver")
|
vds_err("external-file virtual dataset sources require a file resolver")
|
||||||
})?;
|
})?;
|
||||||
self.cached_file = Some((String::from(name), resolver(name)?));
|
let mut bytes = resolver(name)?;
|
||||||
|
if let Some(b) = bytes.as_mut() {
|
||||||
|
load_source_file(b)?;
|
||||||
|
}
|
||||||
|
self.cached_file = Some((String::from(name), bytes));
|
||||||
}
|
}
|
||||||
// An external file is handed over whole; its addresses are relative
|
// An external file is handed over whole; its addresses are relative
|
||||||
// to its superblock, so skip any user block.
|
// to its superblock, so skip any user block.
|
||||||
@@ -816,7 +873,10 @@ impl<'a, 'r> Sources<'a, 'r> {
|
|||||||
/// The extent of source dataset `path` in file `file`, or `None` when
|
/// The extent of source dataset `path` in file `file`, or `None` when
|
||||||
/// either does not exist.
|
/// either does not exist.
|
||||||
fn dims(&mut self, file: &str, path: &str) -> Result<Option<Vec<u64>>, FormatError> {
|
fn dims(&mut self, file: &str, path: &str) -> Result<Option<Vec<u64>>, FormatError> {
|
||||||
let Some(bytes) = self.file(file)? else {
|
if file == "." {
|
||||||
|
return Ok(open_source(self.file_data, path)?.map(|s| s.dataspace.dimensions));
|
||||||
|
}
|
||||||
|
let Some(bytes) = self.external(file)? else {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
};
|
};
|
||||||
Ok(open_source(bytes, path)?.map(|s| s.dataspace.dimensions))
|
Ok(open_source(bytes, path)?.map(|s| s.dataspace.dimensions))
|
||||||
@@ -841,7 +901,13 @@ impl<'a, 'r> Sources<'a, 'r> {
|
|||||||
from another file is not supported"
|
from another file is not supported"
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
let Some(bytes) = self.file(file)? else {
|
if file == "." {
|
||||||
|
let Some(src) = open_source(self.file_data, path)? else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
return read_source(self.file_data, src, path, datatype).map(Some);
|
||||||
|
}
|
||||||
|
let Some(bytes) = self.external(file)? else {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
};
|
};
|
||||||
let Some(src) = open_source(bytes, path)? else {
|
let Some(src) = open_source(bytes, path)? else {
|
||||||
@@ -851,6 +917,23 @@ impl<'a, 'r> Sources<'a, 'r> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Check an external source file's superblock extension as libhdf5 does
|
||||||
|
/// when it opens the file, and write any metadata cache image over its
|
||||||
|
/// metadata in place: libhdf5 reads the image's entries instead of the
|
||||||
|
/// file's own, possibly stale, bytes (`crate::superblock_ext`). A source
|
||||||
|
/// file whose image cannot be loaded is an error, as other corrupt source
|
||||||
|
/// files are here.
|
||||||
|
fn load_source_file(whole: &mut [u8]) -> Result<(), FormatError> {
|
||||||
|
let base = crate::signature::find_signature(whole)?;
|
||||||
|
let sb = crate::superblock::Superblock::parse(&whole[base..], 0)?;
|
||||||
|
// The end of file the superblock records; a truncated source file is
|
||||||
|
// read as before, up to its length.
|
||||||
|
let end = sb
|
||||||
|
.data_end(base as u64, whole.len() as u64)
|
||||||
|
.map_or(Ok(whole.len()), |e| to_usize(e).map(|e| base + e))?;
|
||||||
|
crate::superblock_ext::apply_cache_image_in_place(&mut whole[base..end], &sb)
|
||||||
|
}
|
||||||
|
|
||||||
/// Whether elements of `dt` contain addresses into their own file:
|
/// Whether elements of `dt` contain addresses into their own file:
|
||||||
/// variable-length data (global-heap IDs) or references.
|
/// variable-length data (global-heap IDs) or references.
|
||||||
fn holds_file_addresses(dt: &Datatype) -> bool {
|
fn holds_file_addresses(dt: &Datatype) -> bool {
|
||||||
@@ -888,19 +971,23 @@ fn source_message<'h>(
|
|||||||
|
|
||||||
/// Open source dataset `path` of the file in `file_data`, or `None` if there
|
/// Open source dataset `path` of the file in `file_data`, or `None` if there
|
||||||
/// is no such object (libhdf5 reads a missing source as fill).
|
/// is no such object (libhdf5 reads a missing source as fill).
|
||||||
fn open_source(file_data: &[u8], path: &str) -> Result<Option<OpenSource>, FormatError> {
|
fn open_source<S: Storage + ?Sized>(
|
||||||
|
file_data: &S,
|
||||||
|
path: &str,
|
||||||
|
) -> Result<Option<OpenSource>, FormatError> {
|
||||||
use crate::message_type::MessageType;
|
use crate::message_type::MessageType;
|
||||||
use crate::shared_message::message_data_with_sohm;
|
use crate::shared_message::message_data_with_sohm_in as message_data_with_sohm;
|
||||||
|
|
||||||
// `file_data` starts at the superblock (see `Sources::file`).
|
// `file_data` starts at the superblock (see `Sources::external`).
|
||||||
let sb = crate::superblock::Superblock::parse(file_data, 0)?;
|
let sb = crate::superblock::Superblock::parse_in(file_data, 0)?;
|
||||||
let (os, ls) = (sb.offset_size, sb.length_size);
|
let (os, ls) = (sb.offset_size, sb.length_size);
|
||||||
let addr = match crate::group_v2::resolve_path_any(file_data, &sb, path) {
|
let addr = match crate::group_v2::resolve_path_any_in(file_data, &sb, path) {
|
||||||
Ok(a) => a,
|
Ok(a) => a,
|
||||||
Err(FormatError::PathNotFound(_)) => return Ok(None),
|
Err(FormatError::PathNotFound(_)) => return Ok(None),
|
||||||
Err(e) => return Err(e),
|
Err(e) => return Err(e),
|
||||||
};
|
};
|
||||||
let header = crate::object_header::ObjectHeader::parse(file_data, addr as usize, os, ls)?;
|
let header =
|
||||||
|
crate::object_header::ObjectHeader::parse_in(file_data, checked_addr(addr)?, os, ls)?;
|
||||||
let mut src = OpenSource {
|
let mut src = OpenSource {
|
||||||
offset_size: os,
|
offset_size: os,
|
||||||
length_size: ls,
|
length_size: ls,
|
||||||
@@ -919,15 +1006,15 @@ fn open_source(file_data: &[u8], path: &str) -> Result<Option<OpenSource>, Forma
|
|||||||
|
|
||||||
/// Read an opened source dataset in full (its own fill value applied to
|
/// Read an opened source dataset in full (its own fill value applied to
|
||||||
/// unallocated chunks).
|
/// unallocated chunks).
|
||||||
fn read_source(
|
fn read_source<S: Storage + ?Sized>(
|
||||||
file_data: &[u8],
|
file_data: &S,
|
||||||
src: OpenSource,
|
src: OpenSource,
|
||||||
path: &str,
|
path: &str,
|
||||||
datatype: &Datatype,
|
datatype: &Datatype,
|
||||||
) -> Result<SourceData, FormatError> {
|
) -> Result<SourceData, FormatError> {
|
||||||
use crate::filter_pipeline::FilterPipeline;
|
use crate::filter_pipeline::FilterPipeline;
|
||||||
use crate::message_type::MessageType;
|
use crate::message_type::MessageType;
|
||||||
use crate::shared_message::message_data_with_sohm;
|
use crate::shared_message::message_data_with_sohm_in as message_data_with_sohm;
|
||||||
|
|
||||||
let (os, ls) = (src.offset_size, src.length_size);
|
let (os, ls) = (src.offset_size, src.length_size);
|
||||||
let dt_msg = source_message(&src, path, MessageType::Datatype)?;
|
let dt_msg = source_message(&src, path, MessageType::Datatype)?;
|
||||||
@@ -965,7 +1052,7 @@ fn read_source(
|
|||||||
message_data_with_sohm(file_data, m, os, ls).and_then(|d| FilterPipeline::parse(&d))
|
message_data_with_sohm(file_data, m, os, ls).and_then(|d| FilterPipeline::parse(&d))
|
||||||
})
|
})
|
||||||
.transpose()?;
|
.transpose()?;
|
||||||
let raw = crate::fill_value::read_full_with_fill(
|
let raw = crate::fill_value::read_full_with_fill_in(
|
||||||
&src.header.messages,
|
&src.header.messages,
|
||||||
file_data,
|
file_data,
|
||||||
&layout,
|
&layout,
|
||||||
@@ -974,7 +1061,7 @@ fn read_source(
|
|||||||
os,
|
os,
|
||||||
ls,
|
ls,
|
||||||
|| {
|
|| {
|
||||||
crate::data_read::read_raw_data_full(
|
crate::data_read::read_raw_data_full_in(
|
||||||
file_data,
|
file_data,
|
||||||
&layout,
|
&layout,
|
||||||
&src.dataspace,
|
&src.dataspace,
|
||||||
|
|||||||
@@ -5,10 +5,11 @@
|
|||||||
//! `sequence_length(4 LE) + collection_address(offset_size LE) + object_index(4 LE)`.
|
//! `sequence_length(4 LE) + collection_address(offset_size LE) + object_index(4 LE)`.
|
||||||
|
|
||||||
#[cfg(not(feature = "std"))]
|
#[cfg(not(feature = "std"))]
|
||||||
use alloc::{collections::BTreeMap, format, string::String, vec, vec::Vec};
|
use alloc::{borrow::Cow, collections::BTreeMap, format, string::String, vec, vec::Vec};
|
||||||
#[cfg(feature = "std")]
|
#[cfg(feature = "std")]
|
||||||
use std::collections::BTreeMap;
|
use std::{borrow::Cow, collections::BTreeMap};
|
||||||
|
|
||||||
|
use crate::addr::to_usize;
|
||||||
use crate::error::FormatError;
|
use crate::error::FormatError;
|
||||||
use crate::global_heap::{GlobalHeapCollection, GlobalHeapIndex};
|
use crate::global_heap::{GlobalHeapCollection, GlobalHeapIndex};
|
||||||
|
|
||||||
@@ -55,7 +56,7 @@ pub fn parse_vl_references(
|
|||||||
) -> Result<Vec<VlElement>, FormatError> {
|
) -> Result<Vec<VlElement>, FormatError> {
|
||||||
let elem_size = 4 + offset_size as usize + 4; // length + address + index
|
let elem_size = 4 + offset_size as usize + 4; // length + address + index
|
||||||
let total =
|
let total =
|
||||||
(num_elements as usize)
|
to_usize(num_elements)?
|
||||||
.checked_mul(elem_size)
|
.checked_mul(elem_size)
|
||||||
.ok_or(FormatError::UnexpectedEof {
|
.ok_or(FormatError::UnexpectedEof {
|
||||||
expected: usize::MAX,
|
expected: usize::MAX,
|
||||||
@@ -68,7 +69,7 @@ pub fn parse_vl_references(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut elements = Vec::with_capacity(num_elements as usize);
|
let mut elements = Vec::with_capacity(to_usize(num_elements)?);
|
||||||
let mut pos = 0;
|
let mut pos = 0;
|
||||||
|
|
||||||
for _ in 0..num_elements {
|
for _ in 0..num_elements {
|
||||||
@@ -136,13 +137,15 @@ pub fn check_element_size(stored_size: u32, offset_size: u8) -> Result<(), Forma
|
|||||||
|
|
||||||
/// A collection's objects, located in the file data but not copied:
|
/// A collection's objects, located in the file data but not copied:
|
||||||
/// `(index, offset, size)` of the first object with each index, sorted by
|
/// `(index, offset, size)` of the first object with each index, sorted by
|
||||||
/// index.
|
/// index. Over a storage without the whole file in memory, also the
|
||||||
struct CachedCollection {
|
/// collection's bytes (`(offset, bytes)`), read once when it is indexed.
|
||||||
|
struct CachedCollection<'a> {
|
||||||
objects: Vec<(u16, usize, usize)>,
|
objects: Vec<(u16, usize, usize)>,
|
||||||
|
bytes: Option<(usize, Cow<'a, [u8]>)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CachedCollection {
|
impl<'a> CachedCollection<'a> {
|
||||||
fn new(index: GlobalHeapIndex) -> Self {
|
fn new(index: GlobalHeapIndex, bytes: Option<(usize, Cow<'a, [u8]>)>) -> Self {
|
||||||
let mut objects: Vec<(u16, usize, usize)> = index
|
let mut objects: Vec<(u16, usize, usize)> = index
|
||||||
.objects
|
.objects
|
||||||
.iter()
|
.iter()
|
||||||
@@ -151,12 +154,16 @@ impl CachedCollection {
|
|||||||
// Stable, so the first object with a repeated index is kept.
|
// Stable, so the first object with a repeated index is kept.
|
||||||
objects.sort_by_key(|o| o.0);
|
objects.sort_by_key(|o| o.0);
|
||||||
objects.dedup_by_key(|o| o.0);
|
objects.dedup_by_key(|o| o.0);
|
||||||
Self { objects }
|
Self { objects, bytes }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// What this entry costs to keep, in bytes (roughly).
|
/// What this entry costs to keep, in bytes (roughly).
|
||||||
fn cost(&self) -> usize {
|
fn cost(&self) -> usize {
|
||||||
64 + self.objects.len() * core::mem::size_of::<(u16, usize, usize)>()
|
let held = match &self.bytes {
|
||||||
|
Some((_, Cow::Owned(b))) => b.len(),
|
||||||
|
_ => 0,
|
||||||
|
};
|
||||||
|
64 + self.objects.len() * core::mem::size_of::<(u16, usize, usize)>() + held
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get(&self, index: u32) -> Option<(usize, usize)> {
|
fn get(&self, index: u32) -> Option<(usize, usize)> {
|
||||||
@@ -169,6 +176,8 @@ impl CachedCollection {
|
|||||||
/// How many bytes of collection indexes a [`VlResolver`] keeps before it
|
/// How many bytes of collection indexes a [`VlResolver`] keeps before it
|
||||||
/// drops them and starts again. Values are never copied into the cache, so
|
/// drops them and starts again. Values are never copied into the cache, so
|
||||||
/// this bounds what a read retains however many collections it visits.
|
/// this bounds what a read retains however many collections it visits.
|
||||||
|
/// (Over a storage without the whole file in memory the collections' bytes
|
||||||
|
/// are kept too, and count against this.)
|
||||||
const CACHE_BUDGET: usize = 32 << 20;
|
const CACHE_BUDGET: usize = 32 << 20;
|
||||||
|
|
||||||
/// Resolves variable-length elements against a file's global heap, parsing
|
/// Resolves variable-length elements against a file's global heap, parsing
|
||||||
@@ -184,11 +193,18 @@ const CACHE_BUDGET: usize = 32 << 20;
|
|||||||
/// that overlap one another are refused (libhdf5 never writes them), so a
|
/// that overlap one another are refused (libhdf5 never writes them), so a
|
||||||
/// file cannot make the resolver parse the same bytes as the objects of
|
/// file cannot make the resolver parse the same bytes as the objects of
|
||||||
/// many collections.
|
/// many collections.
|
||||||
pub struct VlResolver<'a> {
|
///
|
||||||
file_data: &'a [u8],
|
/// The file is any [`Storage`](crate::storage::Storage) (`S`, a slice by default). Over one without
|
||||||
|
/// the whole file in memory each collection is read once, when first used,
|
||||||
|
/// and kept (within the budget above); [`Self::strings`],
|
||||||
|
/// [`Self::string_bytes`] and [`Self::sequences`] work over any storage,
|
||||||
|
/// [`Self::element`] and [`Self::string_element`], which borrow from the
|
||||||
|
/// file, over a slice.
|
||||||
|
pub struct VlResolver<'a, S: crate::storage::Storage + ?Sized = [u8]> {
|
||||||
|
file_data: &'a S,
|
||||||
offset_size: u8,
|
offset_size: u8,
|
||||||
length_size: u8,
|
length_size: u8,
|
||||||
cache: BTreeMap<u64, CachedCollection>,
|
cache: BTreeMap<u64, CachedCollection<'a>>,
|
||||||
cached_bytes: usize,
|
cached_bytes: usize,
|
||||||
budget: usize,
|
budget: usize,
|
||||||
/// Start → end of every collection parsed so far (kept when the cache
|
/// Start → end of every collection parsed so far (kept when the cache
|
||||||
@@ -200,6 +216,56 @@ impl<'a> VlResolver<'a> {
|
|||||||
/// A resolver over `file_data` (the file from its superblock on), with
|
/// A resolver over `file_data` (the file from its superblock on), with
|
||||||
/// the superblock's offset and length sizes.
|
/// the superblock's offset and length sizes.
|
||||||
pub fn new(file_data: &'a [u8], offset_size: u8, length_size: u8) -> Self {
|
pub fn new(file_data: &'a [u8], offset_size: u8, length_size: u8) -> Self {
|
||||||
|
Self::new_in(file_data, offset_size, length_size)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One element (the first [`element_size`](Self::element_size) bytes of
|
||||||
|
/// `elem`) of a variable-length sequence whose base type is `base_size`
|
||||||
|
/// bytes: its `length × base_size` bytes, or `None` for a null element
|
||||||
|
/// (heap address 0).
|
||||||
|
pub fn element(
|
||||||
|
&mut self,
|
||||||
|
elem: &[u8],
|
||||||
|
base_size: usize,
|
||||||
|
) -> Result<Option<&'a [u8]>, FormatError> {
|
||||||
|
let vl = parse_vl_references(elem, 1, self.offset_size)?;
|
||||||
|
let vl = &vl[0];
|
||||||
|
if vl.collection_address == 0 {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
let (start, size) = self.locate(vl)?;
|
||||||
|
let data = &self.file_data[start..start + size];
|
||||||
|
check_object_size(vl, data.len(), base_size)?;
|
||||||
|
Ok(Some(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One variable-length string element: its bytes up to the first NUL,
|
||||||
|
/// or `None` for a null element (h5dump prints it as `NULL`, h5py
|
||||||
|
/// returns it as empty).
|
||||||
|
pub fn string_element(&mut self, elem: &[u8]) -> Result<Option<&'a [u8]>, FormatError> {
|
||||||
|
Ok(self.element(elem, 1)?.map(cut_at_nul))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `data_len`, the size of `vl`'s heap object, against the `length ×
|
||||||
|
/// base_size` bytes the element says it holds.
|
||||||
|
fn check_object_size(vl: &VlElement, data_len: usize, base_size: usize) -> Result<(), FormatError> {
|
||||||
|
let expected = (vl.length as usize)
|
||||||
|
.checked_mul(base_size)
|
||||||
|
.ok_or_else(|| FormatError::Overflow("variable-length element size".into()))?;
|
||||||
|
if data_len != expected {
|
||||||
|
return Err(FormatError::VlDataError(format!(
|
||||||
|
"global heap object {} in the collection at {} holds {data_len} bytes; the element \
|
||||||
|
says {} × {base_size}",
|
||||||
|
vl.object_index, vl.collection_address, vl.length
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, S: crate::storage::Storage + ?Sized> VlResolver<'a, S> {
|
||||||
|
/// [`VlResolver::new`] over any [`Storage`](crate::storage::Storage).
|
||||||
|
pub fn new_in(file_data: &'a S, offset_size: u8, length_size: u8) -> Self {
|
||||||
Self {
|
Self {
|
||||||
file_data,
|
file_data,
|
||||||
offset_size,
|
offset_size,
|
||||||
@@ -230,49 +296,31 @@ impl<'a> VlResolver<'a> {
|
|||||||
|
|
||||||
/// The bytes of one element: `length × base_size` bytes from the heap,
|
/// The bytes of one element: `length × base_size` bytes from the heap,
|
||||||
/// or `None` for a null element.
|
/// or `None` for a null element.
|
||||||
fn resolve(
|
fn resolve(&mut self, vl: &VlElement, base_size: usize) -> Result<Option<&[u8]>, FormatError> {
|
||||||
&mut self,
|
if vl.collection_address == 0 {
|
||||||
vl: &VlElement,
|
|
||||||
base_size: usize,
|
|
||||||
) -> Result<Option<&'a [u8]>, FormatError> {
|
|
||||||
let addr = vl.collection_address;
|
|
||||||
if addr == 0 {
|
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
let data = self.object(vl)?;
|
let data = self.object(vl)?;
|
||||||
let expected = (vl.length as usize)
|
check_object_size(vl, data.len(), base_size)?;
|
||||||
.checked_mul(base_size)
|
|
||||||
.ok_or_else(|| FormatError::Overflow("variable-length element size".into()))?;
|
|
||||||
if data.len() != expected {
|
|
||||||
return Err(FormatError::VlDataError(format!(
|
|
||||||
"global heap object {} in the collection at {addr} holds {} bytes; the element \
|
|
||||||
says {} × {base_size}",
|
|
||||||
vl.object_index,
|
|
||||||
data.len(),
|
|
||||||
vl.length
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
Ok(Some(data))
|
Ok(Some(data))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// One element (the first [`element_size`](Self::element_size) bytes of
|
/// [`VlResolver::element`] over any storage: the element's bytes
|
||||||
/// `elem`) of a variable-length sequence whose base type is `base_size`
|
/// (borrowed from the resolver's cache of heap collections, so they
|
||||||
/// bytes: its `length × base_size` bytes, or `None` for a null element
|
/// live until the next call), or `None` for a null element.
|
||||||
/// (heap address 0).
|
pub fn element_in(
|
||||||
pub fn element(
|
|
||||||
&mut self,
|
&mut self,
|
||||||
elem: &[u8],
|
elem: &[u8],
|
||||||
base_size: usize,
|
base_size: usize,
|
||||||
) -> Result<Option<&'a [u8]>, FormatError> {
|
) -> Result<Option<&[u8]>, FormatError> {
|
||||||
let vl = parse_vl_references(elem, 1, self.offset_size)?;
|
let vl = parse_vl_references(elem, 1, self.offset_size)?;
|
||||||
self.resolve(&vl[0], base_size)
|
self.resolve(&vl[0], base_size)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// One variable-length string element: its bytes up to the first NUL,
|
/// [`VlResolver::string_element`] over any storage (see
|
||||||
/// or `None` for a null element (h5dump prints it as `NULL`, h5py
|
/// [`element_in`](Self::element_in)).
|
||||||
/// returns it as empty).
|
pub fn string_element_in(&mut self, elem: &[u8]) -> Result<Option<&[u8]>, FormatError> {
|
||||||
pub fn string_element(&mut self, elem: &[u8]) -> Result<Option<&'a [u8]>, FormatError> {
|
Ok(self.element_in(elem, 1)?.map(cut_at_nul))
|
||||||
Ok(self.element(elem, 1)?.map(cut_at_nul))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The strings of the variable-length string elements in `raw`, as
|
/// The strings of the variable-length string elements in `raw`, as
|
||||||
@@ -329,9 +377,20 @@ pub fn read_vl_strings(
|
|||||||
num_elements: u64,
|
num_elements: u64,
|
||||||
offset_size: u8,
|
offset_size: u8,
|
||||||
length_size: u8,
|
length_size: u8,
|
||||||
|
) -> Result<Vec<String>, FormatError> {
|
||||||
|
read_vl_strings_in(file_data, raw_data, num_elements, offset_size, length_size)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`read_vl_strings`] over any [`Storage`](crate::storage::Storage).
|
||||||
|
pub fn read_vl_strings_in<S: crate::storage::Storage + ?Sized>(
|
||||||
|
file_data: &S,
|
||||||
|
raw_data: &[u8],
|
||||||
|
num_elements: u64,
|
||||||
|
offset_size: u8,
|
||||||
|
length_size: u8,
|
||||||
) -> Result<Vec<String>, FormatError> {
|
) -> Result<Vec<String>, FormatError> {
|
||||||
let raw = first_elements(raw_data, num_elements, offset_size)?;
|
let raw = first_elements(raw_data, num_elements, offset_size)?;
|
||||||
VlResolver::new(file_data, offset_size, length_size).strings(raw)
|
VlResolver::new_in(file_data, offset_size, length_size).strings(raw)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The first `num_elements` elements of `raw`, or an error if it is shorter.
|
/// The first `num_elements` elements of `raw`, or an error if it is shorter.
|
||||||
@@ -363,9 +422,20 @@ pub fn read_vl_bytes(
|
|||||||
num_elements: u64,
|
num_elements: u64,
|
||||||
offset_size: u8,
|
offset_size: u8,
|
||||||
length_size: u8,
|
length_size: u8,
|
||||||
|
) -> Result<Vec<Vec<u8>>, FormatError> {
|
||||||
|
read_vl_bytes_in(file_data, raw_data, num_elements, offset_size, length_size)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`read_vl_bytes`] over any [`Storage`](crate::storage::Storage).
|
||||||
|
pub fn read_vl_bytes_in<S: crate::storage::Storage + ?Sized>(
|
||||||
|
file_data: &S,
|
||||||
|
raw_data: &[u8],
|
||||||
|
num_elements: u64,
|
||||||
|
offset_size: u8,
|
||||||
|
length_size: u8,
|
||||||
) -> Result<Vec<Vec<u8>>, FormatError> {
|
) -> Result<Vec<Vec<u8>>, FormatError> {
|
||||||
let refs = parse_vl_references(raw_data, num_elements, offset_size)?;
|
let refs = parse_vl_references(raw_data, num_elements, offset_size)?;
|
||||||
let mut resolver = VlResolver::new(file_data, offset_size, length_size);
|
let mut resolver = VlResolver::new_in(file_data, offset_size, length_size);
|
||||||
let mut result = Vec::with_capacity(refs.len());
|
let mut result = Vec::with_capacity(refs.len());
|
||||||
|
|
||||||
for vl in &refs {
|
for vl in &refs {
|
||||||
@@ -384,10 +454,10 @@ pub fn read_vl_bytes(
|
|||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> VlResolver<'a> {
|
impl<'a, S: crate::storage::Storage + ?Sized> VlResolver<'a, S> {
|
||||||
/// The heap object `vl` points to, whatever its size; its collection is
|
/// Where the heap object `vl` points to lies in the file, whatever its
|
||||||
/// parsed on first use.
|
/// size (`(offset, size)`); its collection is parsed on first use.
|
||||||
fn object(&mut self, vl: &VlElement) -> Result<&'a [u8], FormatError> {
|
fn locate(&mut self, vl: &VlElement) -> Result<(usize, usize), FormatError> {
|
||||||
let addr = vl.collection_address;
|
let addr = vl.collection_address;
|
||||||
// libhdf5 writes a null element with address 0, never the undefined
|
// libhdf5 writes a null element with address 0, never the undefined
|
||||||
// address, and fails to read one ("addr undefined") even when its
|
// address, and fails to read one ("addr undefined") even when its
|
||||||
@@ -401,14 +471,20 @@ impl<'a> VlResolver<'a> {
|
|||||||
if !self.cache.contains_key(&addr) {
|
if !self.cache.contains_key(&addr) {
|
||||||
let offset = usize::try_from(addr).map_err(|_| FormatError::UnexpectedEof {
|
let offset = usize::try_from(addr).map_err(|_| FormatError::UnexpectedEof {
|
||||||
expected: usize::MAX,
|
expected: usize::MAX,
|
||||||
available: self.file_data.len(),
|
available: crate::storage::len_usize(self.file_data),
|
||||||
})?;
|
})?;
|
||||||
let index =
|
let (bytes, base, index) =
|
||||||
GlobalHeapCollection::parse_index(self.file_data, offset, self.length_size)?;
|
GlobalHeapCollection::read_collection(self.file_data, addr, self.length_size)?;
|
||||||
// parse_index checked that the collection lies in the file.
|
// read_collection checked that the collection lies in the file.
|
||||||
let end = offset + index.collection_size as usize;
|
let end = offset + to_usize(index.collection_size)?;
|
||||||
self.check_overlap(offset, end)?;
|
self.check_overlap(offset, end)?;
|
||||||
let coll = CachedCollection::new(index);
|
// With the whole file in memory the objects are sliced from it;
|
||||||
|
// otherwise the collection's bytes are kept.
|
||||||
|
let bytes = match self.file_data.as_contiguous() {
|
||||||
|
Some(_) => None,
|
||||||
|
None => Some((base, bytes)),
|
||||||
|
};
|
||||||
|
let coll = CachedCollection::new(index, bytes);
|
||||||
if self.cached_bytes.saturating_add(coll.cost()) > self.budget {
|
if self.cached_bytes.saturating_add(coll.cost()) > self.budget {
|
||||||
self.cache.clear();
|
self.cache.clear();
|
||||||
self.cached_bytes = 0;
|
self.cached_bytes = 0;
|
||||||
@@ -416,13 +492,27 @@ impl<'a> VlResolver<'a> {
|
|||||||
self.cached_bytes += coll.cost();
|
self.cached_bytes += coll.cost();
|
||||||
self.cache.insert(addr, coll);
|
self.cache.insert(addr, coll);
|
||||||
}
|
}
|
||||||
let (start, size) = self.cache[&addr].get(vl.object_index).ok_or(
|
self.cache[&addr]
|
||||||
FormatError::GlobalHeapObjectNotFound {
|
.get(vl.object_index)
|
||||||
|
.ok_or(FormatError::GlobalHeapObjectNotFound {
|
||||||
collection_address: addr,
|
collection_address: addr,
|
||||||
index: vl.object_index as u16,
|
index: vl.object_index as u16,
|
||||||
},
|
})
|
||||||
)?;
|
}
|
||||||
Ok(&self.file_data[start..start + size])
|
|
||||||
|
/// The heap object `vl` points to, whatever its size; its collection is
|
||||||
|
/// parsed on first use.
|
||||||
|
fn object(&mut self, vl: &VlElement) -> Result<&[u8], FormatError> {
|
||||||
|
let (start, size) = self.locate(vl)?;
|
||||||
|
if let Some(all) = self.file_data.as_contiguous() {
|
||||||
|
return Ok(&all[start..start + size]);
|
||||||
|
}
|
||||||
|
match &self.cache[&vl.collection_address].bytes {
|
||||||
|
Some((base, bytes)) => Ok(&bytes[start - base..start - base + size]),
|
||||||
|
None => Err(FormatError::Storage(
|
||||||
|
"global heap collection bytes were not kept".into(),
|
||||||
|
)),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Record the collection at `start..end`, refusing one that overlaps a
|
/// Record the collection at `start..end`, refusing one that overlaps a
|
||||||
@@ -573,6 +663,32 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn element_in_over_a_storage_matches_element_over_a_slice() {
|
||||||
|
let mut file_data = vec![0u8; 512];
|
||||||
|
build_gcol_at(&mut file_data, 256, &[(1, b"Alice\0x"), (2, b"Bob")]);
|
||||||
|
let mut raw = build_vl_refs(&["Alice\0x", "Bob"], 256, 1, 8);
|
||||||
|
raw.extend(element(0, 0, 0, 8)); // null
|
||||||
|
raw.extend(element(9, 256, 1, 8)); // wrong length: an error
|
||||||
|
let storage = crate::storage::CountingStorage::new(file_data.clone());
|
||||||
|
let dynamic: &dyn crate::storage::Storage = &storage;
|
||||||
|
let mut slice = VlResolver::new(&file_data, 8, 8);
|
||||||
|
let mut any = VlResolver::new_in(dynamic, 8, 8);
|
||||||
|
for e in raw.chunks(16) {
|
||||||
|
let want = slice.element(e, 1).map(|o| o.map(<[u8]>::to_vec));
|
||||||
|
let got = any.element_in(e, 1).map(|o| o.map(<[u8]>::to_vec));
|
||||||
|
assert_eq!(format!("{want:?}"), format!("{got:?}"));
|
||||||
|
let want = slice.string_element(e).map(|o| o.map(<[u8]>::to_vec));
|
||||||
|
let got = any.string_element_in(e).map(|o| o.map(<[u8]>::to_vec));
|
||||||
|
assert_eq!(format!("{want:?}"), format!("{got:?}"));
|
||||||
|
}
|
||||||
|
assert_eq!(
|
||||||
|
any.string_element_in(&raw[..16]).unwrap(),
|
||||||
|
Some(&b"Alice"[..])
|
||||||
|
);
|
||||||
|
assert!(storage.reads() > 0);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn null_vl_element_zero_address() {
|
fn null_vl_element_zero_address() {
|
||||||
let mut raw = Vec::new();
|
let mut raw = Vec::new();
|
||||||
@@ -708,6 +824,7 @@ mod tests {
|
|||||||
let mut r = VlResolver::new(&file_data, 8, 8);
|
let mut r = VlResolver::new(&file_data, 8, 8);
|
||||||
let one = CachedCollection {
|
let one = CachedCollection {
|
||||||
objects: vec![(0, 0, 0); 3],
|
objects: vec![(0, 0, 0); 3],
|
||||||
|
bytes: None,
|
||||||
}
|
}
|
||||||
.cost();
|
.cost();
|
||||||
r.budget = 2 * one + 1;
|
r.budget = 2 * one + 1;
|
||||||
|
|||||||
@@ -0,0 +1,641 @@
|
|||||||
|
//! Crafted Blosc2 frames and chunks cannot make the decoder allocate out of
|
||||||
|
//! proportion to the HDF5 chunk it decodes.
|
||||||
|
//!
|
||||||
|
//! A frame's header, its offsets chunk and its chunk headers all declare
|
||||||
|
//! sizes, and the decoder used to allocate what they declared: a 173-byte
|
||||||
|
//! frame whose offsets chunk claimed 2 GiB was decoded in full before any
|
||||||
|
//! check failed. Every allocation is now bounded by the output limit (the
|
||||||
|
//! HDF5 chunk's size) and the input's length.
|
||||||
|
//!
|
||||||
|
//! Peak heap use is measured with a counting global allocator; the tests
|
||||||
|
//! share it, so each holds `SERIAL` for its whole run.
|
||||||
|
#![cfg(feature = "blosc2")]
|
||||||
|
|
||||||
|
use std::alloc::{GlobalAlloc, Layout, System};
|
||||||
|
use std::sync::Mutex;
|
||||||
|
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||||
|
|
||||||
|
use clawhdf5_format::filters_blosc2::{blosc2_decompress, blosc2_decompress_chunk};
|
||||||
|
|
||||||
|
struct Counting;
|
||||||
|
|
||||||
|
static CURRENT: AtomicUsize = AtomicUsize::new(0);
|
||||||
|
static PEAK: AtomicUsize = AtomicUsize::new(0);
|
||||||
|
static SERIAL: Mutex<()> = Mutex::new(());
|
||||||
|
|
||||||
|
unsafe impl GlobalAlloc for Counting {
|
||||||
|
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
|
||||||
|
let p = unsafe { System.alloc(layout) };
|
||||||
|
if !p.is_null() {
|
||||||
|
let now = CURRENT.fetch_add(layout.size(), Ordering::Relaxed) + layout.size();
|
||||||
|
PEAK.fetch_max(now, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
p
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
|
||||||
|
let p = unsafe { System.alloc_zeroed(layout) };
|
||||||
|
if !p.is_null() {
|
||||||
|
let now = CURRENT.fetch_add(layout.size(), Ordering::Relaxed) + layout.size();
|
||||||
|
PEAK.fetch_max(now, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
p
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
|
||||||
|
unsafe { System.dealloc(ptr, layout) };
|
||||||
|
CURRENT.fetch_sub(layout.size(), Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[global_allocator]
|
||||||
|
static ALLOC: Counting = Counting;
|
||||||
|
|
||||||
|
/// Bytes allocated at the peak of `f`, above what was live when it started.
|
||||||
|
fn peak_during<T>(f: impl FnOnce() -> T) -> (T, usize) {
|
||||||
|
let base = CURRENT.load(Ordering::Relaxed);
|
||||||
|
PEAK.store(base, Ordering::Relaxed);
|
||||||
|
let out = f();
|
||||||
|
(out, PEAK.load(Ordering::Relaxed).saturating_sub(base))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// What decoding one HDF5 chunk of `limit` bytes from `input` may hold at
|
||||||
|
/// once: the output, a few blocks of scratch (each no larger than the
|
||||||
|
/// output), the offsets table, and the Zstandard decoder's state, which has
|
||||||
|
/// a fixed ceiling: a window of at most 128 KiB (or twice the stream) and a
|
||||||
|
/// block's table of sequences (up to 98,303 of 12 bytes, 1.2 MB).
|
||||||
|
fn bound(limit: usize, input: &[u8]) -> usize {
|
||||||
|
6 * limit + 2 * input.len() + (2 << 20)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn lock() -> std::sync::MutexGuard<'static, ()> {
|
||||||
|
SERIAL.lock().unwrap_or_else(|e| e.into_inner())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A 32-byte (extended) Blosc2 chunk header.
|
||||||
|
fn chunk_header(ts: u8, nbytes: i32, blocksize: i32, cbytes: i32, special: u8) -> Vec<u8> {
|
||||||
|
let mut c = vec![5u8, 1, 0x05, ts];
|
||||||
|
for v in [nbytes, blocksize, cbytes] {
|
||||||
|
c.extend_from_slice(&v.to_le_bytes());
|
||||||
|
}
|
||||||
|
c.resize(32, 0);
|
||||||
|
c[31] = special << 4;
|
||||||
|
c
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A chunk of `nbytes` bytes that repeats one value (special type 3).
|
||||||
|
fn repeated(value: &[u8], nbytes: i32, blocksize: i32) -> Vec<u8> {
|
||||||
|
let mut c = chunk_header(
|
||||||
|
value.len() as u8,
|
||||||
|
nbytes,
|
||||||
|
blocksize,
|
||||||
|
32 + value.len() as i32,
|
||||||
|
3,
|
||||||
|
);
|
||||||
|
c.extend_from_slice(value);
|
||||||
|
c
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A frame offset recording a special chunk of `kind` (1 zeros, 2 NaN).
|
||||||
|
fn special_offset(kind: u8) -> [u8; 8] {
|
||||||
|
(((0x80 | kind) as i64) << 56).to_le_bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A B2ND metalayer.
|
||||||
|
fn nd_meta(shape: &[i64], chunks: &[i32], blocks: &[i32]) -> Vec<u8> {
|
||||||
|
let n = shape.len() as u8;
|
||||||
|
let mut m = vec![0x95, 0, n, 0x90 | n];
|
||||||
|
for s in shape {
|
||||||
|
m.push(0xd3);
|
||||||
|
m.extend_from_slice(&s.to_be_bytes());
|
||||||
|
}
|
||||||
|
for dims in [chunks, blocks] {
|
||||||
|
m.push(0x90 | n);
|
||||||
|
for d in dims {
|
||||||
|
m.push(0xd2);
|
||||||
|
m.extend_from_slice(&d.to_be_bytes());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A contiguous frame: header (with a `b2nd` metalayer if given), the data
|
||||||
|
/// chunks, then the offsets chunk.
|
||||||
|
fn frame(
|
||||||
|
meta: Option<&[u8]>,
|
||||||
|
nbytes: i64,
|
||||||
|
typesize: i32,
|
||||||
|
chunksize: i32,
|
||||||
|
data: &[u8],
|
||||||
|
offsets: &[u8],
|
||||||
|
) -> Vec<u8> {
|
||||||
|
let mut h = vec![0u8; 91];
|
||||||
|
h[0] = 0x9e;
|
||||||
|
h[1] = 0xa8;
|
||||||
|
h[2..10].copy_from_slice(b"b2frame\0");
|
||||||
|
h[25] = 2;
|
||||||
|
match meta {
|
||||||
|
Some(m) => {
|
||||||
|
h.extend_from_slice(&[0xde, 0, 1, 0xa4]);
|
||||||
|
h.extend_from_slice(b"b2nd");
|
||||||
|
let at = h.len() as i32 + 5;
|
||||||
|
h.push(0xd2);
|
||||||
|
h.extend_from_slice(&at.to_be_bytes());
|
||||||
|
h.push(0xc6);
|
||||||
|
h.extend_from_slice(&(m.len() as u32).to_be_bytes());
|
||||||
|
h.extend_from_slice(m);
|
||||||
|
}
|
||||||
|
None => h.extend_from_slice(&[0xde, 0, 0]),
|
||||||
|
}
|
||||||
|
let header_len = h.len() as i32;
|
||||||
|
h[11..15].copy_from_slice(&header_len.to_be_bytes());
|
||||||
|
h[30..38].copy_from_slice(&nbytes.to_be_bytes());
|
||||||
|
h[39..47].copy_from_slice(&(data.len() as i64).to_be_bytes());
|
||||||
|
h[48..52].copy_from_slice(&typesize.to_be_bytes());
|
||||||
|
h[58..62].copy_from_slice(&chunksize.to_be_bytes());
|
||||||
|
h.extend_from_slice(data);
|
||||||
|
h.extend_from_slice(offsets);
|
||||||
|
let len = h.len() as u64;
|
||||||
|
h[16..24].copy_from_slice(&len.to_be_bytes());
|
||||||
|
h
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The frame header's own sizes must not size the offsets chunk: a frame
|
||||||
|
/// declaring 32 Mi chunks of 4 bytes, whose offsets chunk (40 bytes) says
|
||||||
|
/// "one repeated offset, 256 MiB of them", made the decoder build all
|
||||||
|
/// 256 MiB of offsets for a 1 MiB HDF5 chunk and then return 4 bytes.
|
||||||
|
#[test]
|
||||||
|
fn offsets_chunk_is_bounded_by_the_output_limit() {
|
||||||
|
let _g = lock();
|
||||||
|
let limit = 1 << 20;
|
||||||
|
let offsets_len: i32 = 256 << 20;
|
||||||
|
let nchunks = offsets_len as i64 / 8;
|
||||||
|
let offsets = repeated(&special_offset(1), offsets_len, 64 << 20);
|
||||||
|
let f = frame(None, nchunks * 4, 4, 4, &[], &offsets);
|
||||||
|
let (r, peak) = peak_during(|| blosc2_decompress(&f, limit));
|
||||||
|
assert!(r.is_err(), "decoded {:?} bytes", r.map(|v| v.len()));
|
||||||
|
assert!(
|
||||||
|
peak <= bound(limit, &f),
|
||||||
|
"peak {peak} bytes for a {}-byte frame",
|
||||||
|
f.len()
|
||||||
|
);
|
||||||
|
// The same frame with a variable chunk size (0): the offsets chunk
|
||||||
|
// alone says how many chunks there are.
|
||||||
|
let f = frame(None, nchunks * 4, 4, 0, &[], &offsets);
|
||||||
|
let (r, peak) = peak_during(|| blosc2_decompress(&f, limit));
|
||||||
|
assert!(r.is_err());
|
||||||
|
assert!(peak <= bound(limit, &f), "chunksize 0: peak {peak} bytes");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A legitimate frame of this shape (one chunk, its offset special) still
|
||||||
|
/// decodes.
|
||||||
|
#[test]
|
||||||
|
fn small_frames_still_decode() {
|
||||||
|
let _g = lock();
|
||||||
|
let offsets = repeated(&special_offset(1), 8, 8);
|
||||||
|
let f = frame(None, 64, 4, 64, &[], &offsets);
|
||||||
|
assert_eq!(blosc2_decompress(&f, 64).unwrap(), vec![0; 64]);
|
||||||
|
let _ = blosc2_decompress_chunk;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A chunk that decodes to nothing kept its declared block size (up to
|
||||||
|
/// 512 MiB) and allocated two scratch blocks of it: about 1 GiB for a
|
||||||
|
/// 20-byte chunk.
|
||||||
|
#[test]
|
||||||
|
fn empty_chunk_does_not_allocate_its_block_size() {
|
||||||
|
let _g = lock();
|
||||||
|
let mut c = vec![5u8, 1, 0x01, 1];
|
||||||
|
for v in [0i32, 0x1FFF_F000, 20] {
|
||||||
|
c.extend_from_slice(&v.to_le_bytes());
|
||||||
|
}
|
||||||
|
c.resize(20, 0);
|
||||||
|
let (r, peak) = peak_during(|| blosc2_decompress_chunk(&c, 1 << 20));
|
||||||
|
assert_eq!(r.map(|v| v.len()).unwrap_or(0), 0);
|
||||||
|
assert!(
|
||||||
|
peak <= bound(0, &c),
|
||||||
|
"peak {peak} bytes for a 20-byte chunk"
|
||||||
|
);
|
||||||
|
// Inside a frame for a non-empty HDF5 chunk it is an error, not data.
|
||||||
|
let offsets = repeated(&0i64.to_le_bytes(), 8, 8);
|
||||||
|
let f = frame(None, 64, 4, 64, &c, &offsets);
|
||||||
|
let (r, peak) = peak_during(|| blosc2_decompress(&f, 64));
|
||||||
|
assert!(r.is_err(), "decoded {:?}", r.map(|v| v.len()));
|
||||||
|
assert!(peak <= bound(64, &f), "in a frame: peak {peak} bytes");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// B2ND chunks were decoded whole, padding included, with up to 16x the
|
||||||
|
/// HDF5 chunk size as their limit. Blocks are now placed as they are
|
||||||
|
/// decoded, so the padding is never held.
|
||||||
|
///
|
||||||
|
/// Ten dimensions: nine of 3 split into blocks of 2 (padded to 4) and one
|
||||||
|
/// of 4, so each chunk is 13x the array. One chunk, stored three ways: as a
|
||||||
|
/// NaN chunk in the frame's offsets, as a repeated-value chunk, and as a
|
||||||
|
/// chunk of stored (uncompressed) blocks.
|
||||||
|
#[test]
|
||||||
|
fn b2nd_padding_is_never_held() {
|
||||||
|
let _g = lock();
|
||||||
|
let ts = 4usize;
|
||||||
|
let mut shape = vec![3i64; 9];
|
||||||
|
shape.push(4);
|
||||||
|
let chunks: Vec<i32> = shape.iter().map(|&s| s as i32).collect();
|
||||||
|
let mut blocks = vec![2i32; 9];
|
||||||
|
blocks.push(4);
|
||||||
|
let meta = nd_meta(&shape, &chunks, &blocks);
|
||||||
|
let items: usize = shape.iter().product::<i64>() as usize;
|
||||||
|
let limit = items * ts;
|
||||||
|
let block_bytes = ts * blocks.iter().product::<i32>() as usize;
|
||||||
|
let ext_bytes = ts * 4usize.pow(9) * 4;
|
||||||
|
assert!(ext_bytes > 13 * limit);
|
||||||
|
let offsets = |off: [u8; 8]| repeated(&off, 8, 8);
|
||||||
|
|
||||||
|
let value = 1.5f32.to_le_bytes();
|
||||||
|
let stored = {
|
||||||
|
// Every block stored raw: block k holds the value k.
|
||||||
|
let mut c = chunk_header(4, ext_bytes as i32, block_bytes as i32, 0, 0);
|
||||||
|
c[2] = 0x02 | 0x10; // memcpyed, not split
|
||||||
|
c.truncate(16);
|
||||||
|
for k in 0..ext_bytes / block_bytes {
|
||||||
|
c.extend((k as f32).to_le_bytes().repeat(block_bytes / 4));
|
||||||
|
}
|
||||||
|
let n = c.len() as i32;
|
||||||
|
c[12..16].copy_from_slice(&n.to_le_bytes());
|
||||||
|
c
|
||||||
|
};
|
||||||
|
let cases: Vec<(&str, Vec<u8>)> = vec![
|
||||||
|
(
|
||||||
|
"NaN offset",
|
||||||
|
frame(
|
||||||
|
Some(&meta),
|
||||||
|
ext_bytes as i64,
|
||||||
|
4,
|
||||||
|
ext_bytes as i32,
|
||||||
|
&[],
|
||||||
|
&offsets(special_offset(2)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"repeated value",
|
||||||
|
frame(
|
||||||
|
Some(&meta),
|
||||||
|
ext_bytes as i64,
|
||||||
|
4,
|
||||||
|
ext_bytes as i32,
|
||||||
|
&repeated(&value, ext_bytes as i32, block_bytes as i32),
|
||||||
|
&offsets(0i64.to_le_bytes()),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"stored blocks",
|
||||||
|
frame(
|
||||||
|
Some(&meta),
|
||||||
|
ext_bytes as i64,
|
||||||
|
4,
|
||||||
|
ext_bytes as i32,
|
||||||
|
&stored,
|
||||||
|
&offsets(0i64.to_le_bytes()),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
for (name, f) in cases {
|
||||||
|
let (r, peak) = peak_during(|| blosc2_decompress(&f, limit));
|
||||||
|
let out = r.unwrap_or_else(|e| panic!("{name}: {e}"));
|
||||||
|
assert_eq!(out.len(), limit, "{name}");
|
||||||
|
match name {
|
||||||
|
"NaN offset" => assert!(
|
||||||
|
out.chunks(4)
|
||||||
|
.all(|v| f32::from_le_bytes(v.try_into().unwrap()).is_nan())
|
||||||
|
),
|
||||||
|
"repeated value" => assert!(out.chunks(4).all(|v| v == value)),
|
||||||
|
_ => {
|
||||||
|
// Element (i0..i9) lies in block (i0/2, .., i8/2), numbered
|
||||||
|
// in C order over a 2x..x2x1 grid of blocks.
|
||||||
|
let mut idx = [0usize; 10];
|
||||||
|
for (e, v) in out.chunks(4).enumerate() {
|
||||||
|
let mut n = e;
|
||||||
|
for d in (0..10).rev() {
|
||||||
|
idx[d] = n % shape[d] as usize;
|
||||||
|
n /= shape[d] as usize;
|
||||||
|
}
|
||||||
|
let k = idx[..9].iter().fold(0, |k, &i| k * 2 + i / 2);
|
||||||
|
assert_eq!(
|
||||||
|
f32::from_le_bytes(v.try_into().unwrap()),
|
||||||
|
k as f32,
|
||||||
|
"{name} {e}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert!(
|
||||||
|
peak <= bound(limit, &f),
|
||||||
|
"{name}: peak {peak} bytes for a {limit}-byte chunk ({}-byte frame)",
|
||||||
|
f.len()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A B2ND chunk larger than the array (here 16x, the old cap) is refused,
|
||||||
|
/// or at least never allocated.
|
||||||
|
#[test]
|
||||||
|
fn b2nd_chunk_larger_than_the_array_is_not_allocated() {
|
||||||
|
let _g = lock();
|
||||||
|
let limit = 1 << 20;
|
||||||
|
let c = 16 * limit as i32;
|
||||||
|
let meta = nd_meta(&[limit as i64], &[c], &[c]);
|
||||||
|
let offsets = repeated(&special_offset(1), 8, 8);
|
||||||
|
let f = frame(Some(&meta), c as i64, 1, c, &[], &offsets);
|
||||||
|
let (r, peak) = peak_during(|| blosc2_decompress(&f, limit));
|
||||||
|
assert!(
|
||||||
|
peak <= bound(limit, &f),
|
||||||
|
"peak {peak} bytes ({:?})",
|
||||||
|
r.map(|v| v.len())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ruzstd reserves a frame's declared window (up to 100 MiB) before it
|
||||||
|
/// decodes a frame with a decoder it has used before: a Blosc2 chunk of
|
||||||
|
/// two 16-byte Zstandard streams, each declaring a 96 MiB window,
|
||||||
|
/// allocated 96 MiB. c-blosc2 compresses each block with its size known,
|
||||||
|
/// so its windows never exceed the block.
|
||||||
|
#[test]
|
||||||
|
fn zstd_window_is_bounded_by_the_output() {
|
||||||
|
let _g = lock();
|
||||||
|
let mut z = 0xfd2f_b528u32.to_le_bytes().to_vec();
|
||||||
|
// No single segment, no checksum; window 2^26 + 4/8 of it = 96 MiB.
|
||||||
|
z.extend_from_slice(&[0x00, (16 << 3) | 4]);
|
||||||
|
// One raw block, last, of 16 bytes.
|
||||||
|
let h = 1 | (16 << 3);
|
||||||
|
z.extend_from_slice(&[h as u8, (h >> 8) as u8, 0]);
|
||||||
|
z.extend_from_slice(&[7; 16]);
|
||||||
|
// Two blocks of 16 bytes, one stream each (not split), Zstandard
|
||||||
|
// (codec 4).
|
||||||
|
let chunk = |z: &[u8]| {
|
||||||
|
let mut c = vec![5u8, 1, 0x10 | (4 << 5), 1];
|
||||||
|
for v in [32i32, 16, 0] {
|
||||||
|
c.extend_from_slice(&v.to_le_bytes());
|
||||||
|
}
|
||||||
|
let first = 24 + 4 + z.len();
|
||||||
|
c.extend_from_slice(&24i32.to_le_bytes());
|
||||||
|
c.extend_from_slice(&(first as i32).to_le_bytes());
|
||||||
|
for _ in 0..2 {
|
||||||
|
c.extend_from_slice(&(z.len() as i32).to_le_bytes());
|
||||||
|
c.extend_from_slice(z);
|
||||||
|
}
|
||||||
|
let n = c.len() as i32;
|
||||||
|
c[12..16].copy_from_slice(&n.to_le_bytes());
|
||||||
|
c
|
||||||
|
};
|
||||||
|
let c = chunk(&z);
|
||||||
|
let (r, peak) = peak_during(|| blosc2_decompress_chunk(&c, 32));
|
||||||
|
assert!(peak <= bound(32, &c), "peak {peak} bytes ({r:?})");
|
||||||
|
assert!(r.is_err(), "{r:?}");
|
||||||
|
// The same streams with a window they can use read.
|
||||||
|
z[5] = 0;
|
||||||
|
assert_eq!(
|
||||||
|
blosc2_decompress_chunk(&chunk(&z), 32).unwrap(),
|
||||||
|
vec![7; 32]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// xorshift64*: deterministic, so a failure reproduces.
|
||||||
|
struct Rng(u64);
|
||||||
|
|
||||||
|
impl Rng {
|
||||||
|
fn next(&mut self) -> u64 {
|
||||||
|
let mut x = self.0;
|
||||||
|
x ^= x >> 12;
|
||||||
|
x ^= x << 25;
|
||||||
|
x ^= x >> 27;
|
||||||
|
self.0 = x;
|
||||||
|
x.wrapping_mul(0x2545_F491_4F6C_DD1D)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn below(&mut self, n: usize) -> usize {
|
||||||
|
(self.next() % n.max(1) as u64) as usize
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A size that tends to the edges: small, a power of two, huge.
|
||||||
|
fn size(&mut self) -> i64 {
|
||||||
|
match self.below(6) {
|
||||||
|
0 => self.below(64) as i64,
|
||||||
|
1 => 1 << self.below(31),
|
||||||
|
2 => i32::MAX as i64 - self.below(4096) as i64,
|
||||||
|
3 => (1i64 << self.below(62)) + self.below(8) as i64,
|
||||||
|
4 => MAX_BLOCK - self.below(3) as i64,
|
||||||
|
_ => self.next() as i32 as i64,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const MAX_BLOCK: i64 = 536_866_816;
|
||||||
|
|
||||||
|
/// One to four edits: bytes, or a size field written little-endian (chunk
|
||||||
|
/// headers) or big-endian (frame headers), most often at a header's size
|
||||||
|
/// fields.
|
||||||
|
fn mutate(rng: &mut Rng, seed: &[u8], data_at: usize) -> Vec<u8> {
|
||||||
|
let mut v = seed.to_vec();
|
||||||
|
for _ in 0..1 + rng.below(4) {
|
||||||
|
let len = v.len();
|
||||||
|
if len < 16 {
|
||||||
|
v.push(rng.next() as u8);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
match rng.below(8) {
|
||||||
|
0 => {
|
||||||
|
let i = rng.below(len);
|
||||||
|
v[i] ^= 1 << rng.below(8);
|
||||||
|
}
|
||||||
|
1 => {
|
||||||
|
let i = rng.below(len);
|
||||||
|
v[i] = rng.next() as u8;
|
||||||
|
}
|
||||||
|
2 => {
|
||||||
|
// Frame header: nbytes, cbytes (i64), typesize, chunksize.
|
||||||
|
let x = rng.size();
|
||||||
|
match rng.below(4) {
|
||||||
|
0 if len >= 38 => v[30..38].copy_from_slice(&x.to_be_bytes()),
|
||||||
|
1 if len >= 47 => v[39..47].copy_from_slice(&x.to_be_bytes()),
|
||||||
|
2 if len >= 52 => v[48..52].copy_from_slice(&(x as i32).to_be_bytes()),
|
||||||
|
_ if len >= 62 => v[58..62].copy_from_slice(&(x as i32).to_be_bytes()),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
3 | 4 => {
|
||||||
|
// A chunk header's nbytes, blocksize or cbytes: in the first
|
||||||
|
// data chunk, or anywhere (the offsets chunk comes last).
|
||||||
|
let at = if rng.below(2) == 0 && data_at + 16 <= len {
|
||||||
|
data_at + 4 * (1 + rng.below(3))
|
||||||
|
} else {
|
||||||
|
rng.below(len - 3)
|
||||||
|
};
|
||||||
|
let x = rng.size() as i32;
|
||||||
|
v[at..at + 4].copy_from_slice(&x.to_le_bytes());
|
||||||
|
}
|
||||||
|
5 => v.truncate(rng.below(len)),
|
||||||
|
6 => {
|
||||||
|
let at = rng.below(len);
|
||||||
|
v[at] = [0x10, 0x20, 0x30, 0x40, 0x05, 0x07, 0x02][rng.below(7)];
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
let i = rng.below(len - 3);
|
||||||
|
let x = rng.size() as i32;
|
||||||
|
v[i..i + 4].copy_from_slice(&x.to_be_bytes());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
v
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Every fixture frame that decodes, with its decoded size.
|
||||||
|
fn seeds() -> Vec<(Vec<u8>, usize)> {
|
||||||
|
let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/blosc2");
|
||||||
|
let mut v = Vec::new();
|
||||||
|
for e in std::fs::read_dir(dir).unwrap() {
|
||||||
|
let p = e.unwrap().path();
|
||||||
|
if p.extension().is_some_and(|x| x == "b2f")
|
||||||
|
&& let Ok(out) = std::fs::read(p.with_extension("out"))
|
||||||
|
{
|
||||||
|
v.push((std::fs::read(&p).unwrap(), out.len()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
v.sort();
|
||||||
|
assert!(v.len() >= 20, "fixtures missing");
|
||||||
|
v
|
||||||
|
}
|
||||||
|
|
||||||
|
fn header_len(frame: &[u8]) -> usize {
|
||||||
|
i32::from_be_bytes(frame[11..15].try_into().unwrap()) as usize
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mutated fixture frames, decoded with their HDF5 chunk size as the
|
||||||
|
/// limit, and their first chunks on their own: whatever they declare, no
|
||||||
|
/// decode holds more than a small multiple of the output and the input.
|
||||||
|
#[test]
|
||||||
|
fn fuzzed_frames_and_chunks_stay_within_the_allocation_bound() {
|
||||||
|
let _g = lock();
|
||||||
|
let seeds = seeds();
|
||||||
|
let mut rng = Rng(0xb2a1);
|
||||||
|
let mut worst = (0.0f64, String::new());
|
||||||
|
for i in 0..20_000 {
|
||||||
|
let (seed, limit) = &seeds[rng.below(seeds.len())];
|
||||||
|
let f = mutate(&mut rng, seed, header_len(seed));
|
||||||
|
let (r, peak) = peak_during(|| blosc2_decompress(&f, *limit));
|
||||||
|
if let Ok(out) = &r {
|
||||||
|
assert!(out.len() <= *limit, "iteration {i}: output past the limit");
|
||||||
|
}
|
||||||
|
assert!(
|
||||||
|
peak <= bound(*limit, &f),
|
||||||
|
"iteration {i}: peak {peak} bytes for a {limit}-byte chunk from {} bytes ({:?})",
|
||||||
|
f.len(),
|
||||||
|
r.map(|v| v.len())
|
||||||
|
);
|
||||||
|
let ratio = peak as f64 / bound(*limit, &f) as f64;
|
||||||
|
if ratio > worst.0 {
|
||||||
|
worst = (
|
||||||
|
ratio,
|
||||||
|
format!(
|
||||||
|
"frame iteration {i}: peak {peak}, limit {limit}, input {}",
|
||||||
|
f.len()
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for i in 0..20_000 {
|
||||||
|
let (seed, _) = &seeds[rng.below(seeds.len())];
|
||||||
|
let at = header_len(seed);
|
||||||
|
let chunk = &seed[at..];
|
||||||
|
let c = mutate(&mut rng, chunk, 0);
|
||||||
|
let limit = 1 << 16;
|
||||||
|
let (r, peak) = peak_during(|| blosc2_decompress_chunk(&c, limit));
|
||||||
|
assert!(
|
||||||
|
peak <= bound(limit, &c),
|
||||||
|
"chunk iteration {i}: peak {peak} bytes from {} bytes ({:?})",
|
||||||
|
c.len(),
|
||||||
|
r.map(|v| v.len())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
eprintln!("worst peak / bound: {:.2} ({})", worst.0, worst.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Frames built from random header sizes, offsets chunks and B2ND shapes
|
||||||
|
/// (chunk and block shapes that pad, special and repeated-value chunks).
|
||||||
|
#[test]
|
||||||
|
fn random_frames_stay_within_the_allocation_bound() {
|
||||||
|
let _g = lock();
|
||||||
|
let mut rng = Rng(0xb2a2);
|
||||||
|
for i in 0..5_000 {
|
||||||
|
let ts = [1usize, 2, 4, 8][rng.below(4)];
|
||||||
|
let ndim = 1 + rng.below(8);
|
||||||
|
let mut shape = Vec::new();
|
||||||
|
let mut chunks = Vec::new();
|
||||||
|
let mut blocks = Vec::new();
|
||||||
|
for _ in 0..ndim {
|
||||||
|
let s = 1 + rng.below(if ndim > 3 { 4 } else { 40 });
|
||||||
|
let c = if rng.below(8) == 0 {
|
||||||
|
s * (1 + rng.below(4))
|
||||||
|
} else {
|
||||||
|
1 + rng.below(s)
|
||||||
|
};
|
||||||
|
let b = 1 + rng.below(c);
|
||||||
|
shape.push(s as i64);
|
||||||
|
chunks.push(c as i32);
|
||||||
|
blocks.push(b as i32);
|
||||||
|
}
|
||||||
|
let items: usize = shape.iter().product::<i64>() as usize;
|
||||||
|
let limit = items * ts;
|
||||||
|
let meta = nd_meta(&shape, &chunks, &blocks);
|
||||||
|
let ext: usize = ts
|
||||||
|
* chunks
|
||||||
|
.iter()
|
||||||
|
.zip(&blocks)
|
||||||
|
.map(|(&c, &b)| (c as usize).div_ceil(b as usize) * b as usize)
|
||||||
|
.product::<usize>();
|
||||||
|
let nchunks: usize = shape
|
||||||
|
.iter()
|
||||||
|
.zip(&chunks)
|
||||||
|
.map(|(&s, &c)| (s as usize).div_ceil(c as usize))
|
||||||
|
.product();
|
||||||
|
let block_bytes = ts * blocks.iter().product::<i32>() as usize;
|
||||||
|
let chunksize = if rng.below(4) == 0 {
|
||||||
|
rng.size()
|
||||||
|
} else {
|
||||||
|
ext as i64
|
||||||
|
};
|
||||||
|
let nbytes = if rng.below(4) == 0 {
|
||||||
|
rng.size()
|
||||||
|
} else {
|
||||||
|
(nchunks * ext) as i64
|
||||||
|
};
|
||||||
|
let off_n = if rng.below(4) == 0 {
|
||||||
|
rng.size() as i32
|
||||||
|
} else {
|
||||||
|
8 * nchunks as i32
|
||||||
|
};
|
||||||
|
let (data, off) = match rng.below(3) {
|
||||||
|
0 => (Vec::new(), special_offset(1 + rng.below(2) as u8)),
|
||||||
|
_ => {
|
||||||
|
let bs = if rng.below(4) == 0 {
|
||||||
|
rng.size() as i32
|
||||||
|
} else {
|
||||||
|
block_bytes as i32
|
||||||
|
};
|
||||||
|
let value: Vec<u8> = (0..ts).map(|_| rng.next() as u8).collect();
|
||||||
|
let n = if rng.below(4) == 0 {
|
||||||
|
rng.size() as i32
|
||||||
|
} else {
|
||||||
|
ext as i32
|
||||||
|
};
|
||||||
|
(repeated(&value, n, bs), 0i64.to_le_bytes())
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let offsets = repeated(&off, off_n, off_n.clamp(1, 8));
|
||||||
|
let meta = (rng.below(4) != 0).then_some(meta.as_slice());
|
||||||
|
let f = frame(meta, nbytes, ts as i32, chunksize as i32, &data, &offsets);
|
||||||
|
let (r, peak) = peak_during(|| blosc2_decompress(&f, limit));
|
||||||
|
assert!(
|
||||||
|
peak <= bound(limit, &f),
|
||||||
|
"iteration {i}: peak {peak} bytes for a {limit}-byte chunk ({:?}, shape {shape:?} \
|
||||||
|
chunks {chunks:?} blocks {blocks:?})",
|
||||||
|
r.map(|v| v.len())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1 @@
|
|||||||
|
filter 35
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,129 @@
|
|||||||
|
"""Generate the Blosc2 frames the `filters_blosc2` unit tests decode.
|
||||||
|
|
||||||
|
Each case is `<name>.b2f` (a Blosc2 contiguous frame, what the HDF5 Blosc2
|
||||||
|
filter stores per chunk) and `<name>.out` (what decoding it must give: the
|
||||||
|
first chunk of a plain frame, or the whole array in C order for a B2ND
|
||||||
|
frame), or `<name>.err` (a frame clawhdf5 must refuse; the file holds a word
|
||||||
|
the error message must contain).
|
||||||
|
|
||||||
|
These cover what files written by h5py + hdf5plugin never contain, but a
|
||||||
|
Blosc2 frame may: special chunks (repeated value, NaN, uninitialised), the
|
||||||
|
delta filter over many blocks and odd type sizes, bit shuffle of blocks that
|
||||||
|
are not a multiple of 8 elements, shuffle with a byte-group size, forced
|
||||||
|
stream splitting, multi-chunk B2ND arrays with padded edge chunks and a
|
||||||
|
chunk of zeros, and features clawhdf5 refuses (dictionaries, registered
|
||||||
|
filters).
|
||||||
|
|
||||||
|
Written with python-blosc2 4.13.1 (c-blosc2 3.3.4) in a scratch venv
|
||||||
|
(`pip install blosc2`). Re-run only to regenerate:
|
||||||
|
|
||||||
|
python generate.py <this directory>
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import blosc2
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
out = sys.argv[1]
|
||||||
|
|
||||||
|
|
||||||
|
def save(name, frame, expected):
|
||||||
|
with open(os.path.join(out, name + ".b2f"), "wb") as f:
|
||||||
|
f.write(frame)
|
||||||
|
with open(os.path.join(out, name + ".out"), "wb") as f:
|
||||||
|
f.write(expected)
|
||||||
|
|
||||||
|
|
||||||
|
def save_err(name, frame, word):
|
||||||
|
with open(os.path.join(out, name + ".b2f"), "wb") as f:
|
||||||
|
f.write(frame)
|
||||||
|
with open(os.path.join(out, name + ".err"), "w") as f:
|
||||||
|
f.write(word)
|
||||||
|
|
||||||
|
|
||||||
|
def plain(data, **cparams):
|
||||||
|
"""A one-chunk super-chunk frame of `data`, as hdf5-blosc2 writes."""
|
||||||
|
data = np.ascontiguousarray(data)
|
||||||
|
cp = blosc2.CParams(typesize=data.dtype.itemsize, **cparams)
|
||||||
|
sc = blosc2.SChunk(chunksize=data.nbytes, cparams=cp)
|
||||||
|
sc.append_data(data)
|
||||||
|
return sc.to_cframe(), data.tobytes()
|
||||||
|
|
||||||
|
|
||||||
|
def special(nitems, dtype, kind, value=None):
|
||||||
|
dt = np.dtype(dtype)
|
||||||
|
sc = blosc2.SChunk(chunksize=nitems * dt.itemsize,
|
||||||
|
cparams=blosc2.CParams(typesize=dt.itemsize))
|
||||||
|
sc.fill_special(nitems, kind, value)
|
||||||
|
return sc.to_cframe()
|
||||||
|
|
||||||
|
|
||||||
|
# Special chunks. A repeated value stays in the frame as a 33+ byte chunk;
|
||||||
|
# NaN and uninitialised chunks become special offsets.
|
||||||
|
save("value_i4", special(300, "<i4", blosc2.SpecialValue.VALUE, 123456),
|
||||||
|
np.full(300, 123456, "<i4").tobytes())
|
||||||
|
save("value_f8", special(250, "<f8", blosc2.SpecialValue.VALUE, -2.5),
|
||||||
|
np.full(250, -2.5, "<f8").tobytes())
|
||||||
|
save("nan_f4", special(500, "<f4", blosc2.SpecialValue.NAN),
|
||||||
|
np.full(500, np.nan, "<f4").tobytes())
|
||||||
|
save("nan_f8", special(300, "<f8", blosc2.SpecialValue.NAN),
|
||||||
|
np.full(300, np.nan, "<f8").tobytes())
|
||||||
|
save("zero_u2", special(2000, "<u2", blosc2.SpecialValue.ZERO), bytes(4000))
|
||||||
|
# Uninitialised values: libhdf5 would hand back whatever memory it had;
|
||||||
|
# clawhdf5 returns zeros.
|
||||||
|
save("uninit_i8", special(64, "<i8", blosc2.SpecialValue.UNINIT), bytes(512))
|
||||||
|
|
||||||
|
rng = np.random.default_rng(11)
|
||||||
|
ramp = lambda n, dt: ((np.arange(n) * 7) % 1000 + rng.integers(0, 3, n)).astype(dt)
|
||||||
|
# Slowly varying: what the delta filter is for (noise would be stored raw).
|
||||||
|
smooth = lambda n, dt: (np.arange(n) // 3 + 1000).astype(dt)
|
||||||
|
|
||||||
|
# Delta over many blocks, for type sizes 1, 2, 4, 8, 3 (bytes) and 16 (u64
|
||||||
|
# pairs).
|
||||||
|
for dt, n, codec in [("<u1", 2000, blosc2.Codec.LZ4), ("<i2", 1000, blosc2.Codec.BLOSCLZ),
|
||||||
|
("<i4", 700, blosc2.Codec.LZ4), ("<u8", 400, blosc2.Codec.BLOSCLZ)]:
|
||||||
|
save(f"delta_{np.dtype(dt).name}_{codec.name.lower()}",
|
||||||
|
*plain(smooth(n, dt), codec=codec, blocksize=256,
|
||||||
|
filters=[blosc2.Filter.DELTA], filters_meta=[0]))
|
||||||
|
rec3 = np.frombuffer(smooth(3 * 300, "<u1").tobytes(), dtype="V3")
|
||||||
|
save("delta_v3", *plain(rec3, blocksize=300, filters=[blosc2.Filter.DELTA], filters_meta=[0]))
|
||||||
|
rec16 = np.frombuffer(smooth(2 * 200, "<u8").tobytes(), dtype="V16")
|
||||||
|
save("delta_shuffle_v16", *plain(rec16, blocksize=512,
|
||||||
|
filters=[blosc2.Filter.DELTA, blosc2.Filter.SHUFFLE],
|
||||||
|
filters_meta=[0, 0]))
|
||||||
|
|
||||||
|
# Bit shuffle of blocks whose element count is not a multiple of 8 (44-byte
|
||||||
|
# blocks of 4-byte elements: 8 transposed, 3 copied).
|
||||||
|
save("bitshuffle_odd_blocks", *plain(ramp(500, "<i4"), codec=blosc2.Codec.ZSTD, blocksize=44,
|
||||||
|
filters=[blosc2.Filter.BITSHUFFLE], filters_meta=[0]))
|
||||||
|
# Shuffle in groups of 2 bytes of an 8-byte type (filters_meta).
|
||||||
|
save("shuffle_meta2", *plain(ramp(500, "<i8"), codec=blosc2.Codec.ZLIB,
|
||||||
|
filters=[blosc2.Filter.SHUFFLE], filters_meta=[2]))
|
||||||
|
# Streams split per byte, and never split.
|
||||||
|
save("always_split", *plain(ramp(1000, "<f4"), codec=blosc2.Codec.LZ4HC,
|
||||||
|
splitmode=blosc2.SplitMode.ALWAYS_SPLIT))
|
||||||
|
save("never_split", *plain(ramp(1500, "<u2"), codec=blosc2.Codec.ZSTD,
|
||||||
|
splitmode=blosc2.SplitMode.NEVER_SPLIT))
|
||||||
|
|
||||||
|
# B2ND arrays of several chunks whose edge chunks and blocks are padded, and
|
||||||
|
# one whose middle chunk is all zeros (a special offset).
|
||||||
|
for name, shape, chunks, blocks, dt in [
|
||||||
|
("b2nd_2d", (37, 29), (10, 16), (4, 6), "<i4"),
|
||||||
|
("b2nd_3d", (9, 10, 7), (4, 5, 3), (3, 2, 2), "<f4"),
|
||||||
|
("b2nd_4d", (5, 7, 5, 6), (3, 2, 5, 4), (2, 2, 3, 3), "<u2"),
|
||||||
|
]:
|
||||||
|
a = ramp(int(np.prod(shape)), dt).reshape(shape)
|
||||||
|
arr = blosc2.asarray(a, chunks=chunks, blocks=blocks)
|
||||||
|
save(name, arr.to_cframe(), a.tobytes())
|
||||||
|
a = ramp(60 * 20, "<i2").reshape(60, 20)
|
||||||
|
a[20:40, :] = 0
|
||||||
|
arr = blosc2.asarray(a, chunks=(20, 20), blocks=(8, 16))
|
||||||
|
save("b2nd_zero_chunk", arr.to_cframe(), a.tobytes())
|
||||||
|
|
||||||
|
# Refused: a dictionary, and a registered filter (bytedelta).
|
||||||
|
frame, _ = plain(ramp(4000, "<i4"), codec=blosc2.Codec.ZSTD, use_dict=True, blocksize=2048)
|
||||||
|
save_err("zstd_dict", frame, "dictionar")
|
||||||
|
frame, _ = plain(ramp(500, "<i4"), filters=[blosc2.Filter.SHUFFLE, blosc2.Filter.BYTEDELTA],
|
||||||
|
filters_meta=[0, 4])
|
||||||
|
save_err("bytedelta", frame, "filter 35")
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user