hdf5-pure 0.33.0

Pure-Rust HDF5 library: read, write, and edit files in place (WASM-compatible, no C dependencies)
Documentation
# Groups & Attributes

HDF5 files are hierarchical: datasets live inside groups, groups nest inside other groups, and any object (the root, a group, or a dataset) can carry typed metadata in the form of attributes. This page covers building a nested hierarchy with the write API, attaching attributes of several types, and walking the structure back when reading.

!!! tip "Runnable example"
    A complete, runnable version of everything on this page lives in
    [`examples/groups_and_attributes.rs`](https://github.com/stephenberry/hdf5-pure/blob/main/examples/groups_and_attributes.rs).
    Run it with:

    ```bash
    cargo run --example groups_and_attributes
    ```

## Building a hierarchy

Groups are created with a builder API. `FileBuilder::create_group(name)` returns a `GroupBuilder` for a top-level group, and `GroupBuilder::create_group(name)` returns a nested `GroupBuilder` for a sub-group. Each group builder can hold datasets (via `create_dataset`), attributes (via `set_attr`), and further sub-groups.

A group builder is not part of the file until it is finished and attached: call `finish()` to turn it into a `FinishedGroup`, then pass that to its parent's `add_group()`. For a sub-group, the parent is another `GroupBuilder`; for a top-level group, the parent is the `FileBuilder`.

```rust
use hdf5_pure::{AttrValue, File, FileBuilder};

let mut builder = FileBuilder::new();

// A group with its own datasets and attributes.
let mut sensors = builder.create_group("sensors");
sensors.set_attr("location", AttrValue::AsciiString("lab_a".into()));
sensors.set_attr("channels", AttrValue::I64Array(vec![0, 1, 2]));
sensors
    .create_dataset("pressure")
    .with_f32_data(&[101.3, 101.5, 101.4]);
sensors
    .create_dataset("humidity")
    .with_f32_data(&[40.0, 41.5]);

// A nested sub-group. Build it, finish it, and attach it to its parent.
let mut imu = sensors.create_group("imu");
imu.set_attr("model", AttrValue::String("MPU-9250".into()));
imu.create_dataset("accel").with_f64_data(&[0.0, 0.0, 9.81]);
sensors.add_group(imu.finish());

// Attach the top-level group to the file.
builder.add_group(sensors.finish());
```

The pattern is uniform at every level: build, finish, attach. A child must be finished and added before its parent is finished, since `finish()` consumes the builder.

See the [writing guide](writing.md) for the full dataset builder API used inside groups.

!!! note "Group format is fixed, not configurable"
    There is no group creation property list: every group is written with the same fixed, timestamp-free, new-style layout regardless of settings or child count. See [Limitations](../reference/limitations.md#group-creation-property-list-gcpl) for details.

## Attributes

Attributes are small named pieces of metadata. Set them on the root via `FileBuilder::set_attr`, on a group via `GroupBuilder::set_attr`, or on a dataset via `DatasetBuilder::set_attr` (the dataset form is chainable and returns `&mut Self`). The value is an `AttrValue`, an enum covering the common scalar, array, and string encodings:

```rust
// Root-level metadata.
builder.set_attr("title", AttrValue::String("experiment 7".into()));
builder.set_attr("run", AttrValue::I64(7));
builder.set_attr("calibration", AttrValue::F64Array(vec![0.1, 0.2, 0.3]));
```

The `AttrValue` variants and their HDF5 encodings are:

| Variant | HDF5 encoding |
|---|---|
| `AttrValue::F64` | 64-bit float scalar |
| `AttrValue::F64Array` | 64-bit float array |
| `AttrValue::I32` | Signed 32-bit integer scalar |
| `AttrValue::I64` | Signed 64-bit integer scalar |
| `AttrValue::I64Array` | Signed 64-bit integer array |
| `AttrValue::U32` | Unsigned 32-bit integer scalar |
| `AttrValue::U64` | Unsigned 64-bit integer scalar |
| `AttrValue::String` | UTF-8 string (null-padded) |
| `AttrValue::StringArray` | Array of UTF-8 strings |
| `AttrValue::AsciiString` | Fixed-width ASCII string (charset = ASCII) |
| `AttrValue::AsciiStringArray` | Array of fixed-width ASCII strings (null-padded to the longest element) |
| `AttrValue::VarLenAsciiArray` | Array of variable-length ASCII strings (uses a global heap collection) |

!!! note
    `AttrValue::AsciiString`, `AttrValue::AsciiStringArray`, and `AttrValue::VarLenAsciiArray` exist for compatibility with MATLAB and matio, which expect fixed-width or variable-length ASCII rather than UTF-8 for certain conventional attributes. See the [data types reference](../reference/data-types.md) for the full type mapping.

## Reading the hierarchy back

Open the file and start from `File::root()`, which returns a `Group` for the root. From any `Group` you can list its contents and read its attributes:

- `groups()` returns the names of child groups (`Vec<String>`).
- `datasets()` returns the names of child datasets (`Vec<String>`).
- `attrs()` returns the attributes as a `HashMap<String, AttrValue>`.

```rust
let file = File::from_bytes(builder.finish().unwrap()).unwrap();

let root = file.root();
let root_attrs = root.attrs().unwrap(); // HashMap<String, AttrValue>

let sensors = file.group("sensors").unwrap();
println!("child groups: {:?}", sensors.groups().unwrap());   // ["imu"]
println!("datasets:     {:?}", sensors.datasets().unwrap()); // ["humidity", "pressure"]
println!("attributes:   {:?}", sensors.attrs().unwrap());
```

`File::group(path)` resolves a group by path, and `Group::group(name)` resolves a child relative to that group. The names returned by `groups()` and `datasets()` are not sorted in any guaranteed order, so sort them yourself if you need a stable listing.

### Reading an attribute value

An attribute reads back as the variant it was written from: the dataspace kind distinguishes a scalar from a one-element array, and the datatype's charset selects the `Ascii` variants. A `VarLenAsciiArray` of one element stays a `VarLenAsciiArray`, and an `AsciiString` does not arrive as a `String`.

That fidelity means several variants can carry the same logical value, so match on the variant only when the encoding is what you care about. Otherwise use the accessors, each of which spans every variant that can hold the shape it names:

```rust
let attrs = file.root().attrs().unwrap();

// Any single string, either charset, scalar or one-element array.
let class: Option<&str> = attrs.get("MATLAB_class").and_then(AttrValue::as_str);

// Every element, with a scalar reading as one element.
let fields: Option<Vec<&str>> = attrs.get("MATLAB_fields").and_then(AttrValue::as_strings);
```

`as_i64`, `as_u64`, `as_f64`, `to_i64s`, `to_u64s` and `to_f64s` do the same for numbers. The prefix states the cost: `as_*` borrows or copies, `to_*` allocates. `as_i64` widens the narrower integer variants and reports `None` for a value above `i64::MAX` rather than wrapping it — per element, so the same holds for `to_i64s` at any length. The float accessors do not convert integers, so a caller that accepts either asks for both.

What a read cannot recover, because `AttrValue` has no way to express it. Each of these reads correctly but would be rewritten differently:

- **Width.** Integers and floats widen to `i64`/`u64`/`f64`; there are no narrower array variants.
- **Variable-length strings.** A true `H5T_STRING` with `STRSIZE = VAR` — what h5py writes, and what this crate's writer never emits — has no variant of its own and reads as the fixed-width variant of the same charset.
- **Rank.** Every array variant is one-dimensional, so a rank-2 attribute reads as its elements flattened.
- **Padding and declared width.** A fixed-width string reports its content, not its `STRSIZE` or which padding it used.

The variant may become **more specific** in a future release as `AttrValue` grows narrower variants, so match with a `_` arm — which the `#[non_exhaustive]` enum requires anyway — or read through the accessors, which are unaffected by such a change.

### Addressing datasets

Datasets are addressable two ways: by full path from the file, or by name from their parent group. Both resolve to the same dataset.

```rust
// Full path from the file.
let accel = file.dataset("sensors/imu/accel").unwrap();
println!("{:?}", accel.read_f64().unwrap()); // [0.0, 0.0, 9.81]

// By name, relative to the parent group.
let imu = file.group("sensors/imu").unwrap();
let accel = imu.dataset("accel").unwrap();
```

See the [reading guide](reading.md) for the dataset read API (`read_f64`, `shape`, and friends).

## MATLAB struct convention

The ASCII attribute variants exist primarily so that groups can follow MATLAB's struct convention: a struct is a group carrying `MATLAB_class = "struct"` and a `MATLAB_fields` attribute (typically `AttrValue::VarLenAsciiArray`) listing the field names, with each field stored as a child dataset tagged with its own `MATLAB_class`. See the [MATLAB interop page](../interop/matlab.md) for the full convention and worked examples.