use base64::Engine as _;
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use bevy::prelude::*;
use ring::signature::{ED25519, Ed25519KeyPair, KeyPair, UnparsedPublicKey};
use secure_gate::{ExposeSecret, dynamic_alias, fixed_alias};
mod asset_loader;
pub mod encrypt_key_registry;
mod ext;
#[macro_use]
mod macros;
mod pack_format;
pub use asset_loader::{
DlcLoader, DlcPack, DlcPackLoader, DlcPackLoaderSettings, EncryptedAsset, parse_encrypted,
};
pub use pack_format::{
BlockMetadata, CompressionLevel, DEFAULT_BLOCK_SIZE, DLC_PACK_MAGIC, DLC_PACK_VERSION_LATEST,
ManifestEntry, V4ManifestEntry, is_data_executable, is_forbidden_extension,
pack_encrypted_pack, parse_encrypted_pack,
};
use serde::{Deserialize, Serialize};
use thiserror::Error;
use crate::asset_loader::DlcPackLoaded;
#[allow(unused_imports)]
pub use bevy_dlc_macro::include_signed_license_aes;
#[doc(hidden)]
pub use crate::macros::__decode_embedded_signed_license_aes;
pub use crate::ext::AppExt;
pub fn register_encryption_key(dlc_id: &str, key: EncryptionKey) {
encrypt_key_registry::insert(dlc_id, key);
}
pub mod prelude {
pub use crate::ext::*;
pub use crate::{
DlcError, DlcId, DlcKey, DlcLoader, DlcPack, DlcPackLoader, DlcPackLoaderSettings,
DlcPlugin, EncryptedAsset, PackItem, Product, SignedLicense,
asset_loader::DlcPackEntry, asset_loader::DlcPackLoaded, is_dlc_entry_loaded,
is_dlc_loaded,
};
pub use bevy_dlc_macro::include_signed_license_aes;
pub use crate::include_dlc_key_and_license_aes;
}
pub struct DlcPlugin {
dlc_key: DlcKey,
signed_license: SignedLicense,
}
impl DlcPlugin {
pub(crate) fn new(dlc_key: DlcKey, signed_license: SignedLicense) -> Self {
Self {
dlc_key,
signed_license,
}
}
}
impl From<(DlcKey, SignedLicense)> for DlcPlugin {
fn from(tuple: (DlcKey, SignedLicense)) -> Self {
DlcPlugin::new(tuple.0, tuple.1)
}
}
impl Plugin for DlcPlugin {
fn build(&self, app: &mut App) {
if let Some(encrypt_key) = extract_encrypt_key_from_license(&self.signed_license) {
let dlcs = extract_dlc_ids_from_license(&self.signed_license);
for dlc_id in dlcs {
let key_for_dlc = encrypt_key.with_secret(|kb| EncryptionKey::new(*kb));
encrypt_key_registry::insert(&dlc_id, key_for_dlc);
}
}
app.init_resource::<asset_loader::DlcPackRegistrarFactories>();
app.insert_resource(self.dlc_key.clone())
.init_asset_loader::<asset_loader::DlcLoader<Image>>()
.init_asset_loader::<asset_loader::DlcLoader<Scene>>()
.init_asset_loader::<asset_loader::DlcLoader<Mesh>>()
.init_asset_loader::<asset_loader::DlcLoader<Font>>()
.init_asset_loader::<asset_loader::DlcLoader<AudioSource>>()
.init_asset_loader::<asset_loader::DlcLoader<ColorMaterial>>()
.init_asset_loader::<asset_loader::DlcLoader<StandardMaterial>>()
.init_asset_loader::<asset_loader::DlcLoader<Gltf>>()
.init_asset_loader::<asset_loader::DlcLoader<bevy::gltf::GltfMesh>>()
.init_asset_loader::<asset_loader::DlcLoader<Shader>>()
.init_asset_loader::<asset_loader::DlcLoader<DynamicScene>>()
.init_asset_loader::<asset_loader::DlcLoader<AnimationClip>>()
.init_asset_loader::<asset_loader::DlcLoader<AnimationGraph>>();
let factories = app
.world()
.get_resource::<asset_loader::DlcPackRegistrarFactories>()
.cloned();
let pack_loader = asset_loader::DlcPackLoader {
registrars: asset_loader::collect_pack_registrars(factories.as_ref()),
factories,
};
app.register_asset_loader(pack_loader);
app.init_asset::<asset_loader::DlcPack>();
app.add_systems(Update, trigger_dlc_events);
}
}
fn trigger_dlc_events(
mut events: MessageReader<AssetEvent<DlcPack>>,
packs: Res<Assets<DlcPack>>,
mut commands: Commands,
) {
for event in events.read() {
match event {
AssetEvent::Added { id } => {
if let Some(pack) = packs.get(*id) {
let dlc_id = pack.id().clone();
commands.trigger(DlcPackLoaded::new(dlc_id.clone(), pack.clone()));
}
}
_ => {}
}
}
}
pub fn is_dlc_loaded(dlc_id: impl Into<DlcId>) -> impl Fn() -> bool + Send + Sync + 'static {
let id_string = dlc_id.into().0;
move || !encrypt_key_registry::asset_path_for(&id_string).is_none()
}
pub fn is_dlc_entry_loaded(
dlc_id: impl Into<DlcId>,
entry: impl Into<String>,
) -> impl Fn(Res<Assets<DlcPack>>) -> bool + Send + Sync + 'static {
let id_string = dlc_id.into().0;
let entry_name = entry.into();
move |dlc_packs: Res<Assets<DlcPack>>| {
if !encrypt_key_registry::asset_path_for(&id_string).is_none() {
dlc_packs
.iter()
.filter(|p| p.1.id() == &DlcId::from(id_string.clone()))
.any(|pack| pack.1.find_entry(&entry_name).is_some())
} else {
false
}
}
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[serde(transparent)]
pub struct DlcId(pub String);
impl std::fmt::Display for DlcId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.fmt(f)
}
}
impl Clone for DlcId {
fn clone(&self) -> Self {
DlcId(self.0.clone())
}
}
impl DlcId {
pub fn as_str(&self) -> &str {
self.0.as_str()
}
}
impl From<&str> for DlcId {
fn from(s: &str) -> Self {
DlcId(s.to_owned())
}
}
impl From<String> for DlcId {
fn from(s: String) -> Self {
DlcId(s)
}
}
impl AsRef<str> for DlcId {
fn as_ref(&self) -> &str {
&self.0
}
}
fixed_alias!(pub PrivateKey, 32, "A secure wrapper for a 32-byte Ed25519 signing seed (private key) used to create signed licenses. This should be protected and never exposed in logs or error messages.");
#[derive(Clone, Copy, PartialEq, Eq)]
pub struct PublicKey(pub [u8; 32]);
impl AsRef<[u8]> for PublicKey {
fn as_ref(&self) -> &[u8] {
&self.0
}
}
impl std::fmt::Debug for PublicKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "PublicKey({} bytes)", 32)
}
}
dynamic_alias!(pub SignedLicense, String, "A compact offline-signed token containing DLC ids and an optional embedded encrypt key. Treat as sensitive and do not leak raw secrets in logs. This ");
pub fn extract_encrypt_key_from_license(license: &SignedLicense) -> Option<EncryptionKey> {
license.with_secret(|token_str| {
let parts: Vec<&str> = token_str.split('.').collect();
if parts.len() != 2 {
return None;
}
let payload = URL_SAFE_NO_PAD.decode(parts[0].as_bytes()).ok()?;
let payload_json: serde_json::Value = serde_json::from_slice(&payload).ok()?;
let key_b64 = payload_json.get("encrypt_key").and_then(|v| v.as_str())?;
URL_SAFE_NO_PAD
.decode(key_b64.as_bytes())
.ok()
.and_then(|key_bytes| Some(EncryptionKey::new(key_bytes.try_into().ok()?)))
})
}
pub fn extract_dlc_ids_from_license(license: &SignedLicense) -> Vec<String> {
license.with_secret(|token_str| {
let parts: Vec<&str> = token_str.split('.').collect();
if parts.len() != 2 {
return Vec::new();
}
if let Ok(payload) = URL_SAFE_NO_PAD.decode(parts[0].as_bytes()) {
if let Ok(payload_json) = serde_json::from_slice::<serde_json::Value>(&payload) {
if let Some(dlcs_array) = payload_json.get("dlcs").and_then(|v| v.as_array()) {
let mut dlcs = Vec::new();
for dlc in dlcs_array {
if let Some(dlc_id) = dlc.as_str() {
dlcs.push(dlc_id.to_string());
}
}
return dlcs;
}
}
}
Vec::new()
})
}
pub fn extract_product_from_license(license: &SignedLicense) -> Option<String> {
license.with_secret(|token_str| {
let parts: Vec<&str> = token_str.split('.').collect();
if parts.len() != 2 {
return None;
}
if let Ok(payload) = URL_SAFE_NO_PAD.decode(parts[0].as_bytes()) {
if let Ok(payload_json) = serde_json::from_slice::<serde_json::Value>(&payload) {
if let Some(prod) = payload_json.get("product").and_then(|v| v.as_str()) {
return Some(prod.to_string());
}
}
}
None
})
}
#[derive(Resource, Clone, PartialEq, Eq, Debug)]
pub struct Product(String);
impl std::fmt::Display for Product {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.fmt(f)
}
}
impl AsRef<str> for Product {
fn as_ref(&self) -> &str {
&self.0
}
}
impl Product {
pub fn as_str(&self) -> &str {
self.0.as_str()
}
}
impl From<String> for Product {
fn from(s: String) -> Self {
Product(s)
}
}
impl From<&str> for Product {
fn from(s: &str) -> Self {
Product(s.to_owned())
}
}
fixed_alias!(pub EncryptionKey, 32, "A secure encrypt key (symmetric key for encrypting DLC pack entries). This should be protected and never exposed in logs or error messages.");
#[derive(Resource)]
pub enum DlcKey {
Private {
privkey: PrivateKey,
pubkey: PublicKey,
},
Public {
pubkey: PublicKey,
},
}
impl DlcKey {
pub fn new(pubkey: &str, privkey: &str) -> Result<Self, DlcError> {
let decoded_pub = URL_SAFE_NO_PAD
.decode(pubkey.as_bytes())
.map_err(|e| DlcError::CryptoError(format!("invalid pubkey base64: {}", e)))?;
if decoded_pub.len() != 32 {
return Err(DlcError::CryptoError("public key must be 32 bytes".into()));
}
let mut pub_bytes = [0u8; 32];
pub_bytes.copy_from_slice(&decoded_pub);
let decoded_priv = URL_SAFE_NO_PAD
.decode(privkey.as_bytes())
.map_err(|e| DlcError::CryptoError(format!("invalid privkey base64: {}", e)))?;
if decoded_priv.len() != 32 {
return Err(DlcError::CryptoError("private key must be 32 bytes".into()));
}
let mut priv_bytes = [0u8; 32];
priv_bytes.copy_from_slice(&decoded_priv);
Self::from_priv_and_pub(PrivateKey::from(priv_bytes), PublicKey(pub_bytes))
}
pub fn public(pubkey: &str) -> Result<Self, DlcError> {
let decoded_pub = URL_SAFE_NO_PAD
.decode(pubkey.as_bytes())
.map_err(|e| DlcError::CryptoError(format!("invalid pubkey base64: {}", e)))?;
if decoded_pub.len() != 32 {
return Err(DlcError::CryptoError("public key must be 32 bytes".into()));
}
let mut pub_bytes = [0u8; 32];
pub_bytes.copy_from_slice(&decoded_pub);
Ok(DlcKey::Public {
pubkey: PublicKey(pub_bytes),
})
}
pub(crate) fn from_priv_and_pub(
privkey: PrivateKey,
publickey: PublicKey,
) -> Result<Self, DlcError> {
let kp = privkey
.with_secret(|priv_bytes| {
Ed25519KeyPair::from_seed_and_public_key(priv_bytes, &publickey.0)
})
.map_err(|e| DlcError::CryptoError(format!("invalid seed: {:?}", e)))?;
let mut pub_bytes = [0u8; 32];
pub_bytes.copy_from_slice(kp.public_key().as_ref());
privkey
.with_secret(|priv_bytes| {
Ed25519KeyPair::from_seed_and_public_key(priv_bytes, &pub_bytes)
})
.map_err(|e| DlcError::CryptoError(format!("keypair validation failed: {:?}", e)))?;
Ok(DlcKey::Private {
privkey,
pubkey: PublicKey(pub_bytes),
})
}
pub fn generate_random() -> Self {
let privkey: PrivateKey = PrivateKey::from_random();
let pair = privkey
.with_secret(|priv_bytes| Ed25519KeyPair::from_seed_unchecked(priv_bytes))
.expect("derive public key from seed");
let mut pub_bytes = [0u8; 32];
pub_bytes.copy_from_slice(pair.public_key().as_ref());
Self::from_priv_and_pub(privkey, PublicKey(pub_bytes))
.unwrap_or_else(|e| panic!("generate_complete failed: {:?}", e))
}
pub fn get_public_key(&self) -> &PublicKey {
match self {
DlcKey::Private { pubkey, .. } => pubkey,
DlcKey::Public { pubkey: public } => public,
}
}
pub fn sign(&self, data: &[u8]) -> Result<[u8; 64], DlcError> {
match self {
DlcKey::Private { privkey, pubkey } => privkey.with_secret(|seed| {
let pair = Ed25519KeyPair::from_seed_and_public_key(seed, pubkey.as_ref())
.map_err(|e| DlcError::CryptoError(e.to_string()))?;
let sig = pair.sign(data);
let mut sig_bytes = [0u8; 64];
sig_bytes.copy_from_slice(sig.as_ref());
Ok(sig_bytes)
}),
DlcKey::Public { .. } => Err(DlcError::PrivateKeyRequired),
}
}
pub fn verify(&self, data: &[u8], signature: &[u8; 64]) -> Result<(), DlcError> {
let pubkey = self.get_public_key();
ring::signature::UnparsedPublicKey::new(&ring::signature::ED25519, pubkey.as_ref())
.verify(data, signature.as_ref())
.map_err(|_| DlcError::SignatureInvalid)
}
pub fn create_signed_license<D>(
&self,
dlcs: impl IntoIterator<Item = D>,
product: Product,
) -> Result<SignedLicense, DlcError>
where
D: std::fmt::Display,
{
let mut payload = serde_json::Map::new();
payload.insert(
"dlcs".to_string(),
serde_json::Value::Array(
dlcs.into_iter()
.map(|s| serde_json::Value::String(s.to_string()))
.collect(),
),
);
payload.insert(
"product".to_string(),
serde_json::Value::String(product.as_ref().to_string()),
);
match self {
DlcKey::Private { privkey, .. } => {
let sig_token = privkey.with_secret(
|encrypt_key_bytes| -> Result<SignedLicense, DlcError> {
payload.insert(
"encrypt_key".to_string(),
serde_json::Value::String(URL_SAFE_NO_PAD.encode(encrypt_key_bytes)),
);
let payload_value = serde_json::Value::Object(payload);
let payload_bytes = serde_json::to_vec(&payload_value)
.map_err(|e| DlcError::TokenCreationFailed(e.to_string()))?;
let sig = self.sign(&payload_bytes)?;
Ok(SignedLicense::from(format!(
"{}.{}",
URL_SAFE_NO_PAD.encode(&payload_bytes),
URL_SAFE_NO_PAD.encode(sig.as_ref())
)))
},
)?;
Ok(sig_token)
}
DlcKey::Public { .. } => Err(DlcError::PrivateKeyRequired),
}
}
pub fn extend_signed_license<D>(
&self,
existing: &SignedLicense,
new_dlcs: impl IntoIterator<Item = D>,
product: Product,
) -> Result<SignedLicense, DlcError>
where
D: std::fmt::Display,
{
let mut combined_dlcs: Vec<String> = existing.with_secret(|token_str| {
let parts: Vec<&str> = token_str.split('.').collect();
if parts.len() != 2 {
return Vec::new();
}
if let Ok(payload) = URL_SAFE_NO_PAD.decode(parts[0].as_bytes()) {
if let Ok(payload_json) = serde_json::from_slice::<serde_json::Value>(&payload) {
if let Some(dlcs_array) = payload_json.get("dlcs").and_then(|v| v.as_array()) {
let mut dlcs = Vec::new();
for dlc in dlcs_array {
if let Some(dlc_id) = dlc.as_str() {
dlcs.push(dlc_id.to_string());
}
}
return dlcs;
}
}
}
Vec::new()
});
for new_dlc in new_dlcs {
let dlc_str = new_dlc.to_string();
if !combined_dlcs.contains(&dlc_str) {
combined_dlcs.push(dlc_str);
}
}
self.create_signed_license(combined_dlcs, product)
}
pub fn verify_signed_license(&self, license: &SignedLicense) -> bool {
license.with_secret(|full_token| {
let parts: Vec<&str> = full_token.split('.').collect();
if parts.len() != 2 {
return false;
}
let payload = URL_SAFE_NO_PAD.decode(parts[0]);
let sig_bytes = URL_SAFE_NO_PAD.decode(parts[1]);
if payload.is_err() || sig_bytes.is_err() {
return false;
}
let payload = payload.unwrap();
let sig_bytes = sig_bytes.unwrap();
if sig_bytes.len() != 64 {
return false;
}
let public = self.get_public_key().0;
let public_key = UnparsedPublicKey::new(&ED25519, public);
public_key.verify(&payload, &sig_bytes).is_ok()
})
}
}
impl Clone for DlcKey {
fn clone(&self) -> Self {
match self {
DlcKey::Private { privkey, pubkey } => privkey.with_secret(|s| DlcKey::Private {
privkey: PrivateKey::new(*s),
pubkey: *pubkey,
}),
DlcKey::Public { pubkey } => DlcKey::Public { pubkey: *pubkey },
}
}
}
impl std::fmt::Display for DlcKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", URL_SAFE_NO_PAD.encode(self.get_public_key().0))
}
}
impl From<&DlcKey> for String {
fn from(k: &DlcKey) -> Self {
k.to_string()
}
}
#[derive(Clone, Debug)]
pub struct PackItem {
path: String,
original_extension: Option<String>,
type_path: Option<String>,
plaintext: Vec<u8>,
}
#[allow(dead_code)]
impl PackItem {
pub fn new(path: impl Into<String>, plaintext: impl Into<Vec<u8>>) -> Result<Self, DlcError> {
let path = path.into();
let bytes = plaintext.into();
if bytes.len() >= 4 && bytes.starts_with(DLC_PACK_MAGIC) {
return Err(DlcError::Other(format!(
"cannot pack existing dlcpack container as an item: {}",
path
)));
}
if pack_format::is_data_executable(&bytes) {
return Err(DlcError::Other(format!(
"input data looks like an executable payload, which is not allowed: {}",
path
)));
}
let ext_str = std::path::Path::new(&path)
.extension()
.and_then(|e| e.to_str());
if let Some(ext) = ext_str {
if pack_format::is_forbidden_extension(ext) {
return Err(DlcError::Other(format!(
"input path contains forbidden extension (.{}): {}",
ext, path
)));
}
}
Ok(Self {
path: path.clone(),
original_extension: ext_str.map(|s| s.to_string()),
type_path: None,
plaintext: bytes,
})
}
pub fn with_extension(mut self, ext: impl Into<String>) -> Result<Self, DlcError> {
let ext_s = ext.into();
if pack_format::is_forbidden_extension(&ext_s) {
return Err(DlcError::Other(format!(
"forbidden extension (.{}): {}",
ext_s, self.path
)));
}
self.original_extension = Some(ext_s);
Ok(self)
}
pub fn with_type_path(mut self, type_path: impl Into<String>) -> Self {
self.type_path = Some(type_path.into());
self
}
pub fn with_type<T: Asset>(self) -> Self {
self.with_type_path(T::type_path())
}
pub fn path(&self) -> &str {
&self.path
}
pub fn plaintext(&self) -> &[u8] {
&self.plaintext
}
pub fn ext(&self) -> Option<String> {
if let Some(ext) = std::path::Path::new(&self.path)
.extension()
.and_then(|e| e.to_str())
.and_then(|s| {
if s.is_empty() {
None
} else {
Some(s.to_string())
}
})
{
Some(ext)
} else {
self.original_extension.clone()
}
}
pub fn type_path(&self) -> Option<String> {
self.type_path.clone()
}
}
impl From<PackItem> for (String, Option<String>, Option<String>, Vec<u8>) {
fn from(item: PackItem) -> Self {
(
item.path,
item.original_extension,
item.type_path,
item.plaintext,
)
}
}
#[derive(Error, Debug, Clone)]
pub enum DlcError {
#[error("invalid public key: {0}")]
InvalidPublicKey(String),
#[error("malformed private key: {0}")]
MalformedLicense(String),
#[error("signature verification failed")]
SignatureInvalid,
#[error("payload parse failed: {0}")]
PayloadInvalid(String),
#[error("private key creation failed: {0}")]
TokenCreationFailed(String),
#[error("private key required for this operation")]
PrivateKeyRequired,
#[error("invalid public key")]
InvalidPassphrase,
#[error("crypto error: {0}")]
CryptoError(String),
#[error("encryption failed: {0}")]
EncryptionFailed(String),
#[error("{0}")]
DecryptionFailed(String),
#[error("invalid encrypt key: {0}")]
InvalidEncryptKey(String),
#[error("invalid nonce: {0}")]
InvalidNonce(String),
#[error("dlc locked: {0}")]
DlcLocked(String),
#[error("no encrypt key for dlc: {0}")]
NoEncryptKey(String),
#[error("private key product does not match")]
TokenProductMismatch,
#[error("deprecated version: v{0}")]
DeprecatedVersion(String),
#[error("{0}")]
Other(String),
}
impl From<std::io::Error> for DlcError {
fn from(e: std::io::Error) -> Self {
DlcError::Other(e.to_string())
}
}
impl From<ring::error::Unspecified> for DlcError {
fn from(e: ring::error::Unspecified) -> Self {
DlcError::CryptoError(format!("crypto error: {:?}", e))
}
}
#[cfg(test)]
mod tests {
use crate::ext::*;
use super::*;
#[test]
fn pack_encrypted_pack_rejects_nested_dlc() {
let mut v = Vec::new();
v.extend_from_slice(DLC_PACK_MAGIC);
v.extend_from_slice(b"inner");
let err = PackItem::new("a.txt", v).unwrap_err();
assert!(err.to_string().contains("cannot pack existing dlcpack"));
}
#[test]
fn pack_encrypted_pack_rejects_nested_dlcpack() {
let mut v = Vec::new();
v.extend_from_slice(DLC_PACK_MAGIC);
v.extend_from_slice(b"innerpack");
let err = PackItem::new("b.dlcpack", v);
assert!(err.is_err());
}
#[test]
fn is_data_executable_detects_pe_header() {
assert!(is_data_executable(&[0x4D, 0x5A, 0, 0]));
}
#[test]
fn is_data_executable_detects_shebang() {
assert!(is_data_executable(b"#! /bin/sh"));
}
#[test]
fn is_data_executable_ignores_plain_text() {
assert!(!is_data_executable(b"hello"));
}
#[test]
fn packitem_rejects_binary_data() {
let mut v = Vec::new();
v.extend_from_slice(&[0x4D, 0x5A, 0, 0]);
let pack_item = PackItem::new("evil.dat", v);
assert!(pack_item.is_err());
}
#[test]
fn dlc_id_serde_roundtrip() {
let id = DlcId::from("expansion_serde");
let s = serde_json::to_string(&id).expect("serialize dlc id");
assert_eq!(s, "\"expansion_serde\"");
let decoded: DlcId = serde_json::from_str(&s).expect("deserialize dlc id");
assert_eq!(decoded.to_string(), "expansion_serde");
}
#[test]
fn extend_signed_license_merges_dlc_ids() {
let product = Product::from("test_product");
let dlc_key = DlcKey::generate_random();
let initial = dlc_key
.create_signed_license(&["expansion_a", "expansion_b"], product.clone())
.expect("create initial license");
let extended = dlc_key
.extend_signed_license(&initial, &["expansion_c"], product.clone())
.expect("extend license");
assert!(dlc_key.verify_signed_license(&extended));
let verified_dlcs = extract_dlc_ids_from_license(&extended);
assert_eq!(verified_dlcs.len(), 3);
assert!(verified_dlcs.contains(&"expansion_a".to_string()));
assert!(verified_dlcs.contains(&"expansion_b".to_string()));
assert!(verified_dlcs.contains(&"expansion_c".to_string()));
}
#[test]
fn extend_signed_license_deduplicates() {
let product = Product::from("test_product");
let dlc_key = DlcKey::generate_random();
let initial = dlc_key
.create_signed_license(&["expansion_a"], product.clone())
.expect("create initial license");
let extended = dlc_key
.extend_signed_license(&initial, &["expansion_a"], product.clone())
.expect("extend license");
assert!(dlc_key.verify_signed_license(&extended));
let verified_dlcs = extract_dlc_ids_from_license(&extended);
let count = verified_dlcs.iter().filter(|d| d == &"expansion_a").count();
assert_eq!(count, 1, "Should not duplicate dlc_ids");
}
#[test]
#[serial_test::serial]
fn register_dlc_type_adds_pack_registrar_factory() {
let mut app = App::new();
app.add_plugins(AssetPlugin::default());
#[derive(Asset, TypePath)]
struct TestAsset;
app.init_asset::<TestAsset>();
app.register_dlc_type::<TestAsset>();
let factories = app
.world()
.get_resource::<asset_loader::DlcPackRegistrarFactories>()
.expect("should have factories resource");
assert!(
factories
.0
.read()
.unwrap()
.iter()
.any(|f| asset_loader::fuzzy_type_path_match(
f.type_name(),
TestAsset::type_path()
))
);
}
#[test]
#[serial_test::serial]
fn register_dlc_type_is_idempotent_for_pack_factories() {
let mut app = App::new();
app.add_plugins(AssetPlugin::default());
#[derive(Asset, TypePath)]
struct TestAsset2;
app.init_asset::<TestAsset2>();
app.register_dlc_type::<TestAsset2>();
app.register_dlc_type::<TestAsset2>();
let factories = app
.world()
.get_resource::<asset_loader::DlcPackRegistrarFactories>()
.expect("should have factories resource");
let count = factories
.0
.read()
.unwrap()
.iter()
.filter(|f| asset_loader::fuzzy_type_path_match(f.type_name(), TestAsset2::type_path()))
.count();
assert_eq!(count, 1);
}
}
#[cfg(test)]
#[allow(dead_code)]
pub mod test_helpers {
use crate::{EncryptionKey, encrypt_key_registry};
pub fn register_test_encryption_key(dlc_id: &str, key: EncryptionKey) {
encrypt_key_registry::insert(dlc_id, key);
}
pub fn register_test_asset_path(dlc_id: &str, path: &str) {
encrypt_key_registry::register_asset_path(dlc_id, path);
}
pub fn clear_test_registry() {
encrypt_key_registry::clear_all();
}
pub fn is_malicious_data(data: &[u8]) -> bool {
crate::pack_format::is_data_executable(data)
}
}