use crate::{Issue, ValidationContext};
const URL_TEXT_ELEMENTS: &[&str] = &[
"MediaFile",
"Impression",
"Error",
"ClickThrough",
"ClickTracking",
"CustomClick",
"IconClickThrough",
"IconClickTracking",
"IconViewTracking",
"NonLinearClickThrough",
"NonLinearClickTracking",
"CompanionClickThrough",
"CompanionClickTracking",
"Viewable",
"NotViewable",
"ViewUndetermined",
"VASTAdTagURI",
"Tracking",
];
#[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 mut xml = input.to_owned();
let mut applied: Vec<AppliedFix> = Vec::new();
let http_count = xml.matches("http://").count();
if http_count > 0 {
xml = xml.replace("http://", "https://");
let pre_doc = crate::parse::parse(input);
let mut had_mediafile_http = false;
let mut had_tracking_http = false;
check_http_elements(
&pre_doc.root,
&mut had_mediafile_http,
&mut had_tracking_http,
);
if had_mediafile_http {
applied.push(AppliedFix {
rule_id: "VAST-2.0-mediafile-https",
description: format!("Upgraded {} HTTP URL(s) to HTTPS", http_count),
path: "/VAST".to_owned(),
});
}
if had_tracking_http {
applied.push(AppliedFix {
rule_id: "VAST-2.0-tracking-https",
description: format!("Upgraded {} HTTP URL(s) to HTTPS", http_count),
path: "/VAST".to_owned(),
});
}
}
let without_cond = remove_conditional_ad_attr(&xml);
if without_cond != xml {
applied.push(AppliedFix {
rule_id: "VAST-4.0-conditionalad",
description: "Removed deprecated conditionalAd attribute from <Ad>".to_owned(),
path: "/VAST".to_owned(),
});
xml = without_cond;
}
let remaining = crate::validate_with_context(&xml, context).issues;
FixResult {
xml,
applied,
remaining,
}
}
fn check_http_elements(
node: &crate::parse::Node,
had_mediafile: &mut bool,
had_tracking: &mut bool,
) {
if node.text.starts_with("http://") {
if node.name == "MediaFile" {
*had_mediafile = true;
} else if URL_TEXT_ELEMENTS.contains(&node.name.as_str()) {
*had_tracking = true;
}
}
for child in &node.children {
check_http_elements(child, had_mediafile, had_tracking);
}
}
fn remove_conditional_ad_attr(input: &str) -> String {
const NEEDLE: &str = "conditionalAd=";
let mut out = String::with_capacity(input.len());
let mut rest = input;
while !rest.is_empty() {
if rest.starts_with(NEEDLE) {
while out.ends_with(' ') || out.ends_with('\t') {
out.pop();
}
rest = &rest[NEEDLE.len()..];
if let Some(quote_char) = rest.chars().next() {
if quote_char == '"' || quote_char == '\'' {
rest = &rest[quote_char.len_utf8()..]; let close = rest.find(quote_char).unwrap_or(rest.len());
rest = &rest[close..];
if rest.starts_with(quote_char) {
rest = &rest[quote_char.len_utf8()..];
}
}
}
} else {
let ch = rest.chars().next().unwrap();
out.push(ch);
rest = &rest[ch.len_utf8()..];
}
}
out
}
#[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);
}
}