Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove line-wrap dependency #139

Merged
merged 1 commit into from
Mar 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }

Expand Down
8 changes: 5 additions & 3 deletions src/data.rs
Original file line number Diff line number Diff line change
@@ -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.
///
Expand Down Expand Up @@ -48,15 +50,15 @@ impl Data {

/// Create a `Data` object from an XML plist (Base-64) encoded string.
pub fn from_xml_format(b64_str: &str) -> Result<Self, InvalidXmlData> {
base64_standard
BASE64_STANDARD
.decode(b64_str)
.map_err(InvalidXmlData)
.map(Data::new)
}

/// 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)
}
}

Expand Down
12 changes: 7 additions & 5 deletions src/stream/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -90,7 +90,7 @@ enum StackItem<'a> {
pub struct XmlWriteOptions {
root_element: bool,
indent_char: u8,
indent_amount: usize,
indent_count: usize,
}

impl XmlWriteOptions {
Expand Down Expand Up @@ -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
}

Expand All @@ -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,
}
}
Expand Down
124 changes: 68 additions & 56 deletions src/stream/xml_writer.rs
Original file line number Diff line number Diff line change
@@ -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#"<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
Expand All @@ -25,6 +31,8 @@ enum Element {
pub struct XmlWriter<W: Write> {
xml_writer: EventWriter<W>,
write_root_element: bool,
indent_char: u8,
indent_count: usize,
started_plist: bool,
stack: Vec<Element>,
expecting_key: bool,
Expand All @@ -44,15 +52,17 @@ impl<W: Write> XmlWriter<W> {
}

pub fn new_with_options(writer: W, opts: &XmlWriteOptions) -> XmlWriter<W> {
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,
Expand All @@ -66,9 +76,9 @@ impl<W: Write> XmlWriter<W> {
}

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(())
}

Expand All @@ -84,12 +94,6 @@ impl<W: Write> XmlWriter<W> {
Ok(())
}

fn write_value(&mut self, value: &str) -> Result<(), Error> {
self.xml_writer
.write_event(XmlEvent::Text(BytesText::new(value)))?;
Ok(())
}

fn write_event<F: FnOnce(&mut Self) -> Result<(), Error>>(
&mut self,
f: F,
Expand Down Expand Up @@ -232,8 +236,19 @@ impl<W: Write> Writer for XmlWriter<W> {

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(|_| ())
})
}

Expand Down Expand Up @@ -287,50 +302,47 @@ impl From<XmlWriterError> 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
// <data>
// AAAA..AA (68 characters per line)
// </data>
// 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)]
Expand Down
Loading