Skip to main content

structured_asn1/memory/
string.rs

1//! Owned UTF-8 storage using the accounted vector's safe allocation API.
2
3use super::{Memory, MemoryError, OwnedVec};
4
5/// Independently owned UTF-8 text with retained capacity accounting.
6///
7/// Only valid UTF-8 may be appended; raw mutable bytes are never exposed.
8/// Borrowing as `str` uses safe UTF-8 validation and admits its scan first.
9/// This avoids an additional unchecked conversion outside the allocator adapter.
10#[derive(Debug)]
11pub struct OwnedString {
12    bytes: OwnedVec<u8>,
13}
14
15impl OwnedString {
16    pub(super) fn new(memory: &Memory) -> Self {
17        Self {
18            bytes: memory.vector(),
19        }
20    }
21
22    /// The domain retaining this text's storage.
23    #[must_use]
24    pub fn memory(&self) -> &Memory {
25        self.bytes.memory()
26    }
27
28    /// The initialized UTF-8 length in bytes.
29    #[must_use]
30    pub fn len(&self) -> usize {
31        self.bytes.len()
32    }
33
34    /// Whether this text contains no characters.
35    #[must_use]
36    pub fn is_empty(&self) -> bool {
37        self.bytes.is_empty()
38    }
39
40    /// Allocated byte capacity, retained until this text is dropped.
41    #[must_use]
42    pub fn capacity(&self) -> usize {
43        self.bytes.capacity()
44    }
45
46    /// Borrows the text without copying, charging safe UTF-8 validation to the
47    /// current execution domain, independently of the retained storage domain.
48    ///
49    /// # Panics
50    ///
51    /// Panics if the private UTF-8 invariant is violated. Safe construction
52    /// and append operations preserve that invariant.
53    pub fn as_str(&self, execution: &Memory) -> Result<&str, MemoryError> {
54        execution.charge_work(self.bytes.len() as u64)?;
55        Ok(core::str::from_utf8(&self.bytes).expect("only valid UTF-8 is appended"))
56    }
57
58    /// The UTF-8 bytes, as an allocation-free view; callers account traversal.
59    #[must_use]
60    pub fn as_bytes(&self) -> &[u8] {
61        &self.bytes
62    }
63
64    /// Appends valid UTF-8 after admitting capacity and copying work.
65    pub fn try_push_str(&mut self, text: &str) -> Result<(), MemoryError> {
66        self.bytes.try_extend_from_slice(text.as_bytes())
67    }
68
69    /// Appends one scalar using a fixed stack buffer, without hidden allocation.
70    pub fn try_push(&mut self, character: char) -> Result<(), MemoryError> {
71        self.memory().charge_work(1)?;
72        let mut encoded = [0; 4];
73        self.try_push_str(character.encode_utf8(&mut encoded))
74    }
75
76    /// Creates an independently charged copy in the same domain.
77    pub fn try_copy(&self) -> Result<Self, MemoryError> {
78        Ok(Self {
79            bytes: self.bytes.try_copy()?,
80        })
81    }
82}
83
84#[cfg(test)]
85mod tests {
86    use super::super::MemoryAccount;
87    use super::*;
88    use alloc::sync::Arc;
89
90    #[test]
91    fn repeated_scalar_append_has_amortized_growth() {
92        // Growing by one exact byte for each scalar makes a bounded 32K
93        // display quadratic. Actual capacity and work must remain linear.
94        const LENGTH: usize = 32_768;
95        let memory = Memory::new(Arc::new(MemoryAccount::new(LENGTH * 2, LENGTH * 8)));
96        let mut text = memory.string();
97        for _ in 0..LENGTH {
98            text.try_push('a').unwrap();
99        }
100        assert_eq!(text.len(), LENGTH);
101        assert_eq!(memory.used(), text.capacity());
102        assert!(memory.work_used() <= LENGTH * 8);
103        drop(text);
104        assert_eq!(memory.used(), 0);
105    }
106
107    #[test]
108    fn text_is_valid_utf8_and_retains_its_capacity_after_move() {
109        // Non-ASCII scalars must keep valid UTF-8 across fallible append and
110        // ownership transfer; dropping the original handle cannot refund storage.
111        let memory = Memory::new(Arc::new(MemoryAccount::new(64, 1024)));
112        let mut text = memory.string();
113        text.try_push_str("Дія").unwrap();
114        text.try_push('🦀').unwrap();
115        assert_eq!(text.as_str(&memory).unwrap(), "Дія🦀");
116        let capacity = text.capacity();
117        let observer = memory.clone();
118        drop(memory);
119        assert_eq!(observer.used(), capacity);
120        let copy = text.try_copy().unwrap();
121        assert_eq!(copy.as_str(&observer).unwrap(), "Дія🦀");
122        assert_eq!(observer.used(), capacity + copy.capacity());
123        drop(text);
124        assert_eq!(observer.used(), copy.capacity());
125        drop(copy);
126        assert_eq!(observer.used(), 0);
127    }
128
129    #[test]
130    fn refusal_preserves_text_and_does_not_hide_utf8_scan_work() {
131        // A rejected append preserves the original scalar; a later borrowed
132        // conversion must refuse before scanning once the work quota is spent.
133        let memory = Memory::new(Arc::new(MemoryAccount::new(4, 1024)));
134        let mut text = memory.string();
135        text.try_push('🦀').unwrap();
136        assert_eq!(text.try_push('!'), Err(MemoryError::LimitExceeded));
137        assert_eq!(text.as_str(&memory).unwrap(), "🦀");
138        memory
139            .charge_work((1024 - memory.work_used()) as u64)
140            .unwrap();
141        assert_eq!(text.as_str(&memory), Err(MemoryError::WorkExceeded));
142        assert_eq!(text.as_bytes(), "🦀".as_bytes());
143    }
144
145    #[test]
146    fn retained_text_cannot_pay_for_a_new_operations_validation() {
147        // A long-lived trust-store string keeps its storage charge, but a new
148        // request with no work allowance must refuse even if the store has work.
149        let storage = Memory::new(Arc::new(MemoryAccount::new(64, 1024)));
150        let execution = Memory::new(Arc::new(MemoryAccount::new(64, 0)));
151        let mut text = storage.string();
152        text.try_push_str("issuer").unwrap();
153        let used = storage.work_used();
154        assert_eq!(text.as_str(&execution), Err(MemoryError::WorkExceeded));
155        assert_eq!(storage.work_used(), used);
156        assert_eq!(storage.used(), text.capacity());
157        assert_eq!(execution.used(), 0);
158    }
159}