Skip to main content

anodizer_core/config/
upx.rs

1use schemars::JsonSchema;
2use serde::{Deserialize, Deserializer, Serialize};
3
4use super::{StringOrBool, deserialize_string_or_bool_opt};
5
6// ---------------------------------------------------------------------------
7// UpxConfig
8// ---------------------------------------------------------------------------
9
10#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
11#[serde(default, deny_unknown_fields)]
12pub struct UpxConfig {
13    /// Unique identifier for this UPX config.
14    pub id: Option<String>,
15    /// Build IDs filter: only compress binaries from builds whose `id` is in this list.
16    pub ids: Option<Vec<String>>,
17    /// Whether to compress binaries with UPX.
18    /// Accepts a bool or a template string that evaluates to a bool.
19    #[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
20    pub enabled: Option<StringOrBool>,
21    /// UPX executable path or name (default: "upx").
22    pub binary: String,
23    /// Extra arguments passed to UPX (e.g., ["-9", "--brute"]).
24    pub args: Vec<String>,
25    /// When true, fail the build if UPX is not found.
26    pub required: bool,
27    /// Target triples to compress binaries for (empty means all targets).
28    pub targets: Option<Vec<String>>,
29    /// UPX compression level string (e.g., "1"-"9", "best"). Maps to `--compress` flag.
30    pub compress: Option<String>,
31    /// Use LZMA compression (--lzma flag).
32    pub lzma: Option<bool>,
33    /// Use brute-force compression (--brute flag). Very slow but produces smallest output.
34    pub brute: Option<bool>,
35}
36
37impl Default for UpxConfig {
38    fn default() -> Self {
39        UpxConfig {
40            id: None,
41            ids: None,
42            enabled: None,
43            binary: "upx".to_string(),
44            args: Vec::new(),
45            required: false,
46            targets: None,
47            compress: None,
48            lzma: None,
49            brute: None,
50        }
51    }
52}
53
54/// Custom deserializer for the `upx` field.
55/// Accepts:
56///   - null/missing → empty vec (via serde default)
57///   - a single object → vec of one UpxConfig
58///   - an array → vec of UpxConfig
59pub(super) fn deserialize_upx<'de, D>(deserializer: D) -> Result<Vec<UpxConfig>, D::Error>
60where
61    D: Deserializer<'de>,
62{
63    use serde::de::{self, Visitor};
64
65    struct UpxVisitor;
66
67    impl<'de> Visitor<'de> for UpxVisitor {
68        type Value = Vec<UpxConfig>;
69
70        fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
71            f.write_str("a UPX config object or an array of UPX config objects")
72        }
73
74        fn visit_seq<A: de::SeqAccess<'de>>(self, mut seq: A) -> Result<Self::Value, A::Error> {
75            let mut configs = Vec::new();
76            while let Some(item) = seq.next_element::<UpxConfig>()? {
77                configs.push(item);
78            }
79            Ok(configs)
80        }
81
82        fn visit_map<M: de::MapAccess<'de>>(self, map: M) -> Result<Self::Value, M::Error> {
83            let config = UpxConfig::deserialize(de::value::MapAccessDeserializer::new(map))?;
84            Ok(vec![config])
85        }
86
87        fn visit_unit<E: de::Error>(self) -> Result<Self::Value, E> {
88            Ok(Vec::new())
89        }
90
91        fn visit_none<E: de::Error>(self) -> Result<Self::Value, E> {
92            Ok(Vec::new())
93        }
94    }
95
96    deserializer.deserialize_any(UpxVisitor)
97}
98
99#[cfg(test)]
100mod tests {
101    use super::*;
102
103    // `upx:` uses the hand-written `deserialize_upx` (single-object OR array OR
104    // null → Vec<UpxConfig>). Each visitor arm is driven through a wrapper
105    // struct that mirrors the real field attributes.
106    #[derive(Deserialize)]
107    struct UpxWrapper {
108        #[serde(default, deserialize_with = "deserialize_upx")]
109        upx: Vec<UpxConfig>,
110    }
111
112    #[test]
113    fn default_fills_upx_binary_and_empty_lists() {
114        let d = UpxConfig::default();
115        assert_eq!(d.binary, "upx");
116        assert!(d.args.is_empty());
117        assert!(!d.required);
118        assert!(d.id.is_none());
119        assert!(d.ids.is_none());
120        assert!(d.enabled.is_none());
121        assert!(d.targets.is_none());
122        assert!(d.compress.is_none());
123        assert!(d.lzma.is_none());
124        assert!(d.brute.is_none());
125    }
126
127    #[test]
128    fn single_object_becomes_one_element_vec() {
129        let w: UpxWrapper = serde_yaml_ng::from_str("upx:\n  compress: best\n").unwrap();
130        assert_eq!(w.upx.len(), 1);
131        assert_eq!(w.upx[0].compress.as_deref(), Some("best"));
132        // Fields omitted from YAML still take their struct defaults.
133        assert_eq!(w.upx[0].binary, "upx");
134    }
135
136    #[test]
137    fn array_collects_every_entry() {
138        let w: UpxWrapper =
139            serde_yaml_ng::from_str("upx:\n  - id: a\n  - id: b\n  - id: c\n").unwrap();
140        assert_eq!(w.upx.len(), 3);
141        assert_eq!(w.upx[0].id.as_deref(), Some("a"));
142        assert_eq!(w.upx[2].id.as_deref(), Some("c"));
143    }
144
145    #[test]
146    fn null_and_missing_yield_empty_vec() {
147        let null: UpxWrapper = serde_yaml_ng::from_str("upx: null").unwrap();
148        assert!(null.upx.is_empty());
149        let missing: UpxWrapper = serde_yaml_ng::from_str("{}").unwrap();
150        assert!(missing.upx.is_empty());
151    }
152
153    #[test]
154    fn unknown_field_is_rejected() {
155        // `deny_unknown_fields` on UpxConfig must reject typos rather than
156        // silently drop them into a no-op.
157        let r: Result<UpxWrapper, _> = serde_yaml_ng::from_str("upx:\n  compres: best\n");
158        assert!(r.is_err(), "unknown field `compres` must be rejected");
159    }
160
161    #[test]
162    fn enabled_accepts_bool_and_template_string() {
163        let as_bool: UpxWrapper = serde_yaml_ng::from_str("upx:\n  enabled: true\n").unwrap();
164        assert_eq!(as_bool.upx[0].enabled, Some(StringOrBool::Bool(true)));
165
166        let as_tmpl: UpxWrapper =
167            serde_yaml_ng::from_str("upx:\n  enabled: \"{{ if IsSnapshot }}false{{ endif }}\"\n")
168                .unwrap();
169        assert_eq!(
170            as_tmpl.upx[0].enabled,
171            Some(StringOrBool::String(
172                "{{ if IsSnapshot }}false{{ endif }}".into()
173            ))
174        );
175    }
176
177    #[test]
178    fn full_flag_surface_deserializes() {
179        let w: UpxWrapper = serde_yaml_ng::from_str(
180            "upx:\n  - id: pack\n    binary: /opt/upx\n    args: [\"-9\", \"--brute\"]\n    required: true\n    targets: [\"x86_64-unknown-linux-gnu\"]\n    compress: \"9\"\n    lzma: true\n    brute: false\n    ids: [\"cli\"]\n",
181        )
182        .unwrap();
183        let c = &w.upx[0];
184        assert_eq!(c.binary, "/opt/upx");
185        assert_eq!(c.args, vec!["-9", "--brute"]);
186        assert!(c.required);
187        assert_eq!(c.targets.as_deref().unwrap(), ["x86_64-unknown-linux-gnu"]);
188        assert_eq!(c.compress.as_deref(), Some("9"));
189        assert_eq!(c.lzma, Some(true));
190        assert_eq!(c.brute, Some(false));
191        assert_eq!(c.ids.as_deref().unwrap(), ["cli"]);
192    }
193}