Skip to main content

stet_core/
string_store.rs

1// stet - A PostScript Interpreter
2// Copyright (c) 2026 Scott Bowman
3// SPDX-License-Identifier: Apache-2.0 OR MIT
4
5//! Contiguous byte buffer for PostScript string storage.
6//!
7//! Strings are identified by `EntityId` indices into the entity table,
8//! which provides indirection for save/restore COW semantics.
9
10use crate::entity_table::EntityTable;
11use crate::object::EntityId;
12
13/// Storage for PostScript string byte data.
14pub struct StringStore {
15    data: Vec<u8>,
16    pub entities: EntityTable,
17}
18
19impl StringStore {
20    pub fn new() -> Self {
21        Self {
22            data: Vec::new(),
23            entities: EntityTable::new(),
24        }
25    }
26
27    /// Allocate `len` zero-filled bytes, returning an `EntityId`.
28    /// Number of bytes handed out so far; the store's high-water mark.
29    pub fn data_len(&self) -> usize {
30        self.data.len()
31    }
32
33    /// Slots currently *reserved*, which is what the allocator actually holds.
34    ///
35    /// VM accounting has to use this rather than the length. The backing
36    /// `Vec` grows geometrically, so a store holding just under the ceiling by
37    /// length asks the allocator for roughly twice that on its next growth —
38    /// which is how `{ 1000000 string pop } loop` still aborted with a 16 GB
39    /// request after a length-based check had passed at 8 GB.
40    pub fn data_capacity(&self) -> usize {
41        self.data.capacity()
42    }
43
44    /// Release every byte and entity from the given marks onward.
45    ///
46    /// Used by `restore` to reclaim the objects a save level created. See
47    /// [`crate::entity_table::EntityTable::truncate`] for the safety argument
48    /// and the `EntityId`-reuse caveat.
49    pub fn truncate_to(&mut self, data_len: usize, entity_len: usize) {
50        self.data.truncate(data_len);
51        self.entities.truncate(entity_len);
52    }
53
54    pub fn allocate(&mut self, len: usize) -> EntityId {
55        let offset = self.data.len() as u32;
56        self.data.resize(self.data.len() + len, 0);
57        self.entities.allocate(offset, len as u32, 0, false, 0)
58    }
59
60    /// Allocate and copy `bytes` into the store.
61    pub fn allocate_from(&mut self, bytes: &[u8]) -> EntityId {
62        let offset = self.data.len() as u32;
63        self.data.extend_from_slice(bytes);
64        self.entities
65            .allocate(offset, bytes.len() as u32, 0, false, 0)
66    }
67
68    /// Allocate and copy `bytes` with a specific save level and global flag.
69    pub fn allocate_from_with(
70        &mut self,
71        bytes: &[u8],
72        save_level: u16,
73        global: bool,
74        created_after_save: u32,
75    ) -> EntityId {
76        let offset = self.data.len() as u32;
77        self.data.extend_from_slice(bytes);
78        self.entities.allocate(
79            offset,
80            bytes.len() as u32,
81            save_level,
82            global,
83            created_after_save,
84        )
85    }
86
87    /// Allocate with a specific save level and global flag.
88    pub fn allocate_with(
89        &mut self,
90        len: usize,
91        save_level: u16,
92        global: bool,
93        created_after_save: u32,
94    ) -> EntityId {
95        let offset = self.data.len() as u32;
96        self.data.resize(self.data.len() + len, 0);
97        self.entities
98            .allocate(offset, len as u32, save_level, global, created_after_save)
99    }
100
101    /// Get a slice of the string data via entity table indirection.
102    /// `start` is the byte offset from the entity's base; `len` is the number of bytes.
103    pub fn get(&self, entity: EntityId, start: u32, len: u32) -> &[u8] {
104        let base = self.entities.get(entity).offset as usize + start as usize;
105        &self.data[base..base + len as usize]
106    }
107
108    /// Get a mutable slice of the string data via entity table indirection.
109    /// `start` is the byte offset from the entity's base; `len` is the number of bytes.
110    pub fn get_mut(&mut self, entity: EntityId, start: u32, len: u32) -> &mut [u8] {
111        let base = self.entities.get(entity).offset as usize + start as usize;
112        &mut self.data[base..base + len as usize]
113    }
114
115    /// Set a single byte.
116    pub fn put_byte(&mut self, entity: EntityId, offset: u32, byte: u8) {
117        let base = self.entities.get(entity).offset as usize;
118        self.data[base + offset as usize] = byte;
119    }
120
121    /// Get a single byte.
122    pub fn get_byte(&self, entity: EntityId, offset: u32) -> u8 {
123        let base = self.entities.get(entity).offset as usize;
124        self.data[base + offset as usize]
125    }
126
127    /// Copy entity data to a new region (for COW). Returns the new EntityId
128    /// pointing to the copy. The original entity's offset is updated to
129    /// point at the copy, so the original EntityId now sees the new data.
130    pub fn cow_copy(&mut self, entity: EntityId) -> EntityId {
131        let meta = self.entities.get(entity);
132        let old_offset = meta.offset as usize;
133        let len = meta.len;
134        let save_level = meta.save_level;
135        let is_global = meta.is_global();
136        let created_after_save = meta.created_after_save;
137
138        // Copy data to a new region
139        let temp: Vec<u8> = self.data[old_offset..old_offset + len as usize].to_vec();
140        let new_offset = self.data.len() as u32;
141        self.data.extend_from_slice(&temp);
142
143        // Create a new entity pointing at the OLD data (this is the backup)
144        let copy_id = self.entities.allocate(
145            meta.offset, // points to original data
146            len,
147            save_level,
148            is_global,
149            created_after_save,
150        );
151
152        self.entities.get_mut(copy_id).set_cow_backup();
153
154        // Update the original entity to point at the NEW copy
155        self.entities.get_mut(entity).offset = new_offset;
156
157        copy_id
158    }
159
160    /// Swap offsets between two entities (used by restore).
161    pub fn swap_offsets(&mut self, a: EntityId, b: EntityId) {
162        let off_a = self.entities.get(a).offset;
163        let off_b = self.entities.get(b).offset;
164        self.entities.get_mut(a).offset = off_b;
165        self.entities.get_mut(b).offset = off_a;
166    }
167
168    /// Access to the backing data (for advanced operations).
169    pub fn data(&self) -> &[u8] {
170        &self.data
171    }
172}
173
174impl Default for StringStore {
175    fn default() -> Self {
176        Self::new()
177    }
178}
179
180#[cfg(test)]
181mod tests {
182    use super::*;
183
184    #[test]
185    fn test_allocate_from() {
186        let mut store = StringStore::new();
187        let id = store.allocate_from(b"hello");
188        assert_eq!(store.get(id, 0, 5), b"hello");
189    }
190
191    #[test]
192    fn test_allocate_zeroed() {
193        let mut store = StringStore::new();
194        let id = store.allocate(3);
195        assert_eq!(store.get(id, 0, 3), &[0, 0, 0]);
196    }
197
198    #[test]
199    fn test_put_get_byte() {
200        let mut store = StringStore::new();
201        let id = store.allocate(3);
202        store.put_byte(id, 1, 42);
203        assert_eq!(store.get_byte(id, 0), 0);
204        assert_eq!(store.get_byte(id, 1), 42);
205    }
206
207    #[test]
208    fn test_multiple_strings() {
209        let mut store = StringStore::new();
210        let id1 = store.allocate_from(b"abc");
211        let id2 = store.allocate_from(b"xyz");
212        assert_eq!(store.get(id1, 0, 3), b"abc");
213        assert_eq!(store.get(id2, 0, 3), b"xyz");
214    }
215
216    #[test]
217    fn test_entity_indirection() {
218        let mut store = StringStore::new();
219        let id = store.allocate_from(b"test");
220        let meta = store.entities.get(id);
221        assert_eq!(meta.len, 4);
222        assert_eq!(meta.save_level, 0);
223        assert!(!meta.is_global());
224    }
225
226    #[test]
227    fn test_cow_copy() {
228        let mut store = StringStore::new();
229        let id = store.allocate_from(b"hello");
230
231        // Mutate via the original entity
232        store.put_byte(id, 0, b'H');
233        assert_eq!(store.get(id, 0, 5), b"Hello");
234
235        // COW copy: backup the original, original now points to copy
236        let backup = store.cow_copy(id);
237
238        // Modify the original — should not affect the backup
239        store.put_byte(id, 1, b'a');
240        assert_eq!(store.get(id, 0, 5), b"Hallo");
241        assert_eq!(store.get(backup, 0, 5), b"Hello");
242    }
243
244    #[test]
245    fn test_swap_offsets() {
246        let mut store = StringStore::new();
247        let id1 = store.allocate_from(b"aaa");
248        let id2 = store.allocate_from(b"bbb");
249
250        store.swap_offsets(id1, id2);
251        assert_eq!(store.get(id1, 0, 3), b"bbb");
252        assert_eq!(store.get(id2, 0, 3), b"aaa");
253    }
254
255    #[test]
256    fn test_allocate_with_save_level() {
257        let mut store = StringStore::new();
258        let id = store.allocate_with(5, 2, true, 0);
259        let meta = store.entities.get(id);
260        assert_eq!(meta.save_level, 2);
261        assert!(meta.is_global());
262    }
263}