Skip to content

Commit 0df3e4d

Browse files
committed
Add more error handling, trim more characters from path names
Make the program more robust, add more logs, sanitize paths and make sure they're relative "enough" to be safe. This is in response to issue #1 reported by @art0007i.
1 parent fdb52cd commit 0df3e4d

File tree

1 file changed

+189
-54
lines changed

1 file changed

+189
-54
lines changed

src/main.rs

Lines changed: 189 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,84 +1,219 @@
1-
use argparse::{ArgumentParser, Store, StoreTrue};
2-
use flate2::read::GzDecoder;
3-
use log::{info, LevelFilter};
4-
use simple_logger::SimpleLogger;
51
use std::collections::HashMap;
6-
use std::fs::File;
2+
use std::ffi::OsString;
73
use std::io::Read;
8-
use std::path::PathBuf;
9-
use tar::Archive;
10-
use tokio::fs;
4+
use std::path::{Path, PathBuf};
115

12-
#[tokio::main]
13-
async fn main() -> Result<(), Box<dyn std::error::Error>> {
14-
// Parse command-line arguments
15-
let mut verbose = false;
6+
use argparse::{ArgumentParser, IncrBy, Store};
7+
use flate2::read::GzDecoder;
8+
use log::{debug, error, info, trace, warn, LevelFilter};
9+
use simple_logger::SimpleLogger;
10+
use tokio::io::AsyncWriteExt;
11+
use tokio::task::JoinHandle;
12+
use tokio::{fs, io};
13+
14+
struct Config {
15+
input_path: String,
16+
log_level: LevelFilter,
17+
}
18+
19+
const TRIM_CHARS: &[char] = &['\0', ' ', '\n', '\t', '\r', '/', '.'];
20+
21+
type AssetMap = HashMap<PathBuf, Vec<u8>>;
22+
type FolderMap = HashMap<OsString, bool>;
23+
type ExtractTask = Vec<JoinHandle<Result<(), io::Error>>>;
24+
25+
fn parse_arguments() -> Config {
26+
let mut verbose = 0;
27+
let mut quiet = 0;
1628
let mut input_path = String::new();
29+
1730
{
1831
let mut parser = ArgumentParser::new();
1932
parser.set_description("Unity package extractor");
33+
parser.refer(&mut quiet).add_option(
34+
&["-q"],
35+
IncrBy(1),
36+
"decrease verbosity, hide warnings.",
37+
);
2038
parser
2139
.refer(&mut verbose)
22-
.add_option(&["-v"], StoreTrue, "Verbose mode");
40+
.add_option(&["-v"], IncrBy(1), "increase verbosity; up to 3.");
2341
parser
2442
.refer(&mut input_path)
2543
.add_argument("input", Store, "*.unitypackage file")
2644
.required();
2745
parser.parse_args_or_exit();
2846
}
2947

30-
if verbose {
31-
SimpleLogger::new()
32-
.with_level(LevelFilter::Info)
33-
.init()
34-
.unwrap();
48+
let log_level = match verbose - quiet {
49+
..=-1 => LevelFilter::Error,
50+
0 => LevelFilter::Warn,
51+
1 => LevelFilter::Info,
52+
2 => LevelFilter::Debug,
53+
3.. => LevelFilter::Trace,
54+
};
55+
56+
Config {
57+
input_path,
58+
log_level,
59+
}
60+
}
61+
62+
fn sanitize_path(path: &str) -> Result<String, io::Error> {
63+
let sanitized_path = path.trim_matches(TRIM_CHARS).replace('\\', "/");
64+
65+
if sanitized_path.contains("..") {
66+
warn!("path «{}» contains .., this isn't supported", path);
67+
return Err(io::Error::new(
68+
io::ErrorKind::InvalidInput,
69+
"Path contains invalid '..'",
70+
));
71+
}
72+
73+
Ok(sanitized_path)
74+
}
75+
76+
fn read_asset_to_memory<R: Read>(
77+
assets: &mut AssetMap,
78+
mut entry: tar::Entry<'_, R>,
79+
path: PathBuf,
80+
) -> Result<(), io::Error> {
81+
debug!("reading asset to memory «{:?}»", path);
82+
let mut asset_data = Vec::new();
83+
entry.read_to_end(&mut asset_data)?;
84+
trace!(
85+
"saving «{:?}» with {} bytes to memory",
86+
path,
87+
asset_data.len(),
88+
);
89+
assets.insert(path, asset_data);
90+
Ok(())
91+
}
92+
93+
fn check_for_folders<R: Read>(
94+
folders: &mut FolderMap,
95+
mut entry: tar::Entry<'_, R>,
96+
path: PathBuf,
97+
) -> Result<(), io::Error> {
98+
debug!("reading asset to memory «{:?}»", path);
99+
let mut metadata = String::new();
100+
entry.read_to_string(&mut metadata)?;
101+
if metadata.contains("folderAsset: yes\n") {
102+
folders.insert(path.into_os_string(), true);
103+
}
104+
Ok(())
105+
}
106+
107+
fn read_destination_path_and_write<R: Read>(
108+
assets: &mut AssetMap,
109+
folders: &FolderMap,
110+
tasks: &mut ExtractTask,
111+
mut entry: tar::Entry<'_, R>,
112+
path: PathBuf,
113+
) -> Result<(), io::Error> {
114+
let mut path_name = String::new();
115+
entry.read_to_string(&mut path_name)?;
116+
117+
let asset_path = path.parent().unwrap().join("asset");
118+
if let Some(asset_data) = assets.remove(&asset_path) {
119+
tasks.push(tokio::spawn(write_asset_to_pathname(
120+
asset_data,
121+
path.clone(),
122+
path_name,
123+
)));
35124
} else {
36-
SimpleLogger::new()
37-
.with_level(LevelFilter::Error)
38-
.init()
39-
.unwrap();
125+
let path_string = path.into_os_string();
126+
if folders.contains_key(&path_string) {
127+
warn!("no asset data found for «{}»", path_name.escape_default());
128+
}
129+
}
130+
Ok(())
131+
}
132+
133+
async fn write_asset_to_pathname(
134+
asset_data: Vec<u8>,
135+
entry_hash: PathBuf,
136+
path_name: String,
137+
) -> Result<(), io::Error> {
138+
let target_path = sanitize_path(&path_name)?;
139+
140+
if path_name != target_path {
141+
debug!(
142+
"sanitizing path «{}» => «{}»",
143+
path_name.escape_default(),
144+
target_path.escape_default(),
145+
);
146+
}
147+
148+
if let Some(parent) = Path::new(&target_path).parent() {
149+
fs::create_dir_all(parent).await?;
40150
}
41151

42-
// Open the unitypackage file
43-
let file = File::open(&input_path)?;
44-
let decoder = GzDecoder::new(file);
45-
let mut archive = Archive::new(decoder);
46-
let mut assets: HashMap<PathBuf, Vec<u8>> = HashMap::new();
152+
info!("extracting: «{:?}» to «{}»", entry_hash, target_path);
153+
let file = fs::File::create(&target_path).await?;
154+
let mut file_writer = io::BufWriter::new(file);
155+
file_writer.write_all(&asset_data).await?;
156+
file_writer.flush().await?;
47157

48-
// Iterate over each entry in the archive
158+
trace!("done extracting «{:?}»", entry_hash);
159+
Ok(())
160+
}
161+
162+
#[tokio::main]
163+
async fn main() -> Result<(), Box<dyn std::error::Error>> {
164+
let config = parse_arguments();
165+
SimpleLogger::new().with_level(config.log_level).init()?;
166+
debug!("opening unitypackage file at {}", &config.input_path);
167+
168+
let file = std::fs::File::open(&config.input_path);
169+
170+
if let Err(err) = file {
171+
error!("cannot open file at {}: {}", config.input_path, err);
172+
std::process::exit(2);
173+
}
174+
175+
let decoder = GzDecoder::new(file?);
176+
let mut archive = tar::Archive::new(decoder);
177+
let mut assets: AssetMap = HashMap::new();
178+
let mut folders: FolderMap = HashMap::new();
179+
let mut tasks: ExtractTask = Vec::new();
180+
181+
debug!("iterating archive's entries");
49182
for entry_result in archive.entries()? {
50-
let mut entry = entry_result?;
51-
let path = entry.path()?.to_path_buf();
183+
let entry = match entry_result {
184+
Ok(file) => file,
185+
Err(e) => {
186+
warn!("error reading entry from archive: {}", e);
187+
continue;
188+
}
189+
};
52190

53-
// If the entry is an 'asset' file, read its content
54-
if path.ends_with("asset") {
55-
let mut asset_data = Vec::new();
56-
entry.read_to_end(&mut asset_data)?;
57-
assets.insert(path.clone(), asset_data);
58-
}
59-
// If the entry is a 'pathname' file, read its content and write the asset
60-
else if path.ends_with("pathname") {
61-
let mut pathname = String::new();
62-
entry.read_to_string(&mut pathname)?;
63-
64-
// Sanitize the pathname
65-
let pathname = pathname.trim().replace('\\', "/");
66-
let target_path = PathBuf::from(&pathname);
67-
68-
// Create directories for the target path
69-
if let Some(parent) = target_path.parent() {
70-
fs::create_dir_all(parent).await?;
191+
let path = match entry.path() {
192+
Ok(p) => p.to_path_buf(),
193+
Err(e) => {
194+
warn!("errors reading path from entry: {}", e);
195+
continue;
71196
}
197+
};
72198

73-
// Write the asset data to the target path
74-
let asset_path = path.parent().unwrap().join("asset");
75-
if let Some(asset_data) = assets.remove(&asset_path) {
76-
fs::write(&target_path, &asset_data).await?;
199+
if path.ends_with("asset") {
200+
read_asset_to_memory(&mut assets, entry, path)?;
201+
} else if path.ends_with("asset.meta") {
202+
check_for_folders(&mut folders, entry, path)?;
203+
} else if path.ends_with("pathname") {
204+
read_destination_path_and_write(&mut assets, &folders, &mut tasks, entry, path)?;
205+
} else {
206+
trace!("skipping entry with name «{:?}»", path)
207+
}
208+
}
77209

78-
info!("Extracted: {}", pathname);
79-
}
210+
debug!("end of archive");
211+
for task in tasks {
212+
if let Err(e) = task.await {
213+
warn!("an extraction task has failed: {}", e);
80214
}
81215
}
216+
info!("done");
82217

83218
Ok(())
84219
}

0 commit comments

Comments
 (0)