Skip to content

Commit a1db5fd

Browse files
authored
Merge pull request #20 from LeagueToolkit/feat/build-ltk-chunk-extensions
feat: build ltk chunk extensions
2 parents 108fbd7 + e8c77f1 commit a1db5fd

File tree

2 files changed

+83
-93
lines changed

2 files changed

+83
-93
lines changed

crates/wadtools/src/extractor.rs

Lines changed: 53 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::utils::{is_chunk_path, WadHashtable};
1+
use crate::utils::{is_hex_chunk_path, truncate_middle, WadHashtable};
22
use color_eyre::eyre::{self, Ok};
33
use eyre::Context;
44
use fancy_regex::Regex;
@@ -9,13 +9,15 @@ use league_toolkit::{
99
use std::{
1010
collections::HashMap,
1111
ffi::OsStr,
12-
fs::{self, DirBuilder, File},
12+
fs::{self, File},
1313
io::{self, Read, Seek},
1414
path::{Path, PathBuf},
1515
};
1616
use tracing_indicatif::span_ext::IndicatifSpanExt;
1717
use tracing_indicatif::style::ProgressStyle;
1818

19+
const MAX_LOG_PATH_LEN: usize = 120;
20+
1921
pub struct Extractor<'chunks> {
2022
decoder: &'chunks mut WadDecoder<'chunks, &'chunks File>,
2123
hashtable: &'chunks WadHashtable,
@@ -55,13 +57,6 @@ impl<'chunks> Extractor<'chunks> {
5557
span.pb_set_message("Extracting chunks");
5658
span.pb_set_finish_message("Extraction complete");
5759

58-
prepare_extraction_directories_absolute(
59-
chunks.iter(),
60-
self.hashtable,
61-
&extract_directory,
62-
self.filter_pattern.as_ref(),
63-
)?;
64-
6560
extract_wad_chunks(
6661
self.decoder,
6762
chunks,
@@ -84,44 +79,6 @@ impl<'chunks> Extractor<'chunks> {
8479
}
8580
}
8681

87-
pub fn prepare_extraction_directories_absolute<'chunks>(
88-
chunks: impl Iterator<Item = (&'chunks u64, &'chunks WadChunk)>,
89-
wad_hashtable: &WadHashtable,
90-
extraction_directory: impl AsRef<Path>,
91-
filter_pattern: Option<&Regex>,
92-
) -> eyre::Result<()> {
93-
// collect all chunk directories
94-
let chunk_directories = chunks.filter_map(|(_, chunk)| {
95-
let chunk_path_str = wad_hashtable.resolve_path(chunk.path_hash());
96-
if let Some(regex) = filter_pattern {
97-
if !regex.is_match(chunk_path_str.as_ref()).unwrap_or(false) {
98-
return None;
99-
}
100-
}
101-
Path::new(chunk_path_str.as_ref())
102-
.parent()
103-
.map(|path| path.to_path_buf())
104-
});
105-
106-
create_extraction_directories(chunk_directories, extraction_directory)?;
107-
108-
Ok(())
109-
}
110-
111-
fn create_extraction_directories(
112-
chunk_directories: impl Iterator<Item = impl AsRef<Path>>,
113-
extraction_directory: impl AsRef<Path>,
114-
) -> eyre::Result<()> {
115-
// this wont error if the directory already exists since recursive mode is enabled
116-
for chunk_directory in chunk_directories {
117-
DirBuilder::new()
118-
.recursive(true)
119-
.create(extraction_directory.as_ref().join(chunk_directory))?;
120-
}
121-
122-
Ok(())
123-
}
124-
12582
pub fn extract_wad_chunks<TSource: Read + Seek>(
12683
decoder: &mut WadDecoder<TSource>,
12784
chunks: &HashMap<u64, WadChunk>,
@@ -137,7 +94,8 @@ pub fn extract_wad_chunks<TSource: Read + Seek>(
13794
let chunk_path = Path::new(chunk_path_str.as_ref());
13895

13996
// advance progress for every chunk (including ones we skip)
140-
report_progress(i as f64 / chunks.len() as f64, chunk_path.to_str())?;
97+
let truncated = truncate_middle(chunk_path_str.as_ref(), MAX_LOG_PATH_LEN);
98+
report_progress(i as f64 / chunks.len() as f64, Some(truncated.as_str()))?;
14199

142100
if let Some(regex) = filter_pattern {
143101
if !regex.is_match(chunk_path_str.as_ref()).unwrap_or(false) {
@@ -146,15 +104,15 @@ pub fn extract_wad_chunks<TSource: Read + Seek>(
146104
}
147105
}
148106

149-
extract_wad_chunk_absolute(decoder, chunk, chunk_path, &extract_directory, filter_type)?;
107+
extract_wad_chunk(decoder, chunk, chunk_path, &extract_directory, filter_type)?;
150108

151109
i += 1;
152110
}
153111

154112
Ok(())
155113
}
156114

157-
pub fn extract_wad_chunk_absolute<'wad, TSource: Read + Seek>(
115+
pub fn extract_wad_chunk<'wad, TSource: Read + Seek>(
158116
decoder: &mut WadDecoder<'wad, TSource>,
159117
chunk: &WadChunk,
160118
chunk_path: impl AsRef<Path>,
@@ -176,8 +134,12 @@ pub fn extract_wad_chunk_absolute<'wad, TSource: Read + Seek>(
176134
return Ok(());
177135
}
178136

179-
let chunk_path = resolve_final_chunk_path(chunk_path, &chunk_data);
180-
let Err(error) = fs::write(extract_directory.as_ref().join(&chunk_path), &chunk_data) else {
137+
let chunk_path = resolve_final_chunk_path(&extract_directory, chunk_path, &chunk_data);
138+
let full_path = extract_directory.as_ref().join(&chunk_path);
139+
if let Some(parent) = full_path.parent() {
140+
fs::create_dir_all(parent)?;
141+
}
142+
let Err(error) = fs::write(&full_path, &chunk_data) else {
181143
return Ok(());
182144
};
183145

@@ -187,33 +149,46 @@ pub fn extract_wad_chunk_absolute<'wad, TSource: Read + Seek>(
187149
} else {
188150
Err(error).wrap_err(format!(
189151
"failed to write chunk (chunk_path: {})",
190-
chunk_path.display()
152+
truncate_middle(&full_path.display().to_string(), MAX_LOG_PATH_LEN)
191153
))
192154
}
193155
}
194156

