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;
148pub(crate) mod str;
149mod str_ext;
150
151#[cfg(not(feature = "no-unsafe"))]
152pub mod dom;
153#[cfg(not(feature = "no-unsafe"))]
154mod raw;
155#[cfg(not(feature = "no-unsafe"))]
156pub(crate) mod string_pool;
157#[cfg(not(feature = "no-unsafe"))]
158#[doc(hidden)]
159pub mod thindom;
160#[cfg(not(feature = "no-unsafe"))]
161pub mod writer;
162
163#[cfg(feature = "no-unsafe")]
164#[path = "raw_no_unsafe.rs"]
165mod raw;
166#[cfg(feature = "no-unsafe")]
167#[path = "string_pool_no_unsafe.rs"]
168pub(crate) mod string_pool;
169#[cfg(feature = "no-unsafe")]
170pub use string_pool::InternedString;
171#[cfg(feature = "no-unsafe")]
172#[path = "dom_no_unsafe.rs"]
173pub mod dom;
174#[cfg(feature = "no-unsafe")]
175#[path = "thindom_no_unsafe.rs"]
176#[doc(hidden)]
177pub mod thindom;
178#[cfg(feature = "no-unsafe")]
179#[path = "writer_no_unsafe.rs"]
180pub mod writer;
181
182pub mod parser;
183
184#[cfg(not(feature = "no-unsafe"))]
185#[cfg(feature = "__internal_expose_string_pool")]
186pub mod __internal {
187    pub use super::string_pool::StringPool;
188}
189
190#[cfg(feature = "no-unsafe")]
191#[cfg(feature = "__internal_expose_string_pool")]
192pub mod __internal {
193    pub use super::string_pool::StringPool;
194}
195
196pub use crate::str::XmlChar;
197#[cfg(feature = "embedded")]
198pub use crate::{as_opt_str, as_qname, as_str, to_ns_str};
199
200#[cfg(not(feature = "no-unsafe"))]
201#[macro_export]
202macro_rules! as_str {
203    ($e:expr) => {
204        $e
205    };
206}
207
208#[cfg(not(feature = "no-unsafe"))]
209#[macro_export]
210macro_rules! as_opt_str {
211    ($e:expr) => {
212        $e
213    };
214}
215
216#[cfg(not(feature = "no-unsafe"))]
217#[macro_export]
218macro_rules! as_qname {
219    ($e:expr) => {
220        $e
221    };
222}
223
224#[cfg(feature = "no-unsafe")]
225#[macro_export]
226macro_rules! as_str {
227    ($e:expr) => {
228        &*$e
229    };
230}
231
232#[cfg(feature = "no-unsafe")]
233#[macro_export]
234macro_rules! as_opt_str {
235    ($e:expr) => {
236        $e.as_deref()
237    };
238}
239
240#[cfg(feature = "no-unsafe")]
241#[macro_export]
242macro_rules! as_qname {
243    ($e:expr) => {
244        $e.get()
245    };
246}
247
248#[cfg(not(feature = "no-unsafe"))]
249#[macro_export]
250macro_rules! to_ns_str {
251    ($e:expr) => {
252        $e
253    };
254}
255
256#[cfg(feature = "no-unsafe")]
257#[macro_export]
258macro_rules! to_ns_str {
259    ($e:expr) => {
260        ($e).to_owned()
261    };
262}
263
264/// String type used for namespace prefix/URI storage.
265/// `&'d str` in default mode, `String` in no-unsafe mode.
266#[cfg(not(feature = "no-unsafe"))]
267pub type NsStr<'d> = &'d str;
268/// String type used for namespace prefix/URI storage.
269/// `&'d str` in default mode, `String` in no-unsafe mode.
270#[cfg(feature = "no-unsafe")]
271///
272/// The lifetime is intentionally retained so cfg-independent generic code has one public type
273/// signature; owned storage does not borrow from `'d` in this mode.
274pub type NsStr<'d> = String;
275
276pub(crate) static XML_NS_PREFIX: &str = "xml";
277pub(crate) static XML_NS_URI: &str = "http://www.w3.org/XML/1998/namespace";
278
279/// A prefixed name. This represents what is found in the string form
280/// of an XML document, and does not apply any namespace mapping.
281#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
282pub struct PrefixedName<'a> {
283    prefix: Option<&'a str>,
284    local_part: &'a str,
285}
286
287impl<'a> PrefixedName<'a> {
288    /// Create a `PrefixedName` without a prefix
289    pub fn new(local_part: &str) -> PrefixedName<'_> {
290        PrefixedName::with_prefix(None, local_part)
291    }
292
293    /// Create a `PrefixedName` without an optional prefix
294    pub fn with_prefix(prefix: Option<&'a str>, local_part: &'a str) -> PrefixedName<'a> {
295        PrefixedName { prefix, local_part }
296    }
297
298    pub fn prefix(&self) -> Option<&str> {
299        self.prefix
300    }
301    pub fn local_part(&self) -> &str {
302        self.local_part
303    }
304}
305
306/// A namespace-qualified name. This represents the name of an element
307/// or attribute *after* the prefix has been mapped to a specific
308/// namespace.
309#[derive(Debug, Copy, Clone, PartialEq)]
310pub struct QName<'s> {
311    namespace_uri: Option<&'s str>,
312    local_part: &'s str,
313}
314
315impl<'s> QName<'s> {
316    /// Create a `QName` without a namespace
317    pub fn new(local_part: &'s str) -> QName<'s> {
318        QName::with_namespace_uri(None, local_part)
319    }
320
321    /// Create a `QName` with an optional namespace
322    pub fn with_namespace_uri(namespace_uri: Option<&'s str>, local_part: &'s str) -> QName<'s> {
323        QName {
324            namespace_uri,
325            local_part,
326        }
327    }
328
329    pub fn namespace_uri(&self) -> Option<&'s str> {
330        self.namespace_uri
331    }
332    pub fn local_part(&self) -> &'s str {
333        self.local_part
334    }
335}
336
337impl<'s> From<(&'s str, &'s str)> for QName<'s> {
338    fn from(v: (&'s str, &'s str)) -> QName<'s> {
339        QName {
340            namespace_uri: Some(v.0),
341            local_part: v.1,
342        }
343    }
344}
345
346impl<'s> From<&'s str> for QName<'s> {
347    fn from(v: &'s str) -> QName<'s> {
348        QName {
349            namespace_uri: None,
350            local_part: v,
351        }
352    }
353}
354
355/// The main entrypoint to an XML document
356///
357/// This is an opaque structure that stores the internal details of
358/// the XML document. Modify the document via `as_document`.
359pub struct Package {
360    identity: usize,
361    storage: raw::Storage,
362    connections: raw::Connections,
363}
364
365impl Default for Package {
366    fn default() -> Package {
367        let s = raw::Storage::new();
368        let root = s.create_root();
369        Package {
370            identity: next_package_identity(),
371            storage: s,
372            connections: raw::Connections::new(root),
373        }
374    }
375}
376
377impl Package {
378    pub fn new() -> Package {
379        Self::default()
380    }
381
382    pub fn as_document(&self) -> dom::Document<'_> {
383        dom::Document::new(self.identity, &self.storage, &self.connections)
384    }
385
386    #[doc(hidden)]
387    pub fn as_thin_document(&self) -> (thindom::Storage<'_>, thindom::Connections<'_>) {
388        // The returned handles borrow this Package, so its address is a stable runtime brand
389        // for rejecting nodes created by a different arena before index or pointer access.
390        let identity = std::ptr::from_ref(self).addr();
391        let s = thindom::Storage::new(&self.storage, identity);
392        #[cfg(not(feature = "no-unsafe"))]
393        let c = thindom::Connections::new(&self.connections, identity);
394        #[cfg(feature = "no-unsafe")]
395        let c = thindom::Connections::new(&self.connections, &self.storage, identity);
396        (s, c)
397    }
398}
399
400impl PartialEq for Package {
401    fn eq(&self, other: &Package) -> bool {
402        std::ptr::eq(self, other)
403    }
404}
405
406impl fmt::Debug for Package {
407    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
408        write!(f, "Package")
409    }
410}