1use serde_json::{Map, Value};
16
17const MAX_DEPTH: usize = 32;
18const MAX_NODES: usize = 100_000;
19
20#[derive(Debug, PartialEq)]
22pub struct Parsed {
23 pub value: Value,
24 pub warnings: Vec<String>,
25}
26
27pub fn parse(src: &str) -> Result<Parsed, String> {
29 parse_inner(src, None)
30}
31
32pub fn parse_var(src: &str, name: &str) -> Result<Parsed, String> {
36 parse_inner(src, Some(name))
37}
38
39fn parse_inner(src: &str, var: Option<&str>) -> Result<Parsed, String> {
40 let mut p = P {
41 s: src.as_bytes(),
42 i: 0,
43 nodes: 0,
44 warnings: Vec::new(),
45 };
46 p.skip_to_array(var)?;
47 let value = p.array(0)?;
48 Ok(Parsed {
49 value,
50 warnings: p.warnings,
51 })
52}
53
54struct P<'a> {
55 s: &'a [u8],
56 i: usize,
57 nodes: usize,
58 warnings: Vec<String>,
59}
60
61impl P<'_> {
62 fn line(&self) -> usize {
63 self.line_at(self.i)
64 }
65
66 fn line_at(&self, at: usize) -> usize {
69 1 + self.s[..at.min(self.s.len())]
70 .iter()
71 .filter(|&&b| b == b'\n')
72 .count()
73 }
74
75 fn peek(&self) -> Option<u8> {
76 self.s.get(self.i).copied()
77 }
78
79 fn ws(&mut self) {
80 loop {
81 match self.peek() {
82 Some(b' ' | b'\t' | b'\r' | b'\n') => self.i += 1,
83 Some(b'#') => self.line_comment(),
84 Some(b'/') if self.s.get(self.i + 1) == Some(&b'/') => self.line_comment(),
85 Some(b'/') if self.s.get(self.i + 1) == Some(&b'*') => {
86 self.i += 2;
87 while self.i < self.s.len()
88 && !(self.s[self.i] == b'*' && self.s.get(self.i + 1) == Some(&b'/'))
89 {
90 self.i += 1;
91 }
92 self.i = (self.i + 2).min(self.s.len());
93 }
94 _ => return,
95 }
96 }
97 }
98
99 fn line_comment(&mut self) {
100 while let Some(b) = self.peek() {
101 if b == b'\n' {
102 return;
103 }
104 self.i += 1;
105 }
106 }
107
108 fn skip_to_array(&mut self, var: Option<&str>) -> Result<(), String> {
111 while self.i < self.s.len() {
112 self.ws();
113 match self.peek() {
114 Some(b'$') if var.is_some() => {
115 let want = var.unwrap_or("").as_bytes();
116 let name_end = self.i + 1 + want.len();
117 let named = self.s.get(self.i + 1..name_end) == Some(want)
118 && !self
119 .s
120 .get(name_end)
121 .is_some_and(|b| b.is_ascii_alphanumeric() || *b == b'_');
122 self.i += 1;
123 if named {
124 self.i = name_end;
125 self.ws();
126 if self.peek() == Some(b'=') && self.s.get(self.i + 1) != Some(&b'=') {
127 self.i += 1;
128 self.ws();
129 return Ok(());
130 }
131 }
132 }
133 Some(b'=') if var.is_none() => {
134 self.i += 1;
135 self.ws();
136 return Ok(());
137 }
138 Some(b'\'' | b'"') => {
139 self.string()?;
140 }
141 Some(_) => self.i += 1,
142 None => break,
143 }
144 }
145 Err("No `$CONFIG = array(...)` assignment found".into())
146 }
147
148 fn keyword_ci(&mut self, kw: &str) -> bool {
149 let end = self.i + kw.len();
150 if end <= self.s.len() && self.s[self.i..end].eq_ignore_ascii_case(kw.as_bytes()) {
151 let after = self.s.get(end).copied();
152 if !after.is_some_and(|b| b.is_ascii_alphanumeric() || b == b'_') {
153 self.i = end;
154 return true;
155 }
156 }
157 false
158 }
159
160 fn array(&mut self, depth: usize) -> Result<Value, String> {
161 if depth > MAX_DEPTH {
162 return Err(format!(
163 "Arrays nest deeper than {MAX_DEPTH} (line {})",
164 self.line()
165 ));
166 }
167 self.ws();
168 let close = if self.keyword_ci("array") {
169 self.ws();
170 if self.peek() != Some(b'(') {
171 return Err(format!(
172 "Expected `(` after `array` on line {}",
173 self.line()
174 ));
175 }
176 self.i += 1;
177 b')'
178 } else if self.peek() == Some(b'[') {
179 self.i += 1;
180 b']'
181 } else {
182 return Err(format!("Expected an array on line {}", self.line()));
183 };
184
185 let mut items: Vec<(Option<Value>, Value)> = Vec::new();
186 loop {
187 self.ws();
188 match self.peek() {
189 None => return Err("The array is never closed".into()),
190 Some(b) if b == close => {
191 self.i += 1;
192 break;
193 }
194 Some(b',') => {
195 self.i += 1;
196 continue;
197 }
198 _ => {}
199 }
200 self.nodes += 1;
201 if self.nodes > MAX_NODES {
202 return Err("The file has too many entries to read".into());
203 }
204 let first = self.value(depth, close)?;
205 self.ws();
206 if self.s[self.i..].starts_with(b"=>") {
207 self.i += 2;
208 let v = self.value(depth, close)?;
209 items.push((Some(first), v));
210 } else {
211 items.push((None, first));
212 }
213 }
214
215 let sequential = items.iter().enumerate().all(|(n, (k, _))| match k {
217 None => true,
218 Some(Value::Number(x)) => x.as_u64() == Some(n as u64),
219 _ => false,
220 });
221 if sequential {
222 return Ok(Value::Array(items.into_iter().map(|(_, v)| v).collect()));
223 }
224 let mut m = Map::new();
225 for (n, (k, v)) in items.into_iter().enumerate() {
226 let key = match k {
227 Some(Value::String(s)) => s,
228 Some(Value::Number(x)) => x.to_string(),
229 Some(_) | None => n.to_string(),
230 };
231 m.insert(key, v);
232 }
233 Ok(Value::Object(m))
234 }
235
236 fn value(&mut self, depth: usize, close: u8) -> Result<Value, String> {
237 self.ws();
238 let start = self.i;
239 let v = match self.peek() {
240 Some(b'\'' | b'"') => Value::String(self.string()?),
241 Some(b'[') => self.array(depth + 1)?,
242 Some(b) if b.is_ascii_digit() || b == b'-' || b == b'+' => self.number(),
243 _ if self.keyword_ci("array") => {
244 self.i -= "array".len();
245 self.array(depth + 1)?
246 }
247 _ if self.keyword_ci("true") => Value::Bool(true),
248 _ if self.keyword_ci("false") => Value::Bool(false),
249 _ if self.keyword_ci("null") => Value::Null,
250 _ => {
251 self.skip_expression(close);
252 self.warnings.push(format!(
253 "line {}: an expression that needs PHP to evaluate was left out",
254 self.line_at(start)
255 ));
256 return Ok(Value::Null);
257 }
258 };
259 self.ws();
261 if self.peek() == Some(b'.') {
262 self.skip_expression(close);
263 self.warnings.push(format!(
264 "line {}: a concatenation was left out",
265 self.line_at(start)
266 ));
267 return Ok(Value::Null);
268 }
269 Ok(v)
270 }
271
272 fn skip_expression(&mut self, close: u8) {
274 let mut depth = 0usize;
275 while let Some(b) = self.peek() {
276 match b {
277 b'\'' | b'"' => {
278 let _ = self.string();
279 continue;
280 }
281 b'(' | b'[' => depth += 1,
282 b')' | b']' if depth > 0 => depth -= 1,
283 b',' if depth == 0 => return,
284 b'=' if depth == 0 && self.s.get(self.i + 1) == Some(&b'>') => return,
285 b if depth == 0 && b == close => return,
286 _ => {}
287 }
288 self.i += 1;
289 }
290 }
291
292 fn number(&mut self) -> Value {
293 let start = self.i;
294 if matches!(self.peek(), Some(b'-' | b'+')) {
295 self.i += 1;
296 }
297 while matches!(self.peek(), Some(b) if b.is_ascii_digit() || b == b'.' || b == b'e' || b == b'E' || b == b'_')
298 {
299 self.i += 1;
300 }
301 let text: String = String::from_utf8_lossy(&self.s[start..self.i]).replace('_', "");
302 if let Ok(n) = text.parse::<i64>() {
303 return Value::from(n);
304 }
305 text.parse::<f64>()
306 .ok()
307 .and_then(serde_json::Number::from_f64)
308 .map_or(Value::Null, Value::Number)
309 }
310
311 fn string(&mut self) -> Result<String, String> {
312 let quote = self.s[self.i];
313 let start = self.i;
314 self.i += 1;
315 let mut out: Vec<u8> = Vec::new();
316 loop {
317 let Some(b) = self.peek() else {
318 return Err(format!(
319 "The string opened on line {} is never closed",
320 self.line_at(start)
321 ));
322 };
323 self.i += 1;
324 if b == quote {
325 break;
326 }
327 if b != b'\\' {
328 out.push(b);
329 continue;
330 }
331 let Some(e) = self.peek() else {
332 return Err(format!(
333 "The string opened on line {} is never closed",
334 self.line_at(start)
335 ));
336 };
337 self.i += 1;
338 if quote == b'\'' {
339 match e {
341 b'\\' | b'\'' => out.push(e),
342 _ => {
343 out.push(b'\\');
344 out.push(e);
345 }
346 }
347 } else {
348 match e {
349 b'n' => out.push(b'\n'),
350 b't' => out.push(b'\t'),
351 b'r' => out.push(b'\r'),
352 b'v' => out.push(0x0b),
353 b'e' => out.push(0x1b),
354 b'f' => out.push(0x0c),
355 b'\\' | b'"' | b'$' => out.push(e),
356 b'0'..=b'7' => {
357 let mut n = u32::from(e - b'0');
358 for _ in 0..2 {
359 match self.peek() {
360 Some(d @ b'0'..=b'7') => {
361 n = n * 8 + u32::from(d - b'0');
362 self.i += 1;
363 }
364 _ => break,
365 }
366 }
367 out.push((n & 0xff) as u8);
368 }
369 b'x' => {
370 let mut n = 0u32;
371 let mut got = 0;
372 while got < 2 {
373 match self.peek().and_then(|d| (d as char).to_digit(16)) {
374 Some(d) => {
375 n = n * 16 + d;
376 self.i += 1;
377 got += 1;
378 }
379 None => break,
380 }
381 }
382 if got == 0 {
383 out.extend_from_slice(b"\\x");
384 } else {
385 out.push(n as u8);
386 }
387 }
388 _ => {
389 out.push(b'\\');
390 out.push(e);
391 }
392 }
393 }
394 }
395 Ok(String::from_utf8_lossy(&out).into_owned())
398 }
399}
400
401#[cfg(test)]
402mod tests {
403 use super::*;
404 use serde_json::json;
405
406 #[test]
407 fn reads_the_shape_nextcloud_writes() {
408 let src = r#"<?php
409$CONFIG = array (
410 'passwordsalt' => 'abc\'def',
411 'secret' => "line\nbreak $x",
412 'trusted_domains' =>
413 array (
414 0 => 'localhost',
415 1 => 'cloud.example.com',
416 ),
417 'dbtype' => 'sqlite3',
418 'version' => '29.0.4.1',
419 'installed' => true,
420 'maintenance' => false,
421 'port' => 3306,
422 'ratio' => 1.5,
423 'objectstore' =>
424 array (
425 'class' => '\\OC\\Files\\ObjectStore\\S3',
426 'arguments' =>
427 array (
428 'bucket' => 'b',
429 'key' => 'AKIA',
430 'secret' => 's3cret',
431 ),
432 ),
433);
434"#;
435 let p = parse(src).unwrap();
436 assert!(p.warnings.is_empty(), "{:?}", p.warnings);
437 assert_eq!(p.value["passwordsalt"], "abc'def");
438 assert_eq!(p.value["secret"], "line\nbreak $x");
439 assert_eq!(
440 p.value["trusted_domains"],
441 json!(["localhost", "cloud.example.com"])
442 );
443 assert_eq!(p.value["installed"], true);
444 assert_eq!(p.value["port"], 3306);
445 assert_eq!(p.value["ratio"], 1.5);
446 assert_eq!(
447 p.value["objectstore"]["class"],
448 "\\OC\\Files\\ObjectStore\\S3"
449 );
450 assert_eq!(p.value["objectstore"]["arguments"]["secret"], "s3cret");
451 }
452
453 #[test]
454 fn short_arrays_comments_and_trailing_commas() {
455 let p = parse("<?php\n// a\n$c = [ # b\n 'a' => 1, /* c */ 'b' => [1, 2,], ];").unwrap();
456 assert_eq!(p.value, json!({"a": 1, "b": [1, 2]}));
457 }
458
459 #[test]
460 fn expressions_that_need_php_are_left_out_and_reported() {
461 let p = parse(
462 "<?php $c = array('a' => getenv('X'), 'b' => 'x' . 'y', 'c' => OC::$ROOT . '/d', 'e' => 'ok');",
463 )
464 .unwrap();
465 assert_eq!(p.value["e"], "ok");
466 assert_eq!(p.value["a"], Value::Null);
467 assert_eq!(p.value["b"], Value::Null);
468 assert_eq!(p.value["c"], Value::Null);
469 assert_eq!(p.warnings.len(), 3, "{:?}", p.warnings);
470 }
471
472 #[test]
473 fn reads_the_named_variable_from_a_script_and_not_the_first_equals() {
474 let src = "<?php\n$use = getenv('X');\nif ($use) {\n $CONFIG = array('a' => getenv('A') ?: 'd', 'b' => 'ok');\n}";
475 assert!(parse(src).is_err(), "the first `=` is not an array");
476 let p = parse_var(src, "CONFIG").unwrap();
477 assert_eq!(p.value["b"], "ok");
478 assert!(!p.warnings.is_empty());
479 assert!(parse_var("<?php $CONFIGX = [1];", "CONFIG").is_err());
480 assert!(parse_var("<?php $CONFIG == [1];", "CONFIG").is_err());
481 }
482
483 #[test]
484 fn refuses_what_it_cannot_bound_or_close() {
485 assert!(parse("<?php $c = array('a' => 'unterminated);").is_err());
486 assert!(parse("<?php nothing here").is_err());
487 assert!(parse("<?php $c = array('a' => 1").is_err());
488 let deep = format!("<?php $c = {}{}", "[".repeat(200), "]".repeat(200));
489 assert!(parse(&deep).unwrap_err().contains("nest deeper"));
490 let wide = format!("<?php $c = [{}];", "1,".repeat(150_000));
491 assert!(parse(&wide).unwrap_err().contains("too many"));
492 }
493}