///|
struct BuildNode {
value : ArrayValue?
compact : Bool
attributes : Array[Attribute]
}
///|
enum BuildLink {
Object(String)
Path(String)
}
///|
/// A new file model; it never modifies an existing HDF5 byte buffer in place.
pub struct Document {
nodes : Map[String, BuildNode]
aliases : Map[String, BuildLink]
limits : Limits
}
///|
pub fn Document::new(
limits~ : Limits = Limits::default(),
) -> Document raise H5Error {
limits.validate()
{
nodes: Map([("/", { value: None, compact: false, attributes: [], })]),
aliases: Map([]),
limits,
}
}
///|
fn canonical_path(path : String) -> String raise H5Error {
let parts = path_parts(path)
if parts.is_empty() {
"/"
} else {
"/" + parts.join("/")
}
}
///|
fn parent_path(path : String) -> (String, String) raise H5Error {
let parts = path_parts(path)
guard parts.last() is Some(name) else {
raise Invalid("root has no parent", 0)
}
ignore(parts.pop())
(if parts.is_empty() { "/" } else { "/" + parts.join("/") }, name)
}
///|
fn Document::reserve(self : Document, path : String) -> Unit raise H5Error {
if self.nodes.contains(path) || self.aliases.contains(path) {
raise Invalid("object path already exists", 0)
}
let parts = path_parts(path)
if parts.length() > self.limits.max_depth {
raise Limit("object path depth")
}
if self.nodes.length() + self.aliases.length() >= self.limits.max_objects {
raise Limit("document objects")
}
let (parent, _) = parent_path(path)
guard self.nodes.get(parent) is Some(node) else { raise NotFound(parent) }
if node.value is Some(_) {
raise Invalid("parent is a dataset", 0)
}
}
///|
/// Parent groups must exist. Failure leaves the document unchanged.
pub fn Document::add_group(
self : Document,
path : String,
) -> Unit raise H5Error {
let path = canonical_path(path)
self.reserve(path)
self.nodes[path] = { value: None, compact: false, attributes: [], }
}
///|
pub fn Document::add_dataset(
self : Document,
path : String,
value : ArrayValue,
compact~ : Bool = false,
) -> Unit raise H5Error {
let path = canonical_path(path)
self.reserve(path)
ignore(
ArrayValue::new(value.dtype, value.space, value.raw, limits=self.limits),
)
if compact && value.raw.length() > 65000 {
raise Limit("compact data must fit a header message")
}
self.nodes[path] = { value: Some(value), compact, attributes: [], }
}
///|
/// Targets are existing real objects, not other aliases. Cyclic group links are valid.
pub fn Document::add_hard_link(
self : Document,
path : String,
target : String,
) -> Unit raise H5Error {
let path = canonical_path(path)
let target = canonical_path(target)
self.reserve(path)
if !self.nodes.contains(target) {
raise NotFound(target)
}
self.aliases[path] = Object(target)
}
///|
/// Soft links may be dangling; writing never accesses their target.
pub fn Document::add_soft_link(
self : Document,
path : String,
target : String,
) -> Unit raise H5Error {
let path = canonical_path(path)
self.reserve(path)
if target == "" || target.contains("\u0000") {
raise Invalid("invalid soft link target", 0)
}
let (_, name) = parent_path(path)
ignore(encode_soft_link(name, target))
self.aliases[path] = Path(target)
}
///|
pub fn Document::set_attribute(
self : Document,
path : String,
name : String,
value : ArrayValue,
) -> Unit raise H5Error {
let path = canonical_path(path)
guard self.nodes.get(path) is Some(node) else { raise NotFound(path) }
let attribute : Attribute = { name, value, }
ignore(
ArrayValue::new(value.dtype, value.space, value.raw, limits=self.limits),
)
ignore(encode_attribute(attribute, self.limits))
for i = 0; i < node.attributes.length(); i = i + 1 {
if node.attributes[i].name == name {
node.attributes[i] = attribute
return
}
}
if node.attributes.length() >= self.limits.max_objects {
raise Limit("attribute count")
}
node.attributes.push(attribute)
}
///|
fn Document::node_paths(self : Document) -> Array[String] {
let paths = self.nodes.keys().collect()
paths.sort()
paths
}
///|
test "document mutations validate parents and fail atomically" {
let doc = Document::new()
doc.add_group("/run")
let v = ArrayValue::from_signed(Scalar, [42L])
doc.add_dataset("run/value", v)
assert_true(
try {
doc.add_group("/missing/child")
false
} catch {
_ => true
},
)
assert_true(
try {
doc.add_group("/run/value/child")
false
} catch {
_ => true
},
)
doc.set_attribute("/run", "units", ArrayValue::from_strings(Scalar, ["K"], 1))
assert_eq(doc.node_paths(), ["/", "/run", "/run/value"])
}