use std::path::Path;
use anyhow::{anyhow, Context, Result};
use base64::Engine;
use ordered_float::NotNan;
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use super::metadata::AppleDesktop;
#[derive(Deserialize, Serialize, PartialEq, Eq, Debug)]
pub struct PropertiesH24 {
#[serde(rename = "ap", default)]
pub appearance: Option<PropertiesAppearance>,
#[serde(rename = "ti")]
pub time_info: Vec<TimeItem>,
}
#[derive(Deserialize, Serialize, PartialEq, Eq, Debug)]
pub struct PropertiesAppearance {
#[serde(rename = "d")]
pub dark: i32,
#[serde(rename = "l")]
pub light: i32,
}
#[derive(Deserialize, Serialize, PartialEq, Eq, Clone, Debug)]
pub struct TimeItem {
#[serde(rename = "i")]
pub index: usize,
#[serde(rename = "t")]
pub time: NotNan<f64>,
}
#[derive(Deserialize, Serialize, PartialEq, Eq, Debug)]
pub struct PropertiesSolar {
#[serde(rename = "ap", default)]
pub appearance: Option<PropertiesAppearance>,
#[serde(rename = "si")]
pub solar_info: Vec<SolarItem>,
}
#[derive(Deserialize, Serialize, PartialEq, Eq, Clone, Debug)]
pub struct SolarItem {
#[serde(rename = "i")]
pub index: usize,
#[serde(rename = "a")]
pub altitude: NotNan<f64>,
#[serde(rename = "z")]
pub azimuth: NotNan<f64>,
}
pub trait Plist: DeserializeOwned + Serialize {
fn from_base64(base64_value: &[u8]) -> Result<Self> {
let decoded = base64::engine::general_purpose::STANDARD
.decode(base64_value)
.with_context(|| "could not decode plist base64")?;
plist::from_bytes(decoded.as_slice()).with_context(|| "could not parse plist bytes")
}
fn from_xml_file<T: AsRef<Path>>(path: T) -> Result<Self> {
plist::from_file(path).with_context(|| "could not read plist from XML file")
}
fn to_xml_file<T: AsRef<Path>>(&self, path: T) -> Result<()> {
plist::to_file_xml(path, &self).with_context(|| "could not write plist to XML file")
}
}
impl Plist for PropertiesH24 {}
impl Plist for PropertiesSolar {}
impl Plist for PropertiesAppearance {}
#[derive(Debug)]
pub enum Properties {
H24(PropertiesH24),
Solar(PropertiesSolar),
Appearance(PropertiesAppearance),
}
impl Properties {
pub fn from_apple_desktop(apple_desktop: &AppleDesktop) -> Result<Self> {
let properties = match apple_desktop {
AppleDesktop::H24(value) => {
Properties::H24(PropertiesH24::from_base64(value.as_bytes())?)
}
AppleDesktop::Solar(value) => {
Properties::Solar(PropertiesSolar::from_base64(value.as_bytes())?)
}
AppleDesktop::Apr(value) => {
Properties::Appearance(PropertiesAppearance::from_base64(value.as_bytes())?)
}
};
Ok(properties)
}
pub fn from_xml_file<P: AsRef<Path>>(path: P) -> Result<Self> {
if let Ok(properties_h24) = PropertiesH24::from_xml_file(&path) {
return Ok(Self::H24(properties_h24));
}
if let Ok(properties_solar) = PropertiesSolar::from_xml_file(&path) {
return Ok(Self::Solar(properties_solar));
}
if let Ok(properties_appearance) = PropertiesAppearance::from_xml_file(&path) {
return Ok(Self::Appearance(properties_appearance));
}
Err(anyhow!(
"invalid properties file {}",
path.as_ref().display()
))
}
pub fn to_xml_file<P: AsRef<Path>>(&self, dest_path: P) -> Result<()> {
match self {
Properties::H24(props) => props.to_xml_file(dest_path),
Properties::Solar(props) => props.to_xml_file(dest_path),
Properties::Appearance(props) => props.to_xml_file(dest_path),
}
}
pub fn num_images(&self) -> usize {
let max_index = match self {
Properties::H24(props) => props.time_info.iter().map(|item| item.index).max(),
Properties::Solar(props) => props.solar_info.iter().map(|item| item.index).max(),
Properties::Appearance(..) => Some(1),
};
max_index.unwrap() + 1
}
pub fn num_frames(&self) -> usize {
match self {
Properties::H24(props) => props.time_info.len(),
Properties::Solar(props) => props.solar_info.len(),
Properties::Appearance(..) => 2,
}
}
pub fn appearance(&self) -> Option<&PropertiesAppearance> {
match self {
Properties::Appearance(ref appearance) => Some(appearance),
Properties::H24(PropertiesH24 {
appearance: maybe_appearance,
..
}) => maybe_appearance.as_ref(),
Properties::Solar(PropertiesSolar {
appearance: maybe_appearance,
..
}) => maybe_appearance.as_ref(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
const H24_PLIST_BASE64: &str = "YnBsaXN0MDDSAQIDBFJhcFJ0adIFBgcIUWRRbBAFEAKiCQrSCwwNDlF0UWkjP9KqqqAAAAAQANILDA8QIwAAAAAAAAAAEAEIDRATIBgaHB4jNygqLDU8RQAAAAAAAAEBAAAAAAAAABEAAAAAAAAAAAAAAAAAAABH";
const SOLAR_PLIST_BASE64: &str = "YnBsaXN0MDDSAQIDBFJhcFJzadIFBgcIUWRRbBABEACiCQrTCwwNDggPUWFRaVF6I0AuAAAAAAAAI0BgQAAAAAAA0wsMDRAHESPAUYAAAAAAACNASwAAAAAAAAgNEBMgGBocHiNCKiwuMDlJUgAAAAAAAAEBAAAAAAAAABIAAAAAAAAAAAAAAAAAAABb";
const APPEARANCE_PLIST_BASE64: &str =
"YnBsaXN0MDDSAQIDBFFsUWQQABABCA0PERMAAAAAAAABAQAAAAAAAAAFAAAAAAAAAAAAAAAAAAAAFQ==";
#[test]
fn test_plist_h24_from_base64() {
let expected = PropertiesH24 {
appearance: Some(PropertiesAppearance { dark: 5, light: 2 }),
time_info: vec![
TimeItem {
index: 0,
time: not_nan!(0.2916666567325592),
},
TimeItem {
index: 1,
time: not_nan!(0.0),
},
],
};
let result = PropertiesH24::from_base64(H24_PLIST_BASE64.as_bytes()).unwrap();
assert_eq!(result, expected);
}
#[test]
fn test_plist_solar_from_base64() {
let expected = PropertiesSolar {
appearance: Some(PropertiesAppearance { dark: 1, light: 0 }),
solar_info: vec![
SolarItem {
index: 0,
altitude: not_nan!(15.0),
azimuth: not_nan!(130.0),
},
SolarItem {
index: 1,
altitude: not_nan!(-70.0),
azimuth: not_nan!(54.0),
},
],
};
let result = PropertiesSolar::from_base64(SOLAR_PLIST_BASE64.as_bytes()).unwrap();
assert_eq!(result, expected);
}
#[test]
fn test_plist_appearance_from_base64() {
let expected = PropertiesAppearance { dark: 1, light: 0 };
let result = PropertiesAppearance::from_base64(APPEARANCE_PLIST_BASE64.as_bytes()).unwrap();
assert_eq!(result, expected);
}
}