Skip to main content

xml_sec/sxd_document/
lib.rs

1//!
2//! ```
3//! # #[cfg(not(feature = "embedded"))]
4//! # {
5//! use sxd_document_no_unsafe::Package;
6//! let package = Package::new();
7//! let doc = package.as_document();
8//!
9//! let hello = doc.create_element("hello");
10//! hello.set_attribute_value("planet", "Earth");
11//! let comment = doc.create_comment("What about other planets?");
12//! let text = doc.create_text("Greetings, Earthlings!");
13//!
14//! hello.append_child(comment);
15//! hello.append_child(text);
16//! doc.root().append_child(hello);
17//! # }
18//! ```
19//!
20//! ### Memory and ownership
21//!
22//! The `Package` struct is responsible for owning every node in the
23//! document. Strings are interned, allowing repeated text to consume
24//! less memory. This is very useful for documents containing lots of
25//! the same attributes and tag names.
26//!
27//! The flip side of this decision is that allocated nodes and strings
28//! are not deallocated until the entire `Package` is dropped. This is
29//! a reasonable decision for two common cases: building up an XML
30//! document and reading an XML document. You may wish to perform
31//! large modifications to your data *before* creating a document.
32//!
33//! ### Namespaces, QNames, and Prefixes
34//!
35//! The names of elements and attributes may use namespaces. XML
36//! namespacing uses URIs to uniquely distinguish items with the same
37//! local name. A qualified name (`QName`) combines this optional URI
38//! with the local name.
39//!
40//! When an XML document is represented as text, namespaces are given
41//! a shorthand reference known as a prefix. Prefix names are
42//! non-authoritative, and only the URI can be used to namespace a
43//! name.
44//!
45//! Elements and attributes may specify a *preferred prefix*, which is
46//! an indication of what the user would like to be used as a
47//! prefix. There are times where the preferred prefix would cause a
48//! conflict, and so an autogenerated prefix will be used instead.
49//!
50//! ### Design decisions
51//!
52//! Try to leverage the type system as much as possible.
53
54#![deny(rust_2018_idioms)]
55
56#[cfg(not(any(feature = "no-unsafe", feature = "raw-pointer-backend")))]
57compile_error!("select either `no-unsafe` or `raw-pointer-backend`");
58
59// Cargo's all-feature verification enables both selectors. Safe precedence keeps that profile
60// free of raw pointers; selecting the legacy backend requires disabling default features.
61
62#[cfg(not(feature = "embedded"))]
63#[macro_use]
64extern crate peresil;
65
66use std::{
67    fmt,
68    sync::atomic::{AtomicUsize, Ordering},
69};
70
71static NEXT_PACKAGE_IDENTITY: AtomicUsize = AtomicUsize::new(1);
72
73fn next_package_identity() -> usize {
74    allocate_package_identity(&NEXT_PACKAGE_IDENTITY)
75}
76
77fn allocate_package_identity(counter: &AtomicUsize) -> usize {
78    let mut current = counter.load(Ordering::Relaxed);
79    loop {
80        let next = current
81            .checked_add(1)
82            .expect("document identity space exhausted");
83        match counter.compare_exchange_weak(current, next, Ordering::Relaxed, Ordering::Relaxed) {
84            Ok(_) => return current,
85            Err(observed) => current = observed,
86        }
87    }
88}
89
90#[cfg(test)]
91mod identity_tests {
92    use super::*;
93
94    #[test]
95    fn exhausted_identity_space_does_not_wrap() {
96        let counter = AtomicUsize::new(usize::MAX);
97        let result = std::panic::catch_unwind(|| allocate_package_identity(&counter));
98        assert!(result.is_err());
99        assert_eq!(counter.load(Ordering::Relaxed), usize::MAX);
100    }
101
102    #[test]
103    fn concurrent_allocations_remain_unique() {
104        let counter = AtomicUsize::new(1);
105        let identities = std::thread::scope(|scope| {
106            let workers: Vec<_> = (0..8)
107                .map(|_| {
108                    scope.spawn(|| {
109                        (0..128)
110                            .map(|_| allocate_package_identity(&counter))
111                            .collect::<Vec<_>>()
112                    })
113                })
114                .collect();
115            workers
116                .into_iter()
117                .flat_map(|worker| worker.join().unwrap())
118                .collect::<Vec<_>>()
119        });
120        let unique: std::collections::HashSet<_> = identities.iter().copied().collect();
121        assert_eq!(unique.len(), 8 * 128);
122        assert_eq!(counter.load(Ordering::Relaxed), 1 + 8 * 128);
123    }
124}
125
126/// Node and edge counts used to reserve storage before projecting into this DOM.
127#[derive(Debug, Clone, Copy, Default)]
128pub struct StorageRequirements {
129    pub roots: usize,
130    pub elements: usize,
131    pub attributes: usize,
132    pub texts: usize,
133    pub comments: usize,
134    pub processing_instructions: usize,
135    pub child_edges: usize,
136    pub namespace_bindings: usize,
137    pub interned_strings: usize,
138    pub string_bytes: usize,
139}
140
141/// Conservative heap-storage estimate for one projected document.
142#[must_use]
143pub fn estimated_storage_bytes(requirements: StorageRequirements) -> usize {
144    raw::estimated_storage_bytes(requirements)
145}
146
147mod lazy_hash_map;
148#[cfg(feature = "no-unsafe")]
149mod namespace_storage;
150pub(crate) mod str;
151mod str_ext;
152
153#[cfg(not(feature = "no-unsafe"))]
154pub mod dom;
155#[cfg(not(feature = "no-unsafe"))]
156mod raw;
157#[cfg(not(feature = "no-unsafe"))]
158pub(crate) mod string_pool;
159#[cfg(not(feature = "no-unsafe"))]
160#[doc(hidden)]
161pub mod thindom;
162#[cfg(not(feature = "no-unsafe"))]
163pub mod writer;
164
165#[cfg(feature = "no-unsafe")]
166#[path = "raw_no_unsafe.rs"]
167mod raw;
168#[cfg(feature = "no-unsafe")]
169mod storage;
170#[cfg(feature = "no-unsafe")]
171#[path = "string_pool_no_unsafe.rs"]
172pub(crate) mod string_pool;
173#[cfg(feature = "no-unsafe")]
174pub use string_pool::{AccountedStringPool, InternedString};
175#[cfg(feature = "no-unsafe")]
176#[path = "dom_no_unsafe.rs"]
177pub mod dom;
178#[cfg(feature = "no-unsafe")]
179#[path = "thindom_no_unsafe.rs"]
180#[doc(hidden)]
181pub mod thindom;
182#[cfg(feature = "no-unsafe")]
183#[path = "writer_no_unsafe.rs"]
184pub mod writer;
185
186pub mod parser;
187
188#[cfg(not(feature = "no-unsafe"))]
189#[cfg(feature = "__internal_expose_string_pool")]
190pub mod __internal {
191    pub use super::string_pool::StringPool;
192}
193
194#[cfg(feature = "no-unsafe")]
195#[cfg(feature = "__internal_expose_string_pool")]
196pub mod __internal {
197    pub use super::string_pool::StringPool;
198}
199
200pub use crate::str::XmlChar;
201#[cfg(feature = "embedded")]
202pub use crate::{as_opt_str, as_qname, as_str, to_ns_str};
203
204#[cfg(not(feature = "no-unsafe"))]
205#[macro_export]
206macro_rules! as_str {
207    ($e:expr) => {
208        $e
209    };
210}
211
212#[cfg(not(feature = "no-unsafe"))]
213#[macro_export]
214macro_rules! as_opt_str {
215    ($e:expr) => {
216        $e
217    };
218}
219
220#[cfg(not(feature = "no-unsafe"))]
221#[macro_export]
222macro_rules! as_qname {
223    ($e:expr) => {
224        $e
225    };
226}
227
228#[cfg(feature = "no-unsafe")]
229#[macro_export]
230macro_rules! as_str {
231    ($e:expr) => {
232        &*$e
233    };
234}
235
236#[cfg(feature = "no-unsafe")]
237#[macro_export]
238macro_rules! as_opt_str {
239    ($e:expr) => {
240        $e.as_deref()
241    };
242}
243
244#[cfg(feature = "no-unsafe")]
245#[macro_export]
246macro_rules! as_qname {
247    ($e:expr) => {
248        $e.get()
249    };
250}
251
252#[cfg(not(feature = "no-unsafe"))]
253#[macro_export]
254macro_rules! to_ns_str {
255    ($e:expr) => {
256        $e
257    };
258}
259
260#[cfg(feature = "no-unsafe")]
261#[macro_export]
262macro_rules! to_ns_str {
263    ($e:expr) => {
264        ($e).to_owned().into()
265    };
266}
267
268/// String type used for namespace prefix/URI storage.
269/// `&'d str` in default mode, `String` in no-unsafe mode.
270#[cfg(not(feature = "no-unsafe"))]
271pub type NsStr<'d> = &'d str;
272/// String type used for namespace prefix/URI storage.
273/// `&'d str` in default mode, `String` in no-unsafe mode.
274#[cfg(all(feature = "no-unsafe", not(feature = "embedded")))]
275///
276/// The lifetime is intentionally retained so cfg-independent generic code has one public type
277/// signature; owned storage does not borrow from `'d` in this mode.
278pub type NsStr<'d> = String;
279
280#[cfg(feature = "embedded")]
281pub type NsStr<'d> = NamespaceText;
282
283/// Namespace payloads preserve the execution owner when identity handles clone.
284#[cfg(feature = "embedded")]
285#[derive(Debug, Clone)]
286pub enum NamespaceText {
287    Native(String),
288    Current(structured_asn1::memory::SharedString),
289    Static(&'static str),
290}
291
292#[cfg(feature = "embedded")]
293impl NamespaceText {
294    pub(crate) fn can_share(&self) -> bool {
295        matches!(self, Self::Current(_) | Self::Static(_))
296    }
297}
298#[cfg(feature = "embedded")]
299impl From<String> for NamespaceText {
300    fn from(value: String) -> Self {
301        Self::Native(value)
302    }
303}
304#[cfg(feature = "embedded")]
305impl From<&str> for NamespaceText {
306    fn from(value: &str) -> Self {
307        Self::Native(value.to_owned())
308    }
309}
310#[cfg(feature = "embedded")]
311impl std::ops::Deref for NamespaceText {
312    type Target = str;
313    fn deref(&self) -> &str {
314        match self {
315            Self::Native(s) => s,
316            Self::Current(s) => s,
317            Self::Static(s) => s,
318        }
319    }
320}
321#[cfg(feature = "embedded")]
322impl AsRef<str> for NamespaceText {
323    fn as_ref(&self) -> &str {
324        self
325    }
326}
327#[cfg(feature = "embedded")]
328impl PartialEq for NamespaceText {
329    fn eq(&self, other: &Self) -> bool {
330        **self == **other
331    }
332}
333#[cfg(feature = "embedded")]
334impl Eq for NamespaceText {}
335#[cfg(feature = "embedded")]
336impl std::borrow::Borrow<str> for NamespaceText {
337    fn borrow(&self) -> &str {
338        self
339    }
340}
341#[cfg(feature = "embedded")]
342impl structured_asn1::memory::HashWork for NamespaceText {
343    fn hash_work(&self) -> Result<u64, structured_asn1::memory::MemoryError> {
344        structured_asn1::memory::HashWork::hash_work(&**self)
345    }
346}
347#[cfg(feature = "embedded")]
348impl std::hash::Hash for NamespaceText {
349    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
350        std::hash::Hash::hash(&**self, state);
351    }
352}
353
354pub(crate) static XML_NS_PREFIX: &str = "xml";
355pub(crate) static XML_NS_URI: &str = "http://www.w3.org/XML/1998/namespace";
356
357/// A prefixed name. This represents what is found in the string form
358/// of an XML document, and does not apply any namespace mapping.
359#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
360pub struct PrefixedName<'a> {
361    prefix: Option<&'a str>,
362    local_part: &'a str,
363}
364
365impl<'a> PrefixedName<'a> {
366    /// Create a `PrefixedName` without a prefix
367    pub fn new(local_part: &str) -> PrefixedName<'_> {
368        PrefixedName::with_prefix(None, local_part)
369    }
370
371    /// Create a `PrefixedName` without an optional prefix
372    pub fn with_prefix(prefix: Option<&'a str>, local_part: &'a str) -> PrefixedName<'a> {
373        PrefixedName { prefix, local_part }
374    }
375
376    /// Borrows the original source, independently of this copied name view.
377    pub fn prefix(&self) -> Option<&'a str> {
378        self.prefix
379    }
380    /// Borrows the original source, independently of this copied name view.
381    pub fn local_part(&self) -> &'a str {
382        self.local_part
383    }
384}
385
386/// A namespace-qualified name. This represents the name of an element
387/// or attribute *after* the prefix has been mapped to a specific
388/// namespace.
389#[derive(Debug, Copy, Clone, PartialEq)]
390pub struct QName<'s> {
391    namespace_uri: Option<&'s str>,
392    local_part: &'s str,
393}
394
395impl<'s> QName<'s> {
396    /// Create a `QName` without a namespace
397    pub fn new(local_part: &'s str) -> QName<'s> {
398        QName::with_namespace_uri(None, local_part)
399    }
400
401    /// Create a `QName` with an optional namespace
402    pub fn with_namespace_uri(namespace_uri: Option<&'s str>, local_part: &'s str) -> QName<'s> {
403        QName {
404            namespace_uri,
405            local_part,
406        }
407    }
408
409    pub fn namespace_uri(&self) -> Option<&'s str> {
410        self.namespace_uri
411    }
412    pub fn local_part(&self) -> &'s str {
413        self.local_part
414    }
415}
416
417impl<'s> From<(&'s str, &'s str)> for QName<'s> {
418    fn from(v: (&'s str, &'s str)) -> QName<'s> {
419        QName {
420            namespace_uri: Some(v.0),
421            local_part: v.1,
422        }
423    }
424}
425
426impl<'s> From<&'s str> for QName<'s> {
427    fn from(v: &'s str) -> QName<'s> {
428        QName {
429            namespace_uri: None,
430            local_part: v,
431        }
432    }
433}
434
435/// The main entrypoint to an XML document
436///
437/// This is an opaque structure that stores the internal details of
438/// the XML document. Modify the document via `as_document`.
439pub struct Package {
440    identity: usize,
441    storage: raw::Storage,
442    connections: raw::Connections,
443}
444
445impl Default for Package {
446    fn default() -> Package {
447        let s = raw::Storage::new();
448        let root = s.create_root();
449        Package {
450            identity: next_package_identity(),
451            storage: s,
452            connections: raw::Connections::new(root),
453        }
454    }
455}
456
457impl Package {
458    pub fn new() -> Package {
459        Self::default()
460    }
461
462    #[cfg(feature = "embedded")]
463    pub(crate) fn try_current(
464        work: &impl structured_asn1::memory::Work,
465    ) -> Result<Self, structured_asn1::memory::MemoryError> {
466        let (storage, root) = raw::current::storage(work)?;
467        Ok(Self {
468            identity: next_package_identity(),
469            storage,
470            connections: raw::Connections::new(root),
471        })
472    }
473
474    pub fn as_document(&self) -> dom::Document<'_> {
475        dom::Document::new(self.identity, &self.storage, &self.connections)
476    }
477
478    #[doc(hidden)]
479    pub fn as_thin_document(&self) -> (thindom::Storage<'_>, thindom::Connections<'_>) {
480        // The returned handles borrow this Package, so its address is a stable runtime brand
481        // for rejecting nodes created by a different arena before index or pointer access.
482        let identity = std::ptr::from_ref(self).addr();
483        let s = thindom::Storage::new(&self.storage, identity);
484        #[cfg(not(feature = "no-unsafe"))]
485        let c = thindom::Connections::new(&self.connections, identity);
486        #[cfg(feature = "no-unsafe")]
487        let c = thindom::Connections::new(&self.connections, &self.storage, identity);
488        (s, c)
489    }
490}
491
492impl PartialEq for Package {
493    fn eq(&self, other: &Package) -> bool {
494        std::ptr::eq(self, other)
495    }
496}
497
498impl fmt::Debug for Package {
499    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
500        write!(f, "Package")
501    }
502}