pub const DLC_PACK_MAGIC: &[u8; 4] = b"BDLP";
pub const DLC_PACK_VERSION_LATEST: u8 = 5;
pub const DEFAULT_BLOCK_SIZE: usize = 10 * 1024 * 1024;
pub type PackMetadata = std::collections::BTreeMap<String, serde_json::Value>;
#[derive(Clone, Debug)]
pub struct ParsedDlcPack {
pub product: Product,
pub dlc_id: DlcId,
pub version: Version,
pub metadata: PackMetadata,
pub metadata_locked: bool,
pub entries: Vec<(String, crate::asset_loader::EncryptedAsset)>,
pub block_metadatas: Vec<BlockMetadata>,
}
pub const FORBIDDEN_EXTENSIONS: &[&str] = &[
"dlcpack",
"pubkey",
"slicense", "7z",
"accda",
"accdb",
"accde",
"accdr",
"ace",
"ade",
"adp",
"app",
"appinstaller",
"application",
"appref",
"appx",
"appxbundle",
"arj",
"asax",
"asd",
"ashx",
"asp",
"aspx",
"b64",
"bas",
"bat",
"bgi",
"bin",
"btm",
"bz",
"bz2",
"bzip",
"bzip2",
"cab",
"cer",
"cfg",
"chi",
"chm",
"cla",
"class",
"cmd",
"com",
"cpi",
"cpio",
"cpl",
"crt",
"crx",
"csh",
"der",
"desktopthemefile",
"diagcab",
"diagcfg",
"diagpkg",
"dll",
"dmg",
"doc",
"docm",
"docx",
"dotm",
"drv",
"eml",
"exe",
"fon",
"fxp",
"gadget",
"grp",
"gz",
"gzip",
"hlp",
"hta",
"htc",
"htm",
"html",
"htt",
"ics",
"img",
"ini",
"ins",
"inx",
"iqy",
"iso",
"isp",
"isu",
"jar",
"jnlp",
"job",
"js",
"jse",
"ksh",
"lha",
"lnk",
"local",
"lz",
"lzh",
"lzma",
"mad",
"maf",
"mag",
"mam",
"manifest",
"maq",
"mar",
"mas",
"mat",
"mav",
"maw",
"mda",
"mdb",
"mde",
"mdt",
"mdw",
"mdz",
"mht",
"mhtml",
"mmc",
"msc",
"msg",
"msh",
"msh1",
"msh1xml",
"msh2",
"msh2xml",
"mshxml",
"msi",
"msix",
"msixbundle",
"msm",
"msp",
"mst",
"msu",
"ocx",
"odt",
"one",
"onepkg",
"onetoc",
"onetoc2",
"ops",
"oxps",
"oxt",
"paf",
"partial",
"pcd",
"pdf",
"pif",
"pl",
"plg",
"pol",
"potm",
"ppam",
"ppkg",
"ppsm",
"ppt",
"pptm",
"pptx",
"prf",
"prg",
"ps1",
"ps1xml",
"ps2",
"ps2xml",
"psc1",
"psc2",
"psm1",
"pst",
"r00",
"r01",
"r02",
"r03",
"rar",
"reg",
"rels",
"rev",
"rgs",
"rpm",
"rtf",
"scf",
"scr",
"sct",
"search",
"settingcontent",
"settingscontent",
"sh",
"shb",
"sldm",
"slk",
"svg",
"swf",
"sys",
"tar",
"tbz",
"tbz2",
"tgz",
"tlb",
"url",
"uue",
"vb",
"vbe",
"vbs",
"vbscript",
"vdx",
"vhd",
"vhdx",
"vsmacros",
"vss",
"vssm",
"vssx",
"vst",
"vstm",
"vstx",
"vsw",
"vsx",
"vtx",
"wbk",
"webarchive",
"website",
"wml",
"ws",
"wsc",
"wsf",
"wsh",
"xar",
"xbap",
"xdp",
"xlam",
"xll",
"xlm",
"xls",
"xlsb",
"xlsm",
"xlsx",
"xltm",
"xlw",
"xml",
"xnk",
"xps",
"xrm",
"xsd",
"xsl",
"xxe",
"xz",
"z",
"zip",
];
pub fn is_data_executable(data: &[u8]) -> bool {
if infer::is_app(data) {
return true;
}
for ext in FORBIDDEN_EXTENSIONS {
if infer::is(data, *ext) {
return true;
}
}
if data.starts_with(b"#!") {
return true;
}
false
}
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
pub struct ManifestEntry {
pub path: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub original_extension: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub type_path: Option<String>,
}
impl ManifestEntry {
pub fn from_pack_item(item: &crate::PackItem) -> Self {
ManifestEntry {
path: item.path.clone(),
original_extension: item.original_extension.clone().filter(|s| !s.is_empty()),
type_path: item.type_path.clone(),
}
}
}
#[derive(Clone, Debug)]
pub struct V5ManifestEntry {
pub path: String,
pub original_extension: String,
pub type_path: Option<String>,
pub block_id: u32,
pub block_offset: u32,
pub size: u32,
}
pub type V4ManifestEntry = V5ManifestEntry;
impl V5ManifestEntry {
pub fn from_pack_item(item: &crate::PackItem, block_id: u32, block_offset: u32) -> Self {
V5ManifestEntry {
path: item.path.clone(),
original_extension: item.original_extension.clone().unwrap_or_default(),
type_path: item.type_path.clone(),
block_id,
block_offset,
size: item.plaintext.len() as u32,
}
}
#[allow(dead_code)]
fn write_binary<W: std::io::Write>(&self, writer: &mut PackWriter<W>) -> std::io::Result<()> {
writer.write_u32(self.path.len() as u32)?;
writer.write_bytes(self.path.as_bytes())?;
writer.write_u8(self.original_extension.len() as u8)?;
writer.write_bytes(self.original_extension.as_bytes())?;
if let Some(ref tp) = self.type_path {
writer.write_u16(tp.len() as u16)?;
writer.write_bytes(tp.as_bytes())?;
} else {
writer.write_u16(0)?;
}
writer.write_u32(self.block_id)?;
writer.write_u32(self.block_offset)?;
writer.write_u32(self.size)
}
#[allow(dead_code)]
fn read_binary<R: std::io::Read>(reader: &mut PackReader<R>) -> std::io::Result<Self> {
let path_len = reader.read_u32()? as usize;
let path = String::from_utf8(reader.read_bytes(path_len)?)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
let ext_len = reader.read_u8()? as usize;
let original_extension = if ext_len > 0 {
String::from_utf8(reader.read_bytes(ext_len)?)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?
} else {
String::new()
};
let type_len = reader.read_u16()? as usize;
let type_path = if type_len > 0 {
let tp = String::from_utf8(reader.read_bytes(type_len)?)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
Some(tp)
} else {
None
};
let block_id = reader.read_u32()?;
let block_offset = reader.read_u32()?;
let size = reader.read_u32()?;
Ok(V5ManifestEntry {
path,
original_extension,
type_path,
block_id,
block_offset,
size,
})
}
}
#[derive(Clone, Debug)]
pub struct BlockMetadata {
pub block_id: u32,
pub file_offset: u64, pub encrypted_size: u32, pub uncompressed_size: u32, pub nonce: [u8; 12], pub crc32: u32, }
impl BlockMetadata {
#[allow(dead_code)]
fn write_binary<W: std::io::Write>(&self, writer: &mut PackWriter<W>) -> std::io::Result<()> {
writer.write_u32(self.block_id)?;
writer.write_u64(self.file_offset)?;
writer.write_u32(self.encrypted_size)?;
writer.write_u32(self.uncompressed_size)?;
writer.write_bytes(&self.nonce)?;
writer.write_u32(self.crc32)
}
#[allow(dead_code)]
fn read_binary<R: std::io::Read>(reader: &mut PackReader<R>) -> std::io::Result<Self> {
let block_id = reader.read_u32()?;
let file_offset = reader.read_u64()?;
let encrypted_size = reader.read_u32()?;
let uncompressed_size = reader.read_u32()?;
let nonce = reader.read_nonce()?;
let crc32 = reader.read_u32()?;
Ok(BlockMetadata {
block_id,
file_offset,
encrypted_size,
uncompressed_size,
nonce,
crc32,
})
}
}
pub(crate) struct PackReader<R: std::io::Read> {
inner: R,
}
impl<R: std::io::Read> PackReader<R> {
pub fn new(inner: R) -> Self {
Self { inner }
}
pub fn read_u8(&mut self) -> std::io::Result<u8> {
let mut buf = [0u8; 1];
self.inner.read_exact(&mut buf)?;
Ok(buf[0])
}
#[allow(dead_code)]
pub fn read_and_decrypt(
&mut self,
key: &crate::EncryptionKey,
len: usize,
nonce: &[u8],
) -> Result<Vec<u8>, DlcError> {
let ciphertext = self.read_bytes(len)?;
let cursor = std::io::Cursor::new(ciphertext);
crate::pack_format::decrypt_with_key(key, cursor, nonce)
}
pub fn read_u16(&mut self) -> std::io::Result<u16> {
let mut buf = [0u8; 2];
self.inner.read_exact(&mut buf)?;
Ok(u16::from_be_bytes(buf))
}
pub fn read_u32(&mut self) -> std::io::Result<u32> {
let mut buf = [0u8; 4];
self.inner.read_exact(&mut buf)?;
Ok(u32::from_be_bytes(buf))
}
#[allow(dead_code)]
pub fn read_u64(&mut self) -> std::io::Result<u64> {
let mut buf = [0u8; 8];
self.inner.read_exact(&mut buf)?;
Ok(u64::from_be_bytes(buf))
}
pub fn read_bytes(&mut self, len: usize) -> std::io::Result<Vec<u8>> {
let mut buf = vec![0u8; len];
self.inner.read_exact(&mut buf)?;
Ok(buf)
}
pub fn read_string_u16(&mut self) -> std::io::Result<String> {
let len = self.read_u16()? as usize;
self.read_string(len)
}
pub fn read_string(&mut self, len: usize) -> std::io::Result<String> {
let bytes = self.read_bytes(len)?;
String::from_utf8(bytes)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))
}
pub fn read_nonce(&mut self) -> std::io::Result<[u8; 12]> {
let mut nonce = [0u8; 12];
self.inner.read_exact(&mut nonce)?;
Ok(nonce)
}
}
#[allow(unused)]
impl<R: std::io::Read + std::io::Seek> PackReader<R> {
pub fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result<u64> {
self.inner.seek(pos)
}
}
pub(crate) struct PackWriter<W: std::io::Write> {
inner: W,
}
impl<W: std::io::Write> PackWriter<W> {
pub fn new(inner: W) -> Self {
Self { inner }
}
#[allow(dead_code)]
pub fn write_encrypted(
&mut self,
key: &crate::EncryptionKey,
nonce: &[u8],
plaintext: &[u8],
) -> Result<(), DlcError> {
use aes_gcm::{Aes256Gcm, KeyInit, Nonce, aead::Aead};
let cipher = key.with_secret(|kb| {
Aes256Gcm::new_from_slice(kb).map_err(|e| DlcError::CryptoError(e.to_string()))
})?;
let ct = cipher
.encrypt(Nonce::from_slice(nonce), plaintext)
.map_err(|_| DlcError::EncryptionFailed("block encryption failed".into()))?;
self.write_bytes(&ct)
.map_err(|e| DlcError::Other(e.to_string()))
}
pub fn write_u8(&mut self, val: u8) -> std::io::Result<()> {
self.inner.write_all(&[val])
}
pub fn write_u16(&mut self, val: u16) -> std::io::Result<()> {
self.inner.write_all(&val.to_be_bytes())
}
pub fn write_u32(&mut self, val: u32) -> std::io::Result<()> {
self.inner.write_all(&val.to_be_bytes())
}
pub fn write_u64(&mut self, val: u64) -> std::io::Result<()> {
self.inner.write_all(&val.to_be_bytes())
}
pub fn write_bytes(&mut self, bytes: &[u8]) -> std::io::Result<()> {
self.inner.write_all(bytes)
}
pub fn write_string_u16(&mut self, s: &str) -> std::io::Result<()> {
let bytes = s.as_bytes();
self.write_u16(bytes.len() as u16)?;
self.write_bytes(bytes)
}
pub fn finish(mut self) -> std::io::Result<W> {
self.inner.flush()?;
Ok(self.inner)
}
}
struct PackHeader {
version: u8,
product: String,
dlc_id: String,
}
impl PackHeader {
fn read<R: std::io::Read>(reader: &mut PackReader<R>) -> std::io::Result<Self> {
let magic = reader.read_bytes(4)?;
if magic != DLC_PACK_MAGIC {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"invalid dlcpack magic",
));
}
let version = reader.read_u8()?;
if version != DLC_PACK_VERSION_LATEST {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("unsupported pack version: {}", version),
));
}
let product = reader.read_string_u16()?;
let dlc_id = reader.read_string_u16()?;
Ok(PackHeader {
version,
product,
dlc_id,
})
}
fn write<W: std::io::Write>(&self, writer: &mut PackWriter<W>) -> std::io::Result<()> {
writer.write_bytes(DLC_PACK_MAGIC)?;
writer.write_u8(self.version)?;
writer.write_string_u16(&self.product)?;
writer.write_string_u16(&self.dlc_id)
}
}
use aes_gcm::{Aes256Gcm, KeyInit, Nonce, aead::AeadInPlace};
use secure_gate::RevealSecret;
use crate::{DlcError, DlcId, EncryptionKey, PackItem, Product};
fn encrypt_pack_metadata_bytes(
metadata: &PackMetadata,
key: &EncryptionKey,
) -> Result<([u8; 12], Vec<u8>), DlcError> {
if metadata.is_empty() {
return Ok(([0u8; 12], Vec::new()));
}
use aes_gcm::aead::Aead;
let plaintext = serde_json::to_vec(metadata)
.map_err(|e| DlcError::Other(format!("failed to serialize pack metadata: {}", e)))?;
let nonce: [u8; 12] = rand::random();
let ciphertext = key.with_secret(|key_bytes| {
let cipher = Aes256Gcm::new_from_slice(key_bytes)
.map_err(|e| DlcError::CryptoError(e.to_string()))?;
cipher
.encrypt(Nonce::from_slice(&nonce), plaintext.as_slice())
.map_err(|_| DlcError::EncryptionFailed("metadata encryption failed".into()))
})?;
Ok((nonce, ciphertext))
}
fn decrypt_pack_metadata_bytes(
ciphertext: Vec<u8>,
nonce: &[u8; 12],
key: &EncryptionKey,
) -> Result<PackMetadata, std::io::Error> {
let plaintext = decrypt_with_key(key, std::io::Cursor::new(ciphertext), nonce)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string()))?;
serde_json::from_slice(&plaintext)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))
}
pub(crate) fn decrypt_with_key<R: std::io::Read>(
key: &crate::EncryptionKey,
mut reader: R,
nonce: &[u8],
) -> Result<Vec<u8>, DlcError> {
let mut buf = Vec::new();
reader
.read_to_end(&mut buf)
.map_err(|e| DlcError::Other(e.to_string()))?;
key.with_secret(|key_bytes| {
if key_bytes.len() != 32 {
return Err(DlcError::InvalidEncryptKey(
"encrypt key must be 32 bytes (AES-256)".into(),
));
}
if nonce.len() != 12 {
return Err(DlcError::InvalidNonce(
"nonce must be 12 bytes (AES-GCM)".into(),
));
}
let cipher = Aes256Gcm::new_from_slice(key_bytes)
.map_err(|e| DlcError::CryptoError(e.to_string()))?;
let nonce = Nonce::from_slice(nonce);
cipher.decrypt_in_place(nonce, &[], &mut buf).map_err(|_| {
DlcError::DecryptionFailed(
"authentication failed (incorrect key or corrupted ciphertext)".to_string(),
)
})
})?;
Ok(buf)
}
#[derive(Debug, Clone, Copy)]
pub enum CompressionLevel {
Fast,
Default,
Best,
}
impl From<CompressionLevel> for flate2::Compression {
fn from(level: CompressionLevel) -> Self {
match level {
CompressionLevel::Fast => flate2::Compression::fast(),
CompressionLevel::Default => flate2::Compression::default(),
CompressionLevel::Best => flate2::Compression::best(),
}
}
}
pub fn pack_encrypted_pack(
dlc_id: &DlcId,
items: &[PackItem],
product: &Product,
key: &EncryptionKey,
block_size: usize,
) -> Result<Vec<u8>, DlcError> {
let metadata = PackMetadata::new();
pack_encrypted_pack_with_metadata(dlc_id, items, product, &metadata, key, block_size)
}
pub fn pack_encrypted_pack_with_metadata(
dlc_id: &DlcId,
items: &[PackItem],
product: &Product,
metadata: &PackMetadata,
key: &EncryptionKey,
block_size: usize,
) -> Result<Vec<u8>, DlcError> {
use aes_gcm::{Aes256Gcm, KeyInit, Nonce, aead::Aead};
use flate2::Compression;
use flate2::write::GzEncoder;
use tar::Builder;
let cipher = key.with_secret(|kb| {
Aes256Gcm::new_from_slice(kb.as_slice()).map_err(|e| DlcError::CryptoError(e.to_string()))
})?;
let mut blocks: Vec<Vec<&PackItem>> = Vec::new();
let mut current_block = Vec::new();
let mut current_size = 0;
for item in items {
if !current_block.is_empty() && current_size + item.plaintext.len() > block_size {
blocks.push(std::mem::take(&mut current_block));
current_size = 0;
}
current_size += item.plaintext.len();
current_block.push(item);
}
if !current_block.is_empty() {
blocks.push(current_block);
}
let mut encrypted_blocks = Vec::new();
let mut manifest_entries = Vec::new();
let mut block_metadatas = Vec::new();
for (block_id, block_items) in blocks.into_iter().enumerate() {
let block_id = block_id as u32;
let mut tar_gz = Vec::new();
let mut uncompressed_size = 0;
{
let mut gz = GzEncoder::new(&mut tar_gz, Compression::default());
{
let mut tar = Builder::new(&mut gz);
let mut offset = 0;
for item in block_items {
let mut header = tar::Header::new_gnu();
header.set_size(item.plaintext.len() as u64);
header.set_mode(0o644);
header.set_cksum();
let path_str = item.path.clone();
manifest_entries.push(V5ManifestEntry {
path: path_str,
original_extension: item.original_extension.clone().unwrap_or_default(),
type_path: item.type_path.clone(),
block_id,
block_offset: offset,
size: item.plaintext.len() as u32,
});
tar.append_data(&mut header, &item.path, &item.plaintext[..])
.map_err(|e| DlcError::Other(e.to_string()))?;
let data_len = item.plaintext.len() as u32;
let padded_len = (data_len + 511) & !511;
offset += 512 + padded_len;
uncompressed_size += data_len;
}
tar.finish().map_err(|e| DlcError::Other(e.to_string()))?;
}
gz.finish().map_err(|e| DlcError::Other(e.to_string()))?;
}
let nonce_bytes: [u8; 12] = rand::random();
let nonce = Nonce::from_slice(&nonce_bytes);
let ciphertext = cipher
.encrypt(nonce, tar_gz.as_slice())
.map_err(|_| DlcError::EncryptionFailed("block encryption failed".into()))?;
let crc32 = crc32fast::hash(&ciphertext);
block_metadatas.push(BlockMetadata {
block_id,
file_offset: 0, encrypted_size: ciphertext.len() as u32,
uncompressed_size,
nonce: nonce_bytes,
crc32,
});
encrypted_blocks.push(ciphertext);
}
let product_str = product.as_ref();
let dlc_id_str = dlc_id.to_string();
let (metadata_nonce, metadata_ciphertext) = encrypt_pack_metadata_bytes(metadata, key)?;
let mut out = Vec::new();
{
let mut writer = PackWriter::new(&mut out);
let header = PackHeader {
version: DLC_PACK_VERSION_LATEST,
product: product_str.to_string(),
dlc_id: dlc_id_str.clone(),
};
header
.write(&mut writer)
.map_err(|e| DlcError::Other(e.to_string()))?;
writer
.write_bytes(&metadata_nonce)
.map_err(|e| DlcError::Other(e.to_string()))?;
writer
.write_u32(metadata_ciphertext.len() as u32)
.map_err(|e| DlcError::Other(e.to_string()))?;
writer
.write_bytes(&metadata_ciphertext)
.map_err(|e| DlcError::Other(e.to_string()))?;
writer
.write_u32(manifest_entries.len() as u32)
.map_err(|e| DlcError::Other(e.to_string()))?;
for entry in &manifest_entries {
entry
.write_binary(&mut writer)
.map_err(|e| DlcError::Other(e.to_string()))?;
}
writer
.write_u32(block_metadatas.len() as u32)
.map_err(|e| DlcError::Other(e.to_string()))?;
writer
.finish()
.map_err(|e| DlcError::Other(e.to_string()))?;
}
let metadata_start_pos = out.len();
{
let mut writer = PackWriter::new(&mut out);
for meta in &block_metadatas {
meta.write_binary(&mut writer)
.map_err(|e| DlcError::Other(e.to_string()))?;
}
writer
.finish()
.map_err(|e| DlcError::Other(e.to_string()))?;
}
for (i, block) in encrypted_blocks.into_iter().enumerate() {
let pos = out.len() as u64;
block_metadatas[i].file_offset = pos;
out.extend_from_slice(&block);
}
{
let mut writer_fixed = PackWriter::new(&mut out[metadata_start_pos..]);
for meta in &block_metadatas {
meta.write_binary(&mut writer_fixed)
.map_err(|e| DlcError::Other(e.to_string()))?;
}
writer_fixed
.finish()
.map_err(|e| DlcError::Other(e.to_string()))?;
}
Ok(out)
}
pub type Version = usize;
pub fn parse_encrypted_pack_info<R: std::io::Read>(
reader: R,
key: Option<&EncryptionKey>,
) -> Result<ParsedDlcPack, std::io::Error> {
let mut reader = PackReader::new(reader);
let header = PackHeader::read(&mut reader)?;
let metadata_nonce = reader.read_nonce()?;
let metadata_len = reader.read_u32()? as usize;
let metadata_ciphertext = reader.read_bytes(metadata_len)?;
let (metadata, metadata_locked) = if metadata_len == 0 {
(PackMetadata::new(), false)
} else if let Some(key) = key {
(
decrypt_pack_metadata_bytes(metadata_ciphertext, &metadata_nonce, key)?,
false,
)
} else {
(PackMetadata::new(), true)
};
let manifest_count = reader.read_u32()? as usize;
let mut manifest: Vec<V5ManifestEntry> = Vec::with_capacity(manifest_count);
for _ in 0..manifest_count {
manifest.push(V5ManifestEntry::read_binary(&mut reader)?);
}
let block_count = reader.read_u32()? as usize;
let mut block_metadatas = Vec::with_capacity(block_count);
for _ in 0..block_count {
block_metadatas.push(BlockMetadata::read_binary(&mut reader)?);
}
let mut entries = Vec::with_capacity(manifest.len());
for entry in manifest {
entries.push((
entry.path,
crate::asset_loader::EncryptedAsset {
dlc_id: header.dlc_id.clone(),
original_extension: entry.original_extension,
type_path: entry.type_path,
nonce: [0u8; 12],
ciphertext: std::sync::Arc::new([]),
block_id: entry.block_id,
block_offset: entry.block_offset,
size: entry.size,
},
));
}
Ok(ParsedDlcPack {
product: Product::from(header.product),
dlc_id: DlcId::from(header.dlc_id),
version: header.version as usize,
metadata,
metadata_locked,
entries,
block_metadatas,
})
}
pub fn parse_encrypted_pack<R: std::io::Read>(
reader: R,
) -> Result<
(
Product,
DlcId,
Version,
Vec<(String, crate::asset_loader::EncryptedAsset)>,
Vec<BlockMetadata>,
),
std::io::Error,
> {
let parsed = parse_encrypted_pack_info(reader, None)?;
Ok((
parsed.product,
parsed.dlc_id,
parsed.version,
parsed.entries,
parsed.block_metadatas,
))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn manifest_roundtrip() {
let item = crate::PackItem::new("foo.txt", b"hello" as &[u8]).expect("pack item");
let entry = ManifestEntry::from_pack_item(&item);
let bytes = serde_json::to_vec(&entry).expect("serialize manifest entry");
let back: ManifestEntry =
serde_json::from_slice(&bytes).expect("deserialize manifest entry");
assert_eq!(entry.path, back.path);
}
#[test]
fn v5_pack_roundtrips_pack_metadata() {
let item = crate::PackItem::new("foo.txt", b"hello" as &[u8]).expect("pack item");
let product = Product::from("game");
let dlc_id = DlcId::from("dlcA");
let key = EncryptionKey::new(rand::random());
let metadata = PackMetadata::from([
(
"chapter".to_string(),
serde_json::Value::String("intro".to_string()),
),
(
"stats".to_string(),
serde_json::json!({"difficulty": 3, "boss": true}),
),
]);
let bytes = pack_encrypted_pack_with_metadata(
&dlc_id,
&[item],
&product,
&metadata,
&key,
DEFAULT_BLOCK_SIZE,
)
.expect("pack with metadata");
assert!(
!bytes
.windows(b"intro".len())
.any(|window| window == b"intro")
);
let parsed_without_key = parse_encrypted_pack_info(&bytes[..], None)
.expect("parse v5 pack without key");
assert!(parsed_without_key.metadata_locked);
assert!(parsed_without_key.metadata.is_empty());
let parsed = parse_encrypted_pack_info(&bytes[..], Some(&key))
.expect("parse v5 pack with key");
assert_eq!(parsed.version, DLC_PACK_VERSION_LATEST as usize);
assert!(!parsed.metadata_locked);
assert_eq!(parsed.metadata, metadata);
assert_eq!(parsed.entries.len(), 1);
}
#[test]
fn parse_rejects_older_versions() {
let bytes = [DLC_PACK_MAGIC.as_slice(), &[4u8]].concat();
let err = parse_encrypted_pack_info(&bytes[..], None)
.expect_err("older versions rejected");
assert!(err.to_string().contains("unsupported pack version: 4"));
}
}