Skip to main content

energyml_storage/
array.rs

1//! Finding, and later reading, the arrays that live outside the package.
2//!
3//! An EnergyML object rarely carries its numbers. It carries a *pointer* to
4//! them, and there are two spellings of that pointer, both in use:
5//!
6//! | | RESQML 2.0.1 / eml 2.0-2.2 | RESQML 2.2 / eml 2.3 |
7//! |---|---|---|
8//! | in the object | `Hdf5Dataset`: a DOR to an `EpcExternalPartReference`, plus `PathInHdfFile` | `ExternalDataArrayPart`: `URI`, `PathInExternalFile`, `MimeType`, `Count`, `StartIndex` |
9//! | file name | in the proxy's `.rels`, as an `externalResource` target | in the object, in `URI` |
10//!
11//! The newer form is the richer one: it carries the slice as well as the
12//! location, which is exactly what ETP's `GetDataSubarrays` needs. Both are
13//! read here, into one [`ArrayLocation`].
14//!
15//! A third pointer is **this library's extension, not the standard's**: a
16//! WITSML 2.1 `ChannelData` whose `FileUri` names an HDF5 file and whose `Data`
17//! holds the path of a dataset in it (ADR-0013).
18//! Located here like the other two, so that everything that walks the arrays
19//! of an object - a cut, an import, a rename, a prefetch - carries it along
20//! instead of losing it in silence.
21//!
22//! # The path inside the file is opaque
23//!
24//! `PathInExternalFile` and `PathInHdfFile` are **strings, and nothing else**.
25//! fesapi writes `/resqml22/<uuid>/Points` and the convention is convenient,
26//! but no part of the schema requires the uuid to be in there, or requires any
27//! shape at all. Nothing here parses, splits or validates the path - it is
28//! handed to the backend exactly as the object wrote it, and a package that
29//! calls its dataset `/my/points` works. `a_path_need_not_contain_a_uuid` in
30//! the tests pins that down, because it is the kind of assumption that gets
31//! added later by someone who has only ever seen fesapi files.
32//!
33//! # And the format is not necessarily HDF5
34//!
35//! Both spellings carry a MIME type, so both can point at something else - a
36//! CSV, a Parquet file, whatever a producer and a consumer agree on. eml 2.3
37//! puts it in the part (`MimeType`); eml 2.0-2.2 put it on the
38//! `EpcExternalPartReference`, where it is a **mandatory** element whose
39//! documentation reads "MIME type for HDF proxy is: application/x-hdf5 (by
40//! RESQML convention)" - a convention, therefore, not a rule. The MIME type is
41//! what picks the backend; HDF5 is merely the one that ships.
42//!
43//! [`locate`] is a **free function over an object already in memory**, not a
44//! method on a store. That is the rule the whole crate follows: the store does
45//! I/O, the logic sits outside it, and nothing has to be written twice for the
46//! synchronous and the asynchronous paths.
47
48use energyml_core::identity;
49use energyml_core::reflect::TypeKind;
50use energyml_core::value::{FieldValue, Reflect, Scalar};
51
52/// Where one array - or one slice of one - actually lives.
53#[derive(Debug, Clone, PartialEq, Eq, Default)]
54pub struct ArrayLocation {
55    /// The file, as the package names it. A bare file name in practice, though
56    /// the schema suggests a URI; both are accepted.
57    ///
58    /// Empty when the object points at a proxy the caller has not resolved:
59    /// see [`locate_with`].
60    pub file: String,
61    /// Path of the dataset inside the file, e.g. `/resqml22/<uuid>/Points`.
62    pub path_in_file: String,
63    /// MIME type declared for the data - `application/x-hdf5`, but the field
64    /// exists precisely so it need not be.
65    ///
66    /// eml 2.3 carries it in the part itself; eml 2.0-2.2 carry it on the
67    /// `EpcExternalPartReference`, which is why resolving a proxy yields this
68    /// as well as the file name (see [`ProxyTarget`]).
69    pub mime: Option<String>,
70    /// Start index per dimension. Empty means "from the beginning".
71    pub start: Vec<i64>,
72    /// Element count per dimension. Empty means "the whole dataset".
73    pub count: Vec<i64>,
74    /// Uuid of the `EpcExternalPartReference` this came through, for the older
75    /// form. `None` for the eml 2.3 form, which has no proxy.
76    pub proxy_uuid: Option<String>,
77    /// Dotted path of the array inside the object, for diagnostics:
78    /// `Geometry.Points.Coordinates`.
79    pub field_path: String,
80    /// The ETP address of the array: the URI of the data object whose resource
81    /// holds it, `DataArrayIdentifier.uri`, the path being
82    /// [`ArrayLocation::path_in_file`] (`pathInResource`). Filled by a store that
83    /// is addressed that way (an ETP dataspace); `None` for a file, which is
84    /// addressed by [`ArrayLocation::file`]. See [`ArrayLocation::etp`].
85    ///
86    /// `TODO(verify)`: which object's URI ETP 1.2 expects for the older form, the
87    /// `EpcExternalPartReference` or the object pointing at it (ETP 1.2 §13,
88    /// DataArray; Energistics Identifier Specification).
89    pub resource_uri: Option<String>,
90}
91
92impl ArrayLocation {
93    /// Whether the file is known. It is not, when the object points at a proxy
94    /// and no resolver was given.
95    ///
96    /// # A shape borrowed from the file, and what ETP will do with it
97    ///
98    /// "Resolved" here means *the store filled in a file name*, and that is a
99    /// disk word in a type that is meant to serve a network store too. It is
100    /// honest for an EPC: the `EpcExternalPartReference` is a real indirection
101    /// and following it is a real step, so an unresolved location is a genuine
102    /// state a caller can be handed.
103    ///
104    /// It does not carry over. An ETP dataspace has no files: a data array is
105    /// addressed by the URI of the object holding it plus a `pathInResource`,
106    /// and `GetDataArrays` takes exactly that pair. A client filling
107    /// [`ArrayLocation::file`] would be inventing a file name so that
108    /// `is_resolved` returns true, which is the wrong way round.
109    ///
110    /// Left as it is, deliberately, rather than generalized on a guess: the
111    /// choice is between widening `file` into an opaque "where the store keeps
112    /// it" string and giving the ETP client its own addressing with a
113    /// conversion at the seam. Which one is right depends on what the client
114    /// actually needs to pass to `GetDataArrays`, and that is settled when the
115    /// client is written, not before. It is recorded as an open question so
116    /// it is a decision taken rather than a leak nobody noticed.
117    pub fn is_resolved(&self) -> bool {
118        !self.file.is_empty()
119    }
120
121    /// A location addressed the ETP way: the URI of the object whose resource
122    /// holds the array, and the path inside that resource. The only place the
123    /// pair becomes a location, so that a store and a server never spell the
124    /// conversion twice.
125    pub fn etp(resource_uri: impl Into<String>, path_in_resource: impl Into<String>) -> Self {
126        Self {
127            path_in_file: path_in_resource.into(),
128            resource_uri: Some(resource_uri.into()),
129            ..Self::default()
130        }
131    }
132
133    /// The ETP address of this location, `(uri, pathInResource)`, when it has
134    /// one.
135    pub fn etp_address(&self) -> Option<(&str, &str)> {
136        self.resource_uri
137            .as_deref()
138            .map(|uri| (uri, self.path_in_file.as_str()))
139    }
140}
141
142/// What an `EpcExternalPartReference` stands for.
143///
144/// Two things, not one: the older EnergyML form keeps the file name in the
145/// proxy's `.rels` **and** the MIME type on the proxy object, so resolving a
146/// proxy has to yield both or the format is lost and every external file is
147/// assumed to be HDF5.
148#[derive(Debug, Clone, PartialEq, Eq, Default)]
149pub struct ProxyTarget {
150    /// The file the proxy names.
151    pub file: String,
152    /// The MIME type the proxy declares, when it declares a usable one.
153    pub mime: Option<String>,
154}
155
156impl ProxyTarget {
157    /// A target that is only a file name.
158    pub fn file(name: impl Into<String>) -> Self {
159        Self {
160            file: name.into(),
161            mime: None,
162        }
163    }
164}
165
166/// Turns the uuid of an `EpcExternalPartReference` into what it stands for.
167///
168/// In an EPC that means reading the proxy's `.rels` for the file name and the
169/// proxy object for the MIME type. Implemented by the store, because only the
170/// store can see either.
171pub trait ProxyResolver {
172    /// What the proxy with this uuid names, if the package knows.
173    fn resolve(&self, proxy_uuid: &str) -> Option<ProxyTarget>;
174}
175
176impl<F: Fn(&str) -> Option<ProxyTarget>> ProxyResolver for F {
177    fn resolve(&self, proxy_uuid: &str) -> Option<ProxyTarget> {
178        self(proxy_uuid)
179    }
180}
181
182/// Nothing to resolve with: proxies stay unresolved, the eml 2.3 form still works.
183pub struct NoProxies;
184
185impl ProxyResolver for NoProxies {
186    fn resolve(&self, _proxy_uuid: &str) -> Option<ProxyTarget> {
187        None
188    }
189}
190
191/// Every array the object points at, both spellings, without a resolver.
192///
193/// Locations that go through a proxy come back with an empty `file` and a
194/// `proxy_uuid` set - enough to ask the store afterwards.
195pub fn locate(object: &dyn Reflect) -> Vec<ArrayLocation> {
196    locate_with(object, &NoProxies)
197}
198
199/// Every array the object points at, resolving proxies through `resolver`.
200pub fn locate_with(object: &dyn Reflect, resolver: &dyn ProxyResolver) -> Vec<ArrayLocation> {
201    let mut out = Vec::new();
202    walk(object, String::new(), resolver, &mut out, 0);
203    out
204}
205
206/// The type names that mean "here is where the numbers are".
207///
208/// Three, not two, and the middle one was missing until 31/08. eml 2.1 and 2.2
209/// carry `ExternalDatasetPart` - reached through a field spelled
210/// `ExternalFileProxy` - which is the older shape's proxy reference with the
211/// newer shape's `PathInExternalFile`. It is neither of its neighbours, and
212/// nothing matched it: every array of every eml 2.1/2.2 package came back as
213/// "declares no external data at all". That is witsml 2.0, prodml 2.0 and
214/// resqml 2.2_dev3, whose 362 polylines refused to draw with their coordinates
215/// sitting right there in the document.
216const NEW_FORM: &str = "ExternalDataArrayPart";
217const MID_FORM: &str = "ExternalDatasetPart";
218const OLD_FORM: &str = "Hdf5Dataset";
219
220/// A WITSML `ChannelData` whose rows are a dataset of an HDF5 file - **an
221/// extension beyond the standard** (ADR-0013).
222///
223/// WITSML TRG v2.1 §7.2 p. 146 gives `ChannelData` two forms: `Data`, *"The
224/// data blob in JSON form"*, and `FileUri`, *"The URI of a file containing the
225/// bulk data. If this field is non-null, then the data field is ignored."*
226/// The extension is the pair: `FileUri` names the file as the standard has it
227/// name one (a relative URI, the same string an `ExternalDataArrayPart.URI`
228/// of the package holds), and `Data` - which the standard ignores once
229/// `FileUri` is set - holds the **absolute path of the dataset** inside it.
230/// [`is_channel_data_in_file`] is the one definition of the form.
231const CHANNEL_DATA: &str = "ChannelData";
232
233/// Whether a `ChannelData`'s `FileUri` and `Data` are the extension of
234/// `CHANNEL_DATA` - the **one** definition, which the locator here and the
235/// series reader of `energyml-domain` both ask.
236///
237/// Both present, and `Data` an absolute HDF5 path: it begins with `/`, which
238/// **no JSON text can** - RFC 8259 §2, a JSON text is optional whitespace then
239/// a value, and a value begins with `{`, `[`, `"`, `-`, a digit, `t`, `f` or
240/// `n`. So the test is not a guess on content: the two syntaxes are disjoint,
241/// and a standard file that writes `FileUri` **beside** an inline blob (the
242/// standard then ignores the blob, §7.2 p. 146) stays the standard's form -
243/// never a dataset path made of JSON. `FileUri` alone is the standard's form
244/// too (a bulk file in the release notes' format, not an HDF5 dataset).
245pub fn is_channel_data_in_file(file_uri: &str, data: &str) -> bool {
246    !file_uri.trim().is_empty() && data.starts_with('/')
247}
248
249/// The MIME type the extension implies: the file `FileUri` names is HDF5 by
250/// the extension's own definition - `ChannelData` has no field to say it.
251const CHANNEL_DATA_MIME: &str = "application/x-hdf5";
252
253/// The field of a located part that names its **file** - what a rename or an
254/// import rewrites. `uri` for the eml 2.3 form; `file_uri` for a WITSML
255/// `ChannelData` (`CHANNEL_DATA`). The two proxy forms name no file here
256/// (their proxy's `.rels` does), and are never asked.
257pub fn file_field(part_type: &str) -> &'static str {
258    match part_type {
259        CHANNEL_DATA => "file_uri",
260        _ => "uri",
261    }
262}
263
264/// The field of a located part that holds the **path inside the file** -
265/// `path_in_hdf_file` for the eml 2.0 form, `data` for a WITSML `ChannelData`,
266/// `path_in_external_file` otherwise.
267pub fn path_field(part_type: &str) -> &'static str {
268    match part_type {
269        OLD_FORM => "path_in_hdf_file",
270        CHANNEL_DATA => "data",
271        _ => "path_in_external_file",
272    }
273}
274
275fn walk(
276    object: &dyn Reflect,
277    path: String,
278    resolver: &dyn ProxyResolver,
279    out: &mut Vec<ArrayLocation>,
280    depth: u32,
281) {
282    // Objects reference each other only through DORs, never by containment, so
283    // the tree is finite. The bound is a seatbelt, not a design assumption.
284    if depth > 64 {
285        return;
286    }
287
288    let info = object.type_info();
289    match info.name {
290        NEW_FORM => {
291            out.push(from_new_form(object, &path));
292            return;
293        }
294        MID_FORM => {
295            out.push(from_mid_form(object, &path, resolver));
296            return;
297        }
298        OLD_FORM => {
299            out.push(from_old_form(object, &path, resolver));
300            return;
301        }
302        CHANNEL_DATA => {
303            if let Some(found) = from_channel_data(object, &path) {
304                out.push(found);
305            }
306            return;
307        }
308        _ => {}
309    }
310
311    for field in info.fields {
312        let Some(value) = object.field(field.name) else {
313            continue;
314        };
315        let child_path = if path.is_empty() {
316            field.xml_name.to_string()
317        } else {
318            format!("{path}.{}", field.xml_name)
319        };
320        walk_value(&value, child_path, resolver, out, depth);
321    }
322}
323
324fn walk_value(
325    value: &FieldValue<'_>,
326    path: String,
327    resolver: &dyn ProxyResolver,
328    out: &mut Vec<ArrayLocation>,
329    depth: u32,
330) {
331    match value {
332        FieldValue::Object(child) => walk(*child, path, resolver, out, depth + 1),
333        FieldValue::List(items) => {
334            for (i, item) in items.iter().enumerate() {
335                walk_value(item, format!("{path}[{i}]"), resolver, out, depth);
336            }
337        }
338        _ => {}
339    }
340}
341
342/// A `ChannelData` naming a file **and** a dataset - [`is_channel_data_in_file`];
343/// `None` for either standard form, which carries no dataset to locate.
344///
345/// The path is handed over as written, like every path here: never trimmed or
346/// split beyond the one character that tells the form.
347fn from_channel_data(object: &dyn Reflect, path: &str) -> Option<ArrayLocation> {
348    let file = string_field(object, "file_uri")?;
349    let dataset = string_field(object, "data")?;
350    if !is_channel_data_in_file(&file, &dataset) {
351        return None;
352    }
353    Some(ArrayLocation {
354        file,
355        path_in_file: dataset,
356        mime: Some(CHANNEL_DATA_MIME.to_string()),
357        start: Vec::new(),
358        count: Vec::new(),
359        proxy_uuid: None,
360        field_path: path.to_string(),
361        resource_uri: None,
362    })
363}
364
365fn from_new_form(object: &dyn Reflect, path: &str) -> ArrayLocation {
366    ArrayLocation {
367        file: string_field(object, "uri").unwrap_or_default(),
368        path_in_file: string_field(object, "path_in_external_file").unwrap_or_default(),
369        mime: string_field(object, "mime_type"),
370        start: int_list(object, "start_index"),
371        count: int_list(object, "count"),
372        proxy_uuid: None,
373        field_path: path.to_string(),
374        resource_uri: None,
375    }
376}
377
378/// eml 2.1 / 2.2: a proxy reference like the old form, a path like the new one.
379///
380/// The DOR is spelled `EpcExternalPartReference` here rather than `HdfProxy`,
381/// and it names the same thing: the part whose `.rels` carries the file name.
382/// `Count` and `StartIndex` are single values, not lists - this shape predates
383/// the multi-dimensional slicing eml 2.3 added - but they go into the same
384/// vectors, so every reader above sees one kind of `ArrayLocation`.
385fn from_mid_form(object: &dyn Reflect, path: &str, resolver: &dyn ProxyResolver) -> ArrayLocation {
386    let proxy_uuid = object
387        .field("epc_external_part_reference")
388        .and_then(|value| match value {
389            FieldValue::Object(dor) => identity::uuid_of_reflect(dor).map(str::to_string),
390            _ => None,
391        })
392        .filter(|uuid| !uuid.is_empty());
393
394    let target = proxy_uuid
395        .as_deref()
396        .and_then(|uuid| resolver.resolve(uuid))
397        .unwrap_or_default();
398    ArrayLocation {
399        file: target.file,
400        path_in_file: string_field(object, "path_in_external_file").unwrap_or_default(),
401        // Declared by the proxy, as in the old form: this part says where in
402        // the file, never which file.
403        mime: target.mime,
404        // **`StartIndex` and `Count` are deliberately not carried over.**
405        //
406        // They are single values describing a *flat* range - `ExternalFileProxy`
407        // repeats, so an array may be split across parts - while
408        // `ArrayLocation::start`/`count` are the per-dimension hyperslab eml 2.3
409        // introduced. The two are not the same thing, and passing one as the
410        // other is not a small error: on this corpus it made every polyline ask
411        // for one dimension of a two-dimensional dataset and refuse, with the
412        // coordinates sitting right there. Empty means "the whole dataset",
413        // which is what the old form has always done and what a single part
414        // means anyway.
415        //
416        // A genuinely *split* array would need flat-range reading, which no
417        // backend here offers; it would come back as one location per part,
418        // each naming the whole dataset. No package in the corpus splits one,
419        // so that is a gap named rather than a guess made.
420        start: Vec::new(),
421        count: Vec::new(),
422        proxy_uuid,
423        field_path: path.to_string(),
424        resource_uri: None,
425    }
426}
427
428fn from_old_form(object: &dyn Reflect, path: &str, resolver: &dyn ProxyResolver) -> ArrayLocation {
429    // `HdfProxy` is a DataObjectReference; its uuid names the
430    // `EpcExternalPartReference` whose `.rels` carries the file name.
431    let proxy_uuid = object
432        .field("hdf_proxy")
433        .and_then(|value| match value {
434            FieldValue::Object(dor) => identity::uuid_of_reflect(dor).map(str::to_string),
435            _ => None,
436        })
437        .filter(|uuid| !uuid.is_empty());
438
439    let target = proxy_uuid
440        .as_deref()
441        .and_then(|uuid| resolver.resolve(uuid))
442        .unwrap_or_default();
443    ArrayLocation {
444        file: target.file,
445        path_in_file: string_field(object, "path_in_hdf_file").unwrap_or_default(),
446        // Declared by the proxy, not by this part - which is the whole reason
447        // resolving a proxy returns two things.
448        mime: target.mime,
449        start: Vec::new(),
450        count: Vec::new(),
451        proxy_uuid,
452        field_path: path.to_string(),
453        resource_uri: None,
454    }
455}
456
457fn string_field(object: &dyn Reflect, name: &str) -> Option<String> {
458    match object.field(name)? {
459        FieldValue::Scalar(Scalar::Str(s)) if !s.is_empty() => Some(s.to_string()),
460        _ => None,
461    }
462}
463
464fn int_list(object: &dyn Reflect, name: &str) -> Vec<i64> {
465    let Some(value) = object.field(name) else {
466        return Vec::new();
467    };
468    let one = |v: &FieldValue<'_>| match v {
469        FieldValue::Scalar(Scalar::Int(i)) => Some(*i),
470        FieldValue::Scalar(Scalar::UInt(u)) => i64::try_from(*u).ok(),
471        _ => None,
472    };
473    match value {
474        FieldValue::List(items) => items.iter().filter_map(one).collect(),
475        other => one(&other).into_iter().collect(),
476    }
477}
478
479/// Which end of a multi-byte number the file writes first.
480///
481/// Not part of [`ArrayMetadata`], and that is deliberate: ETP's
482/// `GetDataArrayMetadata` answers a shape and a type and says nothing about
483/// byte order, because a transfer defines its own. This is a property of **the
484/// file**, and it matters to exactly one kind of caller - one that must write
485/// down how the bytes are stored, as an EML 2.3 external array declaration
486/// does. So it is asked for separately, by
487/// [`crate::backend::ArrayBackend::byte_order`], and never inferred.
488#[derive(Debug, Clone, Copy, PartialEq, Eq)]
489pub enum ByteOrder {
490    /// Least significant byte first.
491    LittleEndian,
492    /// Most significant byte first.
493    BigEndian,
494}
495
496impl std::fmt::Display for ByteOrder {
497    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
498        f.write_str(match self {
499            ByteOrder::LittleEndian => "little-endian",
500            ByteOrder::BigEndian => "big-endian",
501        })
502    }
503}
504
505/// Shape and element type of a dataset, without reading it.
506///
507/// The answer to ETP's `GetDataArrayMetadata`, and what a viewer needs before
508/// deciding whether it can afford the data.
509///
510/// Built with [`ArrayMetadata::new`]; `#[non_exhaustive]`, so that what ETP adds
511/// to `DataArrayMetadata` is not a breaking change for every caller.
512#[derive(Debug, Clone, PartialEq, Eq)]
513#[non_exhaustive]
514pub struct ArrayMetadata {
515    /// Number of elements along each dimension.
516    pub dimensions: Vec<i64>,
517    /// Element type, as the backend names it (`double`, `int64`, …) - the type
518    /// the values travel in (ETP's `transportArrayType`).
519    pub element_type: String,
520    /// ETP `logicalArrayType`, when the store distinguishes what the values mean
521    /// from how they travel (an array of booleans carried as bytes). `None` when
522    /// it does not: the element type is then all there is to say.
523    pub logical_type: Option<String>,
524    /// ETP `preferredSubarrayDimensions`: the block a store would rather be read
525    /// by (its chunking). Empty when it has no preference.
526    pub preferred_subarray_dimensions: Vec<i64>,
527    /// ETP `storeCreated` of the array (microseconds since the Unix epoch, UTC).
528    /// `None` when the store does not keep it.
529    pub store_created: Option<i64>,
530    /// ETP `storeLastWrite` of the array, same unit. `None` when not kept.
531    pub store_last_write: Option<i64>,
532}
533
534impl ArrayMetadata {
535    /// A shape and an element type; everything the store may not know is left
536    /// unknown.
537    pub fn new(dimensions: Vec<i64>, element_type: impl Into<String>) -> Self {
538        Self {
539            dimensions,
540            element_type: element_type.into(),
541            logical_type: None,
542            preferred_subarray_dimensions: Vec::new(),
543            store_created: None,
544            store_last_write: None,
545        }
546    }
547}
548
549/// A block of numbers, in the type the file holds them.
550///
551/// **Typed, not converted.** A dataset of doubles comes back as `Vec<f64>`,
552/// integers as `Vec<i32>`, and so on - because the point of reading a million
553/// points is to hand them to something that draws them, and every conversion on
554/// the way is a copy of the whole array. See [`ArrayData`] for the counting.
555#[derive(Debug, Clone, PartialEq)]
556#[non_exhaustive]
557pub enum ArrayValues {
558    /// `H5T_IEEE_F64` and friends.
559    F64(Vec<f64>),
560    /// 32-bit floats.
561    F32(Vec<f32>),
562    /// Signed integers, by width.
563    I64(Vec<i64>),
564    /// Signed 32-bit.
565    I32(Vec<i32>),
566    /// Signed 16-bit.
567    I16(Vec<i16>),
568    /// Signed 8-bit.
569    I8(Vec<i8>),
570    /// Unsigned 64-bit.
571    U64(Vec<u64>),
572    /// Unsigned 32-bit.
573    U32(Vec<u32>),
574    /// Unsigned 16-bit.
575    U16(Vec<u16>),
576    /// Unsigned 8-bit, and booleans, which HDF5 stores as bytes.
577    U8(Vec<u8>),
578}
579
580impl ArrayValues {
581    /// How many values there are.
582    pub fn len(&self) -> usize {
583        match self {
584            Self::F64(v) => v.len(),
585            Self::F32(v) => v.len(),
586            Self::I64(v) => v.len(),
587            Self::I32(v) => v.len(),
588            Self::I16(v) => v.len(),
589            Self::I8(v) => v.len(),
590            Self::U64(v) => v.len(),
591            Self::U32(v) => v.len(),
592            Self::U16(v) => v.len(),
593            Self::U8(v) => v.len(),
594        }
595    }
596
597    /// Whether there are none.
598    pub fn is_empty(&self) -> bool {
599        self.len() == 0
600    }
601
602    /// The element type, named the way [`ArrayMetadata`] names it.
603    /// Every element widened to `f64` - see [`ArrayData::to_f64`], which is
604    /// this with the shape beside it. **This copies.**
605    pub fn to_f64(&self) -> Vec<f64> {
606        match self {
607            ArrayValues::F64(v) => v.clone(),
608            ArrayValues::F32(v) => v.iter().map(|x| *x as f64).collect(),
609            ArrayValues::I64(v) => v.iter().map(|x| *x as f64).collect(),
610            ArrayValues::I32(v) => v.iter().map(|x| *x as f64).collect(),
611            ArrayValues::I16(v) => v.iter().map(|x| *x as f64).collect(),
612            ArrayValues::I8(v) => v.iter().map(|x| *x as f64).collect(),
613            ArrayValues::U64(v) => v.iter().map(|x| *x as f64).collect(),
614            ArrayValues::U32(v) => v.iter().map(|x| *x as f64).collect(),
615            ArrayValues::U16(v) => v.iter().map(|x| *x as f64).collect(),
616            ArrayValues::U8(v) => v.iter().map(|x| *x as f64).collect(),
617        }
618    }
619
620    /// The element type, named the way [`ArrayMetadata`] names it.
621    pub fn element_type(&self) -> &'static str {
622        match self {
623            Self::F64(_) => "double",
624            Self::F32(_) => "float",
625            Self::I64(_) => "int64",
626            Self::I32(_) => "int32",
627            Self::I16(_) => "int16",
628            Self::I8(_) => "int8",
629            Self::U64(_) => "uint64",
630            Self::U32(_) => "uint32",
631            Self::U16(_) => "uint16",
632            Self::U8(_) => "uint8",
633        }
634    }
635
636    /// The same memory seen as bytes. **No copy** - a borrow, for a caller
637    /// about to hand the block to a GPU, a socket or a file.
638    pub fn as_bytes(&self) -> &[u8] {
639        match self {
640            Self::F64(v) => bytemuck::cast_slice(v),
641            Self::F32(v) => bytemuck::cast_slice(v),
642            Self::I64(v) => bytemuck::cast_slice(v),
643            Self::I32(v) => bytemuck::cast_slice(v),
644            Self::I16(v) => bytemuck::cast_slice(v),
645            Self::I8(v) => bytemuck::cast_slice(v),
646            Self::U64(v) => bytemuck::cast_slice(v),
647            Self::U32(v) => bytemuck::cast_slice(v),
648            Self::U16(v) => bytemuck::cast_slice(v),
649            Self::U8(v) => v,
650        }
651    }
652}
653
654/// A block of numbers, as read, with the shape it came in.
655///
656/// # How many copies
657///
658/// Reading a compressed or chunked dataset cannot be done without the library
659/// materialising it somewhere, so **one** copy - from the file into the
660/// caller's memory - is the floor. What this type is shaped to avoid is the
661/// second and third:
662///
663/// | | |
664/// |---|---|
665/// | [`ArrayData::values`] | filled straight by the backend. One copy |
666/// | [`ArrayValues::as_bytes`] | a borrow of that same memory. **Zero** |
667/// | [`ArrayData::f64`] and friends | a borrow when the type already matches. **Zero** |
668/// | [`ArrayData::to_f64`] | widens every element. One more copy, and you asked for it |
669///
670/// And [`crate::ArrayBackend::read_into`] removes even the first allocation:
671/// the caller supplies the buffer - a vertex buffer, a memory-mapped region -
672/// and the file is decoded straight into it.
673#[derive(Debug, Clone, PartialEq)]
674pub struct ArrayData {
675    /// Shape of what was read.
676    pub metadata: ArrayMetadata,
677    /// The values, in the file's own element type.
678    pub values: ArrayValues,
679}
680
681impl ArrayData {
682    /// How many elements the shape promises.
683    pub fn element_count(&self) -> usize {
684        self.metadata.dimensions.iter().product::<i64>().max(0) as usize
685    }
686
687    /// The block as bytes, borrowed. No copy.
688    pub fn as_bytes(&self) -> &[u8] {
689        self.values.as_bytes()
690    }
691
692    /// The values as `f64`, **borrowed**, when that is what the file holds.
693    ///
694    /// The path a 3D application takes for coordinates: no conversion, no
695    /// allocation, just the numbers.
696    pub fn f64(&self) -> Option<&[f64]> {
697        match &self.values {
698            ArrayValues::F64(v) => Some(v),
699            _ => None,
700        }
701    }
702
703    /// The values as `f32`, borrowed, when that is what the file holds.
704    pub fn f32(&self) -> Option<&[f32]> {
705        match &self.values {
706            ArrayValues::F32(v) => Some(v),
707            _ => None,
708        }
709    }
710
711    /// The values as `i64`, borrowed, when that is what the file holds.
712    pub fn i64(&self) -> Option<&[i64]> {
713        match &self.values {
714            ArrayValues::I64(v) => Some(v),
715            _ => None,
716        }
717    }
718
719    /// The values as `u32`, borrowed, when that is what the file holds -
720    /// what RESQML uses for indices.
721    pub fn u32(&self) -> Option<&[u32]> {
722        match &self.values {
723            ArrayValues::U32(v) => Some(v),
724            _ => None,
725        }
726    }
727
728    /// Every element widened to `f64`.
729    ///
730    /// **This copies**, and it is the only method here that does. Use
731    /// [`ArrayData::f64`] when the file already holds doubles, which for
732    /// coordinates it does.
733    pub fn to_f64(&self) -> Vec<f64> {
734        self.values.to_f64()
735    }
736}
737
738/// A buffer the caller owns, for a backend to decode straight into.
739///
740/// The zero-allocation path: an application that already holds the memory the
741/// numbers must end up in - a vertex buffer, a mapped file, a slice of a larger
742/// block - passes it here and the file is read directly into it. Nothing is
743/// allocated, nothing is copied afterwards.
744#[non_exhaustive]
745pub enum ArrayBuffer<'a> {
746    /// Doubles.
747    F64(&'a mut [f64]),
748    /// Floats.
749    F32(&'a mut [f32]),
750    /// Signed 64-bit.
751    I64(&'a mut [i64]),
752    /// Signed 32-bit.
753    I32(&'a mut [i32]),
754    /// Unsigned 64-bit.
755    U64(&'a mut [u64]),
756    /// Unsigned 32-bit.
757    U32(&'a mut [u32]),
758    /// Bytes.
759    U8(&'a mut [u8]),
760}
761
762impl ArrayBuffer<'_> {
763    /// How many elements it can take.
764    pub fn len(&self) -> usize {
765        match self {
766            Self::F64(b) => b.len(),
767            Self::F32(b) => b.len(),
768            Self::I64(b) => b.len(),
769            Self::I32(b) => b.len(),
770            Self::U64(b) => b.len(),
771            Self::U32(b) => b.len(),
772            Self::U8(b) => b.len(),
773        }
774    }
775
776    /// Whether it can take none.
777    pub fn is_empty(&self) -> bool {
778        self.len() == 0
779    }
780
781    /// The element type it expects, named as [`ArrayMetadata`] names it.
782    pub fn element_type(&self) -> &'static str {
783        match self {
784            Self::F64(_) => "double",
785            Self::F32(_) => "float",
786            Self::I64(_) => "int64",
787            Self::I32(_) => "int32",
788            Self::U64(_) => "uint64",
789            Self::U32(_) => "uint32",
790            Self::U8(_) => "uint8",
791        }
792    }
793}
794
795/// Whether a type is one of the two array pointers.
796///
797/// Useful to a UI that wants to show "this field holds data elsewhere" without
798/// walking into it.
799pub fn is_array_pointer(info: &energyml_core::reflect::TypeInfo) -> bool {
800    info.kind == TypeKind::Struct && (info.name == NEW_FORM || info.name == OLD_FORM)
801}
802
803/// Where an array's numbers were actually found, and why there.
804///
805/// The distance between `declared` and `path` is the whole point: it is what a
806/// person needs told when a package has been renamed, and what the repair
807/// utility writes back so the next reader does not have to search at all.
808#[derive(Clone, PartialEq, Eq, Debug)]
809pub struct Resolution {
810    /// The file name the document wrote. Empty when it wrote none.
811    pub declared: String,
812    /// The file the numbers came out of.
813    pub path: std::path::PathBuf,
814    /// Which kind of place that was - see [`crate::backend::Origin`].
815    pub origin: crate::backend::Origin,
816}
817
818impl Resolution {
819    /// The bare file name of the answer, which is what a package should say.
820    pub fn name(&self) -> String {
821        self.path
822            .file_name()
823            .map(|n| n.to_string_lossy().into_owned())
824            .unwrap_or_else(|| self.path.display().to_string())
825    }
826
827    /// Whether the document already said the right thing.
828    pub fn agrees(&self) -> bool {
829        !self.origin.is_a_departure()
830    }
831}