libxml_rs/abi/exports_uri.rs
1//! C ABI exports for the URI subsystem (`uri.h`) — family closure (11.1-I).
2//!
3//! This module implements the 11 URI entry points assigned to this
4//! workstream:
5//!
6//! 1. `xmlBuildURI` / `xmlBuildURISafe`
7//! 2. `xmlBuildRelativeURI` / `xmlBuildRelativeURISafe`
8//! 3. `xmlCanonicPath`
9//! 4. `xmlPathToURI`
10//! 5. `xmlPrintURI`
11//! 6. `xmlURIEscape`
12//! 7. `xmlNormalizeWindowsPath`
13//! 8. `xmlCheckLanguageID`
14//! 9. `xmlParseURISafe`
15//!
16//! All functions follow the upstream `uri.c` / `xmlIO.c` / `parser.c`
17//! implementations (libxml2 2.15.3, see `archaeology/libxml2-git`),
18//! reusing the internal parser/resolver from `src/xml/uri/mod.rs`
19//! (`parse_uri`, `build_uri`, `resolve_uri`, `normalize_uri_path`,
20//! `xmlURIEscapeStr`, `xmlSaveUri`, `xmlParseURI`).
21//!
22//! All returned strings are allocated with `xmlMalloc` so C callers release
23//! them with `xmlFree`, exactly as with upstream libxml2.
24//!
25//! The `*Safe` variants follow the upstream convention of returning an `int`
26//! status code (0 = success, 1 = invalid argument/URI, -1 = allocation
27//! failure) and storing the result through an out parameter.
28
29#![allow(
30 missing_docs,
31 non_snake_case,
32 non_camel_case_types,
33 non_upper_case_globals
34)]
35#![allow(clippy::missing_safety_doc)]
36#![allow(clippy::not_unsafe_ptr_arg_deref)]
37
38use core::ffi::c_void;
39use core::ptr;
40use std::os::raw::{c_char, c_int};
41
42use crate::abi::allocator;
43use crate::abi::types::xmlChar;
44use crate::xml::uri::{build_uri, parse_uri, resolve_uri, UriParts};
45
46extern "C" {
47 /// `size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);`
48 ///
49 /// `FILE*` is opaque to Rust, so the stream is carried as `*mut c_void`.
50 fn fwrite(ptr: *const c_void, size: usize, nmemb: usize, stream: *mut c_void) -> usize;
51}
52
53// ── Internal helpers ─────────────────────────────────────────────────────────
54
55/// View a NUL-terminated C string as a byte slice (empty when NULL).
56///
57/// # Safety
58///
59/// `p` must be a valid NUL-terminated string (or NULL) for the duration of
60/// the returned slice.
61unsafe fn cstr_bytes<'a>(p: *const c_char) -> &'a [u8] {
62 if p.is_null() {
63 return &[];
64 }
65 let len = unsafe { libc::strlen(p) };
66 unsafe { core::slice::from_raw_parts(p as *const u8, len) }
67}
68
69/// Copy `bytes` into a fresh `xmlMalloc`'d NUL-terminated string.
70///
71/// Returns NULL on allocation failure. An empty `bytes` yields a valid
72/// 1-byte NUL string (never NULL), matching `xmlStrdup("")`.
73unsafe fn dup_c_str(bytes: &[u8]) -> *mut xmlChar {
74 let len = bytes.len();
75 let p = unsafe { allocator::xmlMallocImpl(len + 1) as *mut u8 };
76 if p.is_null() {
77 return ptr::null_mut();
78 }
79 unsafe {
80 ptr::copy_nonoverlapping(bytes.as_ptr(), p, len);
81 *p.add(len) = 0;
82 }
83 p as *mut xmlChar
84}
85
86/// Escape `s` with `xmlURIEscapeStr` semantics (keep unreserved + '%' +
87/// every byte in `list`), returning an `xmlMalloc`'d string.
88///
89/// Returns NULL on allocation failure.
90unsafe fn escape_slice(s: &[u8], list: &[u8]) -> *mut xmlChar {
91 let mut s_buf = s.to_vec();
92 s_buf.push(0);
93 let mut l_buf = list.to_vec();
94 l_buf.push(0);
95 unsafe {
96 crate::xml::uri::xmlURIEscapeStr(
97 s_buf.as_ptr() as *const xmlChar,
98 l_buf.as_ptr() as *const xmlChar,
99 )
100 }
101}
102
103// ── RFC 3986 character grammar (upstream uri.c, with/without ALLOW_UNWISE) ──
104
105/// `unreserved` per RFC 3986, plus the "unwise" set when `allow_unwise`
106/// mirrors upstream `XML_URI_ALLOW_UNWISE` in `xmlIsUnreserved`.
107fn v_unres(b: u8, allow_unwise: bool) -> bool {
108 b.is_ascii_alphanumeric()
109 || matches!(b, b'-' | b'.' | b'_' | b'~')
110 || (allow_unwise && matches!(b, b'{' | b'}' | b'|' | b'\\' | b'^' | b'[' | b']' | b'`'))
111}
112
113/// `sub-delims` per RFC 3986.
114fn v_sub_delim(b: u8) -> bool {
115 matches!(
116 b,
117 b'!' | b'$' | b'&' | b'\'' | b'(' | b')' | b'*' | b'+' | b',' | b';' | b'='
118 )
119}
120
121/// `pct-encoded = "%" HEXDIG HEXDIG` starting at position `i`.
122fn v_pct(s: &[u8], i: usize) -> bool {
123 i + 2 < s.len() && s[i] == b'%' && s[i + 1].is_ascii_hexdigit() && s[i + 2].is_ascii_hexdigit()
124}
125
126/// `pchar = unreserved / pct-encoded / sub-delims / ":" / "@"` at position `i`.
127fn v_pchar(s: &[u8], i: usize, allow_unwise: bool) -> bool {
128 v_unres(s[i], allow_unwise) || v_pct(s, i) || v_sub_delim(s[i]) || s[i] == b':' || s[i] == b'@'
129}
130
131/// Advance over a run of `pchar` bytes.
132fn v_advance_pchar(s: &[u8], i: &mut usize, allow_unwise: bool) {
133 while *i < s.len() && v_pchar(s, *i, allow_unwise) {
134 if s[*i] == b'%' {
135 *i += 3;
136 } else {
137 *i += 1;
138 }
139 }
140}
141
142/// `authority = [ userinfo "@" ] host [ ":" port ]`.
143///
144/// Mirrors the upstream character set: unreserved / pct-encoded /
145/// sub-delims / ":" / "@", with `[...]` IP-literals accepted as a unit
146/// (upstream `xmlParse3986IPLiteral`).
147fn v_authority(s: &[u8], i: &mut usize, allow_unwise: bool) -> bool {
148 while *i < s.len() && !matches!(s[*i], b'/' | b'?' | b'#') {
149 let b = s[*i];
150 if b == b'[' {
151 if let Some(close) = s[*i + 1..].iter().position(|&c| c == b']') {
152 *i += close + 2;
153 } else {
154 return false;
155 }
156 } else if v_unres(b, allow_unwise)
157 || v_pct(s, *i)
158 || v_sub_delim(b)
159 || b == b':'
160 || b == b'@'
161 {
162 if b == b'%' {
163 *i += 3;
164 } else {
165 *i += 1;
166 }
167 } else {
168 return false;
169 }
170 }
171 true
172}
173
174/// `path-abempty = *( "/" segment )` — empty segments allowed.
175fn v_path_abempty(s: &[u8], i: &mut usize, allow_unwise: bool) -> bool {
176 while *i < s.len() && s[*i] == b'/' {
177 *i += 1;
178 v_advance_pchar(s, i, allow_unwise);
179 }
180 true
181}
182
183/// `path-absolute = "/" [ segment-nz *( "/" segment ) ]`, with `i` just
184/// past the leading "/".
185fn v_path_absolute(s: &[u8], i: &mut usize, allow_unwise: bool) -> bool {
186 if *i < s.len() && !matches!(s[*i], b'?' | b'#') {
187 if !v_pchar(s, *i, allow_unwise) {
188 return false;
189 }
190 v_advance_pchar(s, i, allow_unwise);
191 }
192 v_path_abempty(s, i, allow_unwise)
193}
194
195/// `path-rootless = segment-nz *( "/" segment )`.
196fn v_path_rootless(s: &[u8], i: &mut usize, allow_unwise: bool) -> bool {
197 if *i >= s.len() || !v_pchar(s, *i, allow_unwise) {
198 return false;
199 }
200 v_advance_pchar(s, i, allow_unwise);
201 v_path_abempty(s, i, allow_unwise)
202}
203
204/// `path-noscheme = segment-nz-nc *( "/" segment )` — the first segment
205/// must not contain ":".
206fn v_path_noscheme(s: &[u8], i: &mut usize, allow_unwise: bool) -> bool {
207 if *i >= s.len()
208 || !(v_unres(s[*i], allow_unwise) || v_pct(s, *i) || v_sub_delim(s[*i]) || s[*i] == b'@')
209 {
210 return false;
211 }
212 while *i < s.len()
213 && (v_unres(s[*i], allow_unwise) || v_pct(s, *i) || v_sub_delim(s[*i]) || s[*i] == b'@')
214 {
215 if s[*i] == b'%' {
216 *i += 3;
217 } else {
218 *i += 1;
219 }
220 }
221 v_path_abempty(s, i, allow_unwise)
222}
223
224/// Validate a URI reference against the upstream RFC 3986 grammar
225/// (`xmlParse3986URIReference`, uri.c).
226///
227/// `allow_unwise` mirrors parsing with `XML_URI_ALLOW_UNWISE` set, which is
228/// exactly how `xmlURIEscape` parses its input. `xmlParseURISafe` and the
229/// `xmlBuildURI*` family parse without it.
230fn uri_reference_valid(s: &[u8], allow_unwise: bool) -> bool {
231 let n = s.len();
232 let mut i = 0usize;
233 let mut has_scheme = false;
234
235 // scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." ) ":"
236 if i < n && s[i].is_ascii_alphabetic() {
237 let mut j = i;
238 while j < n && (s[j].is_ascii_alphanumeric() || matches!(s[j], b'+' | b'-' | b'.')) {
239 j += 1;
240 }
241 if j < n && s[j] == b':' {
242 has_scheme = true;
243 i = j + 1;
244 }
245 }
246
247 if has_scheme {
248 // hier-part
249 if i + 1 < n && s[i] == b'/' && s[i + 1] == b'/' {
250 i += 2;
251 if !v_authority(s, &mut i, allow_unwise) || !v_path_abempty(s, &mut i, allow_unwise) {
252 return false;
253 }
254 } else if i < n && s[i] == b'/' {
255 i += 1;
256 if !v_path_absolute(s, &mut i, allow_unwise) {
257 return false;
258 }
259 } else if i < n && v_pchar(s, i, allow_unwise) {
260 if !v_path_rootless(s, &mut i, allow_unwise) {
261 return false;
262 }
263 }
264 } else {
265 // relative-ref
266 if i + 1 < n && s[i] == b'/' && s[i + 1] == b'/' {
267 i += 2;
268 if !v_authority(s, &mut i, allow_unwise) || !v_path_abempty(s, &mut i, allow_unwise) {
269 return false;
270 }
271 } else if i < n && s[i] == b'/' {
272 i += 1;
273 if !v_path_absolute(s, &mut i, allow_unwise) {
274 return false;
275 }
276 } else if i < n && v_pchar(s, i, allow_unwise) {
277 if !v_path_noscheme(s, &mut i, allow_unwise) {
278 return false;
279 }
280 }
281 }
282
283 // [ "?" query ]
284 if i < n && s[i] == b'?' {
285 i += 1;
286 while i < n && (v_pchar(s, i, allow_unwise) || matches!(s[i], b'/' | b'?')) {
287 if s[i] == b'%' {
288 i += 3;
289 } else {
290 i += 1;
291 }
292 }
293 }
294 // [ "#" fragment ]
295 if i < n && s[i] == b'#' {
296 i += 1;
297 while i < n && (v_pchar(s, i, allow_unwise) || matches!(s[i], b'/' | b'?')) {
298 if s[i] == b'%' {
299 i += 3;
300 } else {
301 i += 1;
302 }
303 }
304 }
305 i == n
306}
307
308/// Port of upstream `xmlNormalizePath` (uri.c), a filesystem path
309/// normalizer: collapses "./" and extra separators, resolves ".." segments,
310/// keeps a leading "../" on relative paths and a trailing "/" or ".".
311///
312/// On Linux the only effect of `is_file` is keeping "." when the result
313/// would otherwise be empty.
314fn normalize_path(path: &[u8], is_file: bool) -> Vec<u8> {
315 if path.is_empty() {
316 return Vec::new();
317 }
318 let is_sep = |c: u8| c == b'/';
319 let mut out: Vec<u8> = Vec::with_capacity(path.len());
320 let mut cur = 0usize;
321 let mut num_seg: i64 = 0;
322
323 if is_sep(path[0]) {
324 cur += 1;
325 out.push(b'/');
326 }
327
328 while cur < path.len() {
329 // Collapse multiple separators.
330 while cur < path.len() && is_sep(path[cur]) {
331 cur += 1;
332 }
333 if cur >= path.len() {
334 break;
335 }
336
337 if path[cur] == b'.' {
338 if cur + 1 >= path.len() {
339 // "." at end of path → ignore.
340 break;
341 } else if is_sep(path[cur + 1]) {
342 // Skip "./".
343 cur += 2;
344 continue;
345 } else if path[cur + 1] == b'.' && (cur + 2 >= path.len() || is_sep(path[cur + 2])) {
346 if num_seg > 0 {
347 // Remove the last segment and its trailing separator:
348 // the C code backs `out` up past the separator, then
349 // past the segment, stopping at the previous '/'.
350 out.pop(); // the separator after the segment
351 while !out.is_empty() && out.last() != Some(&b'/') {
352 out.pop(); // the segment itself
353 }
354 num_seg -= 1;
355 if cur + 2 >= path.len() {
356 break;
357 }
358 cur += 3;
359 continue;
360 } else if path.get(out.len()).copied() == Some(b'/') {
361 // Ignore extraneous ".." in absolute paths.
362 if cur + 2 >= path.len() {
363 break;
364 }
365 cur += 3;
366 continue;
367 } else {
368 // Keep "../" at the start of relative paths.
369 num_seg -= 1;
370 }
371 }
372 }
373
374 // Copy segment.
375 while cur < path.len() && !is_sep(path[cur]) {
376 out.push(path[cur]);
377 cur += 1;
378 }
379 // Copy separator.
380 if cur < path.len() {
381 cur += 1;
382 out.push(b'/');
383 }
384 num_seg += 1;
385 }
386
387 // Keep "." if the output is empty and it's a file.
388 if is_file && out.is_empty() {
389 out.push(b'.');
390 }
391 out
392}
393
394// ── xmlSaveUri-style serialization (upstream escaping rules) ─────────────────
395
396/// `IS_UNRESERVED` from upstream uri.c: ALPHANUM + mark
397/// (`- _ . ! ~ * ' ( )`).
398fn save_unreserved(b: u8) -> bool {
399 b.is_ascii_alphanumeric()
400 || matches!(
401 b,
402 b'-' | b'_' | b'.' | b'!' | b'~' | b'*' | b'\'' | b'(' | b')'
403 )
404}
405
406/// `IS_RESERVED` from upstream uri.c: `; / ? : @ & = + $ , [ ]`.
407fn save_reserved(b: u8) -> bool {
408 matches!(
409 b,
410 b';' | b'/' | b'?' | b':' | b'@' | b'&' | b'=' | b'+' | b'$' | b',' | b'[' | b']'
411 )
412}
413
414fn push_escaped(out: &mut Vec<u8>, b: u8) {
415 const HEX: &[u8; 16] = b"0123456789ABCDEF";
416 out.push(b'%');
417 out.push(HEX[(b >> 4) as usize]);
418 out.push(HEX[(b & 0x0f) as usize]);
419}
420
421/// Escape `s` into `out`, keeping bytes for which `keep` returns true.
422fn save_escape(out: &mut Vec<u8>, s: &[u8], keep: impl Fn(u8) -> bool) {
423 for &b in s {
424 if keep(b) {
425 out.push(b);
426 } else {
427 push_escaped(out, b);
428 }
429 }
430}
431
432/// Serialize parsed URI parts with the exact escaping of upstream
433/// `xmlSaveUri` (uri.c) — used for the "return the URI itself" tail of
434/// `xmlBuildRelativeURISafe`.
435fn save_uri_parts(p: &UriParts) -> Vec<u8> {
436 let mut out = Vec::new();
437
438 if let Some(scheme) = p.scheme.as_deref() {
439 out.extend_from_slice(scheme);
440 out.push(b':');
441 }
442
443 if let Some(opaque) = p.opaque.as_deref() {
444 // Kept: IS_UNRESERVED || IS_RESERVED.
445 save_escape(&mut out, opaque, |b| save_unreserved(b) || save_reserved(b));
446 } else if p.server.is_some() || p.port != 0 {
447 out.extend_from_slice(b"//");
448 if let Some(user) = p.user.as_deref() {
449 // Kept: IS_UNRESERVED || ";:&=+$,."
450 save_escape(&mut out, user, |b| {
451 save_unreserved(b) || matches!(b, b';' | b':' | b'&' | b'=' | b'+' | b'$' | b',')
452 });
453 out.push(b'@');
454 }
455 if let Some(server) = p.server.as_deref() {
456 // The internal representation already includes the port.
457 out.extend_from_slice(server);
458 }
459 } else if let Some(authority) = p.authority.as_deref() {
460 out.extend_from_slice(b"//");
461 // Kept: IS_UNRESERVED || "$,;:@&=+."
462 save_escape(&mut out, authority, |b| {
463 save_unreserved(b) || matches!(b, b'$' | b',' | b';' | b':' | b'@' | b'&' | b'=' | b'+')
464 });
465 }
466
467 if let Some(path) = p.path.as_deref() {
468 // The colon in file:///d: must not be escaped (upstream special
469 // case), or Windows accesses fail later.
470 let mut path = path;
471 if p.scheme.as_deref() == Some(b"file")
472 && path.starts_with(b"/")
473 && path.len() >= 3
474 && path[1].is_ascii_alphabetic()
475 && path[2] == b':'
476 {
477 out.extend_from_slice(&path[..3]);
478 path = &path[3..];
479 }
480 // Kept: IS_UNRESERVED || "/;@&=+$,."
481 save_escape(&mut out, path, |b| {
482 save_unreserved(b) || matches!(b, b'/' | b';' | b'@' | b'&' | b'=' | b'+' | b'$' | b',')
483 });
484 }
485
486 // The internal parser does not track query_raw, so the escaped query
487 // form is used, as upstream does when query_raw is NULL.
488 if let Some(query) = p.query.as_deref() {
489 out.push(b'?');
490 save_escape(&mut out, query, |b| save_unreserved(b) || save_reserved(b));
491 }
492 if let Some(fragment) = p.fragment.as_deref() {
493 out.push(b'#');
494 save_escape(&mut out, fragment, |b| {
495 save_unreserved(b) || save_reserved(b)
496 });
497 }
498
499 out
500}
501
502// ── xmlBuildURI / xmlBuildURISafe ────────────────────────────────────────────
503
504/// Resolve a URI reference against a base URI (safe variant).
505///
506/// # UPSTREAM-PARITY
507///
508/// ```c
509/// int xmlBuildURISafe(const xmlChar *URI, const xmlChar *base, xmlChar **out);
510/// ```
511///
512/// Implements RFC 3986 §5.2 resolution via the internal `resolve_uri` +
513/// `build_uri`, matching upstream `xmlBuildURISafe` (uri.c):
514///
515/// - `out == NULL` → returns 1
516/// - `URI == NULL` → returns 1
517/// - `base == NULL` → `*out` is a copy of `URI` (returns 0)
518/// - a non-empty `URI` must parse as a URI reference, otherwise 1
519/// - an absolute `URI` (with a scheme) is returned unchanged
520/// - a `base` without "://" is treated as a filesystem path
521/// - an invalid `base` with "://" yields `*out` = the URI itself (0)
522/// - an empty `URI` resolves to `base` with the base fragment ignored
523///
524/// Returns 0 on success, 1 if the URI/base is invalid, -1 on allocation
525/// failure. On failure `*out` is left NULL.
526///
527/// # Safety
528///
529/// `URI`/`base` must be valid NUL-terminated strings or NULL; `out` must be
530/// a valid pointer to a writable `xmlChar*`.
531#[no_mangle]
532pub unsafe extern "C" fn xmlBuildURISafe(
533 uri: *const c_char,
534 base: *const c_char,
535 out: *mut *mut xmlChar,
536) -> c_int {
537 unsafe {
538 if out.is_null() {
539 return 1;
540 }
541 *out = ptr::null_mut();
542 if uri.is_null() {
543 return 1;
544 }
545 let uri_bytes = cstr_bytes(uri);
546
547 if base.is_null() {
548 // Upstream: base == NULL → val = xmlStrdup(URI).
549 *out = dup_c_str(uri_bytes);
550 return if (*out).is_null() { -1 } else { 0 };
551 }
552
553 let base_bytes = cstr_bytes(base);
554
555 // Upstream parses the URI first (strictly); an invalid URI fails
556 // regardless of the base.
557 if !uri_bytes.is_empty() && !uri_reference_valid(uri_bytes, false) {
558 return 1;
559 }
560
561 // Base without "://": treated as a filesystem path
562 // (upstream xmlResolvePath) — approximated by the internal resolver.
563 if !base_bytes.windows(3).any(|w| w == b"://") {
564 let resolved = match resolve_uri(base_bytes, uri_bytes) {
565 Some(v) => v,
566 None => return 1,
567 };
568 *out = dup_c_str(&resolved);
569 return if (*out).is_null() { -1 } else { 0 };
570 }
571
572 // Base with "://": full RFC 3986 merge.
573 if uri_bytes.is_empty() {
574 // Empty reference: upstream returns the base with the base
575 // fragment ignored; an invalid base yields 1 with NULL.
576 if !uri_reference_valid(base_bytes, false) {
577 return 1;
578 }
579 let mut base_parts = match parse_uri(base_bytes) {
580 Some(p) => p,
581 None => return 1,
582 };
583 base_parts.fragment = None;
584 let resolved = build_uri(&base_parts);
585 *out = dup_c_str(&resolved);
586 return if (*out).is_null() { -1 } else { 0 };
587 }
588
589 let ref_parts = match parse_uri(uri_bytes) {
590 Some(p) => p,
591 None => return 1,
592 };
593 if ref_parts.scheme.is_some() {
594 // The URI is absolute — don't modify.
595 *out = dup_c_str(uri_bytes);
596 return if (*out).is_null() { -1 } else { 0 };
597 }
598
599 if !uri_reference_valid(base_bytes, false) {
600 // Invalid base: upstream returns the URI itself with ret 0.
601 let saved = build_uri(&ref_parts);
602 *out = dup_c_str(&saved);
603 return if (*out).is_null() { -1 } else { 0 };
604 }
605
606 // RFC 3986 §5.2.2: a reference with an empty path ("?query" or
607 // "#fragment" only) inherits the base path; the base fragment is
608 // ignored and the reference query (or the base query) applies.
609 let resolved = if uri_bytes.starts_with(b"?") || uri_bytes.starts_with(b"#") {
610 let mut base_parts = match parse_uri(base_bytes) {
611 Some(p) => p,
612 None => return 1,
613 };
614 let ref2 = match parse_uri(uri_bytes) {
615 Some(p) => p,
616 None => return 1,
617 };
618 if ref2.query.is_some() {
619 base_parts.query = ref2.query;
620 }
621 base_parts.fragment = ref2.fragment;
622 build_uri(&base_parts)
623 } else {
624 match resolve_uri(base_bytes, uri_bytes) {
625 Some(v) => v,
626 None => return 1,
627 }
628 };
629 *out = dup_c_str(&resolved);
630 if (*out).is_null() {
631 -1
632 } else {
633 0
634 }
635 }
636}
637
638/// Resolve a URI reference against a base URI.
639///
640/// # UPSTREAM-PARITY
641///
642/// ```c
643/// xmlChar *xmlBuildURI(const xmlChar *URI, const xmlChar *base);
644/// ```
645///
646/// Returns the resolved URI (`xmlMalloc`'d, free with `xmlFree`) or NULL on
647/// error. Both arguments may be NULL (then returns NULL); a NULL `base`
648/// yields a copy of `URI`.
649///
650/// # Safety
651///
652/// `URI`/`base` must be valid NUL-terminated strings or NULL.
653#[no_mangle]
654pub unsafe extern "C" fn xmlBuildURI(uri: *const c_char, base: *const c_char) -> *mut xmlChar {
655 let mut out: *mut xmlChar = ptr::null_mut();
656 let ret = unsafe { xmlBuildURISafe(uri, base, &mut out) };
657 if ret != 0 {
658 return ptr::null_mut();
659 }
660 out
661}
662
663// ── xmlBuildRelativeURI / xmlBuildRelativeURISafe ───────────────────────────
664
665/// Compute a relative URI from `URI` to `base` (safe variant).
666///
667/// # UPSTREAM-PARITY
668///
669/// ```c
670/// int xmlBuildRelativeURISafe(const xmlChar *URI, const xmlChar *base, xmlChar **out);
671/// ```
672///
673/// Port of upstream `xmlBuildRelativeURISafe` (uri.c):
674///
675/// - `out == NULL` → returns 1
676/// - `URI == NULL` or empty → returns 1
677/// - if `URI` contains "://" it must parse as a URI reference; otherwise
678/// `*out` is a copy of `URI` (0)
679/// - strings without "://" are treated as filesystem paths (normalized)
680/// - if `base` is NULL/empty/invalid, or the scheme/server/port differ,
681/// `*out` is the URI itself
682/// - identical paths yield the empty string; otherwise the shortest
683/// relative reference (`../` groups plus the unique suffix) is computed
684///
685/// Returns 0 on success, 1 for invalid arguments, -1 on allocation failure.
686/// On failure `*out` is left NULL.
687///
688/// # Safety
689///
690/// `URI`/`base` must be valid NUL-terminated strings or NULL; `out` must be
691/// a valid pointer to a writable `xmlChar*`.
692#[no_mangle]
693pub unsafe extern "C" fn xmlBuildRelativeURISafe(
694 uri: *const c_char,
695 base: *const c_char,
696 out: *mut *mut xmlChar,
697) -> c_int {
698 unsafe {
699 if out.is_null() {
700 return 1;
701 }
702 *out = ptr::null_mut();
703 if uri.is_null() {
704 return 1;
705 }
706 let uri_bytes = cstr_bytes(uri);
707 if uri_bytes.is_empty() {
708 return 1;
709 }
710
711 // Upstream xmlParseUriOrPath(URI, &ref): strings containing "://"
712 // are parsed as URI references (invalid ones are returned as-is);
713 // other strings are normalized filesystem paths.
714 let mut ref_parts = match parse_uri_or_path(uri_bytes) {
715 Ok(p) => p,
716 Err(raw) => {
717 *out = dup_c_str(&raw);
718 return if (*out).is_null() { -1 } else { 0 };
719 }
720 };
721
722 let mut val: *mut xmlChar = ptr::null_mut();
723 let mut ret: c_int = 0;
724
725 // "Return URI if base is empty" (base == NULL || base[0] == 0).
726 let base_empty = base.is_null() || cstr_bytes(base).is_empty();
727 if !base_empty {
728 let base_bytes = cstr_bytes(base);
729 match parse_uri_or_path(base_bytes) {
730 Ok(base_parts) => {
731 if ref_parts.scheme != base_parts.scheme
732 || ref_parts.server != base_parts.server
733 || ref_parts.port != base_parts.port
734 {
735 // Scheme/server/port differ → return the URI.
736 // val stays NULL; the common tail saves ref_parts.
737 } else if ref_parts.path == base_parts.path {
738 // Identical paths → empty relative reference.
739 val = dup_c_str(b"");
740 if val.is_null() {
741 ret = -1;
742 }
743 } else if base_parts.path.is_none() {
744 // Base has no path: the whole ref path is the suffix.
745 val = escape_slice(ref_parts.path.as_deref().unwrap_or(b""), b"/;&=+$,");
746 if val.is_null() {
747 ret = -1;
748 }
749 } else {
750 // ref->path is guaranteed non-NULL from here on
751 // (upstream replaces a NULL path with "/").
752 if ref_parts.path.is_none() {
753 ref_parts.path = Some(b"/".to_vec());
754 }
755 let b = base_parts.path.as_deref().unwrap();
756 let r = ref_parts.path.as_deref().unwrap();
757
758 // "Return URI if URI and base aren't both absolute
759 // or both relative."
760 if (b.first() == Some(&b'/')) != (r.first() == Some(&b'/')) {
761 // val stays NULL → common tail saves ref_parts.
762 } else {
763 // Find the first differing byte.
764 let mut pos = 0usize;
765 while pos < b.len() && pos < r.len() && b[pos] == r[pos] {
766 pos += 1;
767 }
768 if pos == b.len() && pos == r.len() {
769 // Paths are byte-identical → empty reference.
770 val = dup_c_str(b"");
771 if val.is_null() {
772 ret = -1;
773 }
774 } else {
775 // Back up in ref to the last '/' before pos;
776 // uptr is the unique suffix of the ref path.
777 let mut ix = pos;
778 while ix > 0 {
779 if r[ix - 1] == b'/' {
780 break;
781 }
782 ix -= 1;
783 }
784 let uptr = &r[ix..];
785
786 // Count '/' in base starting at the same ix.
787 let mut nbslash = 0usize;
788 let mut i = ix;
789 while i < b.len() {
790 if b[i] == b'/' {
791 nbslash += 1;
792 }
793 i += 1;
794 }
795 let len = uptr.len() + 1;
796
797 if nbslash == 0 && uptr.is_empty() {
798 // e.g. URI="foo/" base="foo/bar" → "./"
799 val = dup_c_str(b"./");
800 if val.is_null() {
801 ret = -1;
802 }
803 } else if nbslash == 0 {
804 val = escape_slice(uptr, b"/;&=+$,");
805 if val.is_null() {
806 ret = -1;
807 }
808 } else {
809 let mut buf = Vec::with_capacity(len + 3 * nbslash);
810 for _ in 0..nbslash {
811 buf.extend_from_slice(b"../");
812 }
813 if !uptr.is_empty() {
814 if buf.last() == Some(&b'/') && uptr[0] == b'/' {
815 // Avoid "../" + "/suffix" doubling
816 // the separator (upstream vptr fix).
817 buf.extend_from_slice(&uptr[1..]);
818 } else {
819 buf.extend_from_slice(uptr);
820 }
821 }
822 val = escape_slice(&buf, b"/;&=+$,");
823 if val.is_null() {
824 ret = -1;
825 } else {
826 ret = 0;
827 }
828 }
829 }
830 }
831 }
832 }
833 Err(_raw) => {
834 // "Return URI if base is invalid": val stays NULL → the
835 // common tail saves ref_parts.
836 }
837 }
838 }
839
840 // done: if ret == 0 && val == NULL → val = xmlSaveUri(ref).
841 if ret == 0 && val.is_null() {
842 let saved = save_uri_parts(&ref_parts);
843 val = dup_c_str(&saved);
844 if val.is_null() {
845 ret = -1;
846 }
847 }
848 if ret != 0 {
849 if !val.is_null() {
850 allocator::xmlFreeImpl(val as *mut c_void);
851 }
852 val = ptr::null_mut();
853 }
854 *out = val;
855 ret
856 }
857}
858
859/// Upstream `xmlParseUriOrPath`: parse `s` as a URI reference when it
860/// contains "://" (returning the raw string on parse failure), otherwise
861/// normalize it as a filesystem path and parse that.
862fn parse_uri_or_path(s: &[u8]) -> Result<UriParts, Vec<u8>> {
863 if s.windows(3).any(|w| w == b"://") {
864 if !uri_reference_valid(s, false) {
865 return Err(s.to_vec());
866 }
867 let mut parts = parse_uri(s).ok_or_else(|| s.to_vec())?;
868 // Upstream xmlParseUriOrPath also normalizes the parsed path
869 // (xmlNormalizePath(uri->path, /* isFile */ 0)).
870 if let Some(path) = parts.path.take() {
871 parts.path = Some(normalize_path(&path, false));
872 }
873 Ok(parts)
874 } else {
875 let norm = normalize_path(s, true);
876 parse_uri(&norm).ok_or_else(|| s.to_vec())
877 }
878}
879
880/// Compute a relative URI from `URI` to `base`.
881///
882/// # UPSTREAM-PARITY
883///
884/// ```c
885/// xmlChar *xmlBuildRelativeURI(const xmlChar *URI, const xmlChar *base);
886/// ```
887///
888/// Returns the relative URI (`xmlMalloc`'d, free with `xmlFree`) or NULL if
889/// not possible.
890///
891/// # Safety
892///
893/// `URI`/`base` must be valid NUL-terminated strings or NULL.
894#[no_mangle]
895pub unsafe extern "C" fn xmlBuildRelativeURI(
896 uri: *const c_char,
897 base: *const c_char,
898) -> *mut xmlChar {
899 let mut out: *mut xmlChar = ptr::null_mut();
900 let ret = unsafe { xmlBuildRelativeURISafe(uri, base, &mut out) };
901 if ret != 0 {
902 return ptr::null_mut();
903 }
904 out
905}
906
907// ── xmlCanonicPath / xmlPathToURI ────────────────────────────────────────────
908
909/// Prepares a path: if it contains "://" it is treated as a Legacy Extended
910/// IRI and every character not allowed in URIs is escaped; otherwise the
911/// path is copied unmodified.
912///
913/// # UPSTREAM-PARITY
914///
915/// ```c
916/// xmlChar *xmlCanonicPath(const xmlChar *path);
917/// ```
918///
919/// Returns NULL if `path` is NULL.
920///
921/// # Safety
922///
923/// `path` must be a valid NUL-terminated string or NULL.
924#[no_mangle]
925pub unsafe extern "C" fn xmlCanonicPath(path: *const c_char) -> *mut xmlChar {
926 if path.is_null() {
927 return ptr::null_mut();
928 }
929 let bytes = cstr_bytes(path);
930 if bytes.windows(3).any(|w| w == b"://") {
931 // "Absolute uri": escape everything except reserved, unreserved
932 // and the percent sign (upstream xmlCanonicPath).
933 unsafe { escape_slice(bytes, b":/?#[]@!$&()*+,;='%") }
934 } else {
935 unsafe { dup_c_str(bytes) }
936 }
937}
938
939/// Construct a URI expressing the existing path.
940///
941/// # UPSTREAM-PARITY
942///
943/// ```c
944/// xmlChar *xmlPathToURI(const xmlChar *path);
945/// ```
946///
947/// Upstream `xmlPathToURI` is a thin wrapper around `xmlCanonicPath`
948/// (2.15.x uri.c), so this returns the same canonicalized path.
949///
950/// # Safety
951///
952/// `path` must be a valid NUL-terminated string or NULL.
953#[no_mangle]
954pub unsafe extern "C" fn xmlPathToURI(path: *const c_char) -> *mut xmlChar {
955 unsafe { xmlCanonicPath(path) }
956}
957
958// ── xmlPrintURI ──────────────────────────────────────────────────────────────
959
960/// Print the URI string to a `FILE*` stream.
961///
962/// # UPSTREAM-PARITY
963///
964/// ```c
965/// void xmlPrintURI(FILE *stream, xmlURI *uri);
966/// ```
967///
968/// Serializes `uri` with `xmlSaveUri` and writes it to `stream` with
969/// `fwrite` (the upstream equivalent of `fprintf(stream, "%s", out)`).
970/// A NULL `uri` or NULL `stream` is a silent no-op.
971///
972/// # Safety
973///
974/// `stream` must be a valid open `FILE*` (or NULL); `uri` must be a valid
975/// `xmlURI` from `xmlParseURI`/`xmlCreateURI` (or NULL).
976#[no_mangle]
977pub unsafe extern "C" fn xmlPrintURI(stream: *mut c_void, uri: *mut c_void) {
978 if stream.is_null() {
979 return;
980 }
981 let out = unsafe { crate::xml::uri::xmlSaveUri(uri) };
982 if out.is_null() {
983 return;
984 }
985 let len = unsafe { libc::strlen(out as *const c_char) };
986 unsafe {
987 fwrite(out as *const c_void, 1, len, stream);
988 allocator::xmlFreeImpl(out as *mut c_void);
989 }
990}
991
992// ── xmlURIEscape ─────────────────────────────────────────────────────────────
993
994/// Escape a URI string per RFC 2396 (deprecated upstream).
995///
996/// # UPSTREAM-PARITY
997///
998/// ```c
999/// xmlChar *xmlURIEscape(const xmlChar *str);
1000/// ```
1001///
1002/// Port of upstream `xmlURIEscape` (uri.c): the string is parsed with
1003/// `XML_URI_ALLOW_UNWISE` (so "unwise" characters are permitted) and each
1004/// component is re-escaped with its component-specific safe list. Strings
1005/// that don't parse as a URI reference (e.g. containing a space) yield NULL.
1006///
1007/// # Safety
1008///
1009/// `str` must be a valid NUL-terminated string or NULL.
1010#[no_mangle]
1011pub unsafe extern "C" fn xmlURIEscape(str: *const c_char) -> *mut xmlChar {
1012 if str.is_null() {
1013 return ptr::null_mut();
1014 }
1015 let bytes = cstr_bytes(str);
1016 if !uri_reference_valid(bytes, true) {
1017 return ptr::null_mut();
1018 }
1019 let parts = match parse_uri(bytes) {
1020 Some(p) => p,
1021 None => return ptr::null_mut(),
1022 };
1023
1024 let mut result: Vec<u8> = Vec::new();
1025
1026 // Scheme (upstream safe list "+-.").
1027 if let Some(ref scheme) = parts.scheme {
1028 let esc = unsafe { escape_slice(scheme, b"+-.") };
1029 if esc.is_null() {
1030 return ptr::null_mut();
1031 }
1032 result.extend_from_slice(unsafe { cstr_bytes(esc as *const c_char) });
1033 result.push(b':');
1034 unsafe { allocator::xmlFreeImpl(esc as *mut c_void) };
1035 }
1036
1037 // Note: the C struct's `authority` and `opaque` fields are never set by
1038 // the parser (upstream xmlParse3986HierPart), so those blocks of the
1039 // upstream xmlURIEscape are dead code for parsed URIs and are not
1040 // emitted here. `scheme:rest` parses as a rootless path upstream.
1041
1042 // User info.
1043 if let Some(ref user) = parts.user {
1044 let esc = unsafe { escape_slice(user, b";:&=+$,") };
1045 if esc.is_null() {
1046 return ptr::null_mut();
1047 }
1048 result.extend_from_slice(b"//");
1049 result.extend_from_slice(unsafe { cstr_bytes(esc as *const c_char) });
1050 result.push(b'@');
1051 unsafe { allocator::xmlFreeImpl(esc as *mut c_void) };
1052 }
1053
1054 // Server (host part only; the port is emitted separately below).
1055 if let Some(ref host) = parts.host {
1056 let esc = unsafe { escape_slice(host, b"/?;:@") };
1057 if esc.is_null() {
1058 return ptr::null_mut();
1059 }
1060 if parts.user.is_none() {
1061 result.extend_from_slice(b"//");
1062 }
1063 result.extend_from_slice(unsafe { cstr_bytes(esc as *const c_char) });
1064 unsafe { allocator::xmlFreeImpl(esc as *mut c_void) };
1065 }
1066
1067 // Port.
1068 if parts.port > 0 {
1069 result.push(b':');
1070 result.extend_from_slice(format!("{}", parts.port).as_bytes());
1071 }
1072
1073 // Path.
1074 if let Some(ref path) = parts.path {
1075 let esc = unsafe { escape_slice(path, b":@&=+$,/?;") };
1076 if esc.is_null() {
1077 return ptr::null_mut();
1078 }
1079 result.extend_from_slice(unsafe { cstr_bytes(esc as *const c_char) });
1080 unsafe { allocator::xmlFreeImpl(esc as *mut c_void) };
1081 }
1082
1083 // Query. (The internal parser does not track query_raw, so the escaped
1084 // form is used, as upstream does when query_raw is NULL.)
1085 if let Some(ref query) = parts.query {
1086 let esc = unsafe { escape_slice(query, b";/?:@&=+,$") };
1087 if esc.is_null() {
1088 return ptr::null_mut();
1089 }
1090 result.push(b'?');
1091 result.extend_from_slice(unsafe { cstr_bytes(esc as *const c_char) });
1092 unsafe { allocator::xmlFreeImpl(esc as *mut c_void) };
1093 } else if bytes.ends_with(b"?") {
1094 // The internal parser drops an empty query; upstream keeps the "?".
1095 result.push(b'?');
1096 }
1097
1098 // Fragment.
1099 if let Some(ref fragment) = parts.fragment {
1100 let esc = unsafe { escape_slice(fragment, b"#") };
1101 if esc.is_null() {
1102 return ptr::null_mut();
1103 }
1104 result.push(b'#');
1105 result.extend_from_slice(unsafe { cstr_bytes(esc as *const c_char) });
1106 unsafe { allocator::xmlFreeImpl(esc as *mut c_void) };
1107 }
1108
1109 unsafe { dup_c_str(&result) }
1110}
1111
1112// ── xmlNormalizeWindowsPath ──────────────────────────────────────────────────
1113
1114/// Normalize a Windows path.
1115///
1116/// # UPSTREAM-PARITY
1117///
1118/// ```c
1119/// xmlChar *xmlNormalizeWindowsPath(const xmlChar *path);
1120/// ```
1121///
1122/// Upstream libxml2 (2.15.x, xmlIO.c) marks this function deprecated —
1123/// "This never really worked" — and simply returns a copy of `path`.
1124/// Returns NULL if `path` is NULL.
1125///
1126/// # Safety
1127///
1128/// `path` must be a valid NUL-terminated string or NULL.
1129#[no_mangle]
1130pub unsafe extern "C" fn xmlNormalizeWindowsPath(path: *const c_char) -> *mut xmlChar {
1131 if path.is_null() {
1132 return ptr::null_mut();
1133 }
1134 unsafe { dup_c_str(cstr_bytes(path)) }
1135}
1136
1137// ── xmlCheckLanguageID ───────────────────────────────────────────────────────
1138
1139/// Check whether a string is a valid language ID per RFC 3066.
1140///
1141/// # UPSTREAM-PARITY
1142///
1143/// ```c
1144/// int xmlCheckLanguageID(const xmlChar *lang);
1145/// ```
1146///
1147/// Faithful port of upstream `xmlCheckLanguageID` (parser.c): returns 1 for
1148/// valid tags (including the deprecated IANA/user "i-*" and "x-*" forms),
1149/// 0 otherwise, 0 for NULL.
1150///
1151/// # Safety
1152///
1153/// `lang` must be a valid NUL-terminated string or NULL.
1154#[no_mangle]
1155pub unsafe extern "C" fn xmlCheckLanguageID(lang: *const c_char) -> c_int {
1156 if lang.is_null() {
1157 return 0;
1158 }
1159 check_language_id(unsafe { cstr_bytes(lang) })
1160}
1161
1162fn lang_is_alpha(c: u8) -> bool {
1163 c.is_ascii_alphabetic()
1164}
1165
1166fn lang_is_digit(c: u8) -> bool {
1167 c.is_ascii_digit()
1168}
1169
1170/// Byte at `i`, or 0 past the end (mirrors reading the C NUL terminator).
1171fn lang_byte(s: &[u8], i: usize) -> u8 {
1172 s.get(i).copied().unwrap_or(0)
1173}
1174
1175/// The "variant" label of upstream xmlCheckLanguageID: `nxt` is just past
1176/// the variant subtag. Extensions and private-use subtags are not checked.
1177fn lang_variant(s: &[u8], nxt: usize) -> c_int {
1178 match lang_byte(s, nxt) {
1179 0 => 1,
1180 b'-' => 1,
1181 _ => 0,
1182 }
1183}
1184
1185/// The "region" label: `nxt` is just past the region subtag.
1186fn lang_region(s: &[u8], nxt: usize) -> c_int {
1187 match lang_byte(s, nxt) {
1188 0 => return 1,
1189 b'-' => {}
1190 _ => return 0,
1191 }
1192 let mut nxt = nxt + 1;
1193 let cur = nxt;
1194 while lang_is_alpha(lang_byte(s, nxt)) {
1195 nxt += 1;
1196 }
1197 if !(5..=8).contains(&(nxt - cur)) {
1198 return 0;
1199 }
1200 lang_variant(s, nxt)
1201}
1202
1203/// The "region_m49" label: `nxt` points at the first digit.
1204fn lang_region_m49(s: &[u8], nxt: usize) -> c_int {
1205 if lang_is_digit(lang_byte(s, nxt + 1)) && lang_is_digit(lang_byte(s, nxt + 2)) {
1206 lang_region(s, nxt + 3)
1207 } else {
1208 0
1209 }
1210}
1211
1212/// The "script" label: `nxt` is just past the script subtag.
1213fn lang_script(s: &[u8], nxt: usize) -> c_int {
1214 match lang_byte(s, nxt) {
1215 0 => return 1,
1216 b'-' => {}
1217 _ => return 0,
1218 }
1219 let mut nxt = nxt + 1;
1220 let cur = nxt;
1221 if lang_is_digit(lang_byte(s, nxt)) {
1222 return lang_region_m49(s, nxt);
1223 }
1224 while lang_is_alpha(lang_byte(s, nxt)) {
1225 nxt += 1;
1226 }
1227 let len = nxt - cur;
1228 if (5..=8).contains(&len) {
1229 return lang_variant(s, nxt);
1230 }
1231 if len != 2 {
1232 return 0;
1233 }
1234 lang_region(s, nxt)
1235}
1236
1237/// Port of upstream `xmlCheckLanguageID` (parser.c) over a byte slice.
1238fn check_language_id(s: &[u8]) -> c_int {
1239 // Deprecated IANA/user codes: "i-...", "I-...", "x-...", "X-...".
1240 let c0 = lang_byte(s, 0);
1241 let c1 = lang_byte(s, 1);
1242 if (c0 == b'i' || c0 == b'I' || c0 == b'x' || c0 == b'X') && c1 == b'-' {
1243 let mut cur = 2usize;
1244 while lang_is_alpha(lang_byte(s, cur)) {
1245 cur += 1;
1246 }
1247 return if lang_byte(s, cur) == 0 { 1 } else { 0 };
1248 }
1249
1250 // Primary language subtag.
1251 let mut nxt = 0usize;
1252 while lang_is_alpha(lang_byte(s, nxt)) {
1253 nxt += 1;
1254 }
1255 let primary_len = nxt;
1256 if primary_len >= 4 {
1257 // Reserved language codes: 4..=8 chars and must end the tag.
1258 if primary_len > 8 || lang_byte(s, nxt) != 0 {
1259 return 0;
1260 }
1261 return 1;
1262 }
1263 if primary_len < 2 {
1264 return 0;
1265 }
1266 // We got an ISO 639 code.
1267 match lang_byte(s, nxt) {
1268 0 => return 1,
1269 b'-' => {}
1270 _ => return 0,
1271 }
1272 nxt += 1;
1273 let cur = nxt;
1274
1275 // Next subtag: extlang / script / region / variant.
1276 if lang_is_digit(lang_byte(s, nxt)) {
1277 return lang_region_m49(s, nxt);
1278 }
1279 while lang_is_alpha(lang_byte(s, nxt)) {
1280 nxt += 1;
1281 }
1282 let len = nxt - cur;
1283 if len == 4 {
1284 return lang_script(s, nxt);
1285 }
1286 if len == 2 {
1287 return lang_region(s, nxt);
1288 }
1289 if (5..=8).contains(&len) {
1290 return lang_variant(s, nxt);
1291 }
1292 if len != 3 {
1293 return 0;
1294 }
1295 // We parsed an extlang.
1296 match lang_byte(s, nxt) {
1297 0 => return 1,
1298 b'-' => {}
1299 _ => return 0,
1300 }
1301 nxt += 1;
1302 let cur = nxt;
1303
1304 // Now script or region or variant.
1305 if lang_is_digit(lang_byte(s, nxt)) {
1306 return lang_region_m49(s, nxt);
1307 }
1308 while lang_is_alpha(lang_byte(s, nxt)) {
1309 nxt += 1;
1310 }
1311 let len = nxt - cur;
1312 if len == 2 {
1313 return lang_region(s, nxt);
1314 }
1315 if (5..=8).contains(&len) {
1316 return lang_variant(s, nxt);
1317 }
1318 if len != 4 {
1319 return 0;
1320 }
1321 // We parsed a script → falls into the "script" handling.
1322 lang_script(s, nxt)
1323}
1324
1325// ── xmlParseURISafe ──────────────────────────────────────────────────────────
1326
1327/// Parse a URI reference (safe variant).
1328///
1329/// # UPSTREAM-PARITY
1330///
1331/// ```c
1332/// int xmlParseURISafe(const char *str, xmlURI **uri);
1333/// ```
1334///
1335/// Returns 0 on success with `*uri` set to a newly created `xmlURI` (free
1336/// with `xmlFreeURI`), 1 if `str` is NULL/invalid or `uri` is NULL, -1 on
1337/// allocation failure. On failure `*uri` is left NULL. An empty string is
1338/// a valid (empty) URI reference.
1339///
1340/// # Safety
1341///
1342/// `str` must be a valid NUL-terminated string or NULL; `uri` must be a
1343/// valid pointer to a writable `xmlURI*`.
1344#[no_mangle]
1345pub unsafe extern "C" fn xmlParseURISafe(str: *const c_char, uri_out: *mut *mut c_void) -> c_int {
1346 unsafe {
1347 if uri_out.is_null() {
1348 return 1;
1349 }
1350 *uri_out = ptr::null_mut();
1351 if str.is_null() {
1352 return 1;
1353 }
1354 let bytes = cstr_bytes(str);
1355 if bytes.is_empty() {
1356 // The empty reference parses to an empty xmlURI upstream.
1357 *uri_out = crate::xml::uri::xmlCreateURI();
1358 return if (*uri_out).is_null() { -1 } else { 0 };
1359 }
1360 if !uri_reference_valid(bytes, false) {
1361 return 1;
1362 }
1363 let parsed = crate::xml::uri::xmlParseURI(str);
1364 if parsed.is_null() {
1365 return 1;
1366 }
1367 *uri_out = parsed;
1368 0
1369 }
1370}