use crate::parser::{read_u16, read_u32};
use crate::Error;
pub const SVG_TABLE_TAG: [u8; 4] = *b"SVG ";
pub const SVG_VERSION_0: u16 = 0;
#[doc(hidden)]
pub const SVG_HEADER_LEN: usize = 10;
#[doc(hidden)]
pub const SVG_DOCUMENT_RECORD_LEN: usize = 12;
pub const SVG_GZIP_MAGIC: [u8; 3] = [0x1F, 0x8B, 0x08];
const MAX_DOCUMENT_RECORDS: usize = u16::MAX as usize;
#[derive(Debug, Clone, Copy)]
pub struct SvgDocument<'a> {
pub start_glyph_id: u16,
pub end_glyph_id: u16,
pub data: &'a [u8],
}
impl<'a> SvgDocument<'a> {
pub fn is_gzip_encoded(&self) -> bool {
self.data.len() >= SVG_GZIP_MAGIC.len()
&& self.data[..SVG_GZIP_MAGIC.len()] == SVG_GZIP_MAGIC
}
pub fn covers(&self, gid: u16) -> bool {
gid >= self.start_glyph_id && gid <= self.end_glyph_id
}
}
#[derive(Debug, Clone)]
#[doc(hidden)]
pub struct SvgTable<'a> {
version: u16,
documents: Vec<SvgDocument<'a>>,
}
impl<'a> SvgTable<'a> {
pub fn parse(bytes: &'a [u8]) -> Result<Self, Error> {
if bytes.len() < SVG_HEADER_LEN {
return Err(Error::UnexpectedEof);
}
let version = read_u16(bytes, 0)?;
if version != SVG_VERSION_0 {
return Err(Error::BadStructure("SVG: version != 0"));
}
let doc_list_offset = read_u32(bytes, 2)? as usize;
if doc_list_offset == 0 {
return Err(Error::BadStructure("SVG: offsetToSVGDocumentList == 0"));
}
if doc_list_offset + 2 > bytes.len() {
return Err(Error::BadStructure("SVG: document list past end of table"));
}
let num_entries = read_u16(bytes, doc_list_offset)? as usize;
if num_entries == 0 {
return Err(Error::BadStructure("SVG: numEntries == 0"));
}
if num_entries > MAX_DOCUMENT_RECORDS {
return Err(Error::BadStructure("SVG: numEntries cap"));
}
let records_base = doc_list_offset + 2;
let records_end = records_base
.checked_add(
num_entries
.checked_mul(SVG_DOCUMENT_RECORD_LEN)
.ok_or(Error::BadStructure("SVG: document records overflow"))?,
)
.ok_or(Error::BadStructure("SVG: document records overflow"))?;
if records_end > bytes.len() {
return Err(Error::UnexpectedEof);
}
let total_len = bytes.len();
let mut documents: Vec<SvgDocument<'a>> = Vec::with_capacity(num_entries);
let mut prev_end: Option<u16> = None;
for i in 0..num_entries {
let off = records_base + i * SVG_DOCUMENT_RECORD_LEN;
let start_glyph_id = read_u16(bytes, off)?;
let end_glyph_id = read_u16(bytes, off + 2)?;
if start_glyph_id > end_glyph_id {
return Err(Error::BadStructure("SVG: startGlyphID > endGlyphID"));
}
if let Some(prev) = prev_end {
if start_glyph_id <= prev {
return Err(Error::BadStructure(
"SVG: record range not strictly after previous",
));
}
}
prev_end = Some(end_glyph_id);
let svg_doc_offset = read_u32(bytes, off + 4)? as usize;
let svg_doc_length = read_u32(bytes, off + 8)? as usize;
if svg_doc_offset == 0 {
return Err(Error::BadStructure("SVG: svgDocOffset == 0"));
}
if svg_doc_length == 0 {
return Err(Error::BadStructure("SVG: svgDocLength == 0"));
}
let doc_start = doc_list_offset
.checked_add(svg_doc_offset)
.ok_or(Error::BadStructure("SVG: svgDocOffset overflow"))?;
let doc_end = doc_start
.checked_add(svg_doc_length)
.ok_or(Error::BadStructure("SVG: svgDocOffset + length overflow"))?;
if doc_end > total_len {
return Err(Error::BadStructure("SVG: document past end of table"));
}
documents.push(SvgDocument {
start_glyph_id,
end_glyph_id,
data: &bytes[doc_start..doc_end],
});
}
Ok(Self { version, documents })
}
pub fn version(&self) -> u16 {
self.version
}
pub fn documents(&self) -> &[SvgDocument<'a>] {
&self.documents
}
pub fn document_for_glyph(&self, gid: u16) -> Option<&SvgDocument<'a>> {
let idx = self.documents.partition_point(|d| d.start_glyph_id <= gid);
if idx == 0 {
return None;
}
let candidate = &self.documents[idx - 1];
if candidate.covers(gid) {
Some(candidate)
} else {
None
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn build(records: &[(u16, u16, &[u8])]) -> Vec<u8> {
let mut b = Vec::new();
b.extend_from_slice(&SVG_VERSION_0.to_be_bytes());
let doc_list_offset = SVG_HEADER_LEN as u32;
b.extend_from_slice(&doc_list_offset.to_be_bytes());
b.extend_from_slice(&0u32.to_be_bytes()); debug_assert_eq!(b.len(), SVG_HEADER_LEN);
b.extend_from_slice(&(records.len() as u16).to_be_bytes());
let records_base = SVG_HEADER_LEN + 2;
let payload_base = records_base + records.len() * SVG_DOCUMENT_RECORD_LEN;
let mut cur = payload_base - SVG_HEADER_LEN; for (start, end, payload) in records {
b.extend_from_slice(&start.to_be_bytes());
b.extend_from_slice(&end.to_be_bytes());
b.extend_from_slice(&(cur as u32).to_be_bytes());
b.extend_from_slice(&(payload.len() as u32).to_be_bytes());
cur += payload.len();
}
for (_, _, payload) in records {
b.extend_from_slice(payload);
}
b
}
#[test]
fn parses_single_document() {
let svg = b"<svg xmlns=\"http://www.w3.org/2000/svg\"/>";
let bytes = build(&[(3, 5, svg)]);
let table = SvgTable::parse(&bytes).expect("parse");
assert_eq!(table.version(), 0);
assert_eq!(table.documents().len(), 1);
let doc = &table.documents()[0];
assert_eq!(doc.start_glyph_id, 3);
assert_eq!(doc.end_glyph_id, 5);
assert_eq!(doc.data, svg);
assert!(!doc.is_gzip_encoded());
}
#[test]
fn document_for_glyph_resolves_inside_and_outside_ranges() {
let a = b"<svg>A</svg>";
let b = b"<svg>B</svg>";
let bytes = build(&[(10, 12, a), (20, 20, b)]);
let table = SvgTable::parse(&bytes).expect("parse");
assert_eq!(table.document_for_glyph(10).unwrap().data, a);
assert_eq!(table.document_for_glyph(11).unwrap().data, a);
assert_eq!(table.document_for_glyph(12).unwrap().data, a);
assert_eq!(table.document_for_glyph(20).unwrap().data, b);
assert!(table.document_for_glyph(9).is_none());
assert!(table.document_for_glyph(13).is_none());
assert!(table.document_for_glyph(19).is_none());
assert!(table.document_for_glyph(21).is_none());
}
#[test]
fn detects_gzip_encoded_document() {
let mut gz = vec![0x1F, 0x8B, 0x08];
gz.extend_from_slice(&[0x00; 16]); let bytes = build(&[(1, 1, &gz)]);
let table = SvgTable::parse(&bytes).expect("parse");
let doc = &table.documents()[0];
assert!(doc.is_gzip_encoded());
assert_eq!(doc.data.len(), gz.len());
}
#[test]
fn shared_document_across_two_records_round_trips() {
let payload = b"<svg>shared</svg>";
let mut b = Vec::new();
b.extend_from_slice(&SVG_VERSION_0.to_be_bytes());
let doc_list_offset = SVG_HEADER_LEN as u32;
b.extend_from_slice(&doc_list_offset.to_be_bytes());
b.extend_from_slice(&0u32.to_be_bytes()); b.extend_from_slice(&2u16.to_be_bytes()); let records_base = SVG_HEADER_LEN + 2;
let payload_base = records_base + 2 * SVG_DOCUMENT_RECORD_LEN;
let shared_off = (payload_base - SVG_HEADER_LEN) as u32; b.extend_from_slice(&5u16.to_be_bytes());
b.extend_from_slice(&6u16.to_be_bytes());
b.extend_from_slice(&shared_off.to_be_bytes());
b.extend_from_slice(&(payload.len() as u32).to_be_bytes());
b.extend_from_slice(&9u16.to_be_bytes());
b.extend_from_slice(&9u16.to_be_bytes());
b.extend_from_slice(&shared_off.to_be_bytes());
b.extend_from_slice(&(payload.len() as u32).to_be_bytes());
b.extend_from_slice(payload);
let table = SvgTable::parse(&b).expect("parse");
assert_eq!(table.document_for_glyph(5).unwrap().data, payload);
assert_eq!(table.document_for_glyph(9).unwrap().data, payload);
assert!(table.document_for_glyph(7).is_none());
}
#[test]
fn rejects_short_header() {
let b = vec![0u8; SVG_HEADER_LEN - 1];
assert!(matches!(SvgTable::parse(&b), Err(Error::UnexpectedEof)));
}
#[test]
fn rejects_nonzero_version() {
let mut b = build(&[(1, 1, b"<svg/>")]);
b[0..2].copy_from_slice(&1u16.to_be_bytes());
assert!(matches!(SvgTable::parse(&b), Err(Error::BadStructure(_))));
}
#[test]
fn rejects_zero_document_list_offset() {
let mut b = build(&[(1, 1, b"<svg/>")]);
b[2..6].copy_from_slice(&0u32.to_be_bytes());
assert!(matches!(SvgTable::parse(&b), Err(Error::BadStructure(_))));
}
#[test]
fn rejects_zero_num_entries() {
let mut b = Vec::new();
b.extend_from_slice(&SVG_VERSION_0.to_be_bytes());
b.extend_from_slice(&(SVG_HEADER_LEN as u32).to_be_bytes());
b.extend_from_slice(&0u32.to_be_bytes());
b.extend_from_slice(&0u16.to_be_bytes()); assert!(matches!(SvgTable::parse(&b), Err(Error::BadStructure(_))));
}
#[test]
fn rejects_start_greater_than_end() {
let mut b = build(&[(5, 5, b"<svg/>")]);
let rec_off = SVG_HEADER_LEN + 2;
b[rec_off..rec_off + 2].copy_from_slice(&6u16.to_be_bytes());
assert!(matches!(SvgTable::parse(&b), Err(Error::BadStructure(_))));
}
#[test]
fn rejects_records_out_of_order() {
let bytes = build(&[(1, 5, b"<svg>A</svg>"), (3, 8, b"<svg>B</svg>")]);
assert!(matches!(
SvgTable::parse(&bytes),
Err(Error::BadStructure(_))
));
}
#[test]
fn rejects_adjacent_touching_ranges() {
let bytes = build(&[(1, 5, b"<svg>A</svg>"), (5, 8, b"<svg>B</svg>")]);
assert!(matches!(
SvgTable::parse(&bytes),
Err(Error::BadStructure(_))
));
}
#[test]
fn rejects_zero_svg_doc_offset() {
let mut b = build(&[(1, 1, b"<svg/>")]);
let rec_off = SVG_HEADER_LEN + 2;
b[rec_off + 4..rec_off + 8].copy_from_slice(&0u32.to_be_bytes());
assert!(matches!(SvgTable::parse(&b), Err(Error::BadStructure(_))));
}
#[test]
fn rejects_zero_svg_doc_length() {
let mut b = build(&[(1, 1, b"<svg/>")]);
let rec_off = SVG_HEADER_LEN + 2;
b[rec_off + 8..rec_off + 12].copy_from_slice(&0u32.to_be_bytes());
assert!(matches!(SvgTable::parse(&b), Err(Error::BadStructure(_))));
}
#[test]
fn rejects_document_past_end_of_table() {
let mut b = build(&[(1, 1, b"<svg/>")]);
let rec_off = SVG_HEADER_LEN + 2;
let bogus = (b.len() as u32) + 100;
b[rec_off + 4..rec_off + 8].copy_from_slice(&bogus.to_be_bytes());
assert!(matches!(SvgTable::parse(&b), Err(Error::BadStructure(_))));
}
#[test]
fn rejects_truncated_records_array() {
let mut b = build(&[(1, 1, b"<svg/>")]);
let list_off = SVG_HEADER_LEN;
b[list_off..list_off + 2].copy_from_slice(&4u16.to_be_bytes());
assert!(matches!(SvgTable::parse(&b), Err(Error::UnexpectedEof)));
}
#[test]
fn document_for_glyph_below_first_range_is_none() {
let bytes = build(&[(100, 110, b"<svg/>")]);
let table = SvgTable::parse(&bytes).expect("parse");
assert!(table.document_for_glyph(0).is_none());
assert!(table.document_for_glyph(99).is_none());
assert!(table.document_for_glyph(100).is_some());
}
}