Skip to content

Commit 5de4bcc

Browse files
Display impls, LevelFile hashing on demand, read-bundle command
1 parent 2d75291 commit 5de4bcc

File tree

6 files changed

+185
-34
lines changed

6 files changed

+185
-34
lines changed

examples/extract_bundle.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ async fn main() {
1919
std::fs::remove_dir_all(output_dir).ok();
2020

2121
let time = std::time::Instant::now();
22-
let asset = AssetBundle::from_file(&asset_path).unwrap();
23-
asset.extract_files(output_dir).unwrap();
22+
let (_, bundle) = AssetBundle::from_file(&asset_path).unwrap();
23+
bundle.extract_files(output_dir).unwrap();
2424
info!("Extraction took {:?}", time.elapsed());
2525

2626
let version = Version::from_manifest_file("example_manifest.json").unwrap();

src/bin/ffbuildtool.rs

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ enum Commands {
2424
RepairBuild(RepairBuildArgs),
2525
ValidateBuild(ValidateBuildArgs),
2626
#[cfg(feature = "lzma")]
27+
ReadBundle(ReadBundleArgs),
28+
#[cfg(feature = "lzma")]
2729
ExtractBundle(ExtractBundleArgs),
2830
}
2931

@@ -95,6 +97,18 @@ struct ValidateBuildArgs {
9597
uncompressed: bool,
9698
}
9799

100+
#[cfg(feature = "lzma")]
101+
#[derive(Args, Debug)]
102+
struct ReadBundleArgs {
103+
/// Path to the compressed asset bundle
104+
#[clap(short = 'b', long)]
105+
bundle_path: String,
106+
107+
/// Whether to calculate the hashes of each file in the bundle
108+
#[clap(short = 'c', long, action)]
109+
calculate_hashes: bool,
110+
}
111+
98112
#[cfg(feature = "lzma")]
99113
#[derive(Args, Debug)]
100114
struct ExtractBundleArgs {
@@ -219,6 +233,8 @@ async fn main() -> Result<(), String> {
219233
Commands::RepairBuild(args) => repair_build(args).await,
220234
Commands::ValidateBuild(args) => validate_build(args).await,
221235
#[cfg(feature = "lzma")]
236+
Commands::ReadBundle(args) => read_bundle(args).await,
237+
#[cfg(feature = "lzma")]
222238
Commands::ExtractBundle(args) => extract_bundle(args).await,
223239
}
224240
}
@@ -339,11 +355,35 @@ async fn validate_build(args: ValidateBuildArgs) -> Result<(), String> {
339355
Ok(())
340356
}
341357

358+
#[cfg(feature = "lzma")]
359+
async fn read_bundle(args: ReadBundleArgs) -> Result<(), String> {
360+
use std::time::Instant;
361+
362+
let start = Instant::now();
363+
let (header, mut bundle) = ffbuildtool::bundle::AssetBundle::from_file(&args.bundle_path)?;
364+
println!("Bundle read in {}ms", start.elapsed().as_millis());
365+
366+
if args.calculate_hashes {
367+
let start = Instant::now();
368+
bundle.recalculate_all_hashes();
369+
println!("Hashes calculated in {}ms", start.elapsed().as_millis());
370+
}
371+
372+
println!(
373+
"------------------------\n{}\n------------------------\n{}",
374+
header, bundle
375+
);
376+
Ok(())
377+
}
378+
342379
#[cfg(feature = "lzma")]
343380
async fn extract_bundle(args: ExtractBundleArgs) -> Result<(), String> {
344-
use std::path::PathBuf;
381+
use std::{path::PathBuf, time::Instant};
382+
383+
let start = Instant::now();
384+
let (_, bundle) = ffbuildtool::bundle::AssetBundle::from_file(&args.bundle_path)?;
385+
println!("Bundle read in {}ms", start.elapsed().as_millis());
345386

346-
let asset_bundle = ffbuildtool::bundle::AssetBundle::from_file(&args.bundle_path)?;
347387
let output_dir = args.output_dir.unwrap_or({
348388
let bundle_name = ffbuildtool::util::get_file_name_without_parent(&args.bundle_path);
349389
let bundle_name_url_encoded = ffbuildtool::util::url_encode(bundle_name);
@@ -356,5 +396,10 @@ async fn extract_bundle(args: ExtractBundleArgs) -> Result<(), String> {
356396
.to_string()
357397
});
358398
println!("Extracting bundle {} to {}", args.bundle_path, output_dir);
359-
asset_bundle.extract_files(&output_dir)
399+
400+
let start = Instant::now();
401+
bundle.extract_files(&output_dir)?;
402+
println!("Bundle extracted in {}ms", start.elapsed().as_millis());
403+
404+
Ok(())
360405
}

src/bundle.rs

Lines changed: 121 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use std::{
22
collections::HashMap,
33
fs::File,
44
io::{BufRead, BufReader, BufWriter, Read, Write},
5-
path::Path,
5+
path::{Path, PathBuf},
66
};
77

88
use countio::Counter;
@@ -74,6 +74,27 @@ pub struct AssetBundleHeader {
7474
level_ends: Vec<LevelEnds>,
7575
bundle_size: u32,
7676
}
77+
impl std::fmt::Display for AssetBundleHeader {
78+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
79+
writeln!(f, "Signature: {}", self.signature)?;
80+
writeln!(f, "Stream version: {}", self.stream_version)?;
81+
writeln!(f, "Player version: {}", self.player_version)?;
82+
writeln!(f, "Engine version: {}", self.engine_version)?;
83+
writeln!(f, "Min streamed bytes: {}", self.min_streamed_bytes)?;
84+
writeln!(f, "Header size: {}", self.header_size)?;
85+
writeln!(f, "Number of levels: {}", self.num_levels)?;
86+
writeln!(f, "Min levels for load: {}", self.min_levels_for_load)?;
87+
writeln!(f, "Level ends:")?;
88+
for (i, level) in self.level_ends.iter().enumerate() {
89+
writeln!(
90+
f,
91+
" Level {}: compressed @ {}, uncompressed @ {}",
92+
i, level.compressed_end, level.uncompressed_end
93+
)?;
94+
}
95+
write!(f, "Bundle size: {}", self.bundle_size)
96+
}
97+
}
7798
impl AssetBundleHeader {
7899
fn new(level_ends: Vec<LevelEnds>) -> Self {
79100
let num_levels = level_ends.len() as u32;
@@ -253,15 +274,47 @@ impl LevelHeader {
253274
struct LevelFile {
254275
name: String,
255276
data: Vec<u8>,
277+
hash: Option<String>,
256278
}
257279
impl std::fmt::Debug for LevelFile {
258280
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
259281
f.debug_struct("LevelFile")
260282
.field("name", &self.name)
261283
.field("data", &format_args!("{} bytes", self.data.len()))
284+
.field("hash", &self.hash)
262285
.finish()
263286
}
264287
}
288+
impl std::fmt::Display for LevelFile {
289+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
290+
match self.hash.as_ref() {
291+
Some(hash) => write!(
292+
f,
293+
"{} - {} ({} bytes) - {}",
294+
self.name,
295+
util::bytes_to_human_readable(self.data.len() as u64),
296+
self.data.len(),
297+
hash
298+
),
299+
None => write!(
300+
f,
301+
"{} - {} ({} bytes)",
302+
self.name,
303+
util::bytes_to_human_readable(self.data.len() as u64),
304+
self.data.len()
305+
),
306+
}
307+
}
308+
}
309+
impl LevelFile {
310+
fn new(name: String, data: Vec<u8>) -> Self {
311+
Self {
312+
name,
313+
data,
314+
hash: None,
315+
}
316+
}
317+
}
265318

266319
#[derive(Debug)]
267320
struct Level {
@@ -278,10 +331,7 @@ impl Level {
278331
skip_exact(&mut reader, file.offset as usize - offset)?;
279332
let mut data = vec![0; file.size as usize];
280333
reader.read_exact(&mut data)?;
281-
files.push(LevelFile {
282-
name: file.name,
283-
data,
284-
});
334+
files.push(LevelFile::new(file.name, data));
285335
}
286336
Ok(Self { files })
287337
}
@@ -345,8 +395,23 @@ impl Level {
345395
pub struct AssetBundle {
346396
levels: Vec<Level>,
347397
}
398+
impl std::fmt::Display for AssetBundle {
399+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
400+
for (i, level) in self.levels.iter().enumerate() {
401+
writeln!(f, "Level {}", i)?;
402+
for file in &level.files {
403+
writeln!(f, " {}", file)?;
404+
}
405+
write!(f, "End")?;
406+
}
407+
Ok(())
408+
}
409+
}
348410
impl AssetBundle {
349-
fn read<R: Read + BufRead>(reader: &mut R, expected_size: u32) -> Result<Self, Error> {
411+
fn read<R: Read + BufRead>(
412+
reader: &mut R,
413+
expected_size: u32,
414+
) -> Result<(AssetBundleHeader, Self), Error> {
350415
let mut reader = Counter::new(reader);
351416

352417
let header = AssetBundleHeader::read(&mut reader)?;
@@ -374,7 +439,7 @@ impl AssetBundle {
374439
}
375440
}
376441

377-
Ok(Self { levels })
442+
Ok((header, Self { levels }))
378443
}
379444

380445
fn write<W: Write>(&self, writer: &mut W, compression: u32) -> Result<(), Error> {
@@ -399,7 +464,27 @@ impl AssetBundle {
399464
Ok(())
400465
}
401466

402-
pub fn from_file(path: &str) -> Result<Self, String> {
467+
fn get_level_files_from_dir(dir_path: &Path) -> Result<Vec<LevelFile>, Error> {
468+
let mut files = Vec::new();
469+
for entry in std::fs::read_dir(dir_path)? {
470+
let Ok(entry) = entry else {
471+
continue;
472+
};
473+
let path = entry.path();
474+
if path.is_file() {
475+
let Some(name) = path.file_name().unwrap().to_str() else {
476+
continue;
477+
};
478+
let Ok(data) = std::fs::read(&path) else {
479+
continue;
480+
};
481+
files.push(LevelFile::new(name.to_string(), data));
482+
}
483+
}
484+
Ok(files)
485+
}
486+
487+
pub fn from_file(path: &str) -> Result<(AssetBundleHeader, Self), String> {
403488
let file = File::open(path).map_err(|e| format!("Couldn't open file {}: {}", path, e))?;
404489
let metadata = file.metadata().unwrap();
405490
let mut reader = BufReader::new(file);
@@ -419,24 +504,6 @@ impl AssetBundle {
419504
Ok(())
420505
}
421506

422-
pub fn get_uncompressed_info(&self, level: usize) -> Result<HashMap<String, FileInfo>, Error> {
423-
let mut result = HashMap::new();
424-
if level >= self.levels.len() {
425-
return Err(format!("Level {} does not exist", level).into());
426-
}
427-
428-
for file in &self.levels[level].files {
429-
let hash = util::get_buffer_hash(&file.data);
430-
let info = FileInfo {
431-
hash,
432-
size: file.data.len() as u64,
433-
};
434-
result.insert(file.name.clone(), info);
435-
}
436-
437-
Ok(result)
438-
}
439-
440507
pub fn extract_files(&self, output_dir: &str) -> Result<(), String> {
441508
let make_subdirs = self.levels.len() > 1;
442509
for (i, level) in self.levels.iter().enumerate() {
@@ -458,4 +525,32 @@ impl AssetBundle {
458525
}
459526
Ok(())
460527
}
528+
529+
pub fn recalculate_all_hashes(&mut self) {
530+
for level in &mut self.levels {
531+
for file in &mut level.files {
532+
file.hash = Some(util::get_buffer_hash(&file.data));
533+
}
534+
}
535+
}
536+
537+
pub fn get_uncompressed_info(&self, level: usize) -> Result<HashMap<String, FileInfo>, Error> {
538+
let mut result = HashMap::new();
539+
if level >= self.levels.len() {
540+
return Err(format!("Level {} does not exist", level).into());
541+
}
542+
543+
for file in &self.levels[level].files {
544+
let info = FileInfo {
545+
hash: file
546+
.hash
547+
.clone()
548+
.unwrap_or_else(|| util::get_buffer_hash(&file.data)),
549+
size: file.data.len() as u64,
550+
};
551+
result.insert(file.name.clone(), info);
552+
}
553+
554+
Ok(result)
555+
}
461556
}

src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -579,7 +579,7 @@ impl BundleInfo {
579579

580580
#[cfg(feature = "lzma")]
581581
let uncompressed_info = {
582-
let bundle = bundle::AssetBundle::from_file(&file_path)?;
582+
let (_, bundle) = bundle::AssetBundle::from_file(&file_path)?;
583583
// ff assets are always in level 0
584584
bundle.get_uncompressed_info(0)?
585585
};

src/tests.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,8 +89,8 @@ async fn test_extract_bundle() {
8989
let bundle_path = "example_builds/compressed/good/Map_00_00.unity3d";
9090
let output_dir = TempDir::new();
9191

92-
let asset_bundle = crate::bundle::AssetBundle::from_file(bundle_path).unwrap();
93-
asset_bundle.extract_files(output_dir.path()).unwrap();
92+
let (_, bundle) = crate::bundle::AssetBundle::from_file(bundle_path).unwrap();
93+
bundle.extract_files(output_dir.path()).unwrap();
9494

9595
let version = Version::from_manifest_file("example_manifest.json").unwrap();
9696
let bundle_info = version.get_bundle("Map_00_00.unity3d").unwrap();

src/util.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,3 +265,14 @@ pub fn file_path_to_uri(file_path: &str) -> String {
265265
// Add file:/// protocol
266266
format!("file:///{}", path)
267267
}
268+
269+
pub fn bytes_to_human_readable(bytes: u64) -> String {
270+
let units = ["B", "KB", "MB", "GB"];
271+
let mut bytes = bytes as f64;
272+
let mut unit = 0;
273+
while bytes >= 1024.0 && unit < units.len() - 1 {
274+
bytes /= 1024.0;
275+
unit += 1;
276+
}
277+
format!("{:.2} {}", bytes, units[unit])
278+
}

0 commit comments

Comments
 (0)