Skip to main content

energyml_storage/epc/
mod.rs

1//! Reading an EPC file.
2//!
3//! [`EpcStore`] carries **two call surfaces**, and that is deliberate
4//! (ADR-0002):
5//!
6//! - a complete **synchronous** inherent API - what an application that only
7//!   ever touches local files should use. It never writes `async`, never adds
8//!   an executor, and need not know [`EnergymlStorage`] exists;
9//! - an implementation of [`EnergymlStorage`], **asynchronous**, one line per
10//!   method over the synchronous one. That is what lets an application swap an
11//!   EPC for an ETP dataspace by changing the line that builds the store.
12//!
13//! Which to use: the synchronous one, unless being indifferent to the transport
14//! is the point.
15//!
16//! # What it reads, and what it costs
17//!
18//! | Call | Opens | Cost |
19//! |---|---|---|
20//! | [`EpcStore::resources`] | nothing | already in memory after `open` |
21//! | [`EpcStore::headers`] | the first bytes of each part | one partial inflate per object |
22//! | [`EpcStore::get_objects`] | the whole part | full deserialization |
23//!
24//! # Tolerance
25//!
26//! Real packages break the specification - the reference package published with
27//! fesapi breaks four rules at once. This reader accepts
28//! them and records every departure in [`EpcStore::diagnostics`]. Nothing is
29//! swallowed, nothing is fatal that need not be.
30
31mod contained_read;
32mod contents;
33pub mod core_props;
34pub mod header;
35pub mod media;
36pub mod overlay;
37pub mod package;
38pub mod revision;
39pub mod write;
40mod writer;
41
42use std::collections::{BTreeSet, HashMap, HashSet};
43use std::io::BufReader;
44use std::path::{Path, PathBuf};
45use std::sync::{Arc, Mutex, OnceLock};
46
47use async_trait::async_trait;
48use energyml_core::identifiers;
49use energyml_core::identity;
50use energyml_core::naming;
51use energyml_core::reflect::TypeInfo;
52use energyml_core::registry::{DynObject, ReadMode, Registry};
53
54use crate::array::Resolution;
55use crate::backend::Origin;
56use energyml_core::value::Reflect;
57
58use crate::array::{ArrayData, ArrayLocation, ArrayMetadata, ByteOrder, ProxyResolver};
59use crate::error::{Diagnostic, Error, ExternalDataReason, Result};
60use crate::resource::{Dataspace, ObjectHeader, ObjectSummary, Relationship};
61use crate::storage::{Contents, EnergymlStorage, LocalPackage};
62
63use contents::ContainedIndex;
64use overlay::{Change, Overlay, SavePolicy};
65use package::{Package, ReadSeek};
66use revision::What;
67
68/// A part of the package that holds no readable object, and why.
69///
70/// What [`EpcStore::unreadable`] returns: the worklist of a repair, and the
71/// only way to reach a part whose damage cost it its uri.
72#[derive(Debug, Clone, PartialEq, Eq)]
73pub struct Unreadable {
74    /// Name of the part in the archive. Always usable with
75    /// [`EpcStore::raw_part`] and [`EpcStore::set_raw_part`].
76    pub part: String,
77    /// Its uri, when the inventory could still address it. `None` is the worse
78    /// case: the part is in the archive and in no listing.
79    pub uri: Option<String>,
80    /// Content type as `[Content_Types].xml` declares it, when it does.
81    pub content_type: Option<String>,
82    /// How many bytes are there - zero being a diagnosis of its own.
83    pub bytes: usize,
84    /// What went wrong, in the words the reader used.
85    pub reason: String,
86}
87
88/// How a package is written.
89#[derive(Debug, Clone)]
90pub struct SaveOptions {
91    /// `dc:creator` of the core properties. Mandatory in EPC (§3.5.3).
92    ///
93    /// `None` keeps whatever the source package said, and falls back to this
94    /// crate only when it said nothing: the core properties
95    /// are *kept* if they existed and *created* if they were missing. Only
96    /// `created` is always rewritten, because it means the moment the package
97    /// was made and this is that moment.
98    pub creator: Option<String>,
99    /// `dcterms:created`, in W3CDTF. `None` means now - which is what the
100    /// specification asks for, since the field is the *package* creation time.
101    /// Setting it explicitly makes a build reproducible, which is what the
102    /// tests do.
103    pub created: Option<String>,
104    /// Where the XML parts go. The specification imposes nothing (§3.6); flat
105    /// is what the widest range of tools reads back.
106    pub layout: energyml_core::identity::EpcLayout,
107}
108
109impl Default for SaveOptions {
110    fn default() -> Self {
111        Self {
112            creator: None,
113            created: None,
114            layout: energyml_core::identity::EpcLayout::Flat,
115        }
116    }
117}
118
119/// What one part of a package is, for a listing that shows the archive.
120#[derive(Clone, Copy, Debug, PartialEq, Eq)]
121pub enum PartKind {
122    /// An EnergyML object - the only kind the rest of this library reads.
123    Object,
124    /// `[Content_Types].xml`, the index OPC requires. Regenerated on save.
125    ContentTypes,
126    /// A `.rels`. Regenerated on save, from the objects' own references.
127    Relationships,
128    /// `docProps/core.xml`. Regenerated on save.
129    CoreProperties,
130    /// Anything else under `docProps/` - a producer's extension, copied
131    /// verbatim because this library neither writes it nor understands it.
132    PackageExtension,
133    /// A file carried along: a PDF, an image, a note. Copied verbatim.
134    Attachment,
135}
136
137/// One part of the package, as a listing shows it.
138///
139/// The three flags are the difference between the archive and what a save would
140/// write, and they are separate because they are separate facts: a part can be
141/// `added` and not `staged` in no case, `staged` without being `added` (bytes
142/// repaired over an archive part), and `dropped` while still being in the
143/// archive - which is precisely the state a listing has to be able to show.
144#[derive(Clone, Debug)]
145pub struct PartListing {
146    /// The part name, which is its address inside the package.
147    pub part: String,
148    /// What it is: an object, the package's own machinery, or a file along for
149    /// the ride.
150    pub kind: PartKind,
151    /// Uncompressed size, as declared, or the length of what is staged.
152    pub size: Option<u64>,
153    /// What `[Content_Types].xml` declares, or what a caller stated when adding
154    /// the part. `None` leaves the writer to guess from the extension.
155    pub content_type: Option<String>,
156    /// For an object part: the uri it is addressed by.
157    pub uri: Option<String>,
158    /// For an object part: the type the inventory filed it under.
159    pub qualified_type: Option<String>,
160    /// Put here in this session; the archive never had it.
161    pub added: bool,
162    /// Removed in this session; a save will not write it.
163    pub dropped: bool,
164    /// Bytes are staged for it, so what a save writes is not what was opened.
165    pub staged: bool,
166}
167
168/// One EnergyML part of the package, as the inventory knows it.
169struct PartEntry {
170    summary: ObjectSummary,
171    /// The registered type, when the registry knows it. `None` means the
172    /// package holds a version whose crate was not registered: the object is
173    /// still listed - a listing that hides rows is worse than one that shows a
174    /// row it cannot open - but reading it fails with [`Error::UnknownType`].
175    info: Option<&'static TypeInfo>,
176    /// The other versions the root element could also be, best first.
177    ///
178    /// Empty when the identification was unambiguous. When it is not, a read
179    /// walks this list after `info` fails - see [`EpcStore::root_candidates`]
180    /// for why one namespace covers every version of a package and
181    /// `schemaVersion` cannot always settle it.
182    alternatives: Vec<&'static TypeInfo>,
183    /// Why no type reads it, when the answer is a version the registry **knows
184    /// and does not hold** (`Registry::declare_absent`): the named refusal a
185    /// read returns. `None` for a version nobody declared, which keeps the
186    /// substitution and the plain `UnknownType`.
187    refusal: Option<String>,
188    /// The key the overlay and the journal know this entry by - see
189    /// [`slot_key`]. Fixed when the entry is born and never recomputed: the
190    /// uri of an entry can change (a second version of its uuid arrives), its
191    /// slot cannot.
192    slot: String,
193    /// The `objectVersion` the part declared, `None` when it declares none. One
194    /// third of the identity (ADR-0010): it is read by the peek
195    /// every part already gets at open, so it costs no read.
196    object_version: Option<String>,
197}
198
199/// An object this session created that the archive never held.
200///
201/// Kept beside the archive's entries so that a created object is addressable
202/// at once (see the `made` field) - and with the identity it has
203/// beside its namesakes: the slot, and the `objectVersion` that tells it from
204/// another object of the same uuid and type.
205struct Made {
206    slot: String,
207    summary: ObjectSummary,
208    object_version: Option<String>,
209}
210
211/// One object of the package as it stands now: the archive's entries and the
212/// objects created in this session, minus what was removed.
213struct Live<'a> {
214    slot: &'a str,
215    summary: &'a ObjectSummary,
216    version: Option<&'a str>,
217    /// The archive's entry, when it has one.
218    archive: Option<&'a PartEntry>,
219    /// The data object held by value inside a part, when this is one: then
220    /// `archive` is `None` and `slot` is its uri (never an overlay slot), so
221    /// every path that reads a part through [`EpcStore::entry`] fails with
222    /// `NotFound` instead of answering with the container.
223    nested: Option<&'a contents::ContainedEntry>,
224}
225
226/// What an address says, once parsed: a uuid, and - when the address is a URI -
227/// the type and the version it names.
228struct Address {
229    /// Lowercased.
230    uuid: String,
231    qualified_type: Option<String>,
232    version: Option<String>,
233}
234
235/// The identity key of an entry, the way the overlay and the journal spell it.
236///
237/// **An entry is (uuid, qualified type, objectVersion)** - Identifier
238/// Specification v5.0 §3.4.2 (p. 14) puts the version in the URI beside the
239/// uuid, and EPC v1.1 §3.6 (p. 17) lets a package hold a versioned object and
240/// say so in its path. The overlay and the journal were keyed by the uuid; they
241/// are now keyed by this, and **for the first entry of a uuid it is the uuid
242/// itself** - so every package that never had two entries of one uuid (all of
243/// the corpus) keeps exactly the keys it had. A second entry gets a composite:
244/// the uuid, the lowercased type and the version in hex (the journal lowercases
245/// its keys, and a version is case-sensitive), joined by the unit separator, so
246/// the uuid is always what comes before the first one ([`uuid_of_slot`]).
247fn slot_key(uuid: &str, qualified_type: &str, version: Option<&str>) -> String {
248    let mut out = String::with_capacity(uuid.len() + qualified_type.len() + 8);
249    out.push_str(&uuid.to_ascii_lowercase());
250    out.push('\u{1f}');
251    out.push_str(&qualified_type.to_ascii_lowercase());
252    out.push('\u{1f}');
253    for byte in version.unwrap_or_default().bytes() {
254        out.push_str(&format!("{byte:02x}"));
255    }
256    out
257}
258
259/// The lowercased uuid a slot key belongs to.
260fn uuid_of_slot(slot: &str) -> &str {
261    slot.split('\u{1f}').next().unwrap_or(slot)
262}
263
264/// An `objectVersion` as an identity: surrounding blanks do not count, and an
265/// empty one is no version - the element is optional, and an empty one says the
266/// same as none.
267/// The passes a read goes through for a reading mode: strict first, because
268/// telling two releases apart works by letting the wrong one fail, then the
269/// caller's own mode. One list for a part ([`EpcStore::get_object`]) and for an
270/// object held by value, so that the two readers cannot drift apart.
271fn read_passes(mode: ReadMode) -> &'static [ReadMode] {
272    match mode {
273        ReadMode::Strict => &[ReadMode::Strict],
274        ReadMode::Tolerant => &[ReadMode::Strict, ReadMode::Tolerant],
275        ReadMode::Lenient => &[ReadMode::Strict, ReadMode::Lenient],
276    }
277}
278
279fn normal_version(version: Option<&str>) -> Option<String> {
280    version
281        .map(str::trim)
282        .filter(|v| !v.is_empty())
283        .map(str::to_string)
284}
285
286/// An EPC file, open for reading.
287///
288/// Reading only, in this pass. The in-memory layer that makes an open package
289/// modifiable comes with the writing pass; the shape here
290/// is what it will sit on.
291pub struct EpcStore {
292    package: Package,
293    registry: Arc<Registry>,
294    /// Name of the single dataspace this file is: the file name, or whatever
295    /// the caller supplied when it opened bytes rather than a path.
296    name: String,
297    /// The path of the ETP dataspace this file is served as (`Lutetia`,
298    /// `projets/demo`), or `None` for the default dataspace `eml:///`.
299    ///
300    /// Distinct from `name`, which is only the title shown: this one is part of
301    /// every object's identity, because the identity is the full URI (ETP v1.2
302    /// §25.3, `dataspace('...')/...`). Set once at open and never changed, as
303    /// every uri the store has built carries it.
304    dataspace: Option<String>,
305    /// Where it came from, when it came from a disk.
306    path: Option<PathBuf>,
307    entries: Vec<PartEntry>,
308    /// Lowercased uuid → the indices into `entries` of **every** part that
309    /// carries it, in archive order: the same uuid under two `objectVersion`s,
310    /// or under two releases, is two entries. Only an exact duplicate of
311    /// (uuid, type, version) is dropped.
312    by_uuid: HashMap<String, Vec<usize>>,
313    /// Behind a mutex because a lazy reader keeps noticing things after it was
314    /// opened: the fallback taken for a missing HDF5 proxy is discovered the
315    /// first time an array is located.
316    diagnostics: Mutex<Vec<Diagnostic>>,
317    /// Which file each `(declared name, dataset)` pair was actually found in.
318    ///
319    /// The search opens files to ask what they hold; without this, a package
320    /// with a thousand arrays would pay for that a thousand times.
321    array_files: Mutex<HashMap<(String, String), std::path::PathBuf>>,
322    /// The `externalResource` of a part's `.rels`, remembered by part name.
323    ///
324    /// # Why this is not a micro-optimisation
325    ///
326    /// An HDF5 proxy's own `.rels` holds one relationship **per object that
327    /// references it**, and exactly one `externalResource` - the file name. On
328    /// the Lyon package (4484 objects, 2694 array pointers) that part is
329    /// **585 KB and 2687 relationships**, and every array pointer inflated and
330    /// parsed the whole of it to pick the one entry that names the file. 1.6 GB
331    /// of XML, re-parsed, to answer the same question 2694 times.
332    ///
333    /// Measured 08/09/2026, release build, warm cache: evaluating the package's
334    /// 894 `WellboreFrameRepresentation`s took **12.2 s**, of which the frames'
335    /// own HDF5 reads were 1.7 s. h5py reads the same 894 datasets in 0.12 s, so
336    /// the file was never the problem. With this map: **0.9 s**, same 894
337    /// representations and same 5516 points.
338    ///
339    /// **Nothing invalidates it, and that is a property rather than an
340    /// oversight**: [`EpcStore::external_resource_in`] reads through
341    /// `self.package` - the archive - and never consults the overlay, so no
342    /// edit, removal or staged part can change what it answers. A memo of a
343    /// function that already ignores every mutation cannot go stale. The day
344    /// that function learns to read staged bytes, this map has to be cleared
345    /// with them.
346    external_resources: Mutex<HashMap<String, Option<String>>>,
347    /// Every distinct file the package declares as an external resource,
348    /// computed once, on demand. Reading every `.rels` at open would have made
349    /// opening proportional to the number of objects, which is exactly what
350    /// level 1 promises not to be.
351    external_files: OnceLock<Vec<String>>,
352    /// The media relations the **archive** records, computed once:
353    /// `(part, lowercased uuid)` for every `destinationMedia` of an object's
354    /// `.rels`, and for every `sourceMedia` of a non-object part's `.rels`.
355    ///
356    /// On a package of 16 250 parts, loading the scene had become endless - every `media_links` re-read and
357    /// re-parsed every part's `.rels` (3.4 s a call), and the head images asked
358    /// once per object read. Same argument as `external_resources`: it reads
359    /// through `self.package` only, so no edit can make it stale; the overlay's
360    /// relations are laid over it on every call, and the attachments filter is
361    /// applied on every call too, since an edit can add or drop a part.
362    archive_media: OnceLock<Vec<(String, String)>>,
363    /// The data objects the archive's container parts hold **by value**
364    /// (a `Channel` in a `ChannelSet` in a `Log`), found by streaming those
365    /// parts without deserializing them. Built once, the first time a question
366    /// reaches a type that can be contained or a dataspace that holds a
367    /// container; never by `resources()`, never at open. A package with no
368    /// container part (all of RESQML 2.0.1) reads no part to build it.
369    ///
370    /// Reads through `self.package`, the archive, and never the overlay: no
371    /// edit can make it stale. The overlay's effect (a container removed or
372    /// replaced) is applied when it is read, as `live_of` does for `entries`.
373    contained: OnceLock<ContainedIndex>,
374    /// What has changed since the package was opened. Empty until
375    /// something is edited, so a reader pays nothing for it.
376    overlay: Overlay,
377    /// Identities of objects the layer holds that the archive never did.
378    ///
379    /// # Why this exists, and what it was hiding
380    ///
381    /// [`EpcStore::resources`] already walked the overlay, so a created object
382    /// appeared in an inventory. But [`EpcStore::resource`] and
383    /// [`EpcStore::find_by_uuid`] both go through `by_uuid`, which is built from
384    /// the archive and never touched again - so the same object was **listed and
385    /// not addressable**. Anything asking *what is at this uri* got `NotFound`
386    /// for something a caller had just created and could already read through
387    /// [`EpcStore::get_object`].
388    ///
389    /// That is not a small inconsistency. Spelling a reference starts by asking
390    /// for the holder's resource, so **no reference could be written from a
391    /// newly created object at all** - which is what made a workshop create
392    /// eighteen objects and link none of them, reporting only that some links
393    /// could not be written. And `find_by_uuid` is what a reference resolves
394    /// through, so every reference *pointing at* a created object read as
395    /// dangling.
396    ///
397    /// A map rather than a recomputation, because both accessors hand back a
398    /// borrow - and it never needs revisiting, since a uuid is immutable by
399    /// schema rule: written on `put`, dropped on `remove`. Keyed by the
400    /// lowercased uuid, **one list per uuid**: a second version of an object
401    /// created here is a second [`Made`], and its uri is the one
402    /// `reconcile_uris` gave it.
403    made: HashMap<String, Vec<Made>>,
404    /// When changes reach the disk.
405    policy: SavePolicy,
406    /// How to write, when the policy writes on its own.
407    save_options: SaveOptions,
408    /// Set by `close`, so `Drop` knows it has nothing left to complain about.
409    closed: bool,
410    /// How the names in array pointers become paths.
411    external: crate::backend::ExternalFiles,
412    /// What can read an array. Empty is a supported state, not a broken one.
413    backends: crate::backend::Backends,
414    /// External files declared by the caller rather than found in a `.rels`.
415    ///
416    /// The `.rels` of a package that was just opened say what its *author*
417    /// wrote; a proxy created in this session has no `.rels` yet, and would
418    /// otherwise be written out naming no file at all. Lowercased proxy uuid →
419    /// file name.
420    declared_files: std::collections::BTreeMap<String, String>,
421    /// Content type of each attachment added in this session.
422    ///
423    /// The archive's own `[Content_Types].xml` answers for the parts that were
424    /// in it; a part added since has no entry there, and the writer would fall
425    /// back to guessing from the extension. Guessing is a reasonable last
426    /// resort and a poor first one - a caller that *knows* it is handing over a
427    /// PDF should not have that turned into `application/octet-stream` because
428    /// the name ended in `.PDF`.
429    attachment_types: std::collections::BTreeMap<String, String>,
430    /// In-place edits whose `LINKS_OUT` is **provisional**: the journal said the
431    /// references may have moved because the path could not say otherwise, and
432    /// the references the object held *before* the edit are kept here so the
433    /// flag can be taken off when they are found unmoved
434    /// ([`EpcStore::settle_edits`]). Behind a mutex because the settling happens
435    /// when the journal is read, through `&self`.
436    pending_edits: Mutex<Vec<PendingEdit>>,
437    /// The slots whose object was lent out for an edit in place since the last
438    /// [`EpcStore::settle_versions`] - the only objects whose `objectVersion` can
439    /// have drifted from their entry's. Settling walks these and nothing else: it
440    /// runs at the start of every `put`, and walking the whole layer there made
441    /// writing *n* objects cost *n²* (2 699 BSS wells, 70 000 objects,
442    /// from 12 minutes to more than 50).
443    unsettled: BTreeSet<String>,
444    /// Read parts that break the schema's cardinality instead of refusing
445    /// them. Off by default; see [`EpcStore::set_lenient`].
446    lenient: bool,
447    /// Refuse a part that omits something the schema requires. Off by default;
448    /// see [`EpcStore::set_strict`].
449    strict: bool,
450    /// The transaction [`crate::write::EnergymlWriter::begin`] opened, with what
451    /// it was asked: its identity and whether it only reads. The layer itself
452    /// is the overlay's; this is what the writer said of it. Cleared when the
453    /// overlay has no transaction any more, however it ended.
454    writer_transaction: Option<crate::write::Transaction>,
455}
456
457/// The references an object held before an in-place edit, and the journal line
458/// that said they may have moved. See [`EpcStore::edit_at`].
459struct PendingEdit {
460    revision: u64,
461    /// The slot of the entry ([`slot_key`]) - its uuid for all but a second
462    /// entry of one uuid.
463    slot: String,
464    references: Vec<(String, String)>,
465}
466
467/// How many provisional edits are remembered; older ones keep the wider answer.
468const PENDING_EDITS: usize = 256;
469
470/// `(path, uuid)` of every reference an object makes - what the front end's
471/// `referencesMoved` compares, read from the reference graph instead of from the
472/// text.
473fn reference_list(object: &dyn Reflect) -> Vec<(String, String)> {
474    crate::graph::references(object)
475        .into_iter()
476        .map(|pointer| (pointer.path, pointer.uuid.to_ascii_lowercase()))
477        .collect()
478}
479
480impl EpcStore {
481    /// Open an EPC file from disk.
482    ///
483    /// The registry decides which types can be deserialized; it is supplied,
484    /// never assumed, so adding a schema version is an
485    /// `register` call in the application and not a change here.
486    pub fn open(path: impl AsRef<Path>, registry: Arc<Registry>) -> Result<Self> {
487        Self::open_as(path, registry, None)
488    }
489
490    /// [`EpcStore::open`], the file served as the **named** dataspace
491    /// `dataspace` (an ETP dataspace path such as `Lutetia` or `projets/demo`).
492    ///
493    /// Every uri of the store then reads `eml:///dataspace('Lutetia')/...`, so a
494    /// server holding several packages at once gives two distinct objects two
495    /// distinct uris. A uri of another dataspace, or one without a dataspace, is
496    /// [`Error::NotFound`] here: the identity is the full uri. A name the uri
497    /// grammar refuses is [`Error::BadUri`].
498    pub fn open_in_dataspace(
499        path: impl AsRef<Path>,
500        registry: Arc<Registry>,
501        dataspace: &str,
502    ) -> Result<Self> {
503        let dataspace = checked_dataspace(dataspace)?;
504        Self::open_as(path, registry, Some(dataspace))
505    }
506
507    fn open_as(path: impl AsRef<Path>, registry: Arc<Registry>, dataspace: Option<String>) -> Result<Self> {
508        let path = path.as_ref();
509        let _span = tracing::info_span!("epc.open", path = %path.display()).entered();
510        let file = std::fs::File::open(path)?;
511        let name = path
512            .file_name()
513            .map(|n| n.to_string_lossy().into_owned())
514            .unwrap_or_else(|| path.display().to_string());
515        let mut store = Self::from_reader_as(Box::new(file), name, registry, dataspace)?;
516        // An array pointer names its file with a bare name, and the
517        // specification says the file sits beside the package (§3.2.3).
518        if let Some(folder) = path.parent() {
519            store.external.set_root(folder);
520        }
521        store.path = Some(path.to_path_buf());
522        Ok(store)
523    }
524
525    /// Open an EPC from anything readable and seekable - bytes in memory, for
526    /// instance.
527    ///
528    /// A cloud service receives an EPC as the body of a response and has no
529    /// disk to put it on, so this is a requirement rather than a convenience.
530    pub fn from_reader(
531        source: Box<dyn ReadSeek + Send>,
532        name: impl Into<String>,
533        registry: Arc<Registry>,
534    ) -> Result<Self> {
535        Self::from_reader_as(source, name, registry, None)
536    }
537
538    /// [`EpcStore::from_reader`], served as the **named** dataspace `dataspace`;
539    /// see [`EpcStore::open_in_dataspace`].
540    pub fn from_reader_in_dataspace(
541        source: Box<dyn ReadSeek + Send>,
542        name: impl Into<String>,
543        registry: Arc<Registry>,
544        dataspace: &str,
545    ) -> Result<Self> {
546        let dataspace = checked_dataspace(dataspace)?;
547        Self::from_reader_as(source, name, registry, Some(dataspace))
548    }
549
550    fn from_reader_as(
551        source: Box<dyn ReadSeek + Send>,
552        name: impl Into<String>,
553        registry: Arc<Registry>,
554        dataspace: Option<String>,
555    ) -> Result<Self> {
556        let _span = tracing::info_span!("epc.read_package").entered();
557        let mut diagnostics = Vec::new();
558        let package = Package::open(source, &mut diagnostics)?;
559        let mut store = Self {
560            package,
561            registry,
562            name: name.into(),
563            dataspace,
564            path: None,
565            entries: Vec::new(),
566            by_uuid: HashMap::new(),
567            diagnostics: Mutex::new(diagnostics),
568            array_files: Mutex::new(HashMap::new()),
569            external_resources: Mutex::new(HashMap::new()),
570            external_files: OnceLock::new(),
571            archive_media: OnceLock::new(),
572            contained: OnceLock::new(),
573            overlay: Overlay::new(),
574            made: HashMap::new(),
575            policy: SavePolicy::default(),
576            save_options: SaveOptions::default(),
577            closed: false,
578            external: crate::backend::ExternalFiles::default(),
579            backends: crate::backend::Backends::compiled_in(),
580            declared_files: std::collections::BTreeMap::new(),
581            attachment_types: std::collections::BTreeMap::new(),
582            pending_edits: Mutex::new(Vec::new()),
583            unsettled: BTreeSet::new(),
584            lenient: false,
585            strict: false,
586            writer_transaction: None,
587        };
588        store.build_inventory();
589        Ok(store)
590    }
591
592    /// The file this store was opened from, when it was opened from one.
593    pub fn path(&self) -> Option<&Path> {
594        self.path.as_deref()
595    }
596
597    /// The registry this store deserializes with.
598    pub fn registry(&self) -> &Registry {
599        &self.registry
600    }
601
602    /// The same registry, shareable - what opening a **second** store with the
603    /// same schema knowledge needs, and what an extraction does.
604    ///
605    /// Handing out the `Arc` rather than making the caller build a registry
606    /// again is not a convenience: two registries in one process can disagree
607    /// about which versions exist, and a package written by one and read by the
608    /// other would refuse parts it had just produced.
609    pub fn registry_arc(&self) -> Arc<Registry> {
610        Arc::clone(&self.registry)
611    }
612
613    // -- Level 1: the inventory ------------------------------------------
614
615    /// The single dataspace this file is.
616    ///
617    /// An EPC **is** a dataspace, so this returns exactly one entry. That is
618    /// what lets code walking a remote store walk a file unchanged.
619    pub fn dataspace(&self) -> Dataspace {
620        let (created, last_changed) = self.core_property_dates();
621        {
622            let mut dataspace = Dataspace::new(self.dataspace_uri(), self.name.clone());
623            dataspace.created = created;
624            dataspace.last_changed = last_changed;
625            dataspace
626        }
627    }
628
629    /// The uri of the dataspace this file is: `eml:///` for the default one,
630    /// `eml:///dataspace('<path>')` for a named one.
631    fn dataspace_uri(&self) -> String {
632        identifiers::EtpUri::dataspace_uri(self.dataspace_path())
633    }
634
635    /// The path of the named dataspace, `None` for the default one - what the
636    /// uri constructors take.
637    fn dataspace_path(&self) -> Option<&str> {
638        self.dataspace.as_deref()
639    }
640
641    /// Refuse a uri of **another** dataspace. A uri that names an object but
642    /// not this store's dataspace names nothing here (the identity is the full
643    /// uri, Identifier Specification v5.0 §3.4; ETP v1.2 §25.3). A bare uuid, or
644    /// something that is not a data-object uri, is left to the caller's own
645    /// reading: that is the adapter at the entrance.
646    fn in_this_dataspace(&self, uri: &str) -> Result<()> {
647        match identifiers::EtpUri::parse(uri) {
648            Ok(parsed) if parsed.uuid.is_some() && parsed.dataspace.as_deref() != self.dataspace_path() => {
649                Err(Error::NotFound(uri.to_string()))
650            }
651            _ => Ok(()),
652        }
653    }
654
655    /// Every EnergyML object in the package, identity only.
656    ///
657    /// Built once when the package was opened, from `[Content_Types].xml` and -
658    /// only for the parts that file gets wrong - the root element. Then merged
659    /// with the in-memory layer: what was removed is gone from this list, what
660    /// was added is in it.
661    ///
662    /// **One row per entry**, an entry being (uuid, qualified type,
663    /// `objectVersion`): the same uuid under two versions, or under two
664    /// releases, is two rows, each with the uri that names it alone - see
665    /// [`EpcStore::resolve`].
666    pub fn resources(&self) -> Vec<ObjectSummary> {
667        let mut out: Vec<ObjectSummary> = self
668            .entries
669            .iter()
670            .filter(|entry| !matches!(self.overlay.get(&entry.slot), Some(Change::Removed)))
671            .map(|entry| entry.summary.clone())
672            .collect();
673
674        for (slot, change) in self.overlay.changed() {
675            let Change::Put(object) = change else { continue };
676            if self.is_archive_slot(slot) {
677                continue; // already listed, from the archive
678            }
679            // The summary `put` gave it - its uri is reconciled with its
680            // namesakes - or, for a layer that never went through `put`, the
681            // identity the object states.
682            let summary = self
683                .made_by_slot(slot)
684                .map(|made| made.summary.clone())
685                .or_else(|| self.summary_of(object.as_reflect()));
686            out.extend(summary);
687        }
688        out
689    }
690
691    /// The identity an object added to the layer will have once written.
692    ///
693    /// The **short** uri: whether it must be spelled with a version depends on
694    /// the object's namesakes, which only the store knows -
695    /// [`EpcStore::reconcile_uris`] settles it.
696    fn summary_of(&self, object: &dyn Reflect) -> Option<ObjectSummary> {
697        let key = identity::key_of_reflect(object)?;
698        Some(
699            ObjectSummary::new(
700                key.etp_uri(self.dataspace_path()),
701                key.uuid.to_string(),
702                key.qualified_type(),
703            )
704            .with_content_type(key.content_type())
705            .with_locator(key.part_path(self.save_options.layout)),
706        )
707    }
708
709    /// The summary of one object.
710    ///
711    /// The archive's entries and the objects created in this session are one
712    /// namespace: an object created here is addressable the moment it exists -
713    /// an accessor that said otherwise made it impossible to write a reference
714    /// from one. A uri that names more than one object is refused,
715    /// [`Error::Ambiguous`], with the uri of each candidate.
716    pub fn resource(&self, uri: &str) -> Result<&ObjectSummary> {
717        self.locate(uri).map(|live| live.summary)
718    }
719
720    /// Which object an address names - [`EpcStore::resource`] under the name
721    /// the rules are written for. **Rule**: the short uri is accepted when there is
722    /// no conflict (or when the object has no version), and the version is
723    /// required when there is one. So:
724    ///
725    /// - a **versioned** uri (`(uuid=…,version='…')`, Identifier Specification
726    ///   v5.0 §3.4.2, p. 14) names exactly that entry, or nothing -
727    ///   [`Error::NotFound`];
728    /// - a **short** uri names the one entry of its (uuid, type); when there
729    ///   are several, the one that declares no version, if exactly one does not
730    ///   (§3.4.2.1, p. 15 leaves *the most recent* to the server and ETP 1.2
731    ///   gives no way to tell, so this store does not guess); otherwise
732    ///   [`Error::Ambiguous`], naming every candidate;
733    /// - a **bare uuid** names the one entry that carries it, or is ambiguous.
734    ///
735    /// A type that differs only by its release still resolves while there is a
736    /// single candidate (*a wrong version is not a wrong type*): it is the
737    /// checker's business to report it, not the lookup's to refuse it.
738    pub fn resolve(&self, uri: &str) -> Result<&ObjectSummary> {
739        self.resource(uri)
740    }
741
742    /// The object a **reference** names, resolved by (uuid, qualified type,
743    /// `objectVersion` when the reference carries one) with the rules of
744    /// [`EpcStore::resolve`]. A reference whose version no entry carries still
745    /// resolves when it has a single candidate - the checker reports the
746    /// mismatch (`ObjectVersionMismatch`) - and is [`Error::NotFound`] when
747    /// several entries leave no way to say which was meant.
748    pub fn resolve_reference(
749        &self,
750        uuid: &str,
751        qualified_type: Option<&str>,
752        object_version: Option<&str>,
753    ) -> Result<&ObjectSummary> {
754        self.locate_triple(uuid, qualified_type, object_version, true)
755            .map(|live| live.summary)
756    }
757
758    /// **The one lookup.** The entry named by (uuid, qualified type, `objectVersion`),
759    /// each part optional but the uuid: the uuid first, then the type, then the
760    /// version - and the version only matters when more than one entry is left
761    /// (Identifier Specification v5.0 §3.4.2.1, p. 15). [`EpcStore::resolve`] (a
762    /// URI) and [`EpcStore::lookup_uuid`] (a bare uuid) are this with the triple
763    /// read from their argument; every resolution inside the store goes through
764    /// it, and no other code filters entries by uuid, type or version. The refusal
765    /// names what was asked, spelled from the triple.
766    pub fn lookup(
767        &self,
768        uuid: &str,
769        qualified_type: Option<&str>,
770        object_version: Option<&str>,
771    ) -> Result<&ObjectSummary> {
772        self.locate_triple(uuid, qualified_type, object_version, false)
773            .map(|live| live.summary)
774    }
775
776    /// [`EpcStore::lookup`] with only a uuid: the one entry that carries it, or
777    /// [`Error::Ambiguous`] when the uuid alone is not enough.
778    pub fn lookup_uuid(&self, uuid: &str) -> Result<&ObjectSummary> {
779        self.lookup(uuid, None, None)
780    }
781
782    /// The core, over a triple. `reference` is the grace a reference gets for a
783    /// version nobody carries (see `choose`).
784    fn locate_triple(
785        &self,
786        uuid: &str,
787        qualified_type: Option<&str>,
788        object_version: Option<&str>,
789        reference: bool,
790    ) -> Result<Live<'_>> {
791        let version = normal_version(object_version);
792        let given = match qualified_type {
793            Some(qualified_type) => identifiers::EtpUri::object_versioned(
794                self.dataspace_path(),
795                qualified_type,
796                uuid,
797                version.as_deref(),
798            ),
799            None => uuid.to_string(),
800        };
801        Self::choose(
802            self.live_for(&uuid.to_ascii_lowercase(), qualified_type)?,
803            qualified_type,
804            version.as_deref(),
805            &given,
806            reference,
807        )
808    }
809
810    /// The object carrying this uuid, whatever its type.
811    ///
812    /// A uuid identifies an object across every version, so this is what a
813    /// reference resolves through - see [`crate::graph::check_references`]. With
814    /// several entries under one uuid it answers **one of them** - the one that
815    /// declares no `objectVersion` if there is one, otherwise the first in
816    /// archive order - which is enough for what it is used for, *is there such
817    /// an object*. Whoever needs *which* asks [`EpcStore::resolve_reference`]
818    /// or [`EpcStore::resolve`], which refuse to guess.
819    pub fn find_by_uuid(&self, uuid: &str) -> Option<&ObjectSummary> {
820        // A reference resolves through this. Without the layer, every
821        // reference pointing at an object created in this session read as
822        // dangling - the package it was written into held it.
823        Self::primary(self.live_of(&uuid.to_ascii_lowercase())).map(|live| live.summary)
824    }
825
826    /// Every object of the package that carries this uuid - one per entry: the
827    /// same uuid under two `objectVersion`s, or under two releases, is two
828    /// summaries, each with the uri that names it alone. Empty when there is
829    /// none. What a derived view of the package asks when a uuid is journaled and
830    /// it must know whether the uuid still names a single object.
831    pub fn summaries_of(&self, uuid: &str) -> Vec<ObjectSummary> {
832        self.live_of(&uuid.to_ascii_lowercase())
833            .into_iter()
834            .map(|one| one.summary.clone())
835            .collect()
836    }
837
838    /// Every entry of one uuid - the archive's and this session's - minus what
839    /// was removed. `uuid` is lowercased.
840    fn live_of(&self, uuid: &str) -> Vec<Live<'_>> {
841        let mut out = Vec::new();
842        for &at in self.by_uuid.get(uuid).map(Vec::as_slice).unwrap_or_default() {
843            let entry = &self.entries[at];
844            if matches!(self.overlay.get(&entry.slot), Some(Change::Removed)) {
845                continue;
846            }
847            out.push(Live {
848                slot: &entry.slot,
849                summary: &entry.summary,
850                version: self.current_version(&entry.slot, entry.object_version.as_deref()),
851                archive: Some(entry),
852                nested: None,
853            });
854        }
855        for made in self.made.get(uuid).map(Vec::as_slice).unwrap_or_default() {
856            // `made` is not rolled back with a transaction; the layer is, and it
857            // is the layer that says whether the object is still there.
858            if !matches!(self.overlay.get(&made.slot), Some(Change::Put(_))) {
859                continue;
860            }
861            out.push(Live {
862                slot: &made.slot,
863                summary: &made.summary,
864                version: self.current_version(&made.slot, made.object_version.as_deref()),
865                archive: None,
866                nested: None,
867            });
868        }
869        out
870    }
871
872    /// The `objectVersion` an entry has **now**: the object the layer holds when it
873    /// holds one - an edit of the version is visible to a lookup at once - else
874    /// what the entry was born with.
875    fn current_version<'a>(&'a self, slot: &str, stored: Option<&'a str>) -> Option<&'a str> {
876        match self.overlay.get(slot) {
877            Some(Change::Put(object)) => identity::object_version_of_reflect(object.as_reflect())
878                .map(str::trim)
879                .filter(|version| !version.is_empty()),
880            _ => stored,
881        }
882    }
883
884    /// The entry that stands for its uuid when only the uuid is known: the one
885    /// without a version, else the first.
886    fn primary(mut live: Vec<Live<'_>>) -> Option<Live<'_>> {
887        match live.iter().position(|one| one.version.is_none()) {
888            Some(at) => Some(live.swap_remove(at)),
889            None if live.is_empty() => None,
890            None => Some(live.remove(0)),
891        }
892    }
893
894    /// Which entry an address names. See [`EpcStore::resolve`].
895    fn locate(&self, uri: &str) -> Result<Live<'_>> {
896        self.in_this_dataspace(uri)?;
897        let address = address_of(uri)?;
898        Self::choose(
899            self.live_for(&address.uuid, address.qualified_type.as_deref())?,
900            address.qualified_type.as_deref(),
901            address.version.as_deref(),
902            uri,
903            false,
904        )
905    }
906
907    /// The one entry of `live` that an address (or a reference) names - or a
908    /// **named** refusal. `reference` makes a version no entry carries resolve
909    /// when there is a single candidate: a reference is a copy of what its
910    /// target says, and a copy that is stale is a finding, not a dangling
911    /// pointer.
912    fn choose<'a>(
913        mut live: Vec<Live<'a>>,
914        qualified_type: Option<&str>,
915        version: Option<&str>,
916        given: &str,
917        reference: bool,
918    ) -> Result<Live<'a>> {
919        if live.is_empty() {
920            return Err(Error::NotFound(given.to_string()));
921        }
922        // The type narrows when it matches something; when it matches nothing it
923        // is a release apart, and the uuid alone decides.
924        if let Some(qualified_type) = qualified_type
925            && live
926                .iter()
927                .any(|one| one.summary.qualified_type == qualified_type)
928        {
929            live.retain(|one| one.summary.qualified_type == qualified_type);
930        }
931        if let Some(wanted) = version {
932            let hits = live.iter().filter(|one| one.version == Some(wanted)).count();
933            match hits {
934                0 if reference && live.len() == 1 => return Ok(live.remove(0)),
935                0 => return Err(Error::NotFound(given.to_string())),
936                _ => live.retain(|one| one.version == Some(wanted)),
937            }
938        }
939        if live.len() == 1 {
940            return Ok(live.remove(0));
941        }
942        // Several: a short uri still names the one that declares no version -
943        // the rule above - if exactly one does. A bare uuid names no type, but
944        // when every candidate is of one type it says exactly what the short uri
945        // of that type says, and gets the same answer: the readers of the domain
946        // resolve a reference through `ObjectSource::object(uuid)`, and a
947        // reference to the original of a versioned copy must not stop resolving
948        // because the copy arrived. Two types under one uuid stay ambiguous.
949        let one_type = live
950            .iter()
951            .all(|one| one.summary.qualified_type == live[0].summary.qualified_type);
952        if (qualified_type.is_some() || one_type) && version.is_none() {
953            let bare: Vec<usize> = live
954                .iter()
955                .enumerate()
956                .filter(|(_, one)| one.version.is_none())
957                .map(|(at, _)| at)
958                .collect();
959            if let [only] = bare[..] {
960                return Ok(live.remove(only));
961            }
962        }
963        Err(Error::Ambiguous {
964            uri: given.to_string(),
965            candidates: live.iter().map(|one| one.summary.uri.clone()).collect(),
966        })
967    }
968
969    /// The slot of the entry an address names.
970    fn slot_of(&self, uri: &str) -> Result<String> {
971        self.locate(uri).map(|live| live.slot.to_string())
972    }
973
974    /// Whether an archive entry holds this slot.
975    fn is_archive_slot(&self, slot: &str) -> bool {
976        self.by_uuid
977            .get(uuid_of_slot(slot))
978            .is_some_and(|list| list.iter().any(|&at| self.entries[at].slot == slot))
979    }
980
981    /// The object this session created, by slot.
982    fn made_by_slot(&self, slot: &str) -> Option<&Made> {
983        self.made
984            .get(uuid_of_slot(slot))?
985            .iter()
986            .find(|made| made.slot == slot)
987    }
988
989    /// Spell the uri of every live entry of a uuid, again: **with its version
990    /// whenever it declares one**, the short form only for an entry that
991    /// declares none - internally the uri always carries the version number when
992    /// there is one (ADR-0003). The short form stays
993    /// accepted **as an address** ([`EpcStore::resolve`]), never emitted for a
994    /// versioned entry. Called whenever the set of entries of a uuid
995    /// changes: at open, after a put, after a removal. Returns, for each entry
996    /// whose uri moved, its slot and the uri it had - the caller says so in the
997    /// journal (an identity event, ADR-0010: *a second version
998    /// changes the uri of the first*).
999    fn reconcile_uris(&mut self, uuid: &str) -> Vec<(String, String)> {
1000        let key = uuid.to_ascii_lowercase();
1001        // (in the archive?, position, slot, type, version) of everything live.
1002        let mut rows: Vec<(bool, usize, String, String, Option<String>)> = Vec::new();
1003        for &at in self.by_uuid.get(&key).map(Vec::as_slice).unwrap_or_default() {
1004            let entry = &self.entries[at];
1005            if matches!(self.overlay.get(&entry.slot), Some(Change::Removed)) {
1006                continue;
1007            }
1008            rows.push((
1009                true,
1010                at,
1011                entry.slot.clone(),
1012                entry.summary.qualified_type.clone(),
1013                self.current_version(&entry.slot, entry.object_version.as_deref())
1014                    .map(str::to_string),
1015            ));
1016        }
1017        for (at, made) in self
1018            .made
1019            .get(&key)
1020            .map(Vec::as_slice)
1021            .unwrap_or_default()
1022            .iter()
1023            .enumerate()
1024        {
1025            if !matches!(self.overlay.get(&made.slot), Some(Change::Put(_))) {
1026                continue;
1027            }
1028            rows.push((
1029                false,
1030                at,
1031                made.slot.clone(),
1032                made.summary.qualified_type.clone(),
1033                self.current_version(&made.slot, made.object_version.as_deref())
1034                    .map(str::to_string),
1035            ));
1036        }
1037        let mut moved = Vec::new();
1038        let dataspace = self.dataspace.clone();
1039        for (archive, at, slot, qualified_type, version) in &rows {
1040            let summary = if *archive {
1041                &mut self.entries[*at].summary
1042            } else {
1043                match self.made.get_mut(&key) {
1044                    Some(list) => &mut list[*at].summary,
1045                    None => continue,
1046                }
1047            };
1048            let uri = identifiers::EtpUri::object_versioned(
1049                dataspace.as_deref(),
1050                qualified_type,
1051                &summary.uuid,
1052                version.as_deref(),
1053            );
1054            if summary.uri != uri {
1055                moved.push((slot.clone(), std::mem::replace(&mut summary.uri, uri)));
1056            }
1057        }
1058        moved
1059    }
1060
1061    // -- Level 2: the headers --------------------------------------------
1062
1063    /// Citations for the given objects, without deserializing them.
1064    ///
1065    /// One result per requested URI, in the same order; an unreadable object
1066    /// does not fail the others. Each part is inflated only as far as the end
1067    /// of its citation.
1068    pub fn headers(&self, uris: &[String]) -> Vec<Result<ObjectHeader>> {
1069        self.routed(
1070            uris,
1071            |uri| self.header(uri),
1072            |entries| self.contained_headers(entries),
1073        )
1074    }
1075
1076    /// The citation of one object.
1077    ///
1078    /// From the object itself when the in-memory layer holds it - reading the
1079    /// archive would return the citation as it was before the edit.
1080    pub fn header(&self, uri: &str) -> Result<ObjectHeader> {
1081        // An object held by value: its own citation, never its container's.
1082        if let Some(content) = self.contained_entry(uri)? {
1083            return self
1084                .contained_headers(&[content])
1085                .pop()
1086                .unwrap_or_else(|| Err(Error::NotFound(uri.to_string())));
1087        }
1088        let slot = self.slot_of(uri)?;
1089        if let Some(Change::Put(object)) = self.overlay.get(&slot) {
1090            return self.header_of(object.as_reflect());
1091        }
1092        let entry = self.entry(uri)?;
1093        let peeked = self
1094            .package
1095            .with_part(&entry.summary.locator, |r| header::peek_reader(BufReader::new(r)))?;
1096        Ok(contained_read::header_from_peek(&entry.summary, peeked))
1097    }
1098
1099    /// Every object's citation, in inventory order.
1100    ///
1101    /// What an interface listing the file shows. Costs one partial inflate per
1102    /// object and no full deserialization at all.
1103    pub fn all_headers(&self) -> Vec<Result<ObjectHeader>> {
1104        self.entries.iter().map(|e| self.header(&e.summary.uri)).collect()
1105    }
1106
1107    // -- Level 3: the objects --------------------------------------------
1108
1109    /// The objects themselves.
1110    ///
1111    /// One result per requested URI, in the same order.
1112    pub fn get_objects(&self, uris: &[String]) -> Vec<Result<Box<dyn DynObject>>> {
1113        self.routed(
1114            uris,
1115            |uri| self.get_object(uri),
1116            |entries| self.contained_objects(entries),
1117        )
1118    }
1119
1120    /// One object, fully deserialized.
1121    ///
1122    /// The in-memory layer first: an object that was edited must not come back
1123    /// as it was on disk. Each caller gets its own copy.
1124    pub fn get_object(&self, uri: &str) -> Result<Box<dyn DynObject>> {
1125        // An object held by value: extracted from its container's part and read
1126        // by the reader a part goes through.
1127        if let Some(content) = self.contained_entry(uri)? {
1128            return self
1129                .contained_objects(&[content])
1130                .pop()
1131                .unwrap_or_else(|| Err(Error::NotFound(uri.to_string())));
1132        }
1133        match self.overlay.get(&self.slot_of(uri)?) {
1134            Some(Change::Put(object)) => return Ok(object.clone_box()),
1135            Some(Change::Removed) => return Err(Error::NotFound(uri.to_string())),
1136            None => {}
1137        }
1138        let entry = self.entry(uri)?;
1139        let Some(info) = entry.info else {
1140            // A version the project ships and this build does not carry is
1141            // refused by the sentence that names what would bring it; any
1142            // other unregistered type stays the plain unknown one.
1143            return Err(match &entry.refusal {
1144                Some(why) => energyml_core::error::Error::VersionAbsent(why.clone()).into(),
1145                None => Error::UnknownType(entry.summary.content_type.clone()),
1146            });
1147        };
1148        let part = entry.summary.locator.clone();
1149        let bytes = self.raw_part(&part)?;
1150
1151        // The version the identification settled on, then the ones it could not
1152        // rule out. One namespace covers every version of a package and
1153        // `schemaVersion` is optional in practice, so "which release is this"
1154        // is sometimes a question only the parser can answer - and it answers
1155        // it by failing on the wrong one. See `root_candidates`.
1156        let mut attempts = Vec::with_capacity(1 + entry.alternatives.len());
1157        attempts.push(info);
1158        attempts.extend(entry.alternatives.iter().copied());
1159
1160        // **Two passes, and the first one is strict on purpose.**
1161        //
1162        // Telling two releases apart works by letting the wrong one fail, and
1163        // an element required on one side and absent on the other is very often
1164        // the only difference the parser can see. Reading tolerantly on the
1165        // first pass would blunt exactly that: the wrong version would no
1166        // longer refuse, and a part would come back as a plausible object of
1167        // the wrong release with only the fields the two share.
1168        //
1169        // So: strict first, over the candidates best-first - that is the pass
1170        // that decides *which release this is*. Only when no candidate reads
1171        // strictly does the caller's own mode get a turn, and by then the
1172        // question is no longer which version but how much to put right.
1173        let passes = read_passes(self.mode());
1174
1175        let mut first: Option<Error> = None;
1176        for mode in passes {
1177            for (rank, candidate) in attempts.iter().enumerate() {
1178                // Dispatched on the **candidate**, not on its content type. A
1179                // development drop shares that string with the release it precedes
1180                // - `resqml 2.2_dev3` declares `version="2.2"` - so a lookup by
1181                // content type hands every candidate of such a pair the same type,
1182                // and this walk would read the same thing each time while looking
1183                // like it had tried both.
1184                let outcome = self
1185                    .registry
1186                    .read_xml_as_named(candidate, &bytes, *mode)
1187                    .or_else(|first| {
1188                        // A type the registry cannot address by name: fall back to
1189                        // the string, which is what every release pair used before
1190                        // drops existed.
1191                        match candidate.content_type {
1192                            Some(ct) => self.registry.read_xml_as(ct, &bytes, *mode),
1193                            None => self
1194                                .registry
1195                                .read_xml_as(&entry.summary.content_type, &bytes, *mode)
1196                                .map_err(|_| first),
1197                        }
1198                    });
1199                match outcome {
1200                    Ok((object, departures)) => {
1201                        if rank > 0 {
1202                            self.record(Diagnostic::warning(
1203                                &part,
1204                                format!(
1205                                    "would not read as `{}`; read as `{}` instead - the document's \
1206                                 namespace names the package and not the version",
1207                                    attempts[0].qualified_type.unwrap_or(attempts[0].name),
1208                                    candidate.qualified_type.unwrap_or(candidate.name)
1209                                ),
1210                            ));
1211                        }
1212                        // Vocal, as everywhere else here: the object now holds
1213                        // values nobody wrote, and a caller that saves it would put
1214                        // them on disk. The two kinds are kept apart - an absent
1215                        // element is a producer forgetting, a value that could not
1216                        // be read is a producer writing something wrong, and the
1217                        // second is the worse news.
1218                        for item in departures.defaulted {
1219                            self.record(Diagnostic::warning(&part, item.to_string()));
1220                        }
1221                        for item in departures.repaired {
1222                            self.record(Diagnostic::warning(&part, item.to_string()));
1223                        }
1224                        return Ok(object);
1225                    }
1226                    Err(e) => {
1227                        // The error worth reporting is the one from the strict
1228                        // pass over the document's own version: it names what is
1229                        // actually wrong, where a later pass would report the
1230                        // second-best guess failing for a second-best reason.
1231                        if first.is_none() {
1232                            first = Some(e.into());
1233                        }
1234                    }
1235                }
1236            }
1237        }
1238        // Every candidate refused it in every pass. The first error is the one
1239        // to report: it is the version the document itself pointed at, read
1240        // strictly.
1241        Err(first.unwrap_or_else(|| Error::UnknownType(entry.summary.content_type.clone())))
1242    }
1243
1244    /// Read parts that break the schema's cardinality instead of refusing
1245    /// them.
1246    ///
1247    /// Off by default, and it should stay off for anything that will be
1248    /// written back: an absent required element comes back as its Rust default -
1249    /// an empty string, a zero - which is not what the document said. Each
1250    /// one becomes a `Diagnostic`, so [`diagnostics`][Self::diagnostics] tells
1251    /// exactly what was invented and in which part.
1252    ///
1253    /// It earns its place on packages a producer wrote without some mandatory
1254    /// element: refusing those loses the geometry, the references and the
1255    /// citations the file *did* carry, over a field nothing downstream reads.
1256    pub fn set_lenient(&mut self, lenient: bool) {
1257        self.lenient = lenient;
1258    }
1259
1260    /// Refuse a part that omits something the schema requires.
1261    ///
1262    /// Off by default. An absent required element is filled with its Rust
1263    /// default and reported as a diagnostic, because refusing there costs a
1264    /// caller every field the part *did* carry - see
1265    /// [`ReadMode`].
1266    ///
1267    /// Turn it on for the one job where the difference bites: **writing the
1268    /// package back**. An object read tolerantly holds values nobody wrote, and
1269    /// saving it puts them on disk. That is why the round-trip check sets this.
1270    pub fn set_strict(&mut self, strict: bool) {
1271        self.strict = strict;
1272    }
1273
1274    /// Whether parts that omit a required item are refused.
1275    pub fn is_strict(&self) -> bool {
1276        self.strict
1277    }
1278
1279    /// The reading mode the two switches add up to.
1280    fn mode(&self) -> ReadMode {
1281        match (self.strict, self.lenient) {
1282            (true, _) => ReadMode::Strict,
1283            (false, true) => ReadMode::Lenient,
1284            (false, false) => ReadMode::Tolerant,
1285        }
1286    }
1287
1288    /// Whether parts breaking the schema's cardinality are read.
1289    pub fn is_lenient(&self) -> bool {
1290        self.lenient
1291    }
1292
1293    /// The raw XML of one part, as stored - or as staged, once it has been.
1294    ///
1295    /// For a round-trip check, or for a caller that has its own parser.
1296    pub fn part_bytes(&self, uri: &str) -> Result<Vec<u8>> {
1297        let part = self.entry(uri)?.summary.locator.clone();
1298        self.raw_part(&part)
1299    }
1300
1301    /// The bytes of a part **by part name**, whether or not it is an object.
1302    ///
1303    /// The addressing of last resort, and the reason it exists: a part whose
1304    /// root element cannot be read has no uuid, so no uri, so
1305    /// [`EpcStore::part_bytes`] cannot reach it. This can. What comes back is
1306    /// what a save would write - the staged bytes when there are any, the
1307    /// archive's otherwise.
1308    pub fn raw_part(&self, part: &str) -> Result<Vec<u8>> {
1309        if let Some(bytes) = self.overlay.raw(part) {
1310            return Ok(bytes.to_vec());
1311        }
1312        self.package.read(part)
1313    }
1314
1315    /// Stage replacement bytes for a part, to be written **verbatim**.
1316    ///
1317    /// The repair path, and the last one there is. Every other way of changing
1318    /// a package goes through an object: read it, edit it by reflection, let
1319    /// the writer serialise it back. That requires the part to parse, and the
1320    /// whole point here is that it does not.
1321    ///
1322    /// So these bytes are not parsed, not validated and not rewritten - they go
1323    /// into the archive as they are. Two consequences worth stating, because
1324    /// neither is recoverable afterwards:
1325    ///
1326    /// - **The `.rels` are not regenerated for this part.** They are built from
1327    ///   an object's `DataObjectReference`s, and there is no object. Whatever
1328    ///   relationships the part had are the ones it keeps.
1329    /// - **Nothing checks that the bytes are the object the package thinks the
1330    ///   part holds.** Staging a point set under a triangulated set's part name
1331    ///   produces a package that lies, and says nothing.
1332    ///
1333    /// Both are the price of being able to fix a file no parser will touch.
1334    /// [`EpcStore::staged_part_parses`] answers the first question a caller
1335    /// should ask before saving.
1336    pub fn set_raw_part(&mut self, part: &str, bytes: Vec<u8>) {
1337        self.overlay.set_raw(part, bytes);
1338        // A part's bytes are what `get_object` reads, and a part name carries
1339        // no uuid to name: every derived view rebuilds.
1340        self.overlay.touch_all();
1341    }
1342
1343    /// Drop what was staged for a part. `true` when there was something.
1344    pub fn clear_raw_part(&mut self, part: &str) -> bool {
1345        let dropped = self.overlay.clear_raw(part);
1346        if dropped {
1347            self.overlay.touch_all();
1348        }
1349        dropped
1350    }
1351
1352    /// The revision of the last object write - what a derived view of the
1353    /// package (`energyml_domain::package_index`) compares with its own to know
1354    /// whether it is still exact (ADR-0010).
1355    pub fn revision(&self) -> u64 {
1356        self.overlay.revision()
1357    }
1358
1359    /// How many times the package's **files** changed - a part staged or
1360    /// dropped, a media relation made or undone, a transaction opened, merged or
1361    /// thrown away. Not an object write, so not in [`EpcStore::revision`]: a
1362    /// reader of what is attached to an object compares this stamp with its own
1363    /// and re-reads when they differ.
1364    pub fn files_revision(&self) -> u64 {
1365        self.overlay.files_revision()
1366    }
1367
1368    /// Which objects were written after `since`: nothing, these uuids
1369    /// (lower-cased), or everything - a reader re-reads accordingly.
1370    pub fn changed_since(&self, since: u64) -> revision::Changed {
1371        match self.overlay.changed_since(since) {
1372            // The journal names slots; a reader of this answer knows uuids, and
1373            // two versions of one uuid are one name to it.
1374            revision::Changed::Some(slots) => {
1375                revision::Changed::Some(slots.iter().map(|slot| uuid_of_slot(slot).to_string()).collect())
1376            }
1377            other => other,
1378        }
1379    }
1380
1381    /// What was written after `since`, and **what in each object** - the
1382    /// journal of ADR-0010: per object
1383    /// the citation, the body, the links, a creation, a removal, an identity
1384    /// change; the datasets written; whether parts or media moved; the data file
1385    /// names declared. [`revision::Since::Everything`] when the log cannot say:
1386    /// the reader rebuilds, it never concludes "nothing".
1387    ///
1388    /// Uris are in **the store's spelling** (what `resources()` lists); an object
1389    /// the store no longer knows keeps the uri its gate gave.
1390    pub fn changes_since(&self, since: u64) -> revision::Since {
1391        self.settle_edits(None);
1392        match self.overlay.changes_since(since) {
1393            revision::Since::Exact(mut change) => {
1394                // The journal keys its lines by slot ([`slot_key`]): the uuid for
1395                // all but a second entry of one uuid. What a reader gets is the
1396                // uuid, and the uri that tells the entries of one uuid apart.
1397                for object in &mut change.objects {
1398                    if let Some(known) = self.known_uri(&object.uuid) {
1399                        object.uri = Some(known);
1400                    }
1401                    object.uuid = uuid_of_slot(&object.uuid).to_string();
1402                }
1403                change
1404                    .objects
1405                    .sort_by(|a, b| (&a.uuid, &a.uri).cmp(&(&b.uuid, &b.uri)));
1406                revision::Since::Exact(change)
1407            }
1408            everything => everything,
1409        }
1410    }
1411
1412    /// Datasets of a data file were written **by someone other than this store** -
1413    /// the front end through h5wasm, a tool that wrote the `.h5` itself - and the
1414    /// journal must say so, at the same place it says what happened to objects.
1415    /// The writes made through [`EpcStore::write_array_to`] and
1416    /// [`EpcStore::write_arrays_to`] are journaled by those gates; calling this
1417    /// for them too is harmless, only wide.
1418    ///
1419    /// `file` is the name the package gives the file. Returns the new revision.
1420    pub fn arrays_written(&self, file: &str, paths: &[&str]) -> u64 {
1421        if paths.is_empty() {
1422            return self.data_file_written(file);
1423        }
1424        self.overlay.journal().note_datasets(file, paths.iter().copied())
1425    }
1426
1427    /// A data file was brought, replaced or created **as a whole** - an import
1428    /// that copies it, a creation that writes it. Its datasets are not listed:
1429    /// the journal says the file (an empty path).
1430    pub fn data_file_written(&self, file: &str) -> u64 {
1431        self.overlay.journal().note_datasets(file, std::iter::empty())
1432    }
1433
1434    /// The uri this store gives an object it holds or held, if it can tell.
1435    fn known_uri(&self, slot: &str) -> Option<String> {
1436        // The entry the slot belongs to, as the store spells it now - which, for
1437        // an entry held beside a namesake, is the form with its version.
1438        let held = self
1439            .by_uuid
1440            .get(uuid_of_slot(slot))
1441            .and_then(|list| {
1442                list.iter()
1443                    .map(|&at| &self.entries[at])
1444                    .find(|entry| entry.slot == slot)
1445            })
1446            .map(|entry| (&entry.summary, entry.object_version.as_deref()))
1447            .or_else(|| {
1448                self.made_by_slot(slot)
1449                    .map(|made| (&made.summary, made.object_version.as_deref()))
1450            });
1451        if let Some(Change::Put(object)) = self.overlay.get(slot) {
1452            let stated = self.summary_of(object.as_reflect())?;
1453            let stated_version = normal_version(identity::object_version_of_reflect(object.as_reflect()));
1454            // An object edited in place may state another identity than the one
1455            // the entry was born with (an edited uuid, **or an edited
1456            // `objectVersion`** - an object is (uuid, type, version), and two
1457            // versions of one uuid are two objects): its journal line then names
1458            // the identity it states.
1459            return match held {
1460                Some((entry, version))
1461                    if entry.uuid.eq_ignore_ascii_case(&stated.uuid)
1462                        && entry.qualified_type == stated.qualified_type
1463                        && normal_version(version) == stated_version =>
1464                {
1465                    Some(entry.uri.clone())
1466                }
1467                _ => Some(stated.uri),
1468            };
1469        }
1470        held.map(|(summary, _)| summary.uri.clone())
1471    }
1472
1473    /// Take `LINKS_OUT` off the journal lines of in-place edits whose object
1474    /// turns out to make the same references as before - the refinement the
1475    /// design asks for (ADR-0010), done at the gate instead of at the index
1476    /// because the gate is where the *before* is at hand. With `only`, just that
1477    /// object's pending edit (it is about to be edited again).
1478    fn settle_edits(&self, only: Option<&str>) {
1479        let mut pending = self
1480            .pending_edits
1481            .lock()
1482            .unwrap_or_else(|poisoned| poisoned.into_inner());
1483        if pending.is_empty() {
1484            return;
1485        }
1486        let mut kept = Vec::new();
1487        for edit in pending.drain(..) {
1488            if only.is_some_and(|slot| slot != edit.slot) {
1489                kept.push(edit);
1490                continue;
1491            }
1492            if let Some(Change::Put(object)) = self.overlay.get(&edit.slot)
1493                && reference_list(object.as_reflect()) == edit.references
1494            {
1495                self.overlay
1496                    .journal()
1497                    .narrow(edit.revision, &edit.slot, What::LINKS_OUT);
1498            }
1499        }
1500        *pending = kept;
1501    }
1502
1503    /// The parts with bytes staged for them.
1504    pub fn staged_parts(&self) -> Vec<String> {
1505        self.overlay.raw_parts()
1506    }
1507
1508    // -- The package as a container of files ---------------------------------
1509
1510    /// Every part of the package, whatever it is - **the archive as a listing**.
1511    ///
1512    /// # Why the library and not the caller
1513    ///
1514    /// A front end can open the same zip itself, and the answer would be wrong in
1515    /// exactly the way that matters: the bytes on disk are not the package any
1516    /// more once anything has been edited. A part added in this session is in no
1517    /// archive, a part dropped is still in one, and an object edited is neither
1518    /// its old bytes nor a file yet. This walks what a **save would write**, and
1519    /// marks the three states rather than flattening them.
1520    pub fn parts(&self) -> Vec<PartListing> {
1521        let mut out: Vec<PartListing> = Vec::new();
1522        let mut seen: BTreeSet<String> = BTreeSet::new();
1523        // A map, not `entries.iter().find` per part: on a package of 16 250
1524        // parts that was a quarter of a billion comparisons, 2.3 s a call
1525        // (the scene's loading had become endless).
1526        let objects: HashMap<&str, &PartEntry> = self
1527            .entries
1528            .iter()
1529            .map(|e| (e.summary.locator.as_str(), e))
1530            .collect();
1531
1532        for name in self.package.entries() {
1533            if name.ends_with('/') {
1534                continue;
1535            }
1536            seen.insert(name.clone());
1537            let entry = objects.get(name.as_str()).copied();
1538            out.push(PartListing {
1539                kind: match () {
1540                    _ if entry.is_some() => PartKind::Object,
1541                    _ if name == "[Content_Types].xml" => PartKind::ContentTypes,
1542                    _ if name.ends_with(".rels") => PartKind::Relationships,
1543                    _ if name.eq_ignore_ascii_case("docProps/core.xml") => PartKind::CoreProperties,
1544                    _ if name.starts_with("docProps/") => PartKind::PackageExtension,
1545                    _ => PartKind::Attachment,
1546                },
1547                uri: entry.map(|e| e.summary.uri.clone()),
1548                qualified_type: entry.map(|e| e.summary.qualified_type.clone()),
1549                size: self.package.size(name),
1550                content_type: self.package.content_type(name).map(str::to_string),
1551                part: name.clone(),
1552                added: false,
1553                // Both of the ways a part leaves: dropped by a caller, or
1554                // superseded by an object that now stands where it stood.
1555                dropped: self.overlay.is_dropped(name) || self.overlay.is_superseded(name),
1556                staged: self.overlay.raw(name).is_some(),
1557            });
1558        }
1559
1560        // What the archive never held: attachments added since it was opened.
1561        for part in self.overlay.raw_parts() {
1562            if seen.contains(&part) {
1563                continue;
1564            }
1565            let bytes = self.overlay.raw(&part).map(<[u8]>::len).unwrap_or(0) as u64;
1566            out.push(PartListing {
1567                kind: PartKind::Attachment,
1568                uri: None,
1569                qualified_type: None,
1570                size: Some(bytes),
1571                content_type: self.attachment_types.get(&part).cloned(),
1572                part,
1573                added: true,
1574                dropped: false,
1575                staged: true,
1576            });
1577        }
1578
1579        out.sort_by(|a, b| a.part.cmp(&b.part));
1580        out
1581    }
1582
1583    /// Put a file in the package - a PDF, an image, a note.
1584    ///
1585    /// # What it refuses, and why each refusal is not a nuisance
1586    ///
1587    /// - **A part name an object already occupies.** Those bytes are written
1588    ///   from the object on save, so an attachment there would either be
1589    ///   overwritten without a word or would silently replace an object with
1590    ///   something that is not one. Repairing an object's bytes is
1591    ///   [`EpcStore::set_raw_part`], which says what it is doing.
1592    /// - **The package's own infrastructure** - `[Content_Types].xml`, a
1593    ///   `.rels`, `docProps/core.xml`. All three are *regenerated* by the writer
1594    ///   from the objects in memory, so bytes put there would be discarded
1595    ///   silently at the exact moment they were supposed to take effect.
1596    /// - **A name OPC or EPC would not accept, or reserves** - see
1597    ///   [`media::check_part_name`], each rule with its section.
1598    /// - **A name that differs from another part's only by case.** OPC compares
1599    ///   part names without case, so the package would hold one part twice.
1600    /// - **A content type that is not a media type** ([`media::check_media_type`]).
1601    ///
1602    /// The content type is the caller's to state. Absent, the writer guesses
1603    /// from the extension, which is what OPC's `Default` entries are for.
1604    ///
1605    /// Which object the file is about is a relationship, not a content type:
1606    /// [`EpcStore::relate_media`].
1607    pub fn set_attachment(&mut self, part: &str, bytes: Vec<u8>, content_type: Option<String>) -> Result<()> {
1608        let part = part.trim_start_matches('/').to_string();
1609        if part.is_empty() || part.ends_with('/') {
1610            return Err(Error::Package("an attachment needs a part name".to_string()));
1611        }
1612        if self.entries.iter().any(|e| e.summary.locator == part) {
1613            return Err(Error::Package(format!(
1614                "`{part}` is the part of an object; a save writes it from the object, so bytes \
1615                 placed there would be discarded. Use a different name, or `set_raw_part` to \
1616                 repair the object itself."
1617            )));
1618        }
1619        if part == "[Content_Types].xml"
1620            || part.ends_with(".rels")
1621            || part.eq_ignore_ascii_case("docProps/core.xml")
1622        {
1623            return Err(Error::Package(format!(
1624                "`{part}` is regenerated when the package is written; bytes placed there would \
1625                 be discarded"
1626            )));
1627        }
1628        // Then what OPC and EPC say of any part name and any content type -
1629        // each rule with its section in `media`.
1630        let part = media::check_part_name(&part)?;
1631        let content_type = content_type
1632            .map(|declared| media::check_media_type(&declared))
1633            .transpose()?;
1634        if let Some(other) = self
1635            .package
1636            .entries()
1637            .iter()
1638            .filter(|name| !self.overlay.is_dropped(name))
1639            .cloned()
1640            .chain(self.overlay.raw_parts())
1641            .find(|name| *name != part && name.eq_ignore_ascii_case(&part))
1642        {
1643            return Err(Error::Package(format!(
1644                "`{part}` and `{other}` differ only by case, and OPC compares part names without \
1645                 case: the package would hold one part twice"
1646            )));
1647        }
1648
1649        // A part put back after being dropped is a part that is there again.
1650        self.overlay.restore_part(&part);
1651        match content_type {
1652            Some(declared) => {
1653                self.attachment_types.insert(part.clone(), declared);
1654            }
1655            None => {
1656                self.attachment_types.remove(&part);
1657            }
1658        }
1659        self.overlay.set_raw(&part, bytes);
1660        Ok(())
1661    }
1662
1663    /// Take a file back out of the package.
1664    ///
1665    /// Only an attachment: an object is removed through [`EpcStore::remove`],
1666    /// which knows to take its `.rels` and its index entry with it. Refusing
1667    /// here rather than deleting a part behind the inventory's back is the
1668    /// difference between removing an object and making one unreachable.
1669    pub fn remove_attachment(&mut self, part: &str) -> Result<()> {
1670        let part = part.trim_start_matches('/');
1671        if self.entries.iter().any(|e| e.summary.locator == part) {
1672            return Err(Error::Package(format!(
1673                "`{part}` holds an object; remove the object by its uri, so the package's index \
1674                 and relationships go with it"
1675            )));
1676        }
1677        if !self.package.has(part) && self.overlay.raw(part).is_none() {
1678            return Err(Error::Package(format!("no part `{part}` in the package")));
1679        }
1680        // Both, and in this order: bytes added in this session simply cease to
1681        // exist, while a part that came from the archive has to be *remembered*
1682        // as removed or the save would copy it through again.
1683        self.overlay.clear_raw(part);
1684        self.attachment_types.remove(part);
1685        if self.package.has(part) {
1686            self.overlay.drop_part(part);
1687        }
1688        Ok(())
1689    }
1690
1691    /// Say that a part of the package is a media representation of an object -
1692    /// EPC §3.4.2 (pp. 15-16): `destinationMedia` in the object's `.rels`,
1693    /// `sourceMedia` in the part's, both written on save (see [`media`]).
1694    ///
1695    /// `object` is an ETP uri or a bare uuid. Refused by name: an object the
1696    /// package does not hold, and a part that is not one of its attachments -
1697    /// put the bytes in first ([`EpcStore::set_attachment`]); a relationship to
1698    /// an object's own part, or to nothing, is not a media representation.
1699    pub fn relate_media(&mut self, object: &str, part: &str) -> Result<()> {
1700        let (uuid, part) = self.media_ends(object, part)?;
1701        self.overlay.set_media_link(&part, &uuid, true);
1702        Ok(())
1703    }
1704
1705    /// Take a media relation back out. `true` when there was one.
1706    ///
1707    /// The part stays in the package; removing it is
1708    /// [`EpcStore::remove_attachment`], which takes its relations with it.
1709    pub fn unrelate_media(&mut self, object: &str, part: &str) -> Result<bool> {
1710        let (uuid, part) = self.media_ends(object, part)?;
1711        let was = self
1712            .media_links()
1713            .iter()
1714            .any(|link| link.part == part && link.uuid == uuid);
1715        self.overlay.set_media_link(&part, &uuid, false);
1716        Ok(was)
1717    }
1718
1719    /// Every media relation a save would write: what the archive's `.rels`
1720    /// said - `destinationMedia` from an object, `sourceMedia` from a part,
1721    /// either one is the link - with this session's changes over it.
1722    ///
1723    /// **Read back from the archive, so a rewrite keeps them.** The `.rels`
1724    /// are regenerated on save from what the store knows; before this, a media
1725    /// relation read in a package was lost at the next save (a gap found
1726    /// while building the synthetic reference model, ADR-0018). A part no longer in the package takes its
1727    /// relations with it; an object removed does not - the writer names that
1728    /// relation rather than dropping it in silence.
1729    ///
1730    /// This reads the `.rels` of every object and every attachment: a cost
1731    /// proportional to the package, paid by a save anyway.
1732    pub fn media_links(&self) -> Vec<media::MediaLink> {
1733        let mut links: BTreeSet<(String, String)> = self.archive_media().iter().cloned().collect();
1734        let attachments: HashSet<String> = self.attachments().into_iter().collect();
1735        for ((part, uuid), related) in self.overlay.media_links() {
1736            if related {
1737                links.insert((part, uuid));
1738            } else {
1739                links.remove(&(part, uuid));
1740            }
1741        }
1742        links
1743            .into_iter()
1744            .filter(|(part, _)| attachments.contains(part))
1745            .map(|(part, uuid)| media::MediaLink { part, uuid })
1746            .collect()
1747    }
1748
1749    /// What the archive's `.rels` say about media, read once (`archive_media`).
1750    fn archive_media(&self) -> &Vec<(String, String)> {
1751        self.archive_media.get_or_init(|| {
1752            let mut links: BTreeSet<(String, String)> = BTreeSet::new();
1753            let objects: HashMap<&str, &str> = self
1754                .entries
1755                .iter()
1756                .map(|e| (e.summary.locator.as_str(), e.summary.uuid.as_str()))
1757                .collect();
1758            for entry in &self.entries {
1759                let Ok(relationships) = self.relationships_recorded(&entry.summary.locator) else {
1760                    continue;
1761                };
1762                for relationship in relationships {
1763                    if relationship.external || relationship.short_kind() != media::DESTINATION_MEDIA {
1764                        continue;
1765                    }
1766                    if let Some(part) = media::resolve_target(&entry.summary.locator, &relationship.target) {
1767                        links.insert((part, entry.summary.uuid.to_ascii_lowercase()));
1768                    }
1769                }
1770            }
1771            for part in self.package.entries().iter() {
1772                if is_package_infrastructure(part) || objects.contains_key(part.as_str()) {
1773                    continue;
1774                }
1775                let Ok(relationships) = self.relationships_recorded(part) else {
1776                    continue;
1777                };
1778                for relationship in relationships {
1779                    if relationship.external || relationship.short_kind() != media::SOURCE_MEDIA {
1780                        continue;
1781                    }
1782                    let Some(object_part) = media::resolve_target(part, &relationship.target) else {
1783                        continue;
1784                    };
1785                    if let Some(uuid) = objects.get(object_part.as_str()) {
1786                        links.insert((part.clone(), uuid.to_ascii_lowercase()));
1787                    }
1788                }
1789            }
1790            links.into_iter().collect()
1791        })
1792    }
1793
1794    /// The media parts related to one object, by part name.
1795    pub fn media_of(&self, object: &str) -> Result<Vec<String>> {
1796        self.in_this_dataspace(object)?;
1797        let uuid = uuid_in_uri(object)?.to_ascii_lowercase();
1798        Ok(self
1799            .media_links()
1800            .into_iter()
1801            .filter(|link| link.uuid == uuid)
1802            .map(|link| link.part)
1803            .collect())
1804    }
1805
1806    /// The two ends of a media relation, checked: the object's lowercased uuid
1807    /// and the part's name.
1808    fn media_ends(&self, object: &str, part: &str) -> Result<(String, String)> {
1809        self.in_this_dataspace(object)?;
1810        let uuid = uuid_in_uri(object)?.to_ascii_lowercase();
1811        if self.find_by_uuid(&uuid).is_none() {
1812            return Err(Error::NotFound(object.to_string()));
1813        }
1814        let part = part.trim_start_matches('/').to_string();
1815        if !self.attachments().contains(&part) {
1816            return Err(Error::Package(format!(
1817                "`{part}` is not a file of this package, so it cannot be a media representation of \
1818                 {uuid}; put it in first (`set_attachment`)"
1819            )));
1820        }
1821        Ok((uuid, part))
1822    }
1823
1824    /// Whether the bytes staged for a part would now read as an object, and as
1825    /// what.
1826    ///
1827    /// The check to run between staging and saving: `Ok(qualified type)` means
1828    /// the repair worked, `Err` means it did not and says what is still wrong.
1829    ///
1830    /// Deliberately **not** called by [`EpcStore::set_raw_part`]. Staging bytes
1831    /// that still do not parse is a legitimate step - a repair can take more
1832    /// than one pass, and refusing to hold an intermediate state would make the
1833    /// tool useless for the files that need it most.
1834    pub fn staged_part_parses(&self, part: &str) -> Result<String> {
1835        let (info, _) = self.staged_object(part)?;
1836        Ok(info.qualified_type.unwrap_or(info.name).to_string())
1837    }
1838
1839    /// Read what is staged for a part, as the type it says it is.
1840    ///
1841    /// The one place that decides *which* type a repaired part is, so the check
1842    /// and the staging cannot disagree about it. The content type comes from
1843    /// the type found rather than from `[Content_Types].xml`: a part the index
1844    /// never declared - the invisible case - has none to offer, and the root
1845    /// element is what identified it in the first place.
1846    fn staged_object(&self, part: &str) -> Result<(&'static TypeInfo, Box<dyn DynObject>)> {
1847        let bytes = self.raw_part(part)?;
1848        let peeked = header::peek_reader(BufReader::new(std::io::Cursor::new(&bytes)));
1849        if peeked.root_name.is_empty() {
1850            return Err(Error::Package(format!("{part}: no root element")));
1851        }
1852        let declared = self.package.content_type(part).map(str::to_string);
1853        let info = declared
1854            .as_deref()
1855            .and_then(|ct| self.registry.by_content_type(ct).map(|e| e.info))
1856            .or_else(|| self.type_of_root(&peeked.root_name, peeked.schema_version.as_deref()))
1857            .ok_or_else(|| {
1858                Error::Package(format!(
1859                    "{part}: root element `{}` names no type this build knows",
1860                    peeked.root_name
1861                ))
1862            })?;
1863        let content_type = info
1864            .content_type
1865            .map(str::to_string)
1866            .or(declared)
1867            .ok_or_else(|| Error::Package(format!("{part}: no content type to read it as")))?;
1868        let object = self
1869            .registry
1870            .read_xml_as(&content_type, &bytes, self.mode())
1871            .map(|(object, _)| object)
1872            .map_err(|e| Error::Package(format!("{part}: {e}")))?;
1873        Ok((info, object))
1874    }
1875
1876    /// Stage repaired bytes, and put the part back among the objects.
1877    ///
1878    /// The difference from [`EpcStore::set_raw_part`] is what happens on
1879    /// success, and it is the whole reason this exists: bytes that **parse**
1880    /// are not staged as bytes at all. They become an object in the ordinary
1881    /// layer, so the writer serialises it like any other and - the part that
1882    /// matters - **rebuilds its `.rels` from its own `DataObjectReference`s**.
1883    /// A part repaired through `set_raw_part` alone comes out of the save with
1884    /// no relationships, because verbatim bytes have no references a writer can
1885    /// see.
1886    ///
1887    /// So: repair through this, and fall back to [`EpcStore::set_raw_part`]
1888    /// only for bytes that still do not parse and have to be written anyway.
1889    ///
1890    /// Returns the qualified type it now reads as. On failure **nothing is
1891    /// staged**: the store is left exactly as it was.
1892    pub fn stage_repaired_part(&mut self, part: &str, bytes: Vec<u8>) -> Result<String> {
1893        let previous = self.overlay.raw(part).map(<[u8]>::to_vec);
1894        self.overlay.set_raw(part, bytes);
1895        let (info, object) = match self.staged_object(part) {
1896            Ok(found) => found,
1897            Err(e) => {
1898                match previous {
1899                    Some(bytes) => self.overlay.set_raw(part, bytes),
1900                    None => {
1901                        self.overlay.clear_raw(part);
1902                    }
1903                }
1904                return Err(e);
1905            }
1906        };
1907        // It parses, so it rejoins the normal world: the object layer, where
1908        // the writer can see its references and rebuild its `.rels` - and where
1909        // it is placed under the part name its type and uuid dictate, which for
1910        // a part the inventory had lost is not the name it arrived under.
1911        self.overlay.clear_raw(part);
1912        self.overlay.supersede(part);
1913        self.put(object)?;
1914        Ok(info.qualified_type.unwrap_or(info.name).to_string())
1915    }
1916
1917    /// Every part this build cannot turn into an object.
1918    ///
1919    /// The worklist of a repair. Two kinds are in it, and the second is the one
1920    /// that used to be invisible:
1921    ///
1922    /// - a part the inventory knows, whose XML fails to parse - it has a uri;
1923    /// - a part the inventory **dropped**, because the damage reached the root
1924    ///   element or the uuid and nothing could address it. It has no uri, only
1925    ///   a name, which is why [`EpcStore::raw_part`] takes one.
1926    ///
1927    /// Attachments that were never meant to be objects - a PDF, an image - are
1928    /// not in it. The test is the extension and the declared content type: an
1929    /// `.xml` part of an EPC that is not package infrastructure was meant to be
1930    /// an object, and if it is not one, something is wrong with it.
1931    pub fn unreadable(&self) -> Vec<Unreadable> {
1932        let mut out = Vec::new();
1933        for entry in &self.entries {
1934            if matches!(self.overlay.get(&entry.slot), Some(Change::Removed)) {
1935                continue;
1936            }
1937            let part = entry.summary.locator.clone();
1938            if let Err(e) = self.get_object(&entry.summary.uri) {
1939                out.push(Unreadable {
1940                    bytes: self.raw_part(&part).map(|b| b.len()).unwrap_or(0),
1941                    part,
1942                    uri: Some(entry.summary.uri.clone()),
1943                    content_type: Some(entry.summary.content_type.clone()),
1944                    reason: e.to_string(),
1945                });
1946            }
1947        }
1948        for part in self.attachments() {
1949            if !part.to_ascii_lowercase().ends_with(".xml") {
1950                continue;
1951            }
1952            let declared = self.package.content_type(&part).map(str::to_string);
1953            // An attachment declared as something other than EnergyML is an
1954            // attachment, not a casualty.
1955            if let Some(content_type) = &declared
1956                && identity::parse_content_type(content_type).is_err()
1957            {
1958                continue;
1959            }
1960            let reason = self
1961                .diagnostics()
1962                .into_iter()
1963                .find(|d| d.part == part)
1964                .map(|d| d.message)
1965                .unwrap_or_else(|| "the inventory could not address it".to_string());
1966            out.push(Unreadable {
1967                bytes: self.raw_part(&part).map(|b| b.len()).unwrap_or(0),
1968                part,
1969                uri: None,
1970                content_type: declared,
1971                reason,
1972            });
1973        }
1974        out.sort_by(|a, b| a.part.cmp(&b.part));
1975        out
1976    }
1977
1978    /// The types this store can produce: whatever the registry it was given
1979    /// knows about.
1980    pub fn supported_types(&self) -> Vec<&'static TypeInfo> {
1981        self.registry.top_level_types().collect()
1982    }
1983
1984    // -- External data ----------------------------------------------------
1985
1986    /// Every array the object points at, with proxies resolved through this
1987    /// package's relationships.
1988    ///
1989    /// Both spellings are handled: the eml 2.3 `ExternalDataArrayPart`, which
1990    /// carries the file name itself, and the older `Hdf5Dataset`, whose file
1991    /// name is supposed to live in the `.rels` of its
1992    /// `EpcExternalPartReference`. "Supposed to" is doing work in that sentence -
1993    /// see [`EpcStore::external_file_for`].
1994    pub fn arrays_of(&self, object: &dyn Reflect) -> Vec<ArrayLocation> {
1995        // The part this object sits in, whose `.rels` may name its file. Of two
1996        // entries sharing a uuid, the one that is this object - its type and
1997        // version - rather than the first.
1998        let stated = identity::key_of_reflect(object).map(|key| {
1999            (
2000                key.uuid.to_ascii_lowercase(),
2001                key.qualified_type(),
2002                normal_version(key.object_version),
2003            )
2004        });
2005        let source = stated.and_then(|(uuid, qualified_type, version)| {
2006            let list = self.by_uuid.get(&uuid)?;
2007            let own = list.iter().map(|&at| &self.entries[at]).find(|entry| {
2008                entry.summary.qualified_type == qualified_type && entry.object_version == version
2009            });
2010            own.or_else(|| list.first().map(|&at| &self.entries[at]))
2011                .map(|entry| entry.summary.locator.clone())
2012        });
2013        let resolver = |proxy_uuid: &str| self.external_target_for(proxy_uuid, source.as_deref());
2014        crate::array::locate_with(object, &resolver)
2015    }
2016
2017    /// Shape and element type of an array, without reading it - ETP protocol 9,
2018    /// `GetDataArrayMetadata`.
2019    ///
2020    /// What a viewer asks before deciding whether it can afford the data.
2021    pub fn array_metadata(&self, location: &ArrayLocation) -> Result<ArrayMetadata> {
2022        self.attempt(location, |backend, file| {
2023            backend.metadata(file, &location.path_in_file)
2024        })
2025    }
2026
2027    /// Whether **the file the location names** holds its dataset - that file
2028    /// alone.
2029    ///
2030    /// Not [`EpcStore::array_metadata`], whose search goes on to a companion
2031    /// or a neighbour when the named file says no, records the departure and
2032    /// remembers the file it settled on. That is right for reading and wrong
2033    /// for asking *is this path free in this file* before writing there
2034    /// (`import`): the answer would come from another file, and the question
2035    /// would leave a diagnostic and a resolution behind in the package's own
2036    /// health report. Here nothing is searched, recorded or remembered, and
2037    /// every failure - no such file, no backend - is `false`.
2038    pub fn holds_array(&self, location: &ArrayLocation) -> bool {
2039        self.array_backend(location)
2040            .is_ok_and(|(backend, file)| backend.metadata(&file, &location.path_in_file).is_ok())
2041    }
2042
2043    /// Which end of a multi-byte number the file writes first.
2044    ///
2045    /// Asked apart from [`EpcStore::array_metadata`] because it answers a
2046    /// different need: a viewer wants a shape and a type, and a **writer** -
2047    /// one declaring an EML 2.3 external array, whose storage type carries the
2048    /// order - wants this. `None` when the backend cannot say or the width
2049    /// makes the question meaningless; never a guess, because writing the usual
2050    /// order where the file holds the other makes every value read wrong while
2051    /// the document stays valid.
2052    pub fn array_byte_order(&self, location: &ArrayLocation) -> Result<Option<ByteOrder>> {
2053        self.attempt(location, |backend, file| {
2054            backend.byte_order(file, &location.path_in_file)
2055        })
2056    }
2057
2058    /// An array, or the slice the location names - `GetDataArrays` and
2059    /// `GetDataSubarrays`.
2060    ///
2061    /// Fails with [`ExternalDataReason::FeatureNotEnabled`] when this build has
2062    /// no backend for the format: a **missing capability**, which the caller
2063    /// must be able to tell apart from missing data.
2064    pub fn read_array(&self, location: &ArrayLocation) -> Result<ArrayData> {
2065        self.attempt(location, |backend, file| {
2066            backend.read(file, &location.path_in_file, &location.start, &location.count)
2067        })
2068    }
2069
2070    /// Read an array **into memory the caller already owns**.
2071    ///
2072    /// The path a 3D application takes: allocate the vertex buffer once, ask
2073    /// for the shape with [`EpcStore::array_metadata`], and have the file
2074    /// decoded straight into it. Nothing is allocated here and nothing is
2075    /// copied afterwards.
2076    pub fn read_array_into(
2077        &self,
2078        location: &ArrayLocation,
2079        out: &mut crate::array::ArrayBuffer<'_>,
2080    ) -> Result<ArrayMetadata> {
2081        // Not through [`EpcStore::attempt`], and the difference matters: the
2082        // buffer belongs to the caller, so a first attempt that failed halfway
2083        // may already have written into it, and retrying would lay a second
2084        // read over a partial one. The file is settled **before** anything is
2085        // written - one metadata call, remembered, next to a read of a million
2086        // points.
2087        let (backend, file) = match self.array_backend(location) {
2088            Ok((backend, file)) if backend.metadata(&file, &location.path_in_file).is_ok() => (backend, file),
2089            _ => self.search_array_file(location)?,
2090        };
2091        backend.read_into(
2092            &file,
2093            &location.path_in_file,
2094            &location.start,
2095            &location.count,
2096            out,
2097        )
2098    }
2099
2100    /// Check that an external file is the one the package means.
2101    ///
2102    /// EPC §3.2.3 requires an HDF5 file to carry a `uuid` attribute at its root
2103    /// equal to that of its `EpcExternalPartReference`. Nothing else catches
2104    /// the mistake this catches: reading somebody else's file because it
2105    /// happened to have the right name.
2106    ///
2107    /// `Ok(())` when they agree **or when the file claims no uuid** - the
2108    /// newer EnergyML form has no proxy, so there is nothing to compare with.
2109    pub fn verify_external_file(&self, location: &ArrayLocation) -> Result<()> {
2110        let Some(expected) = &location.proxy_uuid else {
2111            return Ok(());
2112        };
2113        if !energyml_core::identifiers::is_uuid(expected) {
2114            return Ok(()); // not a uuid at all; already reported when resolving
2115        }
2116        // Through the same two speeds as a read, so that what is verified is
2117        // the file that would actually be read - checking the guess while
2118        // reading the search's answer would be a cross-check of nothing.
2119        let (found, file) = self.attempt(location, |backend, file| {
2120            Ok((backend.file_uuid(file)?, file.to_path_buf()))
2121        })?;
2122        let Some(found) = found else {
2123            return Ok(());
2124        };
2125        if found.eq_ignore_ascii_case(expected) {
2126            return Ok(());
2127        }
2128        Err(Error::ExternalData {
2129            location: file.display().to_string(),
2130            reason: ExternalDataReason::UuidMismatch {
2131                expected: expected.clone(),
2132                found,
2133            },
2134        })
2135    }
2136
2137    /// Write an array to the file an existing pointer names.
2138    ///
2139    /// The counterpart of [`EpcStore::read_array`], and it goes through the
2140    /// same resolution: the pointer says which file and which path inside it,
2141    /// so replacing the values of an array an object already declares needs
2142    /// nothing else.
2143    ///
2144    /// Writing under a *new* pointer is [`EpcStore::write_array_to`], which
2145    /// takes the file name directly - a location that resolves to nothing
2146    /// cannot say where to put anything.
2147    pub fn write_array(&self, location: &ArrayLocation, data: &crate::array::ArrayData) -> Result<()> {
2148        let name = self.resolved_file(location).ok_or_else(|| Error::ExternalData {
2149            location: location.field_path.clone(),
2150            reason: ExternalDataReason::FileMissing,
2151        })?;
2152        self.write_array_to(&name, &location.path_in_file, data)
2153    }
2154
2155    /// Several arrays into one external file, the file opened **once** by a
2156    /// backend that can ([`crate::ArrayBackend::write_many`]). One result per
2157    /// item, in order; the shape of each is checked as
2158    /// [`EpcStore::write_array_to`] checks it.
2159    pub fn write_arrays_to(
2160        &self,
2161        file_name: &str,
2162        items: &[(&str, &crate::array::ArrayData)],
2163    ) -> Vec<Result<()>> {
2164        // One span per file, with the number of datasets - never one per array.
2165        let _span =
2166            tracing::info_span!("epc.write_arrays", file = file_name, datasets = items.len()).entered();
2167        let checked: Vec<Result<()>> = items
2168            .iter()
2169            .map(|(path_in_file, data)| {
2170                let promised: i64 = data.metadata.dimensions.iter().product::<i64>().max(0);
2171                if promised as usize != data.values.len() {
2172                    return Err(Error::ExternalData {
2173                        location: format!("{file_name}:{path_in_file}"),
2174                        reason: ExternalDataReason::ShapeDisagrees {
2175                            promised,
2176                            held: data.values.len(),
2177                        },
2178                    });
2179                }
2180                Ok(())
2181            })
2182            .collect();
2183        let refuse_all = |why: String| -> Vec<Result<()>> {
2184            items
2185                .iter()
2186                .map(|(path_in_file, _)| {
2187                    Err(Error::ExternalData {
2188                        location: format!("{file_name}:{path_in_file}"),
2189                        reason: ExternalDataReason::Backend(why.clone()),
2190                    })
2191                })
2192                .collect()
2193        };
2194        let probe = ArrayLocation {
2195            file: file_name.to_string(),
2196            mime: None,
2197            ..Default::default()
2198        };
2199        let backend = match self.backends.find(&probe, std::path::Path::new(file_name)) {
2200            Ok(backend) => backend,
2201            Err(why) => return refuse_all(why.to_string()),
2202        };
2203        if !backend.writes() {
2204            return refuse_all(format!(
2205                "the `{}` backend reads but does not write",
2206                backend.name()
2207            ));
2208        }
2209        let path = match self.external.destination_for(file_name) {
2210            Ok(path) => path,
2211            Err(why) => return refuse_all(why.to_string()),
2212        };
2213        // Only the items whose shape agrees go to the backend; the others keep
2214        // their refusal, in place.
2215        let good: Vec<(&str, &crate::array::ArrayData)> = items
2216            .iter()
2217            .zip(&checked)
2218            .filter(|(_, ok)| ok.is_ok())
2219            .map(|(item, _)| *item)
2220            .collect();
2221        // Journaled whatever the backend answers: a write that failed half way
2222        // may have changed some of the datasets, and a journal that says less
2223        // than the file holds leaves a cache serving numbers that are gone.
2224        self.overlay
2225            .journal()
2226            .note_datasets(file_name, good.iter().map(|(path_in_file, _)| *path_in_file));
2227        let mut written = backend.write_many(&path, &good).into_iter();
2228        checked
2229            .into_iter()
2230            .map(|ok| {
2231                if ok.is_ok() {
2232                    written.next().unwrap_or(Ok(()))
2233                } else {
2234                    ok
2235                }
2236            })
2237            .collect()
2238    }
2239
2240    /// Write an array to a named external file, creating it if needed.
2241    ///
2242    /// The file is placed by [`ExternalFiles::destination_for`]: an override if
2243    /// one is pinned, else beside the package. Nothing is created until the
2244    /// backend is found, so a build without HDF5 leaves no empty file behind.
2245    ///
2246    /// This writes the *numbers*. The EnergyML object that points at them, and
2247    /// the `.rels` entry naming the file, are
2248    /// [`EpcStore::register_external_file`] and a `put` of the object - see
2249    /// that method for why the two halves are separate.
2250    ///
2251    /// [`ExternalFiles::destination_for`]: crate::backend::ExternalFiles::destination_for
2252    pub fn write_array_to(
2253        &self,
2254        file_name: &str,
2255        path_in_file: &str,
2256        data: &crate::array::ArrayData,
2257    ) -> Result<()> {
2258        // **The shape and the values must agree**, and it is checked here
2259        // rather than left to each backend: a shape promising more numbers than
2260        // there are writes without error in some formats and reads back with
2261        // zeros at the end. Every writer gets this - the command line, the
2262        // build layer, the Python bridge - because it is the file that would be
2263        // wrong, not one caller.
2264        let promised: i64 = data.metadata.dimensions.iter().product::<i64>().max(0);
2265        if promised as usize != data.values.len() {
2266            return Err(Error::ExternalData {
2267                location: format!("{file_name}:{path_in_file}"),
2268                reason: ExternalDataReason::ShapeDisagrees {
2269                    promised,
2270                    held: data.values.len(),
2271                },
2272            });
2273        }
2274        let probe = ArrayLocation {
2275            file: file_name.to_string(),
2276            path_in_file: path_in_file.to_string(),
2277            mime: None,
2278            ..Default::default()
2279        };
2280        let backend = self.backends.find(&probe, std::path::Path::new(file_name))?;
2281        if !backend.writes() {
2282            return Err(Error::ExternalData {
2283                location: format!("{file_name}:{path_in_file}"),
2284                reason: ExternalDataReason::Backend(format!(
2285                    "the `{}` backend reads but does not write",
2286                    backend.name()
2287                )),
2288            });
2289        }
2290        let path = self.external.destination_for(file_name)?;
2291        // Journaled whatever the backend answers, for the reason given in
2292        // `write_arrays_to`.
2293        self.overlay
2294            .journal()
2295            .note_datasets(file_name, std::iter::once(path_in_file));
2296        backend.write(&path, path_in_file, data)
2297    }
2298
2299    /// Stamp an external file with the uuid of the proxy that owns it.
2300    ///
2301    /// EPC §3.2.3 requires it, and [`EpcStore::verify_external_file`] enforces
2302    /// it on the way back in - so a file written without this is one this very
2303    /// crate refuses to trust.
2304    pub fn stamp_external_file(&self, file_name: &str, proxy_uuid: &str) -> Result<()> {
2305        let probe = ArrayLocation {
2306            file: file_name.to_string(),
2307            ..Default::default()
2308        };
2309        let backend = self.backends.find(&probe, std::path::Path::new(file_name))?;
2310        let path = self.external.destination_for(file_name)?;
2311        backend.set_file_uuid(&path, proxy_uuid)
2312    }
2313
2314    /// What will read the array, and from where.
2315    ///
2316    /// The backend is chosen **before** the file is looked for, and the order
2317    /// matters: a build with no HDF5 backend cannot read the array whether or
2318    /// not the file is on the disk, and "no support for this format" is the
2319    /// more useful of the two answers. Once a backend exists, a file that is
2320    /// not there is a file that is not there.
2321    /// Which file to try **first** for this array.
2322    ///
2323    /// Optimistic on purpose: whatever the search settled on last time, or else
2324    /// the first candidate, handed back without opening anything. In the normal
2325    /// case - the document names a file that exists and holds the dataset -
2326    /// this is the whole resolution and it costs nothing.
2327    ///
2328    /// When the guess is wrong the read fails, and [`EpcStore::attempt`] runs
2329    /// [`EpcStore::search_array_file`], which opens candidates and asks each
2330    /// whether it holds the dataset. Probing first would be simpler to read and
2331    /// would double the file opens of every correct package to pay for the
2332    /// broken ones.
2333    fn array_backend(
2334        &self,
2335        location: &ArrayLocation,
2336    ) -> Result<(&dyn crate::backend::ArrayBackend, std::path::PathBuf)> {
2337        let declared = self.resolved_file(location).unwrap_or_default();
2338        if let Some(found) = self.remembered(&declared, &location.path_in_file) {
2339            let backend = self.backends.find(location, &found)?;
2340            return Ok((backend, found));
2341        }
2342        // Only among the places someone **stated**: what the caller pinned,
2343        // what the caller offered, what the document wrote. A companion or a
2344        // neighbour is a *recovery*, and handing one back unopened would be the
2345        // silent-wrong-file mistake this whole design is arranged against (ADR-0005) -
2346        // those go through the search, which asks before it answers.
2347        let candidates = self.external.candidates(&declared, self.path.as_deref());
2348        // Kept so that "nothing can read this format" does not come out as
2349        // "the file is not there". A build with no HDF5 cannot read the array
2350        // whether or not the file is on the disk, and sending someone to look
2351        // for a file that is sitting right beside the package costs an hour.
2352        let mut no_backend: Option<Error> = None;
2353        for candidate in &candidates {
2354            if candidate.origin > Origin::Declared {
2355                break;
2356            }
2357            match self.backends.find(location, &candidate.path) {
2358                Ok(backend) => return Ok((backend, candidate.path.clone())),
2359                Err(why) => no_backend.get_or_insert(why),
2360            };
2361        }
2362        Err(missing_or_unsupported(location, &self.backends, no_backend, None))
2363    }
2364
2365    /// Which file actually holds this array - by opening candidates and asking.
2366    ///
2367    /// **The content decides, not the name.** The document's name is tried
2368    /// first because it is usually right, but a candidate that exists and does
2369    /// not hold the dataset steps aside for the next one rather than failing
2370    /// the read - see [`ExternalFiles::candidates`] for the order and
2371    /// [`Origin`] for why each place is in it.
2372    ///
2373    /// That is what makes a renamed pair readable. Someone who renames
2374    /// `model.epc` and `model.h5` to `model_final.*` has not lost a number;
2375    /// they have broken a string, and every array they own is still sitting
2376    /// next to the package. Refusing them over the string is refusing to read
2377    /// data that is present.
2378    ///
2379    /// Every departure from what the document named is recorded. Being
2380    /// permissive is a trade - the name is also what stops a package reading
2381    /// somebody else's file - and it is paid for by saying so, once per array
2382    /// pointer, with what was asked for and what was used instead.
2383    ///
2384    /// [`ExternalFiles::candidates`]: crate::backend::ExternalFiles::candidates
2385    /// [`Origin`]: crate::backend::Origin
2386    fn search_array_file(
2387        &self,
2388        location: &ArrayLocation,
2389    ) -> Result<(&dyn crate::backend::ArrayBackend, std::path::PathBuf)> {
2390        let (backend, resolution) = self.search_array(location)?;
2391        Ok((backend, resolution.path))
2392    }
2393
2394    /// The same search, keeping **why** the answer was chosen.
2395    ///
2396    /// What [`EpcStore::resolve_array`] hands a caller that is about to tell a
2397    /// person where their numbers came from, or to write the answer back into
2398    /// the package.
2399    fn search_array(
2400        &self,
2401        location: &ArrayLocation,
2402    ) -> Result<(&dyn crate::backend::ArrayBackend, Resolution)> {
2403        let declared = self.resolved_file(location).unwrap_or_default();
2404        let dataset = location.path_in_file.clone();
2405        let candidates = self.external.candidates(&declared, self.path.as_deref());
2406
2407        let mut opened = 0usize;
2408        let mut no_backend: Option<Error> = None;
2409        for candidate in &candidates {
2410            // A file no backend reads is not a candidate at all: no format, no
2411            // question to ask it. The reason is kept: if *no* candidate ever
2412            // produced a backend, the answer is "nothing here reads this
2413            // format", not "the file is missing".
2414            let backend = match self.backends.find(location, &candidate.path) {
2415                Ok(backend) => backend,
2416                Err(why) => {
2417                    no_backend.get_or_insert(why);
2418                    continue;
2419                }
2420            };
2421            // The question. An empty dataset path asks nothing, and existence
2422            // is a test the candidate list has already applied.
2423            if !dataset.is_empty() {
2424                opened += 1;
2425                if backend.metadata(&candidate.path, &dataset).is_err() {
2426                    continue;
2427                }
2428            }
2429
2430            if candidate.origin.is_a_departure() {
2431                self.record(Diagnostic::warning(
2432                    &location.field_path,
2433                    match declared.is_empty() {
2434                        true => format!(
2435                            "no part, relationship or declaration names the file holding this \
2436                             array; found `{}` - {} - which holds `{dataset}`",
2437                            candidate.path.display(),
2438                            candidate.origin.describe()
2439                        ),
2440                        false => format!(
2441                            "the array names `{declared}`, which does not hold `{dataset}`; \
2442                             read from `{}` instead - {}",
2443                            candidate.path.display(),
2444                            candidate.origin.describe()
2445                        ),
2446                    },
2447                ));
2448            }
2449            self.remember(&declared, &dataset, &candidate.path);
2450            return Ok((
2451                backend,
2452                Resolution {
2453                    declared: declared.clone(),
2454                    path: candidate.path.clone(),
2455                    origin: candidate.origin,
2456                },
2457            ));
2458        }
2459
2460        // Nothing held it. The message says how hard it looked, because "file
2461        // missing" over a folder full of files is the least useful thing this
2462        // could say.
2463        //
2464        // **Kept, not just logged.** This used to go only into the diagnostic
2465        // log, a separate channel a caller may never read; the error itself
2466        // carried nothing past "file not found", which is what a viewer showed
2467        // next to an object whose file was open on disk under a name the
2468        // package's own relationships did not use. The detail now rides on the
2469        // error's `location` too, the way the wasm backend already keeps what
2470        // the browser said. The
2471        // *reason* stays [`ExternalDataReason::FileMissing`], because that is
2472        // what tells the caller to try another candidate or ask for the file;
2473        // only the display string carries the how-hard-it-looked detail.
2474        let detail = match (opened, &no_backend) {
2475            // Zero opens *and* no backend anywhere: the search never asked a
2476            // question, it had nothing to ask it with.
2477            (0, Some(_)) => format!(
2478                "`{dataset}` was not looked for: no backend in this build reads {}",
2479                location.mime.as_deref().unwrap_or("this format")
2480            ),
2481            (0, None) => format!(
2482                "`{dataset}` is in none of the {} candidate file(s) for `{declared}`",
2483                candidates.len()
2484            ),
2485            (n, _) => format!(
2486                "`{dataset}` is in none of the {} candidate file(s) for `{declared}`; \
2487                 {n} of them were opened and did not hold it",
2488                candidates.len()
2489            ),
2490        };
2491        self.record(Diagnostic::warning(&location.field_path, detail.clone()));
2492        Err(missing_or_unsupported(
2493            location,
2494            &self.backends,
2495            no_backend,
2496            Some(&detail),
2497        ))
2498    }
2499
2500    /// Where an array's numbers actually are, and why there.
2501    ///
2502    /// The answer a person needs when the package says one thing and the disk
2503    /// says another: not "file missing", but *this file, for this reason*. What
2504    /// `energyml h5` prints, and what it writes back with `--fix`.
2505    pub fn resolve_array(&self, location: &ArrayLocation) -> Result<Resolution> {
2506        self.search_array(location).map(|(_, resolution)| resolution)
2507    }
2508
2509    /// Do something with an array's file, and if that fails, **look harder**.
2510    ///
2511    /// The two-speed shape the whole resolution rests on: the guess costs
2512    /// nothing and is right almost always; the search costs file opens and only
2513    /// runs when the guess was wrong. A package whose names are correct never
2514    /// pays for the permissiveness that rescues one whose names are not.
2515    ///
2516    /// The retry runs only when the search lands somewhere **different**, so a
2517    /// genuine read error - a corrupt dataset, a shape that does not fit - is
2518    /// reported once and not attempted twice.
2519    fn attempt<T>(
2520        &self,
2521        location: &ArrayLocation,
2522        act: impl Fn(&dyn crate::backend::ArrayBackend, &std::path::Path) -> Result<T>,
2523    ) -> Result<T> {
2524        let first = self.array_backend(location);
2525        if let Ok((backend, path)) = &first
2526            && let Ok(value) = act(*backend, path)
2527        {
2528            return Ok(value);
2529        }
2530        let (backend, path) = self.search_array_file(location)?;
2531        if let Ok((_, guessed)) = &first
2532            && *guessed == path
2533        {
2534            // The search agreed with the guess, so the failure was the read's
2535            // and not the file's. Report that one rather than a second attempt
2536            // at the same bytes.
2537            return act(backend, &path);
2538        }
2539        act(backend, &path)
2540    }
2541
2542    /// What the search settled on for this pair last time.
2543    fn remembered(&self, declared: &str, dataset: &str) -> Option<std::path::PathBuf> {
2544        self.array_files
2545            .lock()
2546            .expect("the resolved-file lock is never held across a panic")
2547            .get(&(declared.to_string(), dataset.to_string()))
2548            .cloned()
2549    }
2550
2551    fn remember(&self, declared: &str, dataset: &str, path: &std::path::Path) {
2552        self.array_files
2553            .lock()
2554            .expect("the resolved-file lock is never held across a panic")
2555            .insert((declared.to_string(), dataset.to_string()), path.to_path_buf());
2556    }
2557
2558    /// The external file the package's own name suggests: `model.epc` →
2559    /// `model.h5`, when that file exists.
2560    ///
2561    /// The fallback of last resort for packages whose pointers are broken -
2562    /// proxies with no file name (SKUA-GOCAD exports), URIs pointing at objects
2563    /// instead of files. It is never preferred to anything the package says,
2564    /// and using it is always recorded.
2565    fn companion_name(&self) -> Option<String> {
2566        let path = self.path.as_deref()?;
2567        let companion = path.with_extension("h5");
2568        if !companion.exists() {
2569            return None;
2570        }
2571        Some(companion.file_name()?.to_string_lossy().into_owned())
2572    }
2573
2574    /// How names in array pointers become paths: the folder to look in, and
2575    /// the files the caller pins by hand.
2576    ///
2577    /// A cloud service has neither the right names nor a disk, and a package
2578    /// moved after export names files that are no longer there.
2579    pub fn external_files_mut(&mut self) -> &mut crate::backend::ExternalFiles {
2580        &mut self.external
2581    }
2582
2583    /// The same settings, read-only - where a named file would be found, and
2584    /// whether this store has a disk at all.
2585    ///
2586    /// Not `external_files`, which answers a different question (the names the
2587    /// package declares) and was here first.
2588    pub fn external_files_ref(&self) -> &crate::backend::ExternalFiles {
2589        &self.external
2590    }
2591
2592    /// Add a backend, tried before the ones compiled in.
2593    ///
2594    /// The extension point: a WebAssembly application plugs h5wasm in here, and
2595    /// CSV or Parquet will arrive the same way.
2596    pub fn add_array_backend(&mut self, backend: Box<dyn crate::backend::ArrayBackend>) {
2597        self.backends.push(backend);
2598    }
2599
2600    /// The file an [`EpcExternalPartReference`] stands for, from its `.rels`.
2601    ///
2602    /// The specification is explicit (EPC §3.2.3): the file name and its MIME
2603    /// type are read **in the relationship**, never in the object - an
2604    /// `EpcExternalPartReference` inside an EPC must carry null `Filename` and
2605    /// `MimeType` attributes.
2606    ///
2607    /// [`EpcExternalPartReference`]: https://schemas.energistics.org
2608    pub fn external_file_of(&self, proxy_uuid: &str) -> Option<String> {
2609        // **What this session declared wins**, the same rule
2610        // [`EpcStore::external_file_map`] applies on the way out. Without it a
2611        // rename would be honoured by the writer and ignored by the reader:
2612        // the saved package would name the new file while every array read in
2613        // this session went on resolving to the old one. Two readers of one
2614        // fact, disagreeing about what it means.
2615        if let Some(declared) = self.declared_files.get(&proxy_uuid.to_ascii_lowercase()) {
2616            return Some(declared.clone());
2617        }
2618        let index = *self.by_uuid.get(&proxy_uuid.to_ascii_lowercase())?.first()?;
2619        self.external_resource_in(&self.entries[index].summary.locator)
2620    }
2621
2622    /// The file an array pointer means, trying every place it is written.
2623    ///
2624    /// The specification gives one answer; the corpus gives four. Observed in
2625    /// `testingPackageCpp22.epc`: two RESQML 2.0.1 objects survive inside an
2626    /// otherwise 2.2 package, and the DOR that should name their
2627    /// `EpcExternalPartReference` carries **the HDF5 file name in its `UUID`
2628    /// element** - `<eml:UUID>testingPackageCpp22.h5</eml:UUID>`. No part has
2629    /// that uuid, because it is not a uuid. A strict reader loses those two
2630    /// arrays; this one finds them and says how.
2631    ///
2632    /// In order:
2633    ///
2634    /// 1. the `externalResource` of the proxy's own `.rels` - what EPC §3.2.3
2635    ///    prescribes, and what works for every conformant object;
2636    /// 2. the "uuid" taken **literally as a file name**, when it is not a uuid
2637    ///    at all and either the package declares an external file by that exact
2638    ///    name or one sits beside the package on disk. Narrow on purpose: it
2639    ///    fires only when the writer's mistake is unambiguous;
2640    /// 3. the `externalResource` of the **referencing object's** `.rels`;
2641    /// 4. the only external file the package declares, if there is exactly one.
2642    ///    A guess, and stated as one: an EPC that names two HDF5 files gets no
2643    ///    answer here rather than a coin toss;
2644    /// 5. when the package declares nothing at all, the **companion file** -
2645    ///    `model.h5` beside `model.epc` - if it exists. The SKUA-GOCAD exports
2646    ///    of the corpus need this: their proxies carry no file name anywhere.
2647    ///
2648    /// Steps 2 to 5 each record a diagnostic. None is silent.
2649    pub fn external_file_for(&self, proxy_uuid: &str, source_part: Option<&str>) -> Option<String> {
2650        if let Some(file) = self.external_file_of(proxy_uuid) {
2651            return Some(file);
2652        }
2653
2654        if !identifiers::is_uuid(proxy_uuid) {
2655            // The fesapi trick: the file name written where a uuid belongs.
2656            // Two ways to be sure enough to act on it - the package declares an
2657            // external file by that exact name, or (a package with no `.rels`
2658            // at all, which happens) a file by that name sits beside the
2659            // package on disk.
2660            let declared = self.external_files().iter().any(|file| file == proxy_uuid);
2661            if declared || self.external.resolve(proxy_uuid).is_ok() {
2662                let evidence = if declared {
2663                    "it is the name of an external file this package declares"
2664                } else {
2665                    "a file by that name sits beside the package"
2666                };
2667                self.record(Diagnostic::warning(
2668                    source_part.unwrap_or_default(),
2669                    format!(
2670                        "the HDF5 proxy reference carries `{proxy_uuid}` where a uuid belongs; \
2671                         {evidence}, and it was used as such"
2672                    ),
2673                ));
2674                return Some(proxy_uuid.to_string());
2675            }
2676        }
2677
2678        if let Some(part) = source_part
2679            && let Some(file) = self.external_resource_in(part)
2680        {
2681            self.record(Diagnostic::warning(
2682                part,
2683                format!(
2684                    "the proxy {proxy_uuid} is not a part of this package; \
2685                     the file name was taken from the object's own relationships (`{file}`)"
2686                ),
2687            ));
2688            return Some(file);
2689        }
2690
2691        match self.external_files() {
2692            [only] => {
2693                self.record(Diagnostic::warning(
2694                    source_part.unwrap_or_default(),
2695                    format!(
2696                        "the proxy {proxy_uuid} is not a part of this package and no relationship \
2697                         names its file; fell back to `{only}`, the only external file the package declares"
2698                    ),
2699                ));
2700                Some(only.clone())
2701            }
2702            [] => {
2703                // Nothing declared anywhere - the SKUA-GOCAD export shape:
2704                // proxies whose `.rels` are absent or empty. The package's own
2705                // name is the last thing left to try.
2706                let companion = self.companion_name()?;
2707                self.record(Diagnostic::warning(
2708                    source_part.unwrap_or_default(),
2709                    format!(
2710                        "the proxy {proxy_uuid} names no file and the package declares none; \
2711                         fell back to `{companion}`, the file sitting beside the package with its name"
2712                    ),
2713                ));
2714                Some(companion)
2715            }
2716            _ => None,
2717        }
2718    }
2719
2720    /// The `externalResource` target recorded for one part.
2721    ///
2722    /// Answered from `external_resources` after the first time, which is where
2723    /// the argument for that map lives - a proxy's `.rels` is read once per
2724    /// package rather than once per array pointer.
2725    fn external_resource_in(&self, part: &str) -> Option<String> {
2726        if let Ok(memo) = self.external_resources.lock()
2727            && let Some(found) = memo.get(part)
2728        {
2729            return found.clone();
2730        }
2731        let found = self
2732            .relationships_recorded(part)
2733            .ok()
2734            .and_then(|relationships| {
2735                relationships
2736                    .into_iter()
2737                    .find(|r| r.is_external_resource())
2738                    .map(|r| r.target)
2739            })
2740            .filter(|target| !target.is_empty());
2741        if let Ok(mut memo) = self.external_resources.lock() {
2742            memo.insert(part.to_string(), found.clone());
2743        }
2744        found
2745    }
2746
2747    /// The contained objects of the archive, built on first use.
2748    ///
2749    /// Reads **only** the parts whose type is a container
2750    /// ([`energyml_core::registry::Registry::is_container`]), each once; the
2751    /// departures it notices are recorded with the others in
2752    /// [`EpcStore::diagnostics`].
2753    pub(crate) fn contained(&self) -> &ContainedIndex {
2754        self.contained.get_or_init(|| {
2755            let (index, diagnostics) = ContainedIndex::build(
2756                self.entries.iter().map(|entry| &entry.summary),
2757                &self.registry,
2758                // `by_uuid` is keyed on the lowercased uuid of the archive's parts.
2759                &|uuid| self.by_uuid.contains_key(uuid),
2760                &mut |part| self.package.read(part),
2761            );
2762            for diagnostic in diagnostics {
2763                self.record(diagnostic);
2764            }
2765            index
2766        })
2767    }
2768
2769    /// Every distinct external file the package declares, across all its
2770    /// `.rels`. Computed once, the first time something needs it.
2771    pub fn external_files(&self) -> &[String] {
2772        self.external_files.get_or_init(|| {
2773            let mut files: Vec<String> = Vec::new();
2774            for part in self.package.entries() {
2775                if !part.ends_with(".rels") {
2776                    continue;
2777                }
2778                let Ok(bytes) = self.package.read(part) else {
2779                    continue;
2780                };
2781                let (relationships, problems) = package::parse_relationships_reporting(&bytes, part);
2782                for problem in problems {
2783                    self.record(problem);
2784                }
2785                for relationship in relationships {
2786                    if relationship.is_external_resource()
2787                        && !relationship.target.is_empty()
2788                        && !files.contains(&relationship.target)
2789                    {
2790                        files.push(relationship.target);
2791                    }
2792                }
2793            }
2794            files.sort();
2795            files
2796        })
2797    }
2798
2799    /// Uuid → file, for the **ordinary objects** whose own `.rels` named one.
2800    ///
2801    /// The counterpart of [`EpcStore::external_file_map`] for the relationship
2802    /// §3.2.3 does not describe. Read back from the archive rather than derived
2803    /// from the arrays: the point is to return what the package said, not to
2804    /// decide what it ought to have said. An object this session created has no
2805    /// recorded `.rels` and so gets nothing, which is right - it never claimed
2806    /// one.
2807    pub fn object_external_file_map(&self) -> std::collections::BTreeMap<String, String> {
2808        self.entries
2809            .iter()
2810            .filter(|entry| {
2811                !(entry.summary.qualified_type.contains("EpcExternalPartReference")
2812                    || entry
2813                        .info
2814                        .is_some_and(|i| i.type_key == "EpcExternalPartReference"))
2815            })
2816            .filter_map(|entry| {
2817                let uuid = entry.summary.uuid.to_ascii_lowercase();
2818                // Declared **only where the package already said something**:
2819                // this map is what the archive stated, and adding a uuid it
2820                // never mentioned would invent a relationship. Replacing the
2821                // value of one it did mention is what a rename is.
2822                let file = match self.declared_files.get(&uuid) {
2823                    Some(declared) => {
2824                        self.external_resource_in(&entry.summary.locator)?;
2825                        declared.clone()
2826                    }
2827                    None => self.external_resource_in(&entry.summary.locator)?,
2828                };
2829                Some((uuid, file))
2830            })
2831            .collect()
2832    }
2833
2834    /// Every `EpcExternalPartReference` in the package, mapped to the file it
2835    /// stands for.
2836    ///
2837    /// What the writer needs: the proxy object must not carry the name itself
2838    /// (EPC §3.2.3), so it travels beside the objects and lands in the proxy's
2839    /// `.rels`.
2840    pub fn external_file_map(&self) -> std::collections::BTreeMap<String, String> {
2841        let mut map: std::collections::BTreeMap<String, String> = self
2842            .entries
2843            .iter()
2844            .filter(|entry| {
2845                entry.summary.qualified_type.contains("EpcExternalPartReference")
2846                    || entry
2847                        .info
2848                        .is_some_and(|i| i.type_key == "EpcExternalPartReference")
2849            })
2850            .filter_map(|entry| {
2851                let uuid = entry.summary.uuid.to_ascii_lowercase();
2852                self.external_file_for(&uuid, Some(&entry.summary.locator))
2853                    .map(|file| (uuid, file))
2854            })
2855            .collect();
2856        // What the caller declared wins: it is the more recent statement, and
2857        // it is the only source for a proxy this session created.
2858        map.extend(self.declared_files.iter().map(|(k, v)| (k.clone(), v.clone())));
2859        map
2860    }
2861
2862    /// Name the external file a proxy stands for.
2863    ///
2864    /// Needed for a proxy created in this session: `save` regenerates every
2865    /// `.rels` from what the store knows, and without this the new
2866    /// `EpcExternalPartReference` is written out naming no file - a warning at
2867    /// best, a package whose arrays cannot be found at worst.
2868    ///
2869    /// Deliberately separate from writing the numbers. The two halves fail
2870    /// independently and for different reasons - a disk full against a package
2871    /// that will not save - and pretending otherwise would leave a file on disk
2872    /// that nothing points at, or a pointer to a file that was never written.
2873    pub fn register_external_file(&mut self, proxy_uuid: &str, file_name: impl Into<String>) {
2874        let file_name = file_name.into();
2875        // A proxy that now names another file resolves its arrays elsewhere:
2876        // whoever read them through the old name has to be told.
2877        self.overlay.journal().note_renamed(proxy_uuid, &file_name);
2878        self.declared_files
2879            .insert(proxy_uuid.to_ascii_lowercase(), file_name);
2880    }
2881
2882    /// The external files this session declared, as opposed to found.
2883    pub fn declared_external_files(&self) -> &std::collections::BTreeMap<String, String> {
2884        &self.declared_files
2885    }
2886
2887    /// The citation of an object held in memory, without going near the archive.
2888    fn header_of(&self, object: &dyn Reflect) -> Result<ObjectHeader> {
2889        let summary = self
2890            .summary_of(object)
2891            .ok_or_else(|| Error::Package("this object carries no identity".to_string()))?;
2892        let citation = identity::title_of_reflect(object).map(str::to_string);
2893        Ok(ObjectHeader {
2894            schema_version: Some(object.type_info().schema_version)
2895                .filter(|version| !version.is_empty())
2896                .map(str::to_string),
2897            object_version: identity::object_version_of_reflect(object).map(str::to_string),
2898            title: citation,
2899            // The rest lives in the citation and is not worth a second walk:
2900            // an application that edited the object has it in hand already.
2901            originator: None,
2902            creation: None,
2903            last_update: None,
2904            format: None,
2905            summary,
2906        })
2907    }
2908
2909    // -- Modifying ---------------------------------------------------------
2910
2911    /// Borrow an object for editing, marking it modified.
2912    ///
2913    /// The normal way to change something. Rust does not report when a field is
2914    /// written, so the mark is taken **when the borrow is taken** - the moment
2915    /// an application asks to edit is the moment it intends to. Simple, exact,
2916    /// and it is the call an editing interface makes anyway.
2917    ///
2918    /// The archive is untouched until a save.
2919    pub fn edit(&mut self, uri: &str) -> Result<&mut dyn Reflect> {
2920        // Nothing is known of what the caller will change: the broad answer, and
2921        // no refinement (the references are compared only when a path is given).
2922        self.edit_what(uri, What::BROAD, false)
2923    }
2924
2925    /// [`EpcStore::edit`] for an edit of **one value, addressed by its dotted
2926    /// path** - what a form or a front end's JSON edit does. The path is what lets
2927    /// the journal say *what* changed ([`What::of_edit`]): a citation path is
2928    /// `CITATION`; any other is `BODY | LINKS_OUT`, and `LINKS_OUT` is taken off
2929    /// again when the object's references are found unmoved, the next time the
2930    /// journal is read ([`EpcStore::changes_since`]) - the rule of the front end's
2931    /// `referencesMoved`, over the reference graph instead of the text.
2932    pub fn edit_at(&mut self, uri: &str, path: &str) -> Result<&mut dyn Reflect> {
2933        let mut what = What::of_edit(path);
2934        // **An edit of the `objectVersion` is an identity change**: the uri an
2935        // object is known by carries its version whenever it declares one
2936        // (internally the uri always carries the version number when there is
2937        // one, ADR-0003), so the uri moves with it. The
2938        // entry keeps its slot; the uri, and every key built on it, do not.
2939        let last = path.rsplit('.').next().unwrap_or(path);
2940        if last.eq_ignore_ascii_case("objectVersion") || last.eq_ignore_ascii_case("VersionString") {
2941            what = What::CITATION | What::BROAD | What::IDENTITY;
2942        }
2943        let refine = what.contains(What::LINKS_OUT) && !what.contains(What::IDENTITY);
2944        self.edit_what(uri, what, refine)
2945    }
2946
2947    /// Give an entry another `objectVersion`, through the layer. The entry keeps
2948    /// its slot; its uri, which carries the version, changes, and the journal
2949    /// says `IDENTITY`. Refused, by name, when a
2950    /// namesake already carries that version - two entries of one identity are one
2951    /// object. Returns the uri the entry is now known by.
2952    pub fn set_object_version(&mut self, uri: &str, version: &str) -> Result<String> {
2953        let version = normal_version(Some(version))
2954            .ok_or_else(|| Error::Package("an empty objectVersion names no version".to_string()))?;
2955        let (slot, uuid, qualified_type) = {
2956            let live = self.locate(uri)?;
2957            (
2958                live.slot.to_string(),
2959                live.summary.uuid.to_ascii_lowercase(),
2960                live.summary.qualified_type.clone(),
2961            )
2962        };
2963        let namesakes: Vec<(Option<String>, String)> = self
2964            .live_of(&uuid)
2965            .into_iter()
2966            .filter(|one| one.summary.qualified_type == qualified_type && one.slot != slot)
2967            .map(|one| (one.version.map(str::to_string), one.summary.uri.clone()))
2968            .collect();
2969        if let Some((_, held)) = namesakes
2970            .iter()
2971            .find(|(held, _)| held.as_deref() == Some(version.as_str()))
2972        {
2973            return Err(Error::Package(format!(
2974                "`{held}` already carries the objectVersion `{version}`: two entries of one identity are one object"
2975            )));
2976        }
2977        let what = What::CITATION | What::BROAD | What::IDENTITY;
2978        let object = self.edit_what(uri, what, false)?;
2979        let value = || energyml_core::value::OwnedValue::Str(version.clone());
2980        energyml_core::path::set(object, "objectVersion", value())
2981            .or_else(|_| energyml_core::path::set(object, "Citation.VersionString", value()))
2982            .map_err(|why| Error::Package(format!("`{uri}` has no place for an objectVersion: {why}")))?;
2983        self.settle_versions();
2984        Ok(self
2985            .live_of(&uuid)
2986            .into_iter()
2987            .find(|one| one.slot == slot)
2988            .map(|one| one.summary.uri.clone())
2989            .unwrap_or_default())
2990    }
2991
2992    /// The version an entry was recorded with, `None` when the slot is unknown.
2993    fn recorded_version(&self, slot: &str) -> Option<Option<String>> {
2994        let uuid = uuid_of_slot(slot);
2995        self.by_uuid
2996            .get(uuid)
2997            .into_iter()
2998            .flatten()
2999            .map(|&at| &self.entries[at])
3000            .find(|entry| entry.slot == slot)
3001            .map(|entry| entry.object_version.clone())
3002            .or_else(|| self.made_by_slot(slot).map(|made| made.object_version.clone()))
3003    }
3004
3005    /// Bring the entries' recorded versions and uris up to what the layer holds:
3006    /// an object edited in place may state another `objectVersion` than its entry
3007    /// was born with. Run at the start of every gate that writes; a lookup does not
3008    /// wait for it (`current_version`), only the spelling of a uri does.
3009    /// Bring every entry's recorded version, and so its uri, up to what the
3010    /// layer holds - after an edit through the borrow [`EpcStore::edit_at`] hands
3011    /// out, which the store cannot watch while the caller holds it. Every writing
3012    /// gate does this first; a face that edits and then lists (the front end's
3013    /// `set` and `set_json`) calls it once its edit is done, so the inventory
3014    /// names the object by the uri its version now gives it.
3015    pub fn settle_identities(&mut self) {
3016        self.settle_versions();
3017    }
3018
3019    fn settle_versions(&mut self) {
3020        if self.unsettled.is_empty() {
3021            return;
3022        }
3023        let mut drifted: Vec<(String, String, Option<String>)> = Vec::new();
3024        for slot in std::mem::take(&mut self.unsettled) {
3025            let Some(Change::Put(object)) = self.overlay.get(&slot) else {
3026                continue;
3027            };
3028            let now = normal_version(identity::object_version_of_reflect(object.as_reflect()));
3029            if self
3030                .recorded_version(&slot)
3031                .is_some_and(|recorded| recorded != now)
3032            {
3033                drifted.push((uuid_of_slot(&slot).to_string(), slot.clone(), now));
3034            }
3035        }
3036        for (uuid, slot, now) in drifted {
3037            let before: Vec<(String, String)> = self
3038                .live_of(&uuid)
3039                .iter()
3040                .map(|one| (one.slot.to_string(), one.summary.uri.clone()))
3041                .collect();
3042            self.set_entry_version(&uuid, &slot, now);
3043            self.moved_uris(&uuid, &slot, &before);
3044        }
3045    }
3046
3047    fn edit_what(&mut self, uri: &str, what: What, refine: bool) -> Result<&mut dyn Reflect> {
3048        self.settle_versions();
3049        let slot = self.slot_of(uri)?;
3050        if !matches!(self.overlay.get(&slot), Some(Change::Put(_))) {
3051            // Not in the layer yet: materialise it there first. Materialising is
3052            // not a write; the borrow below is, and journals itself.
3053            let object = self.get_object(uri)?;
3054            self.overlay.place_unjournaled(&slot, Change::Put(object));
3055        }
3056        // Lent out: the caller may change its `objectVersion`, which the next
3057        // writing gate settles (`unsettled`).
3058        self.unsettled.insert(slot.clone());
3059        let before = match (refine, self.overlay.get(&slot)) {
3060            (true, Some(Change::Put(object))) => Some(reference_list(object.as_reflect())),
3061            _ => None,
3062        };
3063        if before.is_some() {
3064            // An earlier provisional edit of this object is judged against the
3065            // state it left, not against the one this edit is about to leave.
3066            self.settle_edits(Some(&slot));
3067        }
3068        self.overlay
3069            .get_mut_what(&slot, Some(uri), what)
3070            .ok_or_else(|| Error::NotFound(uri.to_string()))?;
3071        if let Some(references) = before {
3072            let revision = self.overlay.revision();
3073            let mut pending = self
3074                .pending_edits
3075                .lock()
3076                .unwrap_or_else(|poisoned| poisoned.into_inner());
3077            if pending.len() >= PENDING_EDITS {
3078                // The oldest keeps the wider answer, which is the safe one.
3079                pending.remove(0);
3080            }
3081            pending.push(PendingEdit {
3082                revision,
3083                slot: slot.clone(),
3084                references,
3085            });
3086        }
3087        let object = self
3088            .overlay
3089            .get_mut_unjournaled(&slot)
3090            .ok_or_else(|| Error::NotFound(uri.to_string()))?;
3091        Ok(object.as_reflect_mut())
3092    }
3093
3094    /// Add an object, or replace one wholesale - ETP protocol 4, `PutDataObjects`.
3095    ///
3096    /// Returns the URI it will be known by.
3097    ///
3098    /// **An ordinary put never creates a second version of an object.** An entry
3099    /// is (uuid, qualified type, `objectVersion`), and this replaces the entry of
3100    /// the object's (uuid, type):
3101    ///
3102    /// - there is none: the object is **new**;
3103    /// - there is one: it is replaced - even when the object states another
3104    ///   `objectVersion` than the entry had (the entry takes it: that is an
3105    ///   edited version, an identity event in the journal, not a second object);
3106    /// - there are several (the package holds versions of it): the one of the
3107    ///   same version is replaced; an object naming a version none of them
3108    ///   carries is refused, [`Error::Ambiguous`] - it could be any of them, and
3109    ///   this store does not guess. Adding a version is
3110    ///   [`EpcStore::put_as_new_version`], and nothing else.
3111    pub fn put(&mut self, object: Box<dyn DynObject>) -> Result<String> {
3112        self.put_entry(object, false)
3113    }
3114
3115    /// Add an object **as a new version** of an object this package holds: the
3116    /// same uuid and type under another `objectVersion` - a second entry, with
3117    /// its own part when the package is written (EPC v1.1 §3.6, p. 17), its own
3118    /// uri, and a `CREATED` line in the journal.
3119    ///
3120    /// The gate exists so that a version is never made by accident: an ordinary
3121    /// [`EpcStore::put`] replaces. It is refused, by name, when the exact
3122    /// identity - uuid, type *and* version, an absent version included - is
3123    /// already there; replacing is what `put` is for.
3124    ///
3125    /// The uri of the entries moves with the new one: the short uri is accepted
3126    /// when there is no conflict (or when the object has no version), the version
3127    /// is required when there is one - the entry that
3128    /// declares no version keeps the short uri, every versioned entry of the
3129    /// (uuid, type) takes the form with its version, and the journal says so for
3130    /// each entry whose uri changed.
3131    pub fn put_as_new_version(&mut self, object: Box<dyn DynObject>) -> Result<String> {
3132        self.put_entry(object, true)
3133    }
3134
3135    fn put_entry(&mut self, object: Box<dyn DynObject>, new_version: bool) -> Result<String> {
3136        self.settle_versions();
3137        let summary = self
3138            .summary_of(object.as_reflect())
3139            .ok_or_else(|| Error::Package("this object carries no uuid, so it cannot be a part".into()))?;
3140        // **Present is not the same as usable.** The reader leaves an absent
3141        // attribute at its default, so an object with no identity arrives
3142        // carrying an empty string rather than nothing at all - and an empty
3143        // string sails past the check above, then becomes the part
3144        // `eml:///resqml22.ColorMapDictionary()`, which is not an address.
3145        // Every later call that takes a uri then refuses it, far from here,
3146        // with a message about the uri rather than about the object.
3147        if !identifiers::is_uuid(&summary.uuid) {
3148            return Err(Error::Package(format!(
3149                "`{}` is not a usable uuid, so this object cannot be a part",
3150                summary.uuid
3151            )));
3152        }
3153        // An object that exists only by value inside a container is neither
3154        // created nor replaced by a put: which one is meant is not decided.
3155        self.refuse_contained_put(&summary)?;
3156        let key = summary.uuid.to_ascii_lowercase();
3157        let version = normal_version(identity::object_version_of_reflect(object.as_reflect()));
3158        let versioned = |version: Option<&str>| {
3159            identifiers::EtpUri::object_versioned(
3160                self.dataspace_path(),
3161                &summary.qualified_type,
3162                &summary.uuid,
3163                version,
3164            )
3165        };
3166
3167        // The entries of this (uuid, type) as they stand: slot and version.
3168        let same_type: Vec<(String, Option<String>, String)> = self
3169            .live_of(&key)
3170            .into_iter()
3171            .filter(|one| one.summary.qualified_type == summary.qualified_type)
3172            .map(|one| {
3173                (
3174                    one.slot.to_string(),
3175                    one.version.map(str::to_string),
3176                    one.summary.uri.clone(),
3177                )
3178            })
3179            .collect();
3180
3181        // Which slot this object takes, and whether it replaces something there.
3182        let (slot, replaces) = if new_version {
3183            if same_type.iter().any(|(_, held, _)| *held == version) {
3184                return Err(Error::Package(format!(
3185                    "`{}` is already held{}: a put replaces it, a new version is another \
3186                     `objectVersion`",
3187                    versioned(version.as_deref()),
3188                    if version.is_none() {
3189                        " (it declares no objectVersion)"
3190                    } else {
3191                        ""
3192                    }
3193                )));
3194            }
3195            (
3196                self.slot_for_new(&key, &summary.qualified_type, version.as_deref()),
3197                false,
3198            )
3199        } else if let Some((slot, _, _)) = same_type.iter().find(|(_, held, _)| *held == version) {
3200            (slot.clone(), true)
3201        } else if let [(slot, _, _)] = &same_type[..] {
3202            (slot.clone(), true)
3203        } else if same_type.is_empty() {
3204            (
3205                self.slot_for_new(&key, &summary.qualified_type, version.as_deref()),
3206                false,
3207            )
3208        } else {
3209            return Err(Error::Ambiguous {
3210                uri: versioned(version.as_deref()),
3211                candidates: same_type.iter().map(|(_, _, uri)| uri.clone()).collect(),
3212            });
3213        };
3214
3215        let before: Vec<(String, String)> = self
3216            .live_of(&key)
3217            .iter()
3218            .map(|one| (one.slot.to_string(), one.summary.uri.clone()))
3219            .collect();
3220
3221        // Whether this replaces an object or adds one decides what the journal
3222        // says: the broad answer for a replacement (a reader that held the old
3223        // one has a different one), `CREATED` for a new entry. An entry removed
3224        // earlier in the layer is new again.
3225        let mut what = if replaces { What::BROAD } else { What::CREATED };
3226        if replaces {
3227            // An object that states another version than its entry had.
3228            let entry_version = self
3229                .live_of(&key)
3230                .into_iter()
3231                .find(|one| one.slot == slot)
3232                .map(|one| one.version.map(str::to_string));
3233            if entry_version.is_some_and(|held| held != version) {
3234                // The uri carries the version: another version is another uri.
3235                what |= What::IDENTITY;
3236                self.set_entry_version(&key, &slot, version.clone());
3237            }
3238        }
3239        // Remembered only when the archive does not hold the entry: an edit to
3240        // an existing part goes through here too, and `entries` answers for it.
3241        if !self.is_archive_slot(&slot) {
3242            let list = self.made.entry(key.clone()).or_default();
3243            let made = Made {
3244                slot: slot.clone(),
3245                summary: summary.clone(),
3246                object_version: version.clone(),
3247            };
3248            match list.iter_mut().find(|held| held.slot == slot) {
3249                Some(held) => *held = made,
3250                None => list.push(made),
3251            }
3252        }
3253        self.overlay
3254            .set_what(&slot, Some(&summary.uri), Change::Put(object), what);
3255        self.moved_uris(&key, &slot, &before);
3256        let uri = self
3257            .live_of(&key)
3258            .into_iter()
3259            .find(|one| one.slot == slot)
3260            .map(|one| one.summary.uri.clone())
3261            .unwrap_or(summary.uri);
3262        self.after_change()?;
3263        Ok(uri)
3264    }
3265
3266    /// The slot for an entry that is new to the package - or comes back to it.
3267    ///
3268    /// An entry the archive held, removed here and put again, takes its own slot
3269    /// back (ADR-0010: re-creating the same uuid after a removal
3270    /// is the same slot); otherwise the uuid itself, if no entry holds it - the
3271    /// rule that leaves every package without namesakes with the keys it always
3272    /// had - and a composite slot if one does.
3273    fn slot_for_new(&self, uuid: &str, qualified_type: &str, version: Option<&str>) -> String {
3274        if let Some(entry) = self
3275            .by_uuid
3276            .get(uuid)
3277            .into_iter()
3278            .flatten()
3279            .map(|&at| &self.entries[at])
3280            .find(|entry| {
3281                entry.summary.qualified_type == qualified_type && entry.object_version.as_deref() == version
3282            })
3283        {
3284            return entry.slot.clone();
3285        }
3286        let taken = self
3287            .by_uuid
3288            .get(uuid)
3289            .into_iter()
3290            .flatten()
3291            .any(|&at| self.entries[at].slot == uuid)
3292            || self
3293                .made
3294                .get(uuid)
3295                .into_iter()
3296                .flatten()
3297                .any(|made| made.slot == uuid);
3298        if taken {
3299            slot_key(uuid, qualified_type, version)
3300        } else {
3301            uuid.to_string()
3302        }
3303    }
3304
3305    /// An entry that states another `objectVersion` than it was born with.
3306    fn set_entry_version(&mut self, uuid: &str, slot: &str, version: Option<String>) {
3307        if let Some(entry) = self
3308            .by_uuid
3309            .get(uuid)
3310            .into_iter()
3311            .flatten()
3312            .copied()
3313            .find(|&at| self.entries[at].slot == slot)
3314        {
3315            self.entries[entry].object_version = version;
3316        } else if let Some(made) = self
3317            .made
3318            .get_mut(uuid)
3319            .and_then(|list| list.iter_mut().find(|made| made.slot == slot))
3320        {
3321            made.object_version = version;
3322        }
3323    }
3324
3325    /// After the set of entries of a uuid changed: spell their uris again, and
3326    /// journal each entry **other than `own`** whose uri moved. `before` is the
3327    /// slot and uri of every live entry before the change.
3328    ///
3329    /// That the second version of an object changes the uri of the first is the
3330    /// price of the short form (ADR-0010): the front end handles it
3331    /// the way it handles an identity change, and the journal has to say it.
3332    fn moved_uris(&mut self, uuid: &str, own: &str, before: &[(String, String)]) {
3333        self.reconcile_uris(uuid);
3334        let after: Vec<(String, String)> = self
3335            .live_of(uuid)
3336            .iter()
3337            .map(|one| (one.slot.to_string(), one.summary.uri.clone()))
3338            .collect();
3339        for (slot, uri) in &after {
3340            if slot == own {
3341                continue;
3342            }
3343            if before.iter().any(|(held, was)| held == slot && was != uri) {
3344                self.overlay
3345                    .journal()
3346                    .touch_what(slot, Some(uri), What::BROAD | What::IDENTITY);
3347            }
3348        }
3349        // The entry itself, when its own uri moved too (a version edited in
3350        // place, or an entry coming back beside a namesake): an identity event.
3351        if let (Some((_, was)), Some((_, now))) = (
3352            before.iter().find(|(slot, _)| slot == own),
3353            after.iter().find(|(slot, _)| slot == own),
3354        ) && was != now
3355        {
3356            self.overlay.journal().touch_what(own, Some(now), What::IDENTITY);
3357        }
3358    }
3359
3360    /// Remove an object - ETP protocol 4, `DeleteDataObjects`.
3361    ///
3362    /// The parts that referenced it keep their references: this does **not**
3363    /// walk the package fixing them up, because deciding what a dangling
3364    /// reference should become is the application's business, not a store's.
3365    /// `energyml check` finds them afterwards.
3366    ///
3367    /// Removing one version of an object leaves the others; the one that is left
3368    /// alone takes the short uri back, and the journal says so.
3369    pub fn remove(&mut self, uri: &str) -> Result<()> {
3370        self.settle_versions();
3371        // An object held by value, or a container whose contained objects live
3372        // only inside it, cannot be removed without losing more than was asked.
3373        self.refuse_contained_remove(uri)?;
3374        let slot = match self.slot_of(uri) {
3375            Ok(slot) => slot,
3376            // Removing what was already removed in this layer is not a failure:
3377            // the answer is the same as the first time.
3378            Err(Error::NotFound(absent)) => {
3379                let uuid = address_of(uri)?.uuid;
3380                if self.overlay.get(&uuid).is_none() {
3381                    return Err(Error::NotFound(absent));
3382                }
3383                uuid
3384            }
3385            Err(other) => return Err(other),
3386        };
3387        let uuid = uuid_of_slot(&slot).to_string();
3388        let before: Vec<(String, String)> = self
3389            .live_of(&uuid)
3390            .iter()
3391            .map(|one| (one.slot.to_string(), one.summary.uri.clone()))
3392            .collect();
3393        if let Some(list) = self.made.get_mut(&uuid) {
3394            list.retain(|made| made.slot != slot);
3395        }
3396        self.overlay
3397            .set_what(&slot, Some(uri), Change::Removed, What::REMOVED);
3398        self.moved_uris(&uuid, &slot, &before);
3399        self.after_change()
3400    }
3401
3402    /// Whether anything has changed since the package was opened.
3403    pub fn is_dirty(&self) -> bool {
3404        self.overlay.is_dirty()
3405    }
3406
3407    /// Choose when changes reach the disk. See [`SavePolicy`].
3408    pub fn set_save_policy(&mut self, policy: SavePolicy) {
3409        self.policy = policy;
3410    }
3411
3412    /// Choose how the package is written when a policy writes on its own.
3413    pub fn set_save_options(&mut self, options: SaveOptions) {
3414        self.save_options = options;
3415    }
3416
3417    /// Mark the edited object and honour [`SavePolicy::OnChange`].
3418    ///
3419    /// Called after a mutation rather than before, so a failed write leaves the
3420    /// change in memory instead of losing it.
3421    fn after_change(&mut self) -> Result<()> {
3422        if self.policy == SavePolicy::OnChange && self.path.is_some() {
3423            let options = self.save_options.clone();
3424            self.save(&options)?;
3425        }
3426        Ok(())
3427    }
3428
3429    // -- Transactions ------------------------------------------------------
3430
3431    /// Start a transaction - ETP protocol 18.
3432    ///
3433    /// One layer more on the in-memory stack, and nothing on disk. ETP v1.2
3434    /// forbids nesting, so a second call is an error.
3435    pub fn start_transaction(&mut self) -> Result<()> {
3436        self.overlay.start_transaction()
3437    }
3438
3439    /// Merge the transaction into the changes below it.
3440    pub fn commit_transaction(&mut self) -> Result<()> {
3441        self.overlay.commit_transaction()?;
3442        self.after_change()
3443    }
3444
3445    /// Throw the transaction away. It never reached the disk, so nothing there
3446    /// has to be undone - which is the whole reason transactions are a layer.
3447    pub fn rollback_transaction(&mut self) -> Result<()> {
3448        // What the layer held decides which entries are live, so the uris of the
3449        // uuids it touched are spelled again once it is gone: a version added in
3450        // the transaction no longer makes its namesake's uri a long one.
3451        let touched: BTreeSet<String> = self
3452            .overlay
3453            .changed()
3454            .iter()
3455            .map(|(slot, _)| uuid_of_slot(slot).to_string())
3456            .collect();
3457        // What the layer below holds again may state another version than an
3458        // entry recorded during the transaction: settle every slot it held.
3459        let held: Vec<String> = self
3460            .overlay
3461            .changed()
3462            .iter()
3463            .map(|(slot, _)| (*slot).clone())
3464            .collect();
3465        self.unsettled.extend(held);
3466        self.overlay.rollback_transaction()?;
3467        for uuid in touched {
3468            self.reconcile_uris(&uuid);
3469        }
3470        Ok(())
3471    }
3472
3473    /// Whether a transaction is open.
3474    pub fn in_transaction(&self) -> bool {
3475        self.overlay.in_transaction()
3476    }
3477
3478    // -- Writing -----------------------------------------------------------
3479
3480    /// Write the package to any sink, and say what could not be represented.
3481    ///
3482    /// **Everything is materialised first.** Regenerating the relationships
3483    /// means walking every object's DORs, so saving reads the whole package -
3484    /// the lazy levels are a convenience for looking at a file, never a way to
3485    /// write half of one (`write.rs`).
3486    ///
3487    /// A part the registry cannot deserialize is **copied verbatim** rather
3488    /// than dropped, with a warning: its bytes are right there, and losing an
3489    /// object because this build lacks its schema version would be the worst
3490    /// possible answer. Its relationships cannot be regenerated, and the
3491    /// warning says so.
3492    pub fn write_to<W: std::io::Write + std::io::Seek>(
3493        &self,
3494        out: W,
3495        options: &SaveOptions,
3496    ) -> Result<Vec<Diagnostic>> {
3497        let mut diagnostics = Vec::new();
3498        let mut objects: Vec<Box<dyn DynObject>> = Vec::new();
3499        let mut verbatim = Vec::new();
3500
3501        for entry in &self.entries {
3502            // Removed in the layer: not written, and its bytes are not kept
3503            // either - that is what removing means.
3504            if matches!(self.overlay.get(&entry.slot), Some(Change::Removed)) {
3505                continue;
3506            }
3507            // Bytes staged for this part win over everything: they are the
3508            // repair path, and the only reason to take it is that the parser
3509            // was wrong about this part. Written exactly as given.
3510            if let Some(bytes) = self.overlay.raw(&entry.summary.locator) {
3511                verbatim.push(write::Attachment {
3512                    part: entry.summary.locator.clone(),
3513                    content_type: Some(entry.summary.content_type.clone()),
3514                    bytes: bytes.to_vec(),
3515                });
3516                continue;
3517            }
3518            match self.get_object(&entry.summary.uri) {
3519                Ok(object) => objects.push(object),
3520                Err(e) => {
3521                    diagnostics.push(Diagnostic::warning(
3522                        entry.summary.locator.clone(),
3523                        format!(
3524                            "{e}; the part was copied unchanged, so nothing is lost - but its \
3525                             relationships could not be regenerated"
3526                        ),
3527                    ));
3528                    verbatim.push(write::Attachment {
3529                        part: entry.summary.locator.clone(),
3530                        content_type: Some(entry.summary.content_type.clone()),
3531                        bytes: self.package.read(&entry.summary.locator)?,
3532                    });
3533                }
3534            }
3535        }
3536
3537        // Objects the layer added, which have no part in the archive: **lent**,
3538        // not copied. The writer only reads them, and `&self` keeps the layer
3539        // still while it does; a deep copy of each was the price of nothing
3540        // (07/10/2026: seventy thousand objects copied for one save).
3541        let added: Vec<&dyn DynObject> = self
3542            .overlay
3543            .changed()
3544            .into_iter()
3545            .filter_map(|(slot, change)| match change {
3546                Change::Put(object) if !self.is_archive_slot(slot) => Some(object.as_ref()),
3547                _ => None,
3548            })
3549            .collect();
3550
3551        let mut package = write::Package::new(
3552            objects
3553                .iter()
3554                .map(|object| object.as_ref())
3555                .chain(added)
3556                .collect(),
3557            self.core_properties_for(options),
3558        );
3559        package.layout = options.layout;
3560        package.external_files = self.external_file_map();
3561        package.object_external_files = self.object_external_file_map();
3562        package.verbatim = verbatim;
3563        package.media_links = self.media_links();
3564
3565        // Attachments and the parts the specification does not describe travel
3566        // as bytes: a PDF is a PDF, and fesapi's `extendedCore.xml` is nobody's
3567        // business but fesapi's.
3568        for part in self.attachments() {
3569            // A part that has been repaired into an object is written by the
3570            // object, under the name its type gives it. Copying the wreck
3571            // through as well would leave the package holding both.
3572            if self.overlay.is_superseded(&part) {
3573                continue;
3574            }
3575            package.verbatim.push(write::Attachment {
3576                // The archive's declaration when it had one, the caller's when
3577                // the part was added in this session. Neither invented: with no
3578                // answer the writer falls back to the extension, which is what
3579                // OPC's `Default` entries are for.
3580                content_type: self
3581                    .package
3582                    .content_type(&part)
3583                    .map(str::to_string)
3584                    .or_else(|| self.attachment_types.get(&part).cloned()),
3585                // `raw_part` rather than `package.read`: a part the inventory
3586                // dropped is reachable only here, and repairing one is the
3587                // whole point of the raw layer.
3588                bytes: self.raw_part(&part)?,
3589                part,
3590            });
3591        }
3592
3593        // `docProps/` is the package's own metadata, so it is not an
3594        // attachment - but everything there beyond `core.xml` is a producer's
3595        // extension that this library neither writes nor understands, and
3596        // dropping it on rewrite is a silent loss. fesapi's
3597        // `extendedCore.xml` is the real case, in both reference packages.
3598        //
3599        // `core.xml` itself is excluded because it is regenerated, and the
3600        // `.rels` because they are rebuilt from the DORs.
3601        let extra: Vec<String> = self
3602            .package
3603            .entries()
3604            .iter()
3605            .filter(|name| name.starts_with("docProps/"))
3606            .filter(|name| !name.eq_ignore_ascii_case("docProps/core.xml"))
3607            .filter(|name| !name.ends_with(".rels") && !name.ends_with('/'))
3608            .cloned()
3609            .collect();
3610        for part in extra {
3611            package.verbatim.push(write::Attachment {
3612                content_type: self.package.content_type(&part).map(str::to_string),
3613                bytes: self.package.read(&part)?,
3614                part,
3615            });
3616        }
3617
3618        write::write_package(&package, out, &mut diagnostics)?;
3619        Ok(diagnostics)
3620    }
3621
3622    /// The core properties to write: the source's, with `created` refreshed.
3623    ///
3624    /// §3.5.3 makes `creator`, `created` and `version` mandatory and closes the
3625    /// list of the rest. `created` is the **package** creation time, so it is
3626    /// this save; everything else is the caller's or the source's.
3627    fn core_properties_for(&self, options: &SaveOptions) -> core_props::CoreProperties {
3628        let mut core = self
3629            .core_properties()
3630            .map(|bytes| core_props::CoreProperties::parse(&bytes))
3631            .unwrap_or_default();
3632
3633        core.created = options.created.clone().unwrap_or_else(core_props::now_w3cdtf);
3634        core.version = core_props::EPC_VERSION.to_string();
3635        if let Some(creator) = &options.creator {
3636            core.creator = creator.clone();
3637        }
3638        if core.creator.is_empty() {
3639            core.creator = format!("Geosiris:energyml-rust:{}", env!("CARGO_PKG_VERSION"));
3640        }
3641        core
3642    }
3643
3644    /// Write the package to a path, atomically.
3645    ///
3646    /// Through a temporary file beside the target, then a rename: a crash
3647    /// leaves the previous package intact, never a half-written archive.
3648    ///
3649    /// The store keeps reading from the archive it was opened on, even when
3650    /// that is the file just replaced. Re-open to see what was written.
3651    pub fn save_as(&self, path: impl AsRef<Path>, options: &SaveOptions) -> Result<Vec<Diagnostic>> {
3652        let path = path.as_ref();
3653        let _span = tracing::info_span!("epc.save", path = %path.display()).entered();
3654        let temporary = path.with_extension(format!("epc-tmp-{}", std::process::id()));
3655        let diagnostics = {
3656            let file = std::fs::File::create(&temporary)?;
3657            let mut writer = std::io::BufWriter::new(file);
3658            let diagnostics = self.write_to(&mut writer, options)?;
3659            // Flush before the rename, or the rename publishes a truncated file.
3660            std::io::Write::flush(&mut writer)?;
3661            diagnostics
3662        };
3663        match std::fs::rename(&temporary, path) {
3664            Ok(()) => Ok(diagnostics),
3665            Err(e) => {
3666                let _ = std::fs::remove_file(&temporary);
3667                Err(e.into())
3668            }
3669        }
3670    }
3671
3672    /// Write the package back over the file it was opened from.
3673    ///
3674    /// Refused while a transaction is open: writing a state a rollback is
3675    /// supposed to be able to undo would put the disk and memory in
3676    /// disagreement, and one of the two would be lying.
3677    pub fn save(&self, options: &SaveOptions) -> Result<Vec<Diagnostic>> {
3678        if self.in_transaction() {
3679            return Err(Error::Package(
3680                "a transaction is open; commit or roll it back before saving".to_string(),
3681            ));
3682        }
3683        let path = self.path.clone().ok_or_else(|| {
3684            Error::Package("this store was opened from bytes, not a file; use `save_as`".into())
3685        })?;
3686        self.save_as(path, options)
3687    }
3688
3689    /// Write the package and close it, honouring [`SavePolicy::OnClose`].
3690    ///
3691    /// Explicit rather than left to `Drop`, because `Drop` returns no `Result`:
3692    /// a save that fails at destruction has nowhere to report it, and a
3693    /// silently failed save is worse than none.
3694    pub fn close(mut self, options: &SaveOptions) -> Result<Vec<Diagnostic>> {
3695        self.closed = true;
3696        if self.policy == SavePolicy::OnClose && self.is_dirty() {
3697            return self.save(options);
3698        }
3699        Ok(Vec::new())
3700    }
3701
3702    // -- The package itself ----------------------------------------------
3703
3704    /// Everything noticed while reading that departs from the specification.
3705    ///
3706    /// Grows as the store reads: a departure found only when an array is
3707    /// located cannot have been known when the archive was indexed.
3708    pub fn diagnostics(&self) -> Vec<Diagnostic> {
3709        self.diagnostics
3710            .lock()
3711            .expect("the diagnostics lock is never held across a panic")
3712            .clone()
3713    }
3714
3715    /// Record a departure, once. The same fallback taken for three hundred
3716    /// arrays is one problem, not three hundred lines.
3717    fn record(&self, diagnostic: Diagnostic) {
3718        let mut diagnostics = self
3719            .diagnostics
3720            .lock()
3721            .expect("the diagnostics lock is never held across a panic");
3722        if diagnostics
3723            .iter()
3724            .any(|d| d.part == diagnostic.part && d.message == diagnostic.message)
3725        {
3726            return;
3727        }
3728        diagnostics.push(diagnostic);
3729    }
3730
3731    /// Every entry **in the archive**, EnergyML or not, exactly as it was
3732    /// opened.
3733    ///
3734    /// Named for what it is since [`EpcStore::parts`] arrived beside it: this
3735    /// one answers about the file, that one about the package - which stop being
3736    /// the same thing the moment anything is edited, added or removed.
3737    pub fn archive_entries(&self) -> &[String] {
3738        self.package.entries()
3739    }
3740
3741    /// Every object the package holds, read, with the ones that did not.
3742    ///
3743    /// The loop four callers had written: read every resource, keep what
3744    /// deserializes, keep the name of what did not. **Unreadable is a list and
3745    /// not a silence** - a question asked about *every* object that quietly
3746    /// answered about some of them is the worst kind of wrong - and it is not
3747    /// an error either, because one bad part must not cost the answer.
3748    ///
3749    /// `of_type` filters on the qualified type before anything is parsed
3750    /// ([`crate::resource::type_matches`]), which is what makes asking for the
3751    /// properties of a package cheap on a package that is mostly geometry.
3752    #[allow(clippy::type_complexity)]
3753    pub fn read_objects(
3754        &self,
3755        of_type: Option<&str>,
3756    ) -> (Vec<(ObjectSummary, Box<dyn DynObject>)>, Vec<String>) {
3757        let mut read = Vec::new();
3758        let mut unreadable = Vec::new();
3759        for summary in self.resources() {
3760            if of_type.is_some_and(|want| !crate::resource::type_matches(&summary.qualified_type, want)) {
3761                continue;
3762            }
3763            match self.get_object(&summary.uri) {
3764                Ok(object) => read.push((summary, object)),
3765                Err(why) => unreadable.push(format!("{}: {why}", summary.uri)),
3766            }
3767        }
3768        (read, unreadable)
3769    }
3770
3771    /// Relationships recorded for one part.
3772    ///
3773    /// An index, not the truth: what an object actually references comes from
3774    /// its DORs (`identity::collect_refs`). Useful to check one against the
3775    /// other - a wrong `.rels` is worse than a missing one.
3776    pub fn relationships(&self, part: &str) -> Result<Vec<Relationship>> {
3777        self.relationships_recorded(part)
3778    }
3779
3780    /// The relationships of a part, with whatever is wrong with the `.rels`
3781    /// recorded on the store rather than dropped on the floor.
3782    fn relationships_recorded(&self, part: &str) -> Result<Vec<Relationship>> {
3783        let (relationships, problems) = self.package.relationships_reporting(part)?;
3784        for problem in problems {
3785            self.record(problem);
3786        }
3787        Ok(relationships)
3788    }
3789
3790    /// The OPC core properties part, as bytes, if the package has one.
3791    ///
3792    /// Mandatory in an EPC (§3.5 of the packaging conventions), which is why
3793    /// its absence is reported at `open` rather than here.
3794    pub fn core_properties(&self) -> Option<Vec<u8>> {
3795        self.package.read("docProps/core.xml").ok()
3796    }
3797
3798    // -- Internals ---------------------------------------------------------
3799
3800    /// Resolve a URI, or a bare uuid, to an inventory entry.
3801    fn entry(&self, uri: &str) -> Result<&PartEntry> {
3802        self.locate(uri)?
3803            .archive
3804            .ok_or_else(|| Error::NotFound(uri.to_string()))
3805    }
3806
3807    /// The file a location names, resolving the proxy if it has not been.
3808    fn resolved_file(&self, location: &ArrayLocation) -> Option<String> {
3809        if location.is_resolved() {
3810            return Some(location.file.clone());
3811        }
3812        Some(
3813            self.external_target_for(location.proxy_uuid.as_deref()?, None)?
3814                .file,
3815        )
3816    }
3817
3818    /// Everything an `EpcExternalPartReference` says: the file, and the format.
3819    ///
3820    /// The two come from two different places, which is easy to miss. The file
3821    /// name is in the proxy's `.rels` (EPC §3.2.3 is explicit that a reader
3822    /// takes it from the relationship). The **MIME type is on the proxy
3823    /// object** - a mandatory element in eml 2.0, whose own documentation says
3824    /// `application/x-hdf5` is a *convention*. Dropping it, as this crate did
3825    /// at first, silently declares every external file in a 2.0.1 package to be
3826    /// HDF5, and a producer writing CSV has no way to say otherwise.
3827    pub fn external_target_for(
3828        &self,
3829        proxy_uuid: &str,
3830        source_part: Option<&str>,
3831    ) -> Option<crate::array::ProxyTarget> {
3832        let file = self.external_file_for(proxy_uuid, source_part)?;
3833        Some(crate::array::ProxyTarget {
3834            file,
3835            mime: self.proxy_mime(proxy_uuid),
3836        })
3837    }
3838
3839    /// The MIME type an `EpcExternalPartReference` declares, if it declares a
3840    /// usable one.
3841    fn proxy_mime(&self, proxy_uuid: &str) -> Option<String> {
3842        let summary = self.find_by_uuid(proxy_uuid)?;
3843        let proxy = self.get_object(&summary.uri).ok()?;
3844        let mime = energyml_core::path::get_string(proxy.as_reflect(), "MimeType").ok()??;
3845        let mime = mime.trim();
3846        // §3.2.3 says these "should be null" inside an EPC, and the schema makes
3847        // the element mandatory: an empty one is the normal case, not an error.
3848        (!mime.is_empty()).then(|| mime.to_string())
3849    }
3850
3851    /// `created` and `modified` from `docProps/core.xml`.
3852    fn core_property_dates(&self) -> (Option<String>, Option<String>) {
3853        let Some(bytes) = self.core_properties() else {
3854            return (None, None);
3855        };
3856        let mut reader = quick_xml::Reader::from_reader(bytes.as_slice());
3857        let mut buf = Vec::new();
3858        let (mut created, mut modified) = (None, None);
3859        let mut current = String::new();
3860        loop {
3861            match reader.read_event_into(&mut buf) {
3862                Err(_) | Ok(quick_xml::events::Event::Eof) => break,
3863                Ok(quick_xml::events::Event::Start(e)) => {
3864                    current = local_name(e.name().as_ref());
3865                }
3866                Ok(quick_xml::events::Event::Text(e)) => {
3867                    let text = e.xml10_content().unwrap_or_default().trim().to_string();
3868                    if !text.is_empty() {
3869                        match current.as_str() {
3870                            "created" => created = Some(text),
3871                            "modified" => modified = Some(text),
3872                            _ => {}
3873                        }
3874                    }
3875                }
3876                _ => {}
3877            }
3878            buf.clear();
3879        }
3880        (created, modified)
3881    }
3882
3883    /// Walk the archive and decide, part by part, what is an EnergyML object.
3884    ///
3885    /// Two passes over each candidate, in this order:
3886    ///
3887    /// 1. `[Content_Types].xml` is believed first - it is the index the
3888    ///    specification defines, and it is right for the overwhelming majority
3889    ///    of parts.
3890    /// 2. When it is absent or names something the registry does not know, the
3891    ///    **root element** decides instead, and the discrepancy is recorded.
3892    ///    This is what rescues the object that the reference package types as
3893    ///    `core-properties`.
3894    fn build_inventory(&mut self) {
3895        let parts: Vec<String> = self
3896            .package
3897            .entries()
3898            .iter()
3899            .filter(|name| is_candidate_part(name))
3900            .cloned()
3901            .collect();
3902
3903        // A part shaped like an object but filed where the conventions put the
3904        // package's own metadata. It is skipped - reading it would put an
3905        // object in a place no writer will ever look for it again - but the
3906        // skip is said, because "it is in the file and the tool never mentioned
3907        // it" is the worst answer a repair tool can give.
3908        let misfiled: Vec<String> = self
3909            .package
3910            .entries()
3911            .iter()
3912            .filter(|name| {
3913                is_package_infrastructure(name)
3914                    && !name.ends_with(".rels")
3915                    && uuid_in_part_name(name).is_some()
3916            })
3917            .cloned()
3918            .collect();
3919        for part in misfiled {
3920            self.record(Diagnostic::note(
3921                part.clone(),
3922                "named like an object but filed under the package's own metadata; not listed",
3923            ));
3924        }
3925
3926        for part in parts {
3927            let Some(mut entry) = self.classify(&part) else {
3928                continue;
3929            };
3930            let key = entry.summary.uuid.to_ascii_lowercase();
3931            if identifiers::is_nil_uuid(&key) {
3932                self.record(Diagnostic::nil_uuid(part.clone(), None));
3933            }
3934            // **An entry is (uuid, qualified type, objectVersion)**, so the same
3935            // uuid under another version - or under another release - is
3936            // another entry, and every part is kept. Only a part that repeats the
3937            // whole identity is dropped, and said: two parts of one identity
3938            // cannot both be the object.
3939            if let Some(previous) = self
3940                .by_uuid
3941                .get(&key)
3942                .into_iter()
3943                .flatten()
3944                .map(|&at| &self.entries[at])
3945                .find(|held| {
3946                    held.summary.qualified_type == entry.summary.qualified_type
3947                        && held.object_version == entry.object_version
3948                })
3949            {
3950                self.record(Diagnostic::warning(
3951                    part.clone(),
3952                    format!(
3953                        "{} already carried by `{}` - same uuid, same type, same objectVersion; \
3954                         the first part wins",
3955                        entry.summary.uri, previous.summary.locator
3956                    ),
3957                ));
3958                continue;
3959            }
3960            // The first entry of a uuid is keyed by the uuid itself, as every
3961            // key was before versions existed; its namesakes by a composite.
3962            entry.slot = if self.by_uuid.contains_key(&key) {
3963                slot_key(
3964                    &key,
3965                    &entry.summary.qualified_type,
3966                    entry.object_version.as_deref(),
3967                )
3968            } else {
3969                key.clone()
3970            };
3971            self.by_uuid.entry(key).or_default().push(self.entries.len());
3972            self.entries.push(entry);
3973        }
3974        // Spell the uri of every uuid with a versioned entry: the form with its
3975        // version (the archive's summaries are built from the part's name and
3976        // content type, which know no version).
3977        let versioned: Vec<String> = self
3978            .by_uuid
3979            .iter()
3980            .filter(|(_, list)| {
3981                list.len() > 1 || list.iter().any(|&at| self.entries[at].object_version.is_some())
3982            })
3983            .map(|(uuid, _)| uuid.clone())
3984            .collect();
3985        for uuid in versioned {
3986            self.reconcile_uris(&uuid);
3987        }
3988
3989        if !self.package.has("docProps/core.xml") {
3990            self.record(Diagnostic::warning(
3991                "docProps/core.xml",
3992                "absent, though the EPC conventions make the core properties mandatory (§3.5)",
3993            ));
3994        }
3995    }
3996
3997    /// Decide what one part is. `None` means "not an EnergyML object".
3998    fn classify(&self, part: &str) -> Option<PartEntry> {
3999        let declared = self.package.content_type(part).map(str::to_string);
4000
4001        // The root element, read once for **every** part.
4002        //
4003        // Peeking only when the index was silent is the hole this closes: a
4004        // `[Content_Types].xml` that types a `TriangulatedSetRepresentation` as
4005        // a `PointSetRepresentation` was believed without a word, and the
4006        // object came out of the store as a plausible object of the wrong type
4007        // - the one failure mode of this reader that produced a wrong answer
4008        // instead of a named refusal. The peek stops at the end of the
4009        // citation, so it costs a few hundred inflated bytes per part.
4010        let peeked = self
4011            .package
4012            .with_part(part, |r| header::peek_reader(BufReader::new(r)))
4013            .ok()
4014            .filter(|p: &header::PeekedHeader| !p.root_name.is_empty());
4015
4016        // 1 - the index, when the document itself does not contradict it.
4017        if let Some(content_type) = &declared
4018            && let Ok(parts) = identity::parse_content_type(content_type)
4019            && !self.root_contradicts(peeked.as_ref(), parts.object_type)
4020        {
4021            let qualified_type = format!(
4022                "{}{}.{}",
4023                parts.domain,
4024                naming::compact_version(parts.version),
4025                parts.object_type
4026            );
4027            // The root does not *contradict* the index - it names nothing this
4028            // build knows, so there is no rival type to prefer and the index
4029            // rightly stands. But it does not agree either, and the read that
4030            // follows will therefore recognise none of the document: what comes
4031            // back is an object of the declared type with **every field at its
4032            // default**, uuid included, and nothing in the values says so.
4033            //
4034            // The decision is left alone; only the silence is not. Saying it is
4035            // the difference between a front end showing a blank object and a front end
4036            // showing a blank object with a reason.
4037            if let Some(peeked) = peeked.as_ref()
4038                && !naming::strip_obj_prefix(&peeked.root_name)
4039                    .eq_ignore_ascii_case(naming::strip_obj_prefix(parts.object_type))
4040            {
4041                self.record(Diagnostic::warning(
4042                    part,
4043                    format!(
4044                        "declared as `{qualified_type}`, but its root element is `{}`, which names \
4045                         no type this build knows; read as declared, so anything the document \
4046                         holds under that root is lost",
4047                        peeked.root_name
4048                    ),
4049                ));
4050            }
4051            let mut alternatives = Vec::new();
4052            let mut info = self.registry.by_content_type(content_type).map(|e| e.info);
4053            // What the substitution below writes into the summary, so the
4054            // listing and the object cannot disagree: a summary promising a
4055            // type no read can ever produce helps nobody, and the diagnostic
4056            // keeps what the file actually said.
4057            let mut qualified_type = qualified_type;
4058            let mut content_type = content_type.clone();
4059            // **A version the registry knows and does not hold is refused, not
4060            // substituted.** The fallback below is for versions the project
4061            // does not ship at all; for one it ships and this build left out
4062            // (`Registry::declare_absent`), reading the part as another release
4063            // of the package would yield a plausible object holding the fields
4064            // the two share. The part stays listed, unreadable, and says why.
4065            let mut refusal = if info.is_none() {
4066                self.registry
4067                    .absent_content_type(&content_type)
4068                    .or_else(|| self.registry.absent_qualified_type(&qualified_type))
4069                    .map(str::to_string)
4070            } else {
4071                None
4072            };
4073            // **A registered content type does not outvote a document that
4074            // names an absent version.** `version=2.2` in the index and
4075            // `schemaVersion="2.0"` on the root contradict each other toward a
4076            // release this build left out; following the index would read a
4077            // 2.0.1 document as 2.2 - a plausible object of the wrong release.
4078            // The document is the part, so the part is refused by the absent
4079            // message. A document with no declaration, one naming a compiled
4080            // release, a development drop or a version nobody declared absent
4081            // leaves the index in charge, as before.
4082            if refusal.is_none()
4083                && info.is_some()
4084                && let Some(peeked_header) = peeked.as_ref()
4085                && let Some((package, why)) = self.absent_declaration(peeked_header)
4086                && package.eq_ignore_ascii_case(parts.domain)
4087            {
4088                let release = peeked_header
4089                    .schema_version
4090                    .as_deref()
4091                    .map(naming::release_of)
4092                    .unwrap_or_default();
4093                qualified_type = format!(
4094                    "{package}{}.{}",
4095                    naming::compact_version(release),
4096                    parts.object_type
4097                );
4098                content_type = format!(
4099                    "application/x-{package}+xml;version={release};type={}",
4100                    parts.object_type
4101                );
4102                info = None;
4103                refusal = Some(why);
4104            }
4105            if let Some(why) = &refusal {
4106                self.record(Diagnostic::warning(
4107                    part,
4108                    format!("`{qualified_type}` cannot be read: {why}; the part is listed but not read"),
4109                ));
4110            }
4111            if info.is_none() && refusal.is_none() {
4112                // The declared version is one this build does not carry. Rather
4113                // than list a row nothing can open, fall back on the document:
4114                // the root element names the concept, its namespace names the
4115                // package, and another release of the same package will read
4116                // most of it. Said out loud, because it is a substitution.
4117                let mut candidates = peeked
4118                    .as_ref()
4119                    .map(|p| self.root_candidates(p).0)
4120                    .unwrap_or_default();
4121                if !candidates.is_empty() {
4122                    let chosen = candidates.remove(0);
4123                    self.record(Diagnostic::warning(
4124                        part,
4125                        format!(
4126                            "`{qualified_type}` is not registered in this build; reading it as \
4127                             `{}` instead, from the root element and its namespace",
4128                            chosen.qualified_type.unwrap_or(chosen.name)
4129                        ),
4130                    ));
4131                    qualified_type = chosen.qualified_type.unwrap_or(chosen.name).to_string();
4132                    content_type = chosen.content_type.unwrap_or_default().to_string();
4133                    info = Some(chosen);
4134                    alternatives = candidates;
4135                } else {
4136                    self.record(Diagnostic::note(
4137                        part,
4138                        format!("`{qualified_type}` is not registered; listed but not readable"),
4139                    ));
4140                }
4141            }
4142            // **A content type cannot name a development drop, so it cannot
4143            // settle the version when the document names one.**
4144            //
4145            // `application/x-resqml+xml;version=2.2;type=X` is what the
4146            // released 2.2 *and* `resqml 2.2_dev3` both write - the drop's
4147            // schemas declare the release's own `version="2.2"`. The index
4148            // therefore resolves to the release whatever the part holds, and
4149            // the branch above never runs because the lookup *succeeded*.
4150            //
4151            // The document knows better: its `schemaVersion` is the only place
4152            // a drop can be named at all. So when it names one and the index
4153            // answered with a release, the candidates decide instead - the same
4154            // "the document beats the index" rule this reader already applies
4155            // to the *type*, extended to the version for the one case where the
4156            // index has no way to express what the document said.
4157            //
4158            // Only the release moves: a candidate naming another **concept** is
4159            // refused, because that is the index's job and it is not in doubt.
4160            if let Some(peeked_header) = peeked.as_ref()
4161                && let Some(declared) = peeked_header.schema_version.as_deref()
4162                && naming::is_prerelease(declared)
4163                && let Some(held) = info
4164                && !naming::is_prerelease(held.version)
4165            {
4166                let mut candidates = self.root_candidates(peeked_header).0;
4167                candidates.retain(|c| c.type_key == held.type_key);
4168                if !candidates.is_empty() {
4169                    let chosen = candidates.remove(0);
4170                    if !std::ptr::eq(chosen, held) {
4171                        self.record(Diagnostic::note(
4172                            part,
4173                            format!(
4174                                "declared `{}` in [Content_Types].xml, which names no development drop; its own schemaVersion says `{declared}`, so it is read as `{}` - the document is the only place a drop can be named",
4175                                content_type,
4176                                chosen.qualified_type.unwrap_or(chosen.name)
4177                            ),
4178                        ));
4179                        qualified_type = chosen.qualified_type.unwrap_or(chosen.name).to_string();
4180                        content_type = chosen.content_type.unwrap_or_default().to_string();
4181                        info = Some(chosen);
4182                    }
4183                    alternatives = candidates;
4184                }
4185            }
4186            let Some(uuid) = self.uuid_of(part, peeked.as_ref()) else {
4187                self.record(Diagnostic::warning(
4188                    part,
4189                    format!(
4190                        "declared as `{qualified_type}` but carries no uuid - none in the part name, \
4191                         none on the root element; it cannot be addressed, so it is not listed"
4192                    ),
4193                ));
4194                return None;
4195            };
4196            return Some(PartEntry {
4197                summary: ObjectSummary::new(
4198                    identifiers::EtpUri::object(self.dataspace_path(), &qualified_type, &uuid),
4199                    uuid,
4200                    qualified_type,
4201                )
4202                .with_content_type(content_type)
4203                .with_locator(part.to_string()),
4204                info,
4205                slot: String::new(),
4206                object_version: normal_version(peeked.as_ref().and_then(|p| p.object_version.as_deref())),
4207                alternatives,
4208                refusal,
4209            });
4210        }
4211
4212        // 2 - the root element, when the index is absent, unusable, or wrong.
4213        //
4214        // Every way out of this branch is a part that will not appear in the
4215        // inventory, so every way out says why. An invisible part is worse than
4216        // an unreadable one: the second can be repaired, the first is not even
4217        // known to be there.
4218        let Some(peeked) = peeked else {
4219            self.record(Diagnostic::note(
4220                part,
4221                match &declared {
4222                    Some(ct) => format!("declared `{ct}`, but nothing readable was found in it"),
4223                    None => "no content type and no readable root element; not listed".to_string(),
4224                },
4225            ));
4226            return None;
4227        };
4228        let Some(uuid) = self.uuid_of(part, Some(&peeked)) else {
4229            self.record(Diagnostic::note(
4230                part,
4231                format!(
4232                    "root element `{}` carries no uuid, and the part name holds none either; not listed",
4233                    peeked.root_name
4234                ),
4235            ));
4236            return None;
4237        };
4238        // **The document declares a version the registry knows and does not
4239        // hold: refuse it by name, before any candidate is offered.** Of the
4240        // candidates `root_candidates` returns, none is this version - the
4241        // best of them would be another release of the package, read with
4242        // whatever fields the two share.
4243        if let Some((package, why)) = self.absent_declaration(&peeked) {
4244            let release = peeked
4245                .schema_version
4246                .as_deref()
4247                .map(naming::release_of)
4248                .unwrap_or_default();
4249            let qualified_type = format!(
4250                "{package}{}.{}",
4251                naming::compact_version(release),
4252                peeked.root_name
4253            );
4254            self.record(Diagnostic::warning(
4255                part,
4256                format!("`{qualified_type}` cannot be read: {why}; the part is listed but not read"),
4257            ));
4258            let content_type = declared.clone().unwrap_or_else(|| {
4259                format!(
4260                    "application/x-{package}+xml;version={release};type={}",
4261                    peeked.root_name
4262                )
4263            });
4264            return Some(PartEntry {
4265                summary: ObjectSummary::new(
4266                    identifiers::EtpUri::object(self.dataspace_path(), &qualified_type, &uuid),
4267                    uuid,
4268                    qualified_type,
4269                )
4270                .with_content_type(content_type)
4271                .with_locator(part.to_string()),
4272                info: None,
4273                slot: String::new(),
4274                object_version: normal_version(peeked.object_version.as_deref()),
4275                alternatives: Vec::new(),
4276                refusal: Some(why),
4277            });
4278        }
4279        let (mut candidates, confident) = self.root_candidates(&peeked);
4280        let Some(info) = (!candidates.is_empty()).then(|| candidates.remove(0)) else {
4281            self.record(Diagnostic::warning(
4282                part,
4283                match &declared {
4284                    Some(ct) => format!(
4285                        "declared `{ct}`, which this build cannot parse, and its root element \
4286                         `{}` names no type it knows; not listed",
4287                        peeked.root_name
4288                    ),
4289                    None => format!(
4290                        "not declared in [Content_Types].xml, and its root element `{}` names no \
4291                         type this build knows; not listed",
4292                        peeked.root_name
4293                    ),
4294                },
4295            ));
4296            return None;
4297        };
4298        let Some(qualified_type) = info.qualified_type.map(str::to_string) else {
4299            self.record(Diagnostic::warning(
4300                part,
4301                format!(
4302                    "root element `{}` resolves to a type with no qualified name; not listed",
4303                    peeked.root_name
4304                ),
4305            ));
4306            return None;
4307        };
4308        self.record(Diagnostic::warning(
4309            part,
4310            match &declared {
4311                Some(ct) => format!(
4312                    "declared as `{ct}`, but its root element is `{}`; the root element wins",
4313                    peeked.root_name
4314                ),
4315                None => format!(
4316                    "not declared in [Content_Types].xml; typed `{qualified_type}` from its root element"
4317                ),
4318            },
4319        ));
4320        // The namespace named the package and `schemaVersion` did not settle
4321        // the release, so more than one is still possible. Said now, because a
4322        // reader that silently picked one and got a plausible object out of it
4323        // is the failure this whole path exists to avoid.
4324        if !candidates.is_empty() && !confident {
4325            self.record(Diagnostic::note(
4326                part,
4327                format!(
4328                    "{} does not identify a single version{}; read as `{qualified_type}`, with {} \
4329                     tried in turn if it will not parse",
4330                    match &peeked.schema_version {
4331                        Some(version) => format!("`schemaVersion=\"{version}\"`"),
4332                        None => "no `schemaVersion`".to_string(),
4333                    },
4334                    match &peeked.namespace {
4335                        Some(_) => " - one namespace covers every version of a package",
4336                        None => "",
4337                    },
4338                    candidates
4339                        .iter()
4340                        .map(|info| info.qualified_type.unwrap_or(info.name))
4341                        .collect::<Vec<_>>()
4342                        .join(", ")
4343                ),
4344            ));
4345        }
4346        Some(PartEntry {
4347            summary: ObjectSummary::new(
4348                identifiers::EtpUri::object(self.dataspace_path(), &qualified_type, &uuid),
4349                uuid,
4350                qualified_type,
4351            )
4352            .with_content_type(info.content_type.unwrap_or_default().to_string())
4353            .with_locator(part.to_string()),
4354            info: Some(info),
4355            slot: String::new(),
4356            object_version: normal_version(peeked.object_version.as_deref()),
4357            alternatives: candidates,
4358            refusal: None,
4359        })
4360    }
4361
4362    /// The registered type whose element is `root_name`, in the version the
4363    /// document declares. Both spellings of the root are accepted, because both
4364    /// occur: the reference package writes `obj_DiscreteProperty` where the
4365    /// schema says `DiscreteProperty`.
4366    /// Which registered type a root element is, and what else it could be.
4367    ///
4368    /// # Why the namespace is not enough, and neither is the name
4369    ///
4370    /// Energistics gave **every version of a package the same namespace**:
4371    /// `resqmlv2` is RESQML 2.0.1 *and* RESQML 2.2, `witsmlv2` is WITSML 2.0
4372    /// *and* 2.1. So the namespace answers "which package" - which is worth
4373    /// something, since `Activity` exists in both `resqml20` and `eml23` with
4374    /// different namespaces - and says nothing at all about the version.
4375    ///
4376    /// The version comes from `schemaVersion`, and that is where it gets thin:
4377    /// the attribute is optional in practice, producers write `2.0` where the
4378    /// release is 2.0.1, and a file may name a version this build does not
4379    /// have. Until 02/08 an unusable `schemaVersion` fell through to
4380    /// `candidates.first()` - registry insertion order, silently. A part read
4381    /// as the wrong version does not fail: it produces a plausible object with
4382    /// the fields the two versions share and nothing else, which is the one
4383    /// outcome this reader is built to avoid.
4384    ///
4385    /// So this returns **every** candidate, best first, and the caller reads
4386    /// down the list until one parses. The order is:
4387    ///
4388    /// 1. same namespace as the document, when it declared one;
4389    /// 2. `schemaVersion` matching exactly - on the release or on the schema's
4390    ///    own two-digit form;
4391    /// 3. `schemaVersion` compatible by prefix, so `2.0` finds 2.0.1;
4392    /// 4. the `obj_` prefix, which only the older releases write - a **positive**
4393    ///    signal when present and no signal at all when absent, because
4394    ///    `ALWYN-RESQML.epc` is a 2.0.1 package whose roots do not carry it;
4395    /// 5. newest first, as a last resort that is at least deterministic.
4396    ///
4397    /// Returns the candidates and whether the first one was **identified**
4398    /// rather than merely preferred - an exact `schemaVersion` match, or a
4399    /// single candidate. The rest are insurance, and silent when the choice
4400    /// was confident: reporting an ambiguity that the document settled would
4401    /// bury the ones it did not.
4402    fn root_candidates(&self, peeked: &header::PeekedHeader) -> (Vec<&'static TypeInfo>, bool) {
4403        let mut candidates: Vec<&'static TypeInfo> = self
4404            .registry
4405            .types_by_key(&peeked.root_name)
4406            .into_iter()
4407            .filter(|info| info.is_top_level)
4408            .collect();
4409
4410        // The package, when the document said which. Applied as a filter and
4411        // not as a preference: a `resqml20.Activity` is not a worse
4412        // `eml23.Activity`, it is a different object.
4413        if let Some(namespace) = &peeked.namespace {
4414            let same: Vec<&'static TypeInfo> = candidates
4415                .iter()
4416                .filter(|info| info.xml_ns.uri == namespace)
4417                .copied()
4418                .collect();
4419            if !same.is_empty() {
4420                candidates = same;
4421            }
4422        }
4423
4424        // The declared version, trimmed to its digits: the 2016 Geosiris
4425        // editor wrote `Resqml V2.0`, and a spelling the comparison cannot
4426        // read must not throw the *whole* declaration away - `V2.0` still
4427        // says 2.0. Found by the BRT acceptance packages: with the raw
4428        // string unusable, every candidate ranked equal, "newest first"
4429        // picked 2.2, and a 2.0.1 part read as a plausible 2.2 object whose
4430        // reference was empty - the exact outcome this ladder exists to
4431        // avoid.
4432        let declared = peeked.schema_version.as_deref();
4433        // Split into the release and the development drop, because those are
4434        // two different questions. `naming::release_of` also drops a leading
4435        // label - the 2016 Geosiris editor wrote `Resqml V2.0`, and a spelling
4436        // the comparison cannot read must not throw the *whole* declaration
4437        // away. Found by the BRT acceptance packages: with the raw string
4438        // unusable every candidate ranked equal, "newest first" picked 2.2,
4439        // and a 2.0.1 part read as a plausible 2.2 object whose reference was
4440        // empty - the exact outcome this ladder exists to avoid.
4441        let declared_release = declared.map(naming::release_of);
4442        let declared_drop = declared.and_then(|v| naming::split_prerelease(v).1);
4443        let obj_prefixed = naming::has_obj_prefix(&peeked.root_name);
4444        let rank = |info: &&'static TypeInfo| -> u8 {
4445            match declared_release {
4446                Some(version) => {
4447                    // Compared on releases: `2.2_dev3` *is* a 2.2, and which
4448                    // of the two crates it is comes next, not here.
4449                    let by_dir = naming::release_of(info.version);
4450                    let by_wire = naming::release_of(info.schema_version);
4451                    if naming::version_eq(by_wire, version) || naming::version_eq(by_dir, version) {
4452                        0
4453                    } else if version_compatible(by_dir, version) || version_compatible(by_wire, version) {
4454                        1
4455                    } else if obj_prefixed && naming::has_obj_prefix(info.object_type) {
4456                        // The declared version helped nobody - the `obj_`
4457                        // prefix is still a positive signal, same as when
4458                        // no version was declared at all.
4459                        2
4460                    } else {
4461                        3
4462                    }
4463                }
4464                // No version to go on: the `obj_` prefix is the only other
4465                // thing the document said about its age.
4466                None if obj_prefixed && naming::has_obj_prefix(info.object_type) => 0,
4467                None => 2,
4468            }
4469        };
4470        // **The release outranks its development drops, and a named drop
4471        // outranks the release.**
4472        //
4473        // Energistics ships `resqml 2.2_dev3` beside `resqml 2.2`, and the drop
4474        // declares the release's own `version="2.2"` - so on the wire the two
4475        // are indistinguishable and *both* rank 0 above. A document saying
4476        // `2.2` is overwhelmingly a released 2.2, so that is tried first and
4477        // the drop is the fallback the strict pass falls to; a document that
4478        // names the drop gets the drop first. Neither is a guess: whichever is
4479        // tried second only ever gets a turn because the first *refused*.
4480        //
4481        // With no drop registered every candidate scores 0 here, which is why
4482        // adding one changes the order of nothing else.
4483        let drop_rank = |info: &&'static TypeInfo| -> u8 {
4484            match (naming::split_prerelease(info.version).1, declared_drop) {
4485                (None, None) => 0,
4486                (Some(held), Some(said)) if naming::prerelease_eq(held, said) => 0,
4487                // Asked for a drop and offered the release it became, or asked
4488                // for the release and offered a drop of it: the near miss.
4489                (None, Some(_)) | (Some(_), None) => 1,
4490                // A different drop of the same release: further still.
4491                (Some(_), Some(_)) => 2,
4492            }
4493        };
4494        candidates.sort_by(|a, b| {
4495            rank(a)
4496                .cmp(&rank(b))
4497                .then_with(|| drop_rank(a).cmp(&drop_rank(b)))
4498                // Newest first among equals: deterministic, and the better bet
4499                // for a file that did not say.
4500                .then_with(|| b.version.cmp(a.version))
4501        });
4502        let confident = candidates.len() <= 1
4503            || (declared.is_some() && candidates.first().map(|i| (rank(i), drop_rank(i))) == Some((0, 0)));
4504        (candidates, confident)
4505    }
4506
4507    /// The package and the refusal, when the document's own namespace and
4508    /// `schemaVersion` name a version the registry was told is absent.
4509    ///
4510    /// The namespace answers "which package" (`resqmlv2` is RESQML, whatever
4511    /// the release) and `schemaVersion` answers the release; both have to be
4512    /// there, since a document that says one only does not name a version.
4513    fn absent_declaration(&self, peeked: &header::PeekedHeader) -> Option<(String, String)> {
4514        let uri = peeked.namespace.as_deref()?;
4515        let declared = peeked.schema_version.as_deref()?;
4516        let package = energyml_core::ns::well_known_namespaces()
4517            .iter()
4518            .find(|known| known.uri == uri)?
4519            .prefix;
4520        self.registry
4521            .absent(package, declared)
4522            .map(|why| (package.to_string(), why.to_string()))
4523    }
4524
4525    /// The best candidate alone, for the callers that only need a name.
4526    fn type_of_root(&self, root_name: &str, schema_version: Option<&str>) -> Option<&'static TypeInfo> {
4527        self.root_candidates(&header::PeekedHeader {
4528            root_name: root_name.to_string(),
4529            schema_version: schema_version.map(str::to_string),
4530            ..Default::default()
4531        })
4532        .0
4533        .first()
4534        .copied()
4535    }
4536
4537    /// Whether the root element names a **different** type from the one the
4538    /// index declares, in a way this build can adjudicate.
4539    ///
4540    /// Both spellings are one name - the reference package writes
4541    /// `obj_DiscreteProperty` in the document and `DiscreteProperty` in the
4542    /// content type - and the comparison ignores case, because a difference of
4543    /// case is a producer's habit rather than a claim about the type.
4544    ///
4545    /// A root element this build does not know is **not** a contradiction: the
4546    /// declared type would be thrown away for nothing and the part would drop
4547    /// out of the inventory entirely. Only a disagreement where the document's
4548    /// own word can be honoured counts, so the worst case remains "listed under
4549    /// the type it declares", never "lost".
4550    fn root_contradicts(&self, peeked: Option<&header::PeekedHeader>, declared_type: &str) -> bool {
4551        let Some(peeked) = peeked else {
4552            return false;
4553        };
4554        let root = naming::strip_obj_prefix(&peeked.root_name);
4555        if root.eq_ignore_ascii_case(naming::strip_obj_prefix(declared_type)) {
4556            return false;
4557        }
4558        self.type_of_root(&peeked.root_name, peeked.schema_version.as_deref())
4559            .is_some_and(|info| info.qualified_type.is_some())
4560    }
4561
4562    /// The uuid of a part: from its name if it is spelled the way the
4563    /// specification asks (`<objectType>_<uuid>.xml`, §3.2.1), from its root
4564    /// element otherwise.
4565    fn uuid_of(&self, part: &str, peeked: Option<&header::PeekedHeader>) -> Option<String> {
4566        if let Some(uuid) = uuid_in_part_name(part) {
4567            return Some(uuid);
4568        }
4569        let peeked = peeked?;
4570        if peeked.uuid.is_some() {
4571            self.record(Diagnostic::note(
4572                part,
4573                "part name does not end in `_<uuid>.xml`; the uuid was read from the object",
4574            ));
4575        }
4576        peeked.uuid.clone()
4577    }
4578}
4579
4580/// A last resort, not a save.
4581///
4582/// `Drop` cannot return a `Result`, so it cannot write: a failure would have
4583/// nowhere to go. What it can do is refuse to let unsaved work disappear in
4584/// silence.
4585impl Drop for EpcStore {
4586    fn drop(&mut self) {
4587        if !self.closed && self.policy == SavePolicy::OnClose && self.overlay.is_dirty() {
4588            eprintln!(
4589                "energyml-storage: `{}` was dropped with unsaved changes and the OnClose policy. \
4590                 Drop cannot report a failed write, so nothing was written - call `close()`.",
4591                self.name
4592            );
4593        }
4594    }
4595}
4596
4597/// Resolving proxies is something only the package can do, so the store is the
4598/// resolver.
4599impl ProxyResolver for EpcStore {
4600    fn resolve(&self, proxy_uuid: &str) -> Option<crate::array::ProxyTarget> {
4601        self.external_target_for(proxy_uuid, None)
4602    }
4603}
4604
4605/// The asynchronous face of the same store: one line per method.
4606///
4607/// Nothing happens here that does not happen synchronously above. That is the
4608/// point of option D - the wrappers are mechanical, so the two surfaces cannot
4609/// drift apart.
4610#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
4611#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
4612impl EnergymlStorage for EpcStore {
4613    fn store_kind(&self) -> &'static str {
4614        "EPC"
4615    }
4616
4617    async fn dataspaces(&self) -> Result<Vec<Dataspace>> {
4618        Ok(vec![self.dataspace()])
4619    }
4620
4621    async fn resources(&self) -> Result<Vec<ObjectSummary>> {
4622        Ok(EpcStore::resources(self))
4623    }
4624
4625    async fn contents(&self) -> Result<Contents> {
4626        EpcStore::contents(self)
4627    }
4628
4629    async fn headers(&self, uris: &[String]) -> Vec<Result<ObjectHeader>> {
4630        EpcStore::headers(self, uris)
4631    }
4632
4633    async fn get_objects(&self, uris: &[String]) -> Vec<Result<Box<dyn DynObject>>> {
4634        EpcStore::get_objects(self, uris)
4635    }
4636
4637    async fn supported_types(&self) -> Vec<&'static TypeInfo> {
4638        EpcStore::supported_types(self)
4639    }
4640
4641    fn arrays_of(&self, object: &dyn Reflect) -> Vec<ArrayLocation> {
4642        EpcStore::arrays_of(self, object)
4643    }
4644
4645    async fn array_metadata(&self, location: &ArrayLocation) -> Result<ArrayMetadata> {
4646        EpcStore::array_metadata(self, location)
4647    }
4648
4649    async fn read_array(&self, location: &ArrayLocation) -> Result<ArrayData> {
4650        EpcStore::read_array(self, location)
4651    }
4652
4653    fn diagnostics(&self) -> Vec<Diagnostic> {
4654        EpcStore::diagnostics(self)
4655    }
4656
4657    /// An EPC writes, opens transactions, keeps the XML of each part and says
4658    /// what changed; it is one dataspace, writes whole arrays only, keeps
4659    /// neither deletions nor ETP's store times, and states the kind of the edges
4660    /// of the MLs the rules of [`crate::kinds`] cover - computed from the
4661    /// registered types and the policy, never listed twice.
4662    fn capabilities(&self) -> crate::capabilities::Capabilities {
4663        crate::capabilities::Capabilities {
4664            writes: true,
4665            transactions: true,
4666            raw_formats: vec![crate::raw::RawFormat::Xml],
4667            changes: true,
4668            edge_kinds: true,
4669            edge_kind_coverage: crate::capabilities::EdgeKindCoverage::Mls(
4670                crate::kinds::StandardRules::new().covered_prefixes(&self.registry),
4671            ),
4672            // Reading the objects held by value: listed, linked, read. Not pruned.
4673            contained_objects: self.knows_containment(),
4674            prune_contained: false,
4675            store_times: crate::capabilities::StoreTimes::NotKept,
4676            ..Default::default()
4677        }
4678    }
4679
4680    /// The EPC derives its edges from the objects, and judges their kind by the
4681    /// standard rules (RESQML, the WITSML examples and derivation; [`crate::kinds`]).
4682    fn kind_rules(&self) -> Option<std::sync::Arc<dyn crate::kinds::KindRules>> {
4683        Some(std::sync::Arc::new(crate::kinds::StandardRules::over(
4684            self.registry.clone(),
4685        )))
4686    }
4687
4688    async fn get_raw(
4689        &self,
4690        uris: &[String],
4691        format: crate::raw::RawFormat,
4692    ) -> Vec<Result<crate::raw::RawObject>> {
4693        self.raw_objects(uris, format)
4694    }
4695}
4696
4697impl LocalPackage for EpcStore {
4698    /// What a save would carry, not what the archive held.
4699    ///
4700    /// Three differences, and each one was a way for this list to lie: a part
4701    /// dropped in this session is gone, a file added in it is here, and neither
4702    /// is visible in `package.entries()`.
4703    fn attachments(&self) -> Vec<String> {
4704        // A set, not `entries.iter().any` per part: 16 250 parts against as many
4705        // objects was a quarter of a billion comparisons (02/10/2026).
4706        let objects: HashSet<&str> = self.entries.iter().map(|e| e.summary.locator.as_str()).collect();
4707        let mut out: Vec<String> = self
4708            .package
4709            .entries()
4710            .iter()
4711            .filter(|name| !is_package_infrastructure(name))
4712            .filter(|name| !objects.contains(name.as_str()))
4713            .filter(|name| !self.overlay.is_dropped(name))
4714            .cloned()
4715            .collect();
4716        for part in self.overlay.raw_parts() {
4717            let known = self.package.has(&part)
4718                || objects.contains(part.as_str())
4719                || is_package_infrastructure(&part);
4720            if !known {
4721                out.push(part);
4722            }
4723        }
4724        out.sort();
4725        out.dedup();
4726        out
4727    }
4728
4729    fn read_attachment(&self, part: &str) -> Result<Vec<u8>> {
4730        // `raw_part`, not `package.read`: a file added in this session is not in
4731        // the archive at all, and one that was replaced is not what the archive
4732        // holds. Reading the archive here would answer about a package that no
4733        // longer exists. And a part this session removed is gone for the
4734        // reader too - the archive still holds its bytes until the save.
4735        if self.overlay.raw(part).is_none() && self.overlay.is_dropped(part) {
4736            return Err(Error::Package(format!(
4737                "no part `{part}` in the package: it was removed"
4738            )));
4739        }
4740        self.raw_part(part)
4741    }
4742
4743    fn relationships(&self, part: &str) -> Result<Vec<Relationship>> {
4744        self.relationships_recorded(part)
4745    }
4746}
4747
4748/// Whether a declared version is a prefix of a candidate's, or the reverse.
4749///
4750/// `schemaVersion="2.0"` on a RESQML 2.0.1 object is the schema's own rule -
4751/// *"the schema version is the first 2 digits of an ML version"* - so the two
4752/// have to meet, and they meet on the dotted components rather than on the
4753/// string: `2.1` must not match `2.10`.
4754fn version_compatible(candidate: &str, declared: &str) -> bool {
4755    let parts = |value: &str| -> Vec<String> {
4756        value
4757            .trim()
4758            .split('.')
4759            .map(|piece| piece.trim().to_string())
4760            .filter(|piece| !piece.is_empty())
4761            .collect()
4762    };
4763    let (a, b) = (parts(candidate), parts(declared));
4764    if a.is_empty() || b.is_empty() {
4765        return false;
4766    }
4767    a.iter().zip(b.iter()).all(|(x, y)| x == y)
4768}
4769
4770/// Absent data, or an absent capability - and never the first when it is the
4771/// second.
4772///
4773/// A build with no HDF5 cannot read the array whether or not the file is on the
4774/// disk, and *"no support for this format"* is the more useful of the two
4775/// answers: the other one sends someone hunting for a file that is sitting
4776/// beside the package. `no_backend` carries the reason the registry gave, when
4777/// it was ever asked; `backends.is_empty()` covers the case where the candidate
4778/// list was empty so nothing was asked at all.
4779///
4780/// `detail`, when given, is how hard [`EpcStore::search_array`] looked before
4781/// giving up - folded into the `FileMissing` error's own display string so a
4782/// caller that only prints the error, not the diagnostic log beside it, still
4783/// gets more than *file not found*. The reason itself stays `FileMissing`:
4784/// that is what a caller matches on to try another candidate or ask for the
4785/// file, and it must not depend on whether a search ever ran.
4786fn missing_or_unsupported(
4787    location: &ArrayLocation,
4788    backends: &crate::backend::Backends,
4789    no_backend: Option<Error>,
4790    detail: Option<&str>,
4791) -> Error {
4792    if let Some(reason) = no_backend {
4793        return reason;
4794    }
4795    if backends.is_empty() {
4796        return Error::ExternalData {
4797            location: location.field_path.clone(),
4798            reason: ExternalDataReason::FeatureNotEnabled {
4799                feature: "hdf5",
4800                mime: location.mime.clone(),
4801            },
4802        };
4803    }
4804    let location = match detail {
4805        Some(detail) => format!("{} - {detail}", location.field_path),
4806        None => location.field_path.clone(),
4807    };
4808    Error::ExternalData {
4809        location,
4810        reason: ExternalDataReason::FileMissing,
4811    }
4812}
4813
4814/// Parts that describe the package rather than belong to it.
4815fn is_package_infrastructure(name: &str) -> bool {
4816    name == "[Content_Types].xml"
4817        || name.ends_with(".rels")
4818        || name.contains("/_rels/")
4819        || name.starts_with("_rels/")
4820        || name.starts_with("docProps/")
4821        || name.ends_with('/')
4822}
4823
4824/// Whether a part is worth asking "is this an EnergyML object?".
4825fn is_candidate_part(name: &str) -> bool {
4826    !is_package_infrastructure(name) && name.to_ascii_lowercase().ends_with(".xml")
4827}
4828
4829/// `resqml20/obj_Activity_<uuid>.xml` → the uuid.
4830///
4831/// The name of an XML part is `<objectType>_<uuid>.xml` (EPC §3.2.1). A uuid
4832/// contains no underscore, so the last one separates the two.
4833fn uuid_in_part_name(part: &str) -> Option<String> {
4834    let file = part.rsplit('/').next()?;
4835    let stem = file.get(..file.len().checked_sub(4)?)?;
4836    let candidate = stem.rsplit('_').next()?;
4837    identifiers::is_uuid(candidate).then(|| candidate.to_string())
4838}
4839
4840/// The dataspace path a caller asked for, checked through the one door for
4841/// uris: it must read back, through [`identifiers::EtpUri::parse`], as exactly
4842/// that dataspace and nothing else. A name the grammar refuses (a lone quote,
4843/// say) or one that is empty is a named refusal, not a uri built by hand.
4844fn checked_dataspace(name: &str) -> Result<String> {
4845    let uri = identifiers::EtpUri::dataspace_uri(Some(name));
4846    let refuse = |reason: &str| Error::BadUri {
4847        uri: uri.clone(),
4848        reason: format!("`{name}` is not a usable dataspace name: {reason}"),
4849    };
4850    let parsed =
4851        identifiers::EtpUri::parse(&uri).map_err(|_| refuse("it does not fit the ETP 1.2 grammar"))?;
4852    if name.is_empty() || !parsed.is_dataspace() || parsed.dataspace.as_deref() != Some(name) {
4853        return Err(refuse("it must be a non-empty dataspace path"));
4854    }
4855    Ok(name.to_string())
4856}
4857
4858/// What an address names: the uuid, and - for a URI - the qualified type and the
4859/// `objectVersion` it spells (Identifier Specification v5.0 §3.4.2, p. 14). A
4860/// bare uuid names neither.
4861fn address_of(uri: &str) -> Result<Address> {
4862    if let Ok(parsed) = identifiers::EtpUri::parse(uri)
4863        && let Some(uuid) = parsed.uuid.clone()
4864    {
4865        return Ok(Address {
4866            uuid: uuid.to_ascii_lowercase(),
4867            qualified_type: parsed.qualified_type().map(|qualified| qualified.to_string()),
4868            version: normal_version(parsed.object_version.as_deref()),
4869        });
4870    }
4871    let trimmed = uri.trim();
4872    if identifiers::is_uuid(trimmed) {
4873        return Ok(Address {
4874            uuid: trimmed.to_ascii_lowercase(),
4875            qualified_type: None,
4876            version: None,
4877        });
4878    }
4879    Err(Error::BadUri {
4880        uri: uri.to_string(),
4881        reason: "expected an ETP data-object URI or a bare uuid".to_string(),
4882    })
4883}
4884
4885/// The uuid an address names - an ETP URI, or a bare uuid for the caller who
4886/// has one in hand.
4887fn uuid_in_uri(uri: &str) -> Result<String> {
4888    if let Ok(parsed) = identifiers::EtpUri::parse(uri)
4889        && let Some(uuid) = parsed.uuid
4890    {
4891        return Ok(uuid);
4892    }
4893    let trimmed = uri.trim();
4894    if identifiers::is_uuid(trimmed) {
4895        return Ok(trimmed.to_string());
4896    }
4897    Err(Error::BadUri {
4898        uri: uri.to_string(),
4899        reason: "expected an ETP data-object URI or a bare uuid".to_string(),
4900    })
4901}
4902
4903fn local_name(raw: &[u8]) -> String {
4904    String::from_utf8_lossy(raw.rsplit(|b| *b == b':').next().unwrap_or_default()).into_owned()
4905}
4906
4907#[cfg(test)]
4908mod tests {
4909    use super::*;
4910
4911    #[test]
4912    fn a_missing_capability_never_comes_out_as_a_missing_file() {
4913        // The distinction this exists for, and the hour it costs when it is
4914        // lost: a build with no HDF5 reported `file not found` for arrays whose
4915        // `.h5` was sitting beside the package, which sends the reader after
4916        // the file, the paths and the `.rels` - everything except the build.
4917        let location = ArrayLocation {
4918            field_path: "NodeMd.Values.ExternalDataArrayPart[0]".into(),
4919            path_in_file: "/resqmlv2/x/node_md".into(),
4920            mime: Some("application/x-hdf5".into()),
4921            ..Default::default()
4922        };
4923
4924        // Nothing compiled in, and no candidate was ever asked.
4925        let empty = crate::backend::Backends::none();
4926        assert!(empty.is_empty());
4927        assert!(matches!(
4928            missing_or_unsupported(&location, &empty, None, None),
4929            Error::ExternalData {
4930                reason: ExternalDataReason::FeatureNotEnabled { .. },
4931                ..
4932            }
4933        ));
4934
4935        // A candidate was asked and the registry said it handles nothing of
4936        // this kind: that reason is better than anything invented here.
4937        let refused = Error::ExternalData {
4938            location: "somewhere".into(),
4939            reason: ExternalDataReason::FeatureNotEnabled {
4940                feature: "hdf5",
4941                mime: Some("application/x-hdf5".into()),
4942            },
4943        };
4944        assert!(matches!(
4945            missing_or_unsupported(&location, &empty, Some(refused), None),
4946            Error::ExternalData {
4947                reason: ExternalDataReason::FeatureNotEnabled { .. },
4948                ..
4949            }
4950        ));
4951
4952        // A backend exists and every candidate was opened and did not hold the
4953        // dataset: that one really is a missing file, and must stay one. Only
4954        // checkable in a build that has a backend - which is the point.
4955        #[cfg(feature = "hdf5")]
4956        {
4957            let stocked = crate::backend::Backends::compiled_in();
4958            assert!(!stocked.is_empty());
4959            assert!(matches!(
4960                missing_or_unsupported(&location, &stocked, None, None),
4961                Error::ExternalData {
4962                    reason: ExternalDataReason::FileMissing,
4963                    ..
4964                }
4965            ));
4966        }
4967    }
4968
4969    #[test]
4970    fn a_part_name_yields_its_uuid() {
4971        assert_eq!(
4972            uuid_in_part_name("resqml20/obj_Activity_f0d2a4ec-1234-4f22-9d1a-000000000001.xml").as_deref(),
4973            Some("f0d2a4ec-1234-4f22-9d1a-000000000001")
4974        );
4975        assert_eq!(
4976            uuid_in_part_name("f0d2a4ec-1234-4f22-9d1a-000000000001.xml").as_deref(),
4977            Some("f0d2a4ec-1234-4f22-9d1a-000000000001")
4978        );
4979        assert_eq!(uuid_in_part_name("docProps/core.xml"), None);
4980    }
4981
4982    #[test]
4983    fn an_address_may_be_a_uri_or_a_uuid() {
4984        let uuid = "f0d2a4ec-1234-4f22-9d1a-000000000001";
4985        assert_eq!(
4986            uuid_in_uri(&format!("eml:///resqml20.obj_Activity({uuid})")).unwrap(),
4987            uuid
4988        );
4989        assert_eq!(uuid_in_uri(uuid).unwrap(), uuid);
4990        assert!(uuid_in_uri("not an address").is_err());
4991    }
4992
4993    #[test]
4994    fn infrastructure_is_not_an_object() {
4995        assert!(!is_candidate_part("[Content_Types].xml"));
4996        assert!(!is_candidate_part("_rels/.rels"));
4997        assert!(!is_candidate_part("resqml20/_rels/obj_Activity_x.xml.rels"));
4998        assert!(!is_candidate_part("docProps/core.xml"));
4999        assert!(!is_candidate_part("media/report.pdf"));
5000        assert!(is_candidate_part("resqml20/obj_Activity_x.xml"));
5001    }
5002}