use std::path::Path;
use std::sync::Arc;
use energyml_core::error::{Error, Result};
use energyml_core::registry::Registry;
use quick_xml::events::Event;
pub fn save_reindexed(
store: &crate::EpcStore,
out: &Path,
registry: Arc<Registry>,
options: &crate::SaveOptions,
) -> crate::Result<Vec<crate::Diagnostic>> {
let temporary = out.with_extension("relabel-pass1.epc");
store.save_as(&temporary, options)?;
let reopened = match crate::EpcStore::open(&temporary, registry) {
Ok(store) => store,
Err(e) => {
return Err(crate::Error::Package(format!(
"the relabelled parts were written to {} but it could not be reopened to rebuild the relationships, so that byte-perfect package is left there: {e}",
temporary.display()
)));
}
};
let diagnostics = match reopened.save_as(out, options) {
Ok(diagnostics) => diagnostics,
Err(e) => {
return Err(crate::Error::Package(format!(
"the relabelled parts were written to {} but the relationships could not be rebuilt, so that byte-perfect package is left there: {e}",
temporary.display()
)));
}
};
let _ = std::fs::remove_file(&temporary);
Ok(diagnostics)
}
pub fn save_reindexed_bytes(
store: &crate::EpcStore,
registry: Arc<Registry>,
options: &crate::SaveOptions,
) -> std::result::Result<Vec<u8>, (String, Vec<u8>)> {
let mut first = std::io::Cursor::new(Vec::new());
if let Err(e) = store.write_to(&mut first, options) {
return Err((
format!("the relabelled package could not be written: {e}"),
Vec::new(),
));
}
let bytes = first.into_inner();
let reopened = match crate::EpcStore::from_reader(
Box::new(std::io::Cursor::new(bytes.clone())),
"relabel-pass1.epc",
registry,
) {
Ok(store) => store,
Err(e) => {
return Err((
format!(
"the parts were relabelled but the package could not be reopened to rebuild its relationships, so what comes back is byte-perfect with the index it had: {e}"
),
bytes,
));
}
};
let mut second = std::io::Cursor::new(Vec::new());
if let Err(e) = reopened.write_to(&mut second, options) {
return Err((
format!(
"the parts were relabelled but the relationships could not be rebuilt, so what comes back is byte-perfect with the index it had: {e}"
),
bytes,
));
}
Ok(second.into_inner())
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Survey {
pub package: String,
pub targets: Vec<(String, String)>,
pub versions: Vec<String>,
pub undecided: usize,
}
impl Survey {
pub fn refusal(&self) -> Option<String> {
(self.versions.len() > 1).then(|| {
format!(
"this package holds `{}` parts on {} releases ({}) - an EPC may mix them \
(EPC §3.2), so say which release to relabel",
self.package,
self.versions.len(),
self.versions.join(", ")
)
})
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Outcome {
pub replaced: Vec<(String, String)>,
pub added: Vec<String>,
pub already: usize,
pub failed: Vec<String>,
}
impl Outcome {
pub fn changed(&self) -> usize {
self.replaced.len() + self.added.len()
}
}
pub fn survey(store: &crate::EpcStore, package: &str, from: Option<&str>) -> Survey {
let wanted = package.trim().to_ascii_lowercase();
let mut targets = Vec::new();
let mut versions: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
let mut undecided = 0usize;
for summary in store.resources() {
match energyml_core::identifiers::QualifiedType::parse(&summary.qualified_type) {
Ok(qt) if qt.domain.eq_ignore_ascii_case(&wanted) => {
let dotted = qt.dotted_version();
versions.insert(dotted.clone());
if let Some(only) = from
&& !energyml_core::naming::version_eq(&dotted, only)
{
continue;
}
targets.push((summary.locator.clone(), summary.qualified_type.clone()));
}
Ok(_) => {}
Err(_) => undecided += 1,
}
}
Survey {
package: wanted,
targets,
versions: versions.into_iter().collect(),
undecided,
}
}
pub fn apply(store: &mut crate::EpcStore, survey: &Survey, version: &str) -> Outcome {
let mut out = Outcome::default();
for (part, qualified) in &survey.targets {
let bytes = match store.raw_part(part) {
Ok(bytes) => bytes,
Err(e) => {
out.failed.push(format!("{part}: cannot be read as bytes: {e}"));
continue;
}
};
match set_schema_version(&bytes, version) {
Ok((_, Relabel::Unchanged)) => out.already += 1,
Ok((written, Relabel::Replaced { was })) => {
out.replaced.push((qualified.clone(), was));
store.set_raw_part(part, written);
}
Ok((written, Relabel::Added)) => {
out.added.push(qualified.clone());
store.set_raw_part(part, written);
}
Err(e) => out.failed.push(format!("{part}: {e}")),
}
}
out
}
const ATTRIBUTE: &[u8] = b"schemaVersion";
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Relabel {
Unchanged,
Replaced {
was: String,
},
Added,
}
impl Relabel {
pub fn changed(&self) -> bool {
!matches!(self, Relabel::Unchanged)
}
}
pub fn set_schema_version(xml: &[u8], version: &str) -> Result<(Vec<u8>, Relabel)> {
if version.trim().is_empty() {
return Err(Error::parse(version, "schemaVersion", "must not be empty"));
}
if let Some(bad) = version.chars().find(|c| matches!(c, '"' | '\'' | '<' | '&')) {
return Err(Error::parse(
version,
"schemaVersion",
format!("contains `{bad}`, which an attribute value cannot carry unescaped"),
));
}
let (open, close) = root_tag(xml)?;
let tag = &xml[open..close];
match find_attribute(tag) {
Some(span) => {
let held = std::str::from_utf8(&tag[span.clone()])
.map_err(|_| Error::parse("", "schemaVersion", "value is not UTF-8"))?;
if held == version {
return Ok((xml.to_vec(), Relabel::Unchanged));
}
let was = held.to_string();
let mut out = Vec::with_capacity(xml.len() + version.len());
out.extend_from_slice(&xml[..open + span.start]);
out.extend_from_slice(version.as_bytes());
out.extend_from_slice(&xml[open + span.end..]);
Ok((out, Relabel::Replaced { was }))
}
None => {
let at = open + name_end(tag);
let mut out = Vec::with_capacity(xml.len() + version.len() + 20);
out.extend_from_slice(&xml[..at]);
out.extend_from_slice(b" ");
out.extend_from_slice(ATTRIBUTE);
out.extend_from_slice(b"=\"");
out.extend_from_slice(version.as_bytes());
out.extend_from_slice(b"\"");
out.extend_from_slice(&xml[at..]);
Ok((out, Relabel::Added))
}
}
}
fn root_tag(xml: &[u8]) -> Result<(usize, usize)> {
let mut reader = quick_xml::Reader::from_reader(xml);
let mut buf = Vec::new();
loop {
match reader.read_event_into(&mut buf) {
Ok(Event::Start(_)) | Ok(Event::Empty(_)) => {
let close = reader.buffer_position() as usize;
let open = xml[..close]
.iter()
.rposition(|b| *b == b'<')
.ok_or_else(|| Error::parse("", "root element", "no start tag"))?;
return Ok((open, close));
}
Ok(Event::Eof) => {
return Err(Error::parse("", "root element", "the part has no elements"));
}
Ok(_) => buf.clear(),
Err(e) => return Err(Error::parse("", "root element", e.to_string())),
}
}
}
fn name_end(tag: &[u8]) -> usize {
let mut at = 1; while at < tag.len() && !tag[at].is_ascii_whitespace() && tag[at] != b'>' && tag[at] != b'/' {
at += 1;
}
at
}
fn find_attribute(tag: &[u8]) -> Option<std::ops::Range<usize>> {
let mut at = name_end(tag);
while at < tag.len() {
if !tag[at].is_ascii_whitespace() {
at += 1;
continue;
}
let name = at + 1;
if !tag[name..].starts_with(ATTRIBUTE) {
at += 1;
continue;
}
let mut cursor = name + ATTRIBUTE.len();
while cursor < tag.len() && tag[cursor].is_ascii_whitespace() {
cursor += 1;
}
if cursor >= tag.len() || tag[cursor] != b'=' {
at += 1;
continue;
}
cursor += 1;
while cursor < tag.len() && tag[cursor].is_ascii_whitespace() {
cursor += 1;
}
let quote = *tag.get(cursor)?;
if quote != b'"' && quote != b'\'' {
return None;
}
cursor += 1;
let end = cursor + tag[cursor..].iter().position(|b| *b == quote)?;
return Some(cursor..end);
}
None
}
#[cfg(test)]
mod tests {
use super::*;
fn text(bytes: &[u8]) -> String {
String::from_utf8(bytes.to_vec()).expect("utf-8")
}
#[test]
fn an_existing_declaration_is_replaced_and_nothing_else_moves() {
let xml = br#"<?xml version="1.0"?>
<Feature xmlns="urn:x" uuid="abc" schemaVersion="2.2"><Citation/></Feature>"#;
let (out, what) = set_schema_version(xml, "2.2_dev3").expect("relabels");
assert_eq!(
what,
Relabel::Replaced {
was: "2.2".to_string()
}
);
assert!(text(&out).contains(r#"schemaVersion="2.2_dev3""#));
assert_eq!(
text(&out).replace(r#"schemaVersion="2.2_dev3""#, r#"schemaVersion="2.2""#),
text(xml),
"only the one value changed"
);
}
#[test]
fn an_absent_declaration_is_added_after_the_element_name() {
let xml = br#"<Feature xmlns="urn:x" uuid="abc"/>"#;
let (out, what) = set_schema_version(xml, "2.2_dev3").expect("relabels");
assert_eq!(what, Relabel::Added);
assert_eq!(
text(&out),
r#"<Feature schemaVersion="2.2_dev3" xmlns="urn:x" uuid="abc"/>"#
);
}
#[test]
fn a_part_already_saying_it_is_left_untouched() {
let xml = br#"<Feature schemaVersion="2.2_dev3"/>"#;
let (out, what) = set_schema_version(xml, "2.2_dev3").expect("relabels");
assert_eq!(what, Relabel::Unchanged);
assert!(!what.changed());
assert_eq!(out, xml.to_vec(), "an unchanged part is copied, not rebuilt");
}
#[test]
fn single_quotes_are_read_and_the_style_is_kept() {
let xml = br#"<Feature schemaVersion='2.2' uuid='a'/>"#;
let (out, _) = set_schema_version(xml, "2.2_dev3").expect("relabels");
assert_eq!(text(&out), r#"<Feature schemaVersion='2.2_dev3' uuid='a'/>"#);
}
#[test]
fn a_comment_before_the_root_does_not_become_the_root() {
let xml = br#"<?xml version="1.0"?><!-- <NotTheRoot schemaVersion="9"/> -->
<Feature schemaVersion="2.2"/>"#;
let (out, what) = set_schema_version(xml, "2.2_dev3").expect("relabels");
assert_eq!(
what,
Relabel::Replaced {
was: "2.2".to_string()
}
);
assert!(
text(&out).contains(r#"<!-- <NotTheRoot schemaVersion="9"/> -->"#),
"the comment is untouched: {}",
text(&out)
);
assert!(text(&out).contains(r#"<Feature schemaVersion="2.2_dev3"/>"#));
}
#[test]
fn an_attribute_merely_ending_in_the_name_is_not_it() {
let xml = br#"<Feature xschemaVersion="9" schemaVersion="2.2"/>"#;
let (out, _) = set_schema_version(xml, "2.2_dev3").expect("relabels");
assert!(text(&out).contains(r#"xschemaVersion="9""#), "{}", text(&out));
assert!(text(&out).contains(r#"schemaVersion="2.2_dev3""#));
}
#[test]
fn a_version_an_attribute_cannot_carry_is_refused_not_escaped() {
let xml = br#"<Feature schemaVersion="2.2"/>"#;
for bad in ["2.2\"dev", "2.2&dev", "2.2<dev", " "] {
assert!(
set_schema_version(xml, bad).is_err(),
"`{bad}` must be refused rather than written"
);
}
}
#[test]
fn a_part_with_no_element_at_all_is_an_error() {
assert!(set_schema_version(b"<?xml version=\"1.0\"?>", "2.2").is_err());
assert!(set_schema_version(b"", "2.2").is_err());
}
}