1use pcre2::bytes::{Captures, Regex, RegexBuilder};
25use serde::Deserialize as _;
26use serde_json::{Map, Value};
27use vivacity_core::phpjson::{php_json_encode_with, STDCLASS_MARKER};
28
29use crate::platform::is_platform_package;
30use crate::version::preg_quote;
31
32const PCRE2_ERROR_MATCHLIMIT: i32 = -47;
35
36const INDENT_DEFAULT: &str = " ";
38
39const DEFINES: &str = r#"(?(DEFINE)
41 (?<number> -? (?= [1-9]|0(?!\d) ) \d++ (?:\.\d++)? (?:[eE] [+-]?+ \d++)? )
42 (?<boolean> true | false | null )
43 (?<string> " (?:[^"\\]*+ | \\ ["\\bfnrt\/] | \\ u [0-9A-Fa-f]{4} )* " )
44 (?<array> \[ (?: (?&json) \s*+ (?: , (?&json) \s*+ )*+ )?+ \s*+ \] )
45 (?<pair> \s*+ (?&string) \s*+ : (?&json) \s*+ )
46 (?<object> \{ (?: (?&pair) (?: , (?&pair) )*+ )?+ \s*+ \} )
47 (?<json> \s*+ (?: (?&number) | (?&boolean) | (?&string) | (?&array) | (?&object) ) )
48 )"#;
49
50#[derive(Debug, thiserror::Error)]
51pub enum ManipulatorError {
52 #[error("The json file must be an object ({{}})")]
54 NotAnObject,
55 #[error("The input does not contain valid JSON\n{0}")]
57 Parse(String),
58 #[error("regex: {0}")]
61 Regex(String),
62 #[error("{0}")]
65 Logic(String),
66 #[error("unsupported: {0}")]
68 Unsupported(String),
69}
70
71type Result<T> = std::result::Result<T, ManipulatorError>;
72
73#[derive(Clone, Copy, Default)]
75struct Flags {
76 s: bool,
77 x: bool,
78 i: bool,
79 m: bool,
80}
81
82const SX: Flags = Flags {
83 s: true,
84 x: true,
85 i: false,
86 m: false,
87};
88const X: Flags = Flags {
89 s: false,
90 x: true,
91 i: false,
92 m: false,
93};
94const IX: Flags = Flags {
95 s: false,
96 x: true,
97 i: true,
98 m: false,
99};
100const I: Flags = Flags {
101 s: false,
102 x: false,
103 i: true,
104 m: false,
105};
106const S: Flags = Flags {
107 s: true,
108 x: false,
109 i: false,
110 m: false,
111};
112const NONE: Flags = Flags {
113 s: false,
114 x: false,
115 i: false,
116 m: false,
117};
118
119fn compile(pattern: &str, flags: Flags) -> Result<Regex> {
120 RegexBuilder::new()
121 .dotall(flags.s)
122 .extended(flags.x)
123 .caseless(flags.i)
124 .multi_line(flags.m)
125 .build(pattern)
126 .map_err(|e| ManipulatorError::Regex(format!("{e} in `{pattern}`")))
127}
128
129fn captures<'s>(re: &Regex, subject: &'s str) -> Result<Option<Captures<'s>>> {
131 re.captures(subject.as_bytes())
132 .map_err(|e| ManipulatorError::Regex(e.to_string()))
133}
134
135fn captures_or_limit<'s>(re: &Regex, subject: &'s str) -> Result<Option<Captures<'s>>> {
139 match re.captures(subject.as_bytes()) {
140 Ok(c) => Ok(c),
141 Err(e) if e.code() == PCRE2_ERROR_MATCHLIMIT => Ok(None),
142 Err(e) => Err(ManipulatorError::Regex(e.to_string())),
143 }
144}
145
146fn named<'s>(caps: &Captures<'s>, name: &str) -> Option<&'s str> {
148 caps.name(name)
149 .and_then(|m| std::str::from_utf8(m.as_bytes()).ok())
150}
151
152fn group<'s>(caps: &Captures<'s>, i: usize) -> Option<&'s str> {
153 caps.get(i)
154 .and_then(|m| std::str::from_utf8(m.as_bytes()).ok())
155}
156
157fn replace_all(
159 re: &Regex,
160 subject: &str,
161 mut f: impl FnMut(&Captures<'_>) -> Result<String>,
162) -> Result<(String, usize)> {
163 let mut out = String::with_capacity(subject.len());
164 let mut last = 0;
165 let mut count = 0;
166 for caps in re.captures_iter(subject.as_bytes()) {
167 let caps = caps.map_err(|e| ManipulatorError::Regex(e.to_string()))?;
168 let m = caps
169 .get(0)
170 .ok_or_else(|| ManipulatorError::Regex("no group 0".into()))?;
171 out.push_str(&subject[last..m.start()]);
172 out.push_str(&f(&caps)?);
173 last = m.end();
174 count += 1;
175 }
176 out.push_str(&subject[last..]);
177 Ok((out, count))
178}
179
180fn replace_literal(re: &Regex, subject: &str, replacement: &str) -> Result<(String, usize)> {
184 replace_all(re, subject, |_| Ok(replacement.to_owned()))
185}
186
187fn encode(value: &Value) -> Result<String> {
190 php_json_encode_with(value, vivacity_core::phpjson::FLAGS_JSONFILE)
191 .map_err(|e| ManipulatorError::Logic(format!("JSON encoding failed: {e}")))
192}
193
194fn encode_str(s: &str) -> Result<String> {
195 encode(&Value::String(s.to_owned()))
196}
197
198const PHP_JSON_DEPTH: usize = 512;
200
201fn json_decode(s: &str) -> std::result::Result<Value, String> {
206 let (normalized, depth) = scan_json(s);
207 if depth > PHP_JSON_DEPTH {
208 return Err("Maximum stack depth exceeded".into());
209 }
210 let mut de = serde_json::Deserializer::from_str(&normalized);
211 de.disable_recursion_limit();
212 let v = Value::deserialize(&mut de).map_err(|e| e.to_string())?;
213 de.end().map_err(|e| e.to_string())?;
214 Ok(v)
215}
216
217fn scan_json(s: &str) -> (String, usize) {
220 let bytes = s.as_bytes();
221 let mut out = String::with_capacity(s.len());
222 let mut depth = 0usize;
223 let mut max_depth = 0usize;
224 let mut i = 0;
225 let mut in_string = false;
226 let mut last = 0;
227 while i < bytes.len() {
228 let c = bytes[i];
229 if in_string {
230 match c {
231 b'\\' => i += 1,
232 b'"' => in_string = false,
233 _ => {}
234 }
235 } else {
236 match c {
237 b'"' => in_string = true,
238 b'[' | b'{' => {
239 depth += 1;
240 max_depth = max_depth.max(depth);
241 }
242 b']' | b'}' => depth = depth.saturating_sub(1),
243 b'-' if bytes.get(i + 1) == Some(&b'0')
244 && !bytes
245 .get(i + 2)
246 .is_some_and(|n| matches!(n, b'.' | b'e' | b'E' | b'0'..=b'9')) =>
247 {
248 out.push_str(&s[last..i]);
249 last = i + 1;
250 }
251 _ => {}
252 }
253 }
254 i += 1;
255 }
256 out.push_str(&s[last..]);
257 (out, max_depth)
258}
259
260fn parse_json(contents: &str) -> Result<Value> {
262 json_decode(contents).map_err(ManipulatorError::Parse)
263}
264
265fn decodes_truthy(s: &str, assoc: bool) -> bool {
272 match json_decode(s) {
273 Ok(v) => {
274 if !assoc && has_nul_key(&v) {
275 return false;
276 }
277 match &v {
278 Value::Object(m) => !assoc || !m.is_empty(),
279 other => php_truthy(other),
280 }
281 }
282 Err(_) => false,
283 }
284}
285
286fn has_nul_key(v: &Value) -> bool {
287 match v {
288 Value::Object(m) => m.iter().any(|(k, v)| k.starts_with('\0') || has_nul_key(v)),
289 Value::Array(a) => a.iter().any(has_nul_key),
290 _ => false,
291 }
292}
293
294fn php_truthy(v: &Value) -> bool {
295 match v {
296 Value::Null => false,
297 Value::Bool(b) => *b,
298 Value::Number(n) => n.as_f64().is_some_and(|f| f != 0.0),
299 Value::String(s) => !(s.is_empty() || s == "0"),
300 Value::Array(a) => !a.is_empty(),
301 Value::Object(m) => !m.is_empty(),
302 }
303}
304
305fn isset<'v>(decoded: &'v Value, key: &str) -> Option<&'v Value> {
307 php_index(decoded, key).filter(|v| !v.is_null())
308}
309
310fn php_index<'v>(value: &'v Value, key: &str) -> Option<&'v Value> {
313 match value {
314 Value::Object(m) => m.get(key),
315 Value::Array(a) => canonical_index(key).and_then(|i| a.get(i)),
316 Value::String(s) => php_int_key(key)
320 .filter(|&i| {
321 (0..s.len() as i64).contains(&(if i < 0 { i + s.len() as i64 } else { i }))
322 })
323 .map(|_| value),
324 _ => None,
325 }
326}
327
328fn php_int_key(key: &str) -> Option<i64> {
331 if let Some(i) = canonical_index(key) {
332 return i64::try_from(i).ok();
333 }
334 let rest = key.strip_prefix('-')?;
335 if rest.starts_with(['1', '2', '3', '4', '5', '6', '7', '8', '9'])
336 && rest.bytes().all(|b| b.is_ascii_digit())
337 {
338 return key.parse().ok();
339 }
340 None
341}
342
343fn canonical_index(key: &str) -> Option<usize> {
345 if key == "0" {
346 return Some(0);
347 }
348 if key.starts_with(['1', '2', '3', '4', '5', '6', '7', '8', '9'])
349 && key.bytes().all(|b| b.is_ascii_digit())
350 {
351 return key.parse().ok();
352 }
353 None
354}
355
356fn php_is_list(m: &Map<String, Value>) -> bool {
359 m.keys()
360 .enumerate()
361 .all(|(i, k)| canonical_index(k) == Some(i))
362}
363
364fn truthy_str(s: &str) -> bool {
366 !(s.is_empty() || s == "0")
367}
368
369pub fn strnatcmp(a: &str, b: &str) -> std::cmp::Ordering {
371 use std::cmp::Ordering::{Equal, Greater, Less};
372 let (a, b) = (a.as_bytes(), b.as_bytes());
373 if a.is_empty() || b.is_empty() {
374 return a.len().cmp(&b.len());
375 }
376 let at = |s: &[u8], i: usize| s.get(i).copied().unwrap_or(0);
378 let (mut ap, mut bp) = (0usize, 0usize);
379 let (mut ca, mut cb) = (a[0], b[0]);
380 while ca == b'0' && ap + 1 < a.len() && a[ap + 1].is_ascii_digit() {
381 ap += 1;
382 ca = a[ap];
383 }
384 while cb == b'0' && bp + 1 < b.len() && b[bp + 1].is_ascii_digit() {
385 bp += 1;
386 cb = b[bp];
387 }
388 let is_space = |c: u8| matches!(c, b' ' | b'\t' | b'\n' | 0x0B | 0x0C | b'\r');
389 loop {
390 while is_space(ca) {
391 ap += 1;
392 ca = at(a, ap);
393 }
394 while is_space(cb) {
395 bp += 1;
396 cb = at(b, bp);
397 }
398 if ca.is_ascii_digit() && cb.is_ascii_digit() {
399 let fractional = ca == b'0' || cb == b'0';
400 let result = if fractional {
401 compare_left(a, &mut ap, b, &mut bp)
402 } else {
403 compare_right(a, &mut ap, b, &mut bp)
404 };
405 if result != Equal {
406 return result;
407 }
408 if ap >= a.len() && bp >= b.len() {
409 return Equal;
410 }
411 if ap >= a.len() {
412 return Less;
413 }
414 if bp >= b.len() {
415 return Greater;
416 }
417 ca = a[ap];
418 cb = b[bp];
419 }
420 match ca.cmp(&cb) {
421 Less => return Less,
422 Greater => return Greater,
423 Equal => {}
424 }
425 ap += 1;
426 bp += 1;
427 if ap >= a.len() && bp >= b.len() {
428 return Equal;
429 }
430 if ap >= a.len() {
431 return Less;
432 }
433 if bp >= b.len() {
434 return Greater;
435 }
436 ca = a[ap];
437 cb = b[bp];
438 }
439}
440
441fn compare_left(a: &[u8], ap: &mut usize, b: &[u8], bp: &mut usize) -> std::cmp::Ordering {
443 use std::cmp::Ordering::{Equal, Greater, Less};
444 loop {
445 let da = a.get(*ap).filter(|c| c.is_ascii_digit());
446 let db = b.get(*bp).filter(|c| c.is_ascii_digit());
447 match (da, db) {
448 (None, None) => return Equal,
449 (None, Some(_)) => return Less,
450 (Some(_), None) => return Greater,
451 (Some(x), Some(y)) => match x.cmp(y) {
452 Less => return Less,
453 Greater => return Greater,
454 Equal => {}
455 },
456 }
457 *ap += 1;
458 *bp += 1;
459 }
460}
461
462fn compare_right(a: &[u8], ap: &mut usize, b: &[u8], bp: &mut usize) -> std::cmp::Ordering {
465 use std::cmp::Ordering::{Equal, Greater, Less};
466 let mut bias = Equal;
467 loop {
468 let da = a.get(*ap).filter(|c| c.is_ascii_digit());
469 let db = b.get(*bp).filter(|c| c.is_ascii_digit());
470 match (da, db) {
471 (None, None) => return bias,
472 (None, Some(_)) => return Less,
473 (Some(_), None) => return Greater,
474 (Some(x), Some(y)) => {
475 if bias == Equal {
476 bias = x.cmp(y);
477 }
478 }
479 }
480 *ap += 1;
481 *bp += 1;
482 }
483}
484
485fn sort_prefix(requirement: &str) -> String {
488 if !is_platform_package(requirement) {
489 return format!("5-{requirement}");
490 }
491 let mut s = requirement.to_owned();
494 for (prefix, digit) in [("php", "0"), ("hhvm", "1"), ("ext", "2"), ("lib", "3")] {
495 if s.starts_with(prefix) {
496 s = format!("{digit}-{s}");
497 }
498 }
499 if s.starts_with(|c: char| !c.is_ascii_digit()) {
500 s = format!("4-{s}");
501 }
502 s
503}
504
505fn sort_packages(packages: &mut Map<String, Value>) -> Result<()> {
509 if packages.len() >= 2 && packages.keys().any(|k| php_int_key(k).is_some()) {
510 return Err(ManipulatorError::Logic(
511 "PlatformRepository::isPlatformPackage(): Argument #1 ($name) must be of type string, int given".into(),
512 ));
513 }
514 let mut entries: Vec<(String, Value)> = std::mem::take(packages).into_iter().collect();
515 entries.sort_by(|(a, _), (b, _)| strnatcmp(&sort_prefix(a), &sort_prefix(b)));
516 packages.extend(entries);
517 Ok(())
518}
519
520fn is_stdclass_marker(m: &Map<String, Value>) -> bool {
522 m.len() == 1 && m.contains_key(STDCLASS_MARKER)
523}
524
525pub struct JsonManipulator {
526 contents: String,
527 newline: &'static str,
528 indent: String,
529}
530
531impl JsonManipulator {
532 pub fn new(contents: &str) -> Result<Self> {
535 let contents = contents.trim_matches([' ', '\t', '\n', '\r', '\0', '\x0B']);
536 let contents = if contents.is_empty() { "{}" } else { contents };
537 if captures(&compile(r"^\{(.*)\}$", S)?, contents)?.is_none() {
538 return Err(ManipulatorError::NotAnObject);
539 }
540 let newline = if contents.contains("\r\n") {
541 "\r\n"
542 } else {
543 "\n"
544 };
545 let contents = if contents == "{}" {
546 format!("{{{newline}}}")
547 } else {
548 contents.to_owned()
549 };
550 let indent = detect_indenting(&contents)?;
551 Ok(Self {
552 contents,
553 newline,
554 indent,
555 })
556 }
557
558 pub fn contents(&self) -> String {
560 format!("{}{}", self.contents, self.newline)
561 }
562
563 pub fn add_link(
566 &mut self,
567 link_type: &str,
568 package: &str,
569 constraint: &str,
570 sort: bool,
571 ) -> Result<bool> {
572 let decoded = parse_json(&self.contents)?;
573 if isset(&decoded, link_type).is_none() {
574 let mut m = Map::new();
575 m.insert(package.to_owned(), Value::String(constraint.to_owned()));
576 return self.add_main_key(link_type, &Value::Object(m));
577 }
578
579 let regex = compile(
580 &format!(
581 r#"{DEFINES}^(?P<start>\s*\{{\s*(?:(?&string)\s*:\s*(?&json)\s*,\s*)*?)(?P<property>{}\s*:\s*)(?P<value>(?&json))(?P<end>.*)"#,
582 preg_quote(&encode_str(link_type)?)
583 ),
584 SX,
585 )?;
586 let Some(caps) = captures(®ex, &self.contents)? else {
587 return Ok(false);
588 };
589 let start = named(&caps, "start").unwrap_or("").to_owned();
590 let property = named(&caps, "property").unwrap_or("").to_owned();
591 let end = named(&caps, "end").unwrap_or("").to_owned();
592 let mut links = named(&caps, "value").unwrap_or("").to_owned();
593
594 let package_regex = preg_quote(package).replace('/', "\\\\?/");
596 let regex = compile(
597 &format!(r#"{DEFINES}"(?P<package>{package_regex})"(\s*:\s*)(?&string)"#),
598 IX,
599 )?;
600 if let Some(pm) = captures(®ex, &links)? {
601 let existing = named(&pm, "package").unwrap_or("").to_owned();
602 let package_regex = preg_quote(&existing).replace('/', "\\\\?/");
603 let regex = compile(
604 &format!(r#"{DEFINES}"{package_regex}"(?P<separator>\s*:\s*)(?&string)"#),
605 IX,
606 )?;
607 let name = encode_str(&existing.replace("\\/", "/"))?;
608 links = replace_all(®ex, &links, |m| {
609 Ok(format!(
610 "{name}{}\"{constraint}\"",
611 named(m, "separator").unwrap_or("")
612 ))
613 })?
614 .0;
615 } else {
616 let regex = compile(r"^\s*\{\s*\S+.*?(\s*\}\s*)$", S)?;
617 let tail = captures(®ex, &links)?.and_then(|m| group(&m, 1).map(str::to_owned));
618 if let Some(tail) = tail {
619 let regex = compile(&format!("{}$", preg_quote(&tail)), NONE)?;
620 let replacement = format!(
621 ",{nl}{ind}{ind}{}: {}{tail}",
622 encode_str(package)?,
623 encode_str(constraint)?,
624 nl = self.newline,
625 ind = self.indent
626 );
627 links = replace_literal(®ex, &links, &replacement)?.0;
628 } else {
629 links = format!(
630 "{{{nl}{ind}{ind}{}: {}{nl}{ind}}}",
631 encode_str(package)?,
632 encode_str(constraint)?,
633 nl = self.newline,
634 ind = self.indent
635 );
636 }
637 }
638
639 if sort {
640 let requirements = json_decode(&links).map_err(|e| {
641 ManipulatorError::Logic(format!("sortPackages(): links are not JSON: {e}"))
642 })?;
643 links = match requirements {
644 Value::Object(mut m) => {
645 sort_packages(&mut m)?;
646 self.format(&Value::Object(m), 0, false)?
647 }
648 Value::Array(a) => {
651 let mut m = list_to_map(a);
652 sort_packages(&mut m)?;
653 self.format(&Value::Object(m), 0, false)?
654 }
655 _ => {
656 return Err(ManipulatorError::Logic(
657 "sortPackages(): Argument #1 ($packages) must be of type array".into(),
658 ))
659 }
660 };
661 }
662
663 self.contents = format!("{start}{property}{links}{end}");
664 Ok(true)
665 }
666
667 pub fn remove_config_setting(&mut self, name: &str) -> Result<bool> {
669 if name.starts_with("policy.") && name.matches('.').count() >= 2 {
670 return Err(ManipulatorError::Unsupported(format!(
671 "removeConfigSetting({name}): policy lists"
672 )));
673 }
674 self.remove_sub_node("config", name)
675 }
676
677 pub fn add_sub_node(
680 &mut self,
681 main_node: &str,
682 name: &str,
683 value: &Value,
684 append: bool,
685 ) -> Result<bool> {
686 let decoded = parse_json(&self.contents)?;
687 let (name, sub_name) = split_sub_name(main_node, name);
688
689 if isset(&decoded, main_node).is_none() {
690 let inner = match sub_name {
691 Some(sub) => {
692 let mut m = Map::new();
693 m.insert(sub.to_owned(), value.clone());
694 Value::Object(m)
695 }
696 None => value.clone(),
697 };
698 let mut m = Map::new();
699 m.insert(name.to_owned(), inner);
700 self.add_main_key(main_node, &Value::Object(m))?;
701 return Ok(true);
702 }
703
704 let node_regex = self.node_regex(main_node)?;
705 let Some(caps) = captures_or_limit(&node_regex, &self.contents)? else {
706 return Ok(false);
707 };
708 let mut children = named(&caps, "content").unwrap_or("").to_owned();
709
710 if !decodes_truthy(&children, false) {
711 return Ok(false);
712 }
713
714 let child_regex = compile(
715 &format!(
716 r#"{DEFINES}(?P<start>"{}"\s*:\s*)(?P<content>(?&json))(?P<end>,?)"#,
717 preg_quote(name)
718 ),
719 X,
720 )?;
721 if captures(&child_regex, &children)?.is_some() {
722 children = replace_all(&child_regex, &children, |m| {
723 let content = named(m, "content").unwrap_or("");
724 let value = match sub_name {
725 Some(sub) => {
726 let mut cur = match json_decode(content) {
727 Ok(Value::Object(m)) => m,
728 Ok(Value::Array(a)) => list_to_map(a),
729 _ => Map::new(),
730 };
731 cur.insert(sub.to_owned(), value.clone());
732 Value::Object(cur)
733 }
734 None => value.clone(),
735 };
736 Ok(format!(
737 "{}{}{}",
738 named(m, "start").unwrap_or(""),
739 self.format(&value, 1, false)?,
740 named(m, "end").unwrap_or("")
741 ))
742 })?
743 .0;
744 } else {
745 let regex = compile(
746 r"^\{(?P<leadingspace>\s*?)(?P<content>\S+.*?)?(?P<trailingspace>\s*)\}$",
747 S,
748 )?;
749 let Some(m) = captures(®ex, &children)? else {
750 return Err(ManipulatorError::Logic(format!(
751 "Nothing matched above for: {children}"
752 )));
753 };
754 let leading = named(&m, "leadingspace").unwrap_or("").to_owned();
755 let trailing = named(&m, "trailingspace").unwrap_or("").to_owned();
756 let has_content = named(&m, "content").is_some();
757 let value = match sub_name {
758 Some(sub) => {
759 let mut m = Map::new();
760 m.insert(sub.to_owned(), value.clone());
761 Value::Object(m)
762 }
763 None => value.clone(),
764 };
765 let entry = format!("{}: {}", encode_str(name)?, self.format(&value, 1, false)?);
766 if has_content {
767 if append {
768 let regex = compile(&format!("{trailing}}}$"), NONE)?;
769 let replacement = format!(
770 ",{nl}{ind}{ind}{entry}{trailing}}}",
771 nl = self.newline,
772 ind = self.indent
773 );
774 children = replace_literal(®ex, &children, &replacement)?.0;
775 } else {
776 let regex = compile(&format!("^{{{leading}"), NONE)?;
777 let replacement = format!(
778 "{{{leading}{entry},{nl}{ind}{ind}",
779 nl = self.newline,
780 ind = self.indent
781 );
782 children = replace_literal(®ex, &children, &replacement)?.0;
783 }
784 } else {
785 children = format!(
786 "{{{nl}{ind}{ind}{entry}{trailing}}}",
787 nl = self.newline,
788 ind = self.indent
789 );
790 }
791 }
792
793 self.contents = replace_all(&node_regex, &self.contents, |m| {
794 Ok(format!(
795 "{}{children}{}",
796 named(m, "start").unwrap_or(""),
797 named(m, "end").unwrap_or("")
798 ))
799 })?
800 .0;
801 Ok(true)
802 }
803
804 pub fn remove_sub_node(&mut self, main_node: &str, name: &str) -> Result<bool> {
807 let decoded = parse_json(&self.contents)?;
808 if !php_index(&decoded, main_node).is_some_and(php_truthy) {
809 return Ok(true);
810 }
811
812 let node_regex = self.node_regex(main_node)?;
813 let Some(caps) = captures_or_limit(&node_regex, &self.contents)? else {
814 return Ok(false);
815 };
816 let children = named(&caps, "content").unwrap_or("").to_owned();
817
818 if !decodes_truthy(&children, true) {
819 return Ok(false);
820 }
821
822 let (name, sub_name) = split_sub_name(main_node, name);
823
824 let node = php_index(&decoded, main_node).unwrap_or(&Value::Null);
825 let Some(entry) = isset(node, name) else {
826 return Ok(true);
827 };
828 if let Some(sub) = sub_name {
829 if truthy_str(sub) && isset(entry, sub).is_none() {
830 return Ok(true);
831 }
832 }
833
834 let key_regex = preg_quote(name).replace('/', "\\\\?/");
835 let children_clean = if captures(&compile(&format!(r#""{key_regex}"\s*:"#), I)?, &children)?
836 .is_some()
837 {
838 let all = compile(&format!(r#"{DEFINES}"{key_regex}"\s*:\s*(?:(?&json))"#), X)?;
839 let mut best = String::new();
840 let mut any = false;
841 for m in all.captures_iter(children.as_bytes()) {
842 let m = m.map_err(|e| ManipulatorError::Regex(e.to_string()))?;
843 any = true;
844 let whole = group(&m, 0).unwrap_or("");
845 if best.len() < whole.len() {
846 best = whole.to_owned();
847 }
848 }
849 if !any {
850 return Err(ManipulatorError::Logic(
851 "JsonManipulator: $childrenClean is not defined. Please report at https://github.com/composer/composer/issues/new.".into(),
852 ));
853 }
854 let (mut clean, count) = replace_literal(
855 &compile(&format!(r",\s*{}", preg_quote(&best)), I)?,
856 &children,
857 "",
858 )?;
859 if count != 1 {
860 let (clean2, count2) = replace_literal(
861 &compile(&format!(r"{}\s*,?\s*", preg_quote(&best)), I)?,
862 &clean,
863 "",
864 )?;
865 if count2 != 1 {
866 return Ok(false);
867 }
868 clean = clean2;
869 }
870 clean
871 } else {
872 children.clone()
873 };
874
875 let regex = compile(r"^\{\s*?(?P<content>\S+.*?)?(?P<trailingspace>\s*)\}$", S)?;
876 if let Some(m) = captures(®ex, &children_clean)? {
877 if named(&m, "content").is_none() {
878 let empty = format!("{{{}{}}}", self.newline, self.indent);
879 self.contents = replace_all(&node_regex, &self.contents, |m| {
880 Ok(format!(
881 "{}{empty}{}",
882 named(m, "start").unwrap_or(""),
883 named(m, "end").unwrap_or("")
884 ))
885 })?
886 .0;
887 if let Some(sub) = sub_name {
888 let cur = json_decode(&children).map_err(ManipulatorError::Parse)?;
889 let inner = unset_sub(&cur, name, sub)?;
890 self.add_sub_node(main_node, name, &inner, true)?;
891 }
892 return Ok(true);
893 }
894 }
895
896 let replacement = match sub_name {
897 Some(sub) => {
898 let content = named(&caps, "content").unwrap_or("");
899 let mut cur = match json_decode(content) {
900 Ok(Value::Object(m)) => m,
901 Ok(Value::Array(a)) => list_to_map(a),
902 _ => Map::new(),
903 };
904 let inner = unset_sub(&Value::Object(cur.clone()), name, sub)?;
905 cur.insert(name.to_owned(), inner);
906 self.format(&Value::Object(cur), 0, true)?
907 }
908 None => children_clean,
909 };
910 self.contents = replace_all(&node_regex, &self.contents, |m| {
911 Ok(format!(
912 "{}{replacement}{}",
913 named(m, "start").unwrap_or(""),
914 named(m, "end").unwrap_or("")
915 ))
916 })?
917 .0;
918 Ok(true)
919 }
920
921 pub fn add_main_key(&mut self, key: &str, content: &Value) -> Result<bool> {
924 let decoded = parse_json(&self.contents)?;
925 let content = self.format(content, 0, false)?;
926 let encoded_key = encode_str(key)?;
927
928 let regex = compile(
929 &format!(
930 r#"{DEFINES}^(?P<start>\s*\{{\s*(?:(?&string)\s*:\s*(?&json)\s*,\s*)*?)(?P<key>{}\s*:\s*(?&json))(?P<end>.*)"#,
931 preg_quote(&encoded_key)
932 ),
933 SX,
934 )?;
935 if isset(&decoded, key).is_some() {
936 if let Some(m) = captures(®ex, &self.contents)? {
937 let key_text = named(&m, "key").unwrap_or("");
938 if !decodes_truthy(&format!("{{{key_text}}}"), false) {
939 return Ok(false);
940 }
941 self.contents = format!(
942 "{}{encoded_key}: {content}{}",
943 named(&m, "start").unwrap_or(""),
944 named(&m, "end").unwrap_or("")
945 );
946 return Ok(true);
947 }
948 }
949
950 let regex = compile(r"[^{\s](\s*)\}$", NONE)?;
951 if let Some(m) = captures(®ex, &self.contents)? {
952 let ws = group(&m, 1).unwrap_or("").to_owned();
953 let regex = compile(&format!(r"{ws}\}}$"), NONE)?;
954 let replacement = format!(
955 ",{nl}{ind}{encoded_key}: {content}{nl}}}",
956 nl = self.newline,
957 ind = self.indent
958 );
959 self.contents = replace_literal(®ex, &self.contents, &replacement)?.0;
960 return Ok(true);
961 }
962
963 let regex = compile(r"\}$", NONE)?;
964 let replacement = format!(
965 "{ind}{encoded_key}: {content}{nl}}}",
966 nl = self.newline,
967 ind = self.indent
968 );
969 self.contents = replace_literal(®ex, &self.contents, &replacement)?.0;
970 Ok(true)
971 }
972
973 pub fn remove_main_key(&mut self, key: &str) -> Result<bool> {
975 let decoded = parse_json(&self.contents)?;
976 if php_index(&decoded, key).is_none() {
977 return Ok(true);
978 }
979
980 let regex = compile(
981 &format!(
982 r#"{DEFINES}^(?P<start>\s*\{{\s*(?:(?&string)\s*:\s*(?&json)\s*,\s*)*?)(?P<removal>{}\s*:\s*(?&json))\s*,?\s*(?P<end>.*)"#,
983 preg_quote(&encode_str(key)?)
984 ),
985 SX,
986 )?;
987 let Some(m) = captures(®ex, &self.contents)? else {
988 return Ok(false);
989 };
990 let mut start = named(&m, "start").unwrap_or("").to_owned();
991 let removal = named(&m, "removal").unwrap_or("");
992 let end = named(&m, "end").unwrap_or("").to_owned();
993
994 if !decodes_truthy(&format!("{{{removal}}}"), false) {
995 return Ok(false);
996 }
997
998 if captures(&compile(r",\s*$", NONE)?, &start)?.is_some()
1000 && captures(&compile(r"^\}$", NONE)?, &end)?.is_some()
1001 {
1002 let regex = compile(r",(\s*)$", NONE)?;
1003 let stripped =
1004 replace_all(®ex, &start, |m| Ok(group(m, 1).unwrap_or("").to_owned()))?.0;
1005 let chars: Vec<char> = self.indent.chars().collect();
1006 start = stripped.trim_end_matches(chars.as_slice()).to_owned();
1007 }
1008
1009 self.contents = format!("{start}{end}");
1010 if captures(&compile(r"^\{\s*\}\s*$", NONE)?, &self.contents)?.is_some() {
1011 self.contents = "{\n}".to_owned();
1012 }
1013 Ok(true)
1014 }
1015
1016 pub fn remove_main_key_if_empty(&mut self, key: &str) -> Result<bool> {
1018 let decoded = parse_json(&self.contents)?;
1019 let Some(v) = php_index(&decoded, key) else {
1020 return Ok(true);
1021 };
1022 let empty = match v {
1023 Value::Array(a) => a.is_empty(),
1024 Value::Object(m) => m.is_empty(),
1025 _ => false,
1026 };
1027 if empty {
1028 return self.remove_main_key(key);
1029 }
1030 Ok(true)
1031 }
1032
1033 pub fn format(&self, data: &Value, depth: usize, was_object: bool) -> Result<String> {
1036 let indent = |n: usize| self.indent.repeat(n);
1037 let empty_object = |was_object: bool| {
1038 if was_object {
1039 format!("{{{}{}}}", self.newline, indent(depth + 1))
1040 } else {
1041 "[]".to_owned()
1042 }
1043 };
1044 match data {
1045 Value::Object(m) if is_stdclass_marker(m) => Ok(empty_object(true)),
1046 Value::Object(m) if m.is_empty() => Ok(empty_object(was_object)),
1047 Value::Array(a) if a.is_empty() => Ok(empty_object(was_object)),
1048 Value::Array(a) => {
1049 let items = a
1050 .iter()
1051 .map(|v| self.format(v, depth + 1, false))
1052 .collect::<Result<Vec<_>>>()?;
1053 Ok(format!("[{}]", items.join(", ")))
1054 }
1055 Value::Object(m) if php_is_list(m) => {
1056 let items = m
1057 .values()
1058 .map(|v| self.format(v, depth + 1, false))
1059 .collect::<Result<Vec<_>>>()?;
1060 Ok(format!("[{}]", items.join(", ")))
1061 }
1062 Value::Object(m) => {
1063 let mut elems = Vec::with_capacity(m.len());
1064 for (k, v) in m {
1065 elems.push(format!(
1066 "{}{}: {}",
1067 indent(depth + 2),
1068 encode_str(k)?,
1069 self.format(v, depth + 1, false)?
1070 ));
1071 }
1072 Ok(format!(
1073 "{{{nl}{}{nl}{}}}",
1074 elems.join(&format!(",{}", self.newline)),
1075 indent(depth + 1),
1076 nl = self.newline
1077 ))
1078 }
1079 scalar => encode(scalar),
1080 }
1081 }
1082
1083 fn node_regex(&self, main_node: &str) -> Result<Regex> {
1086 compile(
1087 &format!(
1088 r#"{DEFINES}^(?P<start> \s* \{{ \s* (?: (?&string) \s* : (?&json) \s* , \s* )*?{}\s*:\s*)(?P<content>(?&object))(?P<end>.*)"#,
1089 preg_quote(&encode_str(main_node)?)
1090 ),
1091 SX,
1092 )
1093 }
1094}
1095
1096fn split_sub_name<'a>(main_node: &str, name: &'a str) -> (&'a str, Option<&'a str>) {
1098 if matches!(main_node, "config" | "extra" | "scripts") {
1099 if let Some((n, s)) = name.split_once('.') {
1100 return (n, Some(s));
1101 }
1102 }
1103 (name, None)
1104}
1105
1106fn list_to_map(a: Vec<Value>) -> Map<String, Value> {
1108 a.into_iter()
1109 .enumerate()
1110 .map(|(i, v)| (i.to_string(), v))
1111 .collect()
1112}
1113
1114fn unset_sub(cur: &Value, name: &str, sub: &str) -> Result<Value> {
1116 let inner = php_index(cur, name).cloned().unwrap_or(Value::Null);
1117 let mut m = match inner {
1118 Value::Object(m) => m,
1119 Value::Array(a) => list_to_map(a),
1120 Value::Null => return Ok(Value::Null),
1122 Value::Bool(false) => return Ok(Value::Bool(false)),
1123 other => {
1124 return Err(ManipulatorError::Logic(format!(
1125 "Cannot unset offset in a non-array variable ({other})"
1126 )))
1127 }
1128 };
1129 m.shift_remove(sub);
1130 if m.is_empty() {
1131 return Ok(vivacity_core::phpjson::empty_stdclass());
1132 }
1133 Ok(Value::Object(m))
1134}
1135
1136pub fn detect_indenting(json: &str) -> Result<String> {
1138 let re = compile(r#"^([ \t]+)""#, Flags { m: true, ..NONE })?;
1139 Ok(captures(&re, json)?
1140 .and_then(|c| group(&c, 1).map(str::to_owned))
1141 .unwrap_or_else(|| INDENT_DEFAULT.to_owned()))
1142}
1143
1144#[cfg(test)]
1145mod tests {
1146 use super::*;
1147 use std::cmp::Ordering::{Equal, Greater, Less};
1148
1149 #[test]
1150 fn strnatcmp_matches_php() {
1151 assert_eq!(strnatcmp("0-php", "1-hhvm"), Less);
1152 assert_eq!(strnatcmp("ext-json", "ext-Json"), Greater);
1153 assert_eq!(strnatcmp("a10", "a9"), Greater);
1154 assert_eq!(strnatcmp("a 1", "a1"), Equal);
1155 assert_eq!(strnatcmp("a01", "a1"), Less);
1156 assert_eq!(strnatcmp("1.10", "1.9"), Greater);
1157 assert_eq!(strnatcmp("01", "1"), Equal);
1158 assert_eq!(strnatcmp("", "a"), Less);
1159 assert_eq!(strnatcmp("abc", "ab"), Greater);
1160 }
1161
1162 #[test]
1163 fn sort_prefixes() {
1164 assert_eq!(sort_prefix("php"), "0-php");
1165 assert_eq!(sort_prefix("php-64bit"), "0-php-64bit");
1166 assert_eq!(sort_prefix("hhvm"), "1-hhvm");
1167 assert_eq!(sort_prefix("ext-json"), "2-ext-json");
1168 assert_eq!(sort_prefix("lib-icu"), "3-lib-icu");
1169 assert_eq!(sort_prefix("composer-plugin-api"), "4-composer-plugin-api");
1170 assert_eq!(sort_prefix("acme/lib"), "5-acme/lib");
1171 }
1172
1173 #[test]
1174 fn add_link_to_existing_section() {
1175 let mut m =
1176 JsonManipulator::new("{\n \"require\": {\n \"a/b\": \"^1\"\n }\n}\n")
1177 .expect("new");
1178 assert!(m.add_link("require", "c/d", "^2", false).expect("add"));
1179 assert_eq!(
1180 m.contents(),
1181 "{\n \"require\": {\n \"a/b\": \"^1\",\n \"c/d\": \"^2\"\n }\n}\n"
1182 );
1183 assert!(m.add_link("require", "A/B", "^3", true).expect("replace"));
1184 assert_eq!(
1185 m.contents(),
1186 "{\n \"require\": {\n \"a/b\": \"^3\",\n \"c/d\": \"^2\"\n }\n}\n"
1187 );
1188 }
1189
1190 #[test]
1191 fn add_link_creates_section_and_remove_drops_it() {
1192 let mut m = JsonManipulator::new("{}").expect("new");
1193 assert!(m.add_link("require", "a/b", "^1", false).expect("add"));
1194 assert_eq!(
1195 m.contents(),
1196 "{\n \"require\": {\n \"a/b\": \"^1\"\n }\n}\n"
1197 );
1198 assert!(m.remove_sub_node("require", "a/b").expect("remove"));
1199 assert_eq!(m.contents(), "{\n \"require\": {\n }\n}\n");
1200 assert!(m.remove_main_key_if_empty("require").expect("drop"));
1201 assert_eq!(m.contents(), "{\n}\n");
1202 }
1203
1204 #[test]
1205 fn crlf_and_tabs_are_kept() {
1206 let mut m =
1207 JsonManipulator::new("{\r\n\t\"require\": {\r\n\t\t\"a/b\": \"^1\"\r\n\t}\r\n}")
1208 .expect("new");
1209 assert!(m.add_link("require", "c/d", "^2", false).expect("add"));
1210 assert_eq!(
1211 m.contents(),
1212 "{\r\n\t\"require\": {\r\n\t\t\"a/b\": \"^1\",\r\n\t\t\"c/d\": \"^2\"\r\n\t}\r\n}\r\n"
1213 );
1214 }
1215
1216 #[test]
1217 fn config_sub_key_removal() {
1218 let mut m = JsonManipulator::new(
1219 "{\n \"config\": {\n \"allow-plugins\": {\n \"a/b\": true,\n \"c/d\": false\n }\n }\n}",
1220 )
1221 .expect("new");
1222 assert!(m
1223 .remove_config_setting("allow-plugins.a/b")
1224 .expect("remove"));
1225 assert_eq!(
1226 m.contents(),
1227 "{\n \"config\": {\n \"allow-plugins\": {\n \"c/d\": false\n }\n }\n}\n"
1228 );
1229 assert!(m
1230 .remove_config_setting("allow-plugins.c/d")
1231 .expect("remove"));
1232 assert_eq!(
1233 m.contents(),
1234 "{\n \"config\": {\n \"allow-plugins\": {\n }\n }\n}\n"
1235 );
1236 }
1237}