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(
84 &mut self,
85 map: &mut crate::format_opts::OptionMap,
86 ) -> crate::Result<()> {
87 if let Some(v) = map.take_u32("version")? {
88 self.version = v;
89 }
90 if let Some(n) = map.take_u32("compression_level")? {
91 if n > 9 {
92 return Err(crate::Error::InvalidImage(format!(
93 "compression_level {n} out of range (0..=9)"
94 )));
95 }
96 self.compression_level = n;
97 }
98 Ok(())
99 }
100}
101
102pub struct Grf {
104 pub version: u32,
105 pub table_offset: u32,
106 pub seed: u32,
107 pub encrypted_header: bool,
108 pub entries: BTreeMap<String, Entry>,
111 data_end: u64,
114 wasted_space: u64,
118 dirty: bool,
121 fresh: bool,
125}
126
127impl Grf {
128 pub fn format_with(_dev: &mut dyn BlockDevice, opts: &FormatOpts) -> Result<Self> {
132 if opts.version != 0x200 {
133 return Err(crate::Error::Unsupported(format!(
134 "grf: writer only emits v0x200 (asked for {:#x})",
135 opts.version
136 )));
137 }
138 Ok(Self {
139 version: opts.version,
140 table_offset: 0,
141 seed: 0,
142 encrypted_header: false,
143 entries: BTreeMap::new(),
144 data_end: HEADER_SIZE as u64,
145 wasted_space: 0,
146 dirty: true,
147 fresh: true,
148 })
149 }
150
151 pub fn open_dev(dev: &mut dyn BlockDevice) -> Result<Self> {
154 let mut head_buf = [0u8; HEADER_SIZE];
155 dev.read_at(0, &mut head_buf)?;
156 let head = header::Header::decode(&head_buf)?;
157
158 let table_abs = head.table_offset as u64 + HEADER_SIZE as u64;
159 let entries = read_table(dev, table_abs, head.version, head.filecount)?;
160
161 let mut data_end = HEADER_SIZE as u64;
165 for e in entries.values() {
166 let end = HEADER_SIZE as u64 + e.pos as u64 + e.len_aligned as u64;
167 if end > data_end {
168 data_end = end;
169 }
170 }
171
172 let wasted_space = table_abs.saturating_sub(data_end);
177
178 Ok(Self {
179 version: head.version,
180 table_offset: head.table_offset,
181 seed: head.seed,
182 encrypted_header: head.encrypted_header,
183 entries,
184 data_end,
185 wasted_space,
186 dirty: false,
187 fresh: false,
188 })
189 }
190
191 pub fn read_entry(&self, dev: &mut dyn BlockDevice, entry: &Entry) -> Result<Vec<u8>> {
194 let abs = HEADER_SIZE as u64 + entry.pos as u64;
195 let mut comp = vec![0u8; entry.len_aligned as usize];
196 if entry.len_aligned > 0 {
197 dev.read_at(abs, &mut comp)?;
198 }
199 if let Some(cycle) = entry.crypto_cycle() {
200 let flag_type = if cycle == 0 { 1 } else { 0 };
203 crypt::decode_des_etc(&mut comp, flag_type, cycle);
204 }
205 let plain = crate::compression::decompress(
206 crate::compression::Algo::Zlib,
207 &comp[..entry.len as usize],
208 entry.size as usize,
209 )?;
210 Ok(plain)
211 }
212
213 pub fn wasted_space(&self) -> u64 {
216 self.wasted_space
217 }
218}
219
220fn read_table(
221 dev: &mut dyn BlockDevice,
222 table_abs: u64,
223 version: u32,
224 filecount: u32,
225) -> Result<BTreeMap<String, Entry>> {
226 if filecount == 0 {
227 return Ok(BTreeMap::new());
228 }
229
230 let dev_size = dev.total_size();
231 if table_abs >= dev_size {
232 return Err(crate::Error::InvalidImage(
233 "grf: table offset past end of file".into(),
234 ));
235 }
236
237 let entries = match version {
238 0x102 | 0x103 => {
239 let table = read_compressed_table(dev, table_abs, true)?;
246 table::decode_v102(&table)?
247 }
248 0x200 => {
249 let table = read_compressed_table(dev, table_abs, false)?;
250 table::decode_v200(&table)?
251 }
252 other => {
253 return Err(crate::Error::Unsupported(format!(
254 "grf: cannot read table for version {other:#x}"
255 )));
256 }
257 };
258
259 let mut map = BTreeMap::new();
260 for e in entries {
261 map.insert(normalise_path(&e.name), e);
262 }
263 Ok(map)
264}
265
266fn read_compressed_table(
267 dev: &mut dyn BlockDevice,
268 table_abs: u64,
269 legacy_framing: bool,
270) -> Result<Vec<u8>> {
271 let dev_size = dev.total_size();
272 let mut posinfo = [0u8; 8];
273 if table_abs + 8 > dev_size {
274 return Err(crate::Error::InvalidImage(
275 "grf: table header truncated".into(),
276 ));
277 }
278 dev.read_at(table_abs, &mut posinfo)?;
279 let comp_size = u32::from_le_bytes(posinfo[0..4].try_into().unwrap()) as usize;
280 let uncomp_size = u32::from_le_bytes(posinfo[4..8].try_into().unwrap()) as usize;
281
282 let comp_start = table_abs + 8;
283 if comp_start + comp_size as u64 > dev_size {
284 return Err(crate::Error::InvalidImage(
285 "grf: compressed table payload past end of file".into(),
286 ));
287 }
288 let mut comp = vec![0u8; comp_size];
289 dev.read_at(comp_start, &mut comp)?;
290
291 let _ = legacy_framing;
295
296 crate::compression::decompress(crate::compression::Algo::Zlib, &comp, uncomp_size)
297}
298
299fn normalise_path(s: &str) -> String {
302 s.trim_start_matches('/').to_string()
303}
304
305impl crate::fs::FilesystemFactory for Grf {
306 type FormatOpts = FormatOpts;
307
308 fn format(dev: &mut dyn BlockDevice, opts: &Self::FormatOpts) -> Result<Self> {
309 Self::format_with(dev, opts)
310 }
311
312 fn open(dev: &mut dyn BlockDevice) -> Result<Self> {
313 Self::open_dev(dev)
314 }
315}
316
317impl crate::fs::Filesystem for Grf {
318 fn create_file(
319 &mut self,
320 dev: &mut dyn BlockDevice,
321 path: &std::path::Path,
322 src: FileSource,
323 _meta: FileMeta,
324 ) -> Result<()> {
325 let key = normalise_path(
326 path.to_str()
327 .ok_or_else(|| crate::Error::InvalidArgument("grf: non-UTF-8 path".into()))?,
328 );
329 writer::add_file(self, dev, key, src)
330 }
331
332 fn create_dir(
333 &mut self,
334 _dev: &mut dyn BlockDevice,
335 _path: &std::path::Path,
336 _meta: FileMeta,
337 ) -> Result<()> {
338 Ok(())
342 }
343
344 fn create_symlink(
345 &mut self,
346 _dev: &mut dyn BlockDevice,
347 _path: &std::path::Path,
348 _target: &std::path::Path,
349 _meta: FileMeta,
350 ) -> Result<()> {
351 Err(crate::Error::Unsupported(
352 "grf: symlinks are not part of the archive format".into(),
353 ))
354 }
355
356 fn create_device(
357 &mut self,
358 _dev: &mut dyn BlockDevice,
359 _path: &std::path::Path,
360 _kind: crate::fs::DeviceKind,
361 _major: u32,
362 _minor: u32,
363 _meta: FileMeta,
364 ) -> Result<()> {
365 Err(crate::Error::Unsupported(
366 "grf: device nodes are not part of the archive format".into(),
367 ))
368 }
369
370 fn remove(&mut self, _dev: &mut dyn BlockDevice, path: &std::path::Path) -> Result<()> {
371 let key = normalise_path(
372 path.to_str()
373 .ok_or_else(|| crate::Error::InvalidArgument("grf: non-UTF-8 path".into()))?,
374 );
375 writer::remove(self, &key)
376 }
377
378 fn list(
379 &mut self,
380 _dev: &mut dyn BlockDevice,
381 path: &std::path::Path,
382 ) -> Result<Vec<crate::fs::DirEntry>> {
383 let prefix = {
384 let s = path
385 .to_str()
386 .ok_or_else(|| crate::Error::InvalidArgument("grf: non-UTF-8 path".into()))?;
387 let trimmed = s.trim_start_matches('/').trim_end_matches('/');
388 if trimmed.is_empty() {
389 String::new()
390 } else {
391 format!("{trimmed}/")
392 }
393 };
394
395 use std::collections::BTreeMap as B;
399 let mut children: B<String, crate::fs::EntryKind> = B::new();
400 let mut sizes: B<String, u64> = B::new();
401 for (name, entry) in &self.entries {
402 let Some(tail) = name.strip_prefix(&prefix) else {
403 continue;
404 };
405 if tail.is_empty() {
406 continue;
407 }
408 if let Some((leaf, _)) = tail.split_once('/') {
409 children.insert(leaf.to_string(), crate::fs::EntryKind::Dir);
410 sizes.insert(leaf.to_string(), 0);
411 } else {
412 children.insert(tail.to_string(), crate::fs::EntryKind::Regular);
413 sizes.insert(tail.to_string(), entry.size as u64);
414 }
415 }
416 Ok(children
417 .into_iter()
418 .map(|(name, kind)| {
419 let size = *sizes.get(&name).unwrap_or(&0);
420 crate::fs::DirEntry {
421 name,
422 inode: 0,
423 kind,
424 size,
425 }
426 })
427 .collect())
428 }
429
430 fn read_file<'a>(
431 &'a mut self,
432 dev: &'a mut dyn BlockDevice,
433 path: &std::path::Path,
434 ) -> Result<Box<dyn Read + 'a>> {
435 let key = normalise_path(
436 path.to_str()
437 .ok_or_else(|| crate::Error::InvalidArgument("grf: non-UTF-8 path".into()))?,
438 );
439 let entry =
440 self.entries.get(&key).cloned().ok_or_else(|| {
441 crate::Error::InvalidArgument(format!("grf: no entry at {key:?}"))
442 })?;
443 let bytes = self.read_entry(dev, &entry)?;
444 Ok(Box::new(std::io::Cursor::new(bytes)))
445 }
446
447 fn open_file_ro<'a>(
448 &'a mut self,
449 dev: &'a mut dyn BlockDevice,
450 path: &std::path::Path,
451 ) -> Result<Box<dyn crate::fs::FileReadHandle + 'a>> {
452 let key = normalise_path(
460 path.to_str()
461 .ok_or_else(|| crate::Error::InvalidArgument("grf: non-UTF-8 path".into()))?,
462 );
463 let entry =
464 self.entries.get(&key).cloned().ok_or_else(|| {
465 crate::Error::InvalidArgument(format!("grf: no entry at {key:?}"))
466 })?;
467 let bytes = self.read_entry(dev, &entry)?;
468 Ok(Box::new(GrfFileReadHandle {
469 cursor: std::io::Cursor::new(bytes),
470 }))
471 }
472
473 fn flush(&mut self, dev: &mut dyn BlockDevice) -> Result<()> {
474 writer::flush(self, dev)
475 }
476
477 fn mutation_capability(&self) -> MutationCapability {
478 MutationCapability::Mutable
479 }
480}
481
482struct GrfFileReadHandle {
486 cursor: std::io::Cursor<Vec<u8>>,
487}
488
489impl Read for GrfFileReadHandle {
490 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
491 self.cursor.read(buf)
492 }
493}
494
495impl std::io::Seek for GrfFileReadHandle {
496 fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result<u64> {
497 self.cursor.seek(pos)
498 }
499}
500
501impl crate::fs::FileReadHandle for GrfFileReadHandle {
502 fn len(&self) -> u64 {
503 self.cursor.get_ref().len() as u64
504 }
505}
506
507#[cfg(test)]
508mod tests {
509 use super::*;
510 use crate::block::MemoryBackend;
511 use crate::fs::{Filesystem, FilesystemFactory};
512
513 #[test]
514 fn empty_round_trip() {
515 let mut dev = MemoryBackend::new(64 * 1024);
516 let mut grf = Grf::format(&mut dev, &FormatOpts::default()).unwrap();
517 grf.flush(&mut dev).unwrap();
518
519 let reopen = Grf::open(&mut dev).unwrap();
520 assert_eq!(reopen.version, 0x200);
521 assert_eq!(reopen.entries.len(), 0);
522 }
523
524 #[test]
525 fn add_read_round_trip() {
526 let mut dev = MemoryBackend::new(64 * 1024);
527 let mut grf = Grf::format(&mut dev, &FormatOpts::default()).unwrap();
528
529 let body = b"hello, world!";
530 grf.create_file(
531 &mut dev,
532 std::path::Path::new("/data/info.txt"),
533 FileSource::Reader {
534 reader: Box::new(std::io::Cursor::new(body.to_vec())),
535 len: body.len() as u64,
536 },
537 FileMeta::default(),
538 )
539 .unwrap();
540 grf.flush(&mut dev).unwrap();
541
542 let mut reopen = Grf::open(&mut dev).unwrap();
543 assert_eq!(reopen.entries.len(), 1);
544 let entries = reopen
545 .list(&mut dev, std::path::Path::new("/data"))
546 .unwrap();
547 assert!(entries.iter().any(|e| e.name == "info.txt"));
548 let entry = reopen.entries.get("data/info.txt").cloned().unwrap();
549 let bytes = reopen.read_entry(&mut dev, &entry).unwrap();
550 assert_eq!(bytes, body);
551 }
552
553 #[test]
554 fn open_file_ro_random_seek() {
555 use std::io::{Read, Seek, SeekFrom};
556 let mut dev = MemoryBackend::new(64 * 1024);
557 let mut grf = Grf::format(&mut dev, &FormatOpts::default()).unwrap();
558 let body: Vec<u8> = (0..1024u32).map(|i| (i & 0xff) as u8).collect();
559 grf.create_file(
560 &mut dev,
561 std::path::Path::new("/blob.bin"),
562 FileSource::Reader {
563 reader: Box::new(std::io::Cursor::new(body.clone())),
564 len: body.len() as u64,
565 },
566 FileMeta::default(),
567 )
568 .unwrap();
569 grf.flush(&mut dev).unwrap();
570
571 let mut grf = Grf::open(&mut dev).unwrap();
572 let mut h = grf
573 .open_file_ro(&mut dev, std::path::Path::new("/blob.bin"))
574 .unwrap();
575 assert_eq!(h.len(), body.len() as u64);
576 h.seek(SeekFrom::Start(500)).unwrap();
578 let mut chunk = [0u8; 32];
579 h.read_exact(&mut chunk).unwrap();
580 assert_eq!(&chunk[..], &body[500..532]);
581 h.seek(SeekFrom::Current(-32)).unwrap();
583 h.read_exact(&mut chunk).unwrap();
584 assert_eq!(&chunk[..], &body[500..532]);
585 }
586
587 #[test]
588 fn hangul_filename_round_trip() {
589 let mut dev = MemoryBackend::new(64 * 1024);
590 let mut grf = Grf::format(&mut dev, &FormatOpts::default()).unwrap();
591 grf.create_file(
592 &mut dev,
593 std::path::Path::new("/data/한글.txt"),
594 FileSource::Reader {
595 reader: Box::new(std::io::Cursor::new(b"hi".to_vec())),
596 len: 2,
597 },
598 FileMeta::default(),
599 )
600 .unwrap();
601 grf.flush(&mut dev).unwrap();
602
603 let reopen = Grf::open(&mut dev).unwrap();
604 assert!(reopen.entries.contains_key("data/한글.txt"));
605 }
606
607 #[test]
608 fn remove_marks_wasted_space() {
609 let mut dev = MemoryBackend::new(64 * 1024);
610 let mut grf = Grf::format(&mut dev, &FormatOpts::default()).unwrap();
611 grf.create_file(
612 &mut dev,
613 std::path::Path::new("/a.txt"),
614 FileSource::Reader {
615 reader: Box::new(std::io::Cursor::new(vec![0u8; 4096])),
616 len: 4096,
617 },
618 FileMeta::default(),
619 )
620 .unwrap();
621 grf.create_file(
622 &mut dev,
623 std::path::Path::new("/b.txt"),
624 FileSource::Reader {
625 reader: Box::new(std::io::Cursor::new(vec![0u8; 4096])),
626 len: 4096,
627 },
628 FileMeta::default(),
629 )
630 .unwrap();
631 grf.flush(&mut dev).unwrap();
632
633 let mut reopen = Grf::open(&mut dev).unwrap();
634 reopen
635 .remove(&mut dev, std::path::Path::new("/a.txt"))
636 .unwrap();
637 reopen.flush(&mut dev).unwrap();
638 assert!(reopen.wasted_space() > 0);
639 }
640}