195-
fn resolve_final_chunk_path(chunk_path: impl AsRef<Path>, chunk_data: &[u8]) -> PathBuf {
196-
let mut chunk_path = chunk_path.as_ref().to_path_buf();
197-
if chunk_path.extension().is_none() && is_chunk_path(&chunk_path) {
198-
// check for known extensions
199-
match LeagueFileKind::identify_from_bytes(chunk_data) {
200-
LeagueFileKind::Unknown => {
201-
tracing::warn!(
202-
"chunk has no known extension, prepending '.' (chunk_path: {})",
203-
chunk_path.display()
204-
);
205-
206-
chunk_path = chunk_path.with_file_name(OsStr::new(
207-
&(".".to_string() + chunk_path.file_name().unwrap().to_string_lossy().as_ref()),
208-
));
209-
}
210-
file_kind => {
211-
chunk_path.set_extension(file_kind.extension().unwrap());
212-
}
213-
}
157+
fn resolve_final_chunk_path(
158+
extract_directory: impl AsRef<Path>,
159+
chunk_path: impl AsRef<Path>,
160+
chunk_data: &[u8],
161+
) -> PathBuf {
162+
let mut final_path = chunk_path.as_ref().to_path_buf();
163+
164+
// Hashed paths must remain exactly 16 hex characters with no extension
165+
if is_hex_chunk_path(&final_path) {
166+
return final_path;
167+
}
168+
169+
// - If the original path has no extension, affix .ltk (and real extension if known)
170+
// - OR if the destination path collides with an existing directory, affix .ltk
171+
let has_extension = final_path.extension().is_some();
172+
let collides_with_dir = extract_directory.as_ref().join(&final_path).is_dir();
173+
if !has_extension || collides_with_dir {
174+
let original_stem = chunk_path
175+
.as_ref()
176+
.file_stem()
177+
.unwrap_or_default()
178+
.to_string_lossy();
179+
let new_name = build_ltk_name(&original_stem, chunk_data);
180+
final_path.set_file_name(OsStr::new(&new_name));
214181
}
215182

216-
chunk_path
183+
final_path
184+
}
185+
186+
fn build_ltk_name(file_stem: &str, chunk_data: &[u8]) -> String {
187+
let kind = LeagueFileKind::identify_from_bytes(chunk_data);
188+
match kind.extension() {
189+
Some(ext) => format!("{}.ltk.{}", file_stem, ext),
190+
None => format!("{}.ltk", file_stem),
191+
}
217192
}
218193

219194
fn write_long_filename_chunk(
@@ -223,29 +198,15 @@ fn write_long_filename_chunk(
223198
chunk_data: &[u8],
224199
) -> eyre::Result<()> {
225200
let hashed_path = format!("{:016x}", chunk.path_hash());
201+
let disp = chunk_path.as_ref().display().to_string();
202+
let truncated = truncate_middle(&disp, MAX_LOG_PATH_LEN);
226203
tracing::warn!(
227-
"invalid chunk filename, writing as hashed path (chunk_path: {}, hashed_path: {})",
228-
chunk_path.as_ref().display(),
204+
"Long filename detected (chunk_path: {}, hashed_path: {})",
205+
truncated,
229206
&hashed_path
230207
);
231208

232-
let file_kind = LeagueFileKind::identify_from_bytes(chunk_data);
233-
let extension = file_kind.extension();
234-
235-
match file_kind {
236-
LeagueFileKind::Unknown => {
237-
fs::write(extract_directory.as_ref().join(hashed_path), chunk_data)?;
238-
}
239-
_ => {
240-
fs::write(
241-
extract_directory
242-
.as_ref()
243-
.join(format!("{:016x}", chunk.path_hash()))
244-
.with_extension(extension.unwrap()),
245-
chunk_data,
246-
)?;
247-
}
248-
}
209+
fs::write(extract_directory.as_ref().join(hashed_path), chunk_data)?;
249210

250211
Ok(())
251212
}

crates/wadtools/src/utils/mod.rs

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,36 @@ pub fn format_chunk_path_hash(path_hash: u64) -> String {
88
format!("{:016x}", path_hash)
99
}
1010

11-
pub fn is_chunk_path(path: &Path) -> bool {
11+
pub fn is_hex_chunk_path(path: &Path) -> bool {
1212
let file_name = path.file_name().unwrap_or_default().to_string_lossy();
1313
file_name.len() == 16 && file_name.chars().all(|c| c.is_ascii_hexdigit())
1414
}
15+
16+
/// Truncates a string in the middle
17+
pub fn truncate_middle(input: &str, max_len: usize) -> String {
18+
if input.len() <= max_len {
19+
return input.to_string();
20+
}
21+
if max_len <= 3 {
22+
return "...".to_string();
23+
}
24+
let keep = max_len - 3;
25+
let left = keep / 2;
26+
let right = keep - left;
27+
let mut left_iter = input.chars();
28+
let mut left_str = String::with_capacity(left);
29+
for _ in 0..left {
30+
if let Some(c) = left_iter.next() {
31+
left_str.push(c);
32+
}
33+
}
34+
let mut right_iter = input.chars().rev();
35+
let mut right_str = String::with_capacity(right);
36+
for _ in 0..right {
37+
if let Some(c) = right_iter.next() {
38+
right_str.push(c);
39+
}
40+
}
41+
right_str = right_str.chars().rev().collect();
42+
format!("{}...{}", left_str, right_str)
43+
}

0 commit comments

Comments
 (0)