use crate::parse::{Node, VastDocument};
use crate::{Issue, ValidationContext};
#[derive(Debug, Clone)]
pub struct AppliedFix {
pub rule_id: &'static str,
pub description: String,
pub path: String,
}
#[derive(Debug)]
pub struct FixResult {
pub xml: String,
pub applied: Vec<AppliedFix>,
pub remaining: Vec<Issue>,
}
pub fn fix(input: &str) -> FixResult {
fix_with_context(input, ValidationContext::default())
}
pub fn fix_with_context(input: &str, context: ValidationContext) -> FixResult {
let doc = crate::parse::parse(input);
let mut root = doc.root.clone();
let mut applied: Vec<AppliedFix> = Vec::new();
apply_https_fixes(&mut root, "/VAST", &mut applied);
apply_deprecated_attr_fixes(&mut root, "/VAST", &mut applied);
let xml = serialize_doc(&VastDocument {
root,
parse_error: doc.parse_error,
});
let remaining = crate::validate_with_context(&xml, context).issues;
FixResult {
xml,
applied,
remaining,
}
}
const URL_TEXT_ELEMENTS: &[&str] = &[
"MediaFile",
"Impression",
"Error",
"ClickThrough",
"ClickTracking",
"CustomClick",
"IconClickThrough",
"IconClickTracking",
"IconViewTracking",
"NonLinearClickThrough",
"NonLinearClickTracking",
"CompanionClickThrough",
"CompanionClickTracking",
"Viewable",
"NotViewable",
"ViewUndetermined",
"VASTAdTagURI",
"Tracking",
];
fn apply_https_fixes(node: &mut Node, path: &str, applied: &mut Vec<AppliedFix>) {
if URL_TEXT_ELEMENTS.contains(&node.name.as_str()) && node.text.starts_with("http://") {
let rule_id: &'static str = if node.name == "MediaFile" {
"VAST-2.0-mediafile-https"
} else {
"VAST-2.0-tracking-https"
};
node.text = format!("https://{}", &node.text["http://".len()..]);
applied.push(AppliedFix {
rule_id,
description: format!("Upgraded HTTP URL to HTTPS in <{}>", node.name),
path: path.to_owned(),
});
}
for i in 0..node.children.len() {
let child_path = format!("{}/{}[{}]", path, node.children[i].name, i);
apply_https_fixes(&mut node.children[i], &child_path, applied);
}
}
fn apply_deprecated_attr_fixes(node: &mut Node, path: &str, applied: &mut Vec<AppliedFix>) {
if node.name == "Ad" {
if let Some(pos) = node.attrs.iter().position(|a| a.name == "conditionalAd") {
node.attrs.remove(pos);
applied.push(AppliedFix {
rule_id: "VAST-4.0-conditionalad",
description: "Removed deprecated conditionalAd attribute from <Ad>".to_owned(),
path: path.to_owned(),
});
}
}
for i in 0..node.children.len() {
let child_path = format!("{}/{}[{}]", path, node.children[i].name, i);
apply_deprecated_attr_fixes(&mut node.children[i], &child_path, applied);
}
}
fn serialize_doc(doc: &VastDocument) -> String {
let mut out = String::with_capacity(4096);
out.push_str("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
serialize_node(&doc.root, &mut out, 0);
out
}
fn serialize_node(node: &Node, out: &mut String, depth: usize) {
let indent = " ".repeat(depth);
out.push_str(&indent);
out.push('<');
out.push_str(&node.name);
for attr in &node.attrs {
out.push(' ');
out.push_str(&attr.name);
out.push_str("=\"");
out.push_str(&escape_attr(&attr.value));
out.push('"');
}
if node.children.is_empty() && node.text.is_empty() {
out.push_str("/>\n");
return;
}
out.push('>');
if !node.children.is_empty() {
out.push('\n');
for child in &node.children {
serialize_node(child, out, depth + 1);
}
if !node.text.is_empty() {
out.push_str(&indent);
out.push_str(" ");
out.push_str(&escape_text(&node.text));
out.push('\n');
}
out.push_str(&indent);
} else {
out.push_str(&escape_text(&node.text));
}
out.push_str("</");
out.push_str(&node.name);
out.push_str(">\n");
}
#[inline]
fn escape_attr(s: &str) -> String {
s.replace('&', "&")
.replace('"', """)
.replace('<', "<")
.replace('>', ">")
}
#[inline]
fn escape_text(s: &str) -> String {
s.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
}
#[cfg(test)]
mod tests {
use super::*;
const HTTP_VAST: &str = r#"<VAST version="4.2">
<Ad id="1"><InLine>
<AdSystem>Demo</AdSystem>
<AdTitle>Test</AdTitle>
<AdServingId>sid-1</AdServingId>
<Impression>http://track.example.com/imp</Impression>
<Creatives>
<Creative>
<UniversalAdId idRegistry="ad-id.org">UID-1</UniversalAdId>
<Linear>
<Duration>00:00:30</Duration>
<MediaFiles>
<MediaFile delivery="progressive" type="video/mp4"
width="1920" height="1080">
http://cdn.example.com/ad.mp4
</MediaFile>
</MediaFiles>
</Linear>
</Creative>
</Creatives>
</InLine></Ad>
</VAST>"#;
#[test]
fn upgrades_mediafile_url_to_https() {
let result = fix(HTTP_VAST);
assert!(result.xml.contains("https://cdn.example.com/ad.mp4"));
assert!(!result.xml.contains("http://cdn.example.com/ad.mp4"));
assert!(result
.applied
.iter()
.any(|f| f.rule_id == "VAST-2.0-mediafile-https"));
}
#[test]
fn upgrades_impression_url_to_https() {
let result = fix(HTTP_VAST);
assert!(result.xml.contains("https://track.example.com/imp"));
assert!(result
.applied
.iter()
.any(|f| f.rule_id == "VAST-2.0-tracking-https"));
}
#[test]
fn https_urls_are_not_modified() {
let xml = HTTP_VAST.replace("http://cdn", "https://cdn");
let result = fix(&xml);
assert!(!result
.applied
.iter()
.any(|f| f.rule_id == "VAST-2.0-mediafile-https"));
assert!(result.xml.contains("https://cdn.example.com/ad.mp4"));
}
#[test]
fn removes_conditional_ad_attribute() {
let xml = r#"<VAST version="4.1">
<Ad id="1" conditionalAd="true"><InLine>
<AdSystem>Demo</AdSystem>
<AdTitle>Test</AdTitle>
<AdServingId>sid-1</AdServingId>
<Impression>https://t.example.com/imp</Impression>
<Creatives/>
</InLine></Ad>
</VAST>"#;
let result = fix(xml);
assert!(!result.xml.contains("conditionalAd"));
assert!(result
.applied
.iter()
.any(|f| f.rule_id == "VAST-4.0-conditionalad"));
}
#[test]
fn repaired_xml_is_well_formed() {
let result = fix(HTTP_VAST);
let doc = crate::parse::parse(&result.xml);
assert!(doc.parse_error.is_none(), "{:?}", doc.parse_error);
}
#[test]
fn no_applied_fixes_on_clean_document() {
let clean = HTTP_VAST
.replace("http://cdn", "https://cdn")
.replace("http://track", "https://track");
let result = fix(&clean);
assert!(result.applied.is_empty());
}
#[test]
fn fix_result_remaining_only_contains_unfixable_issues() {
let result = fix(HTTP_VAST);
let has_https_remaining = result
.remaining
.iter()
.any(|i| i.id == "VAST-2.0-mediafile-https" || i.id == "VAST-2.0-tracking-https");
assert!(!has_https_remaining);
}
}