#![allow(dead_code)]
#![allow(non_camel_case_types)]
use quick_xml::de::from_str as xml_from_str;
use quick_xml::se::to_string as xml_to_string;
use serde::{de::DeserializeOwned, Serialize};
pub mod security;
pub use security::{SecurityLimits, SecurityError, validate_xml};
pub mod rll;
#[path = "../generated/generated.rs"]
mod generated;
pub use generated::*;
pub fn from_str<T: DeserializeOwned>(xml: &str) -> Result<T, quick_xml::DeError> {
match strip_encrypted_text_nodes(xml) {
Some(processed) => xml_from_str(&processed),
None => xml_from_str(xml),
}
}
pub fn from_str_secure<T: DeserializeOwned>(
xml: &str,
limits: &SecurityLimits,
) -> Result<T, SecureParseError> {
validate_xml(xml.as_bytes(), limits)?;
match strip_encrypted_text_nodes(xml) {
Some(processed) => xml_from_str(&processed).map_err(SecureParseError::ParseError),
None => xml_from_str(xml).map_err(SecureParseError::ParseError),
}
}
#[derive(Debug, thiserror::Error)]
pub enum SecureParseError {
#[error("Security validation failed: {0}")]
SecurityError(#[from] SecurityError),
#[error("XML parsing failed: {0}")]
ParseError(quick_xml::DeError),
}
pub fn to_string<T: Serialize>(value: &T) -> Result<String, quick_xml::SeError> {
xml_to_string(value)
}
fn strip_encrypted_text_nodes(xml: &str) -> Option<String> {
let mut result = String::with_capacity(xml.len());
let mut modified = false;
let mut chunk_start = 0usize;
let bytes = xml.as_bytes();
let mut i = 0usize;
while i < bytes.len() {
if bytes[i] == b'>' {
let text_start = i + 1;
let mut j = text_start;
while j < bytes.len() && bytes[j] != b'<' {
j += 1;
}
if j > text_start {
let text_node = &xml[text_start..j];
let trimmed = text_node.trim();
if trimmed.len() > 100 && trimmed.bytes().all(is_base64_char) {
result.push_str(&xml[chunk_start..text_start]);
chunk_start = j;
modified = true;
}
}
i = j;
} else {
i += 1;
}
}
if modified {
result.push_str(&xml[chunk_start..]);
Some(result)
} else {
None
}
}
#[inline]
fn is_base64_char(b: u8) -> bool {
matches!(b, b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'+' | b'/' | b'=' | b'\n' | b'\r' | b' ' | b'\t')
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn text_node_between_elements() {
let xml = r#"<RSLogix5000Content SchemaRevision="1.0" SoftwareRevision="20.04">
some encrypted blob here
</RSLogix5000Content>"#;
from_str::<Project>(xml).unwrap();
}
}