diff --git a/src/core.rs b/src/core.rs index 1fe83d5..bc29105 100644 --- a/src/core.rs +++ b/src/core.rs @@ -228,9 +228,9 @@ pub fn create_monolithic_document_from_data( match session.retrieve_asset(/*&target_url, */ &base_url, &favicon_ico_url) { Ok((data, final_url, media_type, charset)) => { - let favicon_data_url: Url = - create_data_url(&media_type, &charset, &data, &final_url); - dom = add_favicon(&dom.document, favicon_data_url.to_string()); + let favicon_data_url: String = + create_data_url(&media_type, &charset, data, &final_url, None); + dom = add_favicon(&dom.document, favicon_data_url); } Err(_) => { // Failed to retrieve /favicon.ico diff --git a/src/css.rs b/src/css.rs index 39b5c4b..8ba8c8d 100644 --- a/src/css.rs +++ b/src/css.rs @@ -272,7 +272,7 @@ pub fn process_css<'a>( import_media_type, import_charset, )) => { - let mut import_data_url = create_data_url( + let import_data_url = create_data_url( &import_media_type, "utf-8", embed_stylesheet( @@ -280,13 +280,11 @@ pub fn process_css<'a>( &import_final_url, &import_contents, &import_charset, - ) - .as_bytes(), + ), &import_final_url, + import_full_url.fragment(), ); - import_data_url.set_fragment(import_full_url.fragment()); - result - .push_str(format_quoted_string(import_data_url.as_ref()).as_str()); + result.push_str(format_quoted_string(&import_data_url).as_str()); } Err(_) => { // Keep remote reference if unable to retrieve the asset @@ -314,11 +312,14 @@ pub fn process_css<'a>( match session.retrieve_asset(document_url, &resolved_url) { Ok((data, final_url, media_type, charset)) => { // TODO: if it's @font-face, exclude definitions of non-woff/woff-2 fonts (if woff/woff-2 are present) - let mut data_url = - create_data_url(&media_type, &charset, &data, &final_url); - data_url.set_fragment(resolved_url.fragment()); - result - .push_str(format_quoted_string(data_url.as_ref()).as_str()); + let data_url = create_data_url( + &media_type, + &charset, + data, + &final_url, + resolved_url.fragment(), + ); + result.push_str(format_quoted_string(&data_url).as_str()); } Err(_) => { // Keep remote reference if unable to retrieve the asset @@ -404,14 +405,14 @@ pub fn process_css<'a>( let full_url: Url = resolve_url(document_url, value); match session.retrieve_asset(document_url, &full_url) { Ok((css, final_url, media_type, charset)) => { - let mut data_url = create_data_url( + let data_url = create_data_url( &media_type, "utf-8", - embed_stylesheet(session, &final_url, &css, &charset).as_bytes(), + embed_stylesheet(session, &final_url, &css, &charset), &final_url, + full_url.fragment(), ); - data_url.set_fragment(full_url.fragment()); - result.push_str(format_quoted_string(data_url.as_ref()).as_str()); + result.push_str(format_quoted_string(&data_url).as_str()); } Err(_) => { // Keep remote reference if unable to retrieve the asset @@ -428,10 +429,14 @@ pub fn process_css<'a>( if session.options.output_format == MonolithOutputFormat::HTML { match session.retrieve_asset(document_url, &full_url) { Ok((data, final_url, media_type, charset)) => { - let mut data_url = - create_data_url(&media_type, &charset, &data, &final_url); - data_url.set_fragment(full_url.fragment()); - result.push_str(format_quoted_string(data_url.as_ref()).as_str()); + let data_url = create_data_url( + &media_type, + &charset, + data, + &final_url, + full_url.fragment(), + ); + result.push_str(format_quoted_string(&data_url).as_str()); } Err(_) => { // Keep remote reference if unable to retrieve the asset diff --git a/src/html.rs b/src/html.rs index d5c420c..26ec0f6 100644 --- a/src/html.rs +++ b/src/html.rs @@ -159,15 +159,14 @@ pub fn embed_srcset(session: &mut Session, document_url: &Url, srcset: &str) -> match session.retrieve_asset(document_url, &image_full_url) { Ok((image_data, image_final_url, image_media_type, image_charset)) => { if session.options.output_format == MonolithOutputFormat::HTML { - let mut image_data_url = create_data_url( + // Append retrieved asset as a data URL + result.push_str(&create_data_url( &image_media_type, &image_charset, - &image_data, + image_data, &image_final_url, - ); - // Append retrieved asset as a data URL - image_data_url.set_fragment(image_full_url.fragment()); - result.push_str(image_data_url.as_ref()); + image_full_url.fragment(), + )); } else { // MHTML result.push_str(image_full_url.as_ref()); @@ -808,9 +807,8 @@ pub fn retrieve_and_embed_asset( let css: String = embed_stylesheet(session, &final_url, &data, &charset); // Create and embed data URL (decoded stylesheets are always stored as UTF-8) - let css_data_url = - create_data_url(&media_type, "utf-8", css.as_bytes(), &final_url); - set_node_attr(node, attr_name, Some(css_data_url.to_string())); + let css_data_url = create_data_url(&media_type, "utf-8", css, &final_url, None); + set_node_attr(node, attr_name, Some(css_data_url)); } else if node_name == "frame" || node_name == "iframe" { // (I)FRAMEs are also quite different from conventional resources let mut frame_dom = html_to_dom(&data, charset.clone()); @@ -841,10 +839,14 @@ pub fn retrieve_and_embed_asset( serialize(&mut frame_data, &serializable, SerializeOpts::default()).unwrap(); // Create and embed data URL (serialized frames are always UTF-8) - let mut frame_data_url = - create_data_url(&media_type, "utf-8", &frame_data, &final_url); - frame_data_url.set_fragment(resolved_url.fragment()); - set_node_attr(node, attr_name, Some(frame_data_url.to_string())); + let frame_data_url = create_data_url( + &media_type, + "utf-8", + frame_data, + &final_url, + resolved_url.fragment(), + ); + set_node_attr(node, attr_name, Some(frame_data_url)); } else { // Every other type of element gets processed here @@ -894,17 +896,25 @@ pub fn retrieve_and_embed_asset( } } else { // Create and embed data URL - let mut data_url = - create_data_url(&script_media_type, &charset, &data, &final_url); - data_url.set_fragment(resolved_url.fragment()); - set_node_attr(node, attr_name, Some(data_url.to_string())); + let data_url = create_data_url( + &script_media_type, + &charset, + data, + &final_url, + resolved_url.fragment(), + ); + set_node_attr(node, attr_name, Some(data_url)); } } else { // Create and embed data URL - let mut data_url = - create_data_url(&media_type, &charset, &data, &final_url); - data_url.set_fragment(resolved_url.fragment()); - set_node_attr(node, attr_name, Some(data_url.to_string())); + let data_url = create_data_url( + &media_type, + &charset, + data, + &final_url, + resolved_url.fragment(), + ); + set_node_attr(node, attr_name, Some(data_url)); } } } @@ -1230,17 +1240,14 @@ pub fn walk(session: &mut Session, document_url: &Url, node: &Handle) { } } else { // It's likely a raster image; embed it as data URL - let image_asset_data: Url = create_data_url( + let image_asset_data: String = create_data_url( &media_type, &charset, - &data, + data, &final_url, + None, ); - set_node_attr( - node, - attr_name, - Some(image_asset_data.to_string()), - ); + set_node_attr(node, attr_name, Some(image_asset_data)); } } Err(_) => { diff --git a/src/url.rs b/src/url.rs index dd977c1..f12c4fa 100644 --- a/src/url.rs +++ b/src/url.rs @@ -1,3 +1,5 @@ +use std::fmt::Write; + use dataurl::{DataEncoding, DataUrl}; pub use url::Url; @@ -15,10 +17,20 @@ pub fn clean_url(url: Url) -> Url { url } -pub fn create_data_url(media_type: &str, charset: &str, data: &[u8], final_asset_url: &Url) -> Url { +/// Creates a data URL for an asset, with an optional fragment (already percent-encoded, as Url::fragment() returns it). +/// Passing data as a Vec moves it in without copying. +pub fn create_data_url( + media_type: &str, + charset: &str, + data: impl Into>, + final_asset_url: &Url, + fragment: Option<&str>, +) -> String { + let data: Vec = data.into(); + // TODO: move this block out of this function let media_type: String = if media_type.is_empty() { - detect_media_type(data, final_asset_url) + detect_media_type(&data, final_asset_url) } else { media_type.to_string() }; @@ -26,6 +38,7 @@ pub fn create_data_url(media_type: &str, charset: &str, data: &[u8], final_asset let mut data_url = DataUrl::new(); data_url.set_data_encoding(DataEncoding::Base64); data_url.set_data(data); + data_url.set_fragment(fragment); // Invalid media types get omitted, which makes browsers treat the data as text/plain, same as they would anyway if !media_type.is_empty() { @@ -39,7 +52,10 @@ pub fn create_data_url(media_type: &str, charset: &str, data: &[u8], final_asset let _ = data_url.set_parameter("charset", Some(charset)); } - Url::parse(&data_url.to_string()).unwrap() + // Allocating the exact size upfront avoids reallocations (and copies) while the string grows + let mut result = String::with_capacity(data_url.encoded_len()); + write!(result, "{data_url}").unwrap(); + result } pub fn domain_is_within_domain(domain: &str, domain_to_match_against: &str) -> bool { diff --git a/tests/css/embed_css.rs b/tests/css/embed_css.rs index 2c944f3..ab05094 100644 --- a/tests/css/embed_css.rs +++ b/tests/css/embed_css.rs @@ -36,7 +36,7 @@ mod passing { ("", "p::before{content:'café'}".as_bytes()), ("", b"p::before{content:'caf\xe9'}".as_slice()), ] { - let asset = monolith::url::create_data_url("text/css", charset, bytes, &url); + let asset = monolith::url::create_data_url("text/css", charset, bytes, &url, None); for source in [ format!("@import \"{}\";", asset), @@ -68,6 +68,7 @@ mod passing { "", b"@charset \"windows-1252\";p::before{content:'caf\xe9'}", &url, + None, ); let mut session = Session::new(None, None, MonolithOptions::default()); let result = embed_css(&mut session, &url, &format!("@import \"{}\";", asset)); @@ -89,6 +90,7 @@ mod passing { "", b"p{font-family:'\x83\x81\x83\x43\x83\x8a\x83\x49'}", &url, + None, ); let mut session = Session::new(None, None, MonolithOptions::default()); session.environment_encoding = Some(encoding_rs::SHIFT_JIS); @@ -109,13 +111,19 @@ mod passing { fn nested_import_inherits_importing_stylesheet_encoding() { let url = Url::parse("https://example.com/").unwrap(); // EUC-JP "両", which also happens to be valid UTF-8 ("ξ") - let inner = - monolith::url::create_data_url("text/css", "", b"p::before{content:'\xce\xbe'}", &url); + let inner = monolith::url::create_data_url( + "text/css", + "", + b"p::before{content:'\xce\xbe'}", + &url, + None, + ); let outer = monolith::url::create_data_url( "text/css", "", format!("@charset \"EUC-JP\";@import \"{}\";", inner).as_bytes(), &url, + None, ); let mut session = Session::new(None, None, MonolithOptions::default()); session.environment_encoding = Some(encoding_rs::SHIFT_JIS); diff --git a/tests/html/walk.rs b/tests/html/walk.rs index fbcf38b..a0e2a85 100644 --- a/tests/html/walk.rs +++ b/tests/html/walk.rs @@ -42,7 +42,7 @@ mod passing { "@charset \"UTF-8\";p::before{content:\"café\"}", ), ] { - let asset = monolith::url::create_data_url("text/css", charset, bytes, &url); + let asset = monolith::url::create_data_url("text/css", charset, bytes, &url, None); let source = format!("", asset); let dom = html::html_to_dom(&source.into_bytes(), "utf-8".to_string()); let mut session = Session::new(None, None, MonolithOptions::default()); @@ -80,7 +80,7 @@ mod passing { #[test] fn linked_stylesheet_environment_encoding() { let url = Url::parse("https://example.com/").unwrap(); - let asset = monolith::url::create_data_url("text/css", "", SHIFT_JIS_CSS, &url); + let asset = monolith::url::create_data_url("text/css", "", SHIFT_JIS_CSS, &url, None); let source = format!("", asset); let dom = html::html_to_dom(&source.into_bytes(), "utf-8".to_string()); let mut session = Session::new(None, None, MonolithOptions::default()); @@ -102,14 +102,14 @@ mod passing { #[test] fn frame_encoding() { let url = Url::parse("https://example.com/").unwrap(); - let css = monolith::url::create_data_url("text/css", "", SHIFT_JIS_CSS, &url); + let css = monolith::url::create_data_url("text/css", "", SHIFT_JIS_CSS, &url, None); let mut frame_html: Vec = format!( "", css ) .into_bytes(); frame_html.extend_from_slice(b"\x83\x81\x83\x43\x83\x8a\x83\x49"); - let frame = monolith::url::create_data_url("text/html", "", &frame_html, &url); + let frame = monolith::url::create_data_url("text/html", "", frame_html, &url, None); let source = format!("", frame); let dom = html::html_to_dom(&source.into_bytes(), "utf-8".to_string()); let mut session = Session::new(None, None, MonolithOptions::default()); diff --git a/tests/session/retrieve_asset.rs b/tests/session/retrieve_asset.rs index 7960604..f09a29b 100644 --- a/tests/session/retrieve_asset.rs +++ b/tests/session/retrieve_asset.rs @@ -32,8 +32,8 @@ mod passing { assert_eq!(&media_type, "text/html"); assert_eq!(&charset, ""); assert_eq!( - url::create_data_url(&media_type, &charset, &data, &final_url), - Url::parse("data:text/html;base64,dGFyZ2V0").unwrap(), + url::create_data_url(&media_type, &charset, data, &final_url, None), + "data:text/html;base64,dGFyZ2V0", ); assert_eq!( final_url, @@ -72,8 +72,8 @@ mod passing { assert_eq!(&charset, ""); let data_url = "data:text/javascript;base64,ZG9jdW1lbnQuYm9keS5zdHlsZS5iYWNrZ3JvdW5kQ29sb3IgPSAiZ3JlZW4iOwpkb2N1bWVudC5ib2R5LnN0eWxlLmNvbG9yID0gInJlZCI7Cg=="; assert_eq!( - url::create_data_url(&media_type, &charset, &data, &final_url), - Url::parse(data_url).unwrap() + url::create_data_url(&media_type, &charset, data, &final_url, None), + data_url ); assert_eq!( final_url, diff --git a/tests/url/create_data_url.rs b/tests/url/create_data_url.rs index d7c4ef3..a8f3cac 100644 --- a/tests/url/create_data_url.rs +++ b/tests/url/create_data_url.rs @@ -20,6 +20,7 @@ mod passing { "", data.as_bytes(), &Url::parse("data:,").unwrap(), + None, ); assert_eq!( @@ -36,6 +37,7 @@ mod passing { "", data.as_bytes(), &Url::parse("data:,").unwrap(), + None, ); assert_eq!( @@ -54,6 +56,7 @@ mod passing { charset, data.as_bytes(), &Url::parse("data:,").unwrap(), + None, ); assert_eq!( @@ -72,6 +75,7 @@ mod passing { charset, data.as_bytes(), &Url::parse("data:,").unwrap(), + None, ); assert_eq!(data_url.as_str(), "data:;base64,"); @@ -87,6 +91,7 @@ mod passing { charset, data.as_bytes(), &Url::parse("data:,").unwrap(), + None, ); assert_eq!(data_url.as_str(), "data:;charset=utf8;base64,"); @@ -102,6 +107,7 @@ mod passing { charset, data.as_bytes(), &Url::parse("data:,").unwrap(), + None, ); assert_eq!(data_url.as_str(), "data:text/plain;charset=utf8;base64,"); @@ -113,10 +119,12 @@ mod passing { let data_url = url::create_data_url( "application/octet-stream", "", - &data, + data.clone(), &Url::parse("data:,").unwrap(), + None, ); - let (media_type, _charset, parsed_data) = url::parse_data_url(&data_url); + let (media_type, _charset, parsed_data) = + url::parse_data_url(&Url::parse(&data_url).unwrap()); assert_eq!(media_type, "application/octet-stream"); assert_eq!(parsed_data, data); @@ -124,31 +132,73 @@ mod passing { #[test] fn create_data_url_with_lowercased_media_type() { - let data_url = url::create_data_url("Image/PNG", "", b"", &Url::parse("data:,").unwrap()); + let data_url = + url::create_data_url("Image/PNG", "", b"", &Url::parse("data:,").unwrap(), None); assert_eq!(data_url.as_str(), "data:image/png;base64,"); } #[test] fn create_data_url_with_quoted_charset() { - let data_url = - url::create_data_url("text/css", "\"utf-8\"", b"", &Url::parse("data:,").unwrap()); + let data_url = url::create_data_url( + "text/css", + "\"utf-8\"", + b"", + &Url::parse("data:,").unwrap(), + None, + ); assert_eq!(data_url.as_str(), "data:text/css;charset=utf-8;base64,"); } #[test] fn create_data_url_omits_invalid_media_type() { - let data_url = url::create_data_url("wrong", "", b"", &Url::parse("data:,").unwrap()); + let data_url = url::create_data_url("wrong", "", b"", &Url::parse("data:,").unwrap(), None); assert_eq!(data_url.as_str(), "data:;base64,"); } #[test] fn create_data_url_omits_charset_that_would_break_data_url() { - let data_url = - url::create_data_url("text/css", "utf-8,x", b"", &Url::parse("data:,").unwrap()); + let data_url = url::create_data_url( + "text/css", + "utf-8,x", + b"", + &Url::parse("data:,").unwrap(), + None, + ); assert_eq!(data_url.as_str(), "data:text/css;base64,"); } + + #[test] + fn create_data_url_with_fragment() { + let asset_url = Url::parse("https://example.com/icons.svg#a%20b").unwrap(); + let data_url = url::create_data_url( + "image/svg+xml", + "", + b"", + &asset_url, + asset_url.fragment(), + ); + + assert_eq!(data_url, "data:image/svg+xml;base64,PHN2Zy8+#a%20b"); + } + + #[test] + fn create_data_url_encodes_fragment_like_url_does() { + for fragment in ["", "a b", "<\"`>", "caf\u{e9}", "%41%", "x#y"] { + let data_url = url::create_data_url( + "text/plain", + "", + b"x", + &Url::parse("data:,").unwrap(), + Some(fragment), + ); + let mut expected = Url::parse("data:text/plain;base64,eA==").unwrap(); + expected.set_fragment(Some(fragment)); + + assert_eq!(data_url, expected.as_str(), "{fragment:?}"); + } + } }