1use sim_codec::{
2 DecodePosition, DecodedForm, Input, Output, decode_default_with_codec_and_limits,
3 encode_with_codec,
4};
5use sim_expr_tree_core::FaceBudget;
6use sim_kernel::{
7 CapabilitySet, Cx, EncodeOptions, EncodePosition, Expr, ReadPolicy, Symbol, TrustLevel,
8 read_eval_capability,
9};
10use sim_table_core::TablePath;
11
12use super::*;
13
14mod budget;
15mod model;
16
17use budget::{bounded_value_expr, inspect_expr};
18pub use model::{
19 EncodedFace, FaceContent, FaceDimension, FaceIssue, FaceMetadata, FacePosition,
20 SourceEditOutcome,
21};
22
23const MAX_METADATA_MESSAGE_BYTES: usize = 1_024;
24
25impl ExprTreeCalc {
26 pub fn edit_cell_source(
31 &mut self,
32 path: TablePath,
33 input: Input,
34 caller_policy: ReadPolicy,
35 ) -> SourceEditOutcome {
36 let policy = self.effective_codec_policy(&path);
37 let position = policy.source_position();
38 let Some(codec_name) = policy.source_codec().map(str::to_owned) else {
39 return SourceEditOutcome {
40 metadata: metadata(
41 None,
42 position.into(),
43 FaceIssue::Unsupported {
44 reason: "no source codec selected".to_owned(),
45 },
46 ),
47 };
48 };
49
50 if position == DecodePosition::Eval && caller_policy.trust == TrustLevel::Untrusted {
51 return SourceEditOutcome {
52 metadata: metadata(
53 Some(codec_name),
54 position.into(),
55 FaceIssue::CodecFailure {
56 message: "eval-position source requires a trusted caller".to_owned(),
57 },
58 ),
59 };
60 }
61
62 let authority = self.effective_authority(&path);
63 let read_policy = diminished_read_policy(caller_policy, authority.capabilities(), position);
64 let codec = codec_symbol(&codec_name);
65 let limits = policy.decode_limits();
66 let mut cx = (self.context_factory)();
67 let decoded = cx.with_capabilities(authority.capabilities().clone(), |cx| {
68 decode_default_with_codec_and_limits(cx, &codec, input, read_policy, position, limits)
69 });
70 match decoded {
71 Ok(decoded) => {
72 let source = match decoded {
73 DecodedForm::Datum(datum) => Expr::from(datum),
74 DecodedForm::Term(term) => Expr::from(term),
75 };
76 self.set_cell(path, source);
77 SourceEditOutcome {
78 metadata: metadata(Some(codec_name), position.into(), FaceIssue::Complete),
79 }
80 }
81 Err(error) => SourceEditOutcome {
82 metadata: metadata(
83 Some(codec_name),
84 position.into(),
85 FaceIssue::CodecFailure {
86 message: bounded_message(error.to_string()),
87 },
88 ),
89 },
90 }
91 }
92
93 #[must_use]
96 pub fn source_face(&self, path: &TablePath) -> EncodedFace {
97 let policy = self.effective_codec_policy(path);
98 let position = encode_position_for_decode(policy.source_position());
99 let Some(codec_name) = policy.source_codec().map(str::to_owned) else {
100 return unsupported_face(None, position, "no source codec selected");
101 };
102 let source = self
103 .state
104 .read()
105 .expect("calc state poisoned")
106 .cells
107 .get(&path_key(path))
108 .cloned();
109 let Some(source) = source else {
110 return unsupported_face(Some(codec_name), position, "cell has no authored source");
111 };
112 let mut cx = (self.context_factory)();
113 encode_expr_face(
114 &mut cx,
115 codec_name,
116 position,
117 &source,
118 policy.source_budget(),
119 )
120 }
121
122 #[must_use]
128 pub fn result_face(&self, path: &TablePath) -> EncodedFace {
129 let policy = self.effective_codec_policy(path);
130 let position = policy.result_position();
131 let Some(codec_name) = policy.result_codec().map(str::to_owned) else {
132 return unsupported_face(None, position, "no result codec selected");
133 };
134 let value = match self.current_cell(path) {
135 Ok(value) => value,
136 Err(error) => {
137 return unsupported_face(
138 Some(codec_name),
139 position,
140 &format!("no current result: {}", bounded_message(error.to_string())),
141 );
142 }
143 };
144 let budget = policy.result_budget();
145 let mut cx = (self.context_factory)();
146 let expr = match bounded_value_expr(&mut cx, &value, budget) {
147 Ok(expr) => expr,
148 Err(issue) => {
149 return EncodedFace {
150 content: None,
151 metadata: metadata(Some(codec_name), position.into(), issue),
152 };
153 }
154 };
155 encode_preflighted_expr_face(&mut cx, codec_name, position, &expr, budget)
156 }
157}
158
159fn encode_expr_face(
160 cx: &mut Cx,
161 codec_name: String,
162 position: EncodePosition,
163 expr: &Expr,
164 budget: FaceBudget,
165) -> EncodedFace {
166 if let Err(issue) = inspect_expr(expr, budget) {
167 return EncodedFace {
168 content: None,
169 metadata: metadata(Some(codec_name), position.into(), issue),
170 };
171 }
172 encode_preflighted_expr_face(cx, codec_name, position, expr, budget)
173}
174
175fn encode_preflighted_expr_face(
176 cx: &mut Cx,
177 codec_name: String,
178 position: EncodePosition,
179 expr: &Expr,
180 budget: FaceBudget,
181) -> EncodedFace {
182 let codec = codec_symbol(&codec_name);
183 let options = EncodeOptions {
184 position,
185 ..EncodeOptions::default()
186 };
187 match encode_with_codec(cx, &codec, expr, options) {
188 Ok(output) => {
189 let output_bytes = output_len(&output);
190 if output_bytes > budget.max_bytes() {
191 return EncodedFace {
192 content: None,
193 metadata: metadata(
194 Some(codec_name),
195 position.into(),
196 FaceIssue::Truncated {
197 dimension: FaceDimension::Bytes,
198 limit: budget.max_bytes(),
199 observed: output_bytes,
200 },
201 ),
202 };
203 }
204 EncodedFace {
205 content: Some(match output {
206 Output::Text(text) => FaceContent::Text(text),
207 Output::Bytes(bytes) => FaceContent::Bytes(bytes),
208 }),
209 metadata: metadata(Some(codec_name), position.into(), FaceIssue::Complete),
210 }
211 }
212 Err(error) => EncodedFace {
213 content: None,
214 metadata: metadata(
215 Some(codec_name),
216 position.into(),
217 FaceIssue::CodecFailure {
218 message: bounded_message(error.to_string()),
219 },
220 ),
221 },
222 }
223}
224
225fn unsupported_face(codec: Option<String>, position: EncodePosition, reason: &str) -> EncodedFace {
226 EncodedFace {
227 content: None,
228 metadata: metadata(
229 codec,
230 position.into(),
231 FaceIssue::Unsupported {
232 reason: bounded_message(reason.to_owned()),
233 },
234 ),
235 }
236}
237
238fn output_len(output: &Output) -> usize {
239 match output {
240 Output::Text(text) => text.len(),
241 Output::Bytes(bytes) => bytes.len(),
242 }
243}
244
245fn encode_position_for_decode(position: DecodePosition) -> EncodePosition {
246 match position {
247 DecodePosition::Eval => EncodePosition::Eval,
248 DecodePosition::Quote => EncodePosition::Quote,
249 DecodePosition::Data => EncodePosition::Data,
250 DecodePosition::Pattern => EncodePosition::Pattern,
251 }
252}
253
254fn diminished_read_policy(
255 caller: ReadPolicy,
256 authority: &CapabilitySet,
257 position: DecodePosition,
258) -> ReadPolicy {
259 let mut capabilities = caller.capabilities.intersect(authority);
260 if position != DecodePosition::Eval {
261 let read_eval = read_eval_capability();
262 capabilities = capabilities
263 .iter()
264 .filter(|capability| *capability != &read_eval)
265 .cloned()
266 .fold(CapabilitySet::new(), CapabilitySet::grant);
267 }
268 ReadPolicy {
269 trust: caller.trust,
270 capabilities,
271 }
272}
273
274fn codec_symbol(name: &str) -> Symbol {
275 name.split_once('/')
276 .or_else(|| name.split_once(':'))
277 .map_or_else(
278 || Symbol::new(name.to_owned()),
279 |(namespace, local)| Symbol::qualified(namespace.to_owned(), local.to_owned()),
280 )
281}
282
283fn metadata(codec: Option<String>, position: FacePosition, issue: FaceIssue) -> FaceMetadata {
284 FaceMetadata {
285 codec,
286 position,
287 issue,
288 }
289}
290
291fn bounded_message(message: String) -> String {
292 if message.len() <= MAX_METADATA_MESSAGE_BYTES {
293 return message;
294 }
295 let mut end = MAX_METADATA_MESSAGE_BYTES;
296 while !message.is_char_boundary(end) {
297 end -= 1;
298 }
299 format!("{}...[metadata truncated]", &message[..end])
300}