use crate::{ExecutionError, Result};
use log::debug;
use quick_xml::events::{BytesStart, Event as XmlEvent};
use quick_xml::name::QName;
use quick_xml::Reader;
use std::io::BufRead;
use std::ops::Deref;
#[derive(Debug, PartialEq)]
enum TryTagArms {
Try,
Attempt,
Except,
}
#[derive(Debug)]
pub struct Include {
pub src: String,
pub alt: Option<String>,
pub continue_on_error: bool,
}
#[derive(Debug)]
pub enum Tag<'a> {
Include {
src: String,
alt: Option<String>,
continue_on_error: bool,
},
Try {
attempt_events: Vec<Event<'a>>,
except_events: Vec<Event<'a>>,
},
Assign {
name: String,
value: String,
},
Vars {
name: Option<String>,
},
When {
test: String,
match_name: Option<String>,
},
Choose {
when_branches: Vec<(Tag<'a>, Vec<Event<'a>>)>,
otherwise_events: Vec<Event<'a>>,
},
}
#[derive(Debug)]
#[allow(clippy::upper_case_acronyms)]
pub enum Event<'e> {
Content(XmlEvent<'e>),
InterpolatedContent(XmlEvent<'e>),
ESI(Tag<'e>),
}
struct TagNames {
include: Vec<u8>,
comment: Vec<u8>,
remove: Vec<u8>,
r#try: Vec<u8>,
attempt: Vec<u8>,
except: Vec<u8>,
assign: Vec<u8>,
vars: Vec<u8>,
choose: Vec<u8>,
when: Vec<u8>,
otherwise: Vec<u8>,
}
impl TagNames {
fn init(namespace: &str) -> Self {
Self {
include: format!("{namespace}:include",).into_bytes(),
comment: format!("{namespace}:comment",).into_bytes(),
remove: format!("{namespace}:remove",).into_bytes(),
r#try: format!("{namespace}:try",).into_bytes(),
attempt: format!("{namespace}:attempt",).into_bytes(),
except: format!("{namespace}:except",).into_bytes(),
assign: format!("{namespace}:assign",).into_bytes(),
vars: format!("{namespace}:vars",).into_bytes(),
choose: format!("{namespace}:choose",).into_bytes(),
when: format!("{namespace}:when",).into_bytes(),
otherwise: format!("{namespace}:otherwise",).into_bytes(),
}
}
}
#[derive(Debug, PartialEq)]
enum ContentType {
Normal,
Interpolated,
}
fn do_parse<'a, R>(
reader: &mut Reader<R>,
callback: &mut dyn FnMut(Event<'a>) -> Result<()>,
task: &mut Vec<Event<'a>>,
use_queue: bool,
try_depth: &mut usize,
choose_depth: &mut usize,
current_arm: &mut Option<TryTagArms>,
tag: &TagNames,
content_type: &ContentType,
) -> Result<()>
where
R: BufRead,
{
let mut is_remove_tag = false;
let mut open_include = false;
let mut open_assign = false;
let mut open_vars = false;
let attempt_events = &mut Vec::new();
let except_events = &mut Vec::new();
let when_branches = &mut Vec::new();
let otherwise_events = &mut Vec::new();
let mut buffer = Vec::new();
let mut in_try = false;
let mut in_choose = false;
loop {
match reader.read_event_into(&mut buffer) {
Ok(XmlEvent::Start(e)) if e.name() == QName(&tag.remove) => {
is_remove_tag = true;
}
Ok(XmlEvent::End(e)) if e.name() == QName(&tag.remove) => {
if !is_remove_tag {
return unexpected_closing_tag_error(&e);
}
is_remove_tag = false;
}
_ if is_remove_tag => continue,
Ok(XmlEvent::Empty(e)) if e.name().into_inner().starts_with(&tag.include) => {
include_tag_handler(&e, callback, task, use_queue)?;
}
Ok(XmlEvent::Start(e)) if e.name().into_inner().starts_with(&tag.include) => {
open_include = true;
include_tag_handler(&e, callback, task, use_queue)?;
}
Ok(XmlEvent::End(e)) if e.name().into_inner().starts_with(&tag.include) => {
if !open_include {
return unexpected_closing_tag_error(&e);
}
open_include = false;
}
_ if open_include => continue,
Ok(XmlEvent::Empty(e)) if e.name().into_inner().starts_with(&tag.comment) => continue,
Ok(XmlEvent::Start(ref e)) if e.name() == QName(&tag.r#try) => {
*current_arm = Some(TryTagArms::Try);
*try_depth += 1;
in_try = true;
continue;
}
Ok(XmlEvent::Start(ref e))
if e.name() == QName(&tag.attempt) || e.name() == QName(&tag.except) =>
{
if *current_arm != Some(TryTagArms::Try) {
return unexpected_opening_tag_error(e);
}
if e.name() == QName(&tag.attempt) {
*current_arm = Some(TryTagArms::Attempt);
do_parse(
reader,
callback,
attempt_events,
true,
try_depth,
choose_depth,
current_arm,
tag,
&ContentType::Interpolated,
)?;
} else if e.name() == QName(&tag.except) {
*current_arm = Some(TryTagArms::Except);
do_parse(
reader,
callback,
except_events,
true,
try_depth,
choose_depth,
current_arm,
tag,
&ContentType::Interpolated,
)?;
}
}
Ok(XmlEvent::End(ref e)) if e.name() == QName(&tag.r#try) => {
*current_arm = None;
in_try = false;
if *try_depth == 0 {
return unexpected_closing_tag_error(e);
}
try_end_handler(use_queue, task, attempt_events, except_events, callback)?;
*try_depth -= 1;
continue;
}
Ok(XmlEvent::End(ref e))
if e.name() == QName(&tag.attempt) || e.name() == QName(&tag.except) =>
{
*current_arm = Some(TryTagArms::Try);
if *try_depth == 0 {
return unexpected_closing_tag_error(e);
}
return Ok(());
}
Ok(XmlEvent::Empty(e)) if e.name().into_inner().starts_with(&tag.assign) => {
assign_tag_handler(&e, callback, task, use_queue)?;
}
Ok(XmlEvent::Start(e)) if e.name().into_inner().starts_with(&tag.assign) => {
open_assign = true;
assign_tag_handler(&e, callback, task, use_queue)?;
}
Ok(XmlEvent::End(e)) if e.name().into_inner().starts_with(&tag.assign) => {
if !open_assign {
return unexpected_closing_tag_error(&e);
}
open_assign = false;
}
Ok(XmlEvent::Empty(e)) if e.name().into_inner().starts_with(&tag.vars) => {
vars_tag_handler(&e, callback, task, use_queue)?;
}
Ok(XmlEvent::Start(e)) if e.name().into_inner().starts_with(&tag.vars) => {
open_vars = true;
vars_tag_handler(&e, callback, task, use_queue)?;
}
Ok(XmlEvent::End(e)) if e.name().into_inner().starts_with(&tag.vars) => {
if !open_vars {
return unexpected_closing_tag_error(&e);
}
open_vars = false;
}
Ok(XmlEvent::Start(ref e)) if e.name() == QName(&tag.choose) => {
in_choose = true;
*choose_depth += 1;
}
Ok(XmlEvent::End(ref e)) if e.name() == QName(&tag.choose) => {
in_choose = false;
*choose_depth -= 1;
choose_tag_handler(when_branches, otherwise_events, callback, task, use_queue)?;
}
Ok(XmlEvent::Start(ref e)) if e.name() == QName(&tag.when) => {
if *choose_depth == 0 {
return unexpected_opening_tag_error(e);
}
let when_tag = parse_when(e)?;
let mut when_events = Vec::new();
do_parse(
reader,
callback,
&mut when_events,
true,
try_depth,
choose_depth,
current_arm,
tag,
&ContentType::Interpolated,
)?;
when_branches.push((when_tag, when_events));
}
Ok(XmlEvent::End(e)) if e.name() == QName(&tag.when) => {
if *choose_depth == 0 {
return unexpected_closing_tag_error(&e);
}
return Ok(());
}
Ok(XmlEvent::Start(ref e)) if e.name() == QName(&tag.otherwise) => {
if *choose_depth == 0 {
return unexpected_opening_tag_error(e);
}
do_parse(
reader,
callback,
otherwise_events,
true,
try_depth,
choose_depth,
current_arm,
tag,
&ContentType::Interpolated,
)?;
}
Ok(XmlEvent::End(e)) if e.name() == QName(&tag.otherwise) => {
if *choose_depth == 0 {
return unexpected_closing_tag_error(&e);
}
return Ok(());
}
Ok(XmlEvent::Eof) => {
debug!("End of document");
break;
}
Ok(e) => {
if in_try || in_choose {
continue;
}
let event = if open_vars || content_type == &ContentType::Interpolated {
Event::InterpolatedContent(e.into_owned())
} else {
Event::Content(e.into_owned())
};
if use_queue {
task.push(event);
} else {
callback(event)?;
}
}
_ => {}
}
}
Ok(())
}
pub fn parse_tags<'a, R>(
namespace: &str,
reader: &mut Reader<R>,
callback: &mut dyn FnMut(Event<'a>) -> Result<()>,
) -> Result<()>
where
R: BufRead,
{
debug!("Parsing document...");
let tags = TagNames::init(namespace);
let mut try_depth = 0;
let mut choose_depth = 0;
let mut root = Vec::new();
let mut current_arm: Option<TryTagArms> = None;
do_parse(
reader,
callback,
&mut root,
false,
&mut try_depth,
&mut choose_depth,
&mut current_arm,
&tags,
&ContentType::Normal,
)?;
debug!("Root: {root:?}");
Ok(())
}
fn parse_include<'a>(elem: &BytesStart) -> Result<Tag<'a>> {
let src = match elem
.attributes()
.flatten()
.find(|attr| attr.key.into_inner() == b"src")
{
Some(attr) => String::from_utf8(attr.value.to_vec()).unwrap(),
None => {
return Err(ExecutionError::MissingRequiredParameter(
String::from_utf8(elem.name().into_inner().to_vec()).unwrap(),
"src".to_string(),
));
}
};
let alt = elem
.attributes()
.flatten()
.find(|attr| attr.key.into_inner() == b"alt")
.map(|attr| String::from_utf8(attr.value.to_vec()).unwrap());
let continue_on_error = elem
.attributes()
.flatten()
.find(|attr| attr.key.into_inner() == b"onerror")
.is_some_and(|attr| &attr.value.to_vec() == b"continue");
Ok(Tag::Include {
src,
alt,
continue_on_error,
})
}
fn parse_assign<'a>(elem: &BytesStart) -> Result<Tag<'a>> {
let name = match elem
.attributes()
.flatten()
.find(|attr| attr.key.into_inner() == b"name")
{
Some(attr) => String::from_utf8(attr.value.to_vec()).unwrap(),
None => {
return Err(ExecutionError::MissingRequiredParameter(
String::from_utf8(elem.name().into_inner().to_vec()).unwrap(),
"name".to_string(),
));
}
};
let value = match elem
.attributes()
.flatten()
.find(|attr| attr.key.into_inner() == b"value")
{
Some(attr) => String::from_utf8(attr.value.to_vec()).unwrap(),
None => {
return Err(ExecutionError::MissingRequiredParameter(
String::from_utf8(elem.name().into_inner().to_vec()).unwrap(),
"value".to_string(),
));
}
};
Ok(Tag::Assign { name, value })
}
fn parse_vars<'a>(elem: &BytesStart) -> Result<Tag<'a>> {
let name = elem
.attributes()
.flatten()
.find(|attr| attr.key.into_inner() == b"name")
.map(|attr| String::from_utf8(attr.value.to_vec()).unwrap());
Ok(Tag::Vars { name })
}
fn parse_when<'a>(elem: &BytesStart) -> Result<Tag<'a>> {
let test = match elem
.attributes()
.flatten()
.find(|attr| attr.key.into_inner() == b"test")
{
Some(attr) => String::from_utf8(attr.value.to_vec()).unwrap(),
None => {
return Err(ExecutionError::MissingRequiredParameter(
String::from_utf8(elem.name().into_inner().to_vec()).unwrap(),
"test".to_string(),
));
}
};
let match_name = elem
.attributes()
.flatten()
.find(|attr| attr.key.into_inner() == b"matchname")
.map(|attr| String::from_utf8(attr.value.to_vec()).unwrap());
Ok(Tag::When { test, match_name })
}
fn try_end_handler<'a>(
use_queue: bool,
task: &mut Vec<Event<'a>>,
attempt_events: &mut Vec<Event<'a>>,
except_events: &mut Vec<Event<'a>>,
callback: &mut dyn FnMut(Event<'a>) -> Result<()>,
) -> Result<()> {
if use_queue {
task.push(Event::ESI(Tag::Try {
attempt_events: std::mem::take(attempt_events),
except_events: std::mem::take(except_events),
}));
} else {
callback(Event::ESI(Tag::Try {
attempt_events: std::mem::take(attempt_events),
except_events: std::mem::take(except_events),
}))?;
}
Ok(())
}
fn include_tag_handler<'e>(
elem: &BytesStart,
callback: &mut dyn FnMut(Event<'e>) -> Result<()>,
task: &mut Vec<Event<'e>>,
use_queue: bool,
) -> Result<()> {
if use_queue {
task.push(Event::ESI(parse_include(elem)?));
} else {
callback(Event::ESI(parse_include(elem)?))?;
}
Ok(())
}
fn assign_tag_handler<'e>(
elem: &BytesStart,
callback: &mut dyn FnMut(Event<'e>) -> Result<()>,
task: &mut Vec<Event<'e>>,
use_queue: bool,
) -> Result<()> {
if use_queue {
task.push(Event::ESI(parse_assign(elem)?));
} else {
callback(Event::ESI(parse_assign(elem)?))?;
}
Ok(())
}
fn vars_tag_handler<'e>(
elem: &BytesStart,
callback: &mut dyn FnMut(Event<'e>) -> Result<()>,
task: &mut Vec<Event<'e>>,
use_queue: bool,
) -> Result<()> {
debug!("Handling <esi:vars> tag");
let tag = parse_vars(elem)?;
debug!("Parsed <esi:vars> tag: {tag:?}");
if use_queue {
task.push(Event::ESI(parse_vars(elem)?));
} else {
callback(Event::ESI(parse_vars(elem)?))?;
}
Ok(())
}
fn choose_tag_handler<'a>(
when_branches: &mut Vec<(Tag<'a>, Vec<Event<'a>>)>,
otherwise_events: &mut Vec<Event<'a>>,
callback: &mut dyn FnMut(Event<'a>) -> Result<()>,
task: &mut Vec<Event<'a>>,
use_queue: bool,
) -> Result<()> {
let choose_tag = Tag::Choose {
when_branches: std::mem::take(when_branches),
otherwise_events: std::mem::take(otherwise_events),
};
if use_queue {
task.push(Event::ESI(choose_tag));
} else {
callback(Event::ESI(choose_tag))?;
}
Ok(())
}
fn unexpected_closing_tag_error<T>(e: &T) -> Result<()>
where
T: Deref<Target = [u8]>,
{
Err(ExecutionError::UnexpectedClosingTag(
String::from_utf8_lossy(e).to_string(),
))
}
fn unexpected_opening_tag_error<T>(e: &T) -> Result<()>
where
T: Deref<Target = [u8]>,
{
Err(ExecutionError::UnexpectedOpeningTag(
String::from_utf8_lossy(e).to_string(),
))
}