Skip to main content

bevy_dlc/
pack_format.rs

1/// .dlcpack container magic header (4 bytes) used to identify encrypted pack containers.
2pub const DLC_PACK_MAGIC: &[u8; 4] = b"BDLP";
3
4/// Current supported .dlcpack format version. This is stored in the container header and used to determine how to parse the contents.
5pub const DLC_PACK_VERSION_LATEST: u8 = 4;
6
7/// Default block size for v4 hybrid format (10 MB). Blocks are individually encrypted tar.gz chunks.
8pub const DEFAULT_BLOCK_SIZE: usize = 10 * 1024 * 1024;
9
10/// List of file extensions that are never allowed to be packed.  These are
11/// taken from the same array that used to live in [lib.rs]; we keep the
12/// array for deterministic build and the hash set for fast membership tests.
13pub const FORBIDDEN_EXTENSIONS: &[&str] = &[
14    "dlcpack", "pubkey", "slicense", // these are the expected extensions for keys/licenses; they should be packed with the dedicated macros rather than as raw files
15    // these are generated by a script that pulls a list of extensions that can be used maliciously
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
19/// Lazy hash set used by [`is_forbidden_extension`].
20// static FORBIDDEN_SET: LazyLock<HashSet<&'static str>> = LazyLock::new(|| {
21//     FORBIDDEN_EXTENSIONS.iter().copied().collect()
22// });
23
24// NOTE: if additional heuristics are required in the future they can be
25// reintroduced or re‑exported from the CLI/generation layer rather than the
26// core format library.
27/// Simple heuristic used by the packer to detect executable payloads.  It is
28/// intentionally forgiving; the goal is merely to catch obvious binaries when
29/// a user accidentally tries to pack them.
30pub fn is_data_executable(data: &[u8]) -> bool {
31    if infer::is_app(data) {
32        return true;
33    }
34    
35    for ext in FORBIDDEN_EXTENSIONS {
36        if infer::is(data, *ext) {
37            return true;
38        }
39    }
40
41    if data.starts_with(b"#!") {
42        return true;
43    }
44    false
45}
46
47/// Representation of a single manifest entry inside a v2+ `.dlcpack`.
48#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
49pub struct ManifestEntry {
50    pub path: String,
51    #[serde(skip_serializing_if = "Option::is_none")]
52    pub original_extension: Option<String>,
53    #[serde(skip_serializing_if = "Option::is_none")]
54    pub type_path: Option<String>,
55}
56
57impl ManifestEntry {
58    pub fn from_pack_item(item: &crate::PackItem) -> Self {
59        ManifestEntry {
60            path: item.path.clone(),
61            original_extension: item
62                .original_extension
63                .clone()
64                .filter(|s| !s.is_empty()),
65            type_path: item.type_path.clone(),
66        }
67    }
68}
69
70/// V4 manifest entry: binary-serializable format for faster parsing.
71/// Format: path_len(u32) + path(utf8) + ext_len(u8) + ext(utf8) + type_len(u16) + type(utf8) + block_id(u32) + block_offset(u32) + size(u32)
72#[derive(Clone, Debug)]
73pub struct V4ManifestEntry {
74    pub path: String,
75    pub original_extension: String,
76    pub type_path: Option<String>,
77    /// Which block contains this asset
78    pub block_id: u32,
79    /// Offset within the decompressed block's tar archive
80    pub block_offset: u32,
81    /// Uncompressed size (for progress/buffer allocation)
82    pub size: u32,
83}
84
85impl V4ManifestEntry {
86    pub fn from_pack_item(item: &crate::PackItem, block_id: u32, block_offset: u32) -> Self {
87        V4ManifestEntry {
88            path: item.path.clone(),
89            original_extension: item.original_extension.clone().unwrap_or_default(),
90            type_path: item.type_path.clone(),
91            block_id,
92            block_offset,
93            size: item.plaintext.len() as u32,
94        }
95    }
96
97    /// Write this entry in binary format (used by v4 format conversion)
98    #[allow(dead_code)]
99    fn write_binary<W: std::io::Write>(&self, writer: &mut PackWriter<W>) -> std::io::Result<()> {
100        writer.write_u32(self.path.len() as u32)?;
101        writer.write_bytes(self.path.as_bytes())?;
102
103        writer.write_u8(self.original_extension.len() as u8)?;
104        writer.write_bytes(self.original_extension.as_bytes())?;
105
106        if let Some(ref tp) = self.type_path {
107            writer.write_u16(tp.len() as u16)?;
108            writer.write_bytes(tp.as_bytes())?;
109        } else {
110            writer.write_u16(0)?;
111        }
112
113        writer.write_u32(self.block_id)?;
114        writer.write_u32(self.block_offset)?;
115        writer.write_u32(self.size)
116    }
117
118    /// Read binary format from reader (used by v4 format parsing)
119    #[allow(dead_code)]
120    fn read_binary<R: std::io::Read>(reader: &mut PackReader<R>) -> std::io::Result<Self> {
121        let path_len = reader.read_u32()? as usize;
122        let path = String::from_utf8(reader.read_bytes(path_len)?)
123            .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
124
125        let ext_len = reader.read_u8()? as usize;
126        let original_extension = if ext_len > 0 {
127            String::from_utf8(reader.read_bytes(ext_len)?)
128                .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?
129        } else {
130            String::new()
131        };
132
133        let type_len = reader.read_u16()? as usize;
134        let type_path = if type_len > 0 {
135            let tp = String::from_utf8(reader.read_bytes(type_len)?)
136                .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
137            Some(tp)
138        } else {
139            None
140        };
141
142        let block_id = reader.read_u32()?;
143        let block_offset = reader.read_u32()?;
144        let size = reader.read_u32()?;
145
146        Ok(V4ManifestEntry {
147            path,
148            original_extension,
149            type_path,
150            block_id,
151            block_offset,
152            size,
153        })
154    }
155}
156
157/// V4 block metadata: stores information about a tar-gz block within the pack file
158#[derive(Clone, Debug)]
159pub struct BlockMetadata {
160    pub block_id: u32,
161    pub file_offset: u64,      // Where this block starts in the file
162    pub encrypted_size: u32,   // Size of encrypted ciphertext
163    pub uncompressed_size: u32, // Size after decompression (for buffer allocation)
164    pub nonce: [u8; 12],       // Per-block nonce
165    pub crc32: u32,            // CRC32 checksum for integrity
166}
167
168impl BlockMetadata {
169    #[allow(dead_code)]
170    fn write_binary<W: std::io::Write>(&self, writer: &mut PackWriter<W>) -> std::io::Result<()> {
171        writer.write_u32(self.block_id)?;
172        writer.write_u64(self.file_offset)?;
173        writer.write_u32(self.encrypted_size)?;
174        writer.write_u32(self.uncompressed_size)?;
175        writer.write_bytes(&self.nonce)?;
176        writer.write_u32(self.crc32)
177    }
178
179    #[allow(dead_code)]
180    fn read_binary<R: std::io::Read>(reader: &mut PackReader<R>) -> std::io::Result<Self> {
181        let block_id = reader.read_u32()?;
182        let file_offset = reader.read_u64()?;
183        let encrypted_size = reader.read_u32()?;
184        let uncompressed_size = reader.read_u32()?;
185        let nonce = reader.read_nonce()?;
186        let crc32 = reader.read_u32()?;
187
188        Ok(BlockMetadata {
189            block_id,
190            file_offset,
191            encrypted_size,
192            uncompressed_size,
193            nonce,
194            crc32,
195        })
196    }
197}
198
199// converters and migration helpers have been removed – the crate
200// now only supports v4 packs.  The old `PackConverter` trait, the
201// `V3toV4Converter` implementation and the `PackConverterRegistry` type
202// were used to upgrade on‑disk packs to the latest format; they are
203// retained in history if needed but no longer compiled.
204
205/// Internal helper for binary parsing with offset management.
206pub(crate) struct PackReader<R: std::io::Read> {
207    inner: R,
208}
209
210impl<R: std::io::Read> PackReader<R> {
211    pub fn new(inner: R) -> Self {
212        Self { inner }
213    }
214
215    /// Read a byte.
216    pub fn read_u8(&mut self) -> std::io::Result<u8> {
217        let mut buf = [0u8; 1];
218        self.inner.read_exact(&mut buf)?;
219        Ok(buf[0])
220    }
221
222    /// Convenience method: read `len` bytes and immediately decrypt them with
223    /// the provided AES-GCM key/nonce.
224    #[allow(dead_code)]
225    pub fn read_and_decrypt(
226        &mut self,
227        key: &crate::EncryptionKey,
228        len: usize,
229        nonce: &[u8],
230    ) -> Result<Vec<u8>, DlcError> {
231        let ciphertext = self.read_bytes(len)?;
232
233        let cursor = std::io::Cursor::new(ciphertext);
234        crate::pack_format::decrypt_with_key(key, cursor, nonce)
235    }
236
237    pub fn read_u16(&mut self) -> std::io::Result<u16> {
238        let mut buf = [0u8; 2];
239        self.inner.read_exact(&mut buf)?;
240        Ok(u16::from_be_bytes(buf))
241    }
242
243    pub fn read_u32(&mut self) -> std::io::Result<u32> {
244        let mut buf = [0u8; 4];
245        self.inner.read_exact(&mut buf)?;
246        Ok(u32::from_be_bytes(buf))
247    }
248
249    #[allow(dead_code)]
250    pub fn read_u64(&mut self) -> std::io::Result<u64> {
251        let mut buf = [0u8; 8];
252        self.inner.read_exact(&mut buf)?;
253        Ok(u64::from_be_bytes(buf))
254    }
255
256    pub fn read_bytes(&mut self, len: usize) -> std::io::Result<Vec<u8>> {
257        let mut buf = vec![0u8; len];
258        self.inner.read_exact(&mut buf)?;
259        Ok(buf)
260    }
261
262    pub fn read_string_u16(&mut self) -> std::io::Result<String> {
263        let len = self.read_u16()? as usize;
264        self.read_string(len)
265    }
266
267    pub fn read_string(&mut self, len: usize) -> std::io::Result<String> {
268        let bytes = self.read_bytes(len)?;
269        // Avoid allocation for UTF-8 validation by using from_utf8_lossy internally,
270        // but preserve error semantics for actual validation
271        String::from_utf8(bytes)
272            .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))
273    }
274
275    pub fn read_nonce(&mut self) -> std::io::Result<[u8; 12]> {
276        let mut nonce = [0u8; 12];
277        self.inner.read_exact(&mut nonce)?;
278        Ok(nonce)
279    }
280}
281
282// additional helpers available when the inner reader also implements `Seek`
283#[allow(unused)]
284impl<R: std::io::Read + std::io::Seek> PackReader<R> {
285    /// Seek the underlying reader to `pos` and return the new position.
286    pub fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result<u64> {
287        self.inner.seek(pos)
288    }
289}
290
291/// Internal helper for binary packing.
292pub(crate) struct PackWriter<W: std::io::Write> {
293    inner: W,
294}
295
296impl<W: std::io::Write> PackWriter<W> {
297    pub fn new(inner: W) -> Self {
298        Self { inner }
299    }
300
301    /// Encrypt `plaintext` with the provided key/nonce and write the
302    /// resulting ciphertext to the underlying writer.
303    #[allow(dead_code)]
304    pub fn write_encrypted(
305        &mut self,
306        key: &crate::EncryptionKey,
307        nonce: &[u8],
308        plaintext: &[u8],
309    ) -> Result<(), DlcError> {
310        use aes_gcm::{Aes256Gcm, KeyInit, Nonce, aead::Aead};
311        let cipher = key.with_secret(|kb| {
312            Aes256Gcm::new_from_slice(kb).map_err(|e| DlcError::CryptoError(e.to_string()))
313        })?;
314        let ct = cipher
315            .encrypt(Nonce::from_slice(nonce), plaintext)
316            .map_err(|_| DlcError::EncryptionFailed("block encryption failed".into()))?;
317        self.write_bytes(&ct).map_err(|e| DlcError::Other(e.to_string()))
318    }
319
320    pub fn write_u8(&mut self, val: u8) -> std::io::Result<()> {
321        self.inner.write_all(&[val])
322    }
323
324    pub fn write_u16(&mut self, val: u16) -> std::io::Result<()> {
325        self.inner.write_all(&val.to_be_bytes())
326    }
327
328    pub fn write_u32(&mut self, val: u32) -> std::io::Result<()> {
329        self.inner.write_all(&val.to_be_bytes())
330    }
331
332    pub fn write_u64(&mut self, val: u64) -> std::io::Result<()> {
333        self.inner.write_all(&val.to_be_bytes())
334    }
335
336    pub fn write_bytes(&mut self, bytes: &[u8]) -> std::io::Result<()> {
337        self.inner.write_all(bytes)
338    }
339
340    pub fn write_string_u16(&mut self, s: &str) -> std::io::Result<()> {
341        let bytes = s.as_bytes();
342        self.write_u16(bytes.len() as u16)?;
343        self.write_bytes(bytes)
344    }
345
346    pub fn finish(mut self) -> std::io::Result<W> {
347        self.inner.flush()?;
348        Ok(self.inner)
349    }
350}
351
352/// Represents the header of any version of a `.dlcpack`.
353struct PackHeader {
354    version: u8,
355    product: String,
356    dlc_id: String,
357}
358
359impl PackHeader {
360    fn read<R: std::io::Read>(reader: &mut PackReader<R>) -> std::io::Result<Self> {
361        let magic = reader.read_bytes(4)?;
362        if magic != DLC_PACK_MAGIC {
363            return Err(std::io::Error::new(
364                std::io::ErrorKind::InvalidData,
365                "invalid dlcpack magic",
366            ));
367        }
368
369        let version = reader.read_u8()?;
370        let mut product = String::new();
371
372        if version == 3 || version == 4 {
373            product = reader.read_string_u16()?;
374        } else if version > 4 {
375            return Err(std::io::Error::new(
376                std::io::ErrorKind::InvalidData,
377                format!("unsupported pack version: {}", version),
378            ));
379        }
380
381        let dlc_id = reader.read_string_u16()?;
382
383        Ok(PackHeader {
384            version,
385            product,
386            dlc_id,
387        })
388    }
389
390    fn write<W: std::io::Write>(&self, writer: &mut PackWriter<W>) -> std::io::Result<()> {
391        writer.write_bytes(DLC_PACK_MAGIC)?;
392        writer.write_u8(self.version)?;
393
394        if self.version == 3 || self.version == 4 {
395            writer.write_string_u16(&self.product)?;
396        }
397
398        writer.write_string_u16(&self.dlc_id)
399    }
400}
401
402/// Decrypt with a 32-byte AES key using AES-GCM.  This is the same logic that
403/// used to live in `lib.rs`; moving it here allows crates that only depend on
404/// the pack format helpers to perform decryption without pulling in the
405/// higher-level API.
406
407use aes_gcm::{Aes256Gcm, KeyInit, Nonce, aead::AeadInPlace};
408use secure_gate::ExposeSecret;
409
410use crate::{DlcError, DlcId, EncryptionKey, PackItem, Product};
411
412pub(crate) fn decrypt_with_key<R: std::io::Read>(
413    key: &crate::EncryptionKey,
414    mut reader: R,
415    nonce: &[u8],
416) -> Result<Vec<u8>, DlcError> {
417    // read ciphertext into a single buffer; decrypt it in-place to avoid
418    // allocating a second plaintext buffer.
419    let mut buf = Vec::new();
420    reader
421        .read_to_end(&mut buf)
422        .map_err(|e| DlcError::Other(e.to_string()))?;
423
424    key.with_secret(|key_bytes| {
425        if key_bytes.len() != 32 {
426            return Err(DlcError::InvalidEncryptKey(
427                "encrypt key must be 32 bytes (AES-256)".into(),
428            ));
429        }
430        if nonce.len() != 12 {
431            return Err(DlcError::InvalidNonce(
432                "nonce must be 12 bytes (AES-GCM)".into(),
433            ));
434        }
435        let cipher = Aes256Gcm::new_from_slice(key_bytes)
436            .map_err(|e| DlcError::CryptoError(e.to_string()))?;
437        let nonce = Nonce::from_slice(nonce);
438        // decrypt in-place; `buf` will be overwritten with plaintext
439        cipher
440            .decrypt_in_place(nonce, &[], &mut buf)
441            .map_err(|_|
442                DlcError::DecryptionFailed(
443                    "authentication failed (incorrect key or corrupted ciphertext)".to_string(),
444                )
445            )
446    })?;
447    Ok(buf)
448}
449
450/// Compression level for packing. Controls the trade-off between pack size and packing time.
451/// Higher levels produce smaller files but take longer to create.
452#[derive(Debug, Clone, Copy)]
453pub enum CompressionLevel {
454    /// Fast compression (level 1), suitable for rapid iterations
455    Fast,
456    /// Balanced compression (level 6, default), good trade-off
457    Default,
458    /// Best compression (level 9), smallest file size for distribution
459    Best,
460}
461
462impl From<CompressionLevel> for flate2::Compression {
463    fn from(level: CompressionLevel) -> Self {
464        match level {
465            CompressionLevel::Fast => flate2::Compression::fast(),
466            CompressionLevel::Default => flate2::Compression::default(),
467            CompressionLevel::Best => flate2::Compression::best(),
468        }
469    }
470}
471
472/// Pack multiple entries into a single encrypted `.dlcpack` binary using the latest version format.  This is the main entry point for pack creation and is used by the CLI tool; it can also be used by other tools that want to generate packs programmatically.
473pub fn pack_encrypted_pack(
474    dlc_id: &DlcId,
475    items: &[PackItem],
476    product: &Product,
477    key: &EncryptionKey,
478    block_size: usize,
479) -> Result<Vec<u8>, DlcError> {
480    use aes_gcm::{Aes256Gcm, KeyInit, Nonce, aead::Aead};
481    use flate2::Compression;
482    use flate2::write::GzEncoder;
483    use tar::Builder;
484
485    let cipher = key.with_secret(|kb| {
486        Aes256Gcm::new_from_slice(kb.as_slice()).map_err(|e| DlcError::CryptoError(e.to_string()))
487    })?;
488
489    // 1. Group items into blocks
490    let mut blocks: Vec<Vec<&PackItem>> = Vec::new();
491    let mut current_block = Vec::new();
492    let mut current_size = 0;
493
494    for item in items {
495        if !current_block.is_empty() && current_size + item.plaintext.len() > block_size {
496            blocks.push(std::mem::take(&mut current_block));
497            current_size = 0;
498        }
499        current_size += item.plaintext.len();
500        current_block.push(item);
501    }
502    if !current_block.is_empty() {
503        blocks.push(current_block);
504    }
505
506    // 2. Build blocks and track offsets
507    let mut encrypted_blocks = Vec::new();
508    let mut manifest_entries = Vec::new();
509    let mut block_metadatas = Vec::new();
510
511    for (block_id, block_items) in blocks.into_iter().enumerate() {
512        let block_id = block_id as u32;
513        let mut tar_gz = Vec::new();
514        let mut uncompressed_size = 0;
515        {
516            let mut gz = GzEncoder::new(&mut tar_gz, Compression::default());
517            {
518                let mut tar = Builder::new(&mut gz);
519                let mut offset = 0;
520
521                for item in block_items {
522                    let mut header = tar::Header::new_gnu();
523                    header.set_size(item.plaintext.len() as u64);
524                    header.set_mode(0o644);
525                    header.set_cksum();
526
527                    let path_str = item.path.clone();
528                    manifest_entries.push(V4ManifestEntry {
529                        path: path_str,
530                        original_extension: item.original_extension.clone().unwrap_or_default(),
531                        type_path: item.type_path.clone(),
532                        block_id,
533                        block_offset: offset,
534                        size: item.plaintext.len() as u32,
535                    });
536
537                    tar.append_data(&mut header, &item.path, &item.plaintext[..])
538                        .map_err(|e| DlcError::Other(e.to_string()))?;
539
540                    // tar header is 512, plus data (padded to 512)
541                    let data_len = item.plaintext.len() as u32;
542                    let padded_len = (data_len + 511) & !511;
543                    offset += 512 + padded_len;
544                    uncompressed_size += data_len;
545                }
546                tar.finish().map_err(|e| DlcError::Other(e.to_string()))?;
547            }
548            gz.finish().map_err(|e| DlcError::Other(e.to_string()))?;
549        }
550
551        // Encrypt block
552        let nonce_bytes: [u8; 12] = rand::random();
553        let nonce = Nonce::from_slice(&nonce_bytes);
554        let ciphertext = cipher
555            .encrypt(nonce, tar_gz.as_slice())
556            .map_err(|_| DlcError::EncryptionFailed("block encryption failed".into()))?;
557
558        let crc32 = crc32fast::hash(&ciphertext);
559
560        block_metadatas.push(BlockMetadata {
561            block_id,
562            file_offset: 0, // Fill later
563            encrypted_size: ciphertext.len() as u32,
564            uncompressed_size,
565            nonce: nonce_bytes,
566            crc32,
567        });
568
569        encrypted_blocks.push(ciphertext);
570    }
571
572    // 3. Assemble final binary
573    let product_str = product.as_ref();
574    let dlc_id_str = dlc_id.to_string();
575
576    let mut out = Vec::new();
577    {
578        let mut writer = PackWriter::new(&mut out);
579
580        let header = PackHeader {
581            version: 4,
582            product: product_str.to_string(),
583            dlc_id: dlc_id_str.clone(),
584        };
585        header.write(&mut writer).map_err(|e| DlcError::Other(e.to_string()))?;
586
587        // Manifest
588        writer.write_u32(manifest_entries.len() as u32).map_err(|e| DlcError::Other(e.to_string()))?;
589        for entry in &manifest_entries {
590            entry.write_binary(&mut writer).map_err(|e| DlcError::Other(e.to_string()))?;
591        }
592
593        // Block Metadata placeholder
594        writer.write_u32(block_metadatas.len() as u32).map_err(|e| DlcError::Other(e.to_string()))?;
595        writer.finish().map_err(|e| DlcError::Other(e.to_string()))?;
596    }
597    let metadata_start_pos = out.len();
598    {
599        let mut writer = PackWriter::new(&mut out);
600        for meta in &block_metadatas {
601            meta.write_binary(&mut writer).map_err(|e| DlcError::Other(e.to_string()))?;
602        }
603        writer.finish().map_err(|e| DlcError::Other(e.to_string()))?;
604    }
605
606    // Encrypted Blocks
607    for (i, block) in encrypted_blocks.into_iter().enumerate() {
608        let pos = out.len() as u64;
609        block_metadatas[i].file_offset = pos;
610        out.extend_from_slice(&block);
611    }
612
613    // Rewrite block metadatas with correct file_offsets
614    {
615        let mut writer_fixed = PackWriter::new(&mut out[metadata_start_pos..]);
616        for meta in &block_metadatas {
617            meta.write_binary(&mut writer_fixed).map_err(|e| DlcError::Other(e.to_string()))?;
618        }
619        writer_fixed.finish().map_err(|e| DlcError::Other(e.to_string()))?;
620    }
621
622    Ok(out)
623}
624
625pub type Version = usize;
626
627/// Returns: (product, dlc_id, version, entries)
628pub fn parse_encrypted_pack<R: std::io::Read>(
629    reader: R,
630) -> Result<
631    (
632        Product,
633        DlcId,
634        Version,
635        Vec<(String, crate::asset_loader::EncryptedAsset)>,
636        Vec<BlockMetadata>,
637    ),
638    std::io::Error,
639> {
640    use std::io::ErrorKind;
641
642    let mut reader = PackReader::new(reader);
643    let header = PackHeader::read(&mut reader)?;
644    let mut block_metadatas = Vec::new();
645
646    let entries = if header.version < DLC_PACK_VERSION_LATEST {
647        return Err(std::io::Error::new(
648            ErrorKind::InvalidData,
649            format!("unsupported pack version: {}", header.version),
650        ));
651    } else if header.version == 4 {
652        // version 4: hybrid multi-block format
653        let manifest_count = reader.read_u32()? as usize;
654        let mut manifest: Vec<V4ManifestEntry> = Vec::with_capacity(manifest_count);
655        for _ in 0..manifest_count {
656            manifest.push(V4ManifestEntry::read_binary(&mut reader)?);
657        }
658
659        let block_count = reader.read_u32()? as usize;
660        block_metadatas = Vec::with_capacity(block_count);
661        for _ in 0..block_count {
662            block_metadatas.push(BlockMetadata::read_binary(&mut reader)?);
663        }
664
665        let mut out = Vec::with_capacity(manifest.len());
666        for entry in manifest {
667            out.push((
668                entry.path,
669                crate::asset_loader::EncryptedAsset {
670                    dlc_id: header.dlc_id.clone(),
671                    original_extension: entry.original_extension,
672                    type_path: entry.type_path,
673                    nonce: [0u8; 12],
674                    ciphertext: std::sync::Arc::new([]),
675                    block_id: entry.block_id,
676                    block_offset: entry.block_offset,
677                    size: entry.size,
678                },
679            ));
680        }
681        out
682    } else {
683        // version 2/3: manifest JSON followed by shared nonce and ciphertext
684        let manifest_len = reader.read_u32()? as usize;
685        let manifest_bytes = reader.read_bytes(manifest_len)?;
686        let manifest: Vec<ManifestEntry> = serde_json::from_slice(&manifest_bytes)
687            .map_err(|e| std::io::Error::new(ErrorKind::InvalidData, e))?;
688
689        let mut out = Vec::with_capacity(manifest.len());
690
691        let shared_nonce = reader.read_nonce()?;
692        let shared_ciphertext_len = reader.read_u32()? as usize;
693        let shared_ciphertext: std::sync::Arc<[u8]> = reader.read_bytes(shared_ciphertext_len)?.into();
694
695        for entry in manifest {
696            out.push((
697                entry.path,
698                crate::asset_loader::EncryptedAsset {
699                    dlc_id: header.dlc_id.clone(),
700                    original_extension: entry.original_extension.unwrap_or_default(),
701                    type_path: entry.type_path,
702                    nonce: shared_nonce,
703                    ciphertext: shared_ciphertext.clone(),
704                    block_id: 0,
705                    block_offset: 0,
706                    size: shared_ciphertext.len() as u32,
707                },
708            ));
709        }
710        out
711    };
712
713    Ok((
714        Product::from(header.product),
715        DlcId::from(header.dlc_id),
716        header.version as usize,
717        entries,
718        block_metadatas,
719    ))
720}
721
722#[cfg(test)]
723mod tests {
724    use super::*;
725
726    #[test]
727    fn manifest_roundtrip() {
728        let item = crate::PackItem::new("foo.txt", b"hello" as &[u8]).unwrap();
729        let entry = ManifestEntry::from_pack_item(&item);
730        let bytes = serde_json::to_vec(&entry).unwrap();
731        let back: ManifestEntry = serde_json::from_slice(&bytes).unwrap();
732        assert_eq!(entry.path, back.path);
733    }
734}