1use crate::prelude::*;
42
43use alloc::borrow::Cow;
44use alloc::collections::BTreeMap;
45use core::marker::PhantomData;
46
47use serde::de::DeserializeOwned;
48use serde::{Deserialize, Serialize};
49use shuyi_core::ContentHash;
50
51use crate::bundle::Layer;
52use crate::error::ContentError;
53use crate::fnv::Hasher;
54use crate::format::{AssetKind, AssetPath, DecodeCx, Format};
55
56mod bake;
57
58pub use bake::{Bake, Bakeable, Baked, BakedFile};
59
60pub const EXTENSION: &str = "shuyipak";
62
63pub const MAGIC: [u8; 8] = *b"SHUYIPAK";
65
66pub const VERSION: u32 = 9;
71
72pub const BAKED_MAGIC: [u8; 8] = *b"SHUYIBKD";
74
75pub const MAX_FILES: usize = 1 << 16;
77
78pub const MAX_UNPACKED_BYTES: u64 = 1 << 30;
80
81pub const MAX_BAKED_BYTES: usize = 64 << 20;
84
85const MAX_RATIO: u64 = 255;
88
89const HEADER: usize = 24;
92
93#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
95pub struct PackInfo {
96 pub stamp: String,
100 pub target: Option<String>,
102 pub content: ContentHash,
106}
107
108#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
110enum Codec {
111 Stored,
113 Lz4,
115}
116
117#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
119struct Entry {
120 path: String,
122 kinds: Vec<String>,
124 codec: Codec,
126 offset: u64,
128 stored: u64,
130 size: u64,
132}
133
134#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
136struct Index {
137 info: PackInfo,
138 entries: Vec<Entry>,
139}
140
141#[derive(Debug, Clone, PartialEq, Eq)]
143pub enum Content {
144 Raw(Vec<u8>),
146 Baked(BTreeMap<&'static str, Vec<u8>>),
149}
150
151#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
153pub enum PackError {
154 #[error("not a Shuyi pack: a .shuyipak file starts with \"SHUYIPAK\"")]
156 NotAPack,
157 #[error(
159 "the pack is of pack format version {found} and this engine reads version {VERSION}: \
160 bake it again with `shuyi bake`"
161 )]
162 Version {
163 found: u32,
165 },
166 #[error(
168 "the pack was baked for \"{found}\" and this game is \"{expected}\": bake it again with \
169 `shuyi bake`"
170 )]
171 Stamp {
172 found: String,
174 expected: String,
176 },
177 #[error("the pack is cut short: bake it again with `shuyi bake`")]
179 Truncated,
180 #[error("the pack was changed after it was baked (its checksum differs): bake it again")]
182 Checksum,
183 #[error("the pack is broken: {0}")]
185 Broken(String),
186}
187
188#[derive(Debug, Clone, PartialEq, Eq)]
192pub struct Pack<'a> {
193 info: PackInfo,
194 files: Vec<PackFile<'a>>,
195}
196
197#[derive(Debug, Clone, PartialEq, Eq)]
199pub struct PackFile<'a> {
200 pub path: String,
202 pub kinds: Vec<String>,
204 pub stored: usize,
206 pub bytes: Cow<'a, [u8]>,
209}
210
211fn checksum(bytes: &[u8]) -> u64 {
214 let mut hash = Hasher::new();
215 hash.bytes(bytes);
216 hash.word(bytes.len() as u64);
217 hash.0
218}
219
220fn take<const N: usize>(bytes: &[u8], at: usize) -> Result<[u8; N], PackError> {
222 bytes
223 .get(at..at + N)
224 .and_then(|b| b.try_into().ok())
225 .ok_or(PackError::Truncated)
226}
227
228fn broken(why: impl Into<String>) -> PackError {
230 PackError::Broken(why.into())
231}
232
233fn baked_file(sections: &BTreeMap<&'static str, Vec<u8>>) -> Vec<u8> {
235 let list: Vec<(&str, &[u8])> = sections
236 .iter()
237 .map(|(kind, data)| (*kind, data.as_slice()))
238 .collect();
239 let mut bytes = BAKED_MAGIC.to_vec();
240 bytes.extend(postcard::to_allocvec(&list).unwrap_or_default());
242 bytes
243}
244
245fn sections(bytes: &[u8]) -> Option<Vec<(&str, &[u8])>> {
248 let rest = bytes.strip_prefix(&BAKED_MAGIC)?;
249 let (list, left) = postcard::take_from_bytes::<Vec<(&str, &[u8])>>(rest).ok()?;
250 left.is_empty().then_some(list)
251}
252
253pub fn write(info: &PackInfo, files: &BTreeMap<String, Content>) -> Vec<u8> {
256 let mut entries = Vec::with_capacity(files.len());
257 let mut data = Vec::new();
258 for (path, content) in files {
259 let (kinds, bytes) = match content {
260 Content::Raw(bytes) => (Vec::new(), Cow::Borrowed(bytes.as_slice())),
261 Content::Baked(sections) => (
262 sections.keys().map(|&kind| kind.to_owned()).collect(),
263 Cow::Owned(baked_file(sections)),
264 ),
265 };
266 let packed = lz4_flex::block::compress(&bytes);
267 let (codec, kept) = if packed.len() < bytes.len() {
268 (Codec::Lz4, packed.as_slice())
269 } else {
270 (Codec::Stored, &*bytes)
271 };
272 entries.push(Entry {
273 path: path.clone(),
274 kinds,
275 codec,
276 offset: data.len() as u64,
277 stored: kept.len() as u64,
278 size: bytes.len() as u64,
279 });
280 data.extend_from_slice(kept);
281 }
282 let index = Index {
283 info: info.clone(),
284 entries,
285 };
286 let index = postcard::to_allocvec(&index).unwrap_or_default();
288 let mut body = Vec::with_capacity(index.len() + data.len());
289 body.extend_from_slice(&index);
290 body.extend_from_slice(&data);
291 let mut out = Vec::with_capacity(HEADER + body.len());
292 out.extend_from_slice(&MAGIC);
293 out.extend_from_slice(&VERSION.to_le_bytes());
294 out.extend_from_slice(&checksum(&body).to_le_bytes());
295 out.extend_from_slice(&u32::try_from(index.len()).unwrap_or(u32::MAX).to_le_bytes());
297 out.extend_from_slice(&body);
298 out
299}
300
301fn body(bytes: &[u8]) -> Result<(&[u8], usize), PackError> {
304 if !bytes.starts_with(&MAGIC) {
305 return Err(if MAGIC.starts_with(bytes) {
306 PackError::Truncated
307 } else {
308 PackError::NotAPack
309 });
310 }
311 let found = u32::from_le_bytes(take(bytes, 8)?);
312 if found != VERSION {
313 return Err(PackError::Version { found });
314 }
315 let length = u32::from_le_bytes(take(bytes, 20)?);
316 let body = bytes.get(HEADER..).ok_or(PackError::Truncated)?;
317 let length = usize::try_from(length).map_err(|_| PackError::Truncated)?;
318 if body.len() < length {
319 return Err(PackError::Truncated);
320 }
321 Ok((body, length))
322}
323
324pub fn length(bytes: &[u8]) -> Result<usize, PackError> {
329 let (body, length) = body(bytes)?;
330 let index: Index =
331 postcard::from_bytes(&body[..length]).map_err(|_| broken("its index doesn't read"))?;
332 let data = index
333 .entries
334 .last()
335 .map_or(0, |entry| entry.offset.saturating_add(entry.stored));
336 usize::try_from(data)
337 .ok()
338 .and_then(|data| (HEADER + length).checked_add(data))
339 .filter(|&total| total <= bytes.len())
340 .ok_or(PackError::Truncated)
341}
342
343fn index(bytes: &[u8]) -> Result<(Index, &[u8]), PackError> {
346 let (body, length) = body(bytes)?;
347 let sum = u64::from_le_bytes(take(bytes, 12)?);
348 if checksum(body) != sum {
349 return Err(PackError::Checksum);
350 }
351 let (index, data) = body.split_at(length);
352 let index: Index = postcard::from_bytes(index).map_err(|_| broken("its index doesn't read"))?;
353 Ok((index, data))
354}
355
356fn unpack<'a>(
359 entry: &Entry,
360 data: &'a [u8],
361 end: u64,
362 unpacked: u64,
363) -> Result<Cow<'a, [u8]>, PackError> {
364 let path = &entry.path;
365 if entry.offset != end {
366 return Err(broken(format!(
367 "{path} starts at {} and the file before it ends at {end}",
368 entry.offset
369 )));
370 }
371 if unpacked.saturating_add(entry.size) > MAX_UNPACKED_BYTES {
372 return Err(broken(format!(
373 "its files add up to more than {MAX_UNPACKED_BYTES} bytes"
374 )));
375 }
376 let size = usize::try_from(entry.size).map_err(|_| broken(format!("{path} is too big")))?;
377 let range = usize::try_from(entry.offset)
378 .ok()
379 .zip(usize::try_from(entry.offset.saturating_add(entry.stored)).ok())
380 .and_then(|(start, end)| data.get(start..end))
381 .ok_or_else(|| broken(format!("{path} goes past the end of the pack")))?;
382 match entry.codec {
383 Codec::Stored if entry.stored == entry.size => Ok(Cow::Borrowed(range)),
384 Codec::Stored => Err(broken(format!("{path} is stored with two sizes"))),
385 Codec::Lz4 if entry.size > entry.stored.saturating_mul(MAX_RATIO) => Err(broken(format!(
386 "{path} claims {size} bytes from {} stored",
387 entry.stored
388 ))),
389 Codec::Lz4 => match lz4_flex::block::decompress(range, size) {
390 Ok(bytes) if bytes.len() == size => Ok(Cow::Owned(bytes)),
391 _ => Err(broken(format!("{path} doesn't unpack"))),
392 },
393 }
394}
395
396pub fn info(bytes: &[u8]) -> Result<PackInfo, PackError> {
400 index(bytes).map(|(index, _)| index.info)
401}
402
403impl<'a> Pack<'a> {
404 pub fn open(bytes: &'a [u8], stamp: &str) -> Result<Self, PackError> {
407 let (index, data) = index(bytes)?;
408 if index.info.stamp != stamp {
409 return Err(PackError::Stamp {
410 found: index.info.stamp,
411 expected: stamp.to_owned(),
412 });
413 }
414 if index.entries.len() > MAX_FILES {
415 return Err(broken(format!("it has more than {MAX_FILES} files")));
416 }
417 let mut files: Vec<PackFile<'a>> = Vec::with_capacity(index.entries.len());
418 let (mut end, mut unpacked) = (0, 0);
419 for entry in index.entries {
420 if files.last().is_some_and(|last| last.path >= entry.path) {
421 return Err(broken(format!("{} is out of path order", entry.path)));
422 }
423 let bytes = unpack(&entry, data, end, unpacked)?;
424 if !entry.kinds.is_empty() && sections(&bytes).is_none() {
425 return Err(broken(format!("{} isn't a whole baked file", entry.path)));
426 }
427 end = entry.offset.saturating_add(entry.stored);
428 unpacked = unpacked.saturating_add(entry.size);
429 files.push(PackFile {
430 path: entry.path,
431 kinds: entry.kinds,
432 stored: usize::try_from(entry.stored).unwrap_or(usize::MAX),
433 bytes,
434 });
435 }
436 if usize::try_from(end).ok() != Some(data.len()) {
437 return Err(broken("bytes follow its last file"));
438 }
439 Ok(Self {
440 info: index.info,
441 files,
442 })
443 }
444
445 pub fn info(&self) -> &PackInfo {
447 &self.info
448 }
449
450 pub fn files(&self) -> &[PackFile<'a>] {
452 &self.files
453 }
454
455 pub fn get(&self, path: &str) -> Option<&[u8]> {
457 let at = self
458 .files
459 .binary_search_by(|file| file.path.as_str().cmp(path))
460 .ok()?;
461 Some(&self.files[at].bytes)
462 }
463
464 pub fn into_owned(self) -> Pack<'static> {
467 let files = self
468 .files
469 .into_iter()
470 .map(|file| PackFile {
471 bytes: Cow::Owned(file.bytes.into_owned()),
472 path: file.path,
473 kinds: file.kinds,
474 stored: file.stored,
475 })
476 .collect();
477 Pack {
478 info: self.info,
479 files,
480 }
481 }
482}
483
484impl Pack<'static> {
485 pub fn into_layer(self, name: &'static str) -> Layer {
491 let content = self.info.content;
492 let table: Vec<(&'static str, &'static [u8])> = self
493 .files
494 .into_iter()
495 .map(|file| {
496 let path: &'static str = Box::leak(file.path.into_boxed_str());
497 let bytes: &'static [u8] = match file.bytes {
498 Cow::Borrowed(bytes) => bytes,
499 Cow::Owned(bytes) => Box::leak(bytes.into_boxed_slice()),
500 };
501 (path, bytes)
502 })
503 .collect();
504 Layer::table(name, Box::leak(table.into_boxed_slice())).standing_for(content)
505 }
506}
507
508pub struct Packed<K>(PhantomData<fn() -> K>);
515
516impl<K> Packed<K> {
517 pub const ID: &'static str = "baked";
519
520 pub const fn new() -> Self {
522 Self(PhantomData)
523 }
524}
525
526impl<K> Default for Packed<K> {
527 fn default() -> Self {
528 Self::new()
529 }
530}
531
532impl<K> core::fmt::Debug for Packed<K> {
533 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
534 f.write_str("Packed")
535 }
536}
537
538impl<K: AssetKind + DeserializeOwned> Format for Packed<K> {
539 type Asset = K;
540
541 fn id(&self) -> &'static str {
542 Self::ID
543 }
544
545 fn extensions(&self) -> &'static [&'static str] {
546 &[]
547 }
548
549 fn sniff(&self, bytes: &[u8]) -> bool {
550 bytes.starts_with(&BAKED_MAGIC)
551 }
552
553 fn max_bytes(&self) -> usize {
554 MAX_BAKED_BYTES
555 }
556
557 fn decode(
558 &self,
559 file: &AssetPath,
560 bytes: &[u8],
561 _cx: &DecodeCx<'_>,
562 ) -> Result<K, Vec<ContentError>> {
563 let error = |message: String| vec![ContentError::new(file.shown(), message)];
564 let sections =
565 sections(bytes).ok_or_else(|| error("a baked file that is broken".to_owned()))?;
566 let Some(&(_, data)) = sections.iter().find(|(kind, _)| *kind == K::NAME) else {
567 let kinds: Vec<&str> = sections.iter().map(|&(kind, _)| kind).collect();
568 return Err(error(format!(
569 "baked as {} and not as a {}: bake the {} kind too",
570 kinds.join(", "),
571 K::NAME,
572 K::NAME
573 )));
574 };
575 match postcard::take_from_bytes::<K>(data) {
576 Ok((asset, [])) => Ok(asset),
577 _ => Err(error(format!(
578 "its baked {} doesn't read as this game's: bake it again with `shuyi bake`",
579 K::NAME
580 ))),
581 }
582 }
583}
584
585#[cfg(test)]
586mod one_bit_tests;
587#[cfg(test)]
588mod tests;