Skip to main content

shared/
deserialize.rs

1use indexmap::IndexMap;
2use serde::{Deserialize, Deserializer, de::DeserializeOwned};
3
4#[inline]
5pub fn deserialize_defaultable<'de, T, D>(deserializer: D) -> Result<T, D::Error>
6where
7    T: DeserializeOwned + Default,
8    D: Deserializer<'de>,
9{
10    let value = serde_json::Value::deserialize(deserializer)?;
11
12    Ok(serde_json::from_value(value).unwrap_or_default())
13}
14
15#[inline]
16pub fn deserialize_non_null_option<'de, T, D>(deserializer: D) -> Result<Option<T>, D::Error>
17where
18    T: Deserialize<'de>,
19    D: Deserializer<'de>,
20{
21    T::deserialize(deserializer).map(Some)
22}
23
24pub fn deserialize_stringable_option<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
25where
26    D: Deserializer<'de>,
27{
28    let value = serde_json::Value::deserialize(deserializer).unwrap_or_default();
29    Ok(match value {
30        serde_json::Value::Null => None,
31        serde_json::Value::String(string) if string.is_empty() => None,
32        serde_json::Value::String(string) => Some(string),
33        value => Some(value.to_string()),
34    })
35}
36
37pub fn deserialize_string_option<'de, D>(
38    deserializer: D,
39) -> Result<Option<compact_str::CompactString>, D::Error>
40where
41    D: Deserializer<'de>,
42{
43    let value: Option<compact_str::CompactString> =
44        Option::deserialize(deserializer).unwrap_or_default();
45    Ok(value.filter(|s| !s.is_empty()))
46}
47
48/// Normalises a free-text search term so it can be matched against a uuid column as an
49/// anchored prefix
50pub fn deserialize_search_option<'de, D>(
51    deserializer: D,
52) -> Result<Option<compact_str::CompactString>, D::Error>
53where
54    D: Deserializer<'de>,
55{
56    let value: Option<compact_str::CompactString> =
57        Option::deserialize(deserializer).unwrap_or_default();
58
59    Ok(value
60        .map(|s| match uuid::Uuid::parse_str(s.trim()) {
61            Ok(uuid) => compact_str::format_compact!("{uuid}"),
62            Err(_) => s.trim().into(),
63        })
64        .filter(|s| !s.is_empty()))
65}
66
67pub fn deserialize_array_or_not<'de, D, T: DeserializeOwned>(
68    deserializer: D,
69) -> Result<Vec<T>, D::Error>
70where
71    D: Deserializer<'de>,
72{
73    let value = serde_json::Value::deserialize(deserializer).unwrap_or_default();
74    let value: Vec<T> = match value {
75        serde_json::Value::Array(values) => {
76            serde_json::from_value(serde_json::Value::Array(values))
77                .map_err(serde::de::Error::custom)?
78        }
79        value => vec![serde_json::from_value(value).map_err(serde::de::Error::custom)?],
80    };
81
82    Ok(value)
83}
84
85pub fn deserialize_map_or_not<
86    'de,
87    D,
88    K: DeserializeOwned + std::hash::Hash + Eq,
89    V: DeserializeOwned,
90>(
91    deserializer: D,
92) -> Result<IndexMap<K, V>, D::Error>
93where
94    D: Deserializer<'de>,
95{
96    let value = serde_json::Value::deserialize(deserializer).unwrap_or_default();
97    let value: IndexMap<K, V> = match value {
98        serde_json::Value::Object(map) => serde_json::from_value(serde_json::Value::Object(map))
99            .map_err(serde::de::Error::custom)?,
100        value => {
101            let v = serde_json::from_value(value).map_err(serde::de::Error::custom)?;
102            let mut map = IndexMap::new();
103            map.insert(
104                K::deserialize(serde_json::Value::String("Default".into()))
105                    .map_err(serde::de::Error::custom)?,
106                v,
107            );
108            map
109        }
110    };
111
112    Ok(value)
113}
114
115pub fn deserialize_pre_stringified<'de, D, T: DeserializeOwned>(
116    deserializer: D,
117) -> Result<T, D::Error>
118where
119    D: Deserializer<'de>,
120{
121    let value: serde_json::Value = serde_json::Value::deserialize(deserializer)?;
122    let value: T = match value {
123        serde_json::Value::String(value) => {
124            serde_json::from_str(&value).map_err(serde::de::Error::custom)?
125        }
126        value => serde_json::from_value(value).map_err(serde::de::Error::custom)?,
127    };
128
129    Ok(value)
130}
131
132pub fn deserialize_nest_egg_config_stop<'de, D>(
133    deserializer: D,
134) -> Result<crate::models::nest_egg::NestEggConfigStop, D::Error>
135where
136    D: Deserializer<'de>,
137{
138    let value: serde_json::Value = serde_json::Value::deserialize(deserializer)?;
139    let value: crate::models::nest_egg::NestEggConfigStop = match value {
140        serde_json::Value::String(value) => {
141            serde_json::from_str(&value).unwrap_or_else(|_| match value.strip_prefix('^') {
142                Some(signal) => crate::models::nest_egg::NestEggConfigStop {
143                    r#type: "signal".into(),
144                    value: Some(match signal.to_uppercase().as_str() {
145                        "C" | "SIGINT" => "SIGINT".into(),
146                        signal @ ("SIGABRT" | "SIGTERM" | "SIGQUIT" | "SIGKILL") => signal.into(),
147                        _ => "SIGKILL".into(),
148                    }),
149                },
150                None => crate::models::nest_egg::NestEggConfigStop {
151                    r#type: "command".into(),
152                    value: Some(value.into()),
153                },
154            })
155        }
156        value => serde_json::from_value(value).map_err(serde::de::Error::custom)?,
157    };
158
159    Ok(value)
160}
161
162fn true_fn() -> bool {
163    true
164}
165
166pub fn deserialize_nest_egg_config_files<'de, D>(
167    deserializer: D,
168) -> Result<
169    IndexMap<compact_str::CompactString, crate::models::nest_egg::ExportedNestEggConfigsFilesFile>,
170    D::Error,
171>
172where
173    D: Deserializer<'de>,
174{
175    let value: serde_json::Value = serde_json::Value::deserialize(deserializer)?;
176    let value: serde_json::Value = match value {
177        serde_json::Value::String(value) => serde_json::from_str(&value).unwrap_or_default(),
178        value => value,
179    };
180
181    #[derive(Deserialize, Clone)]
182    pub struct OldExportedNestEggConfigsFilesFile {
183        #[serde(default = "true_fn")]
184        pub create_file: bool,
185        pub parser: crate::models::nest_egg::ServerConfigurationFileParser,
186        pub find: IndexMap<compact_str::CompactString, serde_json::Value>,
187    }
188
189    if let Ok(value) = serde_json::from_value::<
190        IndexMap<compact_str::CompactString, OldExportedNestEggConfigsFilesFile>,
191    >(value.clone())
192    {
193        Ok(value
194            .into_iter()
195            .map(|(k, v)| {
196                (
197                    k,
198                    crate::models::nest_egg::ExportedNestEggConfigsFilesFile {
199                        create_new: v.create_file,
200                        parser: v.parser,
201                        replace: v
202                            .find
203                            .into_iter()
204                            .flat_map(|(k, replace_with)| {
205                                let insert_new = !matches!(
206                                    v.parser,
207                                    crate::models::nest_egg::ServerConfigurationFileParser::File
208                                );
209                                let replacements = match replace_with {
210                                    serde_json::Value::Object(values) => values
211                                        .into_iter()
212                                        .map(|(if_value, replace_with)| {
213                                            (Some(if_value.into()), replace_with)
214                                        })
215                                        .collect(),
216                                    replace_with => vec![(None, replace_with)],
217                                };
218
219                                replacements.into_iter().map(move |(if_value, replace_with)| {
220                                    crate::models::nest_egg::ProcessConfigurationFileReplacement {
221                                        r#match: k.clone(),
222                                        insert_new,
223                                        update_existing: true,
224                                        if_value,
225                                        replace_with,
226                                    }
227                                })
228                            })
229                            .collect(),
230                    },
231                )
232            })
233            .collect())
234    } else {
235        serde_json::from_value(value).map_err(serde::de::Error::custom)
236    }
237}
238
239pub fn deserialize_nest_egg_variable_rules<'de, D>(
240    deserializer: D,
241) -> Result<Vec<compact_str::CompactString>, D::Error>
242where
243    D: Deserializer<'de>,
244{
245    let value: serde_json::Value = serde_json::Value::deserialize(deserializer)?;
246    let value: Vec<compact_str::CompactString> = match value {
247        serde_json::Value::String(value) => value.split('|').map(|v| v.into()).collect(),
248        value => serde_json::from_value(value).map_err(serde::de::Error::custom)?,
249    };
250
251    Ok(value)
252}
253
254pub fn deserialize_public_key<'de, D>(deserializer: D) -> Result<russh::keys::PublicKey, D::Error>
255where
256    D: Deserializer<'de>,
257{
258    let value = <&str>::deserialize(deserializer)?;
259    let public_key = russh::keys::PublicKey::from_openssh(value)
260        .map_err(|_| serde::de::Error::custom("invalid public key"))?;
261
262    Ok(public_key)
263}
264
265#[cfg(test)]
266mod tests {
267    use crate::models::nest_egg::ExportedNestEggConfigs;
268    use serde_json::json;
269
270    fn parse(value: serde_json::Value) -> ExportedNestEggConfigs {
271        serde_json::from_value(value).unwrap()
272    }
273
274    fn stop_of(stop: serde_json::Value) -> (String, Option<String>) {
275        let stop = parse(json!({ "stop": stop })).stop;
276        (stop.r#type.to_string(), stop.value.map(|v| v.to_string()))
277    }
278
279    // deserialize_nest_egg_config_stop
280
281    #[test]
282    fn stop_legacy_caret_c_is_sigint() {
283        assert_eq!(
284            stop_of(json!("^C")),
285            ("signal".into(), Some("SIGINT".into()))
286        );
287    }
288
289    #[test]
290    fn stop_legacy_signal_is_uppercased() {
291        assert_eq!(
292            stop_of(json!("^sigterm")),
293            ("signal".into(), Some("SIGTERM".into()))
294        );
295    }
296
297    #[test]
298    fn stop_legacy_unknown_signal_falls_back_to_sigkill() {
299        assert_eq!(
300            stop_of(json!("^^C")),
301            ("signal".into(), Some("SIGKILL".into()))
302        );
303        assert_eq!(
304            stop_of(json!("^SIGHUP")),
305            ("signal".into(), Some("SIGKILL".into()))
306        );
307    }
308
309    #[test]
310    fn stop_plain_string_is_command() {
311        assert_eq!(
312            stop_of(json!("stop")),
313            ("command".into(), Some("stop".into()))
314        );
315    }
316
317    #[test]
318    fn stop_native_object_passes_through() {
319        assert_eq!(
320            stop_of(json!({ "type": "signal", "value": "SIGTERM" })),
321            ("signal".into(), Some("SIGTERM".into()))
322        );
323    }
324
325    // deserialize_nest_egg_config_files
326
327    #[test]
328    fn files_legacy_scalar_find_becomes_structured_replacement() {
329        let files = parse(json!({
330            "files": {
331                "server.properties": {
332                    "parser": "properties",
333                    "find": { "server-port": "{{server.build.default.port}}" }
334                }
335            }
336        }))
337        .files;
338
339        let file = &files["server.properties"];
340        assert!(file.create_new);
341        assert_eq!(
342            serde_json::to_value(file.parser).unwrap(),
343            json!("properties")
344        );
345        assert_eq!(file.replace.len(), 1);
346        let replacement = &file.replace[0];
347        assert_eq!(replacement.r#match, "server-port");
348        assert!(replacement.insert_new);
349        assert!(replacement.update_existing);
350        assert!(replacement.if_value.is_none());
351        assert_eq!(
352            replacement.replace_with,
353            json!("{{server.build.default.port}}")
354        );
355    }
356
357    #[test]
358    fn files_legacy_if_value_map_expands_in_order() {
359        let files = parse(json!({
360            "files": {
361                "config.yml": {
362                    "parser": "yaml",
363                    "find": {
364                        "listeners[0].host": {
365                            "0.0.0.0": "0.0.0.0:25565",
366                            "127.0.0.1": "127.0.0.1:25565"
367                        }
368                    }
369                }
370            }
371        }))
372        .files;
373
374        let replace = &files["config.yml"].replace;
375        assert_eq!(replace.len(), 2);
376        for (replacement, (old, new)) in replace.iter().zip([
377            ("0.0.0.0", "0.0.0.0:25565"),
378            ("127.0.0.1", "127.0.0.1:25565"),
379        ]) {
380            assert_eq!(replacement.r#match, "listeners[0].host");
381            assert_eq!(replacement.if_value.as_deref(), Some(old));
382            assert_eq!(replacement.replace_with, json!(new));
383            assert!(replacement.update_existing);
384        }
385    }
386
387    #[test]
388    fn files_legacy_file_parser_does_not_insert_and_honours_create_file() {
389        let files = parse(json!({
390            "files": {
391                "eula.txt": {
392                    "parser": "file",
393                    "find": { "eula": "eula=true" },
394                    "create_file": false
395                }
396            }
397        }))
398        .files;
399
400        let file = &files["eula.txt"];
401        assert!(!file.create_new);
402        let replacement = &file.replace[0];
403        assert!(!replacement.insert_new);
404        assert!(replacement.update_existing);
405    }
406
407    #[test]
408    fn files_legacy_accepts_pre_stringified_json() {
409        let legacy = json!({
410            "server.properties": {
411                "parser": "properties",
412                "find": { "server-ip": "0.0.0.0" }
413            }
414        })
415        .to_string();
416        let files = parse(json!({ "files": legacy })).files;
417
418        let replace = &files["server.properties"].replace;
419        assert_eq!(replace.len(), 1);
420        assert_eq!(replace[0].r#match, "server-ip");
421        assert_eq!(replace[0].replace_with, json!("0.0.0.0"));
422    }
423}