1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
//! Turning a resolved path into a name a file system will take.
//!
//! Three things stand between a path a resolver gave and a file on disk: the
//! path may be one the extraction must refuse outright ([`is_evil`]), another
//! path of the same extraction may need it to be a directory
//! ([`DirectoryPaths`]), or the file system may refuse the name when the write
//! is tried ([`is_path_conflict`]). The first two are settled before anything
//! is written, which is what makes one archive and one hash table give one
//! output tree on every run.
//!
//! `docs/design/wad-extractor.md` records why, and what was measured.
use ;
use ;
use LeagueFileKind;
use WadHash;
use crateWadChunk;
use Hash as _;
use ;
/// Whether an extraction's names can be read back as the paths they came from.
///
/// The policy picks between naming a chunk for what its bytes are and keeping
/// every chunk under a name its resolved path can be read out of. The extractor
/// module docs, under "How a chunk is named on disk", say what each name is.
/// Whether `path` is one an extraction must refuse to write.
///
/// A resolver's paths are untrusted, a hash table and name recovery alike;
/// the extractor module docs say why, under "Paths the extraction will not
/// write". A path is evil when joining it onto the output directory would not
/// give a plain file plainly under that directory:
///
/// - it starts at a root, a drive or a network share, so the join ignores the
/// output directory;
/// - a component is `..`, which reaches the directory above;
/// - a component holds a `:`, naming a Windows drive or an alternate data
/// stream instead of a file the directory lists;
/// - a component ends in a dot or a space, which Windows strips before it
/// looks the name up, so `notes.txt.` and `notes.txt` are one file under two
/// names and would walk past the check for two chunks claiming one path;
/// - or it names no file at all, holding nothing but separators and `.`.
///
/// The last two rules are Windows behaviour, applied wherever the extraction
/// runs. That is deliberate: one archive and one hash table then give one
/// output tree on every host, and a test on any of them catches a table that
/// would misbehave on Windows. It costs two conditions.
///
/// `path` is read as the raw string a resolver gave. Turning it into a
/// [`Utf8Path`] first would normalise away the very things this looks for, and
/// `/` and `\` both separate components whatever the host, so a table written
/// on Windows cannot escape on Linux or the other way round.
///
/// The check is lexical. It says the joined path cannot name a file outside the
/// output directory; it says nothing about a symlink an output tree already
/// holds.
pub
/// The directories a set of paths names, each a path in its own right.
///
/// A WAD is a flat map from path to bytes, so it can hold both `x` and `x/y`.
/// A file system holds one or the other, so one of the two has to move. Which
/// one is settled against this, built over the paths an extraction resolved
/// before it writes any of them. Settling it at the write instead would follow
/// whichever worker reached the file system first, and two runs of one archive
/// could give two trees.
pub ;
/// `path` with one `/` between its components and nothing else.
///
/// The components are the ones a join steps onto, so an empty one from `a//b`
/// or from a trailing separator is dropped, as is a `.`, and `\` separates as
/// `/` does whatever the host. Borrowed when `path` is in that form already,
/// which nearly every path a hash table holds is.
pub
/// Whether `error` says something already occupies `path`, rather than the
/// write having failed for a reason a different name would not mend.
///
/// Windows reports a directory standing in the way as `PermissionDenied`, and
/// reports a file it cannot open the same way, so `path.is_dir()` breaks the
/// tie between the two.
pub
/// The name a chunk takes when the file system refuses its own.
///
/// `<hash>.<ext>` under [`NamingPolicy::Descriptive`], and the bare `<hash>`
/// under [`NamingPolicy::Lossless`], which invents no extension.
pub
/// `<name>.ltk`, the name a chunk takes when a directory holds its own.
///
/// The suffix is added and never substituted, so stripping a trailing `.ltk`
/// gives back the path the chunk was named for, whatever that path held. A
/// caller that hashes an extracted file's path back to its chunk needs exactly
/// that, and a name built from the file's stem could not give it: `foo.bin`
/// renamed to `foo.ltk.dds` says nothing about the `.bin` it came from.
pub
/// `path` with the suffix on the end of its file name.
///
/// The suffix goes on the end of the path, which is the same thing: a path's
/// file name is its tail. Going through
/// [`set_file_name`](Utf8PathBuf::set_file_name) would not be, because that
/// re-joins the path with the host's separator and would hand back
/// `assets\thing.ltk` on Windows where every un-renamed chunk reports
/// `assets/thing`. A caller stripping the suffix off that would hash a path
/// the archive was never built from.
pub
/// `path` without the `.ltk` suffix an extraction adds, if it has one.
///
/// The exact inverse of the rename: the suffix is only ever added, never
/// substituted, so taking it off gives back the name the chunk was written
/// for. Borrowed when there is no suffix to take off.
/// The chunk an extracted file was written for.
///
/// Reads the extraction's naming back: the `.ltk` suffix comes off, a hash
/// name parses as itself, and anything else is the path a resolver gave,
/// hashed as the archive keys it. Both naming policies read back the same
/// way, so this takes none.
///
/// The one shape it cannot tell apart is a resolver that named a chunk with
/// sixteen hex digits of its own, which reads as that hash rather than as the
/// path. [`ExtractProgress::is_named`](crate::ExtractProgress::is_named)
/// reports which a chunk was at the time it was written, for a caller that
/// must know.
///
/// # Example
///
/// ```
/// use ltk_wad::{chunk_hash_of, WadHash};
/// use camino::Utf8Path;
///
/// // A hash name, and the same chunk after a directory took its name.
/// let bare = chunk_hash_of(Utf8Path::new("0123456789abcdef"));
/// assert_eq!(bare, WadHash(0x0123456789abcdef));
/// assert_eq!(chunk_hash_of(Utf8Path::new("0123456789abcdef.ltk")), bare);
///
/// // A path a resolver gave reads back the same before and after a rename.
/// let named = chunk_hash_of(Utf8Path::new("assets/thing.bin"));
/// assert_eq!(chunk_hash_of(Utf8Path::new("assets/thing.bin.ltk")), named);
/// ```