1#![allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
21
22use core::ffi::c_void;
23use std::ffi::CStr;
24use std::fs;
25use std::os::raw::{c_char, c_int};
26use std::path::Path;
27use std::ptr;
28
29use once_cell::sync::Lazy;
30use parking_lot::RwLock;
31
32use crate::abi::allocator::{xmlFree, xmlMalloc};
33use crate::abi::structs::_xmlDoc;
34use crate::abi::types::xmlChar;
35use crate::xml::string::{
36 bytes_to_xmlstr, c_strdup, xml_str_starts_with, xml_strcat, xml_strcmp, xml_strdup, xml_strlen,
37 xmlstr_to_bytes,
38};
39
40pub(crate) const XML_CATA_ALLOW_NONE: i32 = 0;
46
47pub(crate) const XML_CATA_ALLOW_GLOBAL: i32 = 1;
49
50pub(crate) const XML_CATA_ALLOW_DOCUMENT: i32 = 2;
52
53pub(crate) const XML_CATA_ALLOW_ALL: i32 = 3;
55
56const DEFAULT_CATALOG: &str = "/etc/xml/catalog";
58
59const XML_CATALOG_FILES_ENV: &str = "XML_CATALOG_FILES";
61
62const SGML_CATALOG_FILES_ENV: &str = "SGML_CATALOG_FILES";
64
65const MAX_CATALOG_FILE_SIZE: usize = 10_485_760;
67
68#[derive(Clone, Debug)]
74enum CatalogEntry {
75 Public { public_id: Vec<u8>, uri: Vec<u8> },
77 System { system_id: Vec<u8>, uri: Vec<u8> },
79 RewriteSystem { prefix: Vec<u8>, rewrite: Vec<u8> },
81 RewriteURI { prefix: Vec<u8>, rewrite: Vec<u8> },
83 DelegatePublic { prefix: Vec<u8>, catalog: Vec<u8> },
85 DelegateSystem { prefix: Vec<u8>, catalog: Vec<u8> },
87 DelegateURI { prefix: Vec<u8>, catalog: Vec<u8> },
89 NextCatalog { catalog: Vec<u8> },
91}
92
93#[derive(Clone, Copy, Debug, PartialEq)]
95enum CatalogFormat {
96 Xml,
97 Sgml,
98}
99
100#[derive(Clone, Debug)]
102struct CatalogInfo {
103 path: Vec<u8>,
104 format: CatalogFormat,
105}
106
107struct CatalogState {
113 entries: Vec<CatalogEntry>,
115 catalogs: Vec<CatalogInfo>,
117 initialized: bool,
119 allow: i32,
121}
122
123impl CatalogState {
124 fn new() -> Self {
125 Self {
126 entries: Vec::new(),
127 catalogs: Vec::new(),
128 initialized: false,
129 allow: XML_CATA_ALLOW_ALL,
130 }
131 }
132
133 fn clear(&mut self) {
135 self.entries.clear();
136 self.catalogs.clear();
137 self.allow = XML_CATA_ALLOW_ALL;
138 }
139}
140
141static CATALOG_STATE: Lazy<RwLock<CatalogState>> = Lazy::new(|| RwLock::new(CatalogState::new()));
143
144fn trim_whitespace(bytes: &[u8]) -> &[u8] {
150 let start = bytes
151 .iter()
152 .position(|b| !b.is_ascii_whitespace())
153 .unwrap_or(bytes.len());
154 let end = bytes
155 .iter()
156 .rposition(|b| !b.is_ascii_whitespace())
157 .map_or(0, |p| p + 1);
158 &bytes[start..end]
159}
160
161fn starts_with(data: &[u8], prefix: &[u8]) -> bool {
163 if data.len() < prefix.len() {
164 return false;
165 }
166 data[..prefix.len()] == prefix[..]
167}
168
169fn starts_with_ignore_ascii_case(data: &[u8], prefix: &[u8]) -> bool {
171 if data.len() < prefix.len() {
172 return false;
173 }
174 data[..prefix.len()]
175 .iter()
176 .zip(prefix.iter())
177 .all(|(a, b)| a.eq_ignore_ascii_case(b))
178}
179
180fn extract_attr_value<'a>(data: &'a [u8], name: &[u8], pos: usize) -> Option<(&'a [u8], usize)> {
185 let remaining = &data[pos..];
186 let name_pos = find_subsequence(remaining, name)?;
188 let after_name = name_pos + name.len();
189 let after_name_slice = &remaining[after_name..];
190
191 let eq_pos = after_name_slice.iter().position(|b| *b == b'=')?;
193
194 let rel_quote_start = after_name_slice[eq_pos + 1..]
196 .iter()
197 .position(|b| *b == b'"' || *b == b'\'')
198 .map(|p| after_name + eq_pos + 1 + p)?;
199 let abs_quote_start = pos + rel_quote_start;
200 let quote_char = data[abs_quote_start];
201 let value_start = abs_quote_start + 1;
203 let value_end = data[value_start..]
204 .iter()
205 .position(|b| *b == quote_char)
206 .map(|p| value_start + p)?;
207
208 Some((&data[value_start..value_end], value_end + 1))
209}
210
211fn find_subsequence(data: &[u8], seq: &[u8]) -> Option<usize> {
213 if seq.is_empty() {
214 return Some(0);
215 }
216 data.windows(seq.len()).position(|w| w == seq)
217}
218
219fn extract_token(line: &[u8], pos: usize) -> Option<(&[u8], usize)> {
221 let line = &line[pos..];
222 let start = line.iter().position(|b| !b.is_ascii_whitespace())?;
223 let end = line[start..]
224 .iter()
225 .position(|b| b.is_ascii_whitespace())
226 .map(|p| start + p)
227 .unwrap_or(line.len());
228 Some((&line[start..end], pos + end))
229}
230
231fn extract_quoted_token(line: &[u8], pos: usize) -> Option<(&[u8], usize)> {
233 let line = &line[pos..];
234 let start = line.iter().position(|b| !b.is_ascii_whitespace())?;
235 if start >= line.len() {
236 return None;
237 }
238 let quote_char = line[start];
239 if quote_char != b'"' && quote_char != b'\'' {
240 return extract_token(line, 0);
242 }
243 let value_start = start + 1;
244 let end = line[value_start..]
245 .iter()
246 .position(|b| *b == quote_char)
247 .map(|p| value_start + p)?;
248 Some((&line[value_start..end], pos + end + 1))
249}
250
251fn parse_sgml_line(line: &[u8], entries: &mut Vec<CatalogEntry>) {
270 let trimmed = trim_whitespace(line);
271 if trimmed.is_empty() || trimmed.starts_with(b"--") {
272 return;
273 }
274
275 let Some((directive, after_directive)) = extract_token(trimmed, 0) else {
277 return;
278 };
279
280 match directive {
281 b"PUBLIC" | b"public" => {
282 let Some((pub_id, after_pub)) = extract_quoted_token(trimmed, after_directive) else {
283 return;
284 };
285 let Some((uri, _)) = extract_quoted_token(trimmed, after_pub) else {
286 return;
287 };
288 entries.push(CatalogEntry::Public {
289 public_id: pub_id.to_vec(),
290 uri: uri.to_vec(),
291 });
292 }
293 b"SYSTEM" | b"system" => {
294 let Some((sys_id, after_sys)) = extract_quoted_token(trimmed, after_directive) else {
295 return;
296 };
297 let Some((uri, _)) = extract_quoted_token(trimmed, after_sys) else {
298 return;
299 };
300 entries.push(CatalogEntry::System {
301 system_id: sys_id.to_vec(),
302 uri: uri.to_vec(),
303 });
304 }
305 b"URI" | b"uri" => {
306 let Some((uri_id, after_uri)) = extract_quoted_token(trimmed, after_directive) else {
308 return;
309 };
310 let Some((replacement, _)) = extract_quoted_token(trimmed, after_uri) else {
311 return;
312 };
313 entries.push(CatalogEntry::System {
314 system_id: uri_id.to_vec(),
315 uri: replacement.to_vec(),
316 });
317 }
318 b"CATALOG" | b"catalog" => {
319 let Some((path, _)) = extract_quoted_token(trimmed, after_directive) else {
320 return;
321 };
322 entries.push(CatalogEntry::NextCatalog {
323 catalog: path.to_vec(),
324 });
325 }
326 _ => {
327 }
329 }
330}
331
332fn parse_sgml_catalog(data: &[u8], entries: &mut Vec<CatalogEntry>) {
334 for line in data.split(|b| *b == b'\n') {
335 parse_sgml_line(line, entries);
336 }
337}
338
339fn parse_xml_catalog(data: &[u8], entries: &mut Vec<CatalogEntry>) {
348 let mut pos = 0;
349 let len = data.len();
350
351 while pos < len {
352 let Some(lt_pos) = data[pos..].iter().position(|b| *b == b'<') else {
354 break;
355 };
356 let tag_start = pos + lt_pos;
357
358 if tag_start + 1 >= len {
360 break;
361 }
362
363 let is_closing = data[tag_start + 1] == b'/';
364 if is_closing {
365 let Some(gt_pos) = data[tag_start..].iter().position(|b| *b == b'>') else {
367 break;
368 };
369 pos = tag_start + gt_pos + 1;
370 continue;
371 }
372
373 if data[tag_start + 1] == b'!' || data[tag_start + 1] == b'?' {
375 let Some(gt_pos) = data[tag_start..].iter().position(|b| *b == b'>') else {
376 break;
377 };
378 pos = tag_start + gt_pos + 1;
379 continue;
380 }
381
382 let tag_name_start = tag_start + 1;
384 let tag_name_end = data[tag_name_start..]
385 .iter()
386 .position(|b| b.is_ascii_whitespace() || *b == b'>' || *b == b'/')
387 .map(|p| tag_name_start + p)
388 .unwrap_or(len);
389
390 let tag_name = &data[tag_name_start..tag_name_end];
391
392 let Some(gt_or_slash_pos) = data[tag_start..]
394 .iter()
395 .position(|b| *b == b'>')
396 .map(|p| tag_start + p)
397 else {
398 break;
399 };
400
401 let is_self_closing = gt_or_slash_pos > 0 && data[gt_or_slash_pos - 1] == b'/';
402 let tag_content_end = if is_self_closing {
403 gt_or_slash_pos + 1
404 } else {
405 let close_tag = {
407 let mut close = Vec::with_capacity(tag_name.len() + 3);
408 close.push(b'<');
409 close.push(b'/');
410 close.extend_from_slice(tag_name);
411 close.push(b'>');
412 close
413 };
414 let close_pos = data[gt_or_slash_pos + 1..]
415 .windows(close_tag.len())
416 .position(|w| w == close_tag.as_slice())
417 .map(|p| gt_or_slash_pos + 1 + p + close_tag.len());
418
419 match close_pos {
420 Some(p) => p,
421 None => {
422 pos = gt_or_slash_pos + 1;
423 continue;
424 }
425 }
426 };
427
428 let tag_body_start = gt_or_slash_pos + 1;
429 let tag_body = &data[tag_body_start
430 ..tag_content_end
431 - if is_self_closing {
432 0
433 } else {
434 tag_name.len() + 3
435 }];
436 let tag_body = trim_whitespace(tag_body);
437
438 match tag_name {
439 b"public" => {
440 let Some((pub_id, _)) = extract_attr_value(data, b"publicId", tag_start) else {
441 pos = tag_content_end;
442 continue;
443 };
444 let Some((uri, _)) = extract_attr_value(data, b"uri", tag_start) else {
445 pos = tag_content_end;
446 continue;
447 };
448 entries.push(CatalogEntry::Public {
449 public_id: pub_id.to_vec(),
450 uri: uri.to_vec(),
451 });
452 }
453 b"system" => {
454 let Some((sys_id, _)) = extract_attr_value(data, b"systemId", tag_start) else {
455 pos = tag_content_end;
456 continue;
457 };
458 let Some((uri, _)) = extract_attr_value(data, b"uri", tag_start) else {
459 pos = tag_content_end;
460 continue;
461 };
462 entries.push(CatalogEntry::System {
463 system_id: sys_id.to_vec(),
464 uri: uri.to_vec(),
465 });
466 }
467 b"rewriteSystem" => {
468 let Some((prefix, _)) = extract_attr_value(data, b"systemIdStartString", tag_start)
469 else {
470 pos = tag_content_end;
471 continue;
472 };
473 let Some((rewrite, _)) = extract_attr_value(data, b"rewritePrefix", tag_start)
474 else {
475 pos = tag_content_end;
476 continue;
477 };
478 entries.push(CatalogEntry::RewriteSystem {
479 prefix: prefix.to_vec(),
480 rewrite: rewrite.to_vec(),
481 });
482 }
483 b"rewriteURI" => {
484 let Some((prefix, _)) = extract_attr_value(data, b"uriStartString", tag_start)
485 else {
486 pos = tag_content_end;
487 continue;
488 };
489 let Some((rewrite, _)) = extract_attr_value(data, b"rewritePrefix", tag_start)
490 else {
491 pos = tag_content_end;
492 continue;
493 };
494 entries.push(CatalogEntry::RewriteURI {
495 prefix: prefix.to_vec(),
496 rewrite: rewrite.to_vec(),
497 });
498 }
499 b"delegatePublic" => {
500 let Some((prefix, _)) = extract_attr_value(data, b"publicIdStartString", tag_start)
501 else {
502 pos = tag_content_end;
503 continue;
504 };
505 let Some((catalog, _)) = extract_attr_value(data, b"catalog", tag_start) else {
506 pos = tag_content_end;
507 continue;
508 };
509 entries.push(CatalogEntry::DelegatePublic {
510 prefix: prefix.to_vec(),
511 catalog: catalog.to_vec(),
512 });
513 }
514 b"delegateSystem" => {
515 let Some((prefix, _)) = extract_attr_value(data, b"systemIdStartString", tag_start)
516 else {
517 pos = tag_content_end;
518 continue;
519 };
520 let Some((catalog, _)) = extract_attr_value(data, b"catalog", tag_start) else {
521 pos = tag_content_end;
522 continue;
523 };
524 entries.push(CatalogEntry::DelegateSystem {
525 prefix: prefix.to_vec(),
526 catalog: catalog.to_vec(),
527 });
528 }
529 b"delegateURI" => {
530 let Some((prefix, _)) = extract_attr_value(data, b"uriStartString", tag_start)
531 else {
532 pos = tag_content_end;
533 continue;
534 };
535 let Some((catalog, _)) = extract_attr_value(data, b"catalog", tag_start) else {
536 pos = tag_content_end;
537 continue;
538 };
539 entries.push(CatalogEntry::DelegateURI {
540 prefix: prefix.to_vec(),
541 catalog: catalog.to_vec(),
542 });
543 }
544 b"nextCatalog" => {
545 let Some((catalog, _)) = extract_attr_value(data, b"catalog", tag_start) else {
546 pos = tag_content_end;
547 continue;
548 };
549 entries.push(CatalogEntry::NextCatalog {
550 catalog: catalog.to_vec(),
551 });
552 }
553 b"group" | b"catalog" => {
554 parse_xml_catalog(tag_body, entries);
556 }
557 _ => {
558 }
560 }
561
562 pos = tag_content_end;
563 }
564}
565
566fn read_file_bytes(path: &str) -> Option<Vec<u8>> {
572 let p = Path::new(path);
573 let metadata = fs::metadata(p).ok()?;
575 if metadata.len() > MAX_CATALOG_FILE_SIZE as u64 {
576 return None;
577 }
578 fs::read(p).ok()
579}
580
581fn detect_catalog_format(data: &[u8]) -> CatalogFormat {
583 let trimmed = trim_whitespace(data);
584 if trimmed.starts_with(b"<?xml") || trimmed.starts_with(b"<catalog") {
585 CatalogFormat::Xml
586 } else {
587 CatalogFormat::Sgml
588 }
589}
590
591fn load_catalog_data(path: &str, data: &[u8], entries: &mut Vec<CatalogEntry>) {
593 let format = detect_catalog_format(data);
594 match format {
595 CatalogFormat::Xml => {
596 parse_xml_catalog(data, entries);
597 }
598 CatalogFormat::Sgml => {
599 parse_sgml_catalog(data, entries);
600 }
601 }
602}
603
604fn load_single_catalog(path: &str, state: &mut CatalogState) {
606 let data = match read_file_bytes(path) {
607 Some(d) => d,
608 None => return,
609 };
610
611 let format = detect_catalog_format(&data);
612 state.catalogs.push(CatalogInfo {
613 path: path.as_bytes().to_vec(),
614 format,
615 });
616
617 load_catalog_data(path, &data, &mut state.entries);
618}
619
620fn load_catalog_list(catalogs: &str, state: &mut CatalogState) {
622 for catalog_path in catalogs.split(':') {
623 let trimmed = catalog_path.trim();
624 if !trimmed.is_empty() {
625 load_single_catalog(trimmed, state);
626 }
627 }
628}
629
630pub(crate) fn init() {
639 let mut state = CATALOG_STATE.write();
640 if state.initialized {
641 return;
642 }
643
644 state.allow = XML_CATA_ALLOW_ALL;
646 crate::xml::globals::set_catalog_defaults(XML_CATA_ALLOW_ALL);
647
648 if let Ok(catalogs) = std::env::var(XML_CATALOG_FILES_ENV) {
650 load_catalog_list(&catalogs, &mut state);
651 }
652
653 if let Ok(catalogs) = std::env::var(SGML_CATALOG_FILES_ENV) {
655 load_catalog_list(&catalogs, &mut state);
656 }
657
658 if Path::new(DEFAULT_CATALOG).exists() {
660 load_single_catalog(DEFAULT_CATALOG, &mut state);
661 }
662
663 state.initialized = true;
664}
665
666pub(crate) fn cleanup() {
670 let mut state = CATALOG_STATE.write();
671 state.clear();
672 state.initialized = false;
673}
674
675pub(crate) fn load_catalog(catalogs: *const c_char) -> *mut c_void {
689 if catalogs.is_null() {
690 return ptr::null_mut();
691 }
692
693 let catalogs_str = unsafe { CStr::from_ptr(catalogs) };
694 let catalogs_str = catalogs_str.to_str().unwrap_or("");
695
696 let mut state = CATALOG_STATE.write();
697
698 if !state.initialized {
700 drop(state);
701 init();
702 state = CATALOG_STATE.write();
703 }
704
705 let count_before = state.catalogs.len();
706 load_catalog_list(catalogs_str, &mut state);
707
708 if state.catalogs.len() > count_before {
709 (state.catalogs.len() as isize) as *mut c_void
711 } else {
712 ptr::null_mut()
713 }
714}
715
716fn catalog_allowed(state: &CatalogState) -> bool {
722 let allow = state.allow;
723 match allow {
724 XML_CATA_ALLOW_NONE => false,
725 XML_CATA_ALLOW_GLOBAL | XML_CATA_ALLOW_DOCUMENT | XML_CATA_ALLOW_ALL => true,
726 _ => false,
727 }
728}
729
730pub(crate) unsafe fn resolve_public(pub_id: *const xmlChar) -> *mut xmlChar {
741 if pub_id.is_null() {
742 return ptr::null_mut();
743 }
744
745 let state = CATALOG_STATE.read();
746 if !catalog_allowed(&state) {
747 return ptr::null_mut();
748 }
749
750 let pub_id_bytes = xmlstr_to_bytes(pub_id);
751
752 for entry in &state.entries {
754 if let CatalogEntry::Public { public_id, uri } = entry {
755 if public_id.as_slice() == pub_id_bytes {
756 return bytes_to_xmlstr(uri);
757 }
758 }
759 }
760
761 let mut best_match: Option<Vec<u8>> = None;
763 let mut best_prefix_len: usize = 0;
764
765 for entry in &state.entries {
766 if let CatalogEntry::DelegatePublic { prefix, catalog } = entry {
767 if pub_id_bytes.starts_with(prefix) && prefix.len() > best_prefix_len {
768 best_prefix_len = prefix.len();
769 if let Some(delegated_data) = read_file_bytes(&String::from_utf8_lossy(catalog)) {
771 let mut temp_entries = Vec::new();
772 parse_xml_catalog(&delegated_data, &mut temp_entries);
773 for temp_entry in &temp_entries {
775 if let CatalogEntry::Public { public_id: dp, uri } = temp_entry {
776 if dp.as_slice() == pub_id_bytes {
777 best_match = Some(uri.clone());
778 }
779 }
780 }
781 }
782 }
783 }
784 }
785
786 best_match
787 .as_ref()
788 .map_or(ptr::null_mut(), |uri| bytes_to_xmlstr(uri))
789}
790
791pub(crate) unsafe fn resolve_system(sys_id: *const xmlChar) -> *mut xmlChar {
804 if sys_id.is_null() {
805 return ptr::null_mut();
806 }
807
808 let state = CATALOG_STATE.read();
809 if !catalog_allowed(&state) {
810 return ptr::null_mut();
811 }
812
813 let sys_id_bytes = xmlstr_to_bytes(sys_id);
814
815 for entry in &state.entries {
817 if let CatalogEntry::System { system_id, uri } = entry {
818 if system_id.as_slice() == sys_id_bytes {
819 return bytes_to_xmlstr(uri);
820 }
821 }
822 }
823
824 let mut best_rewrite: Option<Vec<u8>> = None;
826 let mut best_prefix_len: usize = 0;
827
828 for entry in &state.entries {
829 if let CatalogEntry::RewriteSystem { prefix, rewrite } = entry {
830 if sys_id_bytes.starts_with(prefix) && prefix.len() > best_prefix_len {
831 best_prefix_len = prefix.len();
832 let suffix = &sys_id_bytes[prefix.len()..];
834 let mut result = rewrite.clone();
835 result.extend_from_slice(suffix);
836 best_rewrite = Some(result);
837 }
838 }
839 }
840
841 if let Some(rewritten) = best_rewrite {
842 return bytes_to_xmlstr(&rewritten);
843 }
844
845 for entry in &state.entries {
847 if let CatalogEntry::DelegateSystem { prefix, catalog } = entry {
848 if sys_id_bytes.starts_with(prefix) {
849 if let Some(delegated_data) = read_file_bytes(&String::from_utf8_lossy(catalog)) {
850 let mut temp_entries = Vec::new();
851 parse_xml_catalog(&delegated_data, &mut temp_entries);
852 for temp_entry in &temp_entries {
853 if let CatalogEntry::System { system_id, uri } = temp_entry {
854 if system_id.as_slice() == sys_id_bytes {
855 return bytes_to_xmlstr(uri);
856 }
857 }
858 }
859 }
860 }
861 }
862 }
863
864 ptr::null_mut()
865}
866
867pub(crate) unsafe fn resolve_uri(uri: *const xmlChar) -> *mut xmlChar {
880 if uri.is_null() {
881 return ptr::null_mut();
882 }
883
884 let state = CATALOG_STATE.read();
885 if !catalog_allowed(&state) {
886 return ptr::null_mut();
887 }
888
889 let uri_bytes = xmlstr_to_bytes(uri);
890
891 for entry in &state.entries {
893 if let CatalogEntry::System {
894 system_id,
895 uri: sys_uri,
896 } = entry
897 {
898 if system_id.as_slice() == uri_bytes {
899 return bytes_to_xmlstr(sys_uri);
900 }
901 }
902 }
903
904 let mut best_rewrite: Option<Vec<u8>> = None;
906 let mut best_prefix_len: usize = 0;
907
908 for entry in &state.entries {
909 if let CatalogEntry::RewriteURI { prefix, rewrite } = entry {
910 if uri_bytes.starts_with(prefix) && prefix.len() > best_prefix_len {
911 best_prefix_len = prefix.len();
912 let suffix = &uri_bytes[prefix.len()..];
913 let mut result = rewrite.clone();
914 result.extend_from_slice(suffix);
915 best_rewrite = Some(result);
916 }
917 }
918 }
919
920 if let Some(rewritten) = best_rewrite {
921 return bytes_to_xmlstr(&rewritten);
922 }
923
924 for entry in &state.entries {
926 if let CatalogEntry::DelegateURI { prefix, catalog } = entry {
927 if uri_bytes.starts_with(prefix) {
928 if let Some(delegated_data) = read_file_bytes(&String::from_utf8_lossy(catalog)) {
929 let mut temp_entries = Vec::new();
930 parse_xml_catalog(&delegated_data, &mut temp_entries);
931 for temp_entry in &temp_entries {
932 if let CatalogEntry::System {
933 system_id,
934 uri: sys_uri,
935 } = temp_entry
936 {
937 if system_id.as_slice() == uri_bytes {
938 return bytes_to_xmlstr(sys_uri);
939 }
940 }
941 }
942 }
943 }
944 }
945 }
946
947 ptr::null_mut()
948}
949
950pub(crate) fn set_defaults(allow: c_int) {
965 let mut state = CATALOG_STATE.write();
966 state.allow = allow;
967 crate::xml::globals::set_catalog_defaults(allow);
968}
969
970pub(crate) fn get_defaults() -> c_int {
978 let state = CATALOG_STATE.read();
979 state.allow
980}
981
982pub(crate) unsafe fn add(
999 type_: *const xmlChar,
1000 orig: *const xmlChar,
1001 replace: *const xmlChar,
1002) -> c_int {
1003 if type_.is_null() || orig.is_null() || replace.is_null() {
1004 return -1;
1005 }
1006
1007 let type_bytes = xmlstr_to_bytes(type_);
1008 let orig_bytes = xmlstr_to_bytes(orig);
1009 let replace_bytes = xmlstr_to_bytes(replace);
1010
1011 let mut state = CATALOG_STATE.write();
1012
1013 match type_bytes {
1014 b"public" => {
1015 state.entries.push(CatalogEntry::Public {
1016 public_id: orig_bytes.to_vec(),
1017 uri: replace_bytes.to_vec(),
1018 });
1019 0
1020 }
1021 b"system" => {
1022 state.entries.push(CatalogEntry::System {
1023 system_id: orig_bytes.to_vec(),
1024 uri: replace_bytes.to_vec(),
1025 });
1026 0
1027 }
1028 b"rewriteSystem" => {
1029 state.entries.push(CatalogEntry::RewriteSystem {
1030 prefix: orig_bytes.to_vec(),
1031 rewrite: replace_bytes.to_vec(),
1032 });
1033 0
1034 }
1035 b"rewriteURI" => {
1036 state.entries.push(CatalogEntry::RewriteURI {
1037 prefix: orig_bytes.to_vec(),
1038 rewrite: replace_bytes.to_vec(),
1039 });
1040 0
1041 }
1042 b"delegatePublic" => {
1043 state.entries.push(CatalogEntry::DelegatePublic {
1044 prefix: orig_bytes.to_vec(),
1045 catalog: replace_bytes.to_vec(),
1046 });
1047 0
1048 }
1049 b"delegateSystem" => {
1050 state.entries.push(CatalogEntry::DelegateSystem {
1051 prefix: orig_bytes.to_vec(),
1052 catalog: replace_bytes.to_vec(),
1053 });
1054 0
1055 }
1056 b"delegateURI" => {
1057 state.entries.push(CatalogEntry::DelegateURI {
1058 prefix: orig_bytes.to_vec(),
1059 catalog: replace_bytes.to_vec(),
1060 });
1061 0
1062 }
1063 b"nextCatalog" => {
1064 state.entries.push(CatalogEntry::NextCatalog {
1065 catalog: orig_bytes.to_vec(),
1066 });
1067 0
1068 }
1069 _ => -1,
1070 }
1071}
1072
1073pub(crate) unsafe fn remove(value: *const xmlChar) -> c_int {
1084 if value.is_null() {
1085 return -1;
1086 }
1087
1088 let value_bytes = xmlstr_to_bytes(value);
1089 let mut state = CATALOG_STATE.write();
1090
1091 let before = state.entries.len();
1092 state.entries.retain(|entry| match entry {
1093 CatalogEntry::Public { public_id, .. } => public_id.as_slice() != value_bytes,
1094 CatalogEntry::System { system_id, .. } => system_id.as_slice() != value_bytes,
1095 CatalogEntry::RewriteSystem { prefix, .. } => prefix.as_slice() != value_bytes,
1096 CatalogEntry::RewriteURI { prefix, .. } => prefix.as_slice() != value_bytes,
1097 CatalogEntry::DelegatePublic { prefix, .. } => prefix.as_slice() != value_bytes,
1098 CatalogEntry::DelegateSystem { prefix, .. } => prefix.as_slice() != value_bytes,
1099 CatalogEntry::DelegateURI { prefix, .. } => prefix.as_slice() != value_bytes,
1100 CatalogEntry::NextCatalog { catalog } => catalog.as_slice() != value_bytes,
1101 });
1102
1103 (before - state.entries.len()) as c_int
1104}
1105
1106pub(crate) unsafe fn convert() -> *mut _xmlDoc {
1121 let state = CATALOG_STATE.read();
1122
1123 if state.entries.is_empty() {
1124 return ptr::null_mut();
1125 }
1126
1127 let doc = crate::xml::tree::new_doc(ptr::null_mut());
1129 if doc.is_null() {
1130 return ptr::null_mut();
1131 }
1132
1133 let catalog_name = b"catalog\0" as *const u8 as *const xmlChar;
1135 let root = crate::xml::tree::new_node(ptr::null_mut(), catalog_name);
1136 if root.is_null() {
1137 crate::xml::tree::free_doc(doc);
1138 return ptr::null_mut();
1139 }
1140
1141 let xmlns_name = b"xmlns\0" as *const u8 as *const xmlChar;
1143 let ns_value = b"urn:oasis:names:tc:entity:xmlns:xml:catalog\0" as *const u8 as *const xmlChar;
1144 crate::xml::tree::set_prop(root, xmlns_name, ns_value);
1145
1146 crate::xml::tree::doc_set_root_element(doc, root);
1147
1148 for entry in &state.entries {
1150 let (elem_name, attr1_name, attr1_value, attr2_name, attr2_value) = match entry {
1151 CatalogEntry::Public { public_id, uri } => {
1152 let elem = b"public\0" as *const u8 as *mut xmlChar;
1153 let attr1 = b"publicId\0" as *const u8 as *mut xmlChar;
1154 let val1 = bytes_to_xmlstr(public_id);
1155 let attr2 = b"uri\0" as *const u8 as *mut xmlChar;
1156 let val2 = bytes_to_xmlstr(uri);
1157 (elem, attr1, val1, attr2, val2)
1158 }
1159 CatalogEntry::System { system_id, uri } => {
1160 let elem = b"system\0" as *const u8 as *mut xmlChar;
1161 let attr1 = b"systemId\0" as *const u8 as *mut xmlChar;
1162 let val1 = bytes_to_xmlstr(system_id);
1163 let attr2 = b"uri\0" as *const u8 as *mut xmlChar;
1164 let val2 = bytes_to_xmlstr(uri);
1165 (elem, attr1, val1, attr2, val2)
1166 }
1167 CatalogEntry::RewriteSystem { prefix, rewrite } => {
1168 let elem = b"rewriteSystem\0" as *const u8 as *mut xmlChar;
1169 let attr1 = b"systemIdStartString\0" as *const u8 as *mut xmlChar;
1170 let val1 = bytes_to_xmlstr(prefix);
1171 let attr2 = b"rewritePrefix\0" as *const u8 as *mut xmlChar;
1172 let val2 = bytes_to_xmlstr(rewrite);
1173 (elem, attr1, val1, attr2, val2)
1174 }
1175 CatalogEntry::RewriteURI { prefix, rewrite } => {
1176 let elem = b"rewriteURI\0" as *const u8 as *mut xmlChar;
1177 let attr1 = b"uriStartString\0" as *const u8 as *mut xmlChar;
1178 let val1 = bytes_to_xmlstr(prefix);
1179 let attr2 = b"rewritePrefix\0" as *const u8 as *mut xmlChar;
1180 let val2 = bytes_to_xmlstr(rewrite);
1181 (elem, attr1, val1, attr2, val2)
1182 }
1183 CatalogEntry::DelegatePublic { prefix, catalog } => {
1184 let elem = b"delegatePublic\0" as *const u8 as *mut xmlChar;
1185 let attr1 = b"publicIdStartString\0" as *const u8 as *mut xmlChar;
1186 let val1 = bytes_to_xmlstr(prefix);
1187 let attr2 = b"catalog\0" as *const u8 as *mut xmlChar;
1188 let val2 = bytes_to_xmlstr(catalog);
1189 (elem, attr1, val1, attr2, val2)
1190 }
1191 CatalogEntry::DelegateSystem { prefix, catalog } => {
1192 let elem = b"delegateSystem\0" as *const u8 as *mut xmlChar;
1193 let attr1 = b"systemIdStartString\0" as *const u8 as *mut xmlChar;
1194 let val1 = bytes_to_xmlstr(prefix);
1195 let attr2 = b"catalog\0" as *const u8 as *mut xmlChar;
1196 let val2 = bytes_to_xmlstr(catalog);
1197 (elem, attr1, val1, attr2, val2)
1198 }
1199 CatalogEntry::DelegateURI { prefix, catalog } => {
1200 let elem = b"delegateURI\0" as *const u8 as *mut xmlChar;
1201 let attr1 = b"uriStartString\0" as *const u8 as *mut xmlChar;
1202 let val1 = bytes_to_xmlstr(prefix);
1203 let attr2 = b"catalog\0" as *const u8 as *mut xmlChar;
1204 let val2 = bytes_to_xmlstr(catalog);
1205 (elem, attr1, val1, attr2, val2)
1206 }
1207 CatalogEntry::NextCatalog { catalog } => {
1208 let elem = b"nextCatalog\0" as *const u8 as *mut xmlChar;
1209 let attr1 = b"catalog\0" as *const u8 as *mut xmlChar;
1210 let val1 = bytes_to_xmlstr(catalog);
1211 let attr2 = ptr::null_mut();
1212 let val2 = ptr::null_mut();
1213 (elem, attr1, val1, attr2, val2)
1214 }
1215 };
1216
1217 let child = crate::xml::tree::new_child(root, ptr::null_mut(), elem_name);
1218 if child.is_null() {
1219 if !attr1_value.is_null() {
1221 xmlFree(attr1_value as *mut c_void);
1222 }
1223 if !attr2_value.is_null() {
1224 xmlFree(attr2_value as *mut c_void);
1225 }
1226 continue;
1227 }
1228
1229 crate::xml::tree::set_prop(child, attr1_name, attr1_value);
1230 if !attr2_name.is_null() {
1231 crate::xml::tree::set_prop(child, attr2_name, attr2_value);
1232 }
1233
1234 if !attr1_value.is_null() {
1236 xmlFree(attr1_value as *mut c_void);
1237 }
1238 if !attr2_value.is_null() {
1239 xmlFree(attr2_value as *mut c_void);
1240 }
1241 }
1242
1243 doc
1244}
1245
1246#[cfg(test)]
1251mod tests {
1252 use super::*;
1253 use crate::abi::allocator::xmlFree;
1254 use crate::xml::string::xmlstr_to_bytes;
1255 use std::ffi::CString;
1256 use std::sync::Mutex;
1257
1258 static CATALOG_TEST_MUTEX: Mutex<()> = Mutex::new(());
1267
1268 unsafe fn to_xmlstr(s: &[u8]) -> *const xmlChar {
1270 let ptr = bytes_to_xmlstr(s);
1271 ptr as *const xmlChar
1272 }
1273
1274 unsafe fn to_xmlstr_str(s: &str) -> *const xmlChar {
1276 to_xmlstr(s.as_bytes())
1277 }
1278
1279 unsafe fn free_xmlstr(ptr: *const xmlChar) {
1280 if !ptr.is_null() {
1281 xmlFree(ptr as *mut c_void);
1282 }
1283 }
1284
1285 fn setup() -> std::sync::MutexGuard<'static, ()> {
1292 let guard = CATALOG_TEST_MUTEX.lock().unwrap();
1293 cleanup();
1294 init();
1295 set_defaults(XML_CATA_ALLOW_ALL);
1297 guard
1298 }
1299
1300 fn teardown(_guard: std::sync::MutexGuard<'static, ()>) {
1301 cleanup();
1302 }
1304
1305 #[test]
1308 fn test_resolve_public_basic() {
1309 let _guard = setup();
1310 unsafe {
1311 let type_ = to_xmlstr_str("public");
1313 let pub_id = to_xmlstr_str("-//OASIS//DTD DocBook XML V4.2//EN");
1314 let uri = to_xmlstr_str("http://www.oasis-open.org/docbook/xml/4.2/docbookx.dtd");
1315 assert_eq!(add(type_, pub_id, uri), 0);
1316
1317 let result = resolve_public(pub_id);
1319 assert!(!result.is_null());
1320 assert_eq!(
1321 xmlstr_to_bytes(result),
1322 b"http://www.oasis-open.org/docbook/xml/4.2/docbookx.dtd"
1323 );
1324 xmlFree(result as *mut c_void);
1325
1326 let unknown = to_xmlstr_str("-//Unknown//DTD Unknown//EN");
1328 assert!(resolve_public(unknown).is_null());
1329 free_xmlstr(unknown);
1330
1331 free_xmlstr(type_);
1332 free_xmlstr(pub_id);
1333 free_xmlstr(uri);
1334 teardown(_guard);
1335 }
1336 }
1337
1338 #[test]
1341 fn test_resolve_system_basic() {
1342 let _guard = setup();
1343 unsafe {
1344 let type_ = to_xmlstr_str("system");
1345 let sys_id = to_xmlstr_str("http://example.com/foo.dtd");
1346 let uri = to_xmlstr_str("/local/foo.dtd");
1347 assert_eq!(add(type_, sys_id, uri), 0);
1348
1349 let result = resolve_system(sys_id);
1350 assert!(!result.is_null());
1351 assert_eq!(xmlstr_to_bytes(result), b"/local/foo.dtd");
1352 xmlFree(result as *mut c_void);
1353
1354 free_xmlstr(type_);
1355 free_xmlstr(sys_id);
1356 free_xmlstr(uri);
1357 teardown(_guard);
1358 }
1359 }
1360
1361 #[test]
1364 fn test_resolve_uri_basic() {
1365 let _guard = setup();
1366 unsafe {
1367 let type_ = to_xmlstr_str("system");
1369 let sys_id = to_xmlstr_str("http://example.com/resource.xml");
1370 let uri = to_xmlstr_str("/local/resource.xml");
1371 assert_eq!(add(type_, sys_id, uri), 0);
1372
1373 let result = resolve_uri(sys_id);
1374 assert!(!result.is_null());
1375 assert_eq!(xmlstr_to_bytes(result), b"/local/resource.xml");
1376 xmlFree(result as *mut c_void);
1377
1378 free_xmlstr(type_);
1379 free_xmlstr(sys_id);
1380 free_xmlstr(uri);
1381 teardown(_guard);
1382 }
1383 }
1384
1385 #[test]
1388 fn test_rewrite_system() {
1389 let _guard = setup();
1390 unsafe {
1391 let type_ = to_xmlstr_str("rewriteSystem");
1392 let prefix = to_xmlstr_str("http://example.com/old/");
1393 let rewrite = to_xmlstr_str("http://mirror.example.com/new/");
1394 assert_eq!(add(type_, prefix, rewrite), 0);
1395
1396 let sys_id = to_xmlstr_str("http://example.com/old/path/file.xml");
1397 let result = resolve_system(sys_id);
1398 assert!(!result.is_null());
1399 assert_eq!(
1400 xmlstr_to_bytes(result),
1401 b"http://mirror.example.com/new/path/file.xml"
1402 );
1403 xmlFree(result as *mut c_void);
1404
1405 free_xmlstr(type_);
1406 free_xmlstr(prefix);
1407 free_xmlstr(rewrite);
1408 free_xmlstr(sys_id);
1409 teardown(_guard);
1410 }
1411 }
1412
1413 #[test]
1416 fn test_rewrite_uri() {
1417 let _guard = setup();
1418 unsafe {
1419 let type_ = to_xmlstr_str("rewriteURI");
1420 let prefix = to_xmlstr_str("http://example.com/old/");
1421 let rewrite = to_xmlstr_str("http://mirror.example.com/new/");
1422 assert_eq!(add(type_, prefix, rewrite), 0);
1423
1424 let uri = to_xmlstr_str("http://example.com/old/path/file.xml");
1425 let result = resolve_uri(uri);
1426 assert!(!result.is_null());
1427 assert_eq!(
1428 xmlstr_to_bytes(result),
1429 b"http://mirror.example.com/new/path/file.xml"
1430 );
1431 xmlFree(result as *mut c_void);
1432
1433 free_xmlstr(type_);
1434 free_xmlstr(prefix);
1435 free_xmlstr(rewrite);
1436 free_xmlstr(uri);
1437 teardown(_guard);
1438 }
1439 }
1440
1441 #[test]
1444 fn test_remove_entries() {
1445 let _guard = setup();
1446 unsafe {
1447 let type_ = to_xmlstr_str("public");
1448 let pub_id = to_xmlstr_str("-//TEST//PUBLIC//EN");
1449 let uri = to_xmlstr_str("test.dtd");
1450 assert_eq!(add(type_, pub_id, uri), 0);
1451
1452 assert!(!resolve_public(pub_id).is_null());
1454
1455 assert_eq!(remove(pub_id), 1);
1457
1458 assert!(resolve_public(pub_id).is_null());
1460
1461 free_xmlstr(type_);
1462 free_xmlstr(pub_id);
1463 free_xmlstr(uri);
1464 teardown(_guard);
1465 }
1466 }
1467
1468 #[test]
1471 fn test_catalog_defaults() {
1472 let _guard = setup();
1473
1474 assert_eq!(get_defaults(), XML_CATA_ALLOW_ALL);
1475
1476 set_defaults(XML_CATA_ALLOW_NONE);
1477 assert_eq!(get_defaults(), XML_CATA_ALLOW_NONE);
1478
1479 set_defaults(XML_CATA_ALLOW_GLOBAL);
1480 assert_eq!(get_defaults(), XML_CATA_ALLOW_GLOBAL);
1481
1482 set_defaults(XML_CATA_ALLOW_ALL);
1483 assert_eq!(get_defaults(), XML_CATA_ALLOW_ALL);
1484
1485 teardown(_guard);
1486 }
1487
1488 #[test]
1491 fn test_parse_xml_catalog_in_memory() {
1492 let _guard = setup();
1493 unsafe {
1494 let catalog_xml = br#"<?xml version="1.0"?>
1495<!DOCTYPE catalog PUBLIC "-//OASIS//DTD Entity Resolution XML Catalog V1.0//EN" "http://www.oasis-open.org/committees/entity/release/1.0/catalog.dtd">
1496<catalog xmlns="urn:oasis:names:tc:entity:xmlns:xml:catalog">
1497 <public publicId="-//OASIS//DTD DocBook XML V4.2//EN" uri="http://www.oasis-open.org/docbook/xml/4.2/docbookx.dtd"/>
1498 <system systemId="http://example.com/foo.dtd" uri="/local/foo.dtd"/>
1499 <rewriteSystem systemIdStartString="http://example.com/old/" rewritePrefix="http://mirror.example.com/new/"/>
1500 <rewriteURI uriStartString="http://example.com/old/" rewritePrefix="http://mirror.example.com/new/"/>
1501</catalog>"#;
1502
1503 let mut entries = Vec::new();
1505 parse_xml_catalog(catalog_xml, &mut entries);
1506 assert_eq!(entries.len(), 4);
1507
1508 match &entries[0] {
1510 CatalogEntry::Public { public_id, uri } => {
1511 assert_eq!(public_id.as_slice(), b"-//OASIS//DTD DocBook XML V4.2//EN");
1512 assert_eq!(
1513 uri.as_slice(),
1514 b"http://www.oasis-open.org/docbook/xml/4.2/docbookx.dtd"
1515 );
1516 }
1517 _ => panic!("Expected Public entry"),
1518 }
1519
1520 match &entries[1] {
1522 CatalogEntry::System { system_id, uri } => {
1523 assert_eq!(system_id.as_slice(), b"http://example.com/foo.dtd");
1524 assert_eq!(uri.as_slice(), b"/local/foo.dtd");
1525 }
1526 _ => panic!("Expected System entry"),
1527 }
1528
1529 match &entries[2] {
1531 CatalogEntry::RewriteSystem { prefix, rewrite } => {
1532 assert_eq!(prefix.as_slice(), b"http://example.com/old/");
1533 assert_eq!(rewrite.as_slice(), b"http://mirror.example.com/new/");
1534 }
1535 _ => panic!("Expected RewriteSystem entry"),
1536 }
1537
1538 match &entries[3] {
1540 CatalogEntry::RewriteURI { prefix, rewrite } => {
1541 assert_eq!(prefix.as_slice(), b"http://example.com/old/");
1542 assert_eq!(rewrite.as_slice(), b"http://mirror.example.com/new/");
1543 }
1544 _ => panic!("Expected RewriteURI entry"),
1545 }
1546
1547 teardown(_guard);
1548 }
1549 }
1550
1551 #[test]
1554 fn test_parse_sgml_catalog() {
1555 let _guard = setup();
1556 unsafe {
1557 let sgml_data = br#"-- SGML catalog
1558PUBLIC "-//OASIS//DTD DocBook XML V4.2//EN" "docbookx.dtd"
1559SYSTEM "http://example.com/foo.dtd" "/local/foo.dtd"
1560URI "http://example.com/resource" "/local/resource"
1561"#;
1562
1563 let mut entries = Vec::new();
1564 parse_sgml_catalog(sgml_data, &mut entries);
1565 assert_eq!(entries.len(), 3);
1566
1567 match &entries[0] {
1569 CatalogEntry::Public { public_id, uri } => {
1570 assert_eq!(public_id.as_slice(), b"-//OASIS//DTD DocBook XML V4.2//EN");
1571 assert_eq!(uri.as_slice(), b"docbookx.dtd");
1572 }
1573 _ => panic!("Expected Public entry"),
1574 }
1575
1576 match &entries[1] {
1578 CatalogEntry::System { system_id, uri } => {
1579 assert_eq!(system_id.as_slice(), b"http://example.com/foo.dtd");
1580 assert_eq!(uri.as_slice(), b"/local/foo.dtd");
1581 }
1582 _ => panic!("Expected System entry"),
1583 }
1584
1585 match &entries[2] {
1587 CatalogEntry::System { system_id, uri } => {
1588 assert_eq!(system_id.as_slice(), b"http://example.com/resource");
1589 assert_eq!(uri.as_slice(), b"/local/resource");
1590 }
1591 _ => panic!("Expected System entry for URI"),
1592 }
1593
1594 teardown(_guard);
1595 }
1596 }
1597
1598 #[test]
1601 fn test_resolution_precedence() {
1602 let _guard = setup();
1603 unsafe {
1604 let type_sys = to_xmlstr_str("system");
1606 let sys_id = to_xmlstr_str("http://example.com/target.xml");
1607 let uri_direct = to_xmlstr_str("/direct/uri.xml");
1608 assert_eq!(add(type_sys, sys_id, uri_direct), 0);
1609
1610 let type_rw = to_xmlstr_str("rewriteSystem");
1612 let prefix = to_xmlstr_str("http://example.com/");
1613 let rewrite = to_xmlstr_str("/rewrite/");
1614 assert_eq!(add(type_rw, prefix, rewrite), 0);
1615
1616 let result = resolve_system(sys_id);
1618 assert!(!result.is_null());
1619 assert_eq!(xmlstr_to_bytes(result), b"/direct/uri.xml");
1620 xmlFree(result as *mut c_void);
1621
1622 free_xmlstr(type_sys);
1623 free_xmlstr(sys_id);
1624 free_xmlstr(uri_direct);
1625 free_xmlstr(type_rw);
1626 free_xmlstr(prefix);
1627 free_xmlstr(rewrite);
1628 teardown(_guard);
1629 }
1630 }
1631
1632 #[test]
1635 fn test_convert_sgml_to_xml() {
1636 let _guard = setup();
1637 unsafe {
1638 let type_ = to_xmlstr_str("public");
1639 let pub_id = to_xmlstr_str("-//TEST//PUBLIC//EN");
1640 let uri = to_xmlstr_str("test.dtd");
1641 assert_eq!(add(type_, pub_id, uri), 0);
1642
1643 let doc = convert();
1644 assert!(!doc.is_null());
1645
1646 let root = crate::xml::tree::doc_get_root_element(doc);
1648 assert!(!root.is_null());
1649 let root_name = crate::xml::string::xmlstr_to_bytes((*root).name);
1650 assert_eq!(root_name, b"catalog");
1651
1652 let child = (*root).children;
1654 assert!(!child.is_null());
1655 let child_name = crate::xml::string::xmlstr_to_bytes((*child).name);
1656 assert_eq!(child_name, b"public");
1657
1658 crate::xml::tree::free_doc(doc);
1659 free_xmlstr(type_);
1660 free_xmlstr(pub_id);
1661 free_xmlstr(uri);
1662 teardown(_guard);
1663 }
1664 }
1665
1666 #[test]
1669 fn test_catalog_disallowed() {
1670 let _guard = setup();
1671 unsafe {
1672 let type_ = to_xmlstr_str("system");
1674 let sys_id = to_xmlstr_str("http://example.com/test.dtd");
1675 let uri = to_xmlstr_str("/local/test.dtd");
1676 add(type_, sys_id, uri);
1677
1678 set_defaults(XML_CATA_ALLOW_NONE);
1680
1681 assert!(resolve_system(sys_id).is_null());
1683 assert!(resolve_public(sys_id).is_null());
1684 assert!(resolve_uri(sys_id).is_null());
1685
1686 set_defaults(XML_CATA_ALLOW_ALL);
1687 free_xmlstr(type_);
1688 free_xmlstr(sys_id);
1689 free_xmlstr(uri);
1690 teardown(_guard);
1691 }
1692 }
1693
1694 #[test]
1697 fn test_init_cleanup() {
1698 let _guard = CATALOG_TEST_MUTEX.lock().unwrap();
1699 cleanup();
1700 assert_eq!(CATALOG_STATE.read().initialized, false);
1701
1702 init();
1703 assert_eq!(CATALOG_STATE.read().initialized, true);
1704
1705 cleanup();
1706 assert_eq!(CATALOG_STATE.read().initialized, false);
1707 }
1708
1709 #[test]
1712 fn test_parse_xml_catalog_group() {
1713 let catalog_xml = br#"<?xml version="1.0"?>
1714<catalog xmlns="urn:oasis:names:tc:entity:xmlns:xml:catalog">
1715 <group>
1716 <public publicId="-//GROUP//PUBLIC//EN" uri="group.dtd"/>
1717 <system systemId="http://group.example.com/" uri="/group/"/>
1718 </group>
1719</catalog>"#;
1720
1721 let mut entries = Vec::new();
1722 parse_xml_catalog(catalog_xml, &mut entries);
1723 assert_eq!(entries.len(), 2);
1724
1725 match &entries[0] {
1726 CatalogEntry::Public { public_id, .. } => {
1727 assert_eq!(public_id.as_slice(), b"-//GROUP//PUBLIC//EN");
1728 }
1729 _ => panic!("Expected Public entry"),
1730 }
1731
1732 match &entries[1] {
1733 CatalogEntry::System { system_id, .. } => {
1734 assert_eq!(system_id.as_slice(), b"http://group.example.com/");
1735 }
1736 _ => panic!("Expected System entry"),
1737 }
1738 }
1739
1740 #[test]
1743 fn test_multiple_entries() {
1744 let _guard = setup();
1745 unsafe {
1746 let t = to_xmlstr_str("public");
1748 let id1 = to_xmlstr_str("-//A//PUBLIC//EN");
1749 let uri1 = to_xmlstr_str("a.dtd");
1750 let id2 = to_xmlstr_str("-//B//PUBLIC//EN");
1751 let uri2 = to_xmlstr_str("b.dtd");
1752
1753 assert_eq!(add(t, id1, uri1), 0);
1754 assert_eq!(add(t, id2, uri2), 0);
1755
1756 let r1 = resolve_public(id1);
1757 assert!(!r1.is_null());
1758 assert_eq!(xmlstr_to_bytes(r1), b"a.dtd");
1759 xmlFree(r1 as *mut c_void);
1760
1761 let r2 = resolve_public(id2);
1762 assert!(!r2.is_null());
1763 assert_eq!(xmlstr_to_bytes(r2), b"b.dtd");
1764 xmlFree(r2 as *mut c_void);
1765
1766 free_xmlstr(t);
1767 free_xmlstr(id1);
1768 free_xmlstr(uri1);
1769 free_xmlstr(id2);
1770 free_xmlstr(uri2);
1771 teardown(_guard);
1772 }
1773 }
1774
1775 #[test]
1778 fn test_longest_prefix_wins() {
1779 let _guard = setup();
1780 unsafe {
1781 let t = to_xmlstr_str("rewriteSystem");
1782 let p1 = to_xmlstr_str("http://example.com/");
1783 let r1 = to_xmlstr_str("/general/");
1784 let p2 = to_xmlstr_str("http://example.com/specific/");
1785 let r2 = to_xmlstr_str("/specific/");
1786
1787 add(t, p1, r1);
1788 add(t, p2, r2);
1789
1790 let sys_id = to_xmlstr_str("http://example.com/specific/file.xml");
1791 let result = resolve_system(sys_id);
1792 assert!(!result.is_null());
1793 assert_eq!(xmlstr_to_bytes(result), b"/specific/file.xml");
1794 xmlFree(result as *mut c_void);
1795
1796 free_xmlstr(t);
1797 free_xmlstr(p1);
1798 free_xmlstr(r1);
1799 free_xmlstr(p2);
1800 free_xmlstr(r2);
1801 free_xmlstr(sys_id);
1802 teardown(_guard);
1803 }
1804 }
1805}