diff --git a/Cargo.lock b/Cargo.lock index 7d643ab..20ae384 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,17 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "ahash" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" +dependencies = [ + "getrandom 0.2.8", + "once_cell", + "version_check", +] + [[package]] name = "aho-corasick" version = "0.7.19" @@ -566,6 +577,9 @@ name = "hashbrown" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash", +] [[package]] name = "heck" @@ -792,6 +806,12 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + [[package]] name = "libc" version = "0.2.137" @@ -826,6 +846,25 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "lol_html" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b16d7d17063591adf0017a068a8ce2ed29a4f5402b9e6cac01888ff9c0527ff7" +dependencies = [ + "bitflags", + "cfg-if", + "cssparser", + "encoding_rs", + "hashbrown", + "lazy_static", + "lazycell", + "memchr", + "safemem", + "selectors", + "thiserror", +] + [[package]] name = "mac" version = "0.1.1" @@ -1453,6 +1492,12 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09" +[[package]] +name = "safemem" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072" + [[package]] name = "schannel" version = "0.1.20" @@ -1638,7 +1683,7 @@ checksum = "7bd3e3206899af3f8b12af284fafc038cc1dc2b41d1b89dd17297221c5d225de" [[package]] name = "site_icons" -version = "0.4.13" +version = "0.5.0" dependencies = [ "byteorder", "clap", @@ -1648,6 +1693,7 @@ dependencies = [ "html5ever", "itertools", "log", + "lol_html", "mime_4", "once_cell", "percent-encoding", diff --git a/Cargo.toml b/Cargo.toml index aa0ab6b..f910e68 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "site_icons" -version = "0.4.13" +version = "0.5.0" authors = ["Sam Denty "] edition = "2018" license = "GPL-3.0" @@ -35,6 +35,7 @@ serde = { version = "1.0", features = ["derive", "rc"] } serde_json = "1.0" futures = "0.3.25" tldextract = "0.6.0" +lol_html = "0.3.2" [target.'cfg(target_arch = "wasm32")'.dependencies] reqwest = { package = "reqwest-wasm", version = "0.11.16", features = [ diff --git a/src/icon_info.rs b/src/icon_info.rs index a46b0b7..8feca6d 100644 --- a/src/icon_info.rs +++ b/src/icon_info.rs @@ -11,9 +11,8 @@ use std::{ io, }; -#[derive(Serialize, Deserialize)] -#[serde(rename_all = "lowercase")] enum IconKind { + SVG, PNG, JPEG, ICO, @@ -28,7 +27,7 @@ pub enum IconInfo { JPEG { size: IconSize }, ICO { sizes: IconSizes }, GIF { size: IconSize }, - SVG, + SVG { size: Option }, } impl IconInfo { @@ -40,6 +39,14 @@ impl IconInfo { reader.read_exact(&mut header).await?; match (kind, &header) { + (Some(IconKind::SVG), bytes) => { + let size = get_svg_size(bytes, reader).await?; + Ok(IconInfo::SVG { size }) + } + (_, &[0x60, byte_two]) => { + let size = get_svg_size(&[0x60, byte_two], reader).await?; + Ok(IconInfo::SVG { size }) + } (Some(IconKind::PNG), _) | (_, b"\x89P") => { let size = get_png_size(reader).await?; Ok(IconInfo::PNG { size }) @@ -135,7 +142,25 @@ impl IconInfo { Some(IconKind::ICO) } - (mime::IMAGE, mime::SVG) | (mime::TEXT, mime::PLAIN) => return Ok(IconInfo::SVG), + (mime::IMAGE, mime::GIF) => { + if let Some(sizes) = sizes { + return Ok(IconInfo::GIF { + size: *sizes.largest(), + }); + } + + Some(IconKind::GIF) + } + + (mime::IMAGE, mime::SVG) | (mime::TEXT, mime::PLAIN) => { + if let Some(sizes) = sizes { + return Ok(IconInfo::SVG { + size: Some(*sizes.largest()), + }); + } + + Some(IconKind::SVG) + } _ => None, }; @@ -147,7 +172,7 @@ impl IconInfo { match self { IconInfo::ICO { sizes } => Some(sizes.largest()), IconInfo::PNG { size } | IconInfo::JPEG { size } | IconInfo::GIF { size } => Some(size), - IconInfo::SVG => None, + IconInfo::SVG { size } => size.as_ref(), } } @@ -157,7 +182,7 @@ impl IconInfo { IconInfo::PNG { size } | IconInfo::JPEG { size } | IconInfo::GIF { size } => { Some((*size).into()) } - IconInfo::SVG => None, + IconInfo::SVG { size } => size.map(|size| size.into()), } } @@ -167,7 +192,7 @@ impl IconInfo { IconInfo::JPEG { .. } => "image/jpeg", IconInfo::ICO { .. } => "image/x-icon", IconInfo::GIF { .. } => "image/gif", - IconInfo::SVG => "image/svg+xml", + IconInfo::SVG { .. } => "image/svg+xml", } } } @@ -179,7 +204,17 @@ impl Display for IconInfo { IconInfo::JPEG { size } => write!(f, "jpeg {}", size), IconInfo::GIF { size } => write!(f, "gif {}", size), IconInfo::ICO { sizes } => write!(f, "ico {}", sizes), - IconInfo::SVG => write!(f, "svg"), + IconInfo::SVG { size } => { + write!( + f, + "svg{}", + if let Some(size) = size { + format!(" {}", size) + } else { + "".to_string() + } + ) + } } } } @@ -187,15 +222,20 @@ impl Display for IconInfo { impl Ord for IconInfo { fn cmp(&self, other: &Self) -> Ordering { match (self, other) { - (IconInfo::SVG, IconInfo::SVG) => Ordering::Equal, - (IconInfo::SVG, _) => Ordering::Less, - (_, IconInfo::SVG) => Ordering::Greater, + (IconInfo::SVG { size }, IconInfo::SVG { size: other_size }) => match (size, other_size) { + (Some(_), None) => Ordering::Less, + (None, Some(_)) => Ordering::Greater, + (Some(size), Some(other_size)) => size.cmp(other_size), + (None, None) => Ordering::Equal, + }, + (IconInfo::SVG { .. }, _) => Ordering::Less, + (_, IconInfo::SVG { .. }) => Ordering::Greater, _ => { - let this_size = self.size().unwrap(); + let size = self.size().unwrap(); let other_size = other.size().unwrap(); - this_size.cmp(other_size).then_with(|| match (self, other) { + size.cmp(other_size).then_with(|| match (self, other) { (IconInfo::PNG { .. }, IconInfo::PNG { .. }) => Ordering::Equal, (IconInfo::PNG { .. }, _) => Ordering::Less, (_, IconInfo::PNG { .. }) => Ordering::Greater, diff --git a/src/icon_size/mod.rs b/src/icon_size/mod.rs index 71ca2ca..b5c3618 100644 --- a/src/icon_size/mod.rs +++ b/src/icon_size/mod.rs @@ -3,12 +3,14 @@ mod ico; mod icon_sizes; mod jpeg; mod png; +mod svg; pub use gif::*; pub use ico::*; pub use icon_sizes::*; pub use jpeg::*; pub use png::*; +pub use svg::*; use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; use std::{ diff --git a/src/icon_size/svg.rs b/src/icon_size/svg.rs new file mode 100644 index 0000000..c340849 --- /dev/null +++ b/src/icon_size/svg.rs @@ -0,0 +1,84 @@ +use super::IconSize; +use futures::prelude::*; +use lol_html::{element, errors::RewritingError, HtmlRewriter, Settings}; +use std::{ + error::Error, + fmt::{self, Display}, +}; + +#[derive(Debug)] +struct SizeResult(Option); + +impl Display for SizeResult { + fn fmt(&self, _: &mut fmt::Formatter<'_>) -> fmt::Result { + Ok(()) + } +} + +impl Error for SizeResult {} + +fn parse_size(size: S) -> Option { + size + .to_string() + .parse::() + .ok() + .map(|size| size.round() as u32) +} + +pub async fn get_svg_size( + first_bytes: &[u8; 2], + reader: &mut R, +) -> Result, Box> { + let mut rewriter = HtmlRewriter::new( + Settings { + element_content_handlers: vec![ + // Rewrite insecure hyperlinks + element!("svg", |el| { + let viewbox = el.get_attribute("viewbox"); + + let width = el.get_attribute("width").and_then(parse_size); + let height = el.get_attribute("height").and_then(parse_size); + + Err(Box::new(SizeResult( + if let (Some(width), Some(height)) = (width, height) { + Some(IconSize::new(width, height)) + } else if let Some(viewbox) = viewbox { + regex!(r"^\d+\s+\d+\s+(\d+\.?[\d]?)\s+(\d+\.?[\d]?)") + .captures(&viewbox) + .map(|captures| { + let width = parse_size(captures.get(1).unwrap().as_str()).unwrap(); + let height = parse_size(captures.get(2).unwrap().as_str()).unwrap(); + IconSize::new(width, height) + }) + } else { + None + }, + ))) + }), + ], + ..Settings::default() + }, + |_: &[u8]| {}, + ); + + rewriter.write(first_bytes)?; + + let mut buffer = [0; 100]; + + loop { + let n = reader.read(&mut buffer).await?; + if n == 0 { + return Err("invalid svg".into()); + } + + match rewriter.write(&buffer[..n]) { + Err(RewritingError::ContentHandlerError(result)) => { + let result = result.downcast::().unwrap(); + + return Ok(result.0); + } + + result => result?, + } + } +}