1use std::collections::HashMap;
7use std::io::{Cursor, Read, Seek, SeekFrom};
8
9use byteorder::{BigEndian, ReadBytesExt};
10use tracing::{debug, trace};
11
12use crate::utils::read_uint40_be_from;
13use crate::{Error, Result};
14
15pub mod flags {
17 pub const INCLUDE_CKEY: u8 = 0x01;
19 pub const WRITE_SUPPORT: u8 = 0x02;
21 pub const PATCH_SUPPORT: u8 = 0x04;
23 pub const LOWERCASE: u8 = 0x08;
25}
26
27#[derive(Debug, Clone)]
29pub struct TVFSHeader {
30 pub magic: [u8; 4],
32 pub version: u8,
34 pub header_size: u8,
36 pub ekey_size: u8,
38 pub patch_key_size: u8,
40 pub flags: u8,
42 pub path_table_offset: u64,
44 pub path_table_size: u64,
46 pub vfs_table_offset: u64,
48 pub vfs_table_size: u64,
50 pub cft_table_offset: u64,
52 pub cft_table_size: u64,
54 pub max_metafile_size: u16,
56 pub build_version: u32,
58}
59
60impl TVFSHeader {
61 pub fn parse<R: Read>(reader: &mut R) -> Result<Self> {
63 let mut magic = [0u8; 4];
64 reader.read_exact(&mut magic)?;
65
66 if &magic != b"TVFS" {
68 return Err(Error::IOError(std::io::Error::new(
69 std::io::ErrorKind::InvalidData,
70 format!("Invalid TVFS magic: {magic:?}, expected TVFS"),
71 )));
72 }
73
74 let version = reader.read_u8()?;
75 if version != 1 {
76 debug!("Unexpected TVFS version: {}", version);
77 }
78
79 let header_size = reader.read_u8()?;
80 let ekey_size = reader.read_u8()?;
81 let patch_key_size = reader.read_u8()?;
82 let flags = reader.read_u8()?;
83
84 let path_table_offset = read_uint40_be_from(reader)?;
86 let path_table_size = read_uint40_be_from(reader)?;
87 let vfs_table_offset = read_uint40_be_from(reader)?;
88 let vfs_table_size = read_uint40_be_from(reader)?;
89 let cft_table_offset = read_uint40_be_from(reader)?;
90 let cft_table_size = read_uint40_be_from(reader)?;
91
92 let max_metafile_size = reader.read_u16::<BigEndian>()?;
93 let build_version = reader.read_u32::<BigEndian>()?;
94
95 Ok(TVFSHeader {
96 magic,
97 version,
98 header_size,
99 ekey_size,
100 patch_key_size,
101 flags,
102 path_table_offset,
103 path_table_size,
104 vfs_table_offset,
105 vfs_table_size,
106 cft_table_offset,
107 cft_table_size,
108 max_metafile_size,
109 build_version,
110 })
111 }
112
113 pub fn has_ckey(&self) -> bool {
115 self.flags & flags::INCLUDE_CKEY != 0
116 }
117
118 pub fn has_write_support(&self) -> bool {
120 self.flags & flags::WRITE_SUPPORT != 0
121 }
122
123 pub fn has_patch_support(&self) -> bool {
125 self.flags & flags::PATCH_SUPPORT != 0
126 }
127
128 pub fn has_lowercase_paths(&self) -> bool {
130 self.flags & flags::LOWERCASE != 0
131 }
132}
133
134#[derive(Debug, Clone)]
136pub struct PathEntry {
137 pub path: String,
139 pub hash: u64,
141}
142
143#[derive(Debug, Clone, Copy, PartialEq)]
145pub enum VFSEntryType {
146 File,
148 Deleted,
150 Inline,
152 Link,
154}
155
156#[derive(Debug, Clone)]
158pub struct VFSEntry {
159 pub entry_type: VFSEntryType,
161 pub span_offset: u32,
163 pub span_count: u32,
165 pub path_index: u32,
167 pub file_offset: Option<u64>,
169 pub file_size: Option<u32>,
171}
172
173#[derive(Debug, Clone)]
175pub struct CFTEntry {
176 pub ekey: Vec<u8>,
178 pub file_size: u64,
180 pub espec_index: Option<u32>,
182}
183
184#[derive(Debug, Clone)]
186pub struct TVFSManifest {
187 pub header: TVFSHeader,
189 pub path_table: Vec<PathEntry>,
191 pub vfs_table: Vec<VFSEntry>,
193 pub cft_table: Vec<CFTEntry>,
195 pub espec_table: Option<Vec<String>>,
197 path_map: HashMap<String, usize>,
199}
200
201impl TVFSManifest {
202 pub fn parse(data: &[u8]) -> Result<Self> {
204 let mut cursor = Cursor::new(data);
205
206 let header = TVFSHeader::parse(&mut cursor)?;
208
209 debug!(
210 "Parsing TVFS v{} with {} bytes, flags: {:#04x}",
211 header.version,
212 data.len(),
213 header.flags
214 );
215
216 cursor.seek(SeekFrom::Start(header.path_table_offset))?;
218 let path_table = Self::parse_path_table(&mut cursor, header.path_table_size as usize)?;
219
220 cursor.seek(SeekFrom::Start(header.vfs_table_offset))?;
222 let vfs_table = Self::parse_vfs_table(&mut cursor, header.vfs_table_size as usize)?;
223
224 cursor.seek(SeekFrom::Start(header.cft_table_offset))?;
226 let cft_table = Self::parse_cft_table(
227 &mut cursor,
228 header.cft_table_size as usize,
229 false, )?;
231
232 let espec_table = None;
234
235 let mut path_map = HashMap::new();
237 for (idx, entry) in vfs_table.iter().enumerate() {
238 if entry.path_index < path_table.len() as u32 {
239 let path = &path_table[entry.path_index as usize].path;
240 path_map.insert(path.clone(), idx);
241 }
242 }
243
244 Ok(TVFSManifest {
245 header,
246 path_table,
247 vfs_table,
248 cft_table,
249 espec_table,
250 path_map,
251 })
252 }
253
254 fn parse_path_table<R: Read>(reader: &mut R, size: usize) -> Result<Vec<PathEntry>> {
256 let mut entries = Vec::new();
257 let mut bytes_read = 0usize;
258
259 debug!("Parsing path table with size: {}", size);
260
261 while bytes_read < size {
262 let path_len = reader.read_u8()? as usize;
271 bytes_read += 1;
272
273 if path_len == 0 || bytes_read >= size {
274 break; }
276
277 let mut path_bytes = vec![0u8; path_len];
279 reader.read_exact(&mut path_bytes)?;
280 bytes_read += path_len;
281
282 let path = String::from_utf8(path_bytes).map_err(|e| {
283 Error::IOError(std::io::Error::new(
284 std::io::ErrorKind::InvalidData,
285 format!("Invalid UTF-8 in path: {e}"),
286 ))
287 })?;
288
289 let hash = crate::utils::jenkins3_hashpath(&path);
291
292 trace!("Path entry: {} (hash: {:#x})", path, hash);
293
294 entries.push(PathEntry { path, hash });
295 }
296
297 debug!("Parsed {} path entries", entries.len());
298 Ok(entries)
299 }
300
301 fn parse_vfs_table<R: Read>(reader: &mut R, size: usize) -> Result<Vec<VFSEntry>> {
303 let mut entries = Vec::new();
304 let mut bytes_read = 0usize;
305
306 while bytes_read < size {
307 if bytes_read >= size {
308 break;
309 }
310
311 let type_byte = reader.read_u8()?;
313 bytes_read += 1;
314
315 let entry_type = match type_byte & 0x03 {
316 0 => VFSEntryType::File,
317 1 => VFSEntryType::Deleted,
318 2 => VFSEntryType::Inline,
319 3 => VFSEntryType::Link,
320 _ => unreachable!(),
321 };
322
323 let (span_offset, span_count) = if entry_type == VFSEntryType::File {
325 let mut offset = 0u32;
327 let mut shift = 0;
328 for _ in 0..5 {
329 let byte = reader.read_u8()?;
330 bytes_read += 1;
331 let value = (byte & 0x7F) as u32;
332 offset |= value << shift;
333 if byte & 0x80 == 0 {
334 break;
335 }
336 shift += 7;
337 }
338
339 let mut count = 0u32;
341 shift = 0;
342 for _ in 0..5 {
343 let byte = reader.read_u8()?;
344 bytes_read += 1;
345 let value = (byte & 0x7F) as u32;
346 count |= value << shift;
347 if byte & 0x80 == 0 {
348 break;
349 }
350 shift += 7;
351 }
352
353 (offset, count)
354 } else {
355 (0, 0)
356 };
357
358 let mut path_index = 0u32;
360 let mut shift = 0;
361 for _ in 0..5 {
362 let byte = reader.read_u8()?;
363 bytes_read += 1;
364 let value = (byte & 0x7F) as u32;
365 path_index |= value << shift;
366 if byte & 0x80 == 0 {
367 break;
368 }
369 shift += 7;
370 }
371
372 let (file_offset, file_size) = if entry_type == VFSEntryType::Inline {
374 let offset = read_uint40_be_from(reader)?;
375 bytes_read += 5;
376 let size = reader.read_u32::<BigEndian>()?;
377 bytes_read += 4;
378 (Some(offset), Some(size))
379 } else {
380 (None, None)
381 };
382
383 entries.push(VFSEntry {
384 entry_type,
385 span_offset,
386 span_count,
387 path_index,
388 file_offset,
389 file_size,
390 });
391 }
392
393 debug!("Parsed {} VFS entries", entries.len());
394 Ok(entries)
395 }
396
397 fn parse_cft_table<R: Read>(
399 reader: &mut R,
400 size: usize,
401 has_est_table: bool,
402 ) -> Result<Vec<CFTEntry>> {
403 let mut entries = Vec::new();
404 let mut bytes_read = 0usize;
405
406 while bytes_read < size {
407 let mut ekey = vec![0u8; 16];
409 reader.read_exact(&mut ekey)?;
410 bytes_read += 16;
411
412 let file_size = read_uint40_be_from(reader)?;
414 bytes_read += 5;
415
416 let espec_index = if has_est_table {
418 let index = reader.read_u8()?;
419 bytes_read += 1;
420 Some(index as u32)
421 } else {
422 None
423 };
424
425 entries.push(CFTEntry {
426 ekey,
427 file_size,
428 espec_index,
429 });
430 }
431
432 debug!("Parsed {} CFT entries", entries.len());
433 Ok(entries)
434 }
435
436 pub fn resolve_path(&self, path: &str) -> Option<FileInfo> {
442 let vfs_index = *self.path_map.get(path)?;
444 let vfs_entry = &self.vfs_table[vfs_index];
445
446 match vfs_entry.entry_type {
447 VFSEntryType::File => {
448 let mut spans = Vec::new();
450 for i in 0..vfs_entry.span_count {
451 let cft_index = (vfs_entry.span_offset + i) as usize;
452 if cft_index < self.cft_table.len() {
453 let cft_entry = &self.cft_table[cft_index];
454 spans.push(FileSpan {
455 ekey: cft_entry.ekey.clone(),
456 file_size: cft_entry.file_size,
457 espec: cft_entry.espec_index.and_then(|idx| {
458 self.espec_table.as_ref()?.get(idx as usize).cloned()
459 }),
460 });
461 }
462 }
463
464 Some(FileInfo {
465 path: path.to_string(),
466 entry_type: vfs_entry.entry_type,
467 spans,
468 inline_data: None,
469 })
470 }
471 VFSEntryType::Inline => Some(FileInfo {
472 path: path.to_string(),
473 entry_type: vfs_entry.entry_type,
474 spans: Vec::new(),
475 inline_data: Some((vfs_entry.file_offset?, vfs_entry.file_size?)),
476 }),
477 _ => None,
478 }
479 }
480
481 pub fn list_directory(&self, dir_path: &str) -> Vec<DirEntry> {
483 let mut entries = Vec::new();
484 let dir_prefix = if dir_path.ends_with('/') {
485 dir_path.to_string()
486 } else if dir_path.is_empty() {
487 String::new()
488 } else {
489 format!("{dir_path}/")
490 };
491
492 for path_entry in &self.path_table {
493 if path_entry.path.starts_with(&dir_prefix) {
494 let relative_path = &path_entry.path[dir_prefix.len()..];
495
496 if !relative_path.contains('/') && !relative_path.is_empty() {
498 if let Some(vfs_index) = self.path_map.get(&path_entry.path) {
499 let vfs_entry = &self.vfs_table[*vfs_index];
500
501 let is_directory = false; let size = if vfs_entry.entry_type == VFSEntryType::File {
503 self.calculate_file_size(*vfs_index)
504 } else {
505 0
506 };
507
508 entries.push(DirEntry {
509 name: relative_path.to_string(),
510 path: path_entry.path.clone(),
511 is_directory,
512 size,
513 });
514 }
515 }
516 }
517 }
518
519 entries
520 }
521
522 fn calculate_file_size(&self, vfs_index: usize) -> u64 {
524 let vfs_entry = &self.vfs_table[vfs_index];
525 let mut total_size = 0u64;
526
527 for i in 0..vfs_entry.span_count {
528 let cft_index = (vfs_entry.span_offset + i) as usize;
529 if cft_index < self.cft_table.len() {
530 total_size += self.cft_table[cft_index].file_size;
531 }
532 }
533
534 total_size
535 }
536
537 pub fn file_count(&self) -> usize {
539 self.vfs_table
540 .iter()
541 .filter(|e| e.entry_type == VFSEntryType::File || e.entry_type == VFSEntryType::Inline)
542 .count()
543 }
544
545 pub fn deleted_count(&self) -> usize {
547 self.vfs_table
548 .iter()
549 .filter(|e| e.entry_type == VFSEntryType::Deleted)
550 .count()
551 }
552
553 pub fn total_size(&self) -> u64 {
555 self.cft_table.iter().map(|e| e.file_size).sum()
556 }
557}
558
559#[derive(Debug, Clone)]
561pub struct FileSpan {
562 pub ekey: Vec<u8>,
564 pub file_size: u64,
566 pub espec: Option<String>,
568}
569
570#[derive(Debug, Clone)]
572pub struct FileInfo {
573 pub path: String,
575 pub entry_type: VFSEntryType,
577 pub spans: Vec<FileSpan>,
579 pub inline_data: Option<(u64, u32)>,
581}
582
583#[derive(Debug, Clone)]
585pub struct DirEntry {
586 pub name: String,
588 pub path: String,
590 pub is_directory: bool,
592 pub size: u64,
594}
595
596#[cfg(test)]
597mod tests {
598 use super::*;
599
600 #[test]
601 fn test_tvfs_header_flags() {
602 let header = TVFSHeader {
603 magic: *b"TVFS",
604 version: 1,
605 header_size: 38,
606 ekey_size: 9,
607 patch_key_size: 9,
608 flags: flags::INCLUDE_CKEY | flags::WRITE_SUPPORT,
609 path_table_offset: 100,
610 path_table_size: 200,
611 vfs_table_offset: 300,
612 vfs_table_size: 400,
613 cft_table_offset: 700,
614 cft_table_size: 500,
615 max_metafile_size: 1024,
616 build_version: 42000,
617 };
618
619 assert!(header.has_ckey());
620 assert!(header.has_write_support());
621 assert!(!header.has_patch_support());
622 assert!(!header.has_lowercase_paths());
623 }
624
625 #[test]
626 fn test_vfs_entry_type() {
627 let file_type = VFSEntryType::File;
629 let deleted_type = VFSEntryType::Deleted;
630 let inline_type = VFSEntryType::Inline;
631 let link_type = VFSEntryType::Link;
632
633 assert_ne!(file_type as u8, deleted_type as u8);
635 assert_ne!(file_type as u8, inline_type as u8);
636 assert_ne!(file_type as u8, link_type as u8);
637 }
638
639 #[test]
640 fn test_tvfs_40bit_offsets() {
641 use crate::utils::{read_uint40_be, write_uint40_be};
642
643 let one_tb = 1_099_511_627_776u64; let max_40bit = (1u64 << 40) - 1; assert_eq!(max_40bit, one_tb - 1);
649
650 let encoded = write_uint40_be(max_40bit);
652 assert_eq!(encoded.len(), 5);
653
654 let decoded = read_uint40_be(&encoded).unwrap();
655 assert_eq!(decoded, max_40bit);
656
657 let hundred_gb = 100 * 1024 * 1024 * 1024u64;
659 let encoded_100gb = write_uint40_be(hundred_gb);
660 let decoded_100gb = read_uint40_be(&encoded_100gb).unwrap();
661 assert_eq!(decoded_100gb, hundred_gb);
662 }
663}