///|
/// A concrete object reached through the resolver.
///
/// `file` is the registered source name, or `` for the source passed to
/// `Hdf5Resolver::new`. `path` is the canonical path inside that source after
/// following soft and external links.
#derive(Eq, @debug.Debug)
pub(all) struct Hdf5ResolvedNode {
file : String
path : String
address : UInt64
kind : Hdf5ObjectKind
}
///|
pub extend Hdf5ResolvedNode with Eq::{not_equal, equal}
///|
pub extend Hdf5ResolvedNode with @debug.Debug::{to_repr}
///|
priv struct ResolverSource {
name : String
source : &Hdf5Source
}
///|
/// Resolves paths across a root HDF5 source and registered external-link or
/// external-raw-data targets. Sources are kept in memory and parsed on demand.
pub struct Hdf5Resolver {
priv sources : Array[ResolverSource]
}
///|
fn resolver_root_name() -> String {
""
}
///|
fn resolver_file_name(name : String) -> String raise Hdf5Error {
guard name != "" else { raise InvalidArgument("source name is empty") }
for byte in @utf8.encode(name) {
guard byte != b'\x00' else {
raise InvalidArgument("source names cannot contain NUL")
}
}
let segments : Array[String] = []
for part in name.split("/") {
let segment = part.to_owned()
if segment == "" || segment == "." {
continue
}
if segment == ".." {
if segments.length() > 0 {
segments.pop() |> ignore
}
} else {
segments.push(segment)
}
}
let mut normalized = ""
for index in 0.. 0 {
normalized = normalized + "/"
}
normalized = normalized + segments[index]
}
guard normalized != "" else { raise InvalidArgument("source name is empty") }
normalized
}
///|
fn resolver_parent_directory(name : String) -> String {
let segments : Array[String] = []
for part in name.split("/") {
let segment = part.to_owned()
if segment != "" && segment != "." {
segments.push(segment)
}
}
if segments.length() > 0 {
segments.pop() |> ignore
}
let mut path = ""
for index in 0.. 0 {
path = path + "/"
}
path = path + segments[index]
}
path
}
///|
fn resolver_basename(name : String) -> String {
let segments : Array[String] = []
for part in name.split("/") {
let segment = part.to_owned()
if segment != "" && segment != "." {
segments.push(segment)
}
}
if segments.length() == 0 {
""
} else {
segments[segments.length() - 1]
}
}
///|
fn resolver_object_segments(path : String) -> Array[String] {
let segments : Array[String] = []
for part in path.split("/") {
let segment = part.to_owned()
if segment == "" || segment == "." {
continue
}
if segment == ".." {
if segments.length() > 0 {
segments.pop() |> ignore
}
} else {
segments.push(segment)
}
}
segments
}
///|
fn resolver_object_path(segments : Array[String]) -> String {
if segments.length() == 0 {
return "/"
}
let mut path = ""
for segment in segments {
path = path + "/" + segment
}
path
}
///|
fn resolver_canonical_object_path(base : String, target : String) -> String {
let segments : Array[String] = []
if !target.has_prefix("/") {
for segment in resolver_object_segments(base) {
segments.push(segment)
}
}
for part in target.split("/") {
let segment = part.to_owned()
if segment == "" || segment == "." {
continue
}
if segment == ".." {
if segments.length() > 0 {
segments.pop() |> ignore
}
} else {
segments.push(segment)
}
}
resolver_object_path(segments)
}
///|
fn resolver_append_path(
base : String,
segments : Array[String],
start : Int32,
) -> String {
let result = resolver_object_segments(base)
for index in start.. String {
file + "|" + path
}
///|
fn Hdf5Resolver::source_index(self : Hdf5Resolver, name : String) -> Int32? {
for index in 0.. Hdf5Resolver {
{ sources: [{ name: resolver_root_name(), source, }], }
}
///|
/// Registers an external-link target under a stable source name.
pub fn Hdf5Resolver::add_source(
self : Hdf5Resolver,
name : String,
source : &Hdf5Source,
) -> Unit raise Hdf5Error {
let normalized = resolver_file_name(name)
guard normalized != resolver_root_name() else {
raise InvalidArgument(" is reserved for the root source")
}
guard self.source_index(normalized) is None else {
raise InvalidArgument("source name is already registered: " + normalized)
}
self.sources.push({ name: normalized, source, })
}
///|
/// Opens the root source or a registered external-link target.
pub fn Hdf5Resolver::open(
self : Hdf5Resolver,
name : String,
) -> Hdf5File raise Hdf5Error {
let normalized = if name == resolver_root_name() {
name
} else {
resolver_file_name(name)
}
match self.source_index(normalized) {
Some(index) => Hdf5File::parse_source(self.sources[index].source)
None => raise InvalidField("source is not registered: " + name)
}
}
///|
fn Hdf5Resolver::external_source_name(
self : Hdf5Resolver,
referring_file : String,
target_file : String,
) -> String raise Hdf5Error {
let normalized = resolver_file_name(target_file)
guard self.source_index(normalized) is None else { return normalized }
let directory = if referring_file == resolver_root_name() {
""
} else {
resolver_parent_directory(referring_file)
}
if directory != "" && !target_file.has_prefix("/") {
let relative = resolver_file_name(directory + "/" + target_file)
guard self.source_index(relative) is None else { return relative }
}
let basename = resolver_basename(normalized)
if basename != normalized {
guard self.source_index(basename) is None else { return basename }
}
raise InvalidField("external target file is not registered: " + target_file)
}
///|
fn Hdf5Resolver::resolve_from(
self : Hdf5Resolver,
file_name : String,
file : Hdf5File,
base_path : String,
base_address : UInt64,
path : String,
depth : Int32,
visited : Array[String],
) -> Hdf5ResolvedNode raise Hdf5Error {
guard depth < 64 else { raise InvalidField("too many link indirections") }
let canonical = resolver_canonical_object_path(base_path, path)
let key = resolver_visit_key(file_name, canonical)
guard !visited.contains(key) else {
raise InvalidField("soft or external link cycle detected at " + key)
}
visited.push(key)
let segments = resolver_object_segments(canonical)
if segments.length() == 0 {
let header = file.object_header_at(base_address)
return {
file: file_name,
path: "/",
address: base_address,
kind: object_kind(header),
}
}
let mut current_path = "/"
let mut current_address = file.root_address()
let mut index = 0
while index < segments.length() {
let header = file.object_header_at(current_address)
let links = file.links(header)
let segment = segments[index]
let link = match links.search_by(link => link.name == segment) {
Some(found) => links[found]
None => raise InvalidField("path component not found: " + segment)
}
match link.link_target {
Some(target) => {
let target_path = match link.external_path {
Some(value) => value
None => ""
}
if link.cache_type == 64U {
let target_file = self.external_source_name(file_name, target)
let target_source = self.open(target_file)
let target_root = resolver_canonical_object_path("/", target_path)
let resolved_path = resolver_append_path(
target_root,
segments,
index + 1,
)
return self.resolve_from(
target_file,
target_source,
"/",
target_source.root_address(),
resolved_path,
depth + 1,
visited,
)
}
let rebased = resolver_canonical_object_path(current_path, target)
let resolved_path = resolver_append_path(rebased, segments, index + 1)
return self.resolve_from(
file_name,
file,
"/",
file.root_address(),
resolved_path,
depth + 1,
visited,
)
}
None => {
current_path = resolver_append_path(current_path, segments, index)
current_address = link.object_header_address
index = index + 1
}
}
}
let header = file.object_header_at(current_address)
{
file: file_name,
path: current_path,
address: current_address,
kind: object_kind(header),
}
}
///|
/// Resolves a path in the root source, following soft and external links.
pub fn Hdf5Resolver::resolve(
self : Hdf5Resolver,
path : String,
) -> Hdf5ResolvedNode raise Hdf5Error {
let root = self.open(resolver_root_name())
self.resolve_from(
resolver_root_name(),
root,
"/",
root.root_address(),
path,
0,
[],
)
}
///|
/// Reads a dataset through the resolver, including external-link targets and
/// contiguous external raw-data storage.
pub fn Hdf5Resolver::read_dataset(
self : Hdf5Resolver,
path : String,
) -> Dataset raise Hdf5Error {
let node = self.resolve(path)
guard node.kind == Dataset else {
raise InvalidField("object is not a dataset: " + path)
}
let file = self.open(node.file)
let header = file.object_header_at(node.address)
if header.first(msg_external_data_files) is Some(_) {
let metadata = read_dataset_metadata(
header,
file.superblock.offset_size,
file.superblock.length_size,
)
return self.read_external_dataset(node.file, file, header, metadata)
}
file.read_dataset(node.path)
}
///|
/// Reads dataset metadata through the resolver.
pub fn Hdf5Resolver::read_dataset_metadata(
self : Hdf5Resolver,
path : String,
) -> DatasetMetadata raise Hdf5Error {
let node = self.resolve(path)
guard node.kind == Dataset else {
raise InvalidField("object is not a dataset: " + path)
}
self.open(node.file).read_dataset_metadata(node.path)
}
///|
/// Reads a dataset selection through the resolver, including external raw data.
pub fn Hdf5Resolver::read_dataset_selection(
self : Hdf5Resolver,
path : String,
selection : DatasetSelection,
) -> DatasetSelectionResult raise Hdf5Error {
let node = self.resolve(path)
guard node.kind == Dataset else {
raise InvalidField("object is not a dataset: " + path)
}
let file = self.open(node.file)
let header = file.object_header_at(node.address)
if header.first(msg_external_data_files) is Some(_) {
let metadata = read_dataset_metadata(
header,
file.superblock.offset_size,
file.superblock.length_size,
)
return self.read_external_dataset_selection(
node.file,
file,
header,
metadata,
selection,
)
}
file.read_dataset_selection(node.path, selection)
}
///|
/// Creates a bounded-memory dataset iterator through the resolver, including
/// tiles backed by contiguous external raw-data storage.
pub fn Hdf5Resolver::chunk_iterator(
self : Hdf5Resolver,
path : String,
tile_dimensions : Array[UInt64],
) -> DatasetChunkIterator raise Hdf5Error {
let node = self.resolve(path)
guard node.kind == Dataset else {
raise InvalidField("object is not a dataset: " + path)
}
let file = self.open(node.file)
let header = file.object_header_at(node.address)
let metadata = read_dataset_metadata(
header,
file.superblock.offset_size,
file.superblock.length_size,
)
make_dataset_chunk_iterator(
file,
header,
metadata,
tile_dimensions,
Some(self),
node.file,
)
}
///|
/// Reads attributes through the resolver.
pub fn Hdf5Resolver::attributes(
self : Hdf5Resolver,
path : String,
) -> Array[Attribute] raise Hdf5Error {
let node = self.resolve(path)
self.open(node.file).attributes(node.path)
}