h5rs: URLs as FILE arguments (feature remote)
With the `remote` feature (`remote-https` for https://), ls, dump, stat and diff take an http(s):// (or s3://, gs://, az:// with those clawhdf5-remote features) URL wherever they take a file, and read it by range requests through clawhdf5-remote's block cache. check validates every byte, so it downloads a remote file whole and checks it as before. Without the feature a URL is a clean error naming it. The tools read the file through File::storage instead of as_bytes: object headers, shared messages, attributes, v1 and v2 group links, dense storage (fractal heaps and v2 B-trees), path resolution, chunk listings and variable-length values go through the format crate's *_in functions, and the fractal-heap block verifier reads each block through the storage (a read failure of a remote file is reported as a problem, not as "past the end of the file"). A local file's storage is its mapped bytes, so its reads are still slices. stat's file size comes from the opened file, so it is right for a URL. Tests: tests/remote.rs serves fixtures (old and new formats, a paged file, a metadata cache image, a multi-block fractal heap, compounds, v1 groups) with the clawhdf5-remote test server and requires every subcommand's output and exit status for the URL to equal the local file's, and diff of the two to be clean; 404s, non-HDF5 bodies and https without its feature are clean errors. Local output is unchanged: the old and new h5rs print the same for ls -r -v, dump, stat and check --data on the 747 conformance and CVE corpus files (tank, 2026-09-26; the dumps of h5diff_hyper1/2.h5 were too large for the comparison script, their ls, stat and check agree), except cve-2025-2310.h5, whose dump error messages differ between runs of the old binary too (which failing chunk is reported first). ci-test.sh lints h5rs with remote-https, runs the URL tests and checks h5rs with remote for C. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
@@ -14,9 +14,16 @@ readme = "README.md"
|
|||||||
name = "h5rs"
|
name = "h5rs"
|
||||||
path = "src/main.rs"
|
path = "src/main.rs"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
# FILE arguments may be URLs: http:// with `remote` (no C), https:// with
|
||||||
|
# `remote-https` (rustls + ring, which compiles C).
|
||||||
|
remote = ["dep:clawhdf5-remote"]
|
||||||
|
remote-https = ["remote", "clawhdf5-remote/https"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
clawhdf5 = { path = "../clawhdf5", version = "2.7.0" }
|
clawhdf5 = { path = "../clawhdf5", version = "2.7.0" }
|
||||||
clawhdf5-format = { path = "../clawhdf5-format", version = "2.7.0" }
|
clawhdf5-format = { path = "../clawhdf5-format", version = "2.7.0" }
|
||||||
|
clawhdf5-remote = { path = "../clawhdf5-remote", version = "2.7.0", optional = true }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
|||||||
@@ -24,6 +24,26 @@ Every command takes `--max-bytes N` where it reads values (default 1 GiB): a
|
|||||||
dataset whose dataspace claims more than that is reported instead of read, so
|
dataset whose dataspace claims more than that is reported instead of read, so
|
||||||
a corrupt size cannot exhaust memory.
|
a corrupt size cannot exhaust memory.
|
||||||
|
|
||||||
|
### Remote files
|
||||||
|
|
||||||
|
Built with the `remote` feature, every FILE argument may be an `http://`
|
||||||
|
URL (`remote-https` adds `https://`, through rustls and ring, which compiles
|
||||||
|
C; the default build has neither). The file is read by range requests
|
||||||
|
through [clawhdf5-remote](../clawhdf5-remote/README.md)'s block cache, so
|
||||||
|
`ls` of a large file fetches its metadata blocks, not the file:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo install --path crates/clawhdf5-tools --features remote
|
||||||
|
h5rs ls -r -v http://127.0.0.1:8000/file.h5
|
||||||
|
h5rs dump http://127.0.0.1:8000/file.h5
|
||||||
|
h5rs diff local.h5 http://127.0.0.1:8000/file.h5
|
||||||
|
```
|
||||||
|
|
||||||
|
A URL names the whole file (`FILE/OBJECT` suffixes are for local paths).
|
||||||
|
`check` validates every byte, so it downloads a remote file whole first.
|
||||||
|
The output is the local file's (`tests/remote.rs` compares every
|
||||||
|
subcommand).
|
||||||
|
|
||||||
## `h5rs ls`
|
## `h5rs ls`
|
||||||
|
|
||||||
```console
|
```console
|
||||||
|
|||||||
@@ -148,13 +148,24 @@ pub fn run(args: &mut Args, out: &mut Out) -> std::io::Result<i32> {
|
|||||||
return args.usage_error(out, "missing FILE", USAGE);
|
return args.usage_error(out, "missing FILE", USAGE);
|
||||||
};
|
};
|
||||||
let path = std::path::Path::new(&file);
|
let path = std::path::Path::new(&file);
|
||||||
if !path.is_file() {
|
let mut h5 = if crate::h5::is_url(&file) {
|
||||||
writeln!(out.e, "h5rs check: {file}: no such file")?;
|
// check validates every byte, so a remote file is downloaded whole.
|
||||||
return Ok(2);
|
match H5::open_arg_whole(&file) {
|
||||||
}
|
Ok(h) => h,
|
||||||
let mut h5 = match H5::open(path) {
|
Err(e) => {
|
||||||
Ok(h) => h,
|
writeln!(out.e, "h5rs check: {e}")?;
|
||||||
Err(_) => return unopenable(path, out),
|
return Ok(2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if !path.is_file() {
|
||||||
|
writeln!(out.e, "h5rs check: {file}: no such file")?;
|
||||||
|
return Ok(2);
|
||||||
|
}
|
||||||
|
match H5::open(path) {
|
||||||
|
Ok(h) => h,
|
||||||
|
Err(_) => return unopenable(path, out),
|
||||||
|
}
|
||||||
};
|
};
|
||||||
if let Some(m) = max_bytes {
|
if let Some(m) = max_bytes {
|
||||||
h5.max_bytes = m;
|
h5.max_bytes = m;
|
||||||
|
|||||||
@@ -174,7 +174,7 @@ pub fn run(args: &mut Args, out: &mut Out) -> std::io::Result<i32> {
|
|||||||
}
|
}
|
||||||
let mut files = Vec::new();
|
let mut files = Vec::new();
|
||||||
for f in &pos[..2] {
|
for f in &pos[..2] {
|
||||||
match H5::open(std::path::Path::new(f)) {
|
match H5::open_arg(f) {
|
||||||
Ok(mut h) => {
|
Ok(mut h) => {
|
||||||
if let Some(m) = max_bytes {
|
if let Some(m) = max_bytes {
|
||||||
h.max_bytes = m;
|
h.max_bytes = m;
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ pub fn run(args: &mut Args, out: &mut Out) -> std::io::Result<i32> {
|
|||||||
let Some(file) = file else {
|
let Some(file) = file else {
|
||||||
return args.usage_error(out, "missing FILE", USAGE);
|
return args.usage_error(out, "missing FILE", USAGE);
|
||||||
};
|
};
|
||||||
let mut h5 = match H5::open(std::path::Path::new(&file)) {
|
let mut h5 = match H5::open_arg(&file) {
|
||||||
Ok(h) => h,
|
Ok(h) => h,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
writeln!(out.e, "h5rs dump: {e}")?;
|
writeln!(out.e, "h5rs dump: {e}")?;
|
||||||
|
|||||||
+115
-24
@@ -10,9 +10,9 @@ use std::collections::HashMap;
|
|||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use clawhdf5::File;
|
use clawhdf5::File;
|
||||||
use clawhdf5_format::attribute::{AttributeMessage, extract_attributes_tolerant};
|
use clawhdf5_format::attribute::{AttributeMessage, extract_attributes_tolerant_in};
|
||||||
use clawhdf5_format::attribute_info::AttributeInfoMessage;
|
use clawhdf5_format::attribute_info::AttributeInfoMessage;
|
||||||
use clawhdf5_format::btree_v2::{BTreeV2Header, collect_btree_v2_records};
|
use clawhdf5_format::btree_v2::{BTreeV2Header, collect_btree_v2_records_in};
|
||||||
use clawhdf5_format::data_layout::DataLayout;
|
use clawhdf5_format::data_layout::DataLayout;
|
||||||
use clawhdf5_format::dataspace::{Dataspace, DataspaceType};
|
use clawhdf5_format::dataspace::{Dataspace, DataspaceType};
|
||||||
use clawhdf5_format::datatype::Datatype;
|
use clawhdf5_format::datatype::Datatype;
|
||||||
@@ -24,6 +24,7 @@ use clawhdf5_format::link_info::LinkInfoMessage;
|
|||||||
use clawhdf5_format::link_message::{LinkMessage, LinkTarget};
|
use clawhdf5_format::link_message::{LinkMessage, LinkTarget};
|
||||||
use clawhdf5_format::message_type::MessageType;
|
use clawhdf5_format::message_type::MessageType;
|
||||||
use clawhdf5_format::object_header::ObjectHeader;
|
use clawhdf5_format::object_header::ObjectHeader;
|
||||||
|
use clawhdf5_format::storage::Storage;
|
||||||
use clawhdf5_format::superblock::Superblock;
|
use clawhdf5_format::superblock::Superblock;
|
||||||
use clawhdf5_format::symbol_table::SymbolTableMessage;
|
use clawhdf5_format::symbol_table::SymbolTableMessage;
|
||||||
|
|
||||||
@@ -186,8 +187,11 @@ pub struct Link {
|
|||||||
|
|
||||||
/// An open file.
|
/// An open file.
|
||||||
pub struct H5 {
|
pub struct H5 {
|
||||||
|
/// The file's path, or its URL for a remote file.
|
||||||
pub path: PathBuf,
|
pub path: PathBuf,
|
||||||
pub file: File,
|
pub file: File,
|
||||||
|
/// Size of the whole file in bytes (user block included).
|
||||||
|
pub size: u64,
|
||||||
pub max_bytes: u64,
|
pub max_bytes: u64,
|
||||||
/// Fractal heaps whose blocks were verified: `None` = sound.
|
/// Fractal heaps whose blocks were verified: `None` = sound.
|
||||||
verified_heaps: RefCell<HashMap<u64, Option<Error>>>,
|
verified_heaps: RefCell<HashMap<u64, Option<Error>>>,
|
||||||
@@ -204,19 +208,90 @@ impl H5 {
|
|||||||
path.display()
|
path.display()
|
||||||
))
|
))
|
||||||
})?;
|
})?;
|
||||||
Ok(H5 {
|
let size = std::fs::metadata(path).map(|m| m.len()).unwrap_or(0);
|
||||||
path: path.to_path_buf(),
|
Ok(H5::new(path.to_path_buf(), file, size))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new(path: PathBuf, file: File, size: u64) -> H5 {
|
||||||
|
H5 {
|
||||||
|
path,
|
||||||
file,
|
file,
|
||||||
|
size,
|
||||||
max_bytes: DEFAULT_MAX_BYTES,
|
max_bytes: DEFAULT_MAX_BYTES,
|
||||||
verified_heaps: RefCell::new(HashMap::new()),
|
verified_heaps: RefCell::new(HashMap::new()),
|
||||||
})
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Open a command-line FILE argument: a path, or with the `remote`
|
||||||
|
/// feature an `http(s)://` (or `s3://`, `gs://`, `az://`) URL, read by
|
||||||
|
/// range requests through a block cache.
|
||||||
|
pub fn open_arg(arg: &str) -> Result<H5> {
|
||||||
|
if !is_url(arg) {
|
||||||
|
return H5::open(Path::new(arg));
|
||||||
|
}
|
||||||
|
#[cfg(feature = "remote")]
|
||||||
|
{
|
||||||
|
let storage =
|
||||||
|
clawhdf5_remote::storage_for_url(arg, &clawhdf5_remote::Options::default())
|
||||||
|
.map_err(|e| Error::new(format!("{arg}: {e}")))?;
|
||||||
|
let size = storage.len();
|
||||||
|
let file = File::open_storage(storage).map_err(|e| {
|
||||||
|
Error::new(format!("{arg}: not an HDF5 file this tool can open: {e}"))
|
||||||
|
})?;
|
||||||
|
Ok(H5::new(PathBuf::from(arg), file, size))
|
||||||
|
}
|
||||||
|
#[cfg(not(feature = "remote"))]
|
||||||
|
Err(Error::new(format!(
|
||||||
|
"{arg}: URLs need h5rs built with the `remote` feature"
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`H5::open_arg`], with a remote file downloaded whole into memory
|
||||||
|
/// first — for `check`, which validates every byte of the file anyway
|
||||||
|
/// and parses it as one slice ([`H5::data`]).
|
||||||
|
pub fn open_arg_whole(arg: &str) -> Result<H5> {
|
||||||
|
let h5 = H5::open_arg(arg)?;
|
||||||
|
if h5.file.contiguous_bytes().is_some() {
|
||||||
|
return Ok(h5);
|
||||||
|
}
|
||||||
|
#[cfg(feature = "remote")]
|
||||||
|
{
|
||||||
|
let storage =
|
||||||
|
clawhdf5_remote::storage_for_url(arg, &clawhdf5_remote::Options::default())
|
||||||
|
.map_err(|e| Error::new(format!("{arg}: {e}")))?;
|
||||||
|
let len = usize::try_from(storage.len())
|
||||||
|
.map_err(|_| Error::new(format!("{arg}: too large to download")))?;
|
||||||
|
let bytes = storage
|
||||||
|
.read_at(0, len)
|
||||||
|
.map_err(|e| Error::new(format!("{arg}: {e}")))?
|
||||||
|
.into_owned();
|
||||||
|
let size = bytes.len() as u64;
|
||||||
|
let file = File::from_bytes(bytes).map_err(|e| {
|
||||||
|
Error::new(format!("{arg}: not an HDF5 file this tool can open: {e}"))
|
||||||
|
})?;
|
||||||
|
Ok(H5::new(PathBuf::from(arg), file, size))
|
||||||
|
}
|
||||||
|
#[cfg(not(feature = "remote"))]
|
||||||
|
unreachable!("open_arg refuses URLs without the remote feature")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The file's bytes from the superblock on: what every address indexes.
|
/// The file's bytes from the superblock on: what every address indexes.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// For a remote file opened by [`H5::open_arg`]; read it through
|
||||||
|
/// [`H5::store`], or open it with [`H5::open_arg_whole`].
|
||||||
pub fn data(&self) -> &[u8] {
|
pub fn data(&self) -> &[u8] {
|
||||||
self.file.as_bytes()
|
self.file.as_bytes()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The same bytes as a [`Storage`], for local and remote files alike:
|
||||||
|
/// in memory a read is a slice of [`H5::data`], remote it is served by
|
||||||
|
/// the block cache.
|
||||||
|
pub fn store(&self) -> &dyn Storage {
|
||||||
|
self.file.storage()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn sb(&self) -> &Superblock {
|
pub fn sb(&self) -> &Superblock {
|
||||||
self.file.superblock()
|
self.file.superblock()
|
||||||
}
|
}
|
||||||
@@ -239,8 +314,8 @@ impl H5 {
|
|||||||
if let Some(e) = self.file.cache_image_error() {
|
if let Some(e) = self.file.cache_image_error() {
|
||||||
return Err(Error::at(addr, format!("metadata cache image: {e}")));
|
return Err(Error::at(addr, format!("metadata cache image: {e}")));
|
||||||
}
|
}
|
||||||
let off = usize::try_from(addr).map_err(|_| Error::at(addr, "address out of range"))?;
|
to_usize(addr)?;
|
||||||
ObjectHeader::parse(self.data(), off, self.os(), self.ls())
|
ObjectHeader::parse_in(self.store(), addr, self.os(), self.ls())
|
||||||
.map_err(|e| Error::at(addr, format!("object header: {e}")))
|
.map_err(|e| Error::at(addr, format!("object header: {e}")))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -249,11 +324,14 @@ impl H5 {
|
|||||||
pub fn payload(&self, h: &ObjectHeader, t: MessageType) -> Result<Option<Vec<u8>>> {
|
pub fn payload(&self, h: &ObjectHeader, t: MessageType) -> Result<Option<Vec<u8>>> {
|
||||||
match h.messages.iter().find(|m| m.msg_type == t) {
|
match h.messages.iter().find(|m| m.msg_type == t) {
|
||||||
None => Ok(None),
|
None => Ok(None),
|
||||||
Some(m) => {
|
Some(m) => clawhdf5_format::shared_message::message_data_in(
|
||||||
clawhdf5_format::shared_message::message_data(self.data(), m, self.os(), self.ls())
|
self.store(),
|
||||||
.map(|c| Some(c.into_owned()))
|
m,
|
||||||
.map_err(|e| Error::new(format!("{t:?} message: {e}")))
|
self.os(),
|
||||||
}
|
self.ls(),
|
||||||
|
)
|
||||||
|
.map(|c| Some(c.into_owned()))
|
||||||
|
.map_err(|e| Error::new(format!("{t:?} message: {e}"))),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -305,8 +383,9 @@ impl H5 {
|
|||||||
self.verified_heap(fh)
|
self.verified_heap(fh)
|
||||||
.map_err(|e| e.context("dense attribute storage"))?;
|
.map_err(|e| e.context("dense attribute storage"))?;
|
||||||
}
|
}
|
||||||
let (mut attrs, errs) = extract_attributes_tolerant(self.data(), h, self.os(), self.ls())
|
let (mut attrs, errs) =
|
||||||
.map_err(|e| Error::new(format!("attributes: {e}")))?;
|
extract_attributes_tolerant_in(self.store(), h, self.os(), self.ls())
|
||||||
|
.map_err(|e| Error::new(format!("attributes: {e}")))?;
|
||||||
attrs.sort_by(|a, b| a.name.cmp(&b.name));
|
attrs.sort_by(|a, b| a.name.cmp(&b.name));
|
||||||
Ok((attrs, errs.iter().map(|e| e.to_string()).collect()))
|
Ok((attrs, errs.iter().map(|e| e.to_string()).collect()))
|
||||||
}
|
}
|
||||||
@@ -316,7 +395,7 @@ impl H5 {
|
|||||||
pub fn links(&self, h: &ObjectHeader) -> Result<Vec<Link>> {
|
pub fn links(&self, h: &ObjectHeader) -> Result<Vec<Link>> {
|
||||||
let os = self.os();
|
let os = self.os();
|
||||||
let ls = self.ls();
|
let ls = self.ls();
|
||||||
let data = self.data();
|
let data = self.store();
|
||||||
let mut out = Vec::new();
|
let mut out = Vec::new();
|
||||||
if let Some(m) = h
|
if let Some(m) = h
|
||||||
.messages
|
.messages
|
||||||
@@ -325,7 +404,7 @@ impl H5 {
|
|||||||
{
|
{
|
||||||
let stm = SymbolTableMessage::parse(&m.data, os)
|
let stm = SymbolTableMessage::parse(&m.data, os)
|
||||||
.map_err(|e| Error::new(format!("symbol table message: {e}")))?;
|
.map_err(|e| Error::new(format!("symbol table message: {e}")))?;
|
||||||
let entries = group_v1::resolve_v1_group_entries(data, &stm, os, ls)
|
let entries = group_v1::resolve_v1_group_entries_in(data, &stm, os, ls)
|
||||||
.map_err(|e| Error::at(stm.btree_address, format!("symbol table: {e}")))?;
|
.map_err(|e| Error::at(stm.btree_address, format!("symbol table: {e}")))?;
|
||||||
let has_soft = entries.iter().any(group_v1::is_v1_soft_link);
|
let has_soft = entries.iter().any(group_v1::is_v1_soft_link);
|
||||||
for e in entries {
|
for e in entries {
|
||||||
@@ -337,7 +416,7 @@ impl H5 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if has_soft {
|
if has_soft {
|
||||||
let soft = group_v1::v1_soft_links(data, &stm, os, ls)
|
let soft = group_v1::v1_soft_links_in(data, &stm, os, ls)
|
||||||
.map_err(|e| Error::at(stm.btree_address, format!("soft links: {e}")))?;
|
.map_err(|e| Error::at(stm.btree_address, format!("soft links: {e}")))?;
|
||||||
for (name, target) in soft {
|
for (name, target) in soft {
|
||||||
out.push(Link {
|
out.push(Link {
|
||||||
@@ -387,15 +466,15 @@ impl H5 {
|
|||||||
name_type: u8,
|
name_type: u8,
|
||||||
) -> Result<Vec<Vec<u8>>> {
|
) -> Result<Vec<Vec<u8>>> {
|
||||||
self.verified_heap(heap)?;
|
self.verified_heap(heap)?;
|
||||||
let data = self.data();
|
let data = self.store();
|
||||||
let os = self.os();
|
let os = self.os();
|
||||||
let ls = self.ls();
|
let ls = self.ls();
|
||||||
let fh = FractalHeapHeader::parse(data, to_usize(heap)?, os, ls)
|
let fh = FractalHeapHeader::parse_in(data, to_usize(heap).map(|_| heap)?, os, ls)
|
||||||
.map_err(|e| Error::at(heap, format!("fractal heap header: {e}")))?;
|
.map_err(|e| Error::at(heap, format!("fractal heap header: {e}")))?;
|
||||||
let bt = btree.ok_or_else(|| Error::at(heap, "dense storage without a name index"))?;
|
let bt = btree.ok_or_else(|| Error::at(heap, "dense storage without a name index"))?;
|
||||||
let hdr = BTreeV2Header::parse(data, to_usize(bt)?, os, ls)
|
let hdr = BTreeV2Header::parse_in(data, to_usize(bt).map(|_| bt)?, os, ls)
|
||||||
.map_err(|e| Error::at(bt, format!("v2 B-tree header: {e}")))?;
|
.map_err(|e| Error::at(bt, format!("v2 B-tree header: {e}")))?;
|
||||||
let recs = collect_btree_v2_records(data, &hdr, os, ls)
|
let recs = collect_btree_v2_records_in(data, &hdr, os, ls)
|
||||||
.map_err(|e| Error::at(bt, format!("v2 B-tree: {e}")))?;
|
.map_err(|e| Error::at(bt, format!("v2 B-tree: {e}")))?;
|
||||||
// Name-index records: hash(4) + heap ID; creation-order ones: order(8) + heap ID.
|
// Name-index records: hash(4) + heap ID; creation-order ones: order(8) + heap ID.
|
||||||
let skip = if hdr.tree_type == name_type { 4 } else { 8 };
|
let skip = if hdr.tree_type == name_type { 4 } else { 8 };
|
||||||
@@ -407,7 +486,7 @@ impl H5 {
|
|||||||
.get(skip..skip + idlen)
|
.get(skip..skip + idlen)
|
||||||
.ok_or_else(|| Error::at(bt, "v2 B-tree record shorter than a heap ID"))?;
|
.ok_or_else(|| Error::at(bt, "v2 B-tree record shorter than a heap ID"))?;
|
||||||
let obj = fh
|
let obj = fh
|
||||||
.read_managed_object(data, id, os)
|
.read_managed_object_in(data, id, os)
|
||||||
.map_err(|e| Error::at(heap, format!("fractal heap object: {e}")))?;
|
.map_err(|e| Error::at(heap, format!("fractal heap object: {e}")))?;
|
||||||
out.push(obj);
|
out.push(obj);
|
||||||
}
|
}
|
||||||
@@ -561,7 +640,7 @@ impl H5 {
|
|||||||
if p.is_empty() {
|
if p.is_empty() {
|
||||||
return Ok(self.root());
|
return Ok(self.root());
|
||||||
}
|
}
|
||||||
clawhdf5_format::group_v2::resolve_path_any(self.data(), self.sb(), p)
|
clawhdf5_format::group_v2::resolve_path_any_in(self.store(), self.sb(), p)
|
||||||
.map_err(|e| Error::new(format!("{path}: {e}")))
|
.map_err(|e| Error::new(format!("{path}: {e}")))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -681,7 +760,9 @@ pub fn byte_len(ds: &Dataspace, dt: &Datatype) -> Result<u64> {
|
|||||||
/// Split a `FILE[/object/path]` argument the way h5ls does: the longest
|
/// Split a `FILE[/object/path]` argument the way h5ls does: the longest
|
||||||
/// prefix that is an existing file is the file.
|
/// prefix that is an existing file is the file.
|
||||||
pub fn split_file_arg(arg: &str) -> (String, Option<String>) {
|
pub fn split_file_arg(arg: &str) -> (String, Option<String>) {
|
||||||
if Path::new(arg).is_file() {
|
// A URL names the file only (its path cannot be split against the
|
||||||
|
// local file system).
|
||||||
|
if is_url(arg) || Path::new(arg).is_file() {
|
||||||
return (arg.to_string(), None);
|
return (arg.to_string(), None);
|
||||||
}
|
}
|
||||||
let mut idx: Vec<usize> = arg.match_indices('/').map(|(i, _)| i).collect();
|
let mut idx: Vec<usize> = arg.match_indices('/').map(|(i, _)| i).collect();
|
||||||
@@ -694,3 +775,13 @@ pub fn split_file_arg(arg: &str) -> (String, Option<String>) {
|
|||||||
}
|
}
|
||||||
(arg.to_string(), None)
|
(arg.to_string(), None)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether a FILE argument is a URL (`scheme://...`) rather than a path.
|
||||||
|
pub fn is_url(arg: &str) -> bool {
|
||||||
|
arg.split_once("://").is_some_and(|(scheme, _)| {
|
||||||
|
!scheme.is_empty()
|
||||||
|
&& scheme
|
||||||
|
.chars()
|
||||||
|
.all(|c| c.is_ascii_alphanumeric() || matches!(c, '+' | '-' | '.'))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,10 +4,12 @@
|
|||||||
//! heap header's flag says so). The library reads only the blocks an object
|
//! heap header's flag says so). The library reads only the blocks an object
|
||||||
//! lives in and does not verify block checksums, so `check` does it here.
|
//! lives in and does not verify block checksums, so `check` does it here.
|
||||||
|
|
||||||
|
use std::borrow::Cow;
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
|
||||||
use clawhdf5_format::checksum::jenkins_lookup3;
|
use clawhdf5_format::checksum::jenkins_lookup3;
|
||||||
use clawhdf5_format::fractal_heap::FractalHeapHeader;
|
use clawhdf5_format::fractal_heap::FractalHeapHeader;
|
||||||
|
use clawhdf5_format::storage::{Storage, read_exact_at};
|
||||||
|
|
||||||
use crate::h5::{Error, H5};
|
use crate::h5::{Error, H5};
|
||||||
|
|
||||||
@@ -26,7 +28,7 @@ pub struct HeapReport {
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct Walk<'a> {
|
struct Walk<'a> {
|
||||||
data: &'a [u8],
|
data: &'a dyn Storage,
|
||||||
heap: u64,
|
heap: u64,
|
||||||
fh: FractalHeapHeader,
|
fh: FractalHeapHeader,
|
||||||
checksum_dblocks: bool,
|
checksum_dblocks: bool,
|
||||||
@@ -60,14 +62,14 @@ fn log2(v: u64) -> u32 {
|
|||||||
/// parsed (and its checksum verified) by the library; an error there is
|
/// parsed (and its checksum verified) by the library; an error there is
|
||||||
/// returned as the only problem.
|
/// returned as the only problem.
|
||||||
pub fn verify(h5: &H5, heap: u64) -> HeapReport {
|
pub fn verify(h5: &H5, heap: u64) -> HeapReport {
|
||||||
let data = h5.data();
|
let data = h5.store();
|
||||||
let Ok(off) = usize::try_from(heap) else {
|
let Ok(off) = usize::try_from(heap) else {
|
||||||
return HeapReport {
|
return HeapReport {
|
||||||
problems: vec![Error::at(heap, "fractal heap address out of range")],
|
problems: vec![Error::at(heap, "fractal heap address out of range")],
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
let fh = match FractalHeapHeader::parse(data, off, h5.os(), h5.ls()) {
|
let fh = match FractalHeapHeader::parse_in(data, heap, h5.os(), h5.ls()) {
|
||||||
Ok(f) => f,
|
Ok(f) => f,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
return HeapReport {
|
return HeapReport {
|
||||||
@@ -77,7 +79,15 @@ pub fn verify(h5: &H5, heap: u64) -> HeapReport {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
// Flags: signature(4) version(1) heap ID length(2) filter length(2) flags(1).
|
// Flags: signature(4) version(1) heap ID length(2) filter length(2) flags(1).
|
||||||
let flags = data.get(off + 9).copied().unwrap_or(0);
|
let flags = match data.read_at(off as u64 + 9, 1) {
|
||||||
|
Ok(b) => b.first().copied().unwrap_or(0),
|
||||||
|
Err(e) => {
|
||||||
|
return HeapReport {
|
||||||
|
problems: vec![Error::at(heap, format!("fractal heap header: {e}"))],
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
let mut w = Walk {
|
let mut w = Walk {
|
||||||
data,
|
data,
|
||||||
heap,
|
heap,
|
||||||
@@ -107,11 +117,42 @@ pub fn verify(h5: &H5, heap: u64) -> HeapReport {
|
|||||||
w.r
|
w.r
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Walk<'_> {
|
impl<'a> Walk<'a> {
|
||||||
fn problem(&mut self, addr: u64, msg: impl Into<String>) {
|
fn problem(&mut self, addr: u64, msg: impl Into<String>) {
|
||||||
self.r.problems.push(Error::at(addr, msg));
|
self.r.problems.push(Error::at(addr, msg));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Bytes `[start, end)` of the file: `Ok(None)` when they run past its
|
||||||
|
/// end (what a slice `get` of the whole file answered), `Err` when the
|
||||||
|
/// storage fails to read them (a remote file).
|
||||||
|
fn get(&self, start: usize, end: usize) -> Result<Option<Cow<'a, [u8]>>, String> {
|
||||||
|
let Some(len) = end.checked_sub(start) else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
if end as u64 > self.data.len() {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
read_exact_at(self.data, start as u64, len)
|
||||||
|
.map(Some)
|
||||||
|
.map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`Walk::get`], recording a read failure as a problem at `addr`.
|
||||||
|
fn get_or_note(
|
||||||
|
&mut self,
|
||||||
|
addr: u64,
|
||||||
|
start: usize,
|
||||||
|
end: usize,
|
||||||
|
) -> Option<Option<Cow<'a, [u8]>>> {
|
||||||
|
match self.get(start, end) {
|
||||||
|
Ok(b) => Some(b),
|
||||||
|
Err(e) => {
|
||||||
|
self.problem(addr, format!("fractal heap block: {e}"));
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn row_size(&self, row: usize) -> Option<u64> {
|
fn row_size(&self, row: usize) -> Option<u64> {
|
||||||
let s = self.fh.starting_block_size;
|
let s = self.fh.starting_block_size;
|
||||||
if row <= 1 {
|
if row <= 1 {
|
||||||
@@ -155,10 +196,11 @@ impl Walk<'_> {
|
|||||||
return None;
|
return None;
|
||||||
};
|
};
|
||||||
let hdr_len = 5 + self.os + self.boff_bytes;
|
let hdr_len = 5 + self.os + self.boff_bytes;
|
||||||
let Some(b) = start
|
let b = match start.checked_add(hdr_len) {
|
||||||
.checked_add(hdr_len)
|
Some(e) => self.get_or_note(addr, start, e)?,
|
||||||
.and_then(|e| self.data.get(start..e))
|
None => None,
|
||||||
else {
|
};
|
||||||
|
let Some(b) = b else {
|
||||||
self.problem(
|
self.problem(
|
||||||
addr,
|
addr,
|
||||||
format!("fractal heap {what} block lies past the end of the file"),
|
format!("fractal heap {what} block lies past the end of the file"),
|
||||||
@@ -209,7 +251,10 @@ impl Walk<'_> {
|
|||||||
self.problem(addr, "fractal heap direct block size out of range");
|
self.problem(addr, "fractal heap direct block size out of range");
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let Some(block) = self.data.get(start..end) else {
|
let Some(block) = self.get_or_note(addr, start, end) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(block) = block else {
|
||||||
self.problem(
|
self.problem(
|
||||||
addr,
|
addr,
|
||||||
"fractal heap direct block extends past the end of the file",
|
"fractal heap direct block extends past the end of the file",
|
||||||
@@ -259,14 +304,17 @@ impl Walk<'_> {
|
|||||||
};
|
};
|
||||||
let direct = row < direct_rows;
|
let direct = row < direct_rows;
|
||||||
for _ in 0..width {
|
for _ in 0..width {
|
||||||
let Some(b) = self.data.get(pos..pos + self.os) else {
|
let Some(b) = self.get_or_note(addr, pos, pos + self.os) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(b) = b else {
|
||||||
self.problem(
|
self.problem(
|
||||||
addr,
|
addr,
|
||||||
"fractal heap indirect block extends past the end of the file",
|
"fractal heap indirect block extends past the end of the file",
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let child = le(b);
|
let child = le(&b);
|
||||||
pos += self.os;
|
pos += self.os;
|
||||||
if direct && filtered {
|
if direct && filtered {
|
||||||
pos += self.ls + 4;
|
pos += self.ls + 4;
|
||||||
@@ -277,7 +325,10 @@ impl Walk<'_> {
|
|||||||
off = off.saturating_add(rs);
|
off = off.saturating_add(rs);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let Some(stored) = self.data.get(pos..pos + 4) else {
|
let Some(stored) = self.get_or_note(addr, pos, pos + 4) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(stored) = stored else {
|
||||||
self.problem(
|
self.problem(
|
||||||
addr,
|
addr,
|
||||||
"fractal heap indirect block extends past the end of the file",
|
"fractal heap indirect block extends past the end of the file",
|
||||||
@@ -285,7 +336,10 @@ impl Walk<'_> {
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let stored = u32::from_le_bytes([stored[0], stored[1], stored[2], stored[3]]);
|
let stored = u32::from_le_bytes([stored[0], stored[1], stored[2], stored[3]]);
|
||||||
let computed = jenkins_lookup3(&self.data[start..pos]);
|
let Some(Some(body)) = self.get_or_note(addr, start, pos) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let computed = jenkins_lookup3(&body);
|
||||||
self.r.checksums += 1;
|
self.r.checksums += 1;
|
||||||
if computed != stored {
|
if computed != stored {
|
||||||
self.problem(
|
self.problem(
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
//! Dataset facts shared by `ls`, `dump`, `stat` and `check`: shape text,
|
//! Dataset facts shared by `ls`, `dump`, `stat` and `check`: shape text,
|
||||||
//! layout, filters and storage.
|
//! layout, filters and storage.
|
||||||
|
|
||||||
use clawhdf5_format::chunked_read::{ChunkInfo, list_chunks};
|
use clawhdf5_format::chunked_read::{ChunkInfo, list_chunks_in};
|
||||||
use clawhdf5_format::data_layout::DataLayout;
|
use clawhdf5_format::data_layout::DataLayout;
|
||||||
use clawhdf5_format::dataspace::{Dataspace, DataspaceType};
|
use clawhdf5_format::dataspace::{Dataspace, DataspaceType};
|
||||||
use clawhdf5_format::datatype::Datatype;
|
use clawhdf5_format::datatype::Datatype;
|
||||||
@@ -189,8 +189,8 @@ pub fn chunks(
|
|||||||
let Some(addr) = *btree_address else {
|
let Some(addr) = *btree_address else {
|
||||||
return Ok(Vec::new());
|
return Ok(Vec::new());
|
||||||
};
|
};
|
||||||
list_chunks(
|
list_chunks_in(
|
||||||
h5.data(),
|
h5.store(),
|
||||||
layout,
|
layout,
|
||||||
ds,
|
ds,
|
||||||
dt.type_size() as usize,
|
dt.type_size() as usize,
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ pub fn run(args: &mut Args, out: &mut Out) -> std::io::Result<i32> {
|
|||||||
return args.usage_error(out, "missing FILE", USAGE);
|
return args.usage_error(out, "missing FILE", USAGE);
|
||||||
};
|
};
|
||||||
let (file, obj) = split_file_arg(&target);
|
let (file, obj) = split_file_arg(&target);
|
||||||
let mut h5 = match H5::open(std::path::Path::new(&file)) {
|
let mut h5 = match H5::open_arg(&file) {
|
||||||
Ok(h) => h,
|
Ok(h) => h,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
writeln!(out.e, "h5rs ls: {e}")?;
|
writeln!(out.e, "h5rs ls: {e}")?;
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ pub fn run(args: &mut Args, out: &mut Out) -> std::io::Result<i32> {
|
|||||||
let Some(file) = file else {
|
let Some(file) = file else {
|
||||||
return args.usage_error(out, "missing FILE", USAGE);
|
return args.usage_error(out, "missing FILE", USAGE);
|
||||||
};
|
};
|
||||||
let h5 = match H5::open(std::path::Path::new(&file)) {
|
let h5 = match H5::open_arg(&file) {
|
||||||
Ok(h) => h,
|
Ok(h) => h,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
writeln!(out.e, "h5rs stat: {e}")?;
|
writeln!(out.e, "h5rs stat: {e}")?;
|
||||||
@@ -291,7 +291,7 @@ fn report(h5: &H5, file: &str, s: &Stats, out: &mut Out) -> std::io::Result<()>
|
|||||||
s.attr_objects
|
s.attr_objects
|
||||||
)?;
|
)?;
|
||||||
writeln!(o, "\tMax. # of attributes to objects: {}", s.max_attrs)?;
|
writeln!(o, "\tMax. # of attributes to objects: {}", s.max_attrs)?;
|
||||||
let total = std::fs::metadata(&h5.path).map(|m| m.len()).unwrap_or(0);
|
let total = h5.size;
|
||||||
let ub = h5.file.user_block_size();
|
let ub = h5.file.user_block_size();
|
||||||
writeln!(o, "Summary of file space information:")?;
|
writeln!(o, "Summary of file space information:")?;
|
||||||
writeln!(o, " User block: {ub} bytes")?;
|
writeln!(o, " User block: {ub} bytes")?;
|
||||||
|
|||||||
@@ -151,14 +151,14 @@ pub struct Decoder<'a> {
|
|||||||
pub h5: &'a H5,
|
pub h5: &'a H5,
|
||||||
/// Variable-length elements are resolved as the library resolves them
|
/// Variable-length elements are resolved as the library resolves them
|
||||||
/// (so as libhdf5 does), not by a decoder of our own.
|
/// (so as libhdf5 does), not by a decoder of our own.
|
||||||
vl: RefCell<VlResolver<'a>>,
|
vl: RefCell<VlResolver<'a, dyn clawhdf5_format::storage::Storage + 'a>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Decoder<'a> {
|
impl<'a> Decoder<'a> {
|
||||||
pub fn new(h5: &'a H5) -> Self {
|
pub fn new(h5: &'a H5) -> Self {
|
||||||
Self {
|
Self {
|
||||||
h5,
|
h5,
|
||||||
vl: RefCell::new(VlResolver::new(h5.data(), h5.os(), h5.ls())),
|
vl: RefCell::new(VlResolver::new_in(h5.store(), h5.os(), h5.ls())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -268,7 +268,7 @@ impl<'a> Decoder<'a> {
|
|||||||
/// has it.
|
/// has it.
|
||||||
fn decode_vlen(&self, is_string: bool, base: &Datatype, b: &[u8], depth: u32) -> Value {
|
fn decode_vlen(&self, is_string: bool, base: &Datatype, b: &[u8], depth: u32) -> Value {
|
||||||
if is_string {
|
if is_string {
|
||||||
return match self.vl.borrow_mut().string_element(b) {
|
return match self.vl.borrow_mut().string_element_in(b) {
|
||||||
Ok(Some(s)) => Value::Str(String::from_utf8_lossy(s).into_owned()),
|
Ok(Some(s)) => Value::Str(String::from_utf8_lossy(s).into_owned()),
|
||||||
Ok(None) => Value::NullStr,
|
Ok(None) => Value::NullStr,
|
||||||
Err(e) => Value::Error(e.to_string()),
|
Err(e) => Value::Error(e.to_string()),
|
||||||
@@ -278,8 +278,9 @@ impl<'a> Decoder<'a> {
|
|||||||
if bs == 0 {
|
if bs == 0 {
|
||||||
return Value::Error("VL base type of size 0".into());
|
return Value::Error("VL base type of size 0".into());
|
||||||
}
|
}
|
||||||
let obj = match self.vl.borrow_mut().element(b, bs) {
|
// Copied out: decoding an element may resolve nested ones.
|
||||||
Ok(o) => o.unwrap_or(&[]),
|
let obj = match self.vl.borrow_mut().element_in(b, bs) {
|
||||||
|
Ok(o) => o.unwrap_or(&[]).to_vec(),
|
||||||
Err(e) => return Value::Error(e.to_string()),
|
Err(e) => return Value::Error(e.to_string()),
|
||||||
};
|
};
|
||||||
Value::Seq(
|
Value::Seq(
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
//! `h5rs` on URLs (feature `remote`): every subcommand prints for
|
||||||
|
//! `http://…/file.h5` what it prints for the local file (the name aside).
|
||||||
|
//! The files are served by the range-request test server of
|
||||||
|
//! clawhdf5-remote on 127.0.0.1.
|
||||||
|
|
||||||
|
#![cfg(feature = "remote")]
|
||||||
|
|
||||||
|
#[path = "../../clawhdf5-remote/tests/common/server.rs"]
|
||||||
|
mod server;
|
||||||
|
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::process::Command;
|
||||||
|
|
||||||
|
fn h5rs(args: &[&str]) -> (String, i32) {
|
||||||
|
let out = Command::new(env!("CARGO_BIN_EXE_h5rs"))
|
||||||
|
.args(args)
|
||||||
|
.output()
|
||||||
|
.expect("run h5rs");
|
||||||
|
let text = format!(
|
||||||
|
"{}{}",
|
||||||
|
String::from_utf8_lossy(&out.stdout),
|
||||||
|
String::from_utf8_lossy(&out.stderr)
|
||||||
|
);
|
||||||
|
(text, out.status.code().unwrap_or(-1))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fixtures() -> Vec<PathBuf> {
|
||||||
|
let root = Path::new(env!("CARGO_MANIFEST_DIR"));
|
||||||
|
[
|
||||||
|
"../clawhdf5/tests/fixtures/tall.h5",
|
||||||
|
"../clawhdf5/tests/fixtures/written_by_v2_7_0.h5",
|
||||||
|
"../clawhdf5/tests/fixtures/written_by_v2_7_0_paged.h5",
|
||||||
|
"../clawhdf5/tests/fixtures/h5clear_mdc_image.h5",
|
||||||
|
"../clawhdf5-format/tests/fixtures/fractal_heap_multiblock.h5",
|
||||||
|
"../clawhdf5-format/tests/fixtures/legacy/tcompound.h5",
|
||||||
|
"../clawhdf5-format/tests/fixtures/legacy/h5ex_g_iterate.h5",
|
||||||
|
]
|
||||||
|
.iter()
|
||||||
|
.map(|p| root.join(p))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn every_subcommand_reads_a_url_like_the_local_file() {
|
||||||
|
let files = fixtures();
|
||||||
|
let served: Vec<(String, Vec<u8>)> = files
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(i, p)| {
|
||||||
|
let name = p.file_name().unwrap().to_str().unwrap();
|
||||||
|
(format!("/{i}/{name}"), std::fs::read(p).unwrap())
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let server = server::Server::start(served.clone());
|
||||||
|
for (p, (url_path, _)) in files.iter().zip(&served) {
|
||||||
|
let url = server.url(url_path);
|
||||||
|
let local = p.to_str().unwrap();
|
||||||
|
for cmd in [
|
||||||
|
&["ls", "-r", "-v"][..],
|
||||||
|
&["dump"],
|
||||||
|
&["dump", "--json"],
|
||||||
|
&["stat"],
|
||||||
|
&["check", "--data"],
|
||||||
|
] {
|
||||||
|
fn args<'a>(cmd: &[&'a str], f: &'a str) -> Vec<&'a str> {
|
||||||
|
cmd.iter().copied().chain([f]).collect()
|
||||||
|
}
|
||||||
|
let (want, want_rc) = h5rs(&args(cmd, local));
|
||||||
|
let (got, got_rc) = h5rs(&args(cmd, &url));
|
||||||
|
assert_eq!(
|
||||||
|
got.replace(&url, local),
|
||||||
|
want,
|
||||||
|
"h5rs {} {url}",
|
||||||
|
cmd.join(" ")
|
||||||
|
);
|
||||||
|
assert_eq!(got_rc, want_rc, "h5rs {} {url}", cmd.join(" "));
|
||||||
|
}
|
||||||
|
let (a, rc) = h5rs(&["diff", local, &url]);
|
||||||
|
assert_eq!(rc, 0, "h5rs diff {local} {url}: {a}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn url_errors_are_clean() {
|
||||||
|
let server = server::Server::start(vec![("/x.h5".into(), vec![1u8; 100])]);
|
||||||
|
let (out, rc) = h5rs(&["ls", &server.url("/missing.h5")]);
|
||||||
|
assert_eq!(rc, 2, "{out}");
|
||||||
|
assert!(out.contains("404"), "{out}");
|
||||||
|
let (out, rc) = h5rs(&["ls", &server.url("/x.h5")]);
|
||||||
|
assert_eq!(rc, 2, "{out}");
|
||||||
|
assert!(out.contains("not an HDF5 file"), "{out}");
|
||||||
|
let (out, rc) = h5rs(&["check", &server.url("/missing.h5")]);
|
||||||
|
assert_eq!(rc, 2, "{out}");
|
||||||
|
#[cfg(not(feature = "remote-https"))]
|
||||||
|
{
|
||||||
|
let (out, rc) = h5rs(&["ls", "https://example.com/a.h5"]);
|
||||||
|
assert_eq!(rc, 2, "{out}");
|
||||||
|
assert!(out.contains("`https` feature"), "{out}");
|
||||||
|
}
|
||||||
|
}
|
||||||
+15
-3
@@ -117,21 +117,28 @@ run_step "cargo clippy (remote, all backends)" cargo clippy \
|
|||||||
--features object-store,https,s3,gcs,azure \
|
--features object-store,https,s3,gcs,azure \
|
||||||
-- -D warnings
|
-- -D warnings
|
||||||
|
|
||||||
|
# h5rs with URL arguments.
|
||||||
|
run_step "cargo clippy (h5rs remote)" cargo clippy \
|
||||||
|
-p clawhdf5-tools \
|
||||||
|
--all-targets \
|
||||||
|
--features remote-https \
|
||||||
|
-- -D warnings
|
||||||
|
|
||||||
# The README promises that the core crates build no C by default. Hold it to
|
# The README promises that the core crates build no C by default. Hold it to
|
||||||
# that: fail if a crate that compiles C (a *-sys crate, cc or cmake) enters the
|
# that: fail if a crate that compiles C (a *-sys crate, cc or cmake) enters the
|
||||||
# default dependency tree of any of them. clawhdf5-migrate (bundled SQLite),
|
# default dependency tree of any of them. clawhdf5-migrate (bundled SQLite),
|
||||||
# clawhdf5-napi (Node) and clawhdf5-gpu (graphics drivers) are exempt.
|
# clawhdf5-napi (Node) and clawhdf5-gpu (graphics drivers) are exempt.
|
||||||
# js-sys (clawhdf5-wasm's bindings to JavaScript) builds no C.
|
# js-sys (clawhdf5-wasm's bindings to JavaScript) builds no C.
|
||||||
# clawhdf5-remote is checked by default (plain HTTP) and with its
|
# clawhdf5-remote is checked by default (plain HTTP) and with its
|
||||||
# object-store feature; its https (ring) and s3/gcs/azure (aws-lc-rs)
|
# object-store feature, and h5rs with URL support (remote); the https
|
||||||
# features build C and are opt-in.
|
# (ring) and s3/gcs/azure (aws-lc-rs) features build C and are opt-in.
|
||||||
no_c_in_default_build() {
|
no_c_in_default_build() {
|
||||||
local entry crate features found=0
|
local entry crate features found=0
|
||||||
for entry in clawhdf5-format clawhdf5-io clawhdf5-filters clawhdf5 \
|
for entry in clawhdf5-format clawhdf5-io clawhdf5-filters clawhdf5 \
|
||||||
clawhdf5-agent clawhdf5-ann clawhdf5-accel clawhdf5-netcdf4 clawhdf5-cli \
|
clawhdf5-agent clawhdf5-ann clawhdf5-accel clawhdf5-netcdf4 clawhdf5-cli \
|
||||||
clawhdf5-tools \
|
clawhdf5-tools \
|
||||||
clawhdf5-wasm \
|
clawhdf5-wasm \
|
||||||
clawhdf5-remote clawhdf5-remote:object-store; do
|
clawhdf5-remote clawhdf5-remote:object-store clawhdf5-tools:remote; do
|
||||||
crate=${entry%%:*}
|
crate=${entry%%:*}
|
||||||
features=()
|
features=()
|
||||||
[ "$entry" != "$crate" ] && features=(--features "${entry#*:}")
|
[ "$entry" != "$crate" ] && features=(--features "${entry#*:}")
|
||||||
@@ -210,6 +217,11 @@ run_step "cargo test (remote, object_store backend, s3 URLs)" cargo test \
|
|||||||
-p clawhdf5-remote \
|
-p clawhdf5-remote \
|
||||||
--features object-store,s3
|
--features object-store,s3
|
||||||
|
|
||||||
|
run_step "cargo test (h5rs on URLs)" cargo test \
|
||||||
|
-p clawhdf5-tools \
|
||||||
|
--features remote \
|
||||||
|
--test remote
|
||||||
|
|
||||||
run_step "cargo test (ann parallel)" cargo test \
|
run_step "cargo test (ann parallel)" cargo test \
|
||||||
-p clawhdf5-ann \
|
-p clawhdf5-ann \
|
||||||
--features parallel
|
--features parallel
|
||||||
|
|||||||
Reference in New Issue
Block a user