diff --git a/Cargo.toml b/Cargo.toml index 1559453..abe9d2e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,7 +19,6 @@ enable_unstable_features_that_may_break_with_minor_version_bumps = [] base64 = "0.21.0" time = { version = "0.3.30", features = ["parsing", "formatting"] } indexmap = "2.1.0" -line-wrap = "0.2.0" quick_xml = { package = "quick-xml", version = "0.31.0" } serde = { version = "1.0.2", optional = true } diff --git a/src/data.rs b/src/data.rs index b48fddd..301db8f 100644 --- a/src/data.rs +++ b/src/data.rs @@ -1,6 +1,8 @@ use std::fmt; -use base64::{engine::general_purpose::STANDARD as base64_standard, Engine}; +use base64::{engine::general_purpose::STANDARD as BASE64_STANDARD, Engine}; + +use crate::stream::xml_encode_data_base64; /// A byte buffer used for serialization to and from the plist data type. /// @@ -48,7 +50,7 @@ impl Data { /// Create a `Data` object from an XML plist (Base-64) encoded string. pub fn from_xml_format(b64_str: &str) -> Result { - base64_standard + BASE64_STANDARD .decode(b64_str) .map_err(InvalidXmlData) .map(Data::new) @@ -56,7 +58,7 @@ impl Data { /// Converts the `Data` to an XML plist (Base-64) string. pub fn to_xml_format(&self) -> String { - crate::stream::base64_encode_plist(&self.inner, 0) + xml_encode_data_base64(&self.inner) } } diff --git a/src/stream/mod.rs b/src/stream/mod.rs index 5d793f4..c555161 100644 --- a/src/stream/mod.rs +++ b/src/stream/mod.rs @@ -12,7 +12,7 @@ pub use self::xml_reader::XmlReader; mod xml_writer; pub use self::xml_writer::XmlWriter; #[cfg(feature = "serde")] -pub(crate) use xml_writer::base64_encode_plist; +pub(crate) use xml_writer::encode_data_base64 as xml_encode_data_base64; use std::{ borrow::Cow, @@ -90,7 +90,7 @@ enum StackItem<'a> { pub struct XmlWriteOptions { root_element: bool, indent_char: u8, - indent_amount: usize, + indent_count: usize, } impl XmlWriteOptions { @@ -127,10 +127,12 @@ impl XmlWriteOptions { /// Specifies the character and amount used for indentation. /// + /// `indent_char` must be a valid UTF8 character. + /// /// The default is indenting with a single tab. - pub fn indent(mut self, indent_char: u8, indent_amount: usize) -> Self { + pub fn indent(mut self, indent_char: u8, indent_count: usize) -> Self { self.indent_char = indent_char; - self.indent_amount = indent_amount; + self.indent_count = indent_count; self } @@ -156,7 +158,7 @@ impl Default for XmlWriteOptions { fn default() -> Self { XmlWriteOptions { indent_char: b'\t', - indent_amount: 1, + indent_count: 1, root_element: true, } } diff --git a/src/stream/xml_writer.rs b/src/stream/xml_writer.rs index 6c81460..9775e19 100644 --- a/src/stream/xml_writer.rs +++ b/src/stream/xml_writer.rs @@ -1,16 +1,22 @@ -use base64::{engine::general_purpose::STANDARD as base64_standard, Engine}; +use base64::{engine::general_purpose::STANDARD as BASE64_STANDARD, Engine}; use quick_xml::{ events::{BytesEnd, BytesStart, BytesText, Event as XmlEvent}, Error as XmlWriterError, Writer as EventWriter, }; -use std::{borrow::Cow, io::Write}; +use std::{ + borrow::Cow, + io::{self, Write}, +}; use crate::{ - error::{self, Error, ErrorKind, EventKind}, + error::{self, from_io_without_position, Error, ErrorKind, EventKind}, stream::{Writer, XmlWriteOptions}, Date, Integer, Uid, }; +const DATA_MAX_LINE_CHARS: usize = 68; +const DATA_MAX_LINE_BYTES: usize = 51; + static XML_PROLOGUE: &[u8] = br#" @@ -25,6 +31,8 @@ enum Element { pub struct XmlWriter { xml_writer: EventWriter, write_root_element: bool, + indent_char: u8, + indent_count: usize, started_plist: bool, stack: Vec, expecting_key: bool, @@ -44,15 +52,17 @@ impl XmlWriter { } pub fn new_with_options(writer: W, opts: &XmlWriteOptions) -> XmlWriter { - let xml_writer = if opts.indent_amount == 0 { + let xml_writer = if opts.indent_count == 0 { EventWriter::new(writer) } else { - EventWriter::new_with_indent(writer, opts.indent_char, opts.indent_amount) + EventWriter::new_with_indent(writer, opts.indent_char, opts.indent_count) }; XmlWriter { xml_writer, write_root_element: opts.root_element, + indent_char: opts.indent_char, + indent_count: opts.indent_count, started_plist: false, stack: Vec::new(), expecting_key: false, @@ -66,9 +76,9 @@ impl XmlWriter { } fn write_element_and_value(&mut self, name: &str, value: &str) -> Result<(), Error> { - self.start_element(name)?; - self.write_value(value)?; - self.end_element(name)?; + self.xml_writer + .create_element(name) + .write_text_content(BytesText::new(value))?; Ok(()) } @@ -84,12 +94,6 @@ impl XmlWriter { Ok(()) } - fn write_value(&mut self, value: &str) -> Result<(), Error> { - self.xml_writer - .write_event(XmlEvent::Text(BytesText::new(value)))?; - Ok(()) - } - fn write_event Result<(), Error>>( &mut self, f: F, @@ -232,8 +236,19 @@ impl Writer for XmlWriter { fn write_data(&mut self, value: Cow<[u8]>) -> Result<(), Error> { self.write_value_event(EventKind::Data, |this| { - let base64_data = base64_encode_plist(&value, this.stack.len()); - this.write_element_and_value("data", &base64_data) + this.xml_writer + .create_element("data") + .write_inner_content(|xml_writer| { + write_data_base64( + &value, + true, + this.indent_char, + this.stack.len() * this.indent_count, + xml_writer.get_mut(), + ) + .map_err(from_io_without_position) + }) + .map(|_| ()) }) } @@ -287,50 +302,47 @@ impl From for Error { } } -pub(crate) fn base64_encode_plist(data: &[u8], indent: usize) -> String { +#[cfg(feature = "serde")] +pub(crate) fn encode_data_base64(data: &[u8]) -> String { + // Pre-allocate space for the base64 encoded data. + let num_lines = (data.len() + DATA_MAX_LINE_BYTES - 1) / DATA_MAX_LINE_BYTES; + let max_len = num_lines * (DATA_MAX_LINE_CHARS + 1); + + let mut base64 = Vec::with_capacity(max_len); + write_data_base64(data, false, b'\t', 0, &mut base64).expect("writing to a vec cannot fail"); + String::from_utf8(base64).expect("encoded base64 is ascii") +} + +fn write_data_base64( + data: &[u8], + write_initial_newline: bool, + indent_char: u8, + indent_repeat: usize, + mut writer: impl Write, +) -> io::Result<()> { // XML plist data elements are always formatted by apple tools as // // AAAA..AA (68 characters per line) // - // Allocate space for base 64 string and line endings up front - const LINE_LEN: usize = 68; - let mut line_ending = Vec::with_capacity(1 + indent); - line_ending.push(b'\n'); - (0..indent).for_each(|_| line_ending.push(b'\t')); - - // Find the max length of `data` encoded as a base 64 string with padding - let base64_max_string_len = data.len() * 4 / 3 + 4; - - // Find the max length of the formatted base 64 string as: max length of the base 64 string - // + line endings and indents at the start of the string and after every line - let base64_max_string_len_with_formatting = - base64_max_string_len + (2 + base64_max_string_len / LINE_LEN) * line_ending.len(); - - let mut output = vec![0; base64_max_string_len_with_formatting]; - - // Start output with a line ending and indent - output[..line_ending.len()].copy_from_slice(&line_ending); - - // Encode `data` as a base 64 string - let base64_string_len = base64_standard - .encode_slice(data, &mut output[line_ending.len()..]) - .expect("encoding slice fits base64 buffer"); - - // Line wrap the base 64 encoded string - let line_wrap_len = line_wrap::line_wrap( - &mut output[line_ending.len()..], - base64_string_len, - LINE_LEN, - &line_wrap::SliceLineEnding::new(&line_ending).expect("not empty"), - ); - - // Add the final line ending and indent - output[line_ending.len() + base64_string_len + line_wrap_len..][..line_ending.len()] - .copy_from_slice(&line_ending); - - // Ensure output is the correct length - output.truncate(base64_string_len + line_wrap_len + 2 * line_ending.len()); - String::from_utf8(output).expect("base 64 string must be valid utf8") + let mut encoded = [0; DATA_MAX_LINE_CHARS]; + for (i, line) in data.chunks(DATA_MAX_LINE_BYTES).enumerate() { + // Write newline + if write_initial_newline || i > 0 { + writer.write_all(&[b'\n'])?; + } + + // Write indent + for _ in 0..indent_repeat { + writer.write_all(&[indent_char])?; + } + + // Write bytes + let encoded_len = BASE64_STANDARD + .encode_slice(line, &mut encoded) + .expect("encoded base64 max line length is known"); + writer.write_all(&encoded[..encoded_len])?; + } + Ok(()) } #[cfg(test)]