diff --git a/Cargo.lock b/Cargo.lock index 5b58a767d..b5336d2f1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -685,7 +685,6 @@ dependencies = [ "coset", "ed25519-dalek", "extfmt", - "fast-xml", "getrandom", "glob", "hex", @@ -703,6 +702,7 @@ dependencies = [ "mp4", "pem 3.0.4", "png_pong", + "quick-xml", "rand", "rand_chacha", "rand_core 0.9.0-beta.1", @@ -1496,15 +1496,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a48fe53466ab1f4ea6303bf9d7a0ca8060778590f09fd6c3304cc817aeb9935d" -[[package]] -name = "fast-xml" -version = "0.23.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f7ffc2f9e1373cd82b4d4ce2f84f8162edd48e3932abeb19c06170b66dbbd6c" -dependencies = [ - "memchr", -] - [[package]] name = "fastrand" version = "2.3.0" @@ -3154,6 +3145,15 @@ dependencies = [ "unarray", ] +[[package]] +name = "quick-xml" +version = "0.37.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f22f29bdff3987b4d8632ef95fd6424ec7e4e0a57e2f4fc63e489e75357f6a03" +dependencies = [ + "memchr", +] + [[package]] name = "quote" version = "1.0.37" diff --git a/sdk/Cargo.toml b/sdk/Cargo.toml index db5db8594..a4924be69 100644 --- a/sdk/Cargo.toml +++ b/sdk/Cargo.toml @@ -94,7 +94,7 @@ conv = "0.3.3" coset = "0.3.1" extfmt = "0.1.1" ed25519-dalek = "2.1.1" -fast-xml = "0.23.1" +quick-xml = "0.37.1" hex = "0.4.3" # Version 1.13.0 doesn't compile under Rust < 1.75, pinning to 1.12.0 id3 = "=1.14.0" diff --git a/sdk/src/asset_handlers/svg_io.rs b/sdk/src/asset_handlers/svg_io.rs index 7a7fefcd9..3f0d7fe88 100644 --- a/sdk/src/asset_handlers/svg_io.rs +++ b/sdk/src/asset_handlers/svg_io.rs @@ -12,6 +12,7 @@ // each license. use std::{ + borrow::Cow, fs::{self, File, OpenOptions}, io::{BufReader, Cursor, Seek, SeekFrom, Write}, path::Path, @@ -19,7 +20,7 @@ use std::{ use c2pa_crypto::base64; use conv::ValueFrom; -use fast_xml::{ +use quick_xml::{ events::{BytesText, Event}, Reader, Writer, }; @@ -169,8 +170,8 @@ impl AssetIO for SvgIO { } // create manifest entry -fn create_manifest_tag(data: &[u8], with_meta: bool) -> Result> { - let mut output: Vec = Vec::with_capacity(data.len() + 256); +fn create_manifest_tag(data: &[u8], with_meta: bool) -> Result { + let output: Vec = Vec::with_capacity(data.len() + 256); let mut writer = Writer::new(Cursor::new(output)); let encoded = base64::encode(data); @@ -182,7 +183,7 @@ fn create_manifest_tag(data: &[u8], with_meta: bool) -> Result> { writer .create_element(MANIFEST) .with_attribute((MANIFEST_NS, MANIFEST_NS_VAL)) - .write_text_content(BytesText::from_plain_str(&encoded))?; + .write_text_content(BytesText::from_escaped(&encoded))?; Ok(()) }) .map_err(|_e| Error::XmlWriteError)?; @@ -190,13 +191,15 @@ fn create_manifest_tag(data: &[u8], with_meta: bool) -> Result> { writer .create_element(MANIFEST) .with_attribute((MANIFEST_NS, MANIFEST_NS_VAL)) - .write_text_content(BytesText::from_plain_str(&encoded)) + .write_text_content(BytesText::from_escaped(&encoded)) .map_err(|_e| Error::XmlWriteError)?; } - output = writer.into_inner().into_inner(); + let output = writer.into_inner().into_inner(); + let output_str = String::from_utf8(output).map_err(|_e| Error::XmlWriteError)?; + let event = Event::Text(BytesText::from_escaped(Cow::Owned(output_str))); - Ok(output) + Ok(event) } enum DetectedTagsDepth { @@ -221,9 +224,9 @@ fn detect_manifest_location( let mut output: Option> = None; loop { - match xml_reader.read_event(&mut buf) { + match xml_reader.read_event_into(&mut buf) { Ok(Event::Start(ref e)) => { - let name = String::from_utf8_lossy(e.name()).into_owned(); + let name = String::from_utf8_lossy(e.name().into_inner()).into_owned(); xml_path.push(name); if xml_path.len() == 2 && xml_path[0] == SVG && xml_path[1] == METADATA { @@ -238,18 +241,6 @@ fn detect_manifest_location( { detected_level = DetectedTagsDepth::Manifest; insertion_point = xml_reader.buffer_position(); - - let mut temp_buf = Vec::new(); - let s = xml_reader - .read_text(e.name(), &mut temp_buf) - .map_err(|_e| { - Error::InvalidAsset("XML manifest tag invalid content".to_string()) - })?; - - output = Some(base64::decode(&s).map_err(|_e| { - dbg!(_e); - Error::InvalidAsset("XML bad base64 encoding".to_string()) - })?); } if xml_path.len() == 1 && xml_path[0] == SVG { @@ -257,6 +248,23 @@ fn detect_manifest_location( insertion_point = xml_reader.buffer_position(); } } + Ok(Event::Text(e)) => { + if xml_path.len() == 3 + && xml_path[0] == SVG + && xml_path[1] == METADATA + && xml_path[2] == MANIFEST + { + let encoded_content = e + .unescape() + .map_err(|_e| { + Error::InvalidAsset("XML incorrectly escaped character".to_string()) + })? + .into_owned(); + output = Some(base64::decode(&encoded_content).map_err(|_e| { + Error::InvalidAsset("XML bad base64 encoding".to_string()) + })?); + } + } Ok(Event::End(_)) => { let _p = xml_path.pop(); } @@ -264,7 +272,9 @@ fn detect_manifest_location( Err(_) => return Err(Error::InvalidAsset("XML invalid".to_string())), _ => (), } + buf.clear(); } + let insertion_point = usize::try_from(insertion_point)?; Ok((output, detected_level, insertion_point)) } @@ -272,7 +282,7 @@ fn detect_manifest_location( fn read_xmp(input_stream: &mut dyn CAIRead) -> Result<(Option, DetectedTagsDepth, usize)> { input_stream.rewind()?; - let mut insertion_point = usize::try_from(stream_len(input_stream)?)?; + let mut insertion_point = stream_len(input_stream)?; let mut buf = Vec::new(); let buf_reader = BufReader::new(input_stream); let mut xml_reader = Reader::from_reader(buf_reader); @@ -281,9 +291,9 @@ fn read_xmp(input_stream: &mut dyn CAIRead) -> Result<(Option, DetectedT let mut output = None; loop { - match xml_reader.read_event(&mut buf) { + match xml_reader.read_event_into(&mut buf) { Ok(Event::Start(ref e)) => { - let name: String = String::from_utf8_lossy(e.name()).into_owned(); + let name: String = String::from_utf8_lossy(e.name().into_inner()).into_owned(); xml_path.push(name); if xml_path.len() == 1 && xml_path[0] == SVG { @@ -298,9 +308,7 @@ fn read_xmp(input_stream: &mut dyn CAIRead) -> Result<(Option, DetectedT } Ok(Event::PI(e)) => { let possible_insertion_point = xml_reader.buffer_position(); - let pi = e.unescape_and_decode(&xml_reader).map_err(|_e| { - Error::InvalidAsset("XML bad processing instructions".to_string()) - })?; + let pi = String::from_utf8_lossy(&e); if pi.contains(XPACKET) && pi.contains(XMP_ID) { // reconstruct opening XMP PI tag @@ -309,7 +317,7 @@ fn read_xmp(input_stream: &mut dyn CAIRead) -> Result<(Option, DetectedT detected_level = DetectedTagsDepth::Xmp; // adjust to include the opening XMP PI insertion_point = possible_insertion_point - .checked_sub(tag.len()) + .checked_sub(tag.len() as u64) .ok_or(Error::BadParam("file read out of range".into()))?; } else if pi.contains(XPACKET) { // this has read to the end of xpacket @@ -337,7 +345,9 @@ fn read_xmp(input_stream: &mut dyn CAIRead) -> Result<(Option, DetectedT Err(_) => return Err(Error::InvalidAsset("XML invalid".to_string())), _ => (), } + buf.clear(); } + let insertion_point = usize::try_from(insertion_point)?; Ok((output, detected_level, insertion_point)) } @@ -397,12 +407,10 @@ impl CAIWriter for SvgIO { match detected_tag_location { DetectedTagsDepth::Metadata => { // add manifest case - let manifest_data = create_manifest_tag(store_bytes, false)?; - loop { - match reader.read_event(&mut buf) { + match reader.read_event_into(&mut buf) { Ok(Event::Start(e)) => { - let name = String::from_utf8_lossy(e.name()).into_owned(); + let name = String::from_utf8_lossy(e.name().into_inner()).into_owned(); xml_path.push(name); // writes the event to the writer @@ -414,7 +422,7 @@ impl CAIWriter for SvgIO { if xml_path.len() == 2 && xml_path[0] == SVG && xml_path[1] == METADATA { writer - .write(&manifest_data) + .write_event(create_manifest_tag(store_bytes, false)?) .map_err(|_e| Error::XmlWriteError)?; } } @@ -425,7 +433,7 @@ impl CAIWriter for SvgIO { .write_event(Event::End(e)) .map_err(|_e| Error::XmlWriteError)?; } - Ok(e) => writer.write_event(&e).map_err(|_e| Error::XmlWriteError)?, + Ok(e) => writer.write_event(e).map_err(|_e| Error::XmlWriteError)?, Err(_e) => return Err(Error::InvalidAsset("XML invalid".to_string())), } buf.clear(); @@ -436,9 +444,9 @@ impl CAIWriter for SvgIO { let encoded = base64::encode(store_bytes); loop { - match reader.read_event(&mut buf) { + match reader.read_event_into(&mut buf) { Ok(Event::Start(e)) => { - let name = String::from_utf8_lossy(e.name()).into_owned(); + let name = String::from_utf8_lossy(e.name().into_inner()).into_owned(); xml_path.push(name); // writes the event to the writer @@ -454,7 +462,7 @@ impl CAIWriter for SvgIO { && xml_path[2] == MANIFEST { writer - .write(encoded.as_bytes()) + .write_event(Event::Text(BytesText::new(&encoded))) .map_err(|_e| Error::XmlWriteError)?; } else { writer @@ -469,7 +477,7 @@ impl CAIWriter for SvgIO { .write_event(Event::End(e)) .map_err(|_e| Error::XmlWriteError)?; } - Ok(e) => writer.write_event(&e).map_err(|_e| Error::XmlWriteError)?, + Ok(e) => writer.write_event(e).map_err(|_e| Error::XmlWriteError)?, Err(_e) => return Err(Error::InvalidAsset("XML invalid".to_string())), } buf.clear(); @@ -477,12 +485,10 @@ impl CAIWriter for SvgIO { } _ => { //add metadata & manifest case - let manifest_data = create_manifest_tag(store_bytes, true)?; - loop { - match reader.read_event(&mut buf) { + match reader.read_event_into(&mut buf) { Ok(Event::Start(e)) => { - let name = String::from_utf8_lossy(e.name()).into_owned(); + let name = String::from_utf8_lossy(e.name().into_inner()).into_owned(); xml_path.push(name); // writes the event to the writer @@ -493,7 +499,7 @@ impl CAIWriter for SvgIO { // add manifest data if xml_path.len() == 1 && xml_path[0] == SVG { writer - .write(&manifest_data) + .write_event(create_manifest_tag(store_bytes, true)?) .map_err(|_e| Error::XmlWriteError)?; } } @@ -504,7 +510,7 @@ impl CAIWriter for SvgIO { .write_event(Event::End(e)) .map_err(|_e| Error::XmlWriteError)?; } - Ok(e) => writer.write_event(&e).map_err(|_e| Error::XmlWriteError)?, + Ok(e) => writer.write_event(e).map_err(|_e| Error::XmlWriteError)?, Err(_e) => return Err(Error::InvalidAsset("XML invalid".to_string())), } buf.clear(); @@ -574,9 +580,9 @@ impl CAIWriter for SvgIO { let mut xml_path: Vec = Vec::new(); loop { - match reader.read_event(&mut buf) { + match reader.read_event_into(&mut buf) { Ok(Event::Start(e)) => { - let name = String::from_utf8_lossy(e.name()).into_owned(); + let name = String::from_utf8_lossy(e.name().into_inner()).into_owned(); xml_path.push(name); if xml_path.len() == 3 @@ -623,7 +629,7 @@ impl CAIWriter for SvgIO { .map_err(|_e| Error::XmlWriteError)?; // pass Event through } } - Ok(e) => writer.write_event(&e).map_err(|_e| Error::XmlWriteError)?, + Ok(e) => writer.write_event(e).map_err(|_e| Error::XmlWriteError)?, Err(_e) => return Err(Error::InvalidAsset("XML invalid".to_string())), } buf.clear(); diff --git a/sdk/src/utils/xmp_inmemory_utils.rs b/sdk/src/utils/xmp_inmemory_utils.rs index 1ddaa6d7f..e1addf7c5 100644 --- a/sdk/src/utils/xmp_inmemory_utils.rs +++ b/sdk/src/utils/xmp_inmemory_utils.rs @@ -11,18 +11,16 @@ // specific language governing permissions and limitations under // each license. -use std::io::Cursor; +use std::{io::Cursor, str}; -use fast_xml::{ +use log::error; +use quick_xml::{ events::{BytesStart, Event}, + name::QName, Reader, Writer, }; -use log::error; -use crate::{ - asset_io::CAIRead, jumbf_io::get_cailoader_handler, utils::hash_utils::vec_compare, Error, - Result, -}; +use crate::{asset_io::CAIRead, jumbf_io::get_cailoader_handler, Error, Result}; const RDF_DESCRIPTION: &[u8] = b"rdf:Description"; @@ -58,17 +56,16 @@ impl XmpInfo { /// Extract an a value from XMP using a key fn extract_xmp_key(xmp: &str, key: &str) -> Option { let mut reader = Reader::from_str(xmp); - reader.trim_text(true); - let mut buf = Vec::new(); + reader.config_mut().trim_text(true); loop { - match reader.read_event(&mut buf) { + match reader.read_event() { Ok(Event::Start(ref e)) | Ok(Event::Empty(ref e)) => { - if e.name() == RDF_DESCRIPTION { + if e.name() == QName(RDF_DESCRIPTION) { // attribute case let value = e.attributes().find(|a| { if let Ok(attribute) = a { - vec_compare(attribute.key, key.as_bytes()) + attribute.key == QName(key.as_bytes()) } else { false } @@ -78,18 +75,16 @@ fn extract_xmp_key(xmp: &str, key: &str) -> Option { return Some(s); } } - } else if e.name() == key.as_bytes() { + } else if e.name() == QName(key.as_bytes()) { // tag case - let mut buf: Vec = Vec::new(); - if let Ok(s) = reader.read_text(e.name(), &mut buf) { - return Some(s); + if let Ok(s) = reader.read_text(e.name()) { + return Some(s.to_string()); } } } Ok(Event::Eof) => break, _ => {} } - buf.clear(); } None } @@ -98,24 +93,26 @@ fn extract_xmp_key(xmp: &str, key: &str) -> Option { /// Add a value to XMP using a key, replaces the value if the key exists fn add_xmp_key(xmp: &str, key: &str, value: &str) -> Result { let mut reader = Reader::from_str(xmp); - reader.trim_text(true); + reader.config_mut().trim_text(true); let mut writer = Writer::new_with_indent(Cursor::new(Vec::new()), b' ', 2); - let mut buf = Vec::new(); let mut added = false; loop { let event = reader - .read_event(&mut buf) + .read_event() .map_err(|e| Error::XmpReadError(e.to_string()))?; // println!("{:?}", event); match event { - Event::Start(ref e) if e.name() == RDF_DESCRIPTION => { + Event::Start(ref e) if e.name() == QName(RDF_DESCRIPTION) => { // creates a new element - let mut elem = BytesStart::owned(RDF_DESCRIPTION.to_vec(), RDF_DESCRIPTION.len()); + let mut elem = BytesStart::from_content( + String::from_utf8_lossy(RDF_DESCRIPTION), + RDF_DESCRIPTION.len(), + ); for attr in e.attributes() { match attr { Ok(attr) => { - if attr.key == key.as_bytes() { + if attr.key == QName(key.as_bytes()) { // replace the key/value if it exists elem.push_attribute((key, value)); added = true; @@ -139,13 +136,16 @@ fn add_xmp_key(xmp: &str, key: &str, value: &str) -> Result { .write_event(Event::Start(elem)) .map_err(|e| Error::XmpWriteError(e.to_string()))?; } - Event::Empty(ref e) if e.name() == RDF_DESCRIPTION => { + Event::Empty(ref e) if e.name() == QName(RDF_DESCRIPTION) => { // creates a new element - let mut elem = BytesStart::owned(RDF_DESCRIPTION.to_vec(), RDF_DESCRIPTION.len()); + let mut elem = BytesStart::from_content( + String::from_utf8_lossy(RDF_DESCRIPTION), + RDF_DESCRIPTION.len(), + ); for attr in e.attributes() { match attr { Ok(attr) => { - if attr.key == key.as_bytes() { + if attr.key == QName(key.as_bytes()) { // replace the key/value if it exists elem.push_attribute((key, value)); added = true; @@ -177,7 +177,6 @@ fn add_xmp_key(xmp: &str, key: &str, value: &str) -> Result { } } } - buf.clear(); let result = writer.into_inner().into_inner(); String::from_utf8(result).map_err(|e| Error::XmpWriteError(e.to_string())) }