1use std::{collections::HashSet, sync::LazyLock};
2
3pub const DLC_PACK_MAGIC: &[u8; 4] = b"BDLP";
5
6pub const DLC_PACK_VERSION_LATEST: u8 = 4;
8
9pub const DEFAULT_BLOCK_SIZE: usize = 10 * 1024 * 1024;
11
12pub const FORBIDDEN_EXTENSIONS: &[&str] = &[
16 "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",
17];
18
19static FORBIDDEN_SET: LazyLock<HashSet<&'static str>> = LazyLock::new(|| {
21 FORBIDDEN_EXTENSIONS.iter().copied().collect()
22});
23
24pub fn is_forbidden_extension(ext: &str) -> bool {
26 let lowercase = ext.trim_start_matches('.').to_ascii_lowercase();
27 FORBIDDEN_SET.contains(lowercase.as_str())
28}
29
30pub fn is_data_executable(data: &[u8]) -> bool {
42 if infer::is_app(data) {
43 return true;
44 }
45 if data.starts_with(b"#!") {
46 return true;
47 }
48 false
49}
50
51#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
53pub struct ManifestEntry {
54 pub path: String,
55 #[serde(skip_serializing_if = "Option::is_none")]
56 pub original_extension: Option<String>,
57 #[serde(skip_serializing_if = "Option::is_none")]
58 pub type_path: Option<String>,
59}
60
61impl ManifestEntry {
62 pub fn from_pack_item(item: &crate::PackItem) -> Self {
63 ManifestEntry {
64 path: item.path.clone(),
65 original_extension: item
66 .original_extension
67 .clone()
68 .filter(|s| !s.is_empty()),
69 type_path: item.type_path.clone(),
70 }
71 }
72}
73
74#[derive(Clone, Debug)]
77pub struct V4ManifestEntry {
78 pub path: String,
79 pub original_extension: String,
80 pub type_path: Option<String>,
81 pub block_id: u32,
83 pub block_offset: u32,
85 pub size: u32,
87}
88
89impl V4ManifestEntry {
90 pub fn from_pack_item(item: &crate::PackItem, block_id: u32, block_offset: u32) -> Self {
91 V4ManifestEntry {
92 path: item.path.clone(),
93 original_extension: item.original_extension.clone().unwrap_or_default(),
94 type_path: item.type_path.clone(),
95 block_id,
96 block_offset,
97 size: item.plaintext.len() as u32,
98 }
99 }
100
101 #[allow(dead_code)]
103 fn write_binary<W: std::io::Write>(&self, writer: &mut PackWriter<W>) -> std::io::Result<()> {
104 writer.write_u32(self.path.len() as u32)?;
105 writer.write_bytes(self.path.as_bytes())?;
106
107 writer.write_u8(self.original_extension.len() as u8)?;
108 writer.write_bytes(self.original_extension.as_bytes())?;
109
110 if let Some(ref tp) = self.type_path {
111 writer.write_u16(tp.len() as u16)?;
112 writer.write_bytes(tp.as_bytes())?;
113 } else {
114 writer.write_u16(0)?;
115 }
116
117 writer.write_u32(self.block_id)?;
118 writer.write_u32(self.block_offset)?;
119 writer.write_u32(self.size)
120 }
121
122 #[allow(dead_code)]
124 fn read_binary<R: std::io::Read>(reader: &mut PackReader<R>) -> std::io::Result<Self> {
125 let path_len = reader.read_u32()? as usize;
126 let path = String::from_utf8(reader.read_bytes(path_len)?)
127 .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
128
129 let ext_len = reader.read_u8()? as usize;
130 let original_extension = if ext_len > 0 {
131 String::from_utf8(reader.read_bytes(ext_len)?)
132 .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?
133 } else {
134 String::new()
135 };
136
137 let type_len = reader.read_u16()? as usize;
138 let type_path = if type_len > 0 {
139 let tp = String::from_utf8(reader.read_bytes(type_len)?)
140 .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
141 Some(tp)
142 } else {
143 None
144 };
145
146 let block_id = reader.read_u32()?;
147 let block_offset = reader.read_u32()?;
148 let size = reader.read_u32()?;
149
150 Ok(V4ManifestEntry {
151 path,
152 original_extension,
153 type_path,
154 block_id,
155 block_offset,
156 size,
157 })
158 }
159}
160
161#[derive(Clone, Debug)]
163pub struct BlockMetadata {
164 pub block_id: u32,
165 pub file_offset: u64, pub encrypted_size: u32, pub uncompressed_size: u32, pub nonce: [u8; 12], pub crc32: u32, }
171
172impl BlockMetadata {
173 #[allow(dead_code)]
174 fn write_binary<W: std::io::Write>(&self, writer: &mut PackWriter<W>) -> std::io::Result<()> {
175 writer.write_u32(self.block_id)?;
176 writer.write_u64(self.file_offset)?;
177 writer.write_u32(self.encrypted_size)?;
178 writer.write_u32(self.uncompressed_size)?;
179 writer.write_bytes(&self.nonce)?;
180 writer.write_u32(self.crc32)
181 }
182
183 #[allow(dead_code)]
184 fn read_binary<R: std::io::Read>(reader: &mut PackReader<R>) -> std::io::Result<Self> {
185 let block_id = reader.read_u32()?;
186 let file_offset = reader.read_u64()?;
187 let encrypted_size = reader.read_u32()?;
188 let uncompressed_size = reader.read_u32()?;
189 let nonce = reader.read_nonce()?;
190 let crc32 = reader.read_u32()?;
191
192 Ok(BlockMetadata {
193 block_id,
194 file_offset,
195 encrypted_size,
196 uncompressed_size,
197 nonce,
198 crc32,
199 })
200 }
201}
202
203pub(crate) struct PackReader<R: std::io::Read> {
211 inner: R,
212}
213
214impl<R: std::io::Read> PackReader<R> {
215 pub fn new(inner: R) -> Self {
216 Self { inner }
217 }
218
219 pub fn read_u8(&mut self) -> std::io::Result<u8> {
221 let mut buf = [0u8; 1];
222 self.inner.read_exact(&mut buf)?;
223 Ok(buf[0])
224 }
225
226 #[allow(dead_code)]
229 pub fn read_and_decrypt(
230 &mut self,
231 key: &crate::EncryptionKey,
232 len: usize,
233 nonce: &[u8],
234 ) -> Result<Vec<u8>, DlcError> {
235 let ciphertext = self.read_bytes(len)?;
236
237 let cursor = std::io::Cursor::new(ciphertext);
238 crate::pack_format::decrypt_with_key(key, cursor, nonce)
239 }
240
241 pub fn read_u16(&mut self) -> std::io::Result<u16> {
242 let mut buf = [0u8; 2];
243 self.inner.read_exact(&mut buf)?;
244 Ok(u16::from_be_bytes(buf))
245 }
246
247 pub fn read_u32(&mut self) -> std::io::Result<u32> {
248 let mut buf = [0u8; 4];
249 self.inner.read_exact(&mut buf)?;
250 Ok(u32::from_be_bytes(buf))
251 }
252
253 #[allow(dead_code)]
254 pub fn read_u64(&mut self) -> std::io::Result<u64> {
255 let mut buf = [0u8; 8];
256 self.inner.read_exact(&mut buf)?;
257 Ok(u64::from_be_bytes(buf))
258 }
259
260 pub fn read_bytes(&mut self, len: usize) -> std::io::Result<Vec<u8>> {
261 let mut buf = vec![0u8; len];
262 self.inner.read_exact(&mut buf)?;
263 Ok(buf)
264 }
265
266 pub fn read_string_u16(&mut self) -> std::io::Result<String> {
267 let len = self.read_u16()? as usize;
268 self.read_string(len)
269 }
270
271 pub fn read_string(&mut self, len: usize) -> std::io::Result<String> {
272 let bytes = self.read_bytes(len)?;
273 String::from_utf8(bytes)
276 .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))
277 }
278
279 pub fn read_nonce(&mut self) -> std::io::Result<[u8; 12]> {
280 let mut nonce = [0u8; 12];
281 self.inner.read_exact(&mut nonce)?;
282 Ok(nonce)
283 }
284}
285
286#[allow(unused)]
288impl<R: std::io::Read + std::io::Seek> PackReader<R> {
289 pub fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result<u64> {
291 self.inner.seek(pos)
292 }
293}
294
295pub(crate) struct PackWriter<W: std::io::Write> {
297 inner: W,
298}
299
300impl<W: std::io::Write> PackWriter<W> {
301 pub fn new(inner: W) -> Self {
302 Self { inner }
303 }
304
305 #[allow(dead_code)]
308 pub fn write_encrypted(
309 &mut self,
310 key: &crate::EncryptionKey,
311 nonce: &[u8],
312 plaintext: &[u8],
313 ) -> Result<(), DlcError> {
314 use aes_gcm::{Aes256Gcm, KeyInit, Nonce, aead::Aead};
315 let cipher = key.with_secret(|kb| {
316 Aes256Gcm::new_from_slice(kb).map_err(|e| DlcError::CryptoError(e.to_string()))
317 })?;
318 let ct = cipher
319 .encrypt(Nonce::from_slice(nonce), plaintext)
320 .map_err(|_| DlcError::EncryptionFailed("block encryption failed".into()))?;
321 self.write_bytes(&ct).map_err(|e| DlcError::Other(e.to_string()))
322 }
323
324 pub fn write_u8(&mut self, val: u8) -> std::io::Result<()> {
325 self.inner.write_all(&[val])
326 }
327
328 pub fn write_u16(&mut self, val: u16) -> std::io::Result<()> {
329 self.inner.write_all(&val.to_be_bytes())
330 }
331
332 pub fn write_u32(&mut self, val: u32) -> std::io::Result<()> {
333 self.inner.write_all(&val.to_be_bytes())
334 }
335
336 pub fn write_u64(&mut self, val: u64) -> std::io::Result<()> {
337 self.inner.write_all(&val.to_be_bytes())
338 }
339
340 pub fn write_bytes(&mut self, bytes: &[u8]) -> std::io::Result<()> {
341 self.inner.write_all(bytes)
342 }
343
344 pub fn write_string_u16(&mut self, s: &str) -> std::io::Result<()> {
345 let bytes = s.as_bytes();
346 self.write_u16(bytes.len() as u16)?;
347 self.write_bytes(bytes)
348 }
349
350 pub fn finish(mut self) -> std::io::Result<W> {
351 self.inner.flush()?;
352 Ok(self.inner)
353 }
354}
355
356struct PackHeader {
358 version: u8,
359 product: String,
360 dlc_id: String,
361}
362
363impl PackHeader {
364 fn read<R: std::io::Read>(reader: &mut PackReader<R>) -> std::io::Result<Self> {
365 let magic = reader.read_bytes(4)?;
366 if magic != DLC_PACK_MAGIC {
367 return Err(std::io::Error::new(
368 std::io::ErrorKind::InvalidData,
369 "invalid dlcpack magic",
370 ));
371 }
372
373 let version = reader.read_u8()?;
374 let mut product = String::new();
375
376 if version == 3 || version == 4 {
377 product = reader.read_string_u16()?;
378 } else if version > 4 {
379 return Err(std::io::Error::new(
380 std::io::ErrorKind::InvalidData,
381 format!("unsupported pack version: {}", version),
382 ));
383 }
384
385 let dlc_id = reader.read_string_u16()?;
386
387 Ok(PackHeader {
388 version,
389 product,
390 dlc_id,
391 })
392 }
393
394 fn write<W: std::io::Write>(&self, writer: &mut PackWriter<W>) -> std::io::Result<()> {
395 writer.write_bytes(DLC_PACK_MAGIC)?;
396 writer.write_u8(self.version)?;
397
398 if self.version == 3 || self.version == 4 {
399 writer.write_string_u16(&self.product)?;
400 }
401
402 writer.write_string_u16(&self.dlc_id)
403 }
404}
405
406use aes_gcm::{Aes256Gcm, KeyInit, Nonce, aead::AeadInPlace};
412use secure_gate::ExposeSecret;
413
414use crate::{DlcError, DlcId, EncryptionKey, PackItem, Product};
415
416pub(crate) fn decrypt_with_key<R: std::io::Read>(
417 key: &crate::EncryptionKey,
418 mut reader: R,
419 nonce: &[u8],
420) -> Result<Vec<u8>, DlcError> {
421 let mut buf = Vec::new();
424 reader
425 .read_to_end(&mut buf)
426 .map_err(|e| DlcError::Other(e.to_string()))?;
427
428 key.with_secret(|key_bytes| {
429 if key_bytes.len() != 32 {
430 return Err(DlcError::InvalidEncryptKey(
431 "encrypt key must be 32 bytes (AES-256)".into(),
432 ));
433 }
434 if nonce.len() != 12 {
435 return Err(DlcError::InvalidNonce(
436 "nonce must be 12 bytes (AES-GCM)".into(),
437 ));
438 }
439 let cipher = Aes256Gcm::new_from_slice(key_bytes)
440 .map_err(|e| DlcError::CryptoError(e.to_string()))?;
441 let nonce = Nonce::from_slice(nonce);
442 cipher
444 .decrypt_in_place(nonce, &[], &mut buf)
445 .map_err(|_|
446 DlcError::DecryptionFailed(
447 "authentication failed (incorrect key or corrupted ciphertext)".to_string(),
448 )
449 )
450 })?;
451 Ok(buf)
452}
453
454#[derive(Debug, Clone, Copy)]
457pub enum CompressionLevel {
458 Fast,
460 Default,
462 Best,
464}
465
466impl From<CompressionLevel> for flate2::Compression {
467 fn from(level: CompressionLevel) -> Self {
468 match level {
469 CompressionLevel::Fast => flate2::Compression::fast(),
470 CompressionLevel::Default => flate2::Compression::default(),
471 CompressionLevel::Best => flate2::Compression::best(),
472 }
473 }
474}
475
476pub fn pack_encrypted_pack(
478 dlc_id: &DlcId,
479 items: &[PackItem],
480 product: &Product,
481 key: &EncryptionKey,
482 block_size: usize,
483) -> Result<Vec<u8>, DlcError> {
484 use aes_gcm::{Aes256Gcm, KeyInit, Nonce, aead::Aead};
485 use flate2::Compression;
486 use flate2::write::GzEncoder;
487 use tar::Builder;
488
489 let cipher = key.with_secret(|kb| {
490 Aes256Gcm::new_from_slice(kb.as_slice()).map_err(|e| DlcError::CryptoError(e.to_string()))
491 })?;
492
493 let mut blocks: Vec<Vec<&PackItem>> = Vec::new();
495 let mut current_block = Vec::new();
496 let mut current_size = 0;
497
498 for item in items {
499 if !current_block.is_empty() && current_size + item.plaintext.len() > block_size {
500 blocks.push(std::mem::take(&mut current_block));
501 current_size = 0;
502 }
503 current_size += item.plaintext.len();
504 current_block.push(item);
505 }
506 if !current_block.is_empty() {
507 blocks.push(current_block);
508 }
509
510 let mut encrypted_blocks = Vec::new();
512 let mut manifest_entries = Vec::new();
513 let mut block_metadatas = Vec::new();
514
515 for (block_id, block_items) in blocks.into_iter().enumerate() {
516 let block_id = block_id as u32;
517 let mut tar_gz = Vec::new();
518 let mut uncompressed_size = 0;
519 {
520 let mut gz = GzEncoder::new(&mut tar_gz, Compression::default());
521 {
522 let mut tar = Builder::new(&mut gz);
523 let mut offset = 0;
524
525 for item in block_items {
526 let mut header = tar::Header::new_gnu();
527 header.set_size(item.plaintext.len() as u64);
528 header.set_mode(0o644);
529 header.set_cksum();
530
531 let path_str = item.path.clone();
532 manifest_entries.push(V4ManifestEntry {
533 path: path_str,
534 original_extension: item.original_extension.clone().unwrap_or_default(),
535 type_path: item.type_path.clone(),
536 block_id,
537 block_offset: offset,
538 size: item.plaintext.len() as u32,
539 });
540
541 tar.append_data(&mut header, &item.path, &item.plaintext[..])
542 .map_err(|e| DlcError::Other(e.to_string()))?;
543
544 let data_len = item.plaintext.len() as u32;
546 let padded_len = (data_len + 511) & !511;
547 offset += 512 + padded_len;
548 uncompressed_size += data_len;
549 }
550 tar.finish().map_err(|e| DlcError::Other(e.to_string()))?;
551 }
552 gz.finish().map_err(|e| DlcError::Other(e.to_string()))?;
553 }
554
555 let nonce_bytes: [u8; 12] = rand::random();
557 let nonce = Nonce::from_slice(&nonce_bytes);
558 let ciphertext = cipher
559 .encrypt(nonce, tar_gz.as_slice())
560 .map_err(|_| DlcError::EncryptionFailed("block encryption failed".into()))?;
561
562 let crc32 = crc32fast::hash(&ciphertext);
563
564 block_metadatas.push(BlockMetadata {
565 block_id,
566 file_offset: 0, encrypted_size: ciphertext.len() as u32,
568 uncompressed_size,
569 nonce: nonce_bytes,
570 crc32,
571 });
572
573 encrypted_blocks.push(ciphertext);
574 }
575
576 let product_str = product.as_ref();
578 let dlc_id_str = dlc_id.to_string();
579
580 let mut out = Vec::new();
581 {
582 let mut writer = PackWriter::new(&mut out);
583
584 let header = PackHeader {
585 version: 4,
586 product: product_str.to_string(),
587 dlc_id: dlc_id_str.clone(),
588 };
589 header.write(&mut writer).map_err(|e| DlcError::Other(e.to_string()))?;
590
591 writer.write_u32(manifest_entries.len() as u32).map_err(|e| DlcError::Other(e.to_string()))?;
593 for entry in &manifest_entries {
594 entry.write_binary(&mut writer).map_err(|e| DlcError::Other(e.to_string()))?;
595 }
596
597 writer.write_u32(block_metadatas.len() as u32).map_err(|e| DlcError::Other(e.to_string()))?;
599 writer.finish().map_err(|e| DlcError::Other(e.to_string()))?;
600 }
601 let metadata_start_pos = out.len();
602 {
603 let mut writer = PackWriter::new(&mut out);
604 for meta in &block_metadatas {
605 meta.write_binary(&mut writer).map_err(|e| DlcError::Other(e.to_string()))?;
606 }
607 writer.finish().map_err(|e| DlcError::Other(e.to_string()))?;
608 }
609
610 for (i, block) in encrypted_blocks.into_iter().enumerate() {
612 let pos = out.len() as u64;
613 block_metadatas[i].file_offset = pos;
614 out.extend_from_slice(&block);
615 }
616
617 {
619 let mut writer_fixed = PackWriter::new(&mut out[metadata_start_pos..]);
620 for meta in &block_metadatas {
621 meta.write_binary(&mut writer_fixed).map_err(|e| DlcError::Other(e.to_string()))?;
622 }
623 writer_fixed.finish().map_err(|e| DlcError::Other(e.to_string()))?;
624 }
625
626 Ok(out)
627}
628
629pub type Version = usize;
630
631pub fn parse_encrypted_pack<R: std::io::Read>(
633 reader: R,
634) -> Result<
635 (
636 Product,
637 DlcId,
638 Version,
639 Vec<(String, crate::asset_loader::EncryptedAsset)>,
640 Vec<BlockMetadata>,
641 ),
642 std::io::Error,
643> {
644 use std::io::ErrorKind;
645
646 let mut reader = PackReader::new(reader);
647 let header = PackHeader::read(&mut reader)?;
648 let mut block_metadatas = Vec::new();
649
650 let entries = if header.version < DLC_PACK_VERSION_LATEST {
651 return Err(std::io::Error::new(
652 ErrorKind::InvalidData,
653 format!("unsupported pack version: {}", header.version),
654 ));
655 } else if header.version == 4 {
656 let manifest_count = reader.read_u32()? as usize;
658 let mut manifest: Vec<V4ManifestEntry> = Vec::with_capacity(manifest_count);
659 for _ in 0..manifest_count {
660 manifest.push(V4ManifestEntry::read_binary(&mut reader)?);
661 }
662
663 let block_count = reader.read_u32()? as usize;
664 block_metadatas = Vec::with_capacity(block_count);
665 for _ in 0..block_count {
666 block_metadatas.push(BlockMetadata::read_binary(&mut reader)?);
667 }
668
669 let mut out = Vec::with_capacity(manifest.len());
670 for entry in manifest {
671 out.push((
672 entry.path,
673 crate::asset_loader::EncryptedAsset {
674 dlc_id: header.dlc_id.clone(),
675 original_extension: entry.original_extension,
676 type_path: entry.type_path,
677 nonce: [0u8; 12],
678 ciphertext: std::sync::Arc::new([]),
679 block_id: entry.block_id,
680 block_offset: entry.block_offset,
681 size: entry.size,
682 },
683 ));
684 }
685 out
686 } else {
687 let manifest_len = reader.read_u32()? as usize;
689 let manifest_bytes = reader.read_bytes(manifest_len)?;
690 let manifest: Vec<ManifestEntry> = serde_json::from_slice(&manifest_bytes)
691 .map_err(|e| std::io::Error::new(ErrorKind::InvalidData, e))?;
692
693 let mut out = Vec::with_capacity(manifest.len());
694
695 let shared_nonce = reader.read_nonce()?;
696 let shared_ciphertext_len = reader.read_u32()? as usize;
697 let shared_ciphertext: std::sync::Arc<[u8]> = reader.read_bytes(shared_ciphertext_len)?.into();
698
699 for entry in manifest {
700 out.push((
701 entry.path,
702 crate::asset_loader::EncryptedAsset {
703 dlc_id: header.dlc_id.clone(),
704 original_extension: entry.original_extension.unwrap_or_default(),
705 type_path: entry.type_path,
706 nonce: shared_nonce,
707 ciphertext: shared_ciphertext.clone(),
708 block_id: 0,
709 block_offset: 0,
710 size: shared_ciphertext.len() as u32,
711 },
712 ));
713 }
714 out
715 };
716
717 Ok((
718 Product::from(header.product),
719 DlcId::from(header.dlc_id),
720 header.version as usize,
721 entries,
722 block_metadatas,
723 ))
724}
725
726#[cfg(test)]
727mod tests {
728 use super::*;
729
730 #[test]
731 fn forbidden_list_contains_known() {
732 assert!(is_forbidden_extension("exe"));
733 assert!(is_forbidden_extension("EXE"));
734 assert!(!is_forbidden_extension("png"));
735 }
736
737 #[test]
738 fn manifest_roundtrip() {
739 let item = crate::PackItem::new("foo.txt", b"hello" as &[u8]).unwrap();
740 let entry = ManifestEntry::from_pack_item(&item);
741 let bytes = serde_json::to_vec(&entry).unwrap();
742 let back: ManifestEntry = serde_json::from_slice(&bytes).unwrap();
743 assert_eq!(entry.path, back.path);
744 }
745}