use schemars::JsonSchema;
use serde::{Deserialize, Deserializer, Serialize};
use super::{StringOrBool, deserialize_string_or_bool_opt};
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(default, deny_unknown_fields)]
pub struct UpxConfig {
pub id: Option<String>,
pub ids: Option<Vec<String>>,
#[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
pub enabled: Option<StringOrBool>,
pub binary: String,
pub args: Vec<String>,
pub required: bool,
pub targets: Option<Vec<String>>,
pub compress: Option<String>,
pub lzma: Option<bool>,
pub brute: Option<bool>,
}
impl Default for UpxConfig {
fn default() -> Self {
UpxConfig {
id: None,
ids: None,
enabled: None,
binary: "upx".to_string(),
args: Vec::new(),
required: false,
targets: None,
compress: None,
lzma: None,
brute: None,
}
}
}
pub(super) fn deserialize_upx<'de, D>(deserializer: D) -> Result<Vec<UpxConfig>, D::Error>
where
D: Deserializer<'de>,
{
use serde::de::{self, Visitor};
struct UpxVisitor;
impl<'de> Visitor<'de> for UpxVisitor {
type Value = Vec<UpxConfig>;
fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("a UPX config object or an array of UPX config objects")
}
fn visit_seq<A: de::SeqAccess<'de>>(self, mut seq: A) -> Result<Self::Value, A::Error> {
let mut configs = Vec::new();
while let Some(item) = seq.next_element::<UpxConfig>()? {
configs.push(item);
}
Ok(configs)
}
fn visit_map<M: de::MapAccess<'de>>(self, map: M) -> Result<Self::Value, M::Error> {
let config = UpxConfig::deserialize(de::value::MapAccessDeserializer::new(map))?;
Ok(vec![config])
}
fn visit_unit<E: de::Error>(self) -> Result<Self::Value, E> {
Ok(Vec::new())
}
fn visit_none<E: de::Error>(self) -> Result<Self::Value, E> {
Ok(Vec::new())
}
}
deserializer.deserialize_any(UpxVisitor)
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Deserialize)]
struct UpxWrapper {
#[serde(default, deserialize_with = "deserialize_upx")]
upx: Vec<UpxConfig>,
}
#[test]
fn default_fills_upx_binary_and_empty_lists() {
let d = UpxConfig::default();
assert_eq!(d.binary, "upx");
assert!(d.args.is_empty());
assert!(!d.required);
assert!(d.id.is_none());
assert!(d.ids.is_none());
assert!(d.enabled.is_none());
assert!(d.targets.is_none());
assert!(d.compress.is_none());
assert!(d.lzma.is_none());
assert!(d.brute.is_none());
}
#[test]
fn single_object_becomes_one_element_vec() {
let w: UpxWrapper = serde_yaml_ng::from_str("upx:\n compress: best\n").unwrap();
assert_eq!(w.upx.len(), 1);
assert_eq!(w.upx[0].compress.as_deref(), Some("best"));
assert_eq!(w.upx[0].binary, "upx");
}
#[test]
fn array_collects_every_entry() {
let w: UpxWrapper =
serde_yaml_ng::from_str("upx:\n - id: a\n - id: b\n - id: c\n").unwrap();
assert_eq!(w.upx.len(), 3);
assert_eq!(w.upx[0].id.as_deref(), Some("a"));
assert_eq!(w.upx[2].id.as_deref(), Some("c"));
}
#[test]
fn null_and_missing_yield_empty_vec() {
let null: UpxWrapper = serde_yaml_ng::from_str("upx: null").unwrap();
assert!(null.upx.is_empty());
let missing: UpxWrapper = serde_yaml_ng::from_str("{}").unwrap();
assert!(missing.upx.is_empty());
}
#[test]
fn unknown_field_is_rejected() {
let r: Result<UpxWrapper, _> = serde_yaml_ng::from_str("upx:\n compres: best\n");
assert!(r.is_err(), "unknown field `compres` must be rejected");
}
#[test]
fn enabled_accepts_bool_and_template_string() {
let as_bool: UpxWrapper = serde_yaml_ng::from_str("upx:\n enabled: true\n").unwrap();
assert_eq!(as_bool.upx[0].enabled, Some(StringOrBool::Bool(true)));
let as_tmpl: UpxWrapper =
serde_yaml_ng::from_str("upx:\n enabled: \"{{ if IsSnapshot }}false{{ endif }}\"\n")
.unwrap();
assert_eq!(
as_tmpl.upx[0].enabled,
Some(StringOrBool::String(
"{{ if IsSnapshot }}false{{ endif }}".into()
))
);
}
#[test]
fn full_flag_surface_deserializes() {
let w: UpxWrapper = serde_yaml_ng::from_str(
"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",
)
.unwrap();
let c = &w.upx[0];
assert_eq!(c.binary, "/opt/upx");
assert_eq!(c.args, vec!["-9", "--brute"]);
assert!(c.required);
assert_eq!(c.targets.as_deref().unwrap(), ["x86_64-unknown-linux-gnu"]);
assert_eq!(c.compress.as_deref(), Some("9"));
assert_eq!(c.lzma, Some(true));
assert_eq!(c.brute, Some(false));
assert_eq!(c.ids.as_deref().unwrap(), ["cli"]);
}
}