// SPDX-FileCopyrightText: 2025 Tomasz Kramkowski // SPDX-License-Identifier: GPL-3.0-or-later use std::{ ffi::{OsStr, OsString}, os::unix::ffi::OsStrExt, time::Duration, }; use base64::{engine::general_purpose::STANDARD, Engine as _}; use rumqttc::QoS; use serde::{de, Deserialize, Deserializer}; #[allow(clippy::enum_variant_names)] #[derive(Deserialize, Debug)] #[serde(remote = "QoS", rename_all = "kebab-case")] #[repr(u8)] pub enum QoSDef { AtMostOnce = 0, AtLeastOnce = 1, ExactlyOnce = 2, } pub fn deserialize_qos_opt<'de, D>(deserializer: D) -> Result, D::Error> where D: Deserializer<'de>, { #[derive(Deserialize)] struct Helper(#[serde(with = "QoSDef")] QoS); let helper = Option::deserialize(deserializer)?; Ok(helper.map(|Helper(external)| external)) } pub fn deserialize_timeout<'de, D>(deserializer: D) -> Result where D: Deserializer<'de>, { #[derive(Deserialize)] #[serde(untagged)] enum Untagged { Int(i64), Float(f64), } Ok(match Untagged::deserialize(deserializer)? { Untagged::Int(v) => { if v < 0 { return Err(de::Error::invalid_value( de::Unexpected::Signed(v), &"a non-negative number", )); } if v == 0 { Duration::MAX } else { Duration::from_secs(v as u64) } } Untagged::Float(v) => { if v < 0.0 { return Err(de::Error::invalid_value( de::Unexpected::Float(v), &"a non-negative number", )); } if v == 0.0 { Duration::MAX } else { Duration::from_secs_f64(v) } } }) } pub fn deserialize_timeout_opt<'de, D>(deserializer: D) -> Result, D::Error> where D: Deserializer<'de>, { #[derive(Deserialize)] struct Helper(#[serde(deserialize_with = "deserialize_timeout")] Duration); let helper = Option::deserialize(deserializer)?; Ok(helper.map(|Helper(external)| external)) } pub fn deserialize_box_slice_os_string<'de, D>(deserializer: D) -> Result, D::Error> where D: Deserializer<'de>, { #[derive(Deserialize)] #[serde(untagged)] enum Untagged { String(String), Base64 { b64: String }, } impl TryInto for Untagged { type Error = String; fn try_into(self) -> Result { match self { Untagged::String(s) => Ok(s.into()), Untagged::Base64 { b64 } => match STANDARD.decode(&b64) { Err(_) => Err(b64), Ok(b) => Ok(OsStr::from_bytes(&b).to_owned()), }, } } } Vec::::deserialize(deserializer)? .into_iter() .map(TryInto::::try_into) .collect::>() .map_err(|e| de::Error::invalid_value(de::Unexpected::Str(&e), &"valid Base64")) } impl<'de> Deserialize<'de> for super::Program { fn deserialize>(deserializer: D) -> Result { #[derive(Deserialize)] #[serde(remote = "super::Program")] struct Helper { #[serde(deserialize_with = "deserialize_box_slice_os_string")] command: Box<[OsString]>, #[serde(default, deserialize_with = "deserialize_timeout_opt")] timeout: Option, } #[derive(Deserialize)] #[serde(untagged)] enum Untagged { #[serde(deserialize_with = "deserialize_box_slice_os_string")] Short(Box<[OsString]>), #[serde(with = "Helper")] Full(super::Program), } Ok(match Untagged::deserialize(deserializer)? { Untagged::Short(command) => super::Program { command, timeout: None, }, Untagged::Full(program) => program, }) } } impl<'de> Deserialize<'de> for super::Route { fn deserialize>(deserializer: D) -> Result { #[derive(Deserialize)] #[serde(remote = "super::Route")] struct Helper { programs: Box<[super::Program]>, #[serde(default, deserialize_with = "deserialize_qos_opt")] qos: Option, } #[derive(Deserialize)] #[serde(untagged)] enum Untagged { Short(Box<[super::Program]>), #[serde(with = "Helper")] Full(super::Route), } Ok(match Untagged::deserialize(deserializer)? { Untagged::Short(programs) => super::Route { programs, qos: None, }, Untagged::Full(route) => route, }) } }