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