1pub const DLC_PACK_MAGIC: &[u8; 4] = b"BDLP";
3
4pub const DLC_PACK_VERSION_LATEST: u8 = 4;
6
7pub const DEFAULT_BLOCK_SIZE: usize = 10 * 1024 * 1024;
9
10pub const FORBIDDEN_EXTENSIONS: &[&str] = &[
14 "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",
17];
18
19pub 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#[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#[derive(Clone, Debug)]
73pub struct V4ManifestEntry {
74 pub path: String,
75 pub original_extension: String,
76 pub type_path: Option<String>,
77 pub block_id: u32,
79 pub block_offset: u32,
81 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 #[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 #[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#[derive(Clone, Debug)]
159pub struct BlockMetadata {
160 pub block_id: u32,
161 pub file_offset: u64, pub encrypted_size: u32, pub uncompressed_size: u32, pub nonce: [u8; 12], pub crc32: u32, }
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
199pub(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 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 #[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 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#[allow(unused)]
284impl<R: std::io::Read + std::io::Seek> PackReader<R> {
285 pub fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result<u64> {
287 self.inner.seek(pos)
288 }
289}
290
291pub(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 #[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
352struct 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
402use 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 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 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#[derive(Debug, Clone, Copy)]
453pub enum CompressionLevel {
454 Fast,
456 Default,
458 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
472pub 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 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 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 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 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, 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 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 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 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 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 {
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
627pub 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 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 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}