use std::collections::{BTreeMap, HashSet};
use std::io::{BufReader, Read};
use energyml_core::identifiers::EtpUri;
use energyml_core::ns::{Namespace, XSI};
use energyml_core::reflect::TypeInfo;
use energyml_core::registry::DynObject;
use quick_xml::events::Event;
use super::contents::ContainedEntry;
use super::{EpcStore, Live, header};
use crate::error::{Diagnostic, Error, Result};
use crate::graph::Edge;
use crate::kinds::{Judged, KindRules, StandardRules};
use crate::raw::{RawFormat, RawObject};
use crate::resource::{ObjectHeader, ObjectSummary};
use crate::storage::Contents;
const STORE: &str = "EPC";
type Declarations = Vec<(String, String)>;
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub(crate) struct Scope {
pub declarations: Declarations,
pub schema_version: Option<String>,
}
fn unescape(raw: &[u8]) -> String {
let text = String::from_utf8_lossy(raw);
quick_xml::escape::unescape(&text)
.map(|unescaped| unescaped.into_owned())
.unwrap_or_else(|_| text.into_owned())
}
pub(crate) fn scopes_at(part: &[u8], entries: &[&ContainedEntry]) -> Vec<Option<Scope>> {
let mut out: Vec<Option<Scope>> = vec![None; entries.len()];
let mut order: Vec<usize> = (0..entries.len()).collect();
order.sort_by_key(|&at| entries[at].span.start);
let mut reader = quick_xml::Reader::from_reader(part);
reader.config_mut().trim_text(false);
let mut buf = Vec::new();
let mut frames: Vec<Declarations> = Vec::new();
let mut root_schema: Option<String> = None;
let mut seen_root = false;
let mut next = 0;
while next < order.len() {
let before = usize::try_from(reader.buffer_position()).unwrap_or(usize::MAX);
while next < order.len() && entries[order[next]].span.start <= before {
if entries[order[next]].span.start == before {
out[order[next]] = Some(Scope {
declarations: frames.iter().flatten().cloned().collect(),
schema_version: root_schema.clone(),
});
}
next += 1;
}
if next == order.len() {
break;
}
buf.clear();
match reader.read_event_into(&mut buf) {
Ok(Event::Start(start)) => {
let mut own = Declarations::new();
for attribute in start.attributes().flatten() {
let key = attribute.key.as_ref();
let value = unescape(&attribute.value);
if key == b"xmlns" {
own.push((String::new(), value));
} else if let Some(bound) = key.strip_prefix(b"xmlns:") {
own.push((String::from_utf8_lossy(bound).into_owned(), value));
} else if !seen_root && key == b"schemaVersion" {
root_schema = Some(value);
}
}
seen_root = true;
frames.push(own);
}
Ok(Event::End(_)) => {
frames.pop();
}
Ok(Event::Eof) | Err(_) => break,
Ok(_) => {}
}
}
out
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct Target {
pub element: &'static str,
pub namespace: Namespace,
pub type_name: &'static str,
pub keep_xsi_type: bool,
pub schema_version: &'static str,
}
impl Target {
pub(crate) fn of(info: &TypeInfo) -> Self {
Self {
element: info.root_element.unwrap_or(info.xml_name),
namespace: info.xml_ns,
type_name: info.object_type,
keep_xsi_type: info.root_element.is_some(),
schema_version: info.schema_version,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct Extracted {
pub bytes: Vec<u8>,
pub notes: Vec<String>,
}
struct RawAttr<'t> {
key: &'t str,
value: &'t str,
quote: char,
}
struct StartTag<'t> {
name: &'t str,
attrs: Vec<RawAttr<'t>>,
empty: bool,
head_len: usize,
}
fn parse_start_tag(text: &str) -> std::result::Result<StartTag<'_>, &'static str> {
let bytes = text.as_bytes();
let space = |b: u8| b.is_ascii_whitespace();
if bytes.first() != Some(&b'<') || matches!(bytes.get(1), None | Some(b'/' | b'!' | b'?')) {
return Err("the span does not start at a start tag");
}
let mut at = 1;
while bytes
.get(at)
.is_some_and(|b| !space(*b) && *b != b'>' && *b != b'/')
{
at += 1;
}
let name = &text[1..at];
let mut attrs = Vec::new();
loop {
while bytes.get(at).is_some_and(|b| space(*b)) {
at += 1;
}
match bytes.get(at).copied() {
None => return Err("the start tag is not closed"),
Some(b'>') => {
return Ok(StartTag {
name,
attrs,
empty: false,
head_len: at + 1,
});
}
Some(b'/') => {
return if bytes.get(at + 1) == Some(&b'>') {
Ok(StartTag {
name,
attrs,
empty: true,
head_len: at + 2,
})
} else {
Err("a `/` that does not close the start tag")
};
}
Some(_) => {
let key_start = at;
while bytes
.get(at)
.is_some_and(|b| !space(*b) && *b != b'=' && *b != b'>' && *b != b'/')
{
at += 1;
}
let key = &text[key_start..at];
while bytes.get(at).is_some_and(|b| space(*b)) {
at += 1;
}
if bytes.get(at) != Some(&b'=') {
return Err("an attribute without a value");
}
at += 1;
while bytes.get(at).is_some_and(|b| space(*b)) {
at += 1;
}
let quote = match bytes.get(at).copied() {
Some(q) if q == b'"' || q == b'\'' => q,
_ => return Err("an attribute value that is not quoted"),
};
at += 1;
let value_start = at;
while bytes.get(at).is_some_and(|b| *b != quote) {
at += 1;
}
if at >= bytes.len() {
return Err("an attribute value that is not closed");
}
attrs.push(RawAttr {
key,
value: &text[value_start..at],
quote: char::from(quote),
});
at += 1;
}
}
}
}
fn fail(entry: &ContainedEntry, why: impl std::fmt::Display) -> Error {
Error::Xml(format!(
"contained object `{}` in `{}`: {why}",
entry.summary.uri, entry.part
))
}
fn resolve<'d>(bound: &'d [(String, String)], prefix: &str) -> Option<&'d str> {
bound
.iter()
.rev()
.find(|(declared, _)| declared == prefix)
.map(|(_, uri)| uri.as_str())
}
fn prefix_for<'d>(bound: &'d [(String, String)], uri: &str) -> Option<&'d str> {
bound
.iter()
.rev()
.find(|(prefix, declared)| declared == uri && resolve(bound, prefix) == Some(uri))
.map(|(prefix, _)| prefix.as_str())
}
fn fresh_prefix(bound: &[(String, String)], wanted: &str) -> String {
let base = if wanted.is_empty() { "ns" } else { wanted };
if resolve(bound, base).is_none() {
return base.to_string();
}
(1..)
.map(|n| format!("{base}{n}"))
.find(|candidate| resolve(bound, candidate).is_none())
.unwrap_or_else(|| base.to_string())
}
fn escape_attr(value: &str) -> String {
value
.replace('&', "&")
.replace('<', "<")
.replace('"', """)
}
fn push_declaration(out: &mut String, prefix: &str, uri: &str) {
out.push_str(" xmlns");
if !prefix.is_empty() {
out.push(':');
out.push_str(prefix);
}
out.push_str("=\"");
out.push_str(&escape_attr(uri));
out.push('"');
}
pub(crate) fn extract(
part: &[u8],
entry: &ContainedEntry,
target: &Target,
scope: &Scope,
) -> Result<Extracted> {
let inner = part
.get(entry.span.clone())
.ok_or_else(|| fail(entry, "its span lies outside its part"))?;
let text = std::str::from_utf8(inner)
.map_err(|_| fail(entry, "its part is not UTF-8 (TODO(verify): UTF-16 packages)"))?;
let tag = parse_start_tag(text).map_err(|why| fail(entry, why))?;
let children = if tag.empty {
None
} else {
let close = text
.rfind("</")
.filter(|close| *close >= tag.head_len)
.ok_or_else(|| fail(entry, "its end tag is missing"))?;
let closing = text[close + 2..].trim_end().strip_suffix('>').map(str::trim_end);
if closing != Some(tag.name) {
return Err(fail(entry, "its end tag does not match its start tag"));
}
Some(&text[tag.head_len..close])
};
let own: Declarations = tag
.attrs
.iter()
.filter_map(|attr| {
if attr.key == "xmlns" {
Some((String::new(), unescape(attr.value.as_bytes())))
} else {
attr.key
.strip_prefix("xmlns:")
.map(|prefix| (prefix.to_string(), unescape(attr.value.as_bytes())))
}
})
.collect();
let mut bound: Declarations = scope.declarations.clone();
bound.extend(own.iter().cloned());
let mut carried: Declarations = Vec::new();
for (prefix, uri) in scope.declarations.iter().rev() {
if own.iter().any(|(declared, _)| declared == prefix)
|| carried.iter().any(|(declared, _)| declared == prefix)
{
continue;
}
carried.push((prefix.clone(), uri.clone()));
}
carried.reverse();
let mut added: Declarations = Vec::new();
let root_prefix = match prefix_for(&bound, target.namespace.uri) {
Some(prefix) => prefix.to_string(),
None => {
let prefix = fresh_prefix(&bound, target.namespace.prefix);
added.push((prefix.clone(), target.namespace.uri.to_string()));
bound.push((prefix.clone(), target.namespace.uri.to_string()));
prefix
}
};
let root = if root_prefix.is_empty() {
target.element.to_string()
} else {
format!("{root_prefix}:{}", target.element)
};
let is_xsi_type = |key: &str| {
key.split_once(':')
.is_some_and(|(prefix, local)| local == "type" && resolve(&bound, prefix) == Some(XSI.uri))
};
let had_type = tag.attrs.iter().any(|attr| is_xsi_type(attr.key));
let kept: Vec<&RawAttr<'_>> = tag
.attrs
.iter()
.filter(|attr| target.keep_xsi_type || !is_xsi_type(attr.key))
.collect();
let mut supplied: Vec<(String, String)> = Vec::new();
let mut notes = Vec::new();
if target.keep_xsi_type && !had_type {
let xsi = match prefix_for(&bound, XSI.uri).filter(|prefix| !prefix.is_empty()) {
Some(prefix) => prefix.to_string(),
None => {
let prefix = fresh_prefix(&bound, XSI.prefix);
added.push((prefix.clone(), XSI.uri.to_string()));
bound.push((prefix.clone(), XSI.uri.to_string()));
prefix
}
};
let value = if root_prefix.is_empty() {
target.type_name.to_string()
} else {
format!("{root_prefix}:{}", target.type_name)
};
supplied.push((format!("{xsi}:type"), value));
}
if !tag.attrs.iter().any(|attr| attr.key == "schemaVersion") {
let (version, whose) = match &scope.schema_version {
Some(version) => (version.clone(), "its container's"),
None => (target.schema_version.to_string(), "its type's"),
};
notes.push(format!(
"the element states no schemaVersion (required by the schema); {whose} `{version}` was written in the extract"
));
supplied.push(("schemaVersion".to_string(), version));
}
let mut out = String::with_capacity(text.len() + 256);
out.push('<');
out.push_str(&root);
for (prefix, uri) in carried.iter().chain(added.iter()) {
push_declaration(&mut out, prefix, uri);
}
for attr in kept {
out.push(' ');
out.push_str(attr.key);
out.push('=');
out.push(attr.quote);
out.push_str(attr.value);
out.push(attr.quote);
}
for (key, value) in &supplied {
out.push(' ');
out.push_str(key);
out.push_str("=\"");
out.push_str(&escape_attr(value));
out.push('"');
}
match children {
None => out.push_str("/>"),
Some(children) => {
out.push('>');
out.push_str(children);
out.push_str("</");
out.push_str(&root);
out.push('>');
}
}
Ok(Extracted {
bytes: out.into_bytes(),
notes,
})
}
pub(crate) fn group_by_part<'e>(
wanted: &[(usize, &'e ContainedEntry)],
) -> BTreeMap<&'e str, Vec<(usize, &'e ContainedEntry)>> {
let mut groups: BTreeMap<&str, Vec<(usize, &ContainedEntry)>> = BTreeMap::new();
for (at, entry) in wanted {
groups.entry(entry.part.as_str()).or_default().push((*at, *entry));
}
groups
}
pub(crate) fn per_part<T>(
entries: &[&ContainedEntry],
load: impl FnOnce() -> Result<Vec<u8>>,
each: impl Fn(&[u8], &ContainedEntry, &Scope) -> Result<T>,
) -> Vec<Result<T>> {
let bytes = match load() {
Ok(bytes) => bytes,
Err(error) => {
let why = error.to_string();
return entries
.iter()
.map(|entry| Err(fail(entry, format!("its part cannot be read: {why}"))))
.collect();
}
};
let scopes = scopes_at(&bytes, entries);
entries
.iter()
.zip(scopes)
.map(|(entry, scope)| match scope {
Some(scope) => each(&bytes, entry, &scope),
None => Err(fail(entry, "its span does not start at an element of its part")),
})
.collect()
}
pub(super) fn header_from_peek(summary: &ObjectSummary, peeked: header::PeekedHeader) -> ObjectHeader {
let mut header = ObjectHeader::new(summary.clone());
header.title = peeked.title;
header.originator = peeked.originator;
header.creation = peeked.creation;
header.last_update = peeked.last_update;
header.format = peeked.format;
header.object_version = peeked.object_version;
header.schema_version = peeked.schema_version;
header
}
pub(crate) fn object_version_in(uri: &str) -> Option<Option<&str>> {
let parsed = EtpUri::parse(uri).ok()?;
let Some(version) = parsed.object_version else {
return Some(None);
};
let marker = "version='";
let from = uri.find(marker)? + marker.len();
let tail = uri.get(from..)?;
if tail.starts_with(version.as_str()) && tail[version.len()..].starts_with('\'') {
Some(Some(&tail[..version.len()]))
} else {
None
}
}
impl EpcStore {
fn masked_parts(&self) -> HashSet<&str> {
if !self.overlay.is_dirty() {
return HashSet::new();
}
self.entries
.iter()
.filter(|entry| {
self.overlay.get(&entry.slot).is_some() || self.overlay.raw(&entry.summary.locator).is_some()
})
.map(|entry| entry.summary.locator.as_str())
.collect()
}
fn contained_index(&self) -> Result<&super::contents::ContainedIndex> {
Ok(self.contained())
}
#[doc(hidden)]
pub fn scans(&self) -> u64 {
self.contained
.get()
.map_or(0, |index| index.scanned_parts().len() as u64)
}
pub(super) fn knows_containment(&self) -> bool {
self.registry.relations().next().is_some()
}
pub(super) fn live_for(&self, uuid: &str, qualified_type: Option<&str>) -> Result<Vec<Live<'_>>> {
let mut live = self.live_of(uuid);
let part_answers = match qualified_type {
Some(wanted) => live.iter().any(|one| one.summary.qualified_type == wanted),
None => !live.is_empty(),
};
if part_answers {
return Ok(live);
}
let may_be_contained = match qualified_type {
Some(wanted) => self.registry.may_be_contained(wanted),
None => true,
};
if !may_be_contained {
return Ok(live);
}
let index = self.contained_index()?;
let masked = self.masked_parts();
let mut seen: HashSet<&str> = HashSet::new();
for entry in index.by_uuid(uuid) {
if masked.contains(entry.part.as_str()) || !seen.insert(entry.summary.uri.as_str()) {
continue;
}
let Some(version) = object_version_in(&entry.summary.uri) else {
self.record(Diagnostic::warning(
&entry.part,
format!(
"contained object `{}` is not addressable: its objectVersion cannot be read back from its uri",
entry.summary.uri
),
));
continue;
};
live.push(Live {
slot: &entry.summary.uri,
summary: &entry.summary,
version,
archive: None,
nested: Some(entry),
});
}
Ok(live)
}
pub(super) fn contained_entry(&self, uri: &str) -> Result<Option<&ContainedEntry>> {
Ok(self.locate(uri)?.nested)
}
pub(super) fn routed<T>(
&self,
uris: &[String],
single: impl Fn(&str) -> Result<T>,
group: impl Fn(&[&ContainedEntry]) -> Vec<Result<T>>,
) -> Vec<Result<T>> {
let mut out: Vec<Option<Result<T>>> = uris.iter().map(|_| None).collect();
let mut wanted: Vec<(usize, &ContainedEntry)> = Vec::new();
for (at, uri) in uris.iter().enumerate() {
match self.contained_entry(uri) {
Ok(Some(entry)) => wanted.push((at, entry)),
_ => out[at] = Some(single(uri)),
}
}
for (_, members) in group_by_part(&wanted) {
let entries: Vec<&ContainedEntry> = members.iter().map(|(_, entry)| *entry).collect();
for ((at, _), result) in members.iter().zip(group(&entries)) {
out[*at] = Some(result);
}
}
out.into_iter()
.zip(uris)
.map(|(result, uri)| result.unwrap_or_else(|| Err(Error::NotFound(uri.clone()))))
.collect()
}
fn content_info(&self, entry: &ContainedEntry) -> Result<&'static TypeInfo> {
self.registry
.by_qualified_type(&entry.summary.qualified_type)
.map(|found| found.info)
.ok_or_else(|| Error::UnknownType(entry.summary.qualified_type.clone()))
}
fn extract_one(
&self,
bytes: &[u8],
entry: &ContainedEntry,
scope: &Scope,
) -> Result<(&'static TypeInfo, Vec<u8>)> {
let info = self.content_info(entry)?;
let extracted = extract(bytes, entry, &Target::of(info), scope)?;
for note in &extracted.notes {
self.record(Diagnostic::warning(
&entry.part,
format!("contained object `{}`: {note}", entry.summary.uri),
));
}
Ok((info, extracted.bytes))
}
pub(super) fn contained_objects(&self, entries: &[&ContainedEntry]) -> Vec<Result<Box<dyn DynObject>>> {
let Some(first) = entries.first() else {
return Vec::new();
};
per_part(
entries,
|| self.raw_part(&first.part),
|bytes, entry, scope| {
let (info, document) = self.extract_one(bytes, entry, scope)?;
self.read_extracted(info, &document, entry)
},
)
}
fn read_extracted(
&self,
info: &'static TypeInfo,
document: &[u8],
entry: &ContainedEntry,
) -> Result<Box<dyn DynObject>> {
let mut first: Option<Error> = None;
for mode in super::read_passes(self.mode()) {
match self.registry.read_xml_as_named(info, document, *mode) {
Ok((object, departures)) => {
for item in departures.defaulted {
self.record(Diagnostic::warning(
&entry.part,
format!("contained object `{}`: {item}", entry.summary.uri),
));
}
for item in departures.repaired {
self.record(Diagnostic::warning(
&entry.part,
format!("contained object `{}`: {item}", entry.summary.uri),
));
}
return Ok(object);
}
Err(error) => {
if first.is_none() {
first = Some(error.into());
}
}
}
}
Err(first.unwrap_or_else(|| Error::UnknownType(entry.summary.content_type.clone())))
}
pub(super) fn contained_headers(&self, entries: &[&ContainedEntry]) -> Vec<Result<ObjectHeader>> {
let Some(first) = entries.first() else {
return Vec::new();
};
if let [only] = entries {
return vec![self.streamed_header(only)];
}
match self.raw_part(&first.part) {
Ok(bytes) => entries
.iter()
.map(|entry| match bytes.get(entry.span.clone()) {
Some(slice) => Ok(header_from_peek(&entry.summary, header::peek_reader(slice))),
None => Err(fail(entry, "its span lies outside its part")),
})
.collect(),
Err(error) => {
let why = error.to_string();
entries
.iter()
.map(|entry| Err(fail(entry, format!("its part cannot be read: {why}"))))
.collect()
}
}
}
fn streamed_header(&self, entry: &ContainedEntry) -> Result<ObjectHeader> {
let skip = entry.span.start as u64;
let peeked = self.package.with_part(&entry.part, |source| {
let skipped = std::io::copy(&mut (&mut *source).take(skip), &mut std::io::sink());
match skipped {
Ok(count) if count == skip => Ok(header::peek_reader(BufReader::new(source))),
Ok(_) => Err(fail(entry, "its span lies outside its part")),
Err(error) => Err(Error::Io(error)),
}
})??;
Ok(header_from_peek(&entry.summary, peeked))
}
pub(super) fn contained_raw(&self, entries: &[&ContainedEntry]) -> Vec<Result<RawObject>> {
let Some(first) = entries.first() else {
return Vec::new();
};
per_part(
entries,
|| self.raw_part(&first.part),
|bytes, entry, scope| {
let (_, document) = self.extract_one(bytes, entry, scope)?;
Ok(RawObject::new(
entry.summary.uri.clone(),
RawFormat::Xml,
document,
))
},
)
}
pub fn contents(&self) -> Result<Contents> {
let parts = EpcStore::resources(self);
if !parts
.iter()
.any(|part| self.registry.is_container(&part.qualified_type))
{
return Ok(Contents::default());
}
let index = self.contained_index()?;
let masked = self.masked_parts();
let held: HashSet<&str> = parts.iter().map(|part| part.uri.as_str()).collect();
let rules = StandardRules::over(self.registry.clone());
let mut seen_objects: HashSet<&str> = HashSet::new();
let mut seen_edges: HashSet<(&str, &str)> = HashSet::new();
let mut contents = Contents::default();
let listed = index.in_archive_order().map(|entry| (entry, true));
let shadowed = index.shadowed().iter().map(|entry| (entry, false));
for (entry, own) in listed.chain(shadowed) {
if masked.contains(entry.part.as_str()) {
continue;
}
let uri = entry.summary.uri.as_str();
if own && seen_objects.insert(uri) && !held.contains(uri) {
contents.objects.push(entry.summary.clone());
}
for container in &entry.summary.contained_in {
if !seen_edges.insert((uri, container.as_str())) {
continue;
}
let judged = match crate::kinds::qualified_type_of(container) {
Some(qualified_type) => rules.containment(&qualified_type),
None => Judged::unknown("the container of the edge is not an object URI"),
};
let mut edge = Edge::new(uri, container.as_str(), "").with_judgement(judged);
edge.by_value = true;
contents.edges.push(edge);
}
}
Ok(contents)
}
fn held_as_part(&self, summary: &ObjectSummary) -> bool {
self.live_of(&summary.uuid.to_ascii_lowercase())
.iter()
.any(|one| one.summary.qualified_type == summary.qualified_type)
}
pub(super) fn refuse_contained_put(&self, summary: &ObjectSummary) -> Result<()> {
if !self.registry.may_be_contained(&summary.qualified_type) || self.held_as_part(summary) {
return Ok(());
}
let index = self.contained_index()?;
let uuid = summary.uuid.to_ascii_lowercase();
if index
.by_uuid(&uuid)
.iter()
.any(|entry| entry.summary.qualified_type == summary.qualified_type)
{
return Err(Error::Unsupported {
operation: "writing an object that is held by value inside a container (as a part it would shadow that copy, as an update it would rewrite the container)",
store: STORE,
});
}
Ok(())
}
pub(super) fn refuse_contained_remove(&self, uri: &str) -> Result<()> {
let Ok(live) = self.locate(uri) else {
return Ok(());
};
if live.nested.is_some() {
return Err(Error::Unsupported {
operation: "deleting an object held by value inside a container (it would have to be unjoined and the container rewritten)",
store: STORE,
});
}
if !self.registry.is_container(&live.summary.qualified_type) {
return Ok(());
}
let index = self.contained_index()?;
let part = live.summary.locator.as_str();
let only_inside = index
.entries()
.iter()
.any(|entry| entry.part == part && !self.held_as_part(&entry.summary));
if only_inside {
return Err(Error::Unsupported {
operation: "deleting a container whose contained objects live only inside it",
store: STORE,
});
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use std::io::{Cursor, Write};
use std::sync::Arc;
use energyml_core::identity;
use energyml_core::registry::{ReadMode, Registry};
use super::*;
use crate::EdgeKind;
use crate::kinds::KindBasis;
const WITSML_NS: &str = "http://www.energistics.org/energyml/data/witsmlv2";
const EML_NS: &str = "http://www.energistics.org/energyml/data/commonv2";
const XSI_NS: &str = "http://www.w3.org/2001/XMLSchema-instance";
const LOG: &str = "5a1c0000-0000-4000-8000-000000000001";
const SET: &str = "5a1c0000-0000-4000-8000-000000000002";
const GAMMA: &str = "5a1c0000-0000-4000-8000-000000000003";
const DENSITY: &str = "5a1c0000-0000-4000-8000-000000000004";
const UNKNOWN: &str = "5a1c0000-0000-4000-8000-0000000000ff";
fn registry() -> Arc<Registry> {
Arc::new(energyml_schemas::all())
}
fn title_of(object: &dyn DynObject) -> Option<String> {
identity::title_of_reflect(object.as_reflect()).map(str::to_string)
}
fn citation(title: &str) -> String {
format!(
"<eml:Citation><eml:Title>{title}</eml:Title><eml:Originator>test</eml:Originator>\
<eml:Creation>2026-10-15T00:00:00Z</eml:Creation><eml:Format>test</eml:Format></eml:Citation>\
<eml:ActiveStatus>inactive</eml:ActiveStatus>"
)
}
fn channel(uuid: &str, title: &str, mnemonic: &str) -> String {
format!(
"<witsml:Channel uuid=\"{uuid}\" schemaVersion=\"2.1\">{}\
<witsml:Mnemonic>{mnemonic}</witsml:Mnemonic><witsml:GlobalMnemonic>{mnemonic}</witsml:GlobalMnemonic>\
<witsml:DataKind>double</witsml:DataKind><witsml:Uom>m</witsml:Uom></witsml:Channel>",
citation(title)
)
}
fn log_part() -> String {
format!(
"<witsml:Log xmlns:witsml=\"{WITSML_NS}\" xmlns:eml=\"{EML_NS}\" xmlns:xsi=\"{XSI_NS}\" \
uuid=\"{LOG}\" schemaVersion=\"2.1\">{}\
<witsml:ChannelSet uuid=\"{SET}\" schemaVersion=\"2.1\">{}{}{}</witsml:ChannelSet></witsml:Log>",
citation("Log 1"),
citation("Set 1"),
channel(GAMMA, "Gamma Ray", "GR"),
channel(DENSITY, "Bulk Density", "RHOB"),
)
}
fn span_of(xml: &str, open: &str, close: &str) -> std::ops::Range<usize> {
let start = xml.find(open).expect("the opening text is in the fixture");
let end = xml[start..]
.find(close)
.map(|at| start + at + close.len())
.expect("the closing text is in the fixture");
start..end
}
fn entry_in(
part: &str,
span: std::ops::Range<usize>,
qualified_type: &str,
uuid: &str,
element: &str,
) -> ContainedEntry {
ContainedEntry {
summary: ObjectSummary::new(format!("eml:///{qualified_type}({uuid})"), uuid, qualified_type)
.with_locator(part),
title: None,
part: part.to_string(),
span,
element: element.to_string(),
path: element.to_string(),
}
}
fn target(registry: &Registry, qualified_type: &str) -> Target {
Target::of(
registry
.by_qualified_type(qualified_type)
.expect("the type is registered")
.info,
)
}
fn extract_one(xml: &str, entry: &ContainedEntry, target: &Target) -> Extracted {
let scope = scopes_at(xml.as_bytes(), &[entry])
.pop()
.flatten()
.expect("the span starts at an element");
extract(xml.as_bytes(), entry, target, &scope).expect("the extraction succeeds")
}
fn undeclared_prefixes(xml: &str) -> Vec<String> {
let root_end = xml.find('>').expect("a root tag");
let root = &xml[..root_end];
let mut missing = Vec::new();
let mut rest = xml;
while let Some(at) = rest.find('<') {
rest = &rest[at + 1..];
let named = rest.strip_prefix('/').unwrap_or(rest);
let name: String = named
.chars()
.take_while(|c| !c.is_whitespace() && *c != '>' && *c != '/')
.collect();
if let Some((prefix, _)) = name.split_once(':')
&& !root.contains(&format!("xmlns:{prefix}=\""))
{
missing.push(prefix.to_string());
}
}
missing
}
#[test]
fn a_deep_channel_is_extracted_and_read_by_the_ordinary_reader() {
let registry = registry();
let xml = log_part();
let span = span_of(
&xml,
&format!("<witsml:Channel uuid=\"{DENSITY}\""),
"</witsml:Channel>",
);
let entry = entry_in("log.xml", span, "witsml21.Channel", DENSITY, "witsml:Channel");
let target = target(®istry, "witsml21.Channel");
let extracted = extract_one(&xml, &entry, &target);
assert!(extracted.notes.is_empty(), "{:?}", extracted.notes);
let info = registry
.by_qualified_type("witsml21.Channel")
.expect("registered")
.info;
let (object, _) = registry
.read_xml_as_named(info, &extracted.bytes, ReadMode::Tolerant)
.expect("the standalone document reads");
assert_eq!(identity::uuid_of_reflect(object.as_reflect()), Some(DENSITY));
assert_eq!(title_of(object.as_ref()).as_deref(), Some("Bulk Density"));
}
#[test]
fn a_prefix_declared_only_at_the_root_is_carried_to_the_extract() {
let registry = registry();
let xml = log_part();
let span = span_of(
&xml,
&format!("<witsml:Channel uuid=\"{GAMMA}\""),
"</witsml:Channel>",
);
let entry = entry_in("log.xml", span, "witsml21.Channel", GAMMA, "witsml:Channel");
let extracted = extract_one(&xml, &entry, &target(®istry, "witsml21.Channel"));
let text = String::from_utf8(extracted.bytes).expect("UTF-8");
assert!(text.contains(&format!("xmlns:eml=\"{EML_NS}\"")), "{text}");
assert!(text.contains(&format!("xmlns:witsml=\"{WITSML_NS}\"")), "{text}");
assert_eq!(undeclared_prefixes(&text), Vec::<String>::new(), "{text}");
assert_eq!(text.matches("xmlns:eml=").count(), 1, "{text}");
}
#[test]
fn a_default_namespace_is_carried_and_the_root_stays_unprefixed() {
let registry = registry();
let xml = format!(
"<Log xmlns=\"{WITSML_NS}\" xmlns:eml=\"{EML_NS}\" uuid=\"{LOG}\" schemaVersion=\"2.1\">\
<ChannelSet uuid=\"{SET}\" schemaVersion=\"2.1\">\
<Channel uuid=\"{GAMMA}\" schemaVersion=\"2.1\">{}<Mnemonic>GR</Mnemonic></Channel>\
</ChannelSet></Log>",
citation("Gamma Ray")
);
let span = span_of(&xml, "<Channel ", "</Channel>");
let entry = entry_in("log.xml", span, "witsml21.Channel", GAMMA, "Channel");
let extracted = extract_one(&xml, &entry, &target(®istry, "witsml21.Channel"));
let text = String::from_utf8(extracted.bytes).expect("UTF-8");
assert!(
text.starts_with(&format!("<Channel xmlns=\"{WITSML_NS}\"")),
"{text}"
);
assert!(text.ends_with("</Channel>"), "{text}");
assert!(text.contains(&format!("xmlns:eml=\"{EML_NS}\"")), "{text}");
}
#[test]
fn a_declaration_made_by_an_intermediate_ancestor_is_in_scope() {
let xml = format!(
"<witsml:Log xmlns:witsml=\"{WITSML_NS}\" xmlns:eml=\"{EML_NS}\" uuid=\"{LOG}\" schemaVersion=\"2.1\">\
<witsml:ChannelSet xmlns:ext=\"urn:example:ext\" uuid=\"{SET}\" schemaVersion=\"2.1\">\
<witsml:Channel uuid=\"{GAMMA}\" schemaVersion=\"2.1\"/></witsml:ChannelSet>\
<witsml:ChannelSet uuid=\"{DENSITY}\" schemaVersion=\"2.1\">\
<witsml:Channel uuid=\"{UNKNOWN}\" schemaVersion=\"2.1\"/></witsml:ChannelSet></witsml:Log>"
);
let inside = entry_in(
"log.xml",
span_of(&xml, &format!("<witsml:Channel uuid=\"{GAMMA}\""), "/>"),
"witsml21.Channel",
GAMMA,
"witsml:Channel",
);
let outside = entry_in(
"log.xml",
span_of(&xml, &format!("<witsml:Channel uuid=\"{UNKNOWN}\""), "/>"),
"witsml21.Channel",
UNKNOWN,
"witsml:Channel",
);
let scopes = scopes_at(xml.as_bytes(), &[&outside, &inside]);
let outer = scopes[0].as_ref().expect("a scope");
let inner = scopes[1].as_ref().expect("a scope");
assert!(
inner
.declarations
.contains(&("ext".to_string(), "urn:example:ext".to_string()))
);
assert!(!outer.declarations.iter().any(|(prefix, _)| prefix == "ext"));
assert_eq!(inner.schema_version.as_deref(), Some("2.1"));
assert!(
inner
.declarations
.contains(&("eml".to_string(), EML_NS.to_string()))
);
}
#[test]
fn a_subtype_and_a_renamed_slot_are_written_under_their_own_global_element() {
let registry = registry();
let xml = format!(
"<witsml:ChannelSet xmlns:witsml=\"{WITSML_NS}\" xmlns:eml=\"{EML_NS}\" xmlns:xsi=\"{XSI_NS}\" \
uuid=\"{SET}\" schemaVersion=\"2.1\">\
<witsml:Channel xsi:type=\"witsml:PPFGChannel\" uuid=\"{GAMMA}\" schemaVersion=\"2.1\">{}</witsml:Channel>\
</witsml:ChannelSet>",
citation("Pore pressure")
);
let span = span_of(&xml, "<witsml:Channel ", "</witsml:Channel>");
let entry = entry_in("set.xml", span, "witsml21.PPFGChannel", GAMMA, "witsml:Channel");
let extracted = extract_one(&xml, &entry, &target(®istry, "witsml21.PPFGChannel"));
let text = String::from_utf8(extracted.bytes.clone()).expect("UTF-8");
assert!(text.starts_with("<witsml:PPFGChannel "), "{text}");
assert!(text.ends_with("</witsml:PPFGChannel>"), "{text}");
assert!(!text.contains("xsi:type"), "{text}");
let info = registry
.by_qualified_type("witsml21.PPFGChannel")
.expect("registered")
.info;
let (object, _) = registry
.read_xml_as_named(info, &extracted.bytes, ReadMode::Tolerant)
.expect("reads");
assert_eq!(object.type_info().name, "PPFGChannel");
let xml = format!(
"<witsml:WellboreMarkerSet xmlns:witsml=\"{WITSML_NS}\" xmlns:eml=\"{EML_NS}\" \
uuid=\"{SET}\" schemaVersion=\"2.1\">\
<witsml:FormationMarker uuid=\"{DENSITY}\" schemaVersion=\"2.1\">{}</witsml:FormationMarker>\
</witsml:WellboreMarkerSet>",
citation("Top Chalk")
);
let span = span_of(&xml, "<witsml:FormationMarker ", "</witsml:FormationMarker>");
let entry = entry_in(
"markers.xml",
span,
"witsml21.WellboreMarker",
DENSITY,
"witsml:FormationMarker",
);
let extracted = extract_one(&xml, &entry, &target(®istry, "witsml21.WellboreMarker"));
let text = String::from_utf8(extracted.bytes.clone()).expect("UTF-8");
assert!(text.starts_with("<witsml:WellboreMarker "), "{text}");
assert!(text.ends_with("</witsml:WellboreMarker>"), "{text}");
let info = registry
.by_qualified_type("witsml21.WellboreMarker")
.expect("registered")
.info;
let (object, _) = registry
.read_xml_as_named(info, &extracted.bytes, ReadMode::Tolerant)
.expect("reads");
assert_eq!(title_of(object.as_ref()).as_deref(), Some("Top Chalk"));
}
#[test]
fn a_type_without_an_element_of_its_own_keeps_its_xsi_type() {
let target = Target {
element: "FlowTestActivity",
namespace: Namespace::new("prodml", "http://www.energistics.org/energyml/data/prodmlv2"),
type_name: "InjectionFlowTest",
keep_xsi_type: true,
schema_version: "2.3",
};
let xml = format!(
"<prodml:Holder xmlns:prodml=\"http://www.energistics.org/energyml/data/prodmlv2\" schemaVersion=\"2.3\">\
<prodml:Test uuid=\"{GAMMA}\"/></prodml:Holder>"
);
let entry = entry_in(
"holder.xml",
span_of(&xml, "<prodml:Test ", "/>"),
"prodml23.InjectionFlowTest",
GAMMA,
"prodml:Test",
);
let extracted = extract_one(&xml, &entry, &target);
let text = String::from_utf8(extracted.bytes).expect("UTF-8");
assert!(text.starts_with("<prodml:FlowTestActivity "), "{text}");
assert!(text.contains(&format!("xmlns:xsi=\"{XSI_NS}\"")), "{text}");
assert!(text.contains("xsi:type=\"prodml:InjectionFlowTest\""), "{text}");
assert!(text.contains("schemaVersion=\"2.3\""), "{text}");
assert_eq!(extracted.notes.len(), 1);
assert!(
extracted.notes[0].contains("its container's"),
"{:?}",
extracted.notes
);
}
#[test]
fn a_missing_schema_version_is_supplied_and_said() {
let registry = registry();
let xml = format!(
"<witsml:ChannelSet xmlns:witsml=\"{WITSML_NS}\" xmlns:eml=\"{EML_NS}\" uuid=\"{SET}\">\
<witsml:Channel uuid=\"{GAMMA}\">{}</witsml:Channel></witsml:ChannelSet>",
citation("Gamma Ray")
);
let entry = entry_in(
"set.xml",
span_of(&xml, "<witsml:Channel ", "</witsml:Channel>"),
"witsml21.Channel",
GAMMA,
"witsml:Channel",
);
let extracted = extract_one(&xml, &entry, &target(®istry, "witsml21.Channel"));
let text = String::from_utf8(extracted.bytes).expect("UTF-8");
assert!(text.contains("schemaVersion=\"2.1\""), "{text}");
assert_eq!(extracted.notes.len(), 1, "{:?}", extracted.notes);
assert!(extracted.notes[0].contains("its type's"), "{:?}", extracted.notes);
}
#[test]
fn a_malformed_span_is_refused() {
let registry = registry();
let xml = log_part();
let target = target(®istry, "witsml21.Channel");
let scope = Scope::default();
let from = xml.find("<witsml:Channel ").expect("a channel") + 1;
let torn = entry_in(
"log.xml",
from..from + 20,
"witsml21.Channel",
GAMMA,
"witsml:Channel",
);
assert!(matches!(
extract(xml.as_bytes(), &torn, &target, &scope),
Err(Error::Xml(_))
));
assert_eq!(scopes_at(xml.as_bytes(), &[&torn]), vec![None]);
let start = xml.find("<witsml:Channel ").expect("a channel");
let end = xml.find("</witsml:ChannelSet>").expect("a set end") + "</witsml:ChannelSet>".len();
let crossed = entry_in("log.xml", start..end, "witsml21.Channel", GAMMA, "witsml:Channel");
assert!(matches!(
extract(xml.as_bytes(), &crossed, &target, &scope),
Err(Error::Xml(_))
));
let outside = entry_in(
"log.xml",
xml.len()..xml.len() + 10,
"witsml21.Channel",
GAMMA,
"witsml:Channel",
);
assert!(matches!(
extract(xml.as_bytes(), &outside, &target, &scope),
Err(Error::Xml(_))
));
}
#[test]
fn the_object_version_is_borrowed_from_the_uri_or_not_at_all() {
let plain = format!("eml:///witsml21.Channel({GAMMA})");
assert_eq!(object_version_in(&plain), Some(None));
let versioned = format!("eml:///witsml21.Channel(uuid={GAMMA},version='v1')");
assert_eq!(object_version_in(&versioned), Some(Some("v1")));
let quoted = format!("eml:///witsml21.Channel(uuid={GAMMA},version='O''Neil')");
assert_eq!(object_version_in("ed), None);
assert_eq!(object_version_in("not a uri"), None);
}
#[test]
fn two_contents_of_one_part_are_read_in_one_load() {
let xml = log_part();
let one = entry_in(
"log.xml",
span_of(
&xml,
&format!("<witsml:Channel uuid=\"{GAMMA}\""),
"</witsml:Channel>",
),
"witsml21.Channel",
GAMMA,
"witsml:Channel",
);
let two = entry_in(
"log.xml",
span_of(
&xml,
&format!("<witsml:Channel uuid=\"{DENSITY}\""),
"</witsml:Channel>",
),
"witsml21.Channel",
DENSITY,
"witsml:Channel",
);
let elsewhere = entry_in("other.xml", 0..1, "witsml21.Channel", UNKNOWN, "witsml:Channel");
let wanted = [(0, &one), (1, &elsewhere), (2, &two)];
let groups = group_by_part(&wanted);
assert_eq!(groups.len(), 2);
assert_eq!(
groups["log.xml"].iter().map(|(at, _)| *at).collect::<Vec<_>>(),
vec![0, 2]
);
let loads = std::cell::Cell::new(0u32);
let results = per_part(
&[&one, &two],
|| {
loads.set(loads.get() + 1);
Ok(xml.clone().into_bytes())
},
|bytes, entry, scope| Ok(entry.span.len() + bytes.len() + scope.declarations.len()),
);
assert_eq!(loads.get(), 1);
assert_eq!(results.len(), 2);
assert!(results.iter().all(Result::is_ok));
}
#[test]
fn a_part_that_cannot_be_loaded_fails_its_own_contents_only() {
let xml = log_part();
let entry = entry_in("gone.xml", 0..10, "witsml21.Channel", GAMMA, "witsml:Channel");
let results: Vec<Result<()>> = per_part(
&[&entry, &entry],
|| Err(Error::Package("no part `gone.xml`".into())),
|_, _, _| Ok(()),
);
assert_eq!(results.len(), 2);
for result in results {
let message = result.expect_err("the load failed").to_string();
assert!(
message.contains(GAMMA) && message.contains("gone.xml"),
"{message}"
);
}
let fine = entry_in(
"log.xml",
span_of(&xml, "<witsml:ChannelSet ", "</witsml:ChannelSet>"),
"witsml21.ChannelSet",
SET,
"witsml:ChannelSet",
);
let results = per_part(&[&fine], || Ok(xml.clone().into_bytes()), |_, _, _| Ok(()));
assert!(results[0].is_ok());
}
fn package(extra: &[(&str, String, &str)]) -> Vec<u8> {
let mut buffer = Cursor::new(Vec::new());
{
let mut zip = zip::ZipWriter::new(&mut buffer);
let options: zip::write::SimpleFileOptions =
zip::write::FileOptions::default().compression_method(zip::CompressionMethod::Deflated);
let mut overrides = format!(
"<Override PartName=\"/witsml21/Log_{LOG}.xml\" \
ContentType=\"application/x-witsml+xml;version=2.1;type=Log\"/>"
);
for (name, _, content_type) in extra {
overrides.push_str(&format!(
"<Override PartName=\"/{name}\" ContentType=\"{content_type}\"/>"
));
}
zip.start_file("[Content_Types].xml", options).expect("start");
zip.write_all(
format!(
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
<Types xmlns=\"http://schemas.openxmlformats.org/package/2006/content-types\">\
<Default Extension=\"rels\" ContentType=\"application/vnd.openxmlformats-package.relationships+xml\"/>\
{overrides}</Types>"
)
.as_bytes(),
)
.expect("write");
zip.start_file(format!("witsml21/Log_{LOG}.xml"), options)
.expect("start");
zip.write_all(log_part().as_bytes()).expect("write");
for (name, body, _) in extra {
zip.start_file(*name, options).expect("start");
zip.write_all(body.as_bytes()).expect("write");
}
zip.finish().expect("finish");
}
buffer.into_inner()
}
fn store(extra: &[(&str, String, &str)]) -> EpcStore {
EpcStore::from_reader(Box::new(Cursor::new(package(extra))), "witsml.epc", registry())
.expect("the package opens")
}
fn uri_of(contents: &Contents, qualified_type: &str, uuid: &str) -> String {
contents
.objects
.iter()
.find(|object| object.uuid == uuid && object.qualified_type == qualified_type)
.map(|object| object.uri.clone())
.unwrap_or_else(|| panic!("{qualified_type}({uuid}) is not in the contents: {contents:?}"))
}
#[test]
fn contents_lists_what_resources_does_not_and_links_it_to_its_container() {
let store = store(&[]);
let parts = store.resources();
assert_eq!(parts.len(), 1, "{parts:?}");
assert_eq!(parts[0].qualified_type, "witsml21.Log");
let contents = store.contents().expect("contents");
assert_eq!(contents.objects.len(), 3, "{contents:?}");
let set = uri_of(&contents, "witsml21.ChannelSet", SET);
let gamma = uri_of(&contents, "witsml21.Channel", GAMMA);
let density = uri_of(&contents, "witsml21.Channel", DENSITY);
assert_eq!(contents.edges.len(), 3, "{:?}", contents.edges);
for (from, to) in [(&gamma, &set), (&density, &set), (&set, &parts[0].uri)] {
let edge = contents
.edges
.iter()
.find(|edge| &edge.from == from && &edge.to == to)
.unwrap_or_else(|| panic!("no edge {from} -> {to}: {:?}", contents.edges));
assert!(edge.by_value);
assert_eq!(edge.kind, Some(EdgeKind::Primary));
assert!(
matches!(edge.basis, Some(KindBasis::EtpSpec { page: 157, .. })),
"{edge:?}"
);
}
}
#[test]
fn a_contained_object_is_looked_up_read_cited_and_served_as_itself() {
let store = store(&[]);
let contents = store.contents().expect("contents");
let gamma = uri_of(&contents, "witsml21.Channel", GAMMA);
let density = uri_of(&contents, "witsml21.Channel", DENSITY);
assert_eq!(
store
.lookup(GAMMA, Some("witsml21.Channel"), None)
.expect("found")
.uri,
gamma
);
let object = store.get_object(&gamma).expect("reads");
assert_eq!(identity::uuid_of_reflect(object.as_reflect()), Some(GAMMA));
assert_eq!(title_of(object.as_ref()).as_deref(), Some("Gamma Ray"));
let header = store.header(&gamma).expect("cites");
assert_eq!(header.title.as_deref(), Some("Gamma Ray"));
assert_eq!(header.summary.uri, gamma);
let both = [density.clone(), gamma.clone()];
let titles: Vec<_> = store
.get_objects(&both)
.into_iter()
.map(|result| title_of(result.expect("reads").as_ref()))
.collect();
assert_eq!(
titles,
vec![Some("Bulk Density".to_string()), Some("Gamma Ray".to_string())]
);
let cited: Vec<_> = store
.headers(&both)
.into_iter()
.map(|result| result.expect("cites").title)
.collect();
assert_eq!(
cited,
vec![Some("Bulk Density".to_string()), Some("Gamma Ray".to_string())]
);
let raw = store
.raw_objects(std::slice::from_ref(&gamma), RawFormat::Xml)
.pop()
.expect("one")
.expect("raw");
let text = String::from_utf8(raw.bytes).expect("UTF-8");
assert!(text.starts_with("<witsml:Channel "), "{text}");
assert!(
text.contains("Gamma Ray") && !text.contains("Bulk Density"),
"{text}"
);
}
#[test]
fn an_unknown_contained_uri_is_not_found() {
let store = store(&[]);
let unknown = format!("eml:///witsml21.Channel({UNKNOWN})");
assert!(matches!(store.get_object(&unknown), Err(Error::NotFound(_))));
assert!(matches!(store.header(&unknown), Err(Error::NotFound(_))));
let raw = store.raw_objects(&[unknown], RawFormat::Xml).pop().expect("one");
assert!(matches!(raw, Err(Error::NotFound(_))));
let log = store.resources().pop().expect("the log").uri;
assert!(store.get_object(&log).is_ok());
}
#[test]
fn a_contained_object_that_is_also_a_part_is_the_part() {
let own = format!(
"<witsml:Channel xmlns:witsml=\"{WITSML_NS}\" xmlns:eml=\"{EML_NS}\" uuid=\"{GAMMA}\" schemaVersion=\"2.1\">{}\
<witsml:Mnemonic>GR</witsml:Mnemonic></witsml:Channel>",
citation("Gamma Ray (part)")
);
let store = store(&[(
"witsml21/Channel_gamma.xml",
own,
"application/x-witsml+xml;version=2.1;type=Channel",
)]);
let contents = store.contents().expect("contents");
assert!(
contents.objects.iter().all(|object| object.uuid != GAMMA),
"listed twice: {contents:?}"
);
assert_eq!(
contents
.edges
.iter()
.filter(|edge| edge.from.contains(GAMMA))
.count(),
1,
"{:?}",
contents.edges
);
let uri = store
.resources()
.into_iter()
.find(|part| part.uuid == GAMMA)
.expect("the part")
.uri;
let object = store.get_object(&uri).expect("reads");
assert_eq!(title_of(object.as_ref()).as_deref(), Some("Gamma Ray (part)"));
}
#[test]
fn the_writes_that_would_guess_are_refused_by_name() {
let mut store = store(&[]);
let contents = store.contents().expect("contents");
let gamma = uri_of(&contents, "witsml21.Channel", GAMMA);
let log = store.resources().pop().expect("the log").uri;
let object = store.get_object(&gamma).expect("reads");
assert!(matches!(store.put(object), Err(Error::Unsupported { .. })));
assert!(matches!(store.remove(&gamma), Err(Error::Unsupported { .. })));
match store.remove(&log) {
Err(Error::Unsupported { operation, .. }) => {
assert!(operation.contains("live only inside it"), "{operation}");
}
other => panic!("the log held channels that live only inside it: {other:?}"),
}
assert_eq!(store.resources().len(), 1, "nothing was removed");
}
}