diff --git a/Cargo.toml b/Cargo.toml index 97ab1b25f..79b2f9cbd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,7 +34,7 @@ members = ["capi", "capi/bind_gen", "crates/*"] [dependencies] # core -base64 = { version = "0.13.0", default-features = false, features = ["alloc"] } +base64-simd = { version = "0.7.0", default-features = false } bytemuck = { version = "1.9.1", default-features = false, features = ["derive"] } cfg-if = "1.0.0" itoa = { version = "1.0.3", default-features = false } @@ -115,13 +115,15 @@ criterion = "0.4.0" [features] default = ["image-shrinking", "std"] std = [ + "base64-simd/detect", + "base64-simd/std", "image", "libc", "livesplit-hotkey/std", "memchr/std", "rustybuzz?/std", - "serde/std", "serde_json/std", + "serde/std", "simdutf8/std", "snafu/std", "time/formatting", diff --git a/src/layout/parser/mod.rs b/src/layout/parser/mod.rs index 1c3452254..e845ac9c0 100644 --- a/src/layout/parser/mod.rs +++ b/src/layout/parser/mod.rs @@ -1,5 +1,7 @@ //! Provides the parser for layout files of the original LiveSplit. +use base64_simd::Base64; + use super::{Component, Layout, LayoutDirection}; use crate::{ component::{separator, timer::DeltaGradient}, @@ -19,7 +21,7 @@ use crate::{ Reader, }, }; -use core::str; +use core::{mem::MaybeUninit, num::ParseIntError, str}; mod blank_space; mod current_comparison; @@ -61,7 +63,7 @@ pub enum Error { /// Failed to parse an integer. ParseInt { /// The underlying error. - source: core::num::ParseIntError, + source: ParseIntError, }, /// Failed to parse a boolean. ParseBool, @@ -91,8 +93,8 @@ impl From for Error { } } -impl From for Error { - fn from(source: core::num::ParseIntError) -> Self { +impl From for Error { + fn from(source: ParseIntError) -> Self { Self::ParseInt { source } } } @@ -151,7 +153,12 @@ impl GradientType for DeltaGradientKind { fn build(self, first: Color, second: Color) -> Self::Built { match self { DeltaGradientKind::Transparent => Gradient::Transparent.into(), - DeltaGradientKind::Plain => Gradient::Plain(first).into(), + DeltaGradientKind::Plain => if first.alpha == 0.0 { + Gradient::Transparent + } else { + Gradient::Plain(first) + } + .into(), DeltaGradientKind::Vertical => Gradient::Vertical(first, second).into(), DeltaGradientKind::Horizontal => Gradient::Horizontal(first, second).into(), DeltaGradientKind::PlainWithDeltaColor => DeltaGradient::DeltaPlain, @@ -167,13 +174,10 @@ impl GradientType for GradientKind { GradientKind::Transparent } fn parse(kind: &str) -> Result { - // FIXME: Implement delta color support properly: - // https://github.com/LiveSplit/livesplit-core/issues/380 - Ok(match kind { - "Plain" | "PlainWithDeltaColor" => GradientKind::Plain, - "Vertical" | "VerticalWithDeltaColor" => GradientKind::Vertical, - "Horizontal" | "HorizontalWithDeltaColor" => GradientKind::Horizontal, + "Plain" => GradientKind::Plain, + "Vertical" => GradientKind::Vertical, + "Horizontal" => GradientKind::Horizontal, _ => return Err(Error::ParseGradientType), }) } @@ -297,7 +301,7 @@ where }) } -fn font(reader: &mut Reader<'_>, font_buf: &mut Vec, f: F) -> Result<()> +fn font(reader: &mut Reader<'_>, font_buf: &mut Vec>, f: F) -> Result<()> where F: FnOnce(Font), { @@ -317,11 +321,18 @@ where // The full definition can be found here: // https://referencesource.microsoft.com/#System.Drawing/commonui/System/Drawing/Advanced/Font.cs,130 - let rem = text.get(304..).ok_or(Error::ParseFont)?; - font_buf.clear(); - base64::decode_config_buf(rem, base64::STANDARD, font_buf).map_err(|_| Error::ParseFont)?; + let rem = text.as_bytes().get(304..).ok_or(Error::ParseFont)?; + + font_buf.resize( + Base64::STANDARD.estimated_decoded_length(rem.len()), + MaybeUninit::uninit(), + ); + + let decoded = Base64::STANDARD + .decode(rem, base64_simd::OutBuf::uninit(font_buf)) + .map_err(|_| Error::ParseFont)?; - let mut cursor = font_buf.get(1..).ok_or(Error::ParseFont)?.iter(); + let mut cursor = decoded.get(1..).ok_or(Error::ParseFont)?.iter(); // Strings are encoded as varint for the length + the UTF-8 string data. let mut len = 0; diff --git a/src/run/parser/livesplit.rs b/src/run/parser/livesplit.rs index 8543cc015..44d7cf3e8 100644 --- a/src/run/parser/livesplit.rs +++ b/src/run/parser/livesplit.rs @@ -14,7 +14,8 @@ use crate::{ AtomicDateTime, DateTime, Run, RunMetadata, Segment, Time, TimeSpan, }; use alloc::borrow::Cow; -use core::str; +use base64_simd::Base64; +use core::{mem::MaybeUninit, str}; use time::{Date, PrimitiveDateTime}; /// The Error type for splits files that couldn't be parsed by the LiveSplit @@ -133,17 +134,23 @@ fn parse_date_time(text: &str) -> Result { .ok_or(Error::ParseDate) } -fn image(reader: &mut Reader<'_>, image_buf: &mut Vec, f: F) -> Result<()> +fn image(reader: &mut Reader<'_>, image_buf: &mut Vec>, f: F) -> Result<()> where F: FnOnce(&[u8]), { text_as_escaped_string_err(reader, |text| { if text.len() >= 216 { - image_buf.clear(); - if base64::decode_config_buf(&text.as_bytes()[212..], base64::STANDARD, image_buf) - .is_ok() + let src = &text.as_bytes()[212..]; + + image_buf.resize( + Base64::STANDARD.estimated_decoded_length(src.len()), + MaybeUninit::uninit(), + ); + + if let Ok(decoded) = + Base64::STANDARD.decode(src, base64_simd::OutBuf::uninit(image_buf)) { - f(&image_buf[2..image_buf.len() - 1]); + f(&decoded[2..decoded.len() - 1]); return Ok(()); } } @@ -277,7 +284,7 @@ fn parse_metadata( fn parse_segment( version: Version, reader: &mut Reader<'_>, - image_buf: &mut Vec, + image_buf: &mut Vec>, run: &mut Run, ) -> Result { let mut segment = Segment::new(""); diff --git a/src/run/parser/llanfair_gered.rs b/src/run/parser/llanfair_gered.rs index 0b7202d95..9c77cd64d 100644 --- a/src/run/parser/llanfair_gered.rs +++ b/src/run/parser/llanfair_gered.rs @@ -1,5 +1,7 @@ //! Provides the parser for splits files used by Gered's Llanfair fork. +use core::mem::MaybeUninit; + #[cfg(feature = "std")] use crate::util::byte_parsing::big_endian::strip_u32; #[cfg(feature = "std")] @@ -83,20 +85,30 @@ where #[cfg(feature = "std")] fn image( reader: &mut Reader<'_>, - raw_buf: &mut Vec, + raw_buf: &mut Vec>, png_buf: &mut Vec, mut f: F, ) -> Result<()> where F: FnMut(&[u8]), { + use base64_simd::Base64; + single_child(reader, "ImageIcon", |reader, _| { let (width, height, image) = text_as_str_err::<_, _, Error>(reader, |t| { - raw_buf.clear(); - base64::decode_config_buf(&*t, base64::STANDARD, raw_buf).map_err(|_| Error::Image)?; + let src = t.as_bytes(); + + raw_buf.resize( + Base64::STANDARD.estimated_decoded_length(src.len()), + MaybeUninit::uninit(), + ); + + let decoded = Base64::STANDARD + .decode(src, base64_simd::OutBuf::uninit(raw_buf)) + .map_err(|_| Error::Image)?; let (width, height); - let mut cursor = raw_buf.get(0xD1..).ok_or(Error::Image)?; + let mut cursor = decoded.get(0xD1..).ok_or(Error::Image)?; height = strip_u32(&mut cursor).ok_or(Error::Image)?; width = strip_u32(&mut cursor).ok_or(Error::Image)?; @@ -108,7 +120,7 @@ where Ok(( width, height, - raw_buf.get(0xFE..0xFE + len).ok_or(Error::Image)?, + decoded.get(0xFE..0xFE + len).ok_or(Error::Image)?, )) })?; @@ -126,7 +138,7 @@ where fn parse_segment( total_time: &mut TimeSpan, reader: &mut Reader<'_>, - _raw_buf: &mut Vec, + _raw_buf: &mut Vec>, _png_buf: &mut Vec, ) -> Result { single_child(reader, "Segment", |reader, _| { diff --git a/src/run/saver/livesplit.rs b/src/run/saver/livesplit.rs index b421c0c76..391454fd8 100644 --- a/src/run/saver/livesplit.rs +++ b/src/run/saver/livesplit.rs @@ -32,7 +32,8 @@ use crate::{ DateTime, Run, Time, Timer, TimerPhase, }; use alloc::borrow::Cow; -use core::fmt; +use base64_simd::Base64; +use core::{fmt, mem::MaybeUninit}; use time::UtcOffset; const LSS_IMAGE_HEADER: &[u8; 156] = include_bytes!("lss_image_header.bin"); @@ -65,27 +66,34 @@ fn image( writer: &mut Writer, tag: &str, image: &Image, - base64_buf: &mut String, + base64_buf: &mut Vec>, image_buf: &mut Cow<'_, [u8]>, ) -> fmt::Result { writer.tag(tag, |tag| { let image_data = image.data(); - if !image_data.is_empty() { - let len = image_data.len(); - let image_buf = image_buf.to_mut(); - image_buf.truncate(LSS_IMAGE_HEADER.len()); - image_buf.reserve(len + 6); - image_buf.extend(&(len as u32).to_le_bytes()); - image_buf.push(0x2); - image_buf.extend(image_data); - image_buf.push(0xB); - base64_buf.clear(); - base64::encode_config_buf(image_buf, base64::STANDARD, base64_buf); - if !base64_buf.is_empty() { - return tag.content(|writer| writer.cdata(Text::new_escaped(base64_buf))); - } + if image_data.is_empty() { + return Ok(()); } - Ok(()) + + let len = image_data.len(); + let image_buf = image_buf.to_mut(); + image_buf.truncate(LSS_IMAGE_HEADER.len()); + image_buf.reserve(len + 6); + image_buf.extend(&(len as u32).to_le_bytes()); + image_buf.push(0x2); + image_buf.extend(image_data); + image_buf.push(0xB); + + base64_buf.resize( + Base64::STANDARD.encoded_length(image_buf.len()), + MaybeUninit::uninit(), + ); + + let encoded = Base64::STANDARD + .encode_as_str(image_buf, base64_simd::OutBuf::uninit(base64_buf)) + .unwrap(); + + tag.content(|writer| writer.cdata(Text::new_escaped(encoded))) }) } @@ -164,7 +172,7 @@ pub fn save_timer(timer: &Timer, writer: W) -> fmt::Result { pub fn save_run(run: &Run, writer: W) -> fmt::Result { let writer = &mut Writer::new_with_default_header(writer)?; - let base64_buf = &mut String::new(); + let base64_buf = &mut Vec::new(); let image_buf = &mut Cow::Borrowed(&LSS_IMAGE_HEADER[..]); writer.tag_with_content("Run", [("version", Text::new_escaped("1.8.0"))], |writer| { diff --git a/src/settings/image/mod.rs b/src/settings/image/mod.rs index 7d751d803..105e063f7 100644 --- a/src/settings/image/mod.rs +++ b/src/settings/image/mod.rs @@ -1,5 +1,5 @@ use crate::platform::prelude::*; -use base64::{display::Base64Display, STANDARD}; +use base64_simd::Base64; use core::{ ops::Deref, sync::atomic::{AtomicUsize, Ordering}, @@ -54,10 +54,26 @@ impl Serialize for ImageData { { if serializer.is_human_readable() { if !self.0.is_empty() { - serializer.collect_str(&format_args!( - "data:;base64,{}", - Base64Display::with_config(&self.0, STANDARD) - )) + let mut buf = String::from("data:;base64,"); + + // SAFETY: We encode Base64 to the end of the string, which is + // always valid UTF-8. Once we've written it, we simply increase + // the length of the buffer by the amount of bytes written. + unsafe { + let buf = buf.as_mut_vec(); + let encoded_len = Base64::STANDARD.encoded_length(self.0.len()); + buf.reserve_exact(encoded_len); + let additional_len = Base64::STANDARD + .encode( + &self.0, + base64_simd::OutBuf::uninit(buf.spare_capacity_mut()), + ) + .unwrap() + .len(); + buf.set_len(buf.len() + additional_len); + } + + serializer.serialize_str(&buf) } else { serializer.serialize_str("") } @@ -77,8 +93,11 @@ impl<'de> Deserialize<'de> for ImageData { if data.is_empty() { Ok(ImageData(Box::new([]))) } else if let Some(encoded_image_data) = data.strip_prefix("data:;base64,") { - let image_data = base64::decode(encoded_image_data).map_err(de::Error::custom)?; - Ok(ImageData(image_data.into_boxed_slice())) + let image_data = Base64::STANDARD + .decode_to_boxed_bytes(encoded_image_data.as_bytes()) + .map_err(de::Error::custom)?; + + Ok(ImageData(image_data)) } else { Err(de::Error::custom("Invalid Data URL for image")) }