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    /// Release every byte and entity from the given marks onward.
34    ///
35    /// Used by `restore` to reclaim the objects a save level created. See
36    /// [`crate::entity_table::EntityTable::truncate`] for the safety argument
37    /// and the `EntityId`-reuse caveat.
38    pub fn truncate_to(&mut self, data_len: usize, entity_len: usize) {
39        self.data.truncate(data_len);
40        self.entities.truncate(entity_len);
41    }
42
43    pub fn allocate(&mut self, len: usize) -> EntityId {
44        let offset = self.data.len() as u32;
45        self.data.resize(self.data.len() + len, 0);
46        self.entities.allocate(offset, len as u32, 0, false, 0)
47    }
48
49    /// Allocate and copy `bytes` into the store.
50    pub fn allocate_from(&mut self, bytes: &[u8]) -> EntityId {
51        let offset = self.data.len() as u32;
52        self.data.extend_from_slice(bytes);
53        self.entities
54            .allocate(offset, bytes.len() as u32, 0, false, 0)
55    }
56
57    /// Allocate and copy `bytes` with a specific save level and global flag.
58    pub fn allocate_from_with(
59        &mut self,
60        bytes: &[u8],
61        save_level: u16,
62        global: bool,
63        created_after_save: u32,
64    ) -> EntityId {
65        let offset = self.data.len() as u32;
66        self.data.extend_from_slice(bytes);
67        self.entities.allocate(
68            offset,
69            bytes.len() as u32,
70            save_level,
71            global,
72            created_after_save,
73        )
74    }
75
76    /// Allocate with a specific save level and global flag.
77    pub fn allocate_with(
78        &mut self,
79        len: usize,
80        save_level: u16,
81        global: bool,
82        created_after_save: u32,
83    ) -> EntityId {
84        let offset = self.data.len() as u32;
85        self.data.resize(self.data.len() + len, 0);
86        self.entities
87            .allocate(offset, len as u32, save_level, global, created_after_save)
88    }
89
90    /// Get a slice of the string data via entity table indirection.
91    /// `start` is the byte offset from the entity's base; `len` is the number of bytes.
92    pub fn get(&self, entity: EntityId, start: u32, len: u32) -> &[u8] {
93        let base = self.entities.get(entity).offset as usize + start as usize;
94        &self.data[base..base + len as usize]
95    }
96
97    /// Get a mutable slice of the string data via entity table indirection.
98    /// `start` is the byte offset from the entity's base; `len` is the number of bytes.
99    pub fn get_mut(&mut self, entity: EntityId, start: u32, len: u32) -> &mut [u8] {
100        let base = self.entities.get(entity).offset as usize + start as usize;
101        &mut self.data[base..base + len as usize]
102    }
103
104    /// Set a single byte.
105    pub fn put_byte(&mut self, entity: EntityId, offset: u32, byte: u8) {
106        let base = self.entities.get(entity).offset as usize;
107        self.data[base + offset as usize] = byte;
108    }
109
110    /// Get a single byte.
111    pub fn get_byte(&self, entity: EntityId, offset: u32) -> u8 {
112        let base = self.entities.get(entity).offset as usize;
113        self.data[base + offset as usize]
114    }
115
116    /// Copy entity data to a new region (for COW). Returns the new EntityId
117    /// pointing to the copy. The original entity's offset is updated to
118    /// point at the copy, so the original EntityId now sees the new data.
119    pub fn cow_copy(&mut self, entity: EntityId) -> EntityId {
120        let meta = self.entities.get(entity);
121        let old_offset = meta.offset as usize;
122        let len = meta.len;
123        let save_level = meta.save_level;
124        let is_global = meta.is_global();
125        let created_after_save = meta.created_after_save;
126
127        // Copy data to a new region
128        let temp: Vec<u8> = self.data[old_offset..old_offset + len as usize].to_vec();
129        let new_offset = self.data.len() as u32;
130        self.data.extend_from_slice(&temp);
131
132        // Create a new entity pointing at the OLD data (this is the backup)
133        let copy_id = self.entities.allocate(
134            meta.offset, // points to original data
135            len,
136            save_level,
137            is_global,
138            created_after_save,
139        );
140
141        self.entities.get_mut(copy_id).set_cow_backup();
142
143        // Update the original entity to point at the NEW copy
144        self.entities.get_mut(entity).offset = new_offset;
145
146        copy_id
147    }
148
149    /// Swap offsets between two entities (used by restore).
150    pub fn swap_offsets(&mut self, a: EntityId, b: EntityId) {
151        let off_a = self.entities.get(a).offset;
152        let off_b = self.entities.get(b).offset;
153        self.entities.get_mut(a).offset = off_b;
154        self.entities.get_mut(b).offset = off_a;
155    }
156
157    /// Access to the backing data (for advanced operations).
158    pub fn data(&self) -> &[u8] {
159        &self.data
160    }
161}
162
163impl Default for StringStore {
164    fn default() -> Self {
165        Self::new()
166    }
167}
168
169#[cfg(test)]
170mod tests {
171    use super::*;
172
173    #[test]
174    fn test_allocate_from() {
175        let mut store = StringStore::new();
176        let id = store.allocate_from(b"hello");
177        assert_eq!(store.get(id, 0, 5), b"hello");
178    }
179
180    #[test]
181    fn test_allocate_zeroed() {
182        let mut store = StringStore::new();
183        let id = store.allocate(3);
184        assert_eq!(store.get(id, 0, 3), &[0, 0, 0]);
185    }
186
187    #[test]
188    fn test_put_get_byte() {
189        let mut store = StringStore::new();
190        let id = store.allocate(3);
191        store.put_byte(id, 1, 42);
192        assert_eq!(store.get_byte(id, 0), 0);
193        assert_eq!(store.get_byte(id, 1), 42);
194    }
195
196    #[test]
197    fn test_multiple_strings() {
198        let mut store = StringStore::new();
199        let id1 = store.allocate_from(b"abc");
200        let id2 = store.allocate_from(b"xyz");
201        assert_eq!(store.get(id1, 0, 3), b"abc");
202        assert_eq!(store.get(id2, 0, 3), b"xyz");
203    }
204
205    #[test]
206    fn test_entity_indirection() {
207        let mut store = StringStore::new();
208        let id = store.allocate_from(b"test");
209        let meta = store.entities.get(id);
210        assert_eq!(meta.len, 4);
211        assert_eq!(meta.save_level, 0);
212        assert!(!meta.is_global());
213    }
214
215    #[test]
216    fn test_cow_copy() {
217        let mut store = StringStore::new();
218        let id = store.allocate_from(b"hello");
219
220        // Mutate via the original entity
221        store.put_byte(id, 0, b'H');
222        assert_eq!(store.get(id, 0, 5), b"Hello");
223
224        // COW copy: backup the original, original now points to copy
225        let backup = store.cow_copy(id);
226
227        // Modify the original — should not affect the backup
228        store.put_byte(id, 1, b'a');
229        assert_eq!(store.get(id, 0, 5), b"Hallo");
230        assert_eq!(store.get(backup, 0, 5), b"Hello");
231    }
232
233    #[test]
234    fn test_swap_offsets() {
235        let mut store = StringStore::new();
236        let id1 = store.allocate_from(b"aaa");
237        let id2 = store.allocate_from(b"bbb");
238
239        store.swap_offsets(id1, id2);
240        assert_eq!(store.get(id1, 0, 3), b"bbb");
241        assert_eq!(store.get(id2, 0, 3), b"aaa");
242    }
243
244    #[test]
245    fn test_allocate_with_save_level() {
246        let mut store = StringStore::new();
247        let id = store.allocate_with(5, 2, true, 0);
248        let meta = store.entities.get(id);
249        assert_eq!(meta.save_level, 2);
250        assert!(meta.is_global());
251    }
252}