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}