use quick_xml::events::attributes::{AttrError, Attribute};
use quick_xml::events::{BytesStart, Event};
use quick_xml::name::QName;
use quick_xml::{Reader, Writer};
use std::io::Cursor;
use std::sync::atomic::{AtomicU64, Ordering};
static UNIQUE_ID: AtomicU64 = AtomicU64::new(0);
#[derive(thiserror::Error, Debug)]
#[allow(clippy::enum_variant_names)]
pub enum InkscapeExtPreprocessorError {
#[error("XML error: {0}")]
XmlError(#[from] quick_xml::Error),
#[error("XML attribute error: {0}")]
XmlAttrError(#[from] AttrError),
#[error("JSON encode error: {0}")]
JSONEncodeError(#[from] serde_json::Error),
#[error("UTF8 decode error: {0}")]
UTF8DecodeError(#[from] std::str::Utf8Error),
}
#[derive(Debug, Clone, Default, serde::Deserialize, serde::Serialize, PartialEq)]
pub(crate) struct GroupInfo {
pub(crate) id: Option<String>,
pub(crate) groupmode: Option<String>,
pub(crate) label: Option<String>,
}
impl GroupInfo {
fn id(&mut self, id: &[u8]) {
self.id = Some(String::from_utf8_lossy(id).to_string());
}
fn groupmode(&mut self, groupmode: &[u8]) {
self.groupmode = Some(String::from_utf8_lossy(groupmode).to_string());
}
fn label(&mut self, label: &[u8]) {
self.label = Some(String::from_utf8_lossy(label).to_string());
}
pub(crate) fn encode(self) -> Result<String, InkscapeExtPreprocessorError> {
let encoded = if self.groupmode.is_some() || self.label.is_some() || self.id.is_some() {
self.encode_impl(&base64::prelude::BASE64_URL_SAFE_NO_PAD)?
} else {
format!(
"__vsvg_missing__{}",
UNIQUE_ID.fetch_add(1, Ordering::Relaxed)
)
};
Ok(encoded)
}
fn encode_impl(
self,
engine: &impl base64::Engine,
) -> Result<String, InkscapeExtPreprocessorError> {
let json = serde_json::to_string(&self)?;
let mut encoded = String::from("__vsvg_encoded__");
engine.encode_string(&json, &mut encoded);
Ok(encoded)
}
pub(crate) fn decode(s: &str) -> Option<Self> {
if s.is_empty() {
return None;
}
let group_info = Self::decode_impl(s, &base64::prelude::BASE64_URL_SAFE_NO_PAD);
Some(group_info.unwrap_or_else(|| {
let id = if s.starts_with("__vsvg_missing__") {
None
} else {
Some(s.to_owned())
};
GroupInfo {
id,
groupmode: None,
label: None,
}
}))
}
fn decode_impl(s: &str, engine: &impl base64::Engine) -> Option<Self> {
let s = s.strip_prefix("__vsvg_encoded__")?;
let json = engine.decode(s.as_bytes()).ok()?;
let group_info = serde_json::from_slice(&json).ok()?;
Some(group_info)
}
}
pub(crate) fn preprocess_inkscape_layer(xml: &str) -> Result<String, InkscapeExtPreprocessorError> {
let mut reader = Reader::from_str(xml);
reader.trim_text(true);
let mut writer = Writer::new(Cursor::new(Vec::new()));
loop {
match reader.read_event() {
Ok(Event::Start(e)) if e.name().as_ref() == b"g" => {
let mut elem = BytesStart::new("g");
let mut group_info = GroupInfo::default();
for attr in e.attributes() {
match attr {
Ok(Attribute {
key: QName(b"id"),
value,
}) => group_info.id(&value),
Ok(Attribute {
key: QName(b"inkscape:groupmode"),
value,
}) => group_info.groupmode(&value),
Ok(Attribute {
key: QName(b"inkscape:label"),
value,
}) => group_info.label(&value),
Ok(attr) => elem.push_attribute(attr),
Err(e) => {
return Err(e.into());
}
}
}
elem.push_attribute((b"id".as_slice(), group_info.encode()?.as_bytes()));
writer.write_event(Event::Start(elem))?;
}
Ok(Event::Eof) => break,
Ok(e) => writer.write_event(e)?,
Err(e) => return Err(e.into()),
}
}
let result = writer.into_inner().into_inner();
let result_string = std::str::from_utf8(&result)?.to_owned();
Ok(result_string)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_group_info_id() {
let mut gi = GroupInfo::default();
gi.id("test".as_bytes());
let encoded = gi.clone().encode().unwrap();
assert!(encoded.starts_with("__vsvg_encoded__"));
let decoded = GroupInfo::decode(encoded.as_str());
assert_eq!(decoded, Some(gi));
}
#[test]
fn test_group_info_id_empty_str() {
let mut gi = GroupInfo::default();
gi.id("".as_bytes());
let encoded = gi.clone().encode().unwrap();
assert!(encoded.starts_with("__vsvg_encoded__"));
let decoded = GroupInfo::decode(encoded.as_str());
assert_eq!(decoded, Some(gi));
}
#[test]
fn test_group_info_group_mode() {
let mut gi = GroupInfo::default();
gi.groupmode("layer".as_bytes());
gi.label("my label".as_bytes());
let encoded = gi.clone().encode().unwrap();
assert!(encoded.starts_with("__vsvg_encoded__"));
let decoded = GroupInfo::decode(encoded.as_str());
assert_eq!(decoded, Some(gi));
}
#[test]
fn test_group_info_empty() {
let gi = GroupInfo::default();
let encoded = gi.clone().encode().unwrap();
assert!(encoded.starts_with("__vsvg_missing__"));
let decoded = GroupInfo::decode(encoded.as_str());
assert_eq!(decoded, Some(gi));
}
#[test]
fn test_group_info_empty_unique() {
let gi = GroupInfo::default();
let encoded = gi.clone().encode().unwrap();
let gi2 = GroupInfo::default();
let encoded2 = gi2.clone().encode().unwrap();
assert_ne!(encoded, encoded2);
}
#[test]
fn test_group_info_decode_from_empty() {
let gi = GroupInfo::decode("");
assert_eq!(gi, None);
}
#[test]
fn test_preprocess_inkscape_ext() {
UNIQUE_ID.store(0, Ordering::SeqCst);
let xml = r#"
<?xml version="1.0" encoding="utf-8" ?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape">
<g></g>
<g id=""></g>
<g inkscape:label="3"></g>
<g id="hello" inkscape:label="3" inkscape:groupmode="layer" stroke="white"></g>
</svg>
"#;
let mut preprocessed = preprocess_inkscape_layer(xml).unwrap();
let expected = r#"
<?xml version="1.0" encoding="utf-8" ?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape">
<g id="__vsvg_missing__0"></g>
<g id="__vsvg_encoded__eyJpZCI6IiIsImdyb3VwbW9kZSI6bnVsbCwibGFiZWwiOm51bGx9"></g>
<g id="__vsvg_encoded__eyJpZCI6bnVsbCwiZ3JvdXBtb2RlIjpudWxsLCJsYWJlbCI6IjMifQ"></g>
<g stroke="white" id="__vsvg_encoded__eyJpZCI6ImhlbGxvIiwiZ3JvdXBtb2RlIjoibGF5ZXIiLCJsYWJlbCI6IjMifQ"></g>
</svg>"#.to_string().replace("\n", "");
let idx = preprocessed.find("__vsvg_missing__").unwrap();
preprocessed.replace_range(idx..idx + 17, "__vsvg_missing__0");
assert_eq!(preprocessed, expected);
}
}