use crate::{File, InvalidNZBError, Meta, Segment};
use chrono::DateTime;
use lazy_regex::regex;
use roxmltree::Document;
pub(crate) fn sanitize_xml(xml: &str) -> &str {
let xml_heading_re = regex!(r"^(?i)<\?xml\s+version.*\?>");
let xml_doctype_re = regex!(r"^(?i)<!DOCTYPE.*>");
let mut content = xml.trim();
if let Some(found) = xml_heading_re.find(content) {
content = &content[found.end()..];
content = content.trim_start();
}
if let Some(found) = xml_doctype_re.find(content) {
content = &content[found.end()..];
content = content.trim_start();
}
content
}
pub(crate) fn parse_metadata(nzb: &Document) -> Meta {
let mut title: Option<String> = None;
let mut passwords: Vec<String> = Vec::new();
let mut tags: Vec<String> = Vec::new();
let mut category: Option<String> = None;
for meta in nzb.descendants().filter(|n| n.has_tag_name("meta")) {
if let Some(typ) = meta.attribute("type").map(str::to_lowercase).as_deref() {
match typ {
"title" => {
title = title.or(meta.text().map(String::from));
}
"password" => {
if let Some(text) = meta.text().map(String::from) {
if !passwords.contains(&text) {
passwords.push(text);
}
}
}
"tag" => {
if let Some(text) = meta.text().map(String::from) {
if !tags.contains(&text) {
tags.push(text);
}
}
}
"category" => {
category = category.or(meta.text().map(String::from));
}
_ => {}
}
}
}
Meta {
title,
passwords,
tags,
category,
}
}
pub(crate) fn parse_files(nzb: &Document) -> Result<Vec<File>, InvalidNZBError> {
let mut files = Vec::new();
let file_nodes = nzb.descendants().filter(|n| n.has_tag_name("file"));
for node in file_nodes {
let poster = node.attribute("poster").unwrap().to_string();
let date = node.attribute("date").unwrap().parse::<i64>().unwrap();
let datetime = DateTime::from_timestamp(date, 0).unwrap();
let subject = node.attribute("subject").unwrap().to_string();
let mut groups = Vec::new();
let mut segments = Vec::new();
if let Some(groups_node) = node.descendants().find(|n| n.has_tag_name("groups")) {
for group in groups_node.descendants().filter(|n| n.has_tag_name("group")) {
groups.push(group.text().unwrap().to_string());
}
}
if groups.is_empty() {
return Err(InvalidNZBError::new("Missing or malformed <groups>...</groups>!"));
}
if let Some(segment_node) = node.descendants().find(|n| n.has_tag_name("segments")) {
for segment in segment_node.descendants().filter(|n| n.has_tag_name("segment")) {
let size = match segment.attribute("bytes") {
Some(attr) => match attr.parse::<u32>() {
Ok(size) => size,
Err(_) => continue, },
None => continue, };
let number = match segment.attribute("number") {
Some(attr) => match attr.parse::<u32>() {
Ok(number) => number,
Err(_) => continue, },
None => continue, };
let message_id = match segment.text() {
Some(text) => text,
None => continue, };
segments.push(Segment::new(size, number, message_id));
}
}
if segments.is_empty() {
return Err(InvalidNZBError::new("Missing or malformed <segments>...</segments>!"));
}
files.push(File {
poster,
datetime,
subject,
groups,
segments,
});
}
if files.is_empty() {
return Err(InvalidNZBError::new("Missing or malformed <file>...</file>!"));
}
Ok(files)
}
pub(crate) fn sabnzbd_is_obfuscated(filestem: &str) -> bool {
if regex!(r"^[a-f0-9]{32}$").is_match(filestem) {
return true;
}
if regex!(r"^[a-f0-9.]{40,}$").is_match(filestem) {
return true;
}
if regex!(r"[a-f0-9]{30}").is_match(filestem)
&& regex!(r"\[\w+\]").find_iter(filestem).collect::<Vec<_>>().len() >= 2
{
return true;
}
if regex!(r"^abc\.xyz").is_match(filestem) {
return true;
}
let decimals = filestem.chars().filter(|c| c.is_numeric()).count();
let upperchars = filestem.chars().filter(|c| c.is_uppercase()).count();
let lowerchars = filestem.chars().filter(|c| c.is_lowercase()).count();
let spacesdots = filestem.chars().filter(|&c| c == ' ' || c == '.' || c == '_').count();
if upperchars >= 2 && lowerchars >= 2 && spacesdots >= 1 {
return false;
}
if spacesdots >= 3 {
return false;
}
if (upperchars + lowerchars >= 4) && decimals >= 4 && spacesdots >= 1 {
return false;
}
if filestem.chars().next().is_some_and(|c| c.is_uppercase())
&& lowerchars > 2
&& (upperchars as f64) / (lowerchars as f64) <= 0.25
{
return false;
}
true
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::Path;
fn get_stem(p: &str) -> &str {
Path::new(p).file_stem().map(|f| f.to_str()).flatten().unwrap()
}
#[test]
fn test_sabnzbd_is_obfuscated() {
assert!(sabnzbd_is_obfuscated(get_stem("599c1c9e2bdfb5114044bf25152b7eaa.mkv")));
assert!(sabnzbd_is_obfuscated(get_stem(
"/my/blabla/directory/stuff/599c1c9e2bdfb5114044bf25152b7eaa.mkv"
)));
assert!(sabnzbd_is_obfuscated(get_stem(
"/my/blabla/directory/A Directory Should Not Count 2020/599c1c9e2bdfb5114044bf25152b7eaa.mkv"
)));
assert!(sabnzbd_is_obfuscated(get_stem("/my/blabla/directory/stuff/afgm.avi")));
assert!(sabnzbd_is_obfuscated(get_stem(
"/my/blabla/directory/stuff/afgm2020.avi"
)));
assert!(sabnzbd_is_obfuscated(get_stem("MUGNjK3zi65TtN.mkv")));
assert!(sabnzbd_is_obfuscated(get_stem("T306077.avi")));
assert!(sabnzbd_is_obfuscated(get_stem("bar10nmbkkjjdfr.mkv")));
assert!(sabnzbd_is_obfuscated(get_stem("4rFF-fdtd480p.bin")));
assert!(sabnzbd_is_obfuscated(get_stem(
"e0nFmxBNTprpbQiVQ44WeEwSrBkLlJ7IgaSj3uzFu455FVYG3q.bin"
)));
assert!(sabnzbd_is_obfuscated(get_stem(
"e0nFmxBNTprpbQiVQ44WeEwSrBkLlJ7IgaSj3uzFu455FVYG3q"
))); assert!(sabnzbd_is_obfuscated(get_stem("greatdistro.iso")));
assert!(sabnzbd_is_obfuscated(get_stem("my.download.2020")));
assert!(sabnzbd_is_obfuscated(get_stem("abc.xyz.a4c567edbcbf27.BLA")));
assert!(sabnzbd_is_obfuscated(get_stem("abc.xyz.iso")));
assert!(sabnzbd_is_obfuscated(get_stem(
"0675e29e9abfd2.f7d069dab0b853283cc1b069a25f82.6547"
)));
assert!(sabnzbd_is_obfuscated(get_stem(
"[BlaBla] something [More] something b2.bef89a622e4a23f07b0d3757ad5e8a.a0 [Brrr]"
)));
assert!(!sabnzbd_is_obfuscated(get_stem(
"/my/blabla/directory/stuff/My Favorite Distro S03E04.iso"
)));
assert!(!sabnzbd_is_obfuscated(get_stem(
"/my/blabla/directory/stuff/Great Distro (2020).iso"
)));
assert!(!sabnzbd_is_obfuscated(get_stem("ubuntu.2004.iso")));
assert!(!sabnzbd_is_obfuscated(get_stem(
"/my/blabla/directory/stuff/GreatDistro2020.iso"
)));
assert!(!sabnzbd_is_obfuscated(get_stem("Catullus.avi")));
assert!(!sabnzbd_is_obfuscated(get_stem("Der.Mechaniker.HDRip.XviD-SG.avi")));
assert!(!sabnzbd_is_obfuscated(get_stem(
"Bonjour.1969.FRENCH.BRRiP.XviD.AC3-HuSh.avi"
)));
assert!(!sabnzbd_is_obfuscated(get_stem("Bonjour.1969.avi")));
assert!(!sabnzbd_is_obfuscated(get_stem("This That S01E11")));
assert!(!sabnzbd_is_obfuscated(get_stem("This_That_S01E11")));
assert!(!sabnzbd_is_obfuscated(get_stem("this_that_S01E11")));
assert!(!sabnzbd_is_obfuscated(get_stem("My.Download.2020")));
assert!(!sabnzbd_is_obfuscated(get_stem("this_that_there_here.avi")));
assert!(!sabnzbd_is_obfuscated(get_stem("Lorem Ipsum.avi")));
assert!(!sabnzbd_is_obfuscated(get_stem("Lorem Ipsum"))); }
}