- fix some discrepancies with the xsd
This commit is contained in:
1 parent
4dba00a004
commit
81bd302967
6 files changed
+317
-39
No files matched your search
+1
-1
@@ -3,7 +3,7 @@ resolver = "3"
|
|||||||
members = ["ome-metadata", "ome-metadata-derive"]
|
members = ["ome-metadata", "ome-metadata-derive"]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.5.0"
|
version = "0.6.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
rust-version = "1.88.0"
|
rust-version = "1.88.0"
|
||||||
authors = ["Wim Pomp <w.pomp@nki.nl>"]
|
authors = ["Wim Pomp <w.pomp@nki.nl>"]
|
||||||
|
|||||||
@@ -10,11 +10,11 @@ pub fn ome_metadata_derive_xml(input: TokenStream) -> TokenStream {
|
|||||||
let expanded = quote! {
|
let expanded = quote! {
|
||||||
impl #ident {
|
impl #ident {
|
||||||
pub fn from_xml<T: AsRef<str>>(s: T) -> Result<Self, Error> {
|
pub fn from_xml<T: AsRef<str>>(s: T) -> Result<Self, Error> {
|
||||||
Ok(from_str(s.as_ref())?)
|
from_str(s.as_ref())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn to_xml(&self) -> Result<String, Error> {
|
pub fn to_xml(&self) -> Result<String, Error> {
|
||||||
Ok(to_string(self)?)
|
to_string(self)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ pub mod ome;
|
|||||||
pub mod error;
|
pub mod error;
|
||||||
#[cfg(feature = "python")]
|
#[cfg(feature = "python")]
|
||||||
pub mod py;
|
pub mod py;
|
||||||
|
mod xml;
|
||||||
|
|
||||||
pub use ome::Ome;
|
pub use ome::Ome;
|
||||||
|
|
||||||
@@ -27,6 +28,67 @@ mod tests {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn serialize_ome_root_and_namespaces() -> Result<(), Error> {
|
||||||
|
for (uuid, creator) in [
|
||||||
|
(None, None),
|
||||||
|
(Some("urn:uuid:3e450fae-b8f2-4d35-aa54-702168b2487f"), None),
|
||||||
|
(None, Some("example & <software>")),
|
||||||
|
(
|
||||||
|
Some("urn:uuid:3e450fae-b8f2-4d35-aa54-702168b2487f"),
|
||||||
|
Some("example & <software>"),
|
||||||
|
),
|
||||||
|
] {
|
||||||
|
let ome = Ome {
|
||||||
|
uuid: uuid.map(str::to_owned),
|
||||||
|
creator: creator.map(str::to_owned),
|
||||||
|
..Ome::default()
|
||||||
|
};
|
||||||
|
let xml = ome.to_xml()?;
|
||||||
|
let mut reader = quick_xml::NsReader::from_str(&xml);
|
||||||
|
let (namespace, event) = reader.read_resolved_event().unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
namespace,
|
||||||
|
quick_xml::name::ResolveResult::Bound(quick_xml::name::Namespace(
|
||||||
|
b"http://www.openmicroscopy.org/Schemas/OME/2016-06"
|
||||||
|
))
|
||||||
|
);
|
||||||
|
let root = match event {
|
||||||
|
quick_xml::events::Event::Start(root) | quick_xml::events::Event::Empty(root) => {
|
||||||
|
root
|
||||||
|
}
|
||||||
|
event => panic!("expected OME root, got {event:?}"),
|
||||||
|
};
|
||||||
|
assert_eq!(root.name().as_ref(), b"OME");
|
||||||
|
assert_eq!(
|
||||||
|
root.try_get_attribute("xmlns:xsi")
|
||||||
|
.unwrap()
|
||||||
|
.unwrap()
|
||||||
|
.value
|
||||||
|
.as_ref(),
|
||||||
|
b"http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
root.try_get_attribute("xsi:schemaLocation").unwrap().unwrap().value.as_ref(),
|
||||||
|
b"http://www.openmicroscopy.org/Schemas/OME/2016-06 http://www.openmicroscopy.org/Schemas/OME/2016-06/ome.xsd"
|
||||||
|
);
|
||||||
|
assert_eq!(Ome::from_xml(&xml)?, ome);
|
||||||
|
|
||||||
|
// Exercise a nonempty root as well as the empty metadata document.
|
||||||
|
let ome = Ome {
|
||||||
|
binary_only: Some(crate::ome::OmeBinaryOnly {
|
||||||
|
metadata_file: "metadata.ome.xml".to_owned(),
|
||||||
|
uuid: "urn:uuid:3e450fae-b8f2-4d35-aa54-702168b2487f".to_owned(),
|
||||||
|
}),
|
||||||
|
..ome
|
||||||
|
};
|
||||||
|
let xml = ome.to_xml()?;
|
||||||
|
assert!(xml.ends_with("</OME>"));
|
||||||
|
assert_eq!(Ome::from_xml(&xml)?, ome);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
test_read!(
|
test_read!(
|
||||||
a: "YTL1849A111_2023_05_04__14_46_19_cellnr_1_track"
|
a: "YTL1849A111_2023_05_04__14_46_19_cellnr_1_track"
|
||||||
b: "Experiment-2029"
|
b: "Experiment-2029"
|
||||||
|
|||||||
+62
-23
@@ -2,6 +2,7 @@
|
|||||||
use crate::error::Error;
|
use crate::error::Error;
|
||||||
#[cfg(feature = "python")]
|
#[cfg(feature = "python")]
|
||||||
use crate::py::PyDisplay;
|
use crate::py::PyDisplay;
|
||||||
|
use crate::xml::{from_str, to_string};
|
||||||
use ome_metadata_derive::OmeXML;
|
use ome_metadata_derive::OmeXML;
|
||||||
#[cfg(feature = "python")]
|
#[cfg(feature = "python")]
|
||||||
use ome_metadata_derive::{PyOme, PyOmeComplexEnum, PyOmeEnum, PyOmeUnit};
|
use ome_metadata_derive::{PyOme, PyOmeComplexEnum, PyOmeEnum, PyOmeUnit};
|
||||||
@@ -9,8 +10,6 @@ use ome_metadata_derive::{PyOme, PyOmeComplexEnum, PyOmeEnum, PyOmeUnit};
|
|||||||
use pyo3::IntoPyObjectExt;
|
use pyo3::IntoPyObjectExt;
|
||||||
#[cfg(feature = "python")]
|
#[cfg(feature = "python")]
|
||||||
use pyo3::prelude::*;
|
use pyo3::prelude::*;
|
||||||
use quick_xml::de::from_str;
|
|
||||||
use quick_xml::se::to_string;
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::cmp::PartialEq;
|
use std::cmp::PartialEq;
|
||||||
use strum::{AsRefStr, EnumString, VariantNames};
|
use strum::{AsRefStr, EnumString, VariantNames};
|
||||||
@@ -500,7 +499,7 @@ impl Channel {
|
|||||||
UnitsLength::um
|
UnitsLength::um
|
||||||
}
|
}
|
||||||
pub fn default_color() -> i32 {
|
pub fn default_color() -> i32 {
|
||||||
0
|
-1
|
||||||
}
|
}
|
||||||
pub fn default_excitation_wavelength_unit() -> UnitsLength {
|
pub fn default_excitation_wavelength_unit() -> UnitsLength {
|
||||||
UnitsLength::nm
|
UnitsLength::nm
|
||||||
@@ -2031,11 +2030,7 @@ pub struct Instrument {
|
|||||||
skip_serializing_if = "Option::is_none"
|
skip_serializing_if = "Option::is_none"
|
||||||
)]
|
)]
|
||||||
pub microscope: Option<Microscope>,
|
pub microscope: Option<Microscope>,
|
||||||
#[serde(
|
#[serde(default, rename = "$value", skip_serializing_if = "Vec::is_empty")]
|
||||||
default,
|
|
||||||
rename = "LightSourceGroup",
|
|
||||||
skip_serializing_if = "Vec::is_empty"
|
|
||||||
)]
|
|
||||||
pub light_source_group: Vec<LightSourceGroup>,
|
pub light_source_group: Vec<LightSourceGroup>,
|
||||||
#[serde(default, rename = "Detector", skip_serializing_if = "Vec::is_empty")]
|
#[serde(default, rename = "Detector", skip_serializing_if = "Vec::is_empty")]
|
||||||
pub detector: Vec<Detector>,
|
pub detector: Vec<Detector>,
|
||||||
@@ -3310,7 +3305,7 @@ pub enum NamingConventionType {
|
|||||||
/// ```
|
/// ```
|
||||||
/// use ome_metadata::Ome;
|
/// use ome_metadata::Ome;
|
||||||
///
|
///
|
||||||
/// let xml = r#"<OME xmlns="https://www.openmicroscopy.org/Schemas/OME/2016-06" xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="https://www.openmicroscopy.org/Schemas/OME/2016-06 https://www.openmicroscopy.org/Schemas/OME/2016-06/ome.xsd">
|
/// let xml = r#"<OME xmlns="http://www.openmicroscopy.org/Schemas/OME/2016-06" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.openmicroscopy.org/Schemas/OME/2016-06 http://www.openmicroscopy.org/Schemas/OME/2016-06/ome.xsd">
|
||||||
/// <Image ID="Image:0" Name="test.tif">
|
/// <Image ID="Image:0" Name="test.tif">
|
||||||
/// <AcquisitionDate>2025-01-29T14:42:42</AcquisitionDate>
|
/// <AcquisitionDate>2025-01-29T14:42:42</AcquisitionDate>
|
||||||
/// <Description/>
|
/// <Description/>
|
||||||
@@ -3329,6 +3324,7 @@ pub enum NamingConventionType {
|
|||||||
/// ```
|
/// ```
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, Default, PartialEq)]
|
#[derive(Clone, Debug, Serialize, Deserialize, Default, PartialEq)]
|
||||||
|
#[serde(rename = "OME")]
|
||||||
#[cfg_attr(
|
#[cfg_attr(
|
||||||
feature = "python",
|
feature = "python",
|
||||||
pyclass(
|
pyclass(
|
||||||
@@ -3388,11 +3384,15 @@ pub struct Ome {
|
|||||||
}
|
}
|
||||||
impl Ome {
|
impl Ome {
|
||||||
pub fn from_xml<T: AsRef<str>>(s: T) -> Result<Self, Error> {
|
pub fn from_xml<T: AsRef<str>>(s: T) -> Result<Self, Error> {
|
||||||
Ok(from_str(s.as_ref())?)
|
from_str(s.as_ref())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn to_xml(&self) -> Result<String, Error> {
|
pub fn to_xml(&self) -> Result<String, Error> {
|
||||||
Ok(to_string(self)?.replace("<Ome>", r###"<OME xmlns="https://www.openmicroscopy.org/Schemas/OME/2016-06" xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="https://www.openmicroscopy.org/Schemas/OME/2016-06 https://www.openmicroscopy.org/Schemas/OME/2016-06/ome.xsd">"###).replace("</Ome>", "</OME>"))
|
Ok(to_string(self)?.replacen(
|
||||||
|
"<OME",
|
||||||
|
r#"<OME xmlns="http://www.openmicroscopy.org/Schemas/OME/2016-06" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.openmicroscopy.org/Schemas/OME/2016-06 http://www.openmicroscopy.org/Schemas/OME/2016-06/ome.xsd""#,
|
||||||
|
1,
|
||||||
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#[cfg_attr(
|
#[cfg_attr(
|
||||||
@@ -3943,8 +3943,12 @@ pub struct Plane {
|
|||||||
pub position_z_unit: UnitsLength,
|
pub position_z_unit: UnitsLength,
|
||||||
#[serde(rename = "HashSHA1", skip_serializing_if = "Option::is_none")]
|
#[serde(rename = "HashSHA1", skip_serializing_if = "Option::is_none")]
|
||||||
pub hash_sha1: Option<String>,
|
pub hash_sha1: Option<String>,
|
||||||
#[serde(rename = "AnnotationRef", skip_serializing_if = "Option::is_none")]
|
#[serde(
|
||||||
pub annotation_ref: Option<AnnotationRef>,
|
default,
|
||||||
|
rename = "AnnotationRef",
|
||||||
|
skip_serializing_if = "Vec::is_empty"
|
||||||
|
)]
|
||||||
|
pub annotation_ref: Vec<AnnotationRef>,
|
||||||
}
|
}
|
||||||
impl Plane {
|
impl Plane {
|
||||||
pub fn default_delta_t_unit() -> UnitsTime {
|
pub fn default_delta_t_unit() -> UnitsTime {
|
||||||
@@ -3980,7 +3984,7 @@ impl Default for Plane {
|
|||||||
position_z: None,
|
position_z: None,
|
||||||
position_z_unit: Self::default_position_z_unit(),
|
position_z_unit: Self::default_position_z_unit(),
|
||||||
hash_sha1: None,
|
hash_sha1: None,
|
||||||
annotation_ref: None,
|
annotation_ref: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4471,8 +4475,12 @@ pub struct Roi {
|
|||||||
pub name: Option<String>,
|
pub name: Option<String>,
|
||||||
#[serde(rename = "Union", skip_serializing_if = "Option::is_none")]
|
#[serde(rename = "Union", skip_serializing_if = "Option::is_none")]
|
||||||
pub union: Option<RoiUnion>,
|
pub union: Option<RoiUnion>,
|
||||||
#[serde(rename = "AnnotationRef", skip_serializing_if = "Option::is_none")]
|
#[serde(
|
||||||
pub annotation_ref: Option<AnnotationRef>,
|
default,
|
||||||
|
rename = "AnnotationRef",
|
||||||
|
skip_serializing_if = "Vec::is_empty"
|
||||||
|
)]
|
||||||
|
pub annotation_ref: Vec<AnnotationRef>,
|
||||||
#[serde(rename = "Description", skip_serializing_if = "Option::is_none")]
|
#[serde(rename = "Description", skip_serializing_if = "Option::is_none")]
|
||||||
pub description: Option<String>,
|
pub description: Option<String>,
|
||||||
}
|
}
|
||||||
@@ -4683,7 +4691,7 @@ pub struct Rights {
|
|||||||
)]
|
)]
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, Default, PartialEq, OmeXML)]
|
#[derive(Clone, Debug, Serialize, Deserialize, Default, PartialEq, OmeXML)]
|
||||||
pub struct RoiUnion {
|
pub struct RoiUnion {
|
||||||
#[serde(default, rename = "ShapeGroup", skip_serializing_if = "Vec::is_empty")]
|
#[serde(default, rename = "$value", skip_serializing_if = "Vec::is_empty")]
|
||||||
pub shape_group: Vec<ShapeGroup>,
|
pub shape_group: Vec<ShapeGroup>,
|
||||||
}
|
}
|
||||||
#[cfg_attr(
|
#[cfg_attr(
|
||||||
@@ -5020,8 +5028,8 @@ impl Default for StageLabel {
|
|||||||
)]
|
)]
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, Default, PartialEq, OmeXML)]
|
#[derive(Clone, Debug, Serialize, Deserialize, Default, PartialEq, OmeXML)]
|
||||||
pub struct StructuredAnnotations {
|
pub struct StructuredAnnotations {
|
||||||
#[serde(default, rename = "$value", skip_serializing_if = "Option::is_none")]
|
#[serde(default, rename = "$value", skip_serializing_if = "Vec::is_empty")]
|
||||||
pub content: Option<StructuredAnnotationsContent>,
|
pub content: Vec<StructuredAnnotationsContent>,
|
||||||
}
|
}
|
||||||
#[allow(clippy::enum_variant_names)]
|
#[allow(clippy::enum_variant_names)]
|
||||||
#[derive(
|
#[derive(
|
||||||
@@ -5632,7 +5640,7 @@ pub enum UnitsTime {
|
|||||||
),
|
),
|
||||||
derive(FromPyObject, PyOme)
|
derive(FromPyObject, PyOme)
|
||||||
)]
|
)]
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, Default, PartialEq, OmeXML)]
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, OmeXML)]
|
||||||
pub struct Well {
|
pub struct Well {
|
||||||
#[serde(rename = "@ID")]
|
#[serde(rename = "@ID")]
|
||||||
pub id: String,
|
pub id: String,
|
||||||
@@ -5671,9 +5679,25 @@ pub struct Well {
|
|||||||
)]
|
)]
|
||||||
pub annotation_ref: Vec<AnnotationRef>,
|
pub annotation_ref: Vec<AnnotationRef>,
|
||||||
}
|
}
|
||||||
|
impl Default for Well {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
id: String::new(),
|
||||||
|
column: 0,
|
||||||
|
row: 0,
|
||||||
|
external_description: None,
|
||||||
|
external_identifier: None,
|
||||||
|
r#type: None,
|
||||||
|
color: Self::default_color(),
|
||||||
|
well_sample: Vec::new(),
|
||||||
|
reagent_ref: None,
|
||||||
|
annotation_ref: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
impl Well {
|
impl Well {
|
||||||
pub fn default_color() -> i32 {
|
pub fn default_color() -> i32 {
|
||||||
0
|
-1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#[cfg_attr(
|
#[cfg_attr(
|
||||||
@@ -5760,6 +5784,7 @@ impl Default for WellSample {
|
|||||||
derive(FromPyObject, PyOme)
|
derive(FromPyObject, PyOme)
|
||||||
)]
|
)]
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, Default, PartialEq, OmeXML)]
|
#[derive(Clone, Debug, Serialize, Deserialize, Default, PartialEq, OmeXML)]
|
||||||
|
#[serde(rename = "XMLAnnotation")]
|
||||||
pub struct XmlAnnotation {
|
pub struct XmlAnnotation {
|
||||||
#[serde(rename = "@ID")]
|
#[serde(rename = "@ID")]
|
||||||
pub id: String,
|
pub id: String,
|
||||||
@@ -5790,12 +5815,26 @@ pub struct XmlAnnotation {
|
|||||||
#[serde(rename = "Value")]
|
#[serde(rename = "Value")]
|
||||||
pub value: XmlAnnotationValue,
|
pub value: XmlAnnotationValue,
|
||||||
}
|
}
|
||||||
|
/// Arbitrary XML fragment inside an XMLAnnotation Value element.
|
||||||
|
/// Use the from_xml/to_xml methods to read or write XML; generic Serde
|
||||||
|
/// serialization represents the fragment as escaped text.
|
||||||
#[cfg_attr(
|
#[cfg_attr(
|
||||||
feature = "python",
|
feature = "python",
|
||||||
pyclass(module = "ome_metadata", skip_from_py_object, eq, new = "from_fields")
|
pyclass(
|
||||||
|
module = "ome_metadata",
|
||||||
|
skip_from_py_object,
|
||||||
|
get_all,
|
||||||
|
set_all,
|
||||||
|
eq,
|
||||||
|
new = "from_fields"
|
||||||
|
),
|
||||||
|
derive(FromPyObject, PyOme)
|
||||||
)]
|
)]
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, Default, PartialEq, OmeXML)]
|
#[derive(Clone, Debug, Serialize, Deserialize, Default, PartialEq, OmeXML)]
|
||||||
pub struct XmlAnnotationValue;
|
pub struct XmlAnnotationValue {
|
||||||
|
#[serde(default, rename = "$text")]
|
||||||
|
pub xml: String,
|
||||||
|
}
|
||||||
pub trait Convert: PartialEq {
|
pub trait Convert: PartialEq {
|
||||||
/// conversion factor between this and SI value
|
/// conversion factor between this and SI value
|
||||||
fn as_si(&self) -> Result<f64, Error>;
|
fn as_si(&self) -> Result<f64, Error>;
|
||||||
|
|||||||
+1
-13
@@ -1,5 +1,5 @@
|
|||||||
use crate::error::Error;
|
use crate::error::Error;
|
||||||
use crate::ome::{ExperimentType, MetadataOnly, MicrobeamManipulationType, XmlAnnotationValue};
|
use crate::ome::{ExperimentType, MetadataOnly, MicrobeamManipulationType};
|
||||||
use pyo3::prelude::*;
|
use pyo3::prelude::*;
|
||||||
use pyo3::types::PyTuple;
|
use pyo3::types::PyTuple;
|
||||||
|
|
||||||
@@ -96,18 +96,6 @@ impl PyDisplay for MicrobeamManipulationType {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl<'a, 'py> FromPyObject<'a, 'py> for XmlAnnotationValue {
|
|
||||||
type Error = Error;
|
|
||||||
|
|
||||||
fn extract(_obj: Borrowed<'a, 'py, PyAny>) -> Result<Self, Self::Error> {
|
|
||||||
Ok(Self {})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl PyDisplay for XmlAnnotationValue {
|
|
||||||
fn py_str(&self) -> String {
|
|
||||||
"XmlAnnotationValue".to_string()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl PyDisplay for ExperimentType {
|
impl PyDisplay for ExperimentType {
|
||||||
fn py_str(&self) -> String {
|
fn py_str(&self) -> String {
|
||||||
format!(
|
format!(
|
||||||
|
|||||||
@@ -0,0 +1,189 @@
|
|||||||
|
//! XML boundaries for retaining arbitrary XMLAnnotation payloads.
|
||||||
|
use crate::error::Error;
|
||||||
|
use quick_xml::events::Event;
|
||||||
|
use serde::{Serialize, de::DeserializeOwned};
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
|
pub(crate) fn from_str<T: DeserializeOwned>(xml: &str) -> Result<T, Error> {
|
||||||
|
Ok(quick_xml::de::from_str(&rewrite_values(xml, false)?)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn to_string<T: Serialize>(value: &T) -> Result<String, Error> {
|
||||||
|
rewrite_values(&quick_xml::se::to_string(value)?, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serde sees an escaped string; the public XML API sees an XML fragment.
|
||||||
|
fn rewrite_values(xml: &str, writing: bool) -> Result<String, Error> {
|
||||||
|
let mut reader = quick_xml::Reader::from_str(xml);
|
||||||
|
let mut stack: Vec<(Vec<u8>, BTreeMap<String, String>)> = Vec::new();
|
||||||
|
let mut output = String::new();
|
||||||
|
let mut copied = 0;
|
||||||
|
loop {
|
||||||
|
match reader.read_event().map_err(quick_xml::DeError::from)? {
|
||||||
|
Event::Start(ref start) => {
|
||||||
|
let name = start.local_name();
|
||||||
|
let is_value = (name.as_ref() == b"Value"
|
||||||
|
&& stack
|
||||||
|
.last()
|
||||||
|
.is_some_and(|(name, _)| name.as_slice() == b"XMLAnnotation"))
|
||||||
|
|| (stack.is_empty() && name.as_ref() == b"XmlAnnotationValue");
|
||||||
|
let mut namespaces = stack.last().map(|(_, ns)| ns.clone()).unwrap_or_default();
|
||||||
|
for attribute in start.attributes() {
|
||||||
|
let attribute = attribute
|
||||||
|
.map_err(quick_xml::Error::from)
|
||||||
|
.map_err(quick_xml::DeError::from)?;
|
||||||
|
let key = attribute.key.as_ref();
|
||||||
|
if key == b"xmlns" || key.starts_with(b"xmlns:") {
|
||||||
|
namespaces.insert(
|
||||||
|
String::from_utf8_lossy(key).into_owned(),
|
||||||
|
quick_xml::escape::escape(
|
||||||
|
attribute
|
||||||
|
.decoded_and_normalized_value(
|
||||||
|
quick_xml::XmlVersion::Implicit1_0,
|
||||||
|
reader.decoder(),
|
||||||
|
)
|
||||||
|
.map_err(quick_xml::DeError::from)?,
|
||||||
|
)
|
||||||
|
.into_owned(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if is_value {
|
||||||
|
let inner_start = reader.buffer_position() as usize;
|
||||||
|
let mut depth = 0;
|
||||||
|
let inner_end = loop {
|
||||||
|
let position = reader.buffer_position() as usize;
|
||||||
|
match reader.read_event().map_err(quick_xml::DeError::from)? {
|
||||||
|
Event::Start(_) => depth += 1,
|
||||||
|
Event::End(_) if depth == 0 => break position,
|
||||||
|
Event::End(_) => depth -= 1,
|
||||||
|
Event::Eof => return Err(quick_xml::DeError::UnexpectedEof.into()),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
output.push_str(&xml[copied..inner_start]);
|
||||||
|
let inner = &xml[inner_start..inner_end];
|
||||||
|
if writing {
|
||||||
|
let fragment = quick_xml::escape::unescape(inner)
|
||||||
|
.map_err(|error| quick_xml::DeError::Custom(error.to_string()))?;
|
||||||
|
validate_fragment(&fragment)?;
|
||||||
|
output.push_str(&fragment);
|
||||||
|
} else {
|
||||||
|
let fragment = retain_namespaces(inner, &namespaces)?;
|
||||||
|
output.push_str(&quick_xml::escape::escape(fragment));
|
||||||
|
}
|
||||||
|
copied = inner_end;
|
||||||
|
} else {
|
||||||
|
stack.push((name.as_ref().to_vec(), namespaces));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Event::End(_) => {
|
||||||
|
stack.pop();
|
||||||
|
}
|
||||||
|
Event::Eof => break,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
output.push_str(&xml[copied..]);
|
||||||
|
Ok(output)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_fragment(fragment: &str) -> Result<(), Error> {
|
||||||
|
let wrapped = format!("<Value>{fragment}</Value>");
|
||||||
|
let mut reader = quick_xml::Reader::from_str(&wrapped);
|
||||||
|
let mut depth = 0;
|
||||||
|
loop {
|
||||||
|
match reader.read_event().map_err(quick_xml::DeError::from)? {
|
||||||
|
Event::Start(start) => {
|
||||||
|
for attribute in start.attributes() {
|
||||||
|
attribute
|
||||||
|
.map_err(quick_xml::Error::from)
|
||||||
|
.map_err(quick_xml::DeError::from)?;
|
||||||
|
}
|
||||||
|
depth += 1;
|
||||||
|
}
|
||||||
|
Event::Empty(start) => {
|
||||||
|
for attribute in start.attributes() {
|
||||||
|
attribute
|
||||||
|
.map_err(quick_xml::Error::from)
|
||||||
|
.map_err(quick_xml::DeError::from)?;
|
||||||
|
}
|
||||||
|
if depth == 0 {
|
||||||
|
return Err(quick_xml::DeError::Custom(
|
||||||
|
"XML fragment escapes its Value element".into(),
|
||||||
|
)
|
||||||
|
.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Event::End(_) => {
|
||||||
|
depth -= 1;
|
||||||
|
if depth == 0 && reader.buffer_position() as usize != wrapped.len() {
|
||||||
|
return Err(quick_xml::DeError::Custom(
|
||||||
|
"XML fragment escapes its Value element".into(),
|
||||||
|
)
|
||||||
|
.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Event::DocType(_) | Event::Decl(_) => {
|
||||||
|
return Err(quick_xml::DeError::Custom(
|
||||||
|
"XML declarations and DTDs are not allowed in annotation fragments".into(),
|
||||||
|
)
|
||||||
|
.into());
|
||||||
|
}
|
||||||
|
Event::Eof => break,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if depth != 0 {
|
||||||
|
return Err(quick_xml::DeError::UnexpectedEof.into());
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retain ancestor namespace bindings on top-level payload elements.
|
||||||
|
fn retain_namespaces(
|
||||||
|
fragment: &str,
|
||||||
|
namespaces: &BTreeMap<String, String>,
|
||||||
|
) -> Result<String, Error> {
|
||||||
|
let mut reader = quick_xml::Reader::from_str(fragment);
|
||||||
|
let mut output = String::new();
|
||||||
|
let mut copied = 0;
|
||||||
|
let mut depth = 0;
|
||||||
|
loop {
|
||||||
|
let before = reader.buffer_position() as usize;
|
||||||
|
let event = reader.read_event().map_err(quick_xml::DeError::from)?;
|
||||||
|
let is_start = matches!(event, Event::Start(_));
|
||||||
|
match event {
|
||||||
|
Event::Start(ref start) | Event::Empty(ref start) if depth == 0 => {
|
||||||
|
let mut declared = Vec::new();
|
||||||
|
for attribute in start.attributes() {
|
||||||
|
let attribute = attribute
|
||||||
|
.map_err(quick_xml::Error::from)
|
||||||
|
.map_err(quick_xml::DeError::from)?;
|
||||||
|
declared.push(String::from_utf8_lossy(attribute.key.as_ref()).into_owned());
|
||||||
|
}
|
||||||
|
let insertion = before + 1 + start.name().as_ref().len();
|
||||||
|
output.push_str(&fragment[copied..insertion]);
|
||||||
|
for (name, value) in namespaces {
|
||||||
|
if !declared.contains(name)
|
||||||
|
&& (name == "xmlns"
|
||||||
|
|| fragment
|
||||||
|
.contains(&format!("{}:", name.trim_start_matches("xmlns:"))))
|
||||||
|
{
|
||||||
|
output.push_str(&format!(" {name}=\"{value}\""));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
copied = insertion;
|
||||||
|
if is_start {
|
||||||
|
depth += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Event::Start(_) => depth += 1,
|
||||||
|
Event::End(_) => depth -= 1,
|
||||||
|
Event::Eof => break,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
output.push_str(&fragment[copied..]);
|
||||||
|
Ok(output)
|
||||||
|
}
|
||||||
Reference in new issue
Block a user