#![allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
use core::ffi::c_void;
use std::ffi::CStr;
use std::fs;
use std::os::raw::{c_char, c_int};
use std::path::Path;
use std::ptr;
use once_cell::sync::Lazy;
use parking_lot::RwLock;
use crate::abi::allocator::{xmlFree, xmlMalloc};
use crate::abi::structs::_xmlDoc;
use crate::abi::types::xmlChar;
use crate::xml::string::{
bytes_to_xmlstr, c_strdup, xml_str_starts_with, xml_strcat, xml_strcmp, xml_strdup, xml_strlen,
xmlstr_to_bytes,
};
pub(crate) const XML_CATA_ALLOW_NONE: i32 = 0;
pub(crate) const XML_CATA_ALLOW_GLOBAL: i32 = 1;
pub(crate) const XML_CATA_ALLOW_DOCUMENT: i32 = 2;
pub(crate) const XML_CATA_ALLOW_ALL: i32 = 3;
const DEFAULT_CATALOG: &str = "/etc/xml/catalog";
const XML_CATALOG_FILES_ENV: &str = "XML_CATALOG_FILES";
const SGML_CATALOG_FILES_ENV: &str = "SGML_CATALOG_FILES";
const MAX_CATALOG_FILE_SIZE: usize = 10_485_760;
#[derive(Clone, Debug)]
enum CatalogEntry {
Public { public_id: Vec<u8>, uri: Vec<u8> },
System { system_id: Vec<u8>, uri: Vec<u8> },
RewriteSystem { prefix: Vec<u8>, rewrite: Vec<u8> },
RewriteURI { prefix: Vec<u8>, rewrite: Vec<u8> },
DelegatePublic { prefix: Vec<u8>, catalog: Vec<u8> },
DelegateSystem { prefix: Vec<u8>, catalog: Vec<u8> },
DelegateURI { prefix: Vec<u8>, catalog: Vec<u8> },
NextCatalog { catalog: Vec<u8> },
}
#[derive(Clone, Copy, Debug, PartialEq)]
enum CatalogFormat {
Xml,
Sgml,
}
#[derive(Clone, Debug)]
struct CatalogInfo {
path: Vec<u8>,
format: CatalogFormat,
}
struct CatalogState {
entries: Vec<CatalogEntry>,
catalogs: Vec<CatalogInfo>,
initialized: bool,
allow: i32,
}
impl CatalogState {
fn new() -> Self {
Self {
entries: Vec::new(),
catalogs: Vec::new(),
initialized: false,
allow: XML_CATA_ALLOW_ALL,
}
}
fn clear(&mut self) {
self.entries.clear();
self.catalogs.clear();
self.allow = XML_CATA_ALLOW_ALL;
}
}
static CATALOG_STATE: Lazy<RwLock<CatalogState>> = Lazy::new(|| RwLock::new(CatalogState::new()));
fn trim_whitespace(bytes: &[u8]) -> &[u8] {
let start = bytes
.iter()
.position(|b| !b.is_ascii_whitespace())
.unwrap_or(bytes.len());
let end = bytes
.iter()
.rposition(|b| !b.is_ascii_whitespace())
.map_or(0, |p| p + 1);
&bytes[start..end]
}
fn starts_with(data: &[u8], prefix: &[u8]) -> bool {
if data.len() < prefix.len() {
return false;
}
data[..prefix.len()] == prefix[..]
}
fn starts_with_ignore_ascii_case(data: &[u8], prefix: &[u8]) -> bool {
if data.len() < prefix.len() {
return false;
}
data[..prefix.len()]
.iter()
.zip(prefix.iter())
.all(|(a, b)| a.eq_ignore_ascii_case(b))
}
fn extract_attr_value<'a>(data: &'a [u8], name: &[u8], pos: usize) -> Option<(&'a [u8], usize)> {
let remaining = &data[pos..];
let name_pos = find_subsequence(remaining, name)?;
let after_name = name_pos + name.len();
let after_name_slice = &remaining[after_name..];
let eq_pos = after_name_slice.iter().position(|b| *b == b'=')?;
let rel_quote_start = after_name_slice[eq_pos + 1..]
.iter()
.position(|b| *b == b'"' || *b == b'\'')
.map(|p| after_name + eq_pos + 1 + p)?;
let abs_quote_start = pos + rel_quote_start;
let quote_char = data[abs_quote_start];
let value_start = abs_quote_start + 1;
let value_end = data[value_start..]
.iter()
.position(|b| *b == quote_char)
.map(|p| value_start + p)?;
Some((&data[value_start..value_end], value_end + 1))
}
fn find_subsequence(data: &[u8], seq: &[u8]) -> Option<usize> {
if seq.is_empty() {
return Some(0);
}
data.windows(seq.len()).position(|w| w == seq)
}
fn extract_token(line: &[u8], pos: usize) -> Option<(&[u8], usize)> {
let line = &line[pos..];
let start = line.iter().position(|b| !b.is_ascii_whitespace())?;
let end = line[start..]
.iter()
.position(|b| b.is_ascii_whitespace())
.map(|p| start + p)
.unwrap_or(line.len());
Some((&line[start..end], pos + end))
}
fn extract_quoted_token(line: &[u8], pos: usize) -> Option<(&[u8], usize)> {
let line = &line[pos..];
let start = line.iter().position(|b| !b.is_ascii_whitespace())?;
if start >= line.len() {
return None;
}
let quote_char = line[start];
if quote_char != b'"' && quote_char != b'\'' {
return extract_token(line, 0);
}
let value_start = start + 1;
let end = line[value_start..]
.iter()
.position(|b| *b == quote_char)
.map(|p| value_start + p)?;
Some((&line[value_start..end], pos + end + 1))
}
fn parse_sgml_line(line: &[u8], entries: &mut Vec<CatalogEntry>) {
let trimmed = trim_whitespace(line);
if trimmed.is_empty() || trimmed.starts_with(b"--") {
return;
}
let Some((directive, after_directive)) = extract_token(trimmed, 0) else {
return;
};
match directive {
b"PUBLIC" | b"public" => {
let Some((pub_id, after_pub)) = extract_quoted_token(trimmed, after_directive) else {
return;
};
let Some((uri, _)) = extract_quoted_token(trimmed, after_pub) else {
return;
};
entries.push(CatalogEntry::Public {
public_id: pub_id.to_vec(),
uri: uri.to_vec(),
});
}
b"SYSTEM" | b"system" => {
let Some((sys_id, after_sys)) = extract_quoted_token(trimmed, after_directive) else {
return;
};
let Some((uri, _)) = extract_quoted_token(trimmed, after_sys) else {
return;
};
entries.push(CatalogEntry::System {
system_id: sys_id.to_vec(),
uri: uri.to_vec(),
});
}
b"URI" | b"uri" => {
let Some((uri_id, after_uri)) = extract_quoted_token(trimmed, after_directive) else {
return;
};
let Some((replacement, _)) = extract_quoted_token(trimmed, after_uri) else {
return;
};
entries.push(CatalogEntry::System {
system_id: uri_id.to_vec(),
uri: replacement.to_vec(),
});
}
b"CATALOG" | b"catalog" => {
let Some((path, _)) = extract_quoted_token(trimmed, after_directive) else {
return;
};
entries.push(CatalogEntry::NextCatalog {
catalog: path.to_vec(),
});
}
_ => {
}
}
}
fn parse_sgml_catalog(data: &[u8], entries: &mut Vec<CatalogEntry>) {
for line in data.split(|b| *b == b'\n') {
parse_sgml_line(line, entries);
}
}
fn parse_xml_catalog(data: &[u8], entries: &mut Vec<CatalogEntry>) {
let mut pos = 0;
let len = data.len();
while pos < len {
let Some(lt_pos) = data[pos..].iter().position(|b| *b == b'<') else {
break;
};
let tag_start = pos + lt_pos;
if tag_start + 1 >= len {
break;
}
let is_closing = data[tag_start + 1] == b'/';
if is_closing {
let Some(gt_pos) = data[tag_start..].iter().position(|b| *b == b'>') else {
break;
};
pos = tag_start + gt_pos + 1;
continue;
}
if data[tag_start + 1] == b'!' || data[tag_start + 1] == b'?' {
let Some(gt_pos) = data[tag_start..].iter().position(|b| *b == b'>') else {
break;
};
pos = tag_start + gt_pos + 1;
continue;
}
let tag_name_start = tag_start + 1;
let tag_name_end = data[tag_name_start..]
.iter()
.position(|b| b.is_ascii_whitespace() || *b == b'>' || *b == b'/')
.map(|p| tag_name_start + p)
.unwrap_or(len);
let tag_name = &data[tag_name_start..tag_name_end];
let Some(gt_or_slash_pos) = data[tag_start..]
.iter()
.position(|b| *b == b'>')
.map(|p| tag_start + p)
else {
break;
};
let is_self_closing = gt_or_slash_pos > 0 && data[gt_or_slash_pos - 1] == b'/';
let tag_content_end = if is_self_closing {
gt_or_slash_pos + 1
} else {
let close_tag = {
let mut close = Vec::with_capacity(tag_name.len() + 3);
close.push(b'<');
close.push(b'/');
close.extend_from_slice(tag_name);
close.push(b'>');
close
};
let close_pos = data[gt_or_slash_pos + 1..]
.windows(close_tag.len())
.position(|w| w == close_tag.as_slice())
.map(|p| gt_or_slash_pos + 1 + p + close_tag.len());
match close_pos {
Some(p) => p,
None => {
pos = gt_or_slash_pos + 1;
continue;
}
}
};
let tag_body_start = gt_or_slash_pos + 1;
let tag_body = &data[tag_body_start
..tag_content_end
- if is_self_closing {
0
} else {
tag_name.len() + 3
}];
let tag_body = trim_whitespace(tag_body);
match tag_name {
b"public" => {
let Some((pub_id, _)) = extract_attr_value(data, b"publicId", tag_start) else {
pos = tag_content_end;
continue;
};
let Some((uri, _)) = extract_attr_value(data, b"uri", tag_start) else {
pos = tag_content_end;
continue;
};
entries.push(CatalogEntry::Public {
public_id: pub_id.to_vec(),
uri: uri.to_vec(),
});
}
b"system" => {
let Some((sys_id, _)) = extract_attr_value(data, b"systemId", tag_start) else {
pos = tag_content_end;
continue;
};
let Some((uri, _)) = extract_attr_value(data, b"uri", tag_start) else {
pos = tag_content_end;
continue;
};
entries.push(CatalogEntry::System {
system_id: sys_id.to_vec(),
uri: uri.to_vec(),
});
}
b"rewriteSystem" => {
let Some((prefix, _)) = extract_attr_value(data, b"systemIdStartString", tag_start)
else {
pos = tag_content_end;
continue;
};
let Some((rewrite, _)) = extract_attr_value(data, b"rewritePrefix", tag_start)
else {
pos = tag_content_end;
continue;
};
entries.push(CatalogEntry::RewriteSystem {
prefix: prefix.to_vec(),
rewrite: rewrite.to_vec(),
});
}
b"rewriteURI" => {
let Some((prefix, _)) = extract_attr_value(data, b"uriStartString", tag_start)
else {
pos = tag_content_end;
continue;
};
let Some((rewrite, _)) = extract_attr_value(data, b"rewritePrefix", tag_start)
else {
pos = tag_content_end;
continue;
};
entries.push(CatalogEntry::RewriteURI {
prefix: prefix.to_vec(),
rewrite: rewrite.to_vec(),
});
}
b"delegatePublic" => {
let Some((prefix, _)) = extract_attr_value(data, b"publicIdStartString", tag_start)
else {
pos = tag_content_end;
continue;
};
let Some((catalog, _)) = extract_attr_value(data, b"catalog", tag_start) else {
pos = tag_content_end;
continue;
};
entries.push(CatalogEntry::DelegatePublic {
prefix: prefix.to_vec(),
catalog: catalog.to_vec(),
});
}
b"delegateSystem" => {
let Some((prefix, _)) = extract_attr_value(data, b"systemIdStartString", tag_start)
else {
pos = tag_content_end;
continue;
};
let Some((catalog, _)) = extract_attr_value(data, b"catalog", tag_start) else {
pos = tag_content_end;
continue;
};
entries.push(CatalogEntry::DelegateSystem {
prefix: prefix.to_vec(),
catalog: catalog.to_vec(),
});
}
b"delegateURI" => {
let Some((prefix, _)) = extract_attr_value(data, b"uriStartString", tag_start)
else {
pos = tag_content_end;
continue;
};
let Some((catalog, _)) = extract_attr_value(data, b"catalog", tag_start) else {
pos = tag_content_end;
continue;
};
entries.push(CatalogEntry::DelegateURI {
prefix: prefix.to_vec(),
catalog: catalog.to_vec(),
});
}
b"nextCatalog" => {
let Some((catalog, _)) = extract_attr_value(data, b"catalog", tag_start) else {
pos = tag_content_end;
continue;
};
entries.push(CatalogEntry::NextCatalog {
catalog: catalog.to_vec(),
});
}
b"group" | b"catalog" => {
parse_xml_catalog(tag_body, entries);
}
_ => {
}
}
pos = tag_content_end;
}
}
fn read_file_bytes(path: &str) -> Option<Vec<u8>> {
let p = Path::new(path);
let metadata = fs::metadata(p).ok()?;
if metadata.len() > MAX_CATALOG_FILE_SIZE as u64 {
return None;
}
fs::read(p).ok()
}
fn detect_catalog_format(data: &[u8]) -> CatalogFormat {
let trimmed = trim_whitespace(data);
if trimmed.starts_with(b"<?xml") || trimmed.starts_with(b"<catalog") {
CatalogFormat::Xml
} else {
CatalogFormat::Sgml
}
}
fn load_catalog_data(path: &str, data: &[u8], entries: &mut Vec<CatalogEntry>) {
let format = detect_catalog_format(data);
match format {
CatalogFormat::Xml => {
parse_xml_catalog(data, entries);
}
CatalogFormat::Sgml => {
parse_sgml_catalog(data, entries);
}
}
}
fn load_single_catalog(path: &str, state: &mut CatalogState) {
let data = match read_file_bytes(path) {
Some(d) => d,
None => return,
};
let format = detect_catalog_format(&data);
state.catalogs.push(CatalogInfo {
path: path.as_bytes().to_vec(),
format,
});
load_catalog_data(path, &data, &mut state.entries);
}
fn load_catalog_list(catalogs: &str, state: &mut CatalogState) {
for catalog_path in catalogs.split(':') {
let trimmed = catalog_path.trim();
if !trimmed.is_empty() {
load_single_catalog(trimmed, state);
}
}
}
pub(crate) fn init() {
let mut state = CATALOG_STATE.write();
if state.initialized {
return;
}
state.allow = XML_CATA_ALLOW_ALL;
crate::xml::globals::set_catalog_defaults(XML_CATA_ALLOW_ALL);
if let Ok(catalogs) = std::env::var(XML_CATALOG_FILES_ENV) {
load_catalog_list(&catalogs, &mut state);
}
if let Ok(catalogs) = std::env::var(SGML_CATALOG_FILES_ENV) {
load_catalog_list(&catalogs, &mut state);
}
if Path::new(DEFAULT_CATALOG).exists() {
load_single_catalog(DEFAULT_CATALOG, &mut state);
}
state.initialized = true;
}
pub(crate) fn cleanup() {
let mut state = CATALOG_STATE.write();
state.clear();
state.initialized = false;
}
pub(crate) fn load_catalog(catalogs: *const c_char) -> *mut c_void {
if catalogs.is_null() {
return ptr::null_mut();
}
let catalogs_str = unsafe { CStr::from_ptr(catalogs) };
let catalogs_str = catalogs_str.to_str().unwrap_or("");
let mut state = CATALOG_STATE.write();
if !state.initialized {
drop(state);
init();
state = CATALOG_STATE.write();
}
let count_before = state.catalogs.len();
load_catalog_list(catalogs_str, &mut state);
if state.catalogs.len() > count_before {
(state.catalogs.len() as isize) as *mut c_void
} else {
ptr::null_mut()
}
}
fn catalog_allowed(state: &CatalogState) -> bool {
let allow = state.allow;
match allow {
XML_CATA_ALLOW_NONE => false,
XML_CATA_ALLOW_GLOBAL | XML_CATA_ALLOW_DOCUMENT | XML_CATA_ALLOW_ALL => true,
_ => false,
}
}
pub(crate) unsafe fn resolve_public(pub_id: *const xmlChar) -> *mut xmlChar {
if pub_id.is_null() {
return ptr::null_mut();
}
let state = CATALOG_STATE.read();
if !catalog_allowed(&state) {
return ptr::null_mut();
}
let pub_id_bytes = xmlstr_to_bytes(pub_id);
for entry in &state.entries {
if let CatalogEntry::Public { public_id, uri } = entry {
if public_id.as_slice() == pub_id_bytes {
return bytes_to_xmlstr(uri);
}
}
}
let mut best_match: Option<Vec<u8>> = None;
let mut best_prefix_len: usize = 0;
for entry in &state.entries {
if let CatalogEntry::DelegatePublic { prefix, catalog } = entry {
if pub_id_bytes.starts_with(prefix) && prefix.len() > best_prefix_len {
best_prefix_len = prefix.len();
if let Some(delegated_data) = read_file_bytes(&String::from_utf8_lossy(catalog)) {
let mut temp_entries = Vec::new();
parse_xml_catalog(&delegated_data, &mut temp_entries);
for temp_entry in &temp_entries {
if let CatalogEntry::Public { public_id: dp, uri } = temp_entry {
if dp.as_slice() == pub_id_bytes {
best_match = Some(uri.clone());
}
}
}
}
}
}
}
best_match
.as_ref()
.map_or(ptr::null_mut(), |uri| bytes_to_xmlstr(uri))
}
pub(crate) unsafe fn resolve_system(sys_id: *const xmlChar) -> *mut xmlChar {
if sys_id.is_null() {
return ptr::null_mut();
}
let state = CATALOG_STATE.read();
if !catalog_allowed(&state) {
return ptr::null_mut();
}
let sys_id_bytes = xmlstr_to_bytes(sys_id);
for entry in &state.entries {
if let CatalogEntry::System { system_id, uri } = entry {
if system_id.as_slice() == sys_id_bytes {
return bytes_to_xmlstr(uri);
}
}
}
let mut best_rewrite: Option<Vec<u8>> = None;
let mut best_prefix_len: usize = 0;
for entry in &state.entries {
if let CatalogEntry::RewriteSystem { prefix, rewrite } = entry {
if sys_id_bytes.starts_with(prefix) && prefix.len() > best_prefix_len {
best_prefix_len = prefix.len();
let suffix = &sys_id_bytes[prefix.len()..];
let mut result = rewrite.clone();
result.extend_from_slice(suffix);
best_rewrite = Some(result);
}
}
}
if let Some(rewritten) = best_rewrite {
return bytes_to_xmlstr(&rewritten);
}
for entry in &state.entries {
if let CatalogEntry::DelegateSystem { prefix, catalog } = entry {
if sys_id_bytes.starts_with(prefix) {
if let Some(delegated_data) = read_file_bytes(&String::from_utf8_lossy(catalog)) {
let mut temp_entries = Vec::new();
parse_xml_catalog(&delegated_data, &mut temp_entries);
for temp_entry in &temp_entries {
if let CatalogEntry::System { system_id, uri } = temp_entry {
if system_id.as_slice() == sys_id_bytes {
return bytes_to_xmlstr(uri);
}
}
}
}
}
}
}
ptr::null_mut()
}
pub(crate) unsafe fn resolve_uri(uri: *const xmlChar) -> *mut xmlChar {
if uri.is_null() {
return ptr::null_mut();
}
let state = CATALOG_STATE.read();
if !catalog_allowed(&state) {
return ptr::null_mut();
}
let uri_bytes = xmlstr_to_bytes(uri);
for entry in &state.entries {
if let CatalogEntry::System {
system_id,
uri: sys_uri,
} = entry
{
if system_id.as_slice() == uri_bytes {
return bytes_to_xmlstr(sys_uri);
}
}
}
let mut best_rewrite: Option<Vec<u8>> = None;
let mut best_prefix_len: usize = 0;
for entry in &state.entries {
if let CatalogEntry::RewriteURI { prefix, rewrite } = entry {
if uri_bytes.starts_with(prefix) && prefix.len() > best_prefix_len {
best_prefix_len = prefix.len();
let suffix = &uri_bytes[prefix.len()..];
let mut result = rewrite.clone();
result.extend_from_slice(suffix);
best_rewrite = Some(result);
}
}
}
if let Some(rewritten) = best_rewrite {
return bytes_to_xmlstr(&rewritten);
}
for entry in &state.entries {
if let CatalogEntry::DelegateURI { prefix, catalog } = entry {
if uri_bytes.starts_with(prefix) {
if let Some(delegated_data) = read_file_bytes(&String::from_utf8_lossy(catalog)) {
let mut temp_entries = Vec::new();
parse_xml_catalog(&delegated_data, &mut temp_entries);
for temp_entry in &temp_entries {
if let CatalogEntry::System {
system_id,
uri: sys_uri,
} = temp_entry
{
if system_id.as_slice() == uri_bytes {
return bytes_to_xmlstr(sys_uri);
}
}
}
}
}
}
}
ptr::null_mut()
}
pub(crate) fn set_defaults(allow: c_int) {
let mut state = CATALOG_STATE.write();
state.allow = allow;
crate::xml::globals::set_catalog_defaults(allow);
}
pub(crate) fn get_defaults() -> c_int {
let state = CATALOG_STATE.read();
state.allow
}
pub(crate) unsafe fn add(
type_: *const xmlChar,
orig: *const xmlChar,
replace: *const xmlChar,
) -> c_int {
if type_.is_null() || orig.is_null() || replace.is_null() {
return -1;
}
let type_bytes = xmlstr_to_bytes(type_);
let orig_bytes = xmlstr_to_bytes(orig);
let replace_bytes = xmlstr_to_bytes(replace);
let mut state = CATALOG_STATE.write();
match type_bytes {
b"public" => {
state.entries.push(CatalogEntry::Public {
public_id: orig_bytes.to_vec(),
uri: replace_bytes.to_vec(),
});
0
}
b"system" => {
state.entries.push(CatalogEntry::System {
system_id: orig_bytes.to_vec(),
uri: replace_bytes.to_vec(),
});
0
}
b"rewriteSystem" => {
state.entries.push(CatalogEntry::RewriteSystem {
prefix: orig_bytes.to_vec(),
rewrite: replace_bytes.to_vec(),
});
0
}
b"rewriteURI" => {
state.entries.push(CatalogEntry::RewriteURI {
prefix: orig_bytes.to_vec(),
rewrite: replace_bytes.to_vec(),
});
0
}
b"delegatePublic" => {
state.entries.push(CatalogEntry::DelegatePublic {
prefix: orig_bytes.to_vec(),
catalog: replace_bytes.to_vec(),
});
0
}
b"delegateSystem" => {
state.entries.push(CatalogEntry::DelegateSystem {
prefix: orig_bytes.to_vec(),
catalog: replace_bytes.to_vec(),
});
0
}
b"delegateURI" => {
state.entries.push(CatalogEntry::DelegateURI {
prefix: orig_bytes.to_vec(),
catalog: replace_bytes.to_vec(),
});
0
}
b"nextCatalog" => {
state.entries.push(CatalogEntry::NextCatalog {
catalog: orig_bytes.to_vec(),
});
0
}
_ => -1,
}
}
pub(crate) unsafe fn remove(value: *const xmlChar) -> c_int {
if value.is_null() {
return -1;
}
let value_bytes = xmlstr_to_bytes(value);
let mut state = CATALOG_STATE.write();
let before = state.entries.len();
state.entries.retain(|entry| match entry {
CatalogEntry::Public { public_id, .. } => public_id.as_slice() != value_bytes,
CatalogEntry::System { system_id, .. } => system_id.as_slice() != value_bytes,
CatalogEntry::RewriteSystem { prefix, .. } => prefix.as_slice() != value_bytes,
CatalogEntry::RewriteURI { prefix, .. } => prefix.as_slice() != value_bytes,
CatalogEntry::DelegatePublic { prefix, .. } => prefix.as_slice() != value_bytes,
CatalogEntry::DelegateSystem { prefix, .. } => prefix.as_slice() != value_bytes,
CatalogEntry::DelegateURI { prefix, .. } => prefix.as_slice() != value_bytes,
CatalogEntry::NextCatalog { catalog } => catalog.as_slice() != value_bytes,
});
(before - state.entries.len()) as c_int
}
pub(crate) unsafe fn convert() -> *mut _xmlDoc {
let state = CATALOG_STATE.read();
if state.entries.is_empty() {
return ptr::null_mut();
}
let doc = crate::xml::tree::new_doc(ptr::null_mut());
if doc.is_null() {
return ptr::null_mut();
}
let catalog_name = b"catalog\0" as *const u8 as *const xmlChar;
let root = crate::xml::tree::new_node(ptr::null_mut(), catalog_name);
if root.is_null() {
crate::xml::tree::free_doc(doc);
return ptr::null_mut();
}
let xmlns_name = b"xmlns\0" as *const u8 as *const xmlChar;
let ns_value = b"urn:oasis:names:tc:entity:xmlns:xml:catalog\0" as *const u8 as *const xmlChar;
crate::xml::tree::set_prop(root, xmlns_name, ns_value);
crate::xml::tree::doc_set_root_element(doc, root);
for entry in &state.entries {
let (elem_name, attr1_name, attr1_value, attr2_name, attr2_value) = match entry {
CatalogEntry::Public { public_id, uri } => {
let elem = b"public\0" as *const u8 as *mut xmlChar;
let attr1 = b"publicId\0" as *const u8 as *mut xmlChar;
let val1 = bytes_to_xmlstr(public_id);
let attr2 = b"uri\0" as *const u8 as *mut xmlChar;
let val2 = bytes_to_xmlstr(uri);
(elem, attr1, val1, attr2, val2)
}
CatalogEntry::System { system_id, uri } => {
let elem = b"system\0" as *const u8 as *mut xmlChar;
let attr1 = b"systemId\0" as *const u8 as *mut xmlChar;
let val1 = bytes_to_xmlstr(system_id);
let attr2 = b"uri\0" as *const u8 as *mut xmlChar;
let val2 = bytes_to_xmlstr(uri);
(elem, attr1, val1, attr2, val2)
}
CatalogEntry::RewriteSystem { prefix, rewrite } => {
let elem = b"rewriteSystem\0" as *const u8 as *mut xmlChar;
let attr1 = b"systemIdStartString\0" as *const u8 as *mut xmlChar;
let val1 = bytes_to_xmlstr(prefix);
let attr2 = b"rewritePrefix\0" as *const u8 as *mut xmlChar;
let val2 = bytes_to_xmlstr(rewrite);
(elem, attr1, val1, attr2, val2)
}
CatalogEntry::RewriteURI { prefix, rewrite } => {
let elem = b"rewriteURI\0" as *const u8 as *mut xmlChar;
let attr1 = b"uriStartString\0" as *const u8 as *mut xmlChar;
let val1 = bytes_to_xmlstr(prefix);
let attr2 = b"rewritePrefix\0" as *const u8 as *mut xmlChar;
let val2 = bytes_to_xmlstr(rewrite);
(elem, attr1, val1, attr2, val2)
}
CatalogEntry::DelegatePublic { prefix, catalog } => {
let elem = b"delegatePublic\0" as *const u8 as *mut xmlChar;
let attr1 = b"publicIdStartString\0" as *const u8 as *mut xmlChar;
let val1 = bytes_to_xmlstr(prefix);
let attr2 = b"catalog\0" as *const u8 as *mut xmlChar;
let val2 = bytes_to_xmlstr(catalog);
(elem, attr1, val1, attr2, val2)
}
CatalogEntry::DelegateSystem { prefix, catalog } => {
let elem = b"delegateSystem\0" as *const u8 as *mut xmlChar;
let attr1 = b"systemIdStartString\0" as *const u8 as *mut xmlChar;
let val1 = bytes_to_xmlstr(prefix);
let attr2 = b"catalog\0" as *const u8 as *mut xmlChar;
let val2 = bytes_to_xmlstr(catalog);
(elem, attr1, val1, attr2, val2)
}
CatalogEntry::DelegateURI { prefix, catalog } => {
let elem = b"delegateURI\0" as *const u8 as *mut xmlChar;
let attr1 = b"uriStartString\0" as *const u8 as *mut xmlChar;
let val1 = bytes_to_xmlstr(prefix);
let attr2 = b"catalog\0" as *const u8 as *mut xmlChar;
let val2 = bytes_to_xmlstr(catalog);
(elem, attr1, val1, attr2, val2)
}
CatalogEntry::NextCatalog { catalog } => {
let elem = b"nextCatalog\0" as *const u8 as *mut xmlChar;
let attr1 = b"catalog\0" as *const u8 as *mut xmlChar;
let val1 = bytes_to_xmlstr(catalog);
let attr2 = ptr::null_mut();
let val2 = ptr::null_mut();
(elem, attr1, val1, attr2, val2)
}
};
let child = crate::xml::tree::new_child(root, ptr::null_mut(), elem_name);
if child.is_null() {
if !attr1_value.is_null() {
xmlFree(attr1_value as *mut c_void);
}
if !attr2_value.is_null() {
xmlFree(attr2_value as *mut c_void);
}
continue;
}
crate::xml::tree::set_prop(child, attr1_name, attr1_value);
if !attr2_name.is_null() {
crate::xml::tree::set_prop(child, attr2_name, attr2_value);
}
if !attr1_value.is_null() {
xmlFree(attr1_value as *mut c_void);
}
if !attr2_value.is_null() {
xmlFree(attr2_value as *mut c_void);
}
}
doc
}
#[cfg(test)]
mod tests {
use super::*;
use crate::abi::allocator::xmlFree;
use crate::xml::string::xmlstr_to_bytes;
use std::ffi::CString;
use std::sync::Mutex;
static CATALOG_TEST_MUTEX: Mutex<()> = Mutex::new(());
unsafe fn to_xmlstr(s: &[u8]) -> *const xmlChar {
let ptr = bytes_to_xmlstr(s);
ptr as *const xmlChar
}
unsafe fn to_xmlstr_str(s: &str) -> *const xmlChar {
to_xmlstr(s.as_bytes())
}
unsafe fn free_xmlstr(ptr: *const xmlChar) {
if !ptr.is_null() {
xmlFree(ptr as *mut c_void);
}
}
fn setup() -> std::sync::MutexGuard<'static, ()> {
let guard = CATALOG_TEST_MUTEX.lock().unwrap();
cleanup();
init();
set_defaults(XML_CATA_ALLOW_ALL);
guard
}
fn teardown(_guard: std::sync::MutexGuard<'static, ()>) {
cleanup();
}
#[test]
fn test_resolve_public_basic() {
let _guard = setup();
unsafe {
let type_ = to_xmlstr_str("public");
let pub_id = to_xmlstr_str("-//OASIS//DTD DocBook XML V4.2//EN");
let uri = to_xmlstr_str("http://www.oasis-open.org/docbook/xml/4.2/docbookx.dtd");
assert_eq!(add(type_, pub_id, uri), 0);
let result = resolve_public(pub_id);
assert!(!result.is_null());
assert_eq!(
xmlstr_to_bytes(result),
b"http://www.oasis-open.org/docbook/xml/4.2/docbookx.dtd"
);
xmlFree(result as *mut c_void);
let unknown = to_xmlstr_str("-//Unknown//DTD Unknown//EN");
assert!(resolve_public(unknown).is_null());
free_xmlstr(unknown);
free_xmlstr(type_);
free_xmlstr(pub_id);
free_xmlstr(uri);
teardown(_guard);
}
}
#[test]
fn test_resolve_system_basic() {
let _guard = setup();
unsafe {
let type_ = to_xmlstr_str("system");
let sys_id = to_xmlstr_str("http://example.com/foo.dtd");
let uri = to_xmlstr_str("/local/foo.dtd");
assert_eq!(add(type_, sys_id, uri), 0);
let result = resolve_system(sys_id);
assert!(!result.is_null());
assert_eq!(xmlstr_to_bytes(result), b"/local/foo.dtd");
xmlFree(result as *mut c_void);
free_xmlstr(type_);
free_xmlstr(sys_id);
free_xmlstr(uri);
teardown(_guard);
}
}
#[test]
fn test_resolve_uri_basic() {
let _guard = setup();
unsafe {
let type_ = to_xmlstr_str("system");
let sys_id = to_xmlstr_str("http://example.com/resource.xml");
let uri = to_xmlstr_str("/local/resource.xml");
assert_eq!(add(type_, sys_id, uri), 0);
let result = resolve_uri(sys_id);
assert!(!result.is_null());
assert_eq!(xmlstr_to_bytes(result), b"/local/resource.xml");
xmlFree(result as *mut c_void);
free_xmlstr(type_);
free_xmlstr(sys_id);
free_xmlstr(uri);
teardown(_guard);
}
}
#[test]
fn test_rewrite_system() {
let _guard = setup();
unsafe {
let type_ = to_xmlstr_str("rewriteSystem");
let prefix = to_xmlstr_str("http://example.com/old/");
let rewrite = to_xmlstr_str("http://mirror.example.com/new/");
assert_eq!(add(type_, prefix, rewrite), 0);
let sys_id = to_xmlstr_str("http://example.com/old/path/file.xml");
let result = resolve_system(sys_id);
assert!(!result.is_null());
assert_eq!(
xmlstr_to_bytes(result),
b"http://mirror.example.com/new/path/file.xml"
);
xmlFree(result as *mut c_void);
free_xmlstr(type_);
free_xmlstr(prefix);
free_xmlstr(rewrite);
free_xmlstr(sys_id);
teardown(_guard);
}
}
#[test]
fn test_rewrite_uri() {
let _guard = setup();
unsafe {
let type_ = to_xmlstr_str("rewriteURI");
let prefix = to_xmlstr_str("http://example.com/old/");
let rewrite = to_xmlstr_str("http://mirror.example.com/new/");
assert_eq!(add(type_, prefix, rewrite), 0);
let uri = to_xmlstr_str("http://example.com/old/path/file.xml");
let result = resolve_uri(uri);
assert!(!result.is_null());
assert_eq!(
xmlstr_to_bytes(result),
b"http://mirror.example.com/new/path/file.xml"
);
xmlFree(result as *mut c_void);
free_xmlstr(type_);
free_xmlstr(prefix);
free_xmlstr(rewrite);
free_xmlstr(uri);
teardown(_guard);
}
}
#[test]
fn test_remove_entries() {
let _guard = setup();
unsafe {
let type_ = to_xmlstr_str("public");
let pub_id = to_xmlstr_str("-//TEST//PUBLIC//EN");
let uri = to_xmlstr_str("test.dtd");
assert_eq!(add(type_, pub_id, uri), 0);
assert!(!resolve_public(pub_id).is_null());
assert_eq!(remove(pub_id), 1);
assert!(resolve_public(pub_id).is_null());
free_xmlstr(type_);
free_xmlstr(pub_id);
free_xmlstr(uri);
teardown(_guard);
}
}
#[test]
fn test_catalog_defaults() {
let _guard = setup();
assert_eq!(get_defaults(), XML_CATA_ALLOW_ALL);
set_defaults(XML_CATA_ALLOW_NONE);
assert_eq!(get_defaults(), XML_CATA_ALLOW_NONE);
set_defaults(XML_CATA_ALLOW_GLOBAL);
assert_eq!(get_defaults(), XML_CATA_ALLOW_GLOBAL);
set_defaults(XML_CATA_ALLOW_ALL);
assert_eq!(get_defaults(), XML_CATA_ALLOW_ALL);
teardown(_guard);
}
#[test]
fn test_parse_xml_catalog_in_memory() {
let _guard = setup();
unsafe {
let catalog_xml = br#"<?xml version="1.0"?>
<!DOCTYPE catalog PUBLIC "-//OASIS//DTD Entity Resolution XML Catalog V1.0//EN" "http://www.oasis-open.org/committees/entity/release/1.0/catalog.dtd">
<catalog xmlns="urn:oasis:names:tc:entity:xmlns:xml:catalog">
<public publicId="-//OASIS//DTD DocBook XML V4.2//EN" uri="http://www.oasis-open.org/docbook/xml/4.2/docbookx.dtd"/>
<system systemId="http://example.com/foo.dtd" uri="/local/foo.dtd"/>
<rewriteSystem systemIdStartString="http://example.com/old/" rewritePrefix="http://mirror.example.com/new/"/>
<rewriteURI uriStartString="http://example.com/old/" rewritePrefix="http://mirror.example.com/new/"/>
</catalog>"#;
let mut entries = Vec::new();
parse_xml_catalog(catalog_xml, &mut entries);
assert_eq!(entries.len(), 4);
match &entries[0] {
CatalogEntry::Public { public_id, uri } => {
assert_eq!(public_id.as_slice(), b"-//OASIS//DTD DocBook XML V4.2//EN");
assert_eq!(
uri.as_slice(),
b"http://www.oasis-open.org/docbook/xml/4.2/docbookx.dtd"
);
}
_ => panic!("Expected Public entry"),
}
match &entries[1] {
CatalogEntry::System { system_id, uri } => {
assert_eq!(system_id.as_slice(), b"http://example.com/foo.dtd");
assert_eq!(uri.as_slice(), b"/local/foo.dtd");
}
_ => panic!("Expected System entry"),
}
match &entries[2] {
CatalogEntry::RewriteSystem { prefix, rewrite } => {
assert_eq!(prefix.as_slice(), b"http://example.com/old/");
assert_eq!(rewrite.as_slice(), b"http://mirror.example.com/new/");
}
_ => panic!("Expected RewriteSystem entry"),
}
match &entries[3] {
CatalogEntry::RewriteURI { prefix, rewrite } => {
assert_eq!(prefix.as_slice(), b"http://example.com/old/");
assert_eq!(rewrite.as_slice(), b"http://mirror.example.com/new/");
}
_ => panic!("Expected RewriteURI entry"),
}
teardown(_guard);
}
}
#[test]
fn test_parse_sgml_catalog() {
let _guard = setup();
unsafe {
let sgml_data = br#"-- SGML catalog
PUBLIC "-//OASIS//DTD DocBook XML V4.2//EN" "docbookx.dtd"
SYSTEM "http://example.com/foo.dtd" "/local/foo.dtd"
URI "http://example.com/resource" "/local/resource"
"#;
let mut entries = Vec::new();
parse_sgml_catalog(sgml_data, &mut entries);
assert_eq!(entries.len(), 3);
match &entries[0] {
CatalogEntry::Public { public_id, uri } => {
assert_eq!(public_id.as_slice(), b"-//OASIS//DTD DocBook XML V4.2//EN");
assert_eq!(uri.as_slice(), b"docbookx.dtd");
}
_ => panic!("Expected Public entry"),
}
match &entries[1] {
CatalogEntry::System { system_id, uri } => {
assert_eq!(system_id.as_slice(), b"http://example.com/foo.dtd");
assert_eq!(uri.as_slice(), b"/local/foo.dtd");
}
_ => panic!("Expected System entry"),
}
match &entries[2] {
CatalogEntry::System { system_id, uri } => {
assert_eq!(system_id.as_slice(), b"http://example.com/resource");
assert_eq!(uri.as_slice(), b"/local/resource");
}
_ => panic!("Expected System entry for URI"),
}
teardown(_guard);
}
}
#[test]
fn test_resolution_precedence() {
let _guard = setup();
unsafe {
let type_sys = to_xmlstr_str("system");
let sys_id = to_xmlstr_str("http://example.com/target.xml");
let uri_direct = to_xmlstr_str("/direct/uri.xml");
assert_eq!(add(type_sys, sys_id, uri_direct), 0);
let type_rw = to_xmlstr_str("rewriteSystem");
let prefix = to_xmlstr_str("http://example.com/");
let rewrite = to_xmlstr_str("/rewrite/");
assert_eq!(add(type_rw, prefix, rewrite), 0);
let result = resolve_system(sys_id);
assert!(!result.is_null());
assert_eq!(xmlstr_to_bytes(result), b"/direct/uri.xml");
xmlFree(result as *mut c_void);
free_xmlstr(type_sys);
free_xmlstr(sys_id);
free_xmlstr(uri_direct);
free_xmlstr(type_rw);
free_xmlstr(prefix);
free_xmlstr(rewrite);
teardown(_guard);
}
}
#[test]
fn test_convert_sgml_to_xml() {
let _guard = setup();
unsafe {
let type_ = to_xmlstr_str("public");
let pub_id = to_xmlstr_str("-//TEST//PUBLIC//EN");
let uri = to_xmlstr_str("test.dtd");
assert_eq!(add(type_, pub_id, uri), 0);
let doc = convert();
assert!(!doc.is_null());
let root = crate::xml::tree::doc_get_root_element(doc);
assert!(!root.is_null());
let root_name = crate::xml::string::xmlstr_to_bytes((*root).name);
assert_eq!(root_name, b"catalog");
let child = (*root).children;
assert!(!child.is_null());
let child_name = crate::xml::string::xmlstr_to_bytes((*child).name);
assert_eq!(child_name, b"public");
crate::xml::tree::free_doc(doc);
free_xmlstr(type_);
free_xmlstr(pub_id);
free_xmlstr(uri);
teardown(_guard);
}
}
#[test]
fn test_catalog_disallowed() {
let _guard = setup();
unsafe {
let type_ = to_xmlstr_str("system");
let sys_id = to_xmlstr_str("http://example.com/test.dtd");
let uri = to_xmlstr_str("/local/test.dtd");
add(type_, sys_id, uri);
set_defaults(XML_CATA_ALLOW_NONE);
assert!(resolve_system(sys_id).is_null());
assert!(resolve_public(sys_id).is_null());
assert!(resolve_uri(sys_id).is_null());
set_defaults(XML_CATA_ALLOW_ALL);
free_xmlstr(type_);
free_xmlstr(sys_id);
free_xmlstr(uri);
teardown(_guard);
}
}
#[test]
fn test_init_cleanup() {
let _guard = CATALOG_TEST_MUTEX.lock().unwrap();
cleanup();
assert_eq!(CATALOG_STATE.read().initialized, false);
init();
assert_eq!(CATALOG_STATE.read().initialized, true);
cleanup();
assert_eq!(CATALOG_STATE.read().initialized, false);
}
#[test]
fn test_parse_xml_catalog_group() {
let catalog_xml = br#"<?xml version="1.0"?>
<catalog xmlns="urn:oasis:names:tc:entity:xmlns:xml:catalog">
<group>
<public publicId="-//GROUP//PUBLIC//EN" uri="group.dtd"/>
<system systemId="http://group.example.com/" uri="/group/"/>
</group>
</catalog>"#;
let mut entries = Vec::new();
parse_xml_catalog(catalog_xml, &mut entries);
assert_eq!(entries.len(), 2);
match &entries[0] {
CatalogEntry::Public { public_id, .. } => {
assert_eq!(public_id.as_slice(), b"-//GROUP//PUBLIC//EN");
}
_ => panic!("Expected Public entry"),
}
match &entries[1] {
CatalogEntry::System { system_id, .. } => {
assert_eq!(system_id.as_slice(), b"http://group.example.com/");
}
_ => panic!("Expected System entry"),
}
}
#[test]
fn test_multiple_entries() {
let _guard = setup();
unsafe {
let t = to_xmlstr_str("public");
let id1 = to_xmlstr_str("-//A//PUBLIC//EN");
let uri1 = to_xmlstr_str("a.dtd");
let id2 = to_xmlstr_str("-//B//PUBLIC//EN");
let uri2 = to_xmlstr_str("b.dtd");
assert_eq!(add(t, id1, uri1), 0);
assert_eq!(add(t, id2, uri2), 0);
let r1 = resolve_public(id1);
assert!(!r1.is_null());
assert_eq!(xmlstr_to_bytes(r1), b"a.dtd");
xmlFree(r1 as *mut c_void);
let r2 = resolve_public(id2);
assert!(!r2.is_null());
assert_eq!(xmlstr_to_bytes(r2), b"b.dtd");
xmlFree(r2 as *mut c_void);
free_xmlstr(t);
free_xmlstr(id1);
free_xmlstr(uri1);
free_xmlstr(id2);
free_xmlstr(uri2);
teardown(_guard);
}
}
#[test]
fn test_longest_prefix_wins() {
let _guard = setup();
unsafe {
let t = to_xmlstr_str("rewriteSystem");
let p1 = to_xmlstr_str("http://example.com/");
let r1 = to_xmlstr_str("/general/");
let p2 = to_xmlstr_str("http://example.com/specific/");
let r2 = to_xmlstr_str("/specific/");
add(t, p1, r1);
add(t, p2, r2);
let sys_id = to_xmlstr_str("http://example.com/specific/file.xml");
let result = resolve_system(sys_id);
assert!(!result.is_null());
assert_eq!(xmlstr_to_bytes(result), b"/specific/file.xml");
xmlFree(result as *mut c_void);
free_xmlstr(t);
free_xmlstr(p1);
free_xmlstr(r1);
free_xmlstr(p2);
free_xmlstr(r2);
free_xmlstr(sys_id);
teardown(_guard);
}
}
}