1pub mod crypt;
41pub mod encoding;
42pub mod header;
43pub mod table;
44pub mod writer;
45
46pub use table::{Entry, GRF_FLAG_DES, GRF_FLAG_FILE, GRF_FLAG_MIXCRYPT};
47
48use std::collections::BTreeMap;
49use std::io::Read;
50
51use crate::Result;
52use crate::block::BlockDevice;
53use crate::fs::{FileMeta, FileSource, MutationCapability};
54
55pub(crate) const HEADER_SIZE: usize = 0x2e;
56
57#[derive(Debug, Clone)]
62pub struct FormatOpts {
63 pub version: u32,
66 pub compression_level: u32,
68}
69
70impl Default for FormatOpts {
71 fn default() -> Self {
72 Self {
73 version: 0x200,
74 compression_level: 6,
75 }
76 }
77}
78
79impl FormatOpts {
80 pub fn apply_options(&mut self, map: &mut crate::format_opts::OptionMap) -> crate::Result<()> {
84 if let Some(v) = map.take_u32("version")? {
85 self.version = v;
86 }
87 if let Some(n) = map.take_u32("compression_level")? {
88 if n > 9 {
89 return Err(crate::Error::InvalidImage(format!(
90 "compression_level {n} out of range (0..=9)"
91 )));
92 }
93 self.compression_level = n;
94 }
95 Ok(())
96 }
97}
98
99pub struct Grf {
101 pub version: u32,
102 pub table_offset: u32,
103 pub seed: u32,
104 pub encrypted_header: bool,
105 pub entries: BTreeMap<String, Entry>,
108 data_end: u64,
111 wasted_space: u64,
115 dirty: bool,
118 fresh: bool,
122 image_end: u64,
126}
127
128impl Grf {
129 pub fn format_with(_dev: &mut dyn BlockDevice, opts: &FormatOpts) -> Result<Self> {
133 if opts.version != 0x200 {
134 return Err(crate::Error::Unsupported(format!(
135 "grf: writer only emits v0x200 (asked for {:#x})",
136 opts.version
137 )));
138 }
139 Ok(Self {
140 version: opts.version,
141 table_offset: 0,
142 seed: 0,
143 encrypted_header: false,
144 entries: BTreeMap::new(),
145 data_end: HEADER_SIZE as u64,
146 wasted_space: 0,
147 dirty: true,
148 fresh: true,
149 image_end: 0,
150 })
151 }
152
153 pub fn open_dev(dev: &mut dyn BlockDevice) -> Result<Self> {
156 let mut head_buf = [0u8; HEADER_SIZE];
157 dev.read_at(0, &mut head_buf)?;
158 let head = header::Header::decode(&head_buf)?;
159
160 let table_abs = head.table_offset as u64 + HEADER_SIZE as u64;
161 let entries = read_table(dev, table_abs, head.version, head.filecount)?;
162
163 let mut data_end = HEADER_SIZE as u64;
167 for e in entries.values() {
168 let end = HEADER_SIZE as u64 + e.pos as u64 + e.len_aligned as u64;
169 if end > data_end {
170 data_end = end;
171 }
172 }
173
174 let wasted_space = table_abs.saturating_sub(data_end);
179
180 Ok(Self {
181 version: head.version,
182 table_offset: head.table_offset,
183 seed: head.seed,
184 encrypted_header: head.encrypted_header,
185 entries,
186 data_end,
187 wasted_space,
188 dirty: false,
189 fresh: false,
190 image_end: 0,
191 })
192 }
193
194 pub fn read_entry(&self, dev: &mut dyn BlockDevice, entry: &Entry) -> Result<Vec<u8>> {
197 let abs = HEADER_SIZE as u64 + entry.pos as u64;
198 let dev_size = dev.total_size();
203 let end = abs.saturating_add(u64::from(entry.len_aligned));
204 if end > dev_size {
205 return Err(crate::Error::InvalidImage(format!(
206 "grf: entry body (pos {} len_aligned {}) past end of device size {}",
207 entry.pos, entry.len_aligned, dev_size
208 )));
209 }
210 let mut comp = vec![0u8; entry.len_aligned as usize];
211 if entry.len_aligned > 0 {
212 dev.read_at(abs, &mut comp)?;
213 }
214 if let Some(cycle) = entry.crypto_cycle() {
215 let flag_type = if cycle == 0 { 1 } else { 0 };
218 crypt::decode_des_etc(&mut comp, flag_type, cycle);
219 }
220 let comp_slice = comp.get(..entry.len as usize).ok_or_else(|| {
221 crate::Error::InvalidImage(format!(
222 "grf: entry compressed len {} exceeds aligned buffer {}",
223 entry.len, entry.len_aligned
224 ))
225 })?;
226 let plain = crate::compression::decompress(
227 crate::compression::Algo::Zlib,
228 comp_slice,
229 entry.size as usize,
230 )?;
231 Ok(plain)
232 }
233
234 pub fn wasted_space(&self) -> u64 {
237 self.wasted_space
238 }
239}
240
241fn read_table(
242 dev: &mut dyn BlockDevice,
243 table_abs: u64,
244 version: u32,
245 filecount: u32,
246) -> Result<BTreeMap<String, Entry>> {
247 if filecount == 0 {
248 return Ok(BTreeMap::new());
249 }
250
251 let dev_size = dev.total_size();
252 if table_abs >= dev_size {
253 return Err(crate::Error::InvalidImage(
254 "grf: table offset past end of file".into(),
255 ));
256 }
257
258 let entries = match version {
259 0x102 | 0x103 => {
260 let table = read_compressed_table(dev, table_abs, true)?;
267 table::decode_v102(&table)?
268 }
269 0x200 => {
270 let table = read_compressed_table(dev, table_abs, false)?;
271 table::decode_v200(&table)?
272 }
273 other => {
274 return Err(crate::Error::Unsupported(format!(
275 "grf: cannot read table for version {other:#x}"
276 )));
277 }
278 };
279
280 let mut map = BTreeMap::new();
281 for e in entries {
282 map.insert(normalise_path(&e.name), e);
283 }
284 Ok(map)
285}
286
287fn read_compressed_table(
288 dev: &mut dyn BlockDevice,
289 table_abs: u64,
290 legacy_framing: bool,
291) -> Result<Vec<u8>> {
292 let dev_size = dev.total_size();
293 let mut posinfo = [0u8; 8];
294 if table_abs + 8 > dev_size {
295 return Err(crate::Error::InvalidImage(
296 "grf: table header truncated".into(),
297 ));
298 }
299 dev.read_at(table_abs, &mut posinfo)?;
300 let comp_size = u32::from_le_bytes(posinfo[0..4].try_into().unwrap()) as usize;
301 let uncomp_size = u32::from_le_bytes(posinfo[4..8].try_into().unwrap()) as usize;
302
303 let comp_start = table_abs + 8;
304 if comp_start + comp_size as u64 > dev_size {
305 return Err(crate::Error::InvalidImage(
306 "grf: compressed table payload past end of file".into(),
307 ));
308 }
309 let mut comp = vec![0u8; comp_size];
310 dev.read_at(comp_start, &mut comp)?;
311
312 let _ = legacy_framing;
316
317 crate::compression::decompress(crate::compression::Algo::Zlib, &comp, uncomp_size)
318}
319
320fn normalise_path(s: &str) -> String {
323 s.trim_start_matches('/').to_string()
324}
325
326impl crate::fs::FilesystemFactory for Grf {
327 type FormatOpts = FormatOpts;
328
329 fn format(dev: &mut dyn BlockDevice, opts: &Self::FormatOpts) -> Result<Self> {
330 Self::format_with(dev, opts)
331 }
332
333 fn open(dev: &mut dyn BlockDevice) -> Result<Self> {
334 Self::open_dev(dev)
335 }
336}
337
338impl crate::fs::Filesystem for Grf {
339 fn create_file(
340 &mut self,
341 dev: &mut dyn BlockDevice,
342 path: &std::path::Path,
343 src: FileSource,
344 _meta: FileMeta,
345 ) -> Result<()> {
346 let key = normalise_path(
347 path.to_str()
348 .ok_or_else(|| crate::Error::InvalidArgument("grf: non-UTF-8 path".into()))?,
349 );
350 writer::add_file(self, dev, key, src)
351 }
352
353 fn create_file_streaming(
358 &mut self,
359 dev: &mut dyn BlockDevice,
360 path: &std::path::Path,
361 body: &mut dyn std::io::Read,
362 len: u64,
363 _meta: FileMeta,
364 ) -> Result<()> {
365 let key = normalise_path(
366 path.to_str()
367 .ok_or_else(|| crate::Error::InvalidArgument("grf: non-UTF-8 path".into()))?,
368 );
369 let mut plain = Vec::with_capacity(len.min(64 * 1024 * 1024) as usize);
370 std::io::Read::take(body, len).read_to_end(&mut plain)?;
371 writer::add_file_bytes(self, dev, key, plain)
372 }
373
374 fn create_dir(
375 &mut self,
376 _dev: &mut dyn BlockDevice,
377 _path: &std::path::Path,
378 _meta: FileMeta,
379 ) -> Result<()> {
380 Ok(())
384 }
385
386 fn create_symlink(
387 &mut self,
388 _dev: &mut dyn BlockDevice,
389 _path: &std::path::Path,
390 _target: &std::path::Path,
391 _meta: FileMeta,
392 ) -> Result<()> {
393 Err(crate::Error::Unsupported(
394 "grf: symlinks are not part of the archive format".into(),
395 ))
396 }
397
398 fn create_device(
399 &mut self,
400 _dev: &mut dyn BlockDevice,
401 _path: &std::path::Path,
402 _kind: crate::fs::DeviceKind,
403 _major: u32,
404 _minor: u32,
405 _meta: FileMeta,
406 ) -> Result<()> {
407 Err(crate::Error::Unsupported(
408 "grf: device nodes are not part of the archive format".into(),
409 ))
410 }
411
412 fn remove(&mut self, _dev: &mut dyn BlockDevice, path: &std::path::Path) -> Result<()> {
413 let key = normalise_path(
414 path.to_str()
415 .ok_or_else(|| crate::Error::InvalidArgument("grf: non-UTF-8 path".into()))?,
416 );
417 writer::remove(self, &key)
418 }
419
420 fn list(
421 &mut self,
422 _dev: &mut dyn BlockDevice,
423 path: &std::path::Path,
424 ) -> Result<Vec<crate::fs::DirEntry>> {
425 let prefix = {
426 let s = path
427 .to_str()
428 .ok_or_else(|| crate::Error::InvalidArgument("grf: non-UTF-8 path".into()))?;
429 let trimmed = s.trim_start_matches('/').trim_end_matches('/');
430 if trimmed.is_empty() {
431 String::new()
432 } else {
433 format!("{trimmed}/")
434 }
435 };
436
437 use std::collections::BTreeMap as B;
441 let mut children: B<String, crate::fs::EntryKind> = B::new();
442 let mut sizes: B<String, u64> = B::new();
443 for (name, entry) in &self.entries {
444 let Some(tail) = name.strip_prefix(&prefix) else {
445 continue;
446 };
447 if tail.is_empty() {
448 continue;
449 }
450 if let Some((leaf, _)) = tail.split_once('/') {
451 children.insert(leaf.to_string(), crate::fs::EntryKind::Dir);
452 sizes.insert(leaf.to_string(), 0);
453 } else {
454 children.insert(tail.to_string(), crate::fs::EntryKind::Regular);
455 sizes.insert(tail.to_string(), entry.size as u64);
456 }
457 }
458 Ok(children
459 .into_iter()
460 .map(|(name, kind)| {
461 let size = *sizes.get(&name).unwrap_or(&0);
462 crate::fs::DirEntry {
463 name,
464 inode: 0,
465 kind,
466 size,
467 }
468 })
469 .collect())
470 }
471
472 fn read_file<'a>(
473 &'a mut self,
474 dev: &'a mut dyn BlockDevice,
475 path: &std::path::Path,
476 ) -> Result<Box<dyn Read + 'a>> {
477 let key = normalise_path(
478 path.to_str()
479 .ok_or_else(|| crate::Error::InvalidArgument("grf: non-UTF-8 path".into()))?,
480 );
481 let entry =
482 self.entries.get(&key).cloned().ok_or_else(|| {
483 crate::Error::InvalidArgument(format!("grf: no entry at {key:?}"))
484 })?;
485 let bytes = self.read_entry(dev, &entry)?;
489 Ok(Box::new(std::io::Cursor::new(bytes)))
490 }
491
492 fn open_file_ro<'a>(
493 &'a mut self,
494 _dev: &'a mut dyn BlockDevice,
495 path: &std::path::Path,
496 ) -> Result<Box<dyn crate::fs::FileReadHandle + 'a>> {
497 let key = normalise_path(
504 path.to_str()
505 .ok_or_else(|| crate::Error::InvalidArgument("grf: non-UTF-8 path".into()))?,
506 );
507 if !self.entries.contains_key(&key) {
508 return Err(crate::Error::InvalidArgument(format!(
509 "grf: no entry at {key:?}"
510 )));
511 }
512 Err(crate::Error::Unsupported(format!(
513 "grf: {key:?} is compressed; only forward reads are supported"
514 )))
515 }
516
517 fn flush(&mut self, dev: &mut dyn BlockDevice) -> Result<()> {
518 writer::flush(self, dev)
519 }
520
521 fn image_len(&self) -> Option<u64> {
524 (self.image_end > 0).then_some(self.image_end)
525 }
526
527 fn mutation_capability(&self) -> MutationCapability {
528 MutationCapability::Mutable
529 }
530}
531
532#[cfg(test)]
533mod tests {
534 use super::*;
535 use crate::block::MemoryBackend;
536 use crate::fs::{Filesystem, FilesystemFactory};
537
538 #[test]
539 fn empty_round_trip() {
540 let mut dev = MemoryBackend::new(64 * 1024);
541 let mut grf = Grf::format(&mut dev, &FormatOpts::default()).unwrap();
542 grf.flush(&mut dev).unwrap();
543
544 let reopen = Grf::open(&mut dev).unwrap();
545 assert_eq!(reopen.version, 0x200);
546 assert_eq!(reopen.entries.len(), 0);
547 }
548
549 #[test]
550 fn add_read_round_trip() {
551 let mut dev = MemoryBackend::new(64 * 1024);
552 let mut grf = Grf::format(&mut dev, &FormatOpts::default()).unwrap();
553
554 let body = b"hello, world!";
555 grf.create_file(
556 &mut dev,
557 std::path::Path::new("/data/info.txt"),
558 FileSource::Reader {
559 reader: Box::new(std::io::Cursor::new(body.to_vec())),
560 len: body.len() as u64,
561 },
562 FileMeta::default(),
563 )
564 .unwrap();
565 grf.flush(&mut dev).unwrap();
566
567 let mut reopen = Grf::open(&mut dev).unwrap();
568 assert_eq!(reopen.entries.len(), 1);
569 let entries = reopen
570 .list(&mut dev, std::path::Path::new("/data"))
571 .unwrap();
572 assert!(entries.iter().any(|e| e.name == "info.txt"));
573 let entry = reopen.entries.get("data/info.txt").cloned().unwrap();
574 let bytes = reopen.read_entry(&mut dev, &entry).unwrap();
575 assert_eq!(bytes, body);
576 }
577
578 #[test]
579 fn open_file_ro_refuses_compressed_member() {
580 use std::io::Read;
584 let mut dev = MemoryBackend::new(64 * 1024);
585 let mut grf = Grf::format(&mut dev, &FormatOpts::default()).unwrap();
586 let body: Vec<u8> = (0..1024u32).map(|i| (i & 0xff) as u8).collect();
587 grf.create_file(
588 &mut dev,
589 std::path::Path::new("/blob.bin"),
590 FileSource::Reader {
591 reader: Box::new(std::io::Cursor::new(body.clone())),
592 len: body.len() as u64,
593 },
594 FileMeta::default(),
595 )
596 .unwrap();
597 grf.flush(&mut dev).unwrap();
598
599 let mut grf = Grf::open(&mut dev).unwrap();
600 match grf.open_file_ro(&mut dev, std::path::Path::new("/blob.bin")) {
602 Err(crate::Error::Unsupported(_)) => {}
603 Ok(_) => panic!("open_file_ro must refuse a compressed grf member"),
604 Err(e) => panic!("expected Unsupported, got {e:?}"),
605 }
606 match grf.open_file_ro(&mut dev, std::path::Path::new("/nope.bin")) {
608 Err(crate::Error::InvalidArgument(_)) => {}
609 Ok(_) => panic!("open_file_ro must fail for a missing entry"),
610 Err(e) => panic!("expected InvalidArgument, got {e:?}"),
611 }
612 let mut r = grf
614 .read_file(&mut dev, std::path::Path::new("/blob.bin"))
615 .unwrap();
616 let mut got = Vec::new();
617 r.read_to_end(&mut got).unwrap();
618 assert_eq!(got, body);
619 }
620
621 #[test]
622 fn hangul_filename_round_trip() {
623 let mut dev = MemoryBackend::new(64 * 1024);
624 let mut grf = Grf::format(&mut dev, &FormatOpts::default()).unwrap();
625 grf.create_file(
626 &mut dev,
627 std::path::Path::new("/data/한글.txt"),
628 FileSource::Reader {
629 reader: Box::new(std::io::Cursor::new(b"hi".to_vec())),
630 len: 2,
631 },
632 FileMeta::default(),
633 )
634 .unwrap();
635 grf.flush(&mut dev).unwrap();
636
637 let reopen = Grf::open(&mut dev).unwrap();
638 assert!(reopen.entries.contains_key("data/한글.txt"));
639 }
640
641 #[test]
642 fn remove_marks_wasted_space() {
643 let mut dev = MemoryBackend::new(64 * 1024);
644 let mut grf = Grf::format(&mut dev, &FormatOpts::default()).unwrap();
645 grf.create_file(
646 &mut dev,
647 std::path::Path::new("/a.txt"),
648 FileSource::Reader {
649 reader: Box::new(std::io::Cursor::new(vec![0u8; 4096])),
650 len: 4096,
651 },
652 FileMeta::default(),
653 )
654 .unwrap();
655 grf.create_file(
656 &mut dev,
657 std::path::Path::new("/b.txt"),
658 FileSource::Reader {
659 reader: Box::new(std::io::Cursor::new(vec![0u8; 4096])),
660 len: 4096,
661 },
662 FileMeta::default(),
663 )
664 .unwrap();
665 grf.flush(&mut dev).unwrap();
666
667 let mut reopen = Grf::open(&mut dev).unwrap();
668 reopen
669 .remove(&mut dev, std::path::Path::new("/a.txt"))
670 .unwrap();
671 reopen.flush(&mut dev).unwrap();
672 assert!(reopen.wasted_space() > 0);
673 }
674}