use std::io::{Read, Seek};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use crate::{error::ModpkgError, Modpkg};
pub const LICENSE_CHUNK_PATH: &str = "_meta_/license";
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
#[cfg_attr(test, derive(proptest_derive::Arbitrary))]
pub enum ModpkgLicense {
#[default]
None,
Spdx {
spdx_id: String,
},
Custom {
name: String,
#[serde(
default,
serialize_with = "serialize_url",
deserialize_with = "deserialize_url"
)]
#[cfg_attr(test, proptest(strategy = "tests::arbitrary_url()"))]
url: Option<String>,
},
}
fn serialize_url<S: Serializer>(url: &Option<String>, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(url.as_deref().unwrap_or(""))
}
fn deserialize_url<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Option<String>, D::Error> {
Ok(Option::<String>::deserialize(deserializer)?.filter(|url| !url.is_empty()))
}
impl<TSource: Read + Seek> Modpkg<TSource> {
pub fn load_license_text(&mut self) -> Result<Vec<u8>, ModpkgError> {
let chunk = *self.chunk(LICENSE_CHUNK_PATH, None)?;
if chunk.layer().is_some() || chunk.wad().is_some() {
return Err(ModpkgError::InvalidMetaChunk);
}
let data = self.decoder().load_chunk_decompressed(&chunk)?;
Ok(data.into_vec())
}
}
#[cfg(test)]
mod tests {
use super::*;
use proptest::prelude::*;
pub(super) fn arbitrary_url() -> impl Strategy<Value = Option<String>> {
proptest::option::of("\\PC{1,32}")
}
proptest! {
#[test]
fn test_license_roundtrip(license: ModpkgLicense) {
let encoded = rmp_serde::to_vec_named(&license).unwrap();
let decoded: ModpkgLicense = rmp_serde::from_slice(&encoded).unwrap();
prop_assert_eq!(license, decoded);
}
}
#[test]
fn test_empty_url_decodes_as_none() {
let encoded = rmp_serde::to_vec_named(&ModpkgLicense::Custom {
name: "My License".to_string(),
url: Some(String::new()),
})
.unwrap();
let decoded: ModpkgLicense = rmp_serde::from_slice(&encoded).unwrap();
assert_eq!(
decoded,
ModpkgLicense::Custom {
name: "My License".to_string(),
url: None,
}
);
}
#[test]
fn test_custom_license_without_url_roundtrip() {
let license = ModpkgLicense::Custom {
name: "My License".to_string(),
url: None,
};
let encoded = rmp_serde::to_vec_named(&license).unwrap();
let decoded: ModpkgLicense = rmp_serde::from_slice(&encoded).unwrap();
assert_eq!(license, decoded);
}
#[test]
fn test_url_less_custom_license_decodes_with_a_legacy_reader() {
#[derive(Debug, Deserialize, PartialEq)]
#[serde(tag = "type", rename_all = "snake_case")]
enum LegacyLicense {
None,
Spdx { spdx_id: String },
Custom { name: String, url: String },
}
let encoded = rmp_serde::to_vec_named(&ModpkgLicense::Custom {
name: "My License".to_string(),
url: None,
})
.unwrap();
let decoded: LegacyLicense = rmp_serde::from_slice(&encoded).unwrap();
assert_eq!(
decoded,
LegacyLicense::Custom {
name: "My License".to_string(),
url: String::new(),
}
);
}
#[test]
fn test_missing_url_key_decodes_as_none() {
#[derive(Serialize)]
#[serde(tag = "type", rename_all = "snake_case")]
enum UrlLessWriter {
Custom { name: String },
}
let encoded = rmp_serde::to_vec_named(&UrlLessWriter::Custom {
name: "My License".to_string(),
})
.unwrap();
let decoded: ModpkgLicense = rmp_serde::from_slice(&encoded).unwrap();
assert_eq!(
decoded,
ModpkgLicense::Custom {
name: "My License".to_string(),
url: None,
}
);
}
}