fix(media-proxy): rasterize svg responses (#1298)

This commit is contained in:
Hampus
2026-07-06 00:07:03 +02:00
committed by GitHub
parent bbd24c4016
commit e8bb455502
7 changed files with 317 additions and 194 deletions
Generated
-26
View File
@@ -1461,12 +1461,6 @@ version = "2.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8"
[[package]]
name = "data-url"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376"
[[package]]
name = "deadpool"
version = "0.12.3"
@@ -2026,7 +2020,6 @@ dependencies = [
"serde",
"serde_json",
"sha2 0.11.0",
"svg-hush",
"tempfile",
"thiserror",
"tokio",
@@ -5258,19 +5251,6 @@ version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "svg-hush"
version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "929223e80cdcec0482207576ea09692dd71b2b559057fc172e292ecec9a97559"
dependencies = [
"base64",
"data-url",
"quick-error 2.0.1",
"url",
"xml",
]
[[package]]
name = "syn"
version = "2.0.117"
@@ -6564,12 +6544,6 @@ dependencies = [
"rustix",
]
[[package]]
name = "xml"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "636f85e5ca6488e96401b61eb7de54f4e44755c988af0f52cf90230c312a1a89"
[[package]]
name = "xmlparser"
version = "0.13.6"
-1
View File
@@ -44,7 +44,6 @@ reqwest-retry = "0.9.1"
serde = {version = "1.0.228", features = ["derive"]}
serde_json = "1.0.150"
sha2 = "0.11.0"
svg-hush = "0.9.6"
tempfile = "3.27.0"
thiserror = "2.0.18"
tokio = {version = "1.52.3", features = ["fs", "io-util", "macros", "net", "rt-multi-thread", "signal", "sync", "time"]}
+5 -2
View File
@@ -29,6 +29,9 @@ fn normalize_mime(content_type: &str) -> &str {
pub fn is_inline_viewable(content_type: &str) -> bool {
let mime = normalize_mime(content_type);
if mime.eq_ignore_ascii_case("image/svg+xml") {
return false;
}
if mime.len() >= 6 && mime[..6].eq_ignore_ascii_case("image/") {
return true;
}
@@ -111,7 +114,7 @@ mod tests {
);
assert_eq!(Decision::Inline, decide("video/mp4", false));
assert_eq!(Decision::Inline, decide("application/pdf", false));
assert_eq!(Decision::Inline, decide("image/svg+xml", false));
assert_eq!(Decision::Attachment, decide("image/svg+xml", false));
assert_eq!(
Decision::Attachment,
decide("application/octet-stream", false)
@@ -132,7 +135,7 @@ mod tests {
#[test]
fn case_insensitive_mime_matching() {
assert_eq!(Decision::Inline, decide("IMAGE/PNG", false));
assert_eq!(Decision::Inline, decide("Image/Svg+Xml", false));
assert_eq!(Decision::Attachment, decide("Image/Svg+Xml", false));
}
#[test]
-1
View File
@@ -27,7 +27,6 @@ pub mod server;
pub mod signing;
pub mod spool;
pub mod storage;
pub mod svg;
pub mod thumbhash;
pub mod timed_semaphore;
pub mod upload_relay;
+18 -2
View File
@@ -20,12 +20,16 @@ pub struct OutputSelection {
pub fn is_output_format_supported(ext: AssetExtension) -> bool {
!matches!(
ext,
AssetExtension::Avif | AssetExtension::Heic | AssetExtension::Heif | AssetExtension::Jxl
AssetExtension::Avif
| AssetExtension::Heic
| AssetExtension::Heif
| AssetExtension::Jxl
| AssetExtension::Svg
)
}
pub fn can_encode_to(ext: AssetExtension) -> bool {
is_output_format_supported(ext) && ext != AssetExtension::Svg
is_output_format_supported(ext)
}
pub fn coerce_unsupported_format(ext: AssetExtension) -> AssetExtension {
@@ -80,6 +84,18 @@ mod tests {
assert_eq!("url-coerced", r.reason);
}
#[test]
fn svg_url_extension_coerces_to_webp() {
let r = select_url_variant(Input {
kind: AssetKind::Avatar,
original: AssetExtension::Svg,
requested_size: Some(128),
manual_format_override: None,
});
assert_eq!(AssetExtension::Webp, r.format);
assert_eq!("url-coerced", r.reason);
}
#[test]
fn manual_query_format_wins_over_url_extension() {
let r = select_url_variant(Input {
+294 -76
View File
@@ -13,7 +13,6 @@ use crate::{
signing,
spool::{SpoolError, spool_to_temp},
storage::{HeadResult, RelayBody, RelayPutOptions, StorageError, Store, StreamObject},
svg,
timed_semaphore::TimedSemaphore,
upload_relay,
};
@@ -262,10 +261,16 @@ async fn metadata_handler(
"allow" => false,
_ => return text(StatusCode::BAD_REQUEST, "Bad Request"),
};
let input = match load_metadata_input(&app, &req).await {
let mut input = match load_metadata_input(&app, &req).await {
Ok(input) => input,
Err(status) => return text(status, status.canonical_reason().unwrap_or("Bad Request")),
};
if req.with_base64.unwrap_or(false) && metadata_input_is_svg(&input) {
input = match rasterize_metadata_svg(&app, input).await {
Ok(input) => input,
Err(response) => return response,
};
}
let json = match media_process::metadata_json_with_options(
&input.data,
&input.filename,
@@ -304,6 +309,55 @@ struct InputData {
filename: String,
}
fn metadata_input_is_svg(input: &InputData) -> bool {
mime::sniff(&input.data).mime == "image/svg+xml"
|| image_extension_from_filename(&input.filename) == Some(AssetExtension::Svg)
}
fn replace_image_extension(filename: &str, ext: AssetExtension) -> String {
let last_slash = filename.rfind('/').map(|idx| idx + 1).unwrap_or(0);
let last_dot = filename[last_slash..]
.rfind('.')
.map(|idx| last_slash + idx);
match last_dot {
Some(idx) => format!("{}.{}", &filename[..idx], ext.name()),
None => format!("{}.{}", filename, ext.name()),
}
}
async fn rasterize_metadata_svg(
app: &Arc<AppState>,
input: InputData,
) -> Result<InputData, Response> {
let options = media_process::ImageOptions {
format: AssetExtension::Webp,
quality: "lossless".to_owned(),
animated: false,
deadline_ms: Some(metrics::now_ms() + app.cfg.transform_timeout_ms as i64),
max_encode_frames: Some(app.cfg.max_encode_frames),
max_encode_duration_ms: Some(app.cfg.max_encode_duration_ms),
..Default::default()
};
match run_transform(app, input.data, options).await {
Ok(media) => Ok(InputData {
data: media.bytes.into(),
filename: replace_image_extension(&input.filename, AssetExtension::Webp),
}),
Err(err) if transform_error_is_timeout(&err) => Err(text_with_source(
StatusCode::GATEWAY_TIMEOUT,
"Gateway Timeout",
"metadata_svg_rasterize_timeout",
input.filename,
)),
Err(err) => Err(text_with_source(
StatusCode::BAD_REQUEST,
"Bad Request",
"metadata_svg_rasterize_failed",
format!("filename={} err={err:?}", input.filename),
)),
}
}
async fn load_metadata_input(
app: &AppState,
req: &MetadataRequest,
@@ -939,7 +993,10 @@ async fn serve_external(
Ok(url) => url,
Err(_) => return text(StatusCode::BAD_REQUEST, "Bad Request"),
};
let wants_transform = params.contains_key("width")
let url_ext_is_svg =
image_extension_from_filename(&url_filename(&url)) == Some(AssetExtension::Svg);
let wants_transform = url_ext_is_svg
|| params.contains_key("width")
|| params.contains_key("height")
|| params.contains_key("format")
|| params.contains_key("quality")
@@ -951,7 +1008,8 @@ async fn serve_external(
.filter(|rv| !rv.is_empty() && rv.bytes().all(|b| b.is_ascii_graphic()));
let forward_range = if wants_transform { None } else { client_range };
let allow_stream = !wants_transform;
let fetched = match fetch_external_with_range(app, &url, forward_range, allow_stream).await {
let mut fetched = match fetch_external_with_range(app, &url, forward_range, allow_stream).await
{
Ok(fetched) if fetched.status.is_success() => fetched,
Ok(fetched) => {
return text_with_source(
@@ -987,6 +1045,47 @@ async fn serve_external(
);
}
};
if forward_range.is_some()
&& fetched.status == StatusCode::PARTIAL_CONTENT
&& is_svg_content_type(&fetched.content_type)
{
fetched = match fetch_external_with_range(app, &url, None, false).await {
Ok(fetched) if fetched.status.is_success() => fetched,
Ok(fetched) => {
return text_with_source(
map_upstream_status(fetched.status),
"Upstream fetch failed",
"external_upstream_status",
format!("url={url} upstream_status={}", fetched.status.as_u16()),
);
}
Err(ExternalFetchError::BlockedUrl) => {
return text_with_source(
StatusCode::BAD_REQUEST,
"Bad Request",
"external_blocked_url",
&url,
);
}
Err(ExternalFetchError::PayloadTooLarge) => {
return text_with_source(
StatusCode::PAYLOAD_TOO_LARGE,
"Payload Too Large",
"external_payload_too_large",
&url,
);
}
Err(err @ ExternalFetchError::TooManyRedirects)
| Err(err @ ExternalFetchError::FetchFailed) => {
return text_with_source(
StatusCode::BAD_GATEWAY,
"Bad Gateway",
"external_fetch_failed",
format!("url={url} err={err:?}"),
);
}
};
}
let filename = url_filename(&fetched.url);
let requested_download = bool_param(params, "download", false);
if forward_range.is_some() && fetched.status == StatusCode::PARTIAL_CONTENT {
@@ -1091,7 +1190,7 @@ fn external_stream_length(
if !content_type_is_trustworthy(content_type) {
return None;
}
if content_type.eq_ignore_ascii_case("image/svg+xml") {
if is_svg_content_type(content_type) {
return None;
}
Some(len)
@@ -1403,29 +1502,6 @@ async fn serve_asset_image(
.or(sniffed_source_ext)
.unwrap_or(asset.original_ext)
};
if source_format == AssetExtension::Svg {
let sanitized = match svg::sanitize(&object.data) {
Ok(bytes) => bytes,
Err(_) => {
return text_with_reason(
StatusCode::BAD_REQUEST,
"Bad Request",
"svg_sanitize_failed",
);
}
};
return media_response(
method,
Bytes::from(sanitized),
"image/svg+xml",
headers.get(header::RANGE).and_then(|v| v.to_str().ok()),
Some(content_disposition_header(
"image/svg+xml",
requested_download,
Some(&asset_filename),
)),
);
}
let serve_content_type = if object.content_type.is_empty()
|| object
.content_type
@@ -1535,7 +1611,9 @@ async fn serve_asset_image(
let src_is_displayable = src_ct.starts_with("image/")
&& src_ct != "image/avif"
&& src_ct != "image/heic"
&& src_ct != "image/heif";
&& src_ct != "image/heif"
&& source_format != AssetExtension::Svg
&& !is_svg_content_type(src_ct);
if !src_is_displayable {
return text_with_source(
StatusCode::INTERNAL_SERVER_ERROR,
@@ -1681,28 +1759,23 @@ async fn serve_stored_passthrough_stream(
return storage_error_response(key, StorageError::StreamTooLong);
}
let content_type = passthrough_content_type(&head, key);
if content_type.eq_ignore_ascii_case("image/svg+xml") {
if is_svg_content_type(&content_type)
|| image_extension_from_filename(key) == Some(AssetExtension::Svg)
{
let object = match app.store.read_object(bucket, key).await {
Ok(object) => object,
Err(err) => return storage_error_response(key, err),
};
let sanitized = match svg::sanitize(&object.data) {
Ok(bytes) => bytes,
Err(_) => {
return text_with_reason(
StatusCode::BAD_REQUEST,
"Bad Request",
"svg_sanitize_failed",
);
}
};
return media_response(
let cache_identity = format!("{bucket}/{key}");
return serve_stored_svg_rasterized(
app,
method,
Bytes::from(sanitized),
&content_type,
headers.get(header::RANGE).and_then(|v| v.to_str().ok()),
passthrough_disposition_header(&disposition, &content_type),
);
object.data,
&cache_identity,
headers,
&disposition,
)
.await;
}
let total_len = match usize::try_from(head.content_length) {
Ok(value) => value,
@@ -1783,6 +1856,91 @@ fn passthrough_disposition_header(
}
}
async fn serve_stored_svg_rasterized(
app: &Arc<AppState>,
method: Method,
data: Bytes,
cache_identity: &str,
headers: &HeaderMap,
disposition: &PassthroughDisposition<'_>,
) -> Response {
let format = AssetExtension::Webp;
let quality = "lossless".to_owned();
let options = media_process::ImageOptions {
format,
quality: quality.clone(),
animated: false,
deadline_ms: Some(metrics::now_ms() + app.cfg.transform_timeout_ms as i64),
max_encode_frames: Some(app.cfg.max_encode_frames),
max_encode_duration_ms: Some(app.cfg.max_encode_duration_ms),
..Default::default()
};
let cache_key = transform_cache_key(TransformCacheKeyInput {
route: TransformRoute::Stored,
cache_identity,
width: None,
height: None,
format,
quality: &quality,
animated: false,
effort: None,
});
if let Some(cached) = app.transform_cache.get(&cache_key) {
metrics::GLOBAL
.transform_cache_hits
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
return media_response(
method,
cached,
format.mime(),
headers.get(header::RANGE).and_then(|v| v.to_str().ok()),
passthrough_disposition_header(disposition, format.mime()),
);
}
metrics::GLOBAL
.transform_cache_misses
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let coalescer_deadline = deadline_instant(options.deadline_ms);
let transformed = match app
.coalescer
.run_once_until(cache_key.clone(), coalescer_deadline, || {
let app = app.clone();
let data = data.clone();
let options = options.clone();
async move {
coalesced_work_result(run_transform(&app, data, options).await)
.map(|media| media.bytes)
}
})
.await
{
Ok(bytes) => bytes,
Err(CoalescerError::RequestTimeout) => {
return text_with_reason(
StatusCode::GATEWAY_TIMEOUT,
"Gateway Timeout",
"coalescer_timeout_svg_rasterize",
);
}
Err(CoalescerError::WorkFailed) => {
return text_with_source(
StatusCode::BAD_REQUEST,
"Bad Request",
"svg_rasterize_failed",
cache_identity,
);
}
};
app.transform_cache.put(cache_key, transformed.clone());
media_response(
method,
transformed,
format.mime(),
headers.get(header::RANGE).and_then(|v| v.to_str().ok()),
passthrough_disposition_header(disposition, format.mime()),
)
}
fn passthrough_head_response(
content_type: &str,
total_len: usize,
@@ -1890,6 +2048,7 @@ async fn serve_stored_with_override(
enum TransformRoute {
Attachment,
External,
Stored,
}
struct ServeBytesRequest<'a> {
@@ -1915,40 +2074,24 @@ async fn serve_bytes_or_transform(app: &Arc<AppState>, request: ServeBytesReques
headers,
} = request;
let animated = animated_param(params, false);
let wants_transform = params.contains_key("width")
|| params.contains_key("height")
|| params.contains_key("format")
|| params.contains_key("quality")
|| animated;
let content_type = if content_type_is_trustworthy(&content_type) {
let sniffed_prefix = mime::sniff(&data[..data.len().min(8192)]);
let content_type = if sniffed_prefix.mime == "image/svg+xml" {
"image/svg+xml".to_owned()
} else if content_type_is_trustworthy(&content_type) {
content_type
} else {
mime::detect(&data[..data.len().min(8192)], filename, Some(&content_type))
};
let source_is_svg = is_svg_content_type(&content_type)
|| image_extension_from_filename(filename) == Some(AssetExtension::Svg);
let wants_transform = source_is_svg
|| params.contains_key("width")
|| params.contains_key("height")
|| params.contains_key("format")
|| params.contains_key("quality")
|| animated;
let requested_download = bool_param(params, "download", false);
if content_type.eq_ignore_ascii_case("image/svg+xml") {
let sanitized = match svg::sanitize(&data) {
Ok(bytes) => bytes,
Err(_) => {
return text_with_reason(
StatusCode::BAD_REQUEST,
"Bad Request",
"svg_sanitize_failed",
);
}
};
let disposition =
content_disposition_header("image/svg+xml", requested_download, Some(filename));
return media_response(
method,
Bytes::from(sanitized),
"image/svg+xml",
headers.get(header::RANGE).and_then(|v| v.to_str().ok()),
Some(disposition),
);
}
if !wants_transform {
let disposition =
content_disposition_header(&content_type, requested_download, Some(filename));
@@ -2098,6 +2241,9 @@ async fn serve_bytes_or_transform(app: &Arc<AppState>, request: ServeBytesReques
image_extension_from_filename(filename).unwrap_or(AssetExtension::Webp)
}
TransformRoute::External => external_default_output_extension(filename, &content_type),
TransformRoute::Stored => {
image_extension_from_filename(filename).unwrap_or(AssetExtension::Webp)
}
};
let requested_format = explicit_requested_format.unwrap_or(default_out_ext);
let requested_supported_format = output_format::coerce_unsupported_format(requested_format);
@@ -2752,6 +2898,11 @@ fn content_type_is_trustworthy(content_type: &str) -> bool {
)
}
fn is_svg_content_type(content_type: &str) -> bool {
mime::normalize(Some(content_type))
.is_some_and(|value| value.eq_ignore_ascii_case("image/svg+xml"))
}
fn extension_from_mime(content_type: &str) -> Option<AssetExtension> {
match mime::normalize(Some(content_type))? {
"image/jpeg" => Some(AssetExtension::Jpeg),
@@ -2784,7 +2935,10 @@ fn transform_response_content_type(
out_ext: AssetExtension,
fallback_content_type: &str,
) -> &str {
if explicit_out_ext.is_some() || out_ext != requested_out_ext {
if explicit_out_ext.is_some()
|| out_ext != requested_out_ext
|| is_svg_content_type(fallback_content_type)
{
out_ext.mime()
} else {
fallback_content_type
@@ -2806,9 +2960,10 @@ fn transform_cache_key(input: TransformCacheKeyInput<'_>) -> String {
let prefix = match input.route {
TransformRoute::Attachment => "attachment",
TransformRoute::External => "external",
TransformRoute::Stored => "stored",
};
let identity = match input.route {
TransformRoute::Attachment => input.cache_identity.to_owned(),
TransformRoute::Attachment | TransformRoute::Stored => input.cache_identity.to_owned(),
TransformRoute::External => sha256_hex(input.cache_identity.as_bytes()),
};
format!(
@@ -3020,6 +3175,60 @@ mod tests {
assert_eq!(None, parse_dimension(Some("999999999")));
}
#[test]
fn metadata_base64_svg_detection_uses_bytes_or_filename() {
let svg_bytes = InputData {
data: Bytes::from_static(br#"<svg xmlns="http://www.w3.org/2000/svg"></svg>"#),
filename: "upload.bin".to_owned(),
};
assert!(metadata_input_is_svg(&svg_bytes));
let svg_filename = InputData {
data: Bytes::from_static(b"not svg"),
filename: "icons/logo.svg".to_owned(),
};
assert!(metadata_input_is_svg(&svg_filename));
let png_filename = InputData {
data: Bytes::from_static(b"not svg"),
filename: "icons/logo.png".to_owned(),
};
assert!(!metadata_input_is_svg(&png_filename));
}
#[test]
fn replace_image_extension_only_changes_last_path_segment() {
assert_eq!(
"avatars/user.icon.webp",
replace_image_extension("avatars/user.icon.svg", AssetExtension::Webp)
);
assert_eq!(
"avatars.v1/user.webp",
replace_image_extension("avatars.v1/user", AssetExtension::Webp)
);
}
#[tokio::test]
async fn metadata_base64_svg_rasterizes_to_webp_bytes() {
let tmp = tempfile::tempdir().unwrap();
let cfg =
upload_relay_test_config(tmp.path(), tmp.path(), b"01234567890123456789012345678901");
let app = test_app_state(cfg);
let input = InputData {
data: Bytes::from_static(
br#"<svg xmlns="http://www.w3.org/2000/svg" width="8" height="8"><rect width="8" height="8" fill="red"/></svg>"#,
),
filename: "icons/logo.svg".to_owned(),
};
let raster = match rasterize_metadata_svg(&app, input).await {
Ok(raster) => raster,
Err(response) => panic!("unexpected status {}", response.status()),
};
assert_eq!("icons/logo.webp", raster.filename);
assert_eq!("image/webp", mime::sniff(&raster.data).mime);
}
#[test]
fn passthrough_content_type_preserves_non_media_metadata() {
let head = HeadResult {
@@ -3692,6 +3901,15 @@ mod tests {
"image/heic"
)
);
assert_eq!(
"image/webp",
transform_response_content_type(
None,
AssetExtension::Webp,
AssetExtension::Webp,
"image/svg+xml; charset=utf-8"
)
);
assert_eq!(
"image/gif",
transform_response_content_type(
-86
View File
@@ -1,86 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
use std::io::Cursor;
#[derive(Debug, thiserror::Error)]
pub enum SvgError {
#[error("svg sanitization failed")]
Sanitize,
}
const MAX_SANITIZED_BYTES: usize = 16 * 1024 * 1024;
pub fn sanitize(input: &[u8]) -> Result<Vec<u8>, SvgError> {
let filter = svg_hush::Filter::new();
let mut out: Vec<u8> = Vec::with_capacity(input.len());
filter
.filter(Cursor::new(input), &mut out)
.map_err(|_| SvgError::Sanitize)?;
if out.len() > MAX_SANITIZED_BYTES {
return Err(SvgError::Sanitize);
}
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn drops_inline_script_elements() {
let svg = br#"<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10">
<script>alert(1)</script>
<rect width="10" height="10" fill="red"/>
</svg>"#;
let cleaned = sanitize(svg).expect("sanitize ok");
let text = std::str::from_utf8(&cleaned).unwrap();
assert!(!text.contains("<script"), "script tag survived: {text}");
assert!(text.contains("<rect"), "rect element missing: {text}");
}
#[test]
fn drops_event_handler_attributes() {
let svg = br#"<svg xmlns="http://www.w3.org/2000/svg" onload="alert(1)">
<rect width="1" height="1" onclick="alert(2)"/>
</svg>"#;
let cleaned = sanitize(svg).expect("sanitize ok");
let text = std::str::from_utf8(&cleaned).unwrap();
assert!(!text.to_ascii_lowercase().contains("onload"));
assert!(!text.to_ascii_lowercase().contains("onclick"));
}
#[test]
fn neutralizes_javascript_hrefs() {
let svg = br#"<svg xmlns="http://www.w3.org/2000/svg">
<a xlink:href="javascript:alert(1)" href="javascript:alert(2)">
<rect width="1" height="1"/>
</a>
</svg>"#;
if let Ok(cleaned) = sanitize(svg) {
let text = std::str::from_utf8(&cleaned).unwrap();
assert!(
!text.to_ascii_lowercase().contains("javascript:"),
"javascript: scheme survived sanitization: {text}"
);
}
}
#[test]
fn preserves_basic_shapes_and_paths() {
let svg = br##"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
<path d="M0 0 L10 10 Z" fill="#abc"/>
<circle cx="5" cy="5" r="3"/>
</svg>"##;
let cleaned = sanitize(svg).expect("sanitize ok");
let text = std::str::from_utf8(&cleaned).unwrap();
assert!(text.contains("<svg"));
assert!(text.contains("<path"));
assert!(text.contains("<circle"));
}
#[test]
fn rejects_invalid_xml() {
let result = sanitize(b"<svg>this is < not valid </svg");
assert!(result.is_err());
}
}