Skip to main content

toml_spanner/item/
owned.rs

1#[cfg(test)]
2#[path = "./owned_tests.rs"]
3mod tests;
4
5use crate::item::{TAG_ARRAY, TAG_STRING, TAG_TABLE};
6use crate::{Array, DateTime, Item, Key, Kind, Span, Table, TableStyle, Value};
7use std::mem::size_of;
8use std::ptr::NonNull;
9
10/// Write cursor into an [`OwnedItem`] allocation, used by
11/// [`Item::emplace_in`] to copy item trees without an arena.
12///
13/// The allocation is split into two contiguous regions:
14/// - **aligned** (front): table entries and array elements (8-byte aligned)
15/// - **string** (back): key names and string values (packed, no alignment)
16pub(crate) struct ItemCopyTarget {
17    pub(crate) aligned: *mut u8,
18    #[cfg(debug_assertions)]
19    pub(crate) aligned_end: *mut u8,
20    pub(crate) string: *mut u8,
21    #[cfg(debug_assertions)]
22    pub(crate) string_end: *mut u8,
23}
24
25impl ItemCopyTarget {
26    /// Bumps the aligned pointer forward by `size` bytes, returning
27    /// a pointer to the start of the allocated region.
28    ///
29    /// # Safety
30    ///
31    /// `size` bytes must remain in the aligned region.
32    pub(crate) unsafe fn alloc_aligned(&mut self, size: usize) -> NonNull<u8> {
33        #[cfg(debug_assertions)]
34        // SAFETY: Both pointers are within (or one-past-the-end of) the
35        // same allocation, so offset_from is well-defined.
36        unsafe {
37            let remaining = self.aligned_end.offset_from(self.aligned) as usize;
38            assert!(size <= remaining);
39        };
40        let ptr = self.aligned;
41        // SAFETY: Caller guarantees sufficient space in the aligned region.
42        unsafe {
43            self.aligned = self.aligned.add(size);
44            NonNull::new_unchecked(ptr)
45        }
46    }
47
48    /// Copies a string into the string region and returns a reference to it.
49    ///
50    /// # Safety
51    ///
52    /// `s.len()` bytes must remain in the string region. The returned
53    /// reference is `'static` only because OwnedItem manages the backing
54    /// memory; the caller must not let it escape.
55    pub(crate) unsafe fn copy_str(&mut self, s: &str) -> &'static str {
56        if s.is_empty() {
57            return "";
58        }
59        let len = s.len();
60        #[cfg(debug_assertions)]
61        // SAFETY: Both pointers are within (or one-past-the-end of) the
62        // same allocation, so offset_from is well-defined.
63        unsafe {
64            let remaining = self.string_end.offset_from(self.string) as usize;
65            assert!(len <= remaining);
66        };
67        // SAFETY: Caller guarantees sufficient space. Source and destination
68        // do not overlap (source is the parsed input or arena, destination is
69        // the OwnedItem allocation).
70        unsafe {
71            std::ptr::copy_nonoverlapping(s.as_ptr(), self.string, len);
72            let result =
73                std::str::from_utf8_unchecked(std::slice::from_raw_parts(self.string, len));
74            self.string = self.string.add(len);
75            result
76        }
77    }
78}
79
80/// Computes the total aligned and string bytes needed to deep-copy `item`.
81fn compute_size(item: &Item<'_>, aligned: &mut usize, strings: &mut usize) {
82    match item.tag() {
83        TAG_STRING => {
84            if let Some(s) = item.as_str() {
85                *strings += s.len();
86            }
87        }
88        TAG_TABLE => {
89            // SAFETY: tag == TAG_TABLE guarantees this is a table item.
90            let table = unsafe { item.as_table_unchecked() };
91            *aligned += table.len() * size_of::<(Key<'_>, Item<'_>)>();
92            for (key, child) in table {
93                *strings += key.name.len();
94                compute_size(child, aligned, strings);
95            }
96        }
97        TAG_ARRAY => {
98            // SAFETY: tag == TAG_ARRAY guarantees this is an array item.
99            let array = unsafe { item.as_array_unchecked() };
100            *aligned += array.len() * size_of::<Item<'_>>();
101            for child in array {
102                compute_size(child, aligned, strings);
103            }
104        }
105        _ => {}
106    }
107}
108
109/// A self-contained TOML table that owns its backing storage.
110///
111/// The table-specific counterpart of [`OwnedItem`]. The inner value
112/// is always a table, so [`table()`](Self::table) is infallible.
113///
114/// # Flattening
115///
116/// [`FromFlattened`](crate::FromFlattened) is not provided because
117/// [`flatten_any`](crate::helper::flatten_any) is more performant
118/// than direct implementation could be, due to current trait definition.
119///
120/// ```rust,ignore
121/// use toml_spanner::Toml;
122/// use toml_spanner::helper::flatten_any;
123///
124/// #[derive(Toml)]
125/// #[toml(Toml)]
126/// struct Config {
127///     name: String,
128///     #[toml(flatten, with = flatten_any)]
129///     extra: OwnedTable,
130/// }
131/// ```
132///
133/// # Examples
134///
135/// ```
136/// use toml_spanner::OwnedTable;
137///
138/// let owned: OwnedTable = toml_spanner::from_str("
139/// host = 'localhost'
140/// port = 8080
141/// ").unwrap();
142///
143/// assert_eq!(owned.get("host").unwrap().as_str(), Some("localhost"));
144/// assert_eq!(owned.len(), 2);
145/// ```
146pub struct OwnedTable {
147    inner: OwnedItem,
148}
149
150impl OwnedTable {
151    /// Returns a reference to the contained [`Table`].
152    #[inline(always)]
153    pub fn table<'a>(&'a self) -> &'a Table<'a> {
154        // SAFETY: OwnedItem guarantees the item is valid for the lifetime of self.
155        unsafe { self.inner.item().as_table_unchecked() }
156    }
157
158    /// Returns the source span, or `0..0` if this table was constructed
159    /// programmatically (format-hints mode).
160    #[inline]
161    pub fn span(&self) -> Span {
162        self.table().span()
163    }
164
165    /// Returns the number of entries.
166    #[inline]
167    pub fn len(&self) -> usize {
168        self.table().len()
169    }
170
171    /// Returns `true` if the table has no entries.
172    #[inline]
173    pub fn is_empty(&self) -> bool {
174        self.table().is_empty()
175    }
176
177    /// Returns references to both key and value for `name`.
178    pub fn get_key_value<'a>(&'a self, name: &str) -> Option<(&'a Key<'a>, &'a Item<'a>)> {
179        self.table().get_key_value(name)
180    }
181
182    /// Returns a reference to the value for `name`.
183    pub fn get<'a>(&'a self, name: &str) -> Option<&'a Item<'a>> {
184        self.table().get(name)
185    }
186
187    /// Returns `true` if the table contains the key.
188    #[inline]
189    pub fn contains_key(&self, name: &str) -> bool {
190        self.table().contains_key(name)
191    }
192
193    /// Returns a slice of all entries.
194    #[inline]
195    pub fn entries<'a>(&'a self) -> &'a [(Key<'a>, Item<'a>)] {
196        self.table().entries()
197    }
198
199    /// Returns an iterator over all entries (key-value pairs).
200    #[inline]
201    pub fn iter<'a>(&'a self) -> std::slice::Iter<'a, (Key<'a>, Item<'a>)> {
202        self.table().iter()
203    }
204
205    /// Returns the contained table as an [`Item`] reference.
206    #[inline]
207    pub fn as_item<'a>(&'a self) -> &'a Item<'a> {
208        self.inner.item()
209    }
210
211    /// Returns the kind of this table (implicit, dotted, header, or inline).
212    #[inline]
213    pub fn style(&self) -> TableStyle {
214        self.table().style()
215    }
216}
217
218impl std::fmt::Debug for OwnedTable {
219    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
220        self.table().fmt(f)
221    }
222}
223
224impl Clone for OwnedTable {
225    fn clone(&self) -> Self {
226        Self {
227            inner: self.inner.clone(),
228        }
229    }
230}
231
232impl PartialEq for OwnedTable {
233    fn eq(&self, other: &Self) -> bool {
234        self.table() == other.table()
235    }
236}
237
238impl<'a> IntoIterator for &'a OwnedTable {
239    type Item = &'a (Key<'a>, Item<'a>);
240    type IntoIter = std::slice::Iter<'a, (Key<'a>, Item<'a>)>;
241
242    fn into_iter(self) -> Self::IntoIter {
243        self.table().iter()
244    }
245}
246
247impl From<&Table<'_>> for OwnedTable {
248    fn from(value: &Table<'_>) -> Self {
249        let owned_item = OwnedItem::from(value.as_item());
250        debug_assert_eq!(owned_item.item().kind(), Kind::Table);
251        Self { inner: owned_item }
252    }
253}
254
255/// A self-contained TOML value that owns its backing storage.
256///
257/// An [`Item`] normally borrows from an [`Arena`](crate::Arena).
258/// `OwnedItem` bundles the value with its own allocation so it can
259/// be stored, returned, or moved independently of any parse context.
260///
261/// Create one by converting from an [`Item`] reference or value, or
262/// deserialize directly via [`FromToml`](crate::FromToml). Access
263/// the underlying [`Item`] through [`item()`](Self::item).
264///
265/// [`FromFlattened`](crate::FromFlattened) is not provided because
266/// [`flatten_any`](crate::helper::flatten_any) is more performant
267/// than direct implementation could be. See [`OwnedTable`] for an example.
268///
269/// # Examples
270///
271/// ```
272/// use toml_spanner::{Arena, OwnedItem, parse};
273///
274/// let arena = Arena::new();
275/// let doc = parse("greeting = 'hello'", &arena).unwrap();
276/// let owned = OwnedItem::from(doc.table()["greeting"].item().unwrap());
277///
278/// drop(arena);
279/// assert_eq!(owned.as_str(), Some("hello"));
280/// ```
281pub struct OwnedItem {
282    // This 'static is a lie! It borrows from ptr.
283    item: Item<'static>,
284    ptr: NonNull<u8>,
285    capacity: usize,
286}
287
288// SAFETY: `OwnedItem` exclusively owns the heap allocation referenced by
289// `ptr` (allocated in `From<&Item>` and freed in `Drop`). The embedded
290// `Item<'static>` has `NonNull`s into that same allocation. No other handle
291// to the allocation exists, and the crate never applies interior mutability
292// to an `OwnedItem` (`Clone` produces a fresh allocation, and there is no
293// method that mutates the buffer through `&self`). Transferring ownership
294// to another thread moves the allocation as one unit; sharing `&OwnedItem`
295// across threads is sound because every accessor returns borrowed data from
296// memory that is only ever read.
297unsafe impl Send for OwnedItem {}
298unsafe impl Sync for OwnedItem {}
299
300// SAFETY: `OwnedTable` is a thin wrapper around `OwnedItem` and inherits
301// the same ownership and immutability invariants.
302unsafe impl Send for OwnedTable {}
303unsafe impl Sync for OwnedTable {}
304
305impl std::fmt::Debug for OwnedItem {
306    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
307        self.item.fmt(f)
308    }
309}
310
311impl Clone for OwnedItem {
312    fn clone(&self) -> Self {
313        OwnedItem::from(self.item())
314    }
315}
316
317impl Drop for OwnedItem {
318    fn drop(&mut self) {
319        if self.capacity > 0 {
320            // Pedantically, remove the allocated Item before deallocating.
321            // probrably not needed, MIRI seams to be happy without it.
322            // self.item = Item::from(false);
323
324            // SAFETY: ptr was allocated with Layout { size: capacity, align: 8 }
325            // in `From<&Item>`. capacity > 0 guarantees a real allocation.
326            unsafe {
327                let layout = std::alloc::Layout::from_size_align_unchecked(self.capacity, 8);
328                std::alloc::dealloc(self.ptr.as_ptr(), layout);
329            }
330        }
331    }
332}
333
334impl<'a> From<&Item<'a>> for OwnedItem {
335    /// Creates an `OwnedItem` by copying `item` into a single managed allocation.
336    ///
337    /// All strings (keys and values) are copied, and all table/array
338    /// backing storage is laid out in one contiguous buffer. The result
339    /// is fully independent of the source arena.
340    fn from(item: &Item<'a>) -> Self {
341        let mut aligned = 0usize;
342        let mut strings = 0usize;
343        compute_size(item, &mut aligned, &mut strings);
344        let total = aligned + strings;
345
346        if total == 0 {
347            // SAFETY: When total is 0 the item is either a non-string scalar
348            // (no borrowed data), an empty string (payload is static ""), or
349            // an empty container (dangling pointer with len 0). Transmuting
350            // the lifetime is safe because nothing actually borrows from an
351            // arena. Item has no Drop impl.
352            return Self {
353                item: unsafe { std::mem::transmute_copy(item) },
354                ptr: NonNull::dangling(),
355                capacity: 0,
356            };
357        }
358
359        let layout = std::alloc::Layout::from_size_align(total, 8).expect("layout overflow");
360        // SAFETY: layout has non-zero size (total > 0).
361        let raw = unsafe { std::alloc::alloc(layout) };
362        let Some(base) = NonNull::new(raw) else {
363            std::alloc::handle_alloc_error(layout);
364        };
365
366        // SAFETY: base.add(aligned) and base.add(total) are within or
367        // one-past-the-end of the allocation.
368        let mut target = unsafe {
369            ItemCopyTarget {
370                aligned: base.as_ptr(),
371                #[cfg(debug_assertions)]
372                aligned_end: base.as_ptr().add(aligned),
373                string: base.as_ptr().add(aligned),
374                #[cfg(debug_assertions)]
375                string_end: base.as_ptr().add(total),
376            }
377        };
378
379        // SAFETY: compute_size computed the exact space needed; emplace_in
380        // consumes exactly that much from target.
381        let new_item = unsafe { item.emplace_in(&mut target) };
382
383        #[cfg(debug_assertions)]
384        {
385            assert_eq!(target.aligned as usize, base.as_ptr() as usize + aligned);
386            assert_eq!(target.string as usize, base.as_ptr() as usize + total);
387        }
388
389        Self {
390            item: new_item,
391            ptr: base,
392            capacity: total,
393        }
394    }
395}
396
397impl<'a> From<Item<'a>> for OwnedItem {
398    /// Creates an `OwnedItem` by copying `item` into a single managed allocation.
399    ///
400    /// This is a convenience wrapper that delegates to `From<&Item>`.
401    fn from(item: Item<'a>) -> Self {
402        OwnedItem::from(&item)
403    }
404}
405
406impl OwnedItem {
407    /// Returns a reference to the contained [`Item`].
408    ///
409    /// The returned item borrows from `self` and provides the same
410    /// accessor methods as any other [`Item`] (`as_str()`, `as_table()`,
411    /// `value()`, etc.).
412    #[inline(always)]
413    pub fn item<'a>(&'a self) -> &'a Item<'a> {
414        &self.item
415    }
416
417    /// Returns the type discriminant of this value.
418    #[inline]
419    pub fn kind(&self) -> Kind {
420        self.item().kind()
421    }
422
423    /// Returns the source span, or `0..0` if this item was constructed
424    /// programmatically (format-hints mode).
425    #[inline]
426    pub fn span(&self) -> Span {
427        self.item().span()
428    }
429
430    /// Returns a borrowed string if this is a string value.
431    #[inline]
432    pub fn as_str(&self) -> Option<&str> {
433        self.item().as_str()
434    }
435
436    /// Returns an `i128` if this is an integer value.
437    #[inline]
438    pub fn as_i128(&self) -> Option<i128> {
439        self.item().as_i128()
440    }
441
442    /// Returns an `i64` if this is an integer value that fits in the `i64` range.
443    #[inline]
444    pub fn as_i64(&self) -> Option<i64> {
445        self.item().as_i64()
446    }
447
448    /// Returns a `u64` if this is an integer value that fits in the `u64` range.
449    #[inline]
450    pub fn as_u64(&self) -> Option<u64> {
451        self.item().as_u64()
452    }
453
454    /// Returns an `f64` if this is a float or integer value.
455    ///
456    /// Integer values are converted to `f64` via `as` cast (lossy for large
457    /// values outside the 2^53 exact-integer range).
458    #[inline]
459    pub fn as_f64(&self) -> Option<f64> {
460        self.item().as_f64()
461    }
462
463    /// Returns a `bool` if this is a boolean value.
464    #[inline]
465    pub fn as_bool(&self) -> Option<bool> {
466        self.item().as_bool()
467    }
468
469    /// Returns a borrowed array if this is an array value.
470    #[inline]
471    pub fn as_array<'a>(&'a self) -> Option<&'a Array<'a>> {
472        self.item().as_array()
473    }
474
475    /// Returns a borrowed table if this is a table value.
476    #[inline]
477    pub fn as_table<'a>(&'a self) -> Option<&'a Table<'a>> {
478        self.item().as_table()
479    }
480
481    /// Returns a borrowed [`DateTime`] if this is a datetime value.
482    #[inline]
483    pub fn as_datetime(&self) -> Option<&DateTime> {
484        self.item().as_datetime()
485    }
486
487    /// Returns a borrowed view for pattern matching.
488    #[inline]
489    pub fn value<'a>(&'a self) -> Value<'a, 'a> {
490        self.item().value()
491    }
492
493    /// Returns `true` if the value is a non-empty table.
494    #[inline]
495    pub fn has_keys(&self) -> bool {
496        self.item().has_keys()
497    }
498
499    /// Returns `true` if the value is a table containing `key`.
500    #[inline]
501    pub fn has_key(&self, key: &str) -> bool {
502        self.item().has_key(key)
503    }
504}
505
506impl PartialEq for OwnedItem {
507    fn eq(&self, other: &Self) -> bool {
508        self.item() == other.item()
509    }
510}
511
512#[cfg(feature = "from-toml")]
513impl<'a> crate::FromToml<'a> for OwnedItem {
514    fn from_toml(_: &mut crate::Context<'a>, item: &Item<'a>) -> Result<Self, crate::Failed> {
515        Ok(OwnedItem::from(item))
516    }
517}
518
519#[cfg(feature = "to-toml")]
520impl crate::ToToml for OwnedItem {
521    fn to_toml<'a>(&'a self, arena: &'a crate::Arena) -> Result<Item<'a>, crate::ToTomlError> {
522        Ok(self.item().clone_in(arena))
523    }
524}
525
526#[cfg(feature = "to-toml")]
527impl crate::ToFlattened for OwnedItem {
528    fn to_flattened<'a>(
529        &'a self,
530        arena: &'a crate::Arena,
531        table: &mut crate::Table<'a>,
532    ) -> Result<(), crate::ToTomlError> {
533        self.item().to_flattened(arena, table)
534    }
535}
536
537#[cfg(feature = "from-toml")]
538impl<'a> crate::FromToml<'a> for OwnedTable {
539    fn from_toml(ctx: &mut crate::Context<'a>, item: &Item<'a>) -> Result<Self, crate::Failed> {
540        let Ok(table) = item.require_table(ctx) else {
541            return Err(crate::Failed);
542        };
543        Ok(OwnedTable::from(table))
544    }
545}
546
547#[cfg(feature = "to-toml")]
548impl crate::ToToml for OwnedTable {
549    fn to_toml<'a>(&'a self, arena: &'a crate::Arena) -> Result<Item<'a>, crate::ToTomlError> {
550        Ok(self.table().as_item().clone_in(arena))
551    }
552}
553
554#[cfg(feature = "to-toml")]
555impl crate::ToFlattened for OwnedTable {
556    fn to_flattened<'a>(
557        &'a self,
558        arena: &'a crate::Arena,
559        table: &mut crate::Table<'a>,
560    ) -> Result<(), crate::ToTomlError> {
561        self.table().to_flattened(arena, table)
562    }
563}