Browse / github.com/storytold/gridcraft / patch
Dependencies: quick-xml 0.41 to 0.42 (xlsx reads names and values as &str)
Dependencies: quick-xml 0.41 to 0.42 (xlsx reads names and values as &str) ## What was wrong The dependabot group update (storytold/gridcraft#2600) fails the gate: quick-xml 0.42 moved its API from bytes to `&str`, so `gridcraft-xlsx` stopped compiling (64 errors in `ods.rs`, `xlsb.rs`, `xml.rs`, `passthrough.rs`). `apps/gridcraft-cli/src/check.rs` uses the same API and would have failed next. ## Cause In 0.42 element and attribute names, namespaces, attribute values, CDATA and entity references are `&str` / `Cow<str>`, and `BytesText::xml_content` can no longer fail. `BytesCData::decode` is gone. ## Fix Only the quick-xml call sites change, with no behaviour chang
- Status
- open
- Score
- 0.00
- Adopters
- 0 other operators
- Author
- Harrison-Booth (imported from pull request #2665, open)
- Base
45f5ab18e4e57bf50ee88d2526deb4b87c5b24cb- Size
- +99 −113 in 7 files
- Review
- pending
- Shared
- 2026-10-28
- Id
c50d379539d8dd1f6fd2acb8
How it went
No other operator has reported using it yet.
Diff
raw · Cargo.lock Cargo.toml apps/gridcraft-cli/src/check/package.rs crates/xlsx/src/ods.rs crates/xlsx/src/passthrough/read.rs crates/xlsx/src/xlsb.rs crates/xlsx/src/xml.rs
@@ -1990,7 +1990,7 @@ dependencies = [ "gridcraft-engine", "gridcraft-mcp", "log",- "quick-xml 0.41.0",+ "quick-xml 0.42.0", "serde_json", "zip 3.0.0", ]@@ -2125,7 +2125,7 @@ dependencies = [ "gridcraft-core", "gridcraft-formula", "gridcraft-model",- "quick-xml 0.41.0",+ "quick-xml 0.42.0", "sha1 0.11.0", "sha2 0.11.1", "thiserror 2.0.21",@@ -37,7 +37,7 @@ log = "0.4" chrono = { version = "0.4.45", default-features = false, features = ["std"] } rayon = "1" regex = { version = "1", default-features = false, features = ["std", "unicode", "perf"] }-quick-xml = "0.41"+quick-xml = "0.42" zip = { version = "3", default-features = false, features = ["deflate"] } sha2 = { version = "0.11", default-features = false } # Decrypting password-protected workbooks ([MS-OFFCRYPTO]): AES and SHA-1, pure Rust (wasm-safe).@@ -12,11 +12,8 @@ pub(super) const REBUILT_KINDS: [&str; 1] = ["calcchain"]; // ---------------------------------------------------------------- package validation -fn local_name(name: &[u8]) -> &[u8] {- match name.iter().rposition(|&b| b == b':') {- Some(i) => name.get(i + 1..).unwrap_or(name),- None => name,- }+fn local_name(name: &str) -> &str {+ name.rsplit(':').next().unwrap_or(name) } /// `(element local name, attributes)` for every start/empty element of an XML part.@@ -35,12 +32,12 @@ fn xml_elements(xml: &[u8]) -> Result<Vec<(String, Vec<(String, String)>)>, Stri .map(|a| { let v = match a.normalized_value(quick_xml::XmlVersion::Explicit1_0) { Ok(v) => v.into_owned(),- Err(_) => String::from_utf8_lossy(&a.value).into_owned(),+ Err(_) => a.value.to_string(), };- (String::from_utf8_lossy(local_name(a.key.as_ref())).into_owned(), v)+ (local_name(a.key.as_ref()).to_string(), v) }) .collect();- out.push((String::from_utf8_lossy(local_name(e.name().as_ref())).into_owned(), attrs));+ out.push((local_name(e.name().as_ref()).to_string(), attrs)); } Ok(Event::Eof) => break, Ok(_) => {}@@ -301,9 +298,9 @@ fn holds_something_uncached(zip: &mut Zip<'_>, name: &str, marker: &'static str, let take = f.size().min(MAX_XML_PART).min(*budget); *budget -= take; let mut reader = quick_xml::Reader::from_reader(std::io::BufReader::new(f.take(take)));- let wanted: Option<&'static [u8]> = match marker {- "comment" => Some(b"comment"),- "shape" => Some(b"shape"),+ let wanted: Option<&'static str> = match marker {+ "comment" => Some("comment"),+ "shape" => Some("shape"), _ => None, }; let mut buf = Vec::new();@@ -18,11 +18,11 @@ use crate::package::{MAX_ENTRIES, Package}; use crate::{IoError, ReadReport}; const MIME: &[u8] = b"application/vnd.oasis.opendocument.spreadsheet";-const OFFICE: &[u8] = b"urn:oasis:names:tc:opendocument:xmlns:office:1.0";-const TABLE: &[u8] = b"urn:oasis:names:tc:opendocument:xmlns:table:1.0";-const TEXT: &[u8] = b"urn:oasis:names:tc:opendocument:xmlns:text:1.0";-const CALC_EXT: &[u8] = b"urn:org:documentfoundation:names:experimental:calc:xmlns:calcext:1.0";-const MANIFEST: &[u8] = b"urn:oasis:names:tc:opendocument:xmlns:manifest:1.0";+const OFFICE: &str = "urn:oasis:names:tc:opendocument:xmlns:office:1.0";+const TABLE: &str = "urn:oasis:names:tc:opendocument:xmlns:table:1.0";+const TEXT: &str = "urn:oasis:names:tc:opendocument:xmlns:text:1.0";+const CALC_EXT: &str = "urn:org:documentfoundation:names:experimental:calc:xmlns:calcext:1.0";+const MANIFEST: &str = "urn:oasis:names:tc:opendocument:xmlns:manifest:1.0"; const MAX_CELLS: u64 = 1_000_000; const MAX_MERGES: u64 = 100_000; const MAX_CELL_TEXT: usize = 1024 * 1024;@@ -64,11 +64,11 @@ pub fn read_ods(bytes: &[u8]) -> Result<(Workbook, ReadReport), IoError> { Ok((import.wb, import.report)) } -fn is_ns(ns: ResolveResult<'_>, expected: &[u8]) -> bool {+fn is_ns(ns: ResolveResult<'_>, expected: &str) -> bool { matches!(ns, ResolveResult::Bound(n) if n.as_ref() == expected) } -fn attr(reader: &mut NsReader<&[u8]>, e: &BytesStart<'_>, ns: &[u8], name: &[u8]) -> Result<Option<String>, IoError> {+fn attr(reader: &mut NsReader<&[u8]>, e: &BytesStart<'_>, ns: &str, name: &str) -> Result<Option<String>, IoError> { let mut found = None; for a in e.attributes() { let a = a.map_err(|e| IoError::Xml(e.to_string()))?;@@ -108,11 +108,11 @@ fn check_manifest(bytes: &[u8]) -> Result<(), IoError> { match &event { Event::Start(e) | Event::Empty(e) => { let (ns, name) = reader.resolver_mut().resolve_element(e.name());- if is_ns(ns.clone(), MANIFEST) && name.as_ref() == b"encryption-data" {+ if is_ns(ns.clone(), MANIFEST) && name.as_ref() == "encryption-data" { return Err(IoError::Format("encrypted ODS files are not supported".into())); } if depth == 0 {- if root_seen || !is_ns(ns, MANIFEST) || name.as_ref() != b"manifest" {+ if root_seen || !is_ns(ns, MANIFEST) || name.as_ref() != "manifest" { return Err(xml_error("expected one manifest:manifest root")); } root_seen = true;@@ -159,29 +159,29 @@ fn tag(reader: &mut NsReader<&[u8]>, e: &BytesStart<'_>) -> Tag { let local = name.as_ref(); if is_ns(ns.clone(), OFFICE) { return match local {- b"document-content" => Tag::Document,- b"body" => Tag::Body,- b"spreadsheet" => Tag::Spreadsheet,+ "document-content" => Tag::Document,+ "body" => Tag::Body,+ "spreadsheet" => Tag::Spreadsheet, _ => Tag::Other, }; } if is_ns(ns.clone(), TABLE) { return match local {- b"table" => Tag::Table,- b"table-row-group" | b"table-header-rows" | b"table-rows" => Tag::Rows,- b"table-row" => Tag::Row,- b"table-cell" => Tag::Cell,- b"covered-table-cell" => Tag::Covered,+ "table" => Tag::Table,+ "table-row-group" | "table-header-rows" | "table-rows" => Tag::Rows,+ "table-row" => Tag::Row,+ "table-cell" => Tag::Cell,+ "covered-table-cell" => Tag::Covered, _ => Tag::Other, }; } if is_ns(ns, TEXT) { return match local {- b"p" | b"h" => Tag::Paragraph,- b"span" | b"a" | b"meta" | b"meta-field" | b"ruby" | b"ruby-base" => Tag::Inline,- b"s" => Tag::Space,- b"tab" => Tag::Tab,- b"line-break" => Tag::Break,+ "p" | "h" => Tag::Paragraph,+ "span" | "a" | "meta" | "meta-field" | "ruby" | "ruby-base" => Tag::Inline,+ "s" => Tag::Space,+ "tab" => Tag::Tab,+ "line-break" => Tag::Break, _ => Tag::Other, }; }@@ -270,22 +270,22 @@ impl Import { } Event::End(_) => self.end()?, Event::Text(t) => {- let text = t.xml_content(XmlVersion::Explicit1_0).map_err(xml_error)?;+ let text = t.xml_content(XmlVersion::Explicit1_0); if self.stack.is_empty() && !text.trim().is_empty() { return Err(xml_error("text outside the document")); } self.append(&text)?; }- Event::CData(t) => self.append(&t.decode().map_err(xml_error)?)?,+ Event::CData(t) => self.append(&t)?, Event::GeneralRef(r) => { let ch = match r.resolve_char_ref().map_err(xml_error)? { Some(c) => c, None => match r.as_ref() {- b"amp" => '&',- b"lt" => '<',- b"gt" => '>',- b"apos" => '\'',- b"quot" => '"',+ "amp" => '&',+ "lt" => '<',+ "gt" => '>',+ "apos" => '\'',+ "quot" => '"', _ => return Err(xml_error("unsupported XML entity")), }, };@@ -316,7 +316,7 @@ impl Import { if self.wb.sheets.len() >= MAX_SHEETS { return Err(IoError::TooLarge("ODS contains more than 1024 sheets".into())); }- let original = attr(reader, e, TABLE, b"name")?.unwrap_or_default();+ let original = attr(reader, e, TABLE, "name")?.unwrap_or_default(); let mut name = crate::read::unique_name(&self.wb, original.trim_matches('\''), self.wb.sheets.len()); if self.wb.check_sheet_name(&name, None).is_err() { name = self.wb.next_sheet_name();@@ -328,30 +328,30 @@ impl Import { } Tag::Row if self.sheet.as_ref().is_some_and(|s| self.stack.get(s.depth + 1..).is_some_and(|p| p.iter().all(|t| *t == Tag::Rows))) => { self.row =- Some(RowDraft { depth, repeat: positive(attr(reader, e, TABLE, b"number-rows-repeated")?, 1, u32::MAX)?, col: 0, cells: vec![] });+ Some(RowDraft { depth, repeat: positive(attr(reader, e, TABLE, "number-rows-repeated")?, 1, u32::MAX)?, col: 0, cells: vec![] }); } Tag::Cell | Tag::Covered if self.row.as_ref().is_some_and(|r| depth == r.depth + 1) => {- let kind = if attr(reader, e, CALC_EXT, b"value-type")?.as_deref() == Some("error") {+ let kind = if attr(reader, e, CALC_EXT, "value-type")?.as_deref() == Some("error") { Some("error".into()) } else {- attr(reader, e, OFFICE, b"value-type")?+ attr(reader, e, OFFICE, "value-type")? };- let key: &[u8] = match kind.as_deref() {- Some("boolean") => b"boolean-value",- Some("date") => b"date-value",- Some("time") => b"time-value",- Some("string") => b"string-value",- _ => b"value",+ let key: &str = match kind.as_deref() {+ Some("boolean") => "boolean-value",+ Some("date") => "date-value",+ Some("time") => "time-value",+ Some("string") => "string-value",+ _ => "value", }; self.cell = Some(CellDraft { depth,- repeat: positive(attr(reader, e, TABLE, b"number-columns-repeated")?, 1, u32::MAX)?,- width: positive(attr(reader, e, TABLE, b"number-columns-spanned")?, 1, MAX_COLS)?,- height: positive(attr(reader, e, TABLE, b"number-rows-spanned")?, 1, MAX_ROWS)?,+ repeat: positive(attr(reader, e, TABLE, "number-columns-repeated")?, 1, u32::MAX)?,+ width: positive(attr(reader, e, TABLE, "number-columns-spanned")?, 1, MAX_COLS)?,+ height: positive(attr(reader, e, TABLE, "number-rows-spanned")?, 1, MAX_ROWS)?, covered: t == Tag::Covered, kind, raw: attr(reader, e, OFFICE, key)?,- formula: attr(reader, e, TABLE, b"formula")?.is_some(),+ formula: attr(reader, e, TABLE, "formula")?.is_some(), text: String::new(), paragraph: None, paragraphs: 0,@@ -372,7 +372,7 @@ impl Import { } Tag::Space | Tag::Tab | Tag::Break if self.captures_text() => { let count = if t == Tag::Space {- let n = attr(reader, e, TEXT, b"c")?.map(|s| s.parse::<u32>()).transpose().map_err(xml_error)?.unwrap_or(1);+ let n = attr(reader, e, TEXT, "c")?.map(|s| s.parse::<u32>()).transpose().map_err(xml_error)?.unwrap_or(1); if n as usize > MAX_CELL_TEXT { return Err(IoError::TooLarge("ODS repeated spaces exceed 1 MiB".into())); }@@ -444,40 +444,39 @@ fn scan_sheet(bytes: &[u8]) -> Option<SheetScan> { Event::Start(e) if !in_root => { in_root = true; for a in e.attributes().with_checks(false).flatten() {- if let Some(p) = a.key.as_ref().strip_prefix(b"xmlns:")- && let (Ok(p), Ok(v)) = (std::str::from_utf8(p), std::str::from_utf8(&a.value))- && !v.contains('"')+ if let Some(p) = a.key.as_ref().strip_prefix("xmlns:")+ && !a.value.contains('"') {- decls.push((p.to_string(), v.to_string()));+ decls.push((p.to_string(), a.value.to_string())); } } } Event::Start(e) => {- let local = e.local_name().as_ref().to_vec();- match local.as_slice() {- b"pageSetup" => out.page_setup = rel_id_attr(&e),- b"legacyDrawing" => out.legacy_drawing = rel_id_attr(&e),- b"legacyDrawingHF" => out.legacy_drawing_hf = rel_id_attr(&e),+ let local = e.local_name().as_ref().to_string();+ match local.as_str() {+ "pageSetup" => out.page_setup = rel_id_attr(&e),+ "legacyDrawing" => out.legacy_drawing = rel_id_attr(&e),+ "legacyDrawingHF" => out.legacy_drawing_hf = rel_id_attr(&e), _ => {} } reader.read_to_end(e.name()).ok()?; let end = usize::try_from(reader.buffer_position()).ok()?;- let wanted = matches!(local.as_slice(), b"picture" | b"oleObjects" | b"controls" | b"AlternateContent");+ let wanted = matches!(local.as_str(), "picture" | "oleObjects" | "controls" | "AlternateContent"); if wanted && end.saturating_sub(start) <= MAX_FRAGMENT { let frag = text.get(start..end)?; push_fragment(&mut out, frag, &local, &decls); } } Event::Empty(e) if in_root => {- let local = e.local_name().as_ref().to_vec();- if local == b"legacyDrawing" || local == b"legacyDrawingHF" || local == b"pageSetup" {+ let local = e.local_name().as_ref().to_string();+ if local == "legacyDrawing" || local == "legacyDrawingHF" || local == "pageSetup" { let id = rel_id_attr(&e);- match local.as_slice() {- b"pageSetup" => out.page_setup = id,- b"legacyDrawing" => out.legacy_drawing = id,+ match local.as_str() {+ "pageSetup" => out.page_setup = id,+ "legacyDrawing" => out.legacy_drawing = id, _ => out.legacy_drawing_hf = id, }- } else if local == b"picture"+ } else if local == "picture" && let Some(frag) = text.get(start..usize::try_from(reader.buffer_position()).ok()?) { push_fragment(&mut out, frag, &local, &decls);@@ -492,16 +491,12 @@ fn scan_sheet(bytes: &[u8]) -> Option<SheetScan> { /// The `r:id` of an element (an `id` attribute with a namespace prefix). fn rel_id_attr(e: &BytesStart<'_>) -> Option<String> {- e.attributes()- .with_checks(false)- .flatten()- .find(|a| a.key.local_name().as_ref() == b"id" && a.key.prefix().is_some())- .and_then(|a| String::from_utf8(a.value.to_vec()).ok())+ e.attributes().with_checks(false).flatten().find(|a| a.key.local_name().as_ref() == "id" && a.key.prefix().is_some()).map(|a| a.value.to_string()) } -fn push_fragment(out: &mut SheetScan, frag: &str, local: &[u8], decls: &[(String, String)]) {+fn push_fragment(out: &mut SheetScan, frag: &str, local: &str, decls: &[(String, String)]) { let frag = with_decls(frag, decls);- let slot = if local == b"AlternateContent" {+ let slot = if local == "AlternateContent" { // The kept element is the one the first choice holds. let Ok(el) = xml::parse(frag.as_bytes()) else { return }; match el.child("Choice").and_then(|c| c.children.first()) {@@ -509,7 +504,7 @@ fn push_fragment(out: &mut SheetScan, frag: &str, local: &[u8], decls: &[(String None => return, } } else {- String::from_utf8_lossy(local).into_owned()+ local.to_string() }; if SHEET_SLOTS.contains(&slot.as_str()) && !out.fragments.iter().any(|(s, _)| *s == slot) { out.fragments.push((slot, frag));@@ -634,7 +629,7 @@ pub(super) fn merge_vml(kept: &str, ours: &str) -> String { pub fn map_rel_ids(x: &str, map: &mut dyn FnMut(&str) -> Option<String>) -> Option<String> { let mut reader = Reader::from_str(x); let mut writer = Writer::new(Vec::with_capacity(x.len()));- let mut prefixes: HashSet<Vec<u8>> = HashSet::new();+ let mut prefixes: HashSet<String> = HashSet::new(); let mut depth = 0usize; loop { match reader.read_event().ok()? {@@ -661,25 +656,23 @@ pub fn map_rel_ids(x: &str, map: &mut dyn FnMut(&str) -> Option<String>) -> Opti String::from_utf8(writer.into_inner()).ok() } -fn map_start(e: &BytesStart<'_>, prefixes: &mut HashSet<Vec<u8>>, map: &mut dyn FnMut(&str) -> Option<String>) -> Option<BytesStart<'static>> {+fn map_start(e: &BytesStart<'_>, prefixes: &mut HashSet<String>, map: &mut dyn FnMut(&str) -> Option<String>) -> Option<BytesStart<'static>> { for a in e.attributes().with_checks(false) { let a = a.ok()?;- if let Some(p) = a.key.as_ref().strip_prefix(b"xmlns:")- && (a.value.as_ref() == NS_REL.as_bytes() || a.value.as_ref() == NS_REL_STRICT.as_bytes())+ if let Some(p) = a.key.as_ref().strip_prefix("xmlns:")+ && (a.value == NS_REL || a.value == NS_REL_STRICT) {- prefixes.insert(p.to_vec());+ prefixes.insert(p.to_string()); } }- let name = String::from_utf8(e.name().as_ref().to_vec()).ok()?;- let mut out = BytesStart::new(name);+ let mut out = BytesStart::new(e.name().as_ref().to_string()); for a in e.attributes().with_checks(false) { let a = a.ok()?; let key = a.key.as_ref();- let is_rel = !key.starts_with(b"xmlns") && a.key.prefix().is_some_and(|p| prefixes.contains(p.as_ref()));+ let is_rel = !key.starts_with("xmlns") && a.key.prefix().is_some_and(|p| prefixes.contains(p.as_ref())); if is_rel {- let old = std::str::from_utf8(&a.value).ok()?;- let new = map(old)?;- out.push_attribute(Attribute { key: QName(key), value: Cow::Owned(esc_attr(&new).into_bytes()) });+ let new = map(&a.value)?;+ out.push_attribute(Attribute { key: QName(key), value: Cow::Owned(esc_attr(&new)) }); } else { out.push_attribute(Attribute { key: QName(key), value: a.value.clone() }); }@@ -18,8 +18,8 @@ const MAIN: &str = "application/vnd.ms-excel.main"; const BINARY_MAIN: &str = "application/vnd.ms-excel.sheet.binary.macroEnabled.main"; const WORKSHEET: &str = "application/vnd.ms-excel.worksheet"; const STRINGS: &str = "application/vnd.ms-excel.sharedStrings";-const CT_NS: &[u8] = b"http://schemas.openxmlformats.org/package/2006/content-types";-const REL_NS: &[u8] = b"http://schemas.openxmlformats.org/package/2006/relationships";+const CT_NS: &str = "http://schemas.openxmlformats.org/package/2006/content-types";+const REL_NS: &str = "http://schemas.openxmlformats.org/package/2006/relationships"; const REL_TYPE: &str = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/"; const STRICT_REL_TYPE: &str = "http://purl.oclc.org/ooxml/officeDocument/relationships/"; const MS_REL_TYPE: &str = "http://schemas.microsoft.com/office/2006/relationships/";@@ -152,7 +152,7 @@ impl Entry { /// The two package metadata parts are flat, namespace-qualified XML documents. Keep only /// direct child entries, and reject truncated XML instead of guessing a partial relationship.-fn metadata(bytes: &[u8], ns: &[u8], root_name: &[u8]) -> Result<Vec<Entry>, IoError> {+fn metadata(bytes: &[u8], ns: &str, root_name: &str) -> Result<Vec<Entry>, IoError> { let mut reader = NsReader::from_reader(bytes); let mut depth = 0usize; let mut seen = false;@@ -172,14 +172,14 @@ fn metadata(bytes: &[u8], ns: &[u8], root_name: &[u8]) -> Result<Vec<Entry>, IoE let mut attrs = vec![]; for a in e.attributes() { let a = a.map_err(|e| IoError::Xml(e.to_string()))?;- if !a.key.as_ref().contains(&b':') {+ if !a.key.as_ref().contains(':') { attrs.push((- String::from_utf8_lossy(a.key.as_ref()).into_owned(),+ a.key.as_ref().to_string(), a.normalized_value(XmlVersion::Explicit1_0).map_err(|e| IoError::Xml(e.to_string()))?.into_owned(), )); } }- entries.push(Entry { name: String::from_utf8_lossy(local.as_ref()).into_owned(), attrs });+ entries.push(Entry { name: local.as_ref().to_string(), attrs }); } if matches!(event, Event::Start(_)) { depth += 1;@@ -189,7 +189,7 @@ fn metadata(bytes: &[u8], ns: &[u8], root_name: &[u8]) -> Result<Vec<Entry>, IoE } } Event::End(_) => depth = depth.checked_sub(1).ok_or_else(|| invalid("unexpected metadata end"))?,- Event::Text(t) if depth == 0 && !t.xml_content(XmlVersion::Explicit1_0).map_err(|e| IoError::Xml(e.to_string()))?.trim().is_empty() => {+ Event::Text(t) if depth == 0 && !t.xml_content(XmlVersion::Explicit1_0).trim().is_empty() => { return Err(invalid("text outside package metadata")); } Event::DocType(_) => return Err(invalid("DOCTYPE in package metadata is unsupported")),@@ -205,7 +205,7 @@ fn metadata(bytes: &[u8], ns: &[u8], root_name: &[u8]) -> Result<Vec<Entry>, IoE fn content_types(package: &mut Package<'_>) -> Result<Vec<Entry>, IoError> { let data = package.read_limited("[Content_Types].xml", MAX_META)?.ok_or_else(|| invalid("missing content types"))?;- metadata(&data, CT_NS, b"Types")+ metadata(&data, CT_NS, "Types") } fn check_type(types: &[Entry], path: &str, expected: &str) -> Result<(), IoError> {@@ -226,7 +226,7 @@ fn relationships(package: &mut Package<'_>, path: &str) -> Result<Vec<Rel>, IoEr let relpath = if dir.is_empty() { format!("_rels/{file}.rels") } else { format!("{dir}/_rels/{file}.rels") }; let data = package.read_limited(&relpath, MAX_META)?.ok_or_else(|| invalid(format!("missing relationships for {path}")))?; let mut out: Vec<Rel> = vec![];- for e in metadata(&data, REL_NS, b"Relationships")? {+ for e in metadata(&data, REL_NS, "Relationships")? { if e.name != "Relationship" { continue; }@@ -172,12 +172,8 @@ pub(crate) fn to_utf8(xml: &[u8]) -> std::borrow::Cow<'_, [u8]> { std::borrow::Cow::Borrowed(xml) } -fn local(name: &[u8]) -> String {- let n = match name.iter().rposition(|&b| b == b':') {- Some(i) => name.get(i + 1..).unwrap_or(name),- None => name,- };- String::from_utf8_lossy(n).into_owned()+fn local(name: &str) -> String {+ name.rsplit(':').next().unwrap_or(name).to_string() } fn start_el(e: &BytesStart<'_>) -> El {@@ -185,12 +181,12 @@ fn start_el(e: &BytesStart<'_>) -> El { for a in e.attributes().with_checks(false).flatten() { let key = a.key.as_ref(); // Namespace declarations carry no data for us.- if key == b"xmlns" || key.starts_with(b"xmlns:") {+ if key == "xmlns" || key.starts_with("xmlns:") { continue; } let v = match a.normalized_value(XmlVersion::Explicit1_0) { Ok(v) => v.into_owned(),- Err(_) => String::from_utf8_lossy(&a.value).into_owned(),+ Err(_) => a.value.to_string(), }; el.attrs.push((local(key), v)); }@@ -281,13 +277,13 @@ pub fn parse_streaming(xml: &[u8], split: &[&str], cb: &mut dyn FnMut(El) -> Res } Event::Text(t) => { if let Some(top) = stack.last_mut() {- let s = t.xml_content(XmlVersion::Explicit1_0).map_err(|e| IoError::Xml(e.to_string()))?;+ let s = t.xml_content(XmlVersion::Explicit1_0); top.text.push_str(&s); } } Event::CData(t) => { if let Some(top) = stack.last_mut() {- top.text.push_str(&String::from_utf8_lossy(&t));+ top.text.push_str(&t); } } Event::GeneralRef(r) => {@@ -295,7 +291,7 @@ pub fn parse_streaming(xml: &[u8], split: &[&str], cb: &mut dyn FnMut(El) -> Res match r.resolve_char_ref() { Ok(Some(c)) => top.text.push(c), _ => {- let name = String::from_utf8_lossy(&r).into_owned();+ let name = r.to_string(); match named_entity(&name) { Some(c) => top.text.push(c), None => {