use crate::detect::Format;
use crate::error::{MalformedDetail, Result, StryptError};
use crate::formats::{MetadataHandler, ParseLimits, StripOptions, Stripped};
use crate::report::{InspectOptions, MetadataReport, StripReport};
mod data_uri;
mod rules;
#[derive(Debug, Clone, Copy)]
pub struct SvgHandler;
impl MetadataHandler for SvgHandler {
fn name(&self) -> &'static str {
Format::Svg.id()
}
fn format(&self) -> Format {
Format::Svg
}
fn inspect(&self, input: &[u8], options: &InspectOptions) -> Result<MetadataReport> {
let outcome = rules::process(text(input)?, options, &ParseLimits::default())?;
Ok(MetadataReport {
format: Format::Svg,
findings: outcome.findings,
notes: outcome.notes,
})
}
fn strip(&self, input: &[u8], options: &StripOptions) -> Result<Stripped> {
let source = text(input)?;
let outcome = rules::process(source, &options.inspect, &options.limits)?;
let bytes = outcome.output.unwrap_or_else(|| source.as_bytes().to_vec());
Ok(Stripped {
report: StripReport {
format: Format::Svg,
removed: outcome.findings,
retained: outcome.retained,
notes: outcome.notes,
input_bytes: crate::container::package::as_u64(input.len()),
output_bytes: crate::container::package::as_u64(bytes.len()),
},
bytes,
})
}
}
fn text(input: &[u8]) -> Result<&str> {
std::str::from_utf8(input).map_err(|e| StryptError::Malformed {
format: Format::Svg,
offset: Some(crate::container::package::as_u64(e.valid_up_to())),
detail: MalformedDetail::UnsupportedFeature,
})
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used, clippy::expect_used)]
use super::*;
use crate::error::UnsupportedKind;
const CLEAN: &str = "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 8 8\">\
<rect width=\"8\" height=\"8\" fill=\"#abcdef\"/></svg>";
fn strip(src: &str) -> Stripped {
SvgHandler
.strip(src.as_bytes(), &StripOptions::default())
.expect("stripping a well-formed drawing")
}
#[test]
fn a_clean_drawing_comes_back_byte_identical() {
let stripped = strip(CLEAN);
assert_eq!(stripped.bytes, CLEAN.as_bytes());
assert!(stripped.report.removed.is_empty());
}
#[test]
fn stripping_is_idempotent_byte_for_byte() {
let dirty = "<?xml version=\"1.0\"?><!-- Generator: A Tool -->\
<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:i=\"http://ns.adobe.com/\" \
i:extraneous=\"self\"><metadata><dc:creator>A Name</dc:creator></metadata>\
<rect/></svg>";
let once = strip(dirty);
let twice = SvgHandler
.strip(&once.bytes, &StripOptions::default())
.unwrap();
assert_eq!(once.bytes, twice.bytes);
assert!(twice.report.removed.is_empty());
}
#[test]
fn what_strip_removes_inspect_can_see() {
let dirty = "<svg xmlns=\"http://www.w3.org/2000/svg\"><!-- gen -->\
<metadata><dc:creator>A Name</dc:creator></metadata><rect/></svg>";
let before = SvgHandler
.inspect(dirty.as_bytes(), &InspectOptions::names_only())
.unwrap();
assert!(before.has_findings());
let after = SvgHandler
.inspect(&strip(dirty).bytes, &InspectOptions::names_only())
.unwrap();
assert!(!after.has_findings(), "{:?}", after.findings);
}
#[test]
fn a_utf16_document_is_refused_rather_than_read_as_bytes() {
let mut utf16 = vec![0xFF, 0xFE];
for unit in "<svg/>".encode_utf16() {
utf16.extend_from_slice(&unit.to_le_bytes());
}
assert!(matches!(
SvgHandler.strip(&utf16, &StripOptions::default()),
Err(StryptError::Malformed { .. })
));
}
#[test]
fn a_scripted_document_is_refused_by_name() {
let src = "<svg xmlns=\"http://www.w3.org/2000/svg\"><script>fetch('x')</script></svg>";
let e = SvgHandler
.strip(src.as_bytes(), &StripOptions::default())
.expect_err("a document that can execute code must be refused");
assert!(
matches!(
e,
StryptError::UnsupportedFormat {
format: UnsupportedKind::ScriptedSvg
}
),
"{e:?}"
);
assert!(e.to_string().contains("script"), "{e}");
}
#[test]
fn an_embedded_photograph_is_stripped_through_its_own_handler() {
let png = png_with_a_text_chunk();
let encoded = data_uri::encode("image/png", &png);
let src =
format!("<svg xmlns=\"http://www.w3.org/2000/svg\"><image href=\"{encoded}\"/></svg>");
let stripped = strip(&src);
let out = String::from_utf8(stripped.bytes).unwrap();
assert!(!out.contains(&encoded), "the URI must have been rewritten");
assert!(out.starts_with("<svg"), "the drawing itself is untouched");
assert!(
!stripped.report.removed.is_empty(),
"the embedded picture's metadata has to be reported"
);
}
fn png_with_a_text_chunk() -> Vec<u8> {
fn chunk(kind: [u8; 4], data: &[u8]) -> Vec<u8> {
let mut out = u32::try_from(data.len()).unwrap().to_be_bytes().to_vec();
out.extend_from_slice(&kind);
out.extend_from_slice(data);
let mut crc = 0xFFFF_FFFFu32;
for byte in kind.iter().chain(data) {
crc ^= u32::from(*byte);
for _ in 0..8 {
crc = if crc & 1 == 1 {
(crc >> 1) ^ 0xEDB8_8320
} else {
crc >> 1
};
}
}
out.extend_from_slice(&(!crc).to_be_bytes());
out
}
let mut png = vec![0x89, b'P', b'N', b'G', 0x0D, 0x0A, 0x1A, 0x0A];
let mut ihdr = 1u32.to_be_bytes().to_vec();
ihdr.extend_from_slice(&1u32.to_be_bytes());
ihdr.extend_from_slice(&[8, 0, 0, 0, 0]);
png.extend(chunk(*b"IHDR", &ihdr));
png.extend(chunk(*b"tEXt", b"Author\0A Name"));
png.extend(chunk(
*b"IDAT",
&[
0x78, 0x01, 0x01, 0x02, 0x00, 0xFD, 0xFF, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01,
],
));
png.extend(chunk(*b"IEND", b""));
png
}
}