Skip to main content

vivacity_core/
phpserialize.rs

1//! PHP `serialize()` of a value that came out of `json_decode($json, true)`:
2//! what `PathRepository::initialize` hashes (`sha1($json . serialize($this
3//! ->options))`) to build the dist reference of a path package. Only the
4//! shapes a JSON document can produce are covered: assoc arrays in
5//! insertion order (`a:n:{…}`; a key that PHP would have turned into an
6//! integer — a canonical decimal in the `int` range — is written `i:`), lists,
7//! strings (`s:<bytes>:"…";`), integers, floats in PHP's shortest round-trip
8//! form (`d:`), booleans and null.
9
10use crate::error::{Error, Result};
11use serde_json::Value;
12use std::fmt::Write as _;
13
14/// `serialize(json_decode($json, true))` for `v`.
15pub fn serialize(v: &Value) -> Result<String> {
16    let mut out = String::new();
17    write_value(v, &mut out)?;
18    Ok(out)
19}
20
21fn write_value(v: &Value, out: &mut String) -> Result<()> {
22    match v {
23        Value::Null => out.push_str("N;"),
24        Value::Bool(b) => {
25            let _ = write!(out, "b:{};", u8::from(*b));
26        }
27        Value::Number(n) => {
28            if let Some(i) = n.as_i64() {
29                let _ = write!(out, "i:{i};");
30            } else if let Some(f) = n.as_f64() {
31                // Beyond the int range PHP decodes a float.
32                let _ = write!(
33                    out,
34                    "d:{};",
35                    crate::phpjson::php_double(f)?.replace('e', "E")
36                );
37            } else {
38                return Err(Error::NonFiniteFloat(f64::NAN));
39            }
40        }
41        Value::String(s) => write_string(s, out),
42        Value::Array(items) => {
43            let _ = write!(out, "a:{}:{{", items.len());
44            for (i, item) in items.iter().enumerate() {
45                let _ = write!(out, "i:{i};");
46                write_value(item, out)?;
47            }
48            out.push('}');
49        }
50        Value::Object(map) => {
51            let _ = write!(out, "a:{}:{{", map.len());
52            for (k, item) in map {
53                match php_int_key(k) {
54                    Some(i) => {
55                        let _ = write!(out, "i:{i};");
56                    }
57                    None => write_string(k, out),
58                }
59                write_value(item, out)?;
60            }
61            out.push('}');
62        }
63    }
64    Ok(())
65}
66
67fn write_string(s: &str, out: &mut String) {
68    let _ = write!(out, "s:{}:\"{s}\";", s.len());
69}
70
71/// `ZEND_HANDLE_NUMERIC_STR`: a key that is a canonical decimal integer
72/// (no leading zero unless `0`, optional `-`, not `-0`) within the int
73/// range becomes an integer key.
74fn php_int_key(k: &str) -> Option<i64> {
75    let digits = k.strip_prefix('-').unwrap_or(k);
76    if digits.is_empty() || !digits.bytes().all(|b| b.is_ascii_digit()) {
77        return None;
78    }
79    if digits.len() > 1 && digits.starts_with('0') {
80        return None;
81    }
82    if k == "-0" {
83        return None;
84    }
85    k.parse::<i64>().ok()
86}
87
88#[cfg(test)]
89mod tests {
90    use super::*;
91    use serde_json::json;
92
93    #[test]
94    fn path_repository_options() {
95        assert_eq!(
96            serialize(&json!({"relative": true})).unwrap(),
97            "a:1:{s:8:\"relative\";b:1;}"
98        );
99        assert_eq!(
100            serialize(&json!({"symlink": false, "versions": {"acme/beta": "2.0.0"}, "relative": true}))
101                .unwrap(),
102            "a:3:{s:7:\"symlink\";b:0;s:8:\"versions\";a:1:{s:9:\"acme/beta\";s:5:\"2.0.0\";}s:8:\"relative\";b:1;}"
103        );
104    }
105
106    #[test]
107    fn scalars_and_keys_like_php() {
108        // `php -r 'echo serialize(json_decode(…, true));'` (PHP 8.4).
109        let v: Value = serde_json::from_str(
110            r#"{"a":1.5,"b":10,"c":null,"d":[1,"x"],"e":{},"f":[],"g":0.1,"h":1.0,"i":100000000000000000000,"j":1e-7,"k":-0.0,"l":"é"}"#,
111        )
112        .unwrap();
113        assert_eq!(
114            serialize(&v).unwrap(),
115            "a:12:{s:1:\"a\";d:1.5;s:1:\"b\";i:10;s:1:\"c\";N;s:1:\"d\";a:2:{i:0;i:1;i:1;s:1:\"x\";}s:1:\"e\";a:0:{}s:1:\"f\";a:0:{}s:1:\"g\";d:0.1;s:1:\"h\";d:1;s:1:\"i\";d:1.0E+20;s:1:\"j\";d:1.0E-7;s:1:\"k\";d:-0;s:1:\"l\";s:2:\"é\";}"
116        );
117        let v: Value = serde_json::from_str(
118            r#"{"1":"x","05":"y","-3":"z","1.0":"w","9223372036854775807":1,"9223372036854775808":2}"#,
119        )
120        .unwrap();
121        assert_eq!(
122            serialize(&v).unwrap(),
123            "a:6:{i:1;s:1:\"x\";s:2:\"05\";s:1:\"y\";i:-3;s:1:\"z\";s:3:\"1.0\";s:1:\"w\";i:9223372036854775807;i:1;s:19:\"9223372036854775808\";i:2;}"
124        );
125        let v: Value = serde_json::from_str(
126            "[18446744073709551615, 9223372036854775808, -9223372036854775808]",
127        )
128        .unwrap();
129        assert_eq!(
130            serialize(&v).unwrap(),
131            "a:3:{i:0;d:1.8446744073709552E+19;i:1;d:9.223372036854776E+18;i:2;i:-9223372036854775808;}"
132        );
133    }
134}