1#![allow(
19 clippy::disallowed_methods,
20 clippy::disallowed_types,
21 reason = "reader I/O: reads bundle files"
22)]
23
24use std::fmt;
25use std::io::{Read, Seek, SeekFrom};
26use std::path::{Path, PathBuf};
27use std::sync::{Arc, Mutex, PoisonError};
28
29use primase_foundations::compress::{Decompressor, Lz4};
30
31use crate::reader::Reader;
32use crate::{Error, Result, invalid};
33
34const MAGIC: &[u8; 8] = b"UnityFS\0";
35
36pub const FORMAT_VERSION: u32 = 8;
38
39const COMPRESSION_MASK: u32 = 0x3f;
41const TABLE_WITH_DIRECTORY: u32 = 0x40;
43const TABLE_AT_END: u32 = 0x80;
45const PAD_BEFORE_DATA: u32 = 0x200;
47
48pub const NODE_SERIALIZED: u32 = 4;
50
51const CACHE_BLOCKS: usize = 16;
53
54#[derive(Clone, Debug, PartialEq, Eq)]
56pub struct BundleHeader {
57 pub format_version: u32,
59 pub player_version: String,
61 pub engine_version: String,
63 pub size: u64,
65 pub table_compressed: u32,
67 pub table_uncompressed: u32,
69 pub flags: u32,
71}
72
73#[derive(Clone, Copy, Debug, PartialEq, Eq)]
75pub struct Block {
76 pub uncompressed: u32,
78 pub compressed: u32,
80 pub flags: u16,
82}
83
84#[derive(Clone, Debug, PartialEq, Eq)]
86pub struct BundleNode {
87 pub path: String,
89 pub offset: u64,
91 pub size: u64,
93 pub flags: u32,
95}
96
97impl BundleNode {
98 pub fn is_serialized(&self) -> bool {
100 self.flags & NODE_SERIALIZED != 0
101 }
102}
103
104pub trait BundleSource: Read + Seek + Send {}
106impl<T: Read + Seek + Send> BundleSource for T {}
107
108#[derive(Clone, Copy, Debug)]
110struct Placed {
111 block: Block,
112 data_start: u64,
113 file_start: u64,
114}
115
116pub struct Bundle {
118 path: PathBuf,
119 header: BundleHeader,
120 hash: [u8; 16],
121 nodes: Vec<BundleNode>,
122 blocks: Vec<Placed>,
123 data_len: u64,
124 source: Mutex<Box<dyn BundleSource>>,
125 recent: Mutex<Vec<(usize, Arc<[u8]>)>>,
126}
127
128impl fmt::Debug for Bundle {
129 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
130 f.debug_struct("Bundle")
131 .field("path", &self.path)
132 .field("header", &self.header)
133 .field("nodes", &self.nodes.len())
134 .field("blocks", &self.blocks.len())
135 .finish()
136 }
137}
138
139fn unsupported_compression(what: &str, code: u32) -> Error {
140 let name = match code {
141 1 => "LZMA",
142 4 => "LZHAM",
143 _ => "an unknown compression",
144 };
145 Error::Unsupported(format!(
146 "{what} compressed with {name} (compression {code}); stored, LZ4 and LZ4HC are read"
147 ))
148}
149
150fn inflate(what: &str, compression: u32, bytes: &[u8], expected: usize) -> Result<Vec<u8>> {
152 match compression {
153 0 => {
154 if bytes.len() == expected {
155 Ok(bytes.to_vec())
156 } else {
157 Err(invalid(format!(
158 "{what} is stored with {} bytes, its size is {expected}",
159 bytes.len()
160 )))
161 }
162 }
163 2 | 3 => Lz4
164 .decompress(bytes, expected)
165 .map_err(|e| invalid(format!("{what}: LZ4: {e}"))),
166 c => Err(unsupported_compression(what, c)),
167 }
168}
169
170impl Bundle {
171 pub fn open(path: &Path) -> Result<Self> {
174 let io = |source| Error::Io {
175 path: path.to_path_buf(),
176 source,
177 };
178 let file = std::fs::File::open(path).map_err(io)?;
179 Self::from_source(path, Box::new(file))
180 }
181
182 pub fn from_source(path: &Path, mut source: Box<dyn BundleSource>) -> Result<Self> {
184 let named = |e: Error| match e {
185 Error::Invalid(m) => invalid(format!("{}: {m}", path.display())),
186 Error::Unsupported(m) => Error::Unsupported(format!("{}: {m}", path.display())),
187 other => other,
188 };
189 Self::parse(path, &mut source)
190 .map_err(named)
191 .map(|parsed| Bundle {
192 path: path.to_path_buf(),
193 header: parsed.header,
194 hash: parsed.hash,
195 nodes: parsed.nodes,
196 blocks: parsed.blocks,
197 data_len: parsed.data_len,
198 source: Mutex::new(source),
199 recent: Mutex::new(Vec::new()),
200 })
201 }
202
203 fn parse(path: &Path, source: &mut Box<dyn BundleSource>) -> Result<Parsed> {
204 let io = |source| Error::Io {
205 path: path.to_path_buf(),
206 source,
207 };
208 let len = source.seek(SeekFrom::End(0)).map_err(io)?;
209 source.seek(SeekFrom::Start(0)).map_err(io)?;
210 let mut head = Vec::new();
212 source.by_ref().take(1024).read_to_end(&mut head).map_err(io)?;
213 let mut r = Reader::new(&head, true);
214 if r.take(MAGIC.len()).ok() != Some(MAGIC.as_slice()) {
215 return Err(invalid("not a UnityFS bundle (signature)"));
216 }
217 let format_version = r.u32()?;
218 if format_version != FORMAT_VERSION {
219 return Err(Error::Unsupported(format!(
220 "UnityFS format version {format_version} (read: {FORMAT_VERSION})"
221 )));
222 }
223 let player_version = r.cstr()?;
224 let engine_version = r.cstr()?;
225 let size = r.u64()?;
226 let table_compressed = r.u32()?;
227 let table_uncompressed = r.u32()?;
228 let flags = r.u32()?;
229 let header = BundleHeader {
230 format_version,
231 player_version,
232 engine_version,
233 size,
234 table_compressed,
235 table_uncompressed,
236 flags,
237 };
238 if size != len {
239 return Err(invalid(format!(
240 "the header says {size} bytes, the file has {len}"
241 )));
242 }
243 let known = COMPRESSION_MASK | TABLE_WITH_DIRECTORY | TABLE_AT_END | PAD_BEFORE_DATA;
244 if flags & !known != 0 {
245 return Err(Error::Unsupported(format!(
246 "UnityFS header flags {flags:#x} (read: {TABLE_WITH_DIRECTORY:#x} plus a compression, with or without \
247 {PAD_BEFORE_DATA:#x})"
248 )));
249 }
250 if flags & TABLE_AT_END != 0 {
251 return Err(Error::Unsupported(format!(
252 "UnityFS header flags {flags:#x}: the block table at the end of the file (no bundle read has it)"
253 )));
254 }
255 if flags & TABLE_WITH_DIRECTORY == 0 {
256 return Err(Error::Unsupported(format!(
257 "UnityFS header flags {flags:#x}: the block table not stored with the directory"
258 )));
259 }
260 let table_at = u64::try_from(r.pos)
262 .ok()
263 .and_then(|p| p.checked_next_multiple_of(16))
264 .ok_or_else(|| invalid("header too long"))?;
265 let table_end = table_at
266 .checked_add(u64::from(table_compressed))
267 .filter(|&e| e <= len)
268 .ok_or_else(|| {
269 invalid(format!(
270 "the block table ({table_compressed} bytes at {table_at}) is past the file's {len} bytes"
271 ))
272 })?;
273 source.seek(SeekFrom::Start(table_at)).map_err(io)?;
274 let mut packed = vec![0; usize::try_from(table_compressed).map_err(|_| invalid("block table size"))?];
275 source.read_exact(&mut packed).map_err(io)?;
276 let table = inflate(
277 "the block table",
278 flags & COMPRESSION_MASK,
279 &packed,
280 usize::try_from(table_uncompressed).map_err(|_| invalid("block table size"))?,
281 )?;
282 let mut t = Reader::new(&table, true);
283 let hash: [u8; 16] = t
284 .take(16)?
285 .try_into()
286 .map_err(|_| invalid("the block table's hash"))?;
287 let block_count = t.i32()?;
288 let block_count = usize::try_from(block_count)
289 .ok()
290 .filter(|&n| n <= t.remaining() / 10)
291 .ok_or_else(|| {
292 invalid(format!(
293 "{block_count} blocks in a block table of {} bytes",
294 table.len()
295 ))
296 })?;
297 let data_at = if flags & PAD_BEFORE_DATA != 0 {
298 table_end
299 .checked_next_multiple_of(16)
300 .ok_or_else(|| invalid("data offset"))?
301 } else {
302 table_end
303 };
304 let mut blocks = Vec::with_capacity(block_count);
305 let (mut data_len, mut file_at) = (0u64, data_at);
306 for i in 0..block_count {
307 let block = Block {
308 uncompressed: t.u32()?,
309 compressed: t.u32()?,
310 flags: t.u16()?,
311 };
312 let compression = u32::from(block.flags) & COMPRESSION_MASK;
313 if !matches!(compression, 0 | 2 | 3) {
314 return Err(unsupported_compression(&format!("block {i}"), compression));
315 }
316 if compression == 0 && block.compressed != block.uncompressed {
317 return Err(invalid(format!(
318 "block {i} is stored with {} bytes, its size is {}",
319 block.compressed, block.uncompressed
320 )));
321 }
322 blocks.push(Placed {
323 block,
324 data_start: data_len,
325 file_start: file_at,
326 });
327 data_len = data_len
328 .checked_add(u64::from(block.uncompressed))
329 .ok_or_else(|| invalid("blocks add up past 2^64 bytes"))?;
330 file_at = file_at
331 .checked_add(u64::from(block.compressed))
332 .ok_or_else(|| invalid("blocks add up past 2^64 bytes"))?;
333 }
334 if file_at != len {
335 return Err(invalid(format!(
336 "the blocks end at {file_at} (data at {data_at}), the file has {len} bytes"
337 )));
338 }
339 let node_count = t.i32()?;
340 let node_count = usize::try_from(node_count)
341 .ok()
342 .filter(|&n| n <= t.remaining() / 21)
343 .ok_or_else(|| invalid(format!("{node_count} nodes in {} bytes", t.remaining())))?;
344 let mut nodes: Vec<BundleNode> = Vec::with_capacity(node_count);
345 for i in 0..node_count {
346 let offset = t.i64()?;
347 let size = t.i64()?;
348 let flags = t.u32()?;
349 let path = t.cstr()?;
350 let (offset, size) = u64::try_from(offset)
351 .ok()
352 .zip(u64::try_from(size).ok())
353 .ok_or_else(|| invalid(format!("node {i} ({path}): offset {offset}, size {size}")))?;
354 if offset.checked_add(size).is_none_or(|end| end > data_len) {
355 return Err(invalid(format!(
356 "node {i} ({path}): bytes {offset}+{size} are outside the blocks' {data_len} bytes"
357 )));
358 }
359 if path.is_empty() || nodes.iter().any(|n| n.path == path) {
360 return Err(invalid(format!(
361 "node {i}: the path {path:?} is empty or twice in the bundle"
362 )));
363 }
364 nodes.push(BundleNode {
365 path,
366 offset,
367 size,
368 flags,
369 });
370 }
371 if t.remaining() != 0 {
372 return Err(invalid(format!(
373 "{} bytes after the nodes in the block table",
374 t.remaining()
375 )));
376 }
377 Ok(Parsed {
378 header,
379 hash,
380 nodes,
381 blocks,
382 data_len,
383 })
384 }
385
386 pub fn path(&self) -> &Path {
388 &self.path
389 }
390
391 pub fn header(&self) -> &BundleHeader {
393 &self.header
394 }
395
396 pub fn hash(&self) -> [u8; 16] {
398 self.hash
399 }
400
401 pub fn nodes(&self) -> &[BundleNode] {
403 &self.nodes
404 }
405
406 pub fn node(&self, path: &str) -> Option<&BundleNode> {
408 self.nodes.iter().find(|n| n.path == path)
409 }
410
411 pub fn blocks(&self) -> impl ExactSizeIterator<Item = Block> + '_ {
413 self.blocks.iter().map(|p| p.block)
414 }
415
416 pub fn data_len(&self) -> u64 {
418 self.data_len
419 }
420
421 fn inflate_block(&self, i: usize) -> Result<Vec<u8>> {
423 let placed = self
424 .blocks
425 .get(i)
426 .ok_or_else(|| invalid(format!("block {i} of {}", self.blocks.len())))?;
427 let mut packed =
428 vec![0; usize::try_from(placed.block.compressed).map_err(|_| invalid("block size"))?];
429 {
430 let io = |source| Error::Io {
431 path: self.path.clone(),
432 source,
433 };
434 let mut source = self.source.lock().unwrap_or_else(PoisonError::into_inner);
435 source.seek(SeekFrom::Start(placed.file_start)).map_err(io)?;
436 source.read_exact(&mut packed).map_err(io)?;
437 }
438 inflate(
439 &format!("{}: block {i}", self.path.display()),
440 u32::from(placed.block.flags) & COMPRESSION_MASK,
441 &packed,
442 usize::try_from(placed.block.uncompressed).map_err(|_| invalid("block size"))?,
443 )
444 }
445
446 fn block(&self, i: usize) -> Result<Arc<[u8]>> {
448 let mut recent = self.recent.lock().unwrap_or_else(PoisonError::into_inner);
449 if let Some(at) = recent.iter().position(|(n, _)| *n == i) {
450 let hit = recent.remove(at);
451 let data = hit.1.clone();
452 recent.push(hit);
453 return Ok(data);
454 }
455 drop(recent);
456 let data: Arc<[u8]> = self.inflate_block(i)?.into();
457 let mut recent = self.recent.lock().unwrap_or_else(PoisonError::into_inner);
458 if recent.len() >= CACHE_BLOCKS {
459 recent.remove(0);
460 }
461 recent.push((i, data.clone()));
462 Ok(data)
463 }
464
465 pub fn read(&self, node: &BundleNode, offset: u64, len: usize) -> Result<Vec<u8>> {
468 let outside = || {
469 invalid(format!(
470 "{}: {}: bytes {offset}+{len} are outside the node's {} bytes",
471 self.path.display(),
472 node.path,
473 node.size
474 ))
475 };
476 let want = u64::try_from(len).map_err(|_| outside())?;
477 if offset.checked_add(want).is_none_or(|end| end > node.size) {
478 return Err(outside());
479 }
480 let start = node.offset.checked_add(offset).ok_or_else(outside)?;
481 let end = start.checked_add(want).ok_or_else(outside)?;
482 let mut out = Vec::new();
483 out.try_reserve_exact(len)
484 .map_err(|_| invalid(format!("{len} bytes do not fit memory")))?;
485 let mut i = self
486 .blocks
487 .partition_point(|p| p.data_start.saturating_add(u64::from(p.block.uncompressed)) <= start);
488 while out.len() < len {
489 let placed = self.blocks.get(i).ok_or_else(|| {
490 invalid(format!(
491 "{}: ran out of blocks at {}",
492 self.path.display(),
493 out.len()
494 ))
495 })?;
496 let data = self.block(i)?;
497 let from = start.max(placed.data_start).saturating_sub(placed.data_start);
498 let to = end
499 .min(
500 placed
501 .data_start
502 .saturating_add(u64::from(placed.block.uncompressed)),
503 )
504 .saturating_sub(placed.data_start);
505 let slice = usize::try_from(from)
506 .ok()
507 .zip(usize::try_from(to).ok())
508 .and_then(|(f, t)| data.get(f..t))
509 .ok_or_else(|| {
510 invalid(format!(
511 "{}: block {i} is shorter than its size",
512 self.path.display()
513 ))
514 })?;
515 out.extend_from_slice(slice);
516 i = i.saturating_add(1);
517 }
518 Ok(out)
519 }
520
521 pub fn read_node(&self, node: &BundleNode) -> Result<Vec<u8>> {
523 let len = usize::try_from(node.size)
524 .map_err(|_| invalid(format!("{}: {} bytes do not fit memory", node.path, node.size)))?;
525 let mut out = Vec::new();
526 out.try_reserve_exact(len)
527 .map_err(|_| invalid(format!("{}: {len} bytes do not fit memory", node.path)))?;
528 let start = node.offset;
529 let end = start
530 .checked_add(node.size)
531 .ok_or_else(|| invalid("node past 2^64 bytes"))?;
532 let first = self
533 .blocks
534 .partition_point(|p| p.data_start.saturating_add(u64::from(p.block.uncompressed)) <= start);
535 for i in first..self.blocks.len() {
536 let Some(placed) = self.blocks.get(i) else { break };
537 if placed.data_start >= end {
538 break;
539 }
540 let data = self.inflate_block(i)?;
541 let from = start.max(placed.data_start).saturating_sub(placed.data_start);
542 let to = end
543 .min(
544 placed
545 .data_start
546 .saturating_add(u64::from(placed.block.uncompressed)),
547 )
548 .saturating_sub(placed.data_start);
549 let slice = usize::try_from(from)
550 .ok()
551 .zip(usize::try_from(to).ok())
552 .and_then(|(f, t)| data.get(f..t))
553 .ok_or_else(|| {
554 invalid(format!(
555 "{}: block {i} is shorter than its size",
556 self.path.display()
557 ))
558 })?;
559 out.extend_from_slice(slice);
560 }
561 if out.len() != len {
562 return Err(invalid(format!(
563 "{}: {} read {} of its {len} bytes",
564 self.path.display(),
565 node.path,
566 out.len()
567 )));
568 }
569 Ok(out)
570 }
571}
572
573struct Parsed {
575 header: BundleHeader,
576 hash: [u8; 16],
577 nodes: Vec<BundleNode>,
578 blocks: Vec<Placed>,
579 data_len: u64,
580}
581
582#[cfg(test)]
583mod tests {
584 use super::*;
585 use std::io::Cursor;
586
587 fn literal_lz4(data: &[u8]) -> Vec<u8> {
589 assert!(data.len() >= 15);
590 let mut out = vec![0xf0];
591 let mut rest = data.len() - 15;
592 while rest >= 255 {
593 out.push(255);
594 rest -= 255;
595 }
596 out.push(u8::try_from(rest).unwrap());
597 out.extend(data);
598 out
599 }
600
601 fn build(
604 blocks: &[(&[u8], u32)],
605 nodes: &[(i64, i64, u32, &str)],
606 table_compression: u32,
607 flags: u32,
608 ) -> Vec<u8> {
609 let mut table = vec![7u8; 16];
610 table.extend((blocks.len() as i32).to_be_bytes());
611 let mut data = Vec::new();
612 for (bytes, compression) in blocks {
613 let stored = match compression {
614 0 => bytes.to_vec(),
615 _ => literal_lz4(bytes),
616 };
617 table.extend((bytes.len() as u32).to_be_bytes());
618 table.extend((stored.len() as u32).to_be_bytes());
619 table.extend((*compression as u16).to_be_bytes());
620 data.extend(stored);
621 }
622 table.extend((nodes.len() as i32).to_be_bytes());
623 for (offset, size, flags, path) in nodes {
624 table.extend(offset.to_be_bytes());
625 table.extend(size.to_be_bytes());
626 table.extend(flags.to_be_bytes());
627 table.extend(path.as_bytes());
628 table.push(0);
629 }
630 let packed = if table_compression == 0 {
631 table.clone()
632 } else {
633 literal_lz4(&table)
634 };
635 let mut out = b"UnityFS\0".to_vec();
636 out.extend(8u32.to_be_bytes());
637 out.extend(b"5.x.x\0");
638 out.extend(b"6000.3.15f1\0");
639 let size_at = out.len();
640 out.extend([0; 8]);
641 out.extend((packed.len() as u32).to_be_bytes());
642 out.extend((table.len() as u32).to_be_bytes());
643 out.extend(flags.to_be_bytes());
644 out.resize(out.len().next_multiple_of(16), 0);
645 out.extend(packed);
646 if flags & PAD_BEFORE_DATA != 0 {
647 out.resize(out.len().next_multiple_of(16), 0);
648 }
649 out.extend(data);
650 let total = out.len() as u64;
651 out[size_at..size_at + 8].copy_from_slice(&total.to_be_bytes());
652 out
653 }
654
655 fn open(bytes: Vec<u8>) -> Result<Bundle> {
656 Bundle::from_source(Path::new("test.bundle"), Box::new(Cursor::new(bytes)))
657 }
658
659 fn sample() -> Vec<u8> {
660 let a: Vec<u8> = (0..40u8).collect();
661 let b: Vec<u8> = (100..130u8).collect();
662 let c: Vec<u8> = (200..240u8).collect();
663 build(
664 &[(&a, 3), (&b, 0), (&c, 2)],
665 &[
666 (0, 25, 4, "first"),
667 (25, 70, 0, "Resources/second"),
668 (95, 15, 4, "third"),
669 ],
670 3,
671 0x243,
672 )
673 }
674
675 #[test]
677 fn nodes_read_whole_and_by_range_across_blocks() {
678 let bundle = open(sample()).unwrap();
679 assert_eq!(bundle.header().engine_version, "6000.3.15f1");
680 assert_eq!(bundle.header().flags, 0x243);
681 assert_eq!(bundle.blocks().len(), 3);
682 assert_eq!(bundle.data_len(), 110);
683 let all: Vec<u8> = (0..40u8).chain(100..130).chain(200..240).collect();
684 for node in bundle.nodes() {
685 let (from, size) = (node.offset as usize, node.size as usize);
686 assert_eq!(
687 bundle.read_node(node).unwrap(),
688 all[from..from + size],
689 "{}",
690 node.path
691 );
692 assert_eq!(bundle.read(node, 0, size).unwrap(), all[from..from + size]);
693 for (offset, len) in [(0, 0), (1, size - 1), (size / 2, size - size / 2), (size, 0)] {
694 assert_eq!(
695 bundle.read(node, offset as u64, len).unwrap(),
696 all[from + offset..from + offset + len],
697 "{} {offset}+{len}",
698 node.path
699 );
700 }
701 assert!(bundle.read(node, 1, size).is_err());
702 }
703 assert!(bundle.node("Resources/second").unwrap().flags == 0);
704 assert!(bundle.node("third").unwrap().is_serialized());
705 assert!(bundle.node("missing").is_none());
706 }
707
708 #[test]
710 fn a_stored_table_and_unpadded_data_are_read() {
711 let a: Vec<u8> = (0..40u8).collect();
712 let bytes = build(&[(&a, 3)], &[(5, 20, 4, "only")], 0, 0x40);
713 let bundle = open(bytes).unwrap();
714 assert_eq!(
715 bundle.read_node(bundle.node("only").unwrap()).unwrap(),
716 (5..25u8).collect::<Vec<_>>()
717 );
718 }
719
720 #[test]
722 fn refusals_name_their_reason() {
723 let good = sample();
724 let with_flags = |flags: u32| {
725 let a: Vec<u8> = (0..40u8).collect();
726 open(build(&[(&a, 3)], &[(0, 10, 4, "n")], 3, flags))
727 };
728 for (flags, needle) in [
729 (0x241, "LZMA"),
730 (0x244, "LZHAM"),
731 (0x2c3, "end of the file"),
732 (0x203, "not stored with the directory"),
733 (0x1243, "header flags"),
734 ] {
735 let e = with_flags(flags).unwrap_err();
736 assert!(matches!(e, Error::Unsupported(_)), "{flags:#x}: {e}");
737 assert!(e.to_string().contains(needle), "{flags:#x}: {e}");
738 }
739 let mut v7 = good.clone();
740 v7[11] = 7;
741 let e = open(v7).unwrap_err();
742 assert!(
743 matches!(e, Error::Unsupported(_)) && e.to_string().contains("format version 7"),
744 "{e}"
745 );
746 let mut magic = good.clone();
747 magic[0] = b'X';
748 assert!(open(magic).unwrap_err().to_string().contains("signature"));
749 let mut cut = good.clone();
750 cut.pop();
751 let e = open(cut).unwrap_err().to_string();
752 assert!(e.contains("the header says"), "{e}");
753 let mut extra = good.clone();
754 extra.push(0);
755 assert!(open(extra).unwrap_err().to_string().contains("the header says"));
756 let a: Vec<u8> = (0..40u8).collect();
757 let outside = open(build(&[(&a, 3)], &[(30, 20, 4, "n")], 3, 0x243))
758 .unwrap_err()
759 .to_string();
760 assert!(outside.contains("outside the blocks"), "{outside}");
761 let twice = open(build(&[(&a, 3)], &[(0, 1, 4, "n"), (1, 1, 4, "n")], 3, 0x243))
762 .unwrap_err()
763 .to_string();
764 assert!(twice.contains("twice"), "{twice}");
765 let negative = open(build(&[(&a, 3)], &[(-1, 1, 4, "n")], 3, 0x243))
766 .unwrap_err()
767 .to_string();
768 assert!(negative.contains("offset -1"), "{negative}");
769 }
770
771 #[test]
773 fn a_block_cannot_claim_more_than_its_lz4_can_make() {
774 let a: Vec<u8> = (0..40u8).collect();
775 let mut bytes = build(&[(&a, 3)], &[(0, 10, 4, "n")], 0, 0x240);
776 let table_at = bytes.windows(16).position(|w| w == [7; 16]).unwrap();
778 let size_at = table_at + 16 + 4;
779 bytes[size_at..size_at + 4].copy_from_slice(&u32::MAX.to_be_bytes());
780 let bundle = open(bytes).unwrap();
781 let e = bundle
782 .read(bundle.node("n").unwrap(), 0, 1)
783 .unwrap_err()
784 .to_string();
785 assert!(e.contains("lz4") || e.contains("LZ4"), "{e}");
786 }
787}