aft/hashline/apply/
registers.rs1use std::collections::BTreeMap;
9
10use crate::hashline::syntax::{
11 HashlineRejection, HashlineRejectionCode, RegisterRef, RejectionStage,
12};
13
14pub const MAX_NAMED_REGISTERS: usize = 64;
16pub const MAX_REGISTER_BYTES: usize = 8 * 1024 * 1024;
18pub const MAX_REGISTER_TOTAL_BYTES: usize = 32 * 1024 * 1024;
20
21pub type RegisterLines = Vec<String>;
23
24#[derive(Clone, Debug, Default, Eq, PartialEq)]
28pub struct RegisterStore {
29 named: BTreeMap<String, RegisterLines>,
30 anonymous: Option<RegisterLines>,
31}
32
33impl RegisterStore {
34 pub fn new() -> Self {
35 Self::default()
36 }
37
38 pub fn named_count(&self) -> usize {
39 self.named.len()
40 }
41
42 pub fn get(&self, register: &RegisterRef) -> Option<&[String]> {
43 match register {
44 RegisterRef::Anonymous => self.anonymous.as_deref(),
45 RegisterRef::Named(name) => self.named.get(name).map(Vec::as_slice),
46 }
47 }
48
49 pub fn total_bytes(&self) -> usize {
50 let named: usize = self
51 .named
52 .values()
53 .map(|lines| register_payload_bytes(lines))
54 .sum();
55 let anonymous = self
56 .anonymous
57 .as_ref()
58 .map(|lines| register_payload_bytes(lines))
59 .unwrap_or(0);
60 named + anonymous
61 }
62
63 pub fn stage(&self) -> StagedRegisters {
66 StagedRegisters {
67 base: self.clone(),
68 working: self.clone(),
69 writes: Vec::new(),
70 }
71 }
72
73 pub fn commit(&mut self, staged: StagedRegisters) {
76 *self = staged.working;
77 }
78
79 pub fn discard(_staged: StagedRegisters) {}
82
83 pub fn clear(&mut self) {
84 self.named.clear();
85 self.anonymous = None;
86 }
87}
88
89#[derive(Clone, Debug, Eq, PartialEq)]
91pub struct StagedRegisters {
92 base: RegisterStore,
93 working: RegisterStore,
94 writes: Vec<RegisterWrite>,
95}
96
97#[derive(Clone, Debug, Eq, PartialEq)]
99pub struct RegisterWrite {
100 pub register: RegisterRef,
101 pub lines: RegisterLines,
102}
103
104impl StagedRegisters {
105 pub fn writes(&self) -> &[RegisterWrite] {
106 &self.writes
107 }
108
109 pub fn working(&self) -> &RegisterStore {
110 &self.working
111 }
112
113 pub fn base(&self) -> &RegisterStore {
114 &self.base
115 }
116
117 pub fn get(&self, register: &RegisterRef) -> Option<&[String]> {
118 self.working.get(register)
119 }
120
121 pub fn capture(
123 &mut self,
124 register: RegisterRef,
125 lines: RegisterLines,
126 ) -> Result<(), HashlineRejection> {
127 check_register_bounds(&self.working, ®ister, &lines)?;
128 match ®ister {
129 RegisterRef::Anonymous => {
130 self.working.anonymous = Some(lines.clone());
131 }
132 RegisterRef::Named(name) => {
133 if !self.working.named.contains_key(name)
134 && self.working.named.len() >= MAX_NAMED_REGISTERS
135 {
136 return Err(register_overflow(
137 "named register count would exceed MAX_NAMED_REGISTERS",
138 ));
139 }
140 self.working.named.insert(name.clone(), lines.clone());
141 }
142 }
143 self.writes.push(RegisterWrite { register, lines });
144 Ok(())
145 }
146
147 pub fn read_for_put(
151 &self,
152 register: &RegisterRef,
153 target_is_span: bool,
154 ) -> Result<RegisterLines, HashlineRejection> {
155 match self.get(register) {
156 Some(lines) if !lines.is_empty() || !target_is_span => Ok(lines.to_vec()),
157 Some(_) => Err(HashlineRejection::new(
158 HashlineRejectionCode::ParseError,
159 RejectionStage::Register,
160 "register paste over a span requires a non-empty capture",
161 )),
162 None if matches!(register, RegisterRef::Named(_)) && !target_is_span => Ok(Vec::new()),
163 None => Err(HashlineRejection::new(
164 HashlineRejectionCode::ParseError,
165 RejectionStage::Register,
166 "register is empty or unknown for this PUT",
167 )),
168 }
169 }
170}
171
172fn register_payload_bytes(lines: &[String]) -> usize {
173 if lines.is_empty() {
174 return 0;
175 }
176 lines.iter().map(|line| line.len()).sum::<usize>() + lines.len().saturating_sub(1)
177}
178
179fn check_register_bounds(
180 store: &RegisterStore,
181 register: &RegisterRef,
182 lines: &[String],
183) -> Result<(), HashlineRejection> {
184 let incoming = register_payload_bytes(lines);
185 if incoming > MAX_REGISTER_BYTES {
186 return Err(register_overflow(
187 "a single register capture exceeds MAX_REGISTER_BYTES",
188 ));
189 }
190
191 let previous = store.get(register).map(register_payload_bytes).unwrap_or(0);
192 let total = store.total_bytes() - previous + incoming;
193 if total > MAX_REGISTER_TOTAL_BYTES {
194 return Err(register_overflow(
195 "register captures would exceed MAX_REGISTER_TOTAL_BYTES",
196 ));
197 }
198
199 if let RegisterRef::Named(name) = register {
200 if !store.named.contains_key(name) && store.named.len() >= MAX_NAMED_REGISTERS {
201 return Err(register_overflow(
202 "named register count would exceed MAX_NAMED_REGISTERS",
203 ));
204 }
205 }
206 Ok(())
207}
208
209fn register_overflow(message: impl Into<String>) -> HashlineRejection {
210 HashlineRejection::new(
211 HashlineRejectionCode::RegisterOverflow,
212 RejectionStage::Register,
213 message,
214 )
215}
216
217#[cfg(test)]
218mod tests {
219 use super::*;
220
221 #[test]
222 fn named_register_count_bound_rejects_without_eviction() {
223 let store = RegisterStore::new();
224 let mut staged = store.stage();
225 for index in 0..MAX_NAMED_REGISTERS {
226 staged
227 .capture(RegisterRef::Named(format!("r{index}")), vec!["x".into()])
228 .expect("within bound");
229 }
230 let err = staged
231 .capture(RegisterRef::Named("overflow".into()), vec!["y".into()])
232 .expect_err("one past the bound");
233 assert_eq!(err.code, HashlineRejectionCode::RegisterOverflow);
234 assert_eq!(err.stage, RejectionStage::Register);
235 RegisterStore::discard(staged);
236 assert_eq!(store.named_count(), 0);
237 }
238
239 #[test]
240 fn commit_publishes_only_after_explicit_success() {
241 let mut store = RegisterStore::new();
242 let mut staged = store.stage();
243 staged
244 .capture(RegisterRef::Named("clip".into()), vec!["body".into()])
245 .unwrap();
246 assert!(store.get(&RegisterRef::Named("clip".into())).is_none());
247 store.commit(staged);
248 assert_eq!(
249 store.get(&RegisterRef::Named("clip".into())),
250 Some(["body".to_string()].as_slice())
251 );
252 }
253
254 #[test]
255 fn discard_leaves_session_registers_untouched() {
256 let mut store = RegisterStore::new();
257 let mut baseline = store.stage();
258 baseline
259 .capture(RegisterRef::Named("keep".into()), vec!["stable".into()])
260 .unwrap();
261 store.commit(baseline);
262
263 let mut staged = store.stage();
264 staged
265 .capture(RegisterRef::Named("temp".into()), vec!["scratch".into()])
266 .unwrap();
267 RegisterStore::discard(staged);
268 assert_eq!(
269 store.get(&RegisterRef::Named("keep".into())),
270 Some(["stable".to_string()].as_slice())
271 );
272 assert!(store.get(&RegisterRef::Named("temp".into())).is_none());
273 }
274}