1use crate::{DataValue, ErrorCode, FileMakerError, Result};
14
15#[derive(Clone, Debug, Eq, PartialEq)]
17pub struct Expression {
18 source: String,
19}
20
21impl Expression {
22 pub fn parse(source: impl Into<String>) -> Result<Self> {
24 let source = source.into();
25 if source.is_empty() || source.len() > 4_096 {
26 return Err(expression_error("expression is empty or too long"));
27 }
28 if source.contains(['(', ')', ';', '`']) {
29 return Err(expression_error(
30 "functions and arbitrary evaluation are not supported",
31 ));
32 }
33 Ok(Self { source })
34 }
35
36 #[must_use]
38 pub fn source(&self) -> &str {
39 &self.source
40 }
41
42 pub fn dependencies(&self) -> Vec<String> {
47 let mut dependencies = std::collections::BTreeSet::new();
48 collect_dependencies(self.source.trim(), &mut dependencies);
49 dependencies.into_iter().collect()
50 }
51
52 pub fn evaluate(&self, root: &DataValue, budget: &mut ExpressionBudget) -> Result<DataValue> {
54 evaluate_expression(self.source.trim(), root, budget)
55 }
56}
57
58fn collect_dependencies(source: &str, dependencies: &mut std::collections::BTreeSet<String>) {
59 for operator in ["||", "&&", "==", "!=", "+"] {
60 if let Some((left, right)) = split_operator(source, operator) {
61 collect_dependencies(left.trim(), dependencies);
62 collect_dependencies(right.trim(), dependencies);
63 return;
64 }
65 }
66 let atom = source.trim();
67 if atom.is_empty()
68 || atom.starts_with('"')
69 || matches!(atom, "true" | "false" | "null")
70 || atom.parse::<i64>().is_ok()
71 {
72 return;
73 }
74 let path = atom.strip_prefix("data.").unwrap_or(atom);
75 if let Some(root) = path.split('.').next().filter(|root| !root.is_empty()) {
76 dependencies.insert(root.to_owned());
77 }
78}
79
80#[derive(Clone, Debug, Eq, PartialEq)]
82pub struct ExpressionBudget {
83 remaining: usize,
84}
85
86impl ExpressionBudget {
87 pub fn new(max_steps: usize) -> Result<Self> {
89 if max_steps == 0 {
90 return Err(FileMakerError::new(
91 ErrorCode::LimitExceeded,
92 "expression step budget must be non-zero",
93 ));
94 }
95 Ok(Self {
96 remaining: max_steps,
97 })
98 }
99
100 fn step(&mut self) -> Result<()> {
101 self.remaining = self.remaining.checked_sub(1).ok_or_else(|| {
102 FileMakerError::new(ErrorCode::LimitExceeded, "expression step budget exceeded")
103 })?;
104 Ok(())
105 }
106}
107
108fn evaluate_expression(
109 source: &str,
110 root: &DataValue,
111 budget: &mut ExpressionBudget,
112) -> Result<DataValue> {
113 budget.step()?;
114 if let Some((left, right)) = split_operator(source, "||") {
115 return Ok(DataValue::Boolean(
116 evaluate_expression(left, root, budget)?.is_truthy()
117 || evaluate_expression(right, root, budget)?.is_truthy(),
118 ));
119 }
120 if let Some((left, right)) = split_operator(source, "&&") {
121 return Ok(DataValue::Boolean(
122 evaluate_expression(left, root, budget)?.is_truthy()
123 && evaluate_expression(right, root, budget)?.is_truthy(),
124 ));
125 }
126 if let Some((left, right)) = split_operator(source, "==") {
127 return Ok(DataValue::Boolean(
128 evaluate_expression(left, root, budget)? == evaluate_expression(right, root, budget)?,
129 ));
130 }
131 if let Some((left, right)) = split_operator(source, "!=") {
132 return Ok(DataValue::Boolean(
133 evaluate_expression(left, root, budget)? != evaluate_expression(right, root, budget)?,
134 ));
135 }
136 if let Some((left, right)) = split_operator(source, "+") {
137 let left = evaluate_expression(left, root, budget)?;
138 let right = evaluate_expression(right, root, budget)?;
139 return add(left, right);
140 }
141 atom(source, root)
142}
143
144fn atom(source: &str, root: &DataValue) -> Result<DataValue> {
145 let source = source.trim();
146 if let Some(value) = source
147 .strip_prefix('"')
148 .and_then(|rest| rest.strip_suffix('"'))
149 {
150 return Ok(DataValue::String(value.to_owned()));
151 }
152 match source {
153 "true" => return Ok(DataValue::Boolean(true)),
154 "false" => return Ok(DataValue::Boolean(false)),
155 "null" => return Ok(DataValue::Null),
156 _ => {}
157 }
158 if let Ok(integer) = source.parse::<i64>() {
159 return Ok(DataValue::Integer(integer));
160 }
161 let path = source.strip_prefix("data.").unwrap_or(source);
162 root.get_path(path)
163 .cloned()
164 .ok_or_else(|| expression_error(format!("binding path `{path}` was not found")))
165}
166
167fn add(left: DataValue, right: DataValue) -> Result<DataValue> {
168 match (left, right) {
169 (DataValue::Integer(left), DataValue::Integer(right)) => left
170 .checked_add(right)
171 .map(DataValue::Integer)
172 .ok_or_else(|| expression_error("integer expression overflow")),
173 (left, right) => Ok(DataValue::String(format!(
174 "{}{}",
175 left.display(),
176 right.display()
177 ))),
178 }
179}
180
181fn split_operator<'a>(source: &'a str, operator: &str) -> Option<(&'a str, &'a str)> {
182 let mut quoted = false;
183 let bytes = source.as_bytes();
184 let operator = operator.as_bytes();
185 let mut index = 0;
186 while index + operator.len() <= bytes.len() {
187 if bytes[index] == b'"' {
188 quoted = !quoted;
189 }
190 if !quoted && &bytes[index..index + operator.len()] == operator {
191 return Some((&source[..index], &source[index + operator.len()..]));
192 }
193 index += 1;
194 }
195 None
196}
197
198fn expression_error(message: impl Into<String>) -> FileMakerError {
199 FileMakerError::new(ErrorCode::DataType, message)
200}
201
202#[cfg(test)]
203mod tests {
204 use std::collections::BTreeMap;
205
206 use super::*;
207
208 #[test]
209 fn evaluates_paths_and_bounded_operators() {
210 let root = DataValue::Object(BTreeMap::from([
211 ("name".to_owned(), DataValue::String("Ada".to_owned())),
212 ("active".to_owned(), DataValue::Boolean(true)),
213 ]));
214 let expression = Expression::parse("data.name + \"!\"").unwrap();
215 assert_eq!(
216 expression
217 .evaluate(&root, &mut ExpressionBudget::new(8).unwrap())
218 .unwrap(),
219 DataValue::String("Ada!".to_owned())
220 );
221 let condition = Expression::parse("active == true").unwrap();
222 assert!(condition
223 .evaluate(&root, &mut ExpressionBudget::new(8).unwrap())
224 .unwrap()
225 .is_truthy());
226 assert_eq!(
227 Expression::parse("data.name + active")
228 .unwrap()
229 .dependencies(),
230 vec!["active".to_owned(), "name".to_owned()]
231 );
232 }
233}