Skip to main content

structured_asn1/memory/
shared_string.rs

1//! Independently retained immutable UTF-8 with one accounted allocation.
2
3use super::{HashWork, Memory, MemoryError, Work, allocator::SharedText};
4use core::{
5    borrow::Borrow,
6    fmt,
7    hash::{Hash, Hasher},
8    ops::Deref,
9};
10
11/// Shared immutable UTF-8, with non-atomic, allocation-free handle cloning.
12///
13/// One allocation contains the bytes, reference count and original storage
14/// account. The last handle deallocates it before releasing its capacity charge.
15/// Creation pays current [`Work`], but no borrowed execution is retained.
16/// This type is neither `Send` nor `Sync`; it has no weak or mutable access.
17/// Cloning panics before changing the count if `usize::MAX` live handles
18/// would be exceeded. Each clone/drop is fixed work; callers admit any
19/// input-dependent fan-out producing handles.
20/// Borrowed views are O(1). Callers fund their own Hash/Eq/formatting/traversal
21/// work; [`HashWork`] enables pre-admission when used as an [`super::OwnedMap`] key.
22///
23/// ```
24/// use std::sync::Arc;
25/// use structured_asn1::memory::{Memory, MemoryAccount, SharedString};
26/// let memory = Memory::new(Arc::new(MemoryAccount::new(1024, 1024)));
27/// let text = SharedString::try_from_str("namespace", &memory)?;
28/// let retained = text.clone();
29/// assert!(SharedString::ptr_eq(&text, &retained));
30/// let capacity = memory.used();
31/// drop(text);
32/// assert_eq!(memory.used(), capacity);
33/// assert_eq!(retained.as_str(), "namespace");
34/// drop(retained);
35/// assert_eq!(memory.used(), 0);
36/// # Ok::<(), structured_asn1::memory::MemoryError>(())
37/// ```
38///
39/// ```compile_fail
40/// use structured_asn1::memory::SharedString;
41/// fn requires_send<T: Send>() {}
42/// requires_send::<SharedString>();
43/// ```
44///
45/// ```compile_fail
46/// use structured_asn1::memory::SharedString;
47/// fn requires_sync<T: Sync>() {}
48/// requires_sync::<SharedString>();
49/// ```
50#[derive(Clone)]
51pub struct SharedString {
52    text: SharedText,
53}
54
55impl SharedString {
56    /// Copies valid UTF-8 after admitting the complete layout and copy work.
57    /// Allocation failure and quota/work refusal remain distinct. Empty text
58    /// also owns a charged header; there is no unaccounted global empty owner.
59    pub fn try_from_str(text: &str, execution: &impl Work) -> Result<Self, MemoryError> {
60        Ok(Self {
61            text: SharedText::new(text, execution)?,
62        })
63    }
64
65    /// Original account retaining the allocation until the last handle drops.
66    #[must_use]
67    pub fn memory(&self) -> &Memory {
68        self.text.memory()
69    }
70
71    /// Initialized UTF-8 byte length, without traversing the text.
72    #[must_use]
73    pub fn len(&self) -> usize {
74        self.text.len()
75    }
76
77    /// Whether this text contains no bytes.
78    #[must_use]
79    pub fn is_empty(&self) -> bool {
80        self.len() == 0
81    }
82
83    /// Full allocator-visible layout size, including metadata and padding.
84    #[must_use]
85    pub fn allocated_bytes(&self) -> usize {
86        self.text.allocated_bytes()
87    }
88
89    /// Borrows immutable UTF-8 in O(1), without revalidation or copying.
90    #[must_use]
91    pub fn as_str(&self) -> &str {
92        self.text.as_str()
93    }
94
95    /// Borrows initialized bytes; callers account their subsequent traversal.
96    #[must_use]
97    pub fn as_bytes(&self) -> &[u8] {
98        self.as_str().as_bytes()
99    }
100
101    /// Whether both handles own the same allocation, without comparing bytes.
102    #[must_use]
103    pub fn ptr_eq(left: &Self, right: &Self) -> bool {
104        left.text.ptr_eq(&right.text)
105    }
106}
107
108impl Deref for SharedString {
109    type Target = str;
110    fn deref(&self) -> &str {
111        self.as_str()
112    }
113}
114impl Borrow<str> for SharedString {
115    fn borrow(&self) -> &str {
116        self.as_str()
117    }
118}
119impl AsRef<str> for SharedString {
120    fn as_ref(&self) -> &str {
121        self.as_str()
122    }
123}
124impl fmt::Debug for SharedString {
125    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
126        self.as_str().fmt(f)
127    }
128}
129impl fmt::Display for SharedString {
130    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
131        f.write_str(self.as_str())
132    }
133}
134impl PartialEq for SharedString {
135    fn eq(&self, other: &Self) -> bool {
136        Self::ptr_eq(self, other) || self.as_str() == other.as_str()
137    }
138}
139impl Eq for SharedString {}
140impl Hash for SharedString {
141    fn hash<H: Hasher>(&self, state: &mut H) {
142        self.as_str().hash(state);
143    }
144}
145impl HashWork for SharedString {
146    fn hash_work(&self) -> Result<u64, MemoryError> {
147        (self.len() as u64)
148            .checked_add(1)
149            .ok_or(MemoryError::WorkExceeded)
150    }
151}
152
153#[cfg(test)]
154mod tests {
155    use super::*;
156    use crate::memory::MemoryAccount;
157    use crate::{Encoding, Limits, Parser};
158    use alloc::{format, sync::Arc};
159
160    fn memory(storage: usize, work: usize) -> Memory {
161        Memory::new(Arc::new(MemoryAccount::new(storage, work)))
162    }
163
164    #[test]
165    fn shared_string_exact_layout_and_work_include_header_padding_and_copy() {
166        // Exact admission includes metadata/padding, not only string bytes;
167        // one byte/work unit below the boundary refuses with zero live storage.
168        for value in ["", "a", "Ж", "𐀀🦀", "123456789"] {
169            let probe = memory(4096, 4096);
170            let text = SharedString::try_from_str(value, &probe).unwrap();
171            let bytes = text.allocated_bytes();
172            assert_eq!(probe.used(), bytes);
173            assert_eq!(probe.work_used(), bytes + value.len());
174            drop(text);
175            assert_eq!(probe.used(), 0);
176            for (storage, work, expected) in [
177                (bytes - 1, usize::MAX, MemoryError::LimitExceeded),
178                (bytes, bytes + value.len() - 1, MemoryError::WorkExceeded),
179            ] {
180                let denied = memory(storage, work);
181                assert_eq!(
182                    SharedString::try_from_str(value, &denied).unwrap_err(),
183                    expected
184                );
185                assert_eq!(denied.used(), 0);
186            }
187            let exact = memory(bytes, bytes + value.len());
188            let text = SharedString::try_from_str(value, &exact).unwrap();
189            assert_eq!(text.as_str(), value);
190            assert_eq!(text.as_bytes(), value.as_bytes());
191            assert_eq!(text.is_empty(), value.is_empty());
192            let clone = text.clone();
193            assert!(SharedString::ptr_eq(&text, &clone));
194            assert_eq!(exact.used(), bytes);
195            assert_eq!(exact.work_used(), bytes + value.len());
196            drop(text);
197            assert_eq!(clone.as_str(), value);
198            drop(clone);
199            assert_eq!(exact.used(), 0);
200        }
201    }
202
203    #[test]
204    fn shared_string_local_exact_copy_and_layout_admission_share_one_execution() {
205        // Admitting copy work cannot bypass a producing Parser's remaining
206        // allocation allowance even when aggregate work remains available.
207        let probe = memory(4096, 4096);
208        let text = SharedString::try_from_str("abc", &probe).unwrap();
209        let needed = text.allocated_bytes() + text.len();
210        drop(text);
211        for local in [needed - 1, needed] {
212            let storage = memory(4096, 1_000_000);
213            let parser = Parser::new(
214                Encoding::Der,
215                Limits {
216                    max_work: local as u64,
217                    ..Limits::default()
218                },
219                storage.clone(),
220            );
221            let reader = parser.read(&[], 0).unwrap();
222            let result = SharedString::try_from_str("abc", &reader.execution());
223            if local == needed {
224                let text = result.unwrap();
225                assert!(text.memory().same_account(&storage));
226                assert_eq!(text.as_str(), "abc");
227                drop(text);
228            } else {
229                assert_eq!(result.unwrap_err(), MemoryError::WorkExceeded);
230            }
231            assert_eq!(storage.used(), 0);
232        }
233    }
234
235    #[test]
236    fn shared_string_views_hashes_and_lifetimes_preserve_immutable_utf8() {
237        // Equal UTF-8 in distinct allocations must keep the str hash and
238        // independently retain the account, without hidden per-clone keepers.
239        use core::hash::BuildHasher;
240        let owner = memory(4096, 4096);
241        let left = SharedString::try_from_str("Straße 🦀", &owner).unwrap();
242        let right = SharedString::try_from_str("Straße 🦀", &owner).unwrap();
243        assert!(!SharedString::ptr_eq(&left, &right));
244        assert_eq!(left, right);
245        assert_eq!(Borrow::<str>::borrow(&left), "Straße 🦀");
246        assert_eq!(AsRef::<str>::as_ref(&left), "Straße 🦀");
247        let hasher = hashbrown::DefaultHashBuilder::default();
248        assert_eq!(hasher.hash_one(&left), hasher.hash_one(left.as_str()));
249        assert_eq!(left.hash_work().unwrap(), left.len() as u64 + 1);
250        assert_eq!(format!("{left}"), "Straße 🦀");
251        assert_eq!(format!("{left:?}"), "\"Straße 🦀\"");
252        let account = Arc::downgrade(&owner.account);
253        drop(owner);
254        assert!(account.upgrade().is_some());
255        drop(left);
256        assert!(account.upgrade().is_some());
257        drop(right);
258        assert!(account.upgrade().is_none());
259    }
260}