ltk_meta 0.9.1

Metadata formats and utilities for League Toolkit
Documentation
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
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
//! How property values lay out in bytes, and one cursor that knows it.
//!
//! Every layer that touches value bytes — the streaming sweep, the buffered object views, the
//! owned decode — moves through a [`Cursor`], so no two of them can disagree about where a
//! value starts, how far it runs, or what its bytes mean. The rules mirror the client's
//! `MetaValue_skipByType`: primitives have a fixed width, strings a `u16` length prefix,
//! complex values carry their byte size ahead of their body, [`Kind::Optional`] is as wide as
//! the zero-or-one element it holds, and [`Kind::BitBool`] is one byte.
//!
//! A [`Kind::Hash`] value occupies 4 or 8 bytes. The file does not store the width. A cursor
//! reads the width of a `Hash` value from a [`HashWidths`] record. The [`resolve`] module computes
//! that record from the declared sizes of the regions that contain each value.
//!
//! A cursor carries the [`Numbering`] its bytes were written under, so nothing downstream has
//! to thread a flag through every call — and a slice can never be read under the wrong
//! numbering by accident, because the two travel together.
//!
//! Two ways past a value, and the difference is the whole strictness model:
//!
//! - [`Cursor::skip_value`] trusts the declared size and moves by it, decoding nothing. This is
//!   what the sweep does to the bodies it will not read.
//! - [`Cursor::walk_value`] is driven by the counts, exactly as the client's parser is, and
//!   compares a sized region's declared size against what those counts consumed. A
//!   disagreement means the file's skip path and parse path describe different bytes, which is
//!   [`Error::InvalidSize`].
//!
//! Nothing here allocates, and nothing decodes a value's contents to move past it. Decoding
//! happens only when a leaf codec is asked for.

mod codec;
pub(crate) mod resolve;

#[cfg(test)]
mod tests;

use ltk_hash::HashWidth;

use crate::{path::ValueShape, property::Kind, Error};

/// The offsets of the 8-byte [`Kind::Hash`] values of one buffer.
///
/// A `Hash` value at an offset that is not in the record is 4 bytes wide. An empty record has no
/// allocation.
#[derive(Debug, Default)]
pub struct HashWidths {
    /// Offsets from the start of the buffer, in ascending order.
    wide: Vec<usize>,
}

/// An empty record. Every `Hash` value of its buffer is 4 bytes wide.
static NARROW: HashWidths = HashWidths::new();

impl HashWidths {
    /// Returns an empty record.
    #[must_use]
    pub const fn new() -> Self {
        Self { wide: Vec::new() }
    }

    /// Returns a record that contains one offset, `offset`.
    #[must_use]
    pub fn wide_at(offset: usize) -> Self {
        Self { wide: vec![offset] }
    }

    /// Removes every offset. Keeps the allocation.
    pub fn clear(&mut self) {
        self.wide.clear();
    }

    /// Returns the width of the `Hash` value at `offset`. Returns [`HashWidth::W8`] if the record
    /// contains `offset`. Returns [`HashWidth::W4`] otherwise.
    #[must_use]
    pub fn width_at(&self, offset: usize) -> HashWidth {
        match !self.wide.is_empty() && self.wide.binary_search(&offset).is_ok() {
            true => HashWidth::W8,
            false => HashWidth::W4,
        }
    }
}

/// Which property-kind numbering a cursor's bytes were written under.
///
/// `WadChunkLink` was added in the middle of the kind enum, so files written before it exist
/// number every complex kind lower than a file written after it does. [`Kind::unpack`] is where
/// the difference is applied; this is how a cursor remembers which side of it the bytes are on.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum Numbering {
    /// `WadChunkLink` exists, and kind bytes mean what [`Kind`] says they mean.
    #[default]
    Current,
    /// Written before `WadChunkLink` existed, so the complex kinds sit lower.
    Legacy,
}

impl Numbering {
    /// The numbering a `legacy` flag names.
    ///
    /// The public reader surface — [`ReadProperty`](crate::traits::ReadProperty),
    /// [`Kind::unpack`] — still spells the question as a `bool`, so this is where that spelling
    /// is converted into the one the cursor carries.
    #[must_use]
    pub fn from_legacy(legacy: bool) -> Self {
        match legacy {
            true => Self::Legacy,
            false => Self::Current,
        }
    }

    /// Whether this is [`Numbering::Legacy`], in the form [`Kind::unpack`] takes.
    #[must_use]
    pub fn is_legacy(self) -> bool {
        matches!(self, Self::Legacy)
    }
}

/// A reading position over a byte slice, and the numbering those bytes use.
///
/// The slice is never copied, so a cursor is [`Copy`] and forking one to read ahead costs
/// nothing. Reading past the end fails with [`Error::IOError`] carrying
/// [`std::io::ErrorKind::UnexpectedEof`].
#[derive(Debug, Clone, Copy)]
pub struct Cursor<'a> {
    buf: &'a [u8],
    pos: usize,
    /// The offset of `buf` in the buffer of `widths`.
    base: usize,
    numbering: Numbering,
    widths: &'a HashWidths,
}

impl<'a> Cursor<'a> {
    /// Returns a cursor at the start of `buf` that reads kind bytes with `numbering`. The cursor
    /// reads every `Hash` value as 4 bytes.
    #[must_use]
    pub fn new(buf: &'a [u8], numbering: Numbering) -> Self {
        Self::with_widths(buf, numbering, &NARROW)
    }

    /// Returns a cursor at the start of `buf` that reads kind bytes with `numbering`. The cursor
    /// reads the width of each `Hash` value from `widths`, by the offset of the value in `buf`.
    #[must_use]
    pub fn with_widths(buf: &'a [u8], numbering: Numbering, widths: &'a HashWidths) -> Self {
        Self {
            buf,
            pos: 0,
            base: 0,
            numbering,
            widths,
        }
    }

    /// Returns a cursor over the next `n` bytes and advances past them.
    ///
    /// The returned cursor has the numbering and the `Hash` widths of this cursor.
    ///
    /// # Errors
    ///
    /// See [`Cursor::take`].
    pub fn sub(&mut self, n: usize) -> Result<Self, Error> {
        let base = self.base + self.pos;
        let buf = self.take(n)?;
        Ok(Self {
            buf,
            pos: 0,
            base,
            numbering: self.numbering,
            widths: self.widths,
        })
    }

    /// Returns the width of a `Hash` value at the position of the cursor.
    #[must_use]
    pub fn hash_width(&self) -> HashWidth {
        self.widths.width_at(self.base + self.pos)
    }

    /// The numbering this cursor reads kind bytes under.
    #[must_use]
    pub fn numbering(&self) -> Numbering {
        self.numbering
    }

    /// The current position, in bytes from the start of the slice.
    #[must_use]
    pub fn position(&self) -> usize {
        self.pos
    }

    /// How many bytes remain ahead of the cursor.
    #[must_use]
    pub fn remaining(&self) -> usize {
        self.buf.len() - self.pos
    }

    /// The bytes ahead of the cursor, without moving it.
    #[must_use]
    pub fn rest(&self) -> &'a [u8] {
        &self.buf[self.pos..]
    }

    /// The next `n` bytes, advancing past them.
    ///
    /// # Errors
    ///
    /// [`Error::IOError`] with [`std::io::ErrorKind::UnexpectedEof`] if fewer than `n` bytes
    /// remain.
    pub fn take(&mut self, n: usize) -> Result<&'a [u8], Error> {
        let end = self.pos.checked_add(n).ok_or_else(eof)?;
        let bytes = self.buf.get(self.pos..end).ok_or_else(eof)?;
        self.pos = end;
        Ok(bytes)
    }

    /// Advances past `n` bytes without touching them.
    ///
    /// # Errors
    ///
    /// See [`Cursor::take`].
    pub fn skip(&mut self, n: usize) -> Result<(), Error> {
        self.take(n).map(|_| ())
    }

    // ── moving past a value ─────────────────────────────────────────────────

    /// Advances past one value of `kind` without decoding its contents.
    ///
    /// A struct or embed whose class hash is `0` is a null pointer: no size field, no body.
    ///
    /// # Errors
    ///
    /// [`Error::IOError`] if a skip distance runs past the end of the slice, or
    /// [`Error::InvalidPropertyTypePrimitive`] if an optional's element kind byte does not
    /// decode — the one place a skip has to read a kind at all.
    pub fn skip_value(&mut self, kind: Kind) -> Result<(), Error> {
        use Kind as K;
        if let Some(width) = kind.fixed_width() {
            return self.skip(width);
        }
        match kind {
            K::Hash => self.skip(self.hash_width().bytes()),
            K::String => {
                let len = self.u16()? as usize;
                self.skip(len)
            }
            K::Container | K::UnorderedContainer => {
                self.skip(1)?; // item kind
                let size = self.u32()? as usize;
                self.skip(size)
            }
            K::Map => {
                self.skip(2)?; // key and value kinds
                let size = self.u32()? as usize;
                self.skip(size)
            }
            K::Struct | K::Embedded => {
                if self.u32()? == 0 {
                    return Ok(());
                }
                let size = self.u32()? as usize;
                self.skip(size)
            }
            K::Optional => {
                let item_kind = self.kind()?;
                match self.bool()? {
                    true => self.skip_value(item_kind),
                    false => Ok(()),
                }
            }
            _ => unreachable!("every kind without a fixed width is matched above"),
        }
    }

    /// Returns a cursor over the bytes of one value of `kind` and advances past the value.
    ///
    /// What the views hand out as a property's or an item's raw bytes: exactly what the writer
    /// emits for that value, and exactly what
    /// [`PropertyExt::size_no_header`](crate::traits::PropertyExt::size_no_header) measures.
    ///
    /// # Errors
    ///
    /// See [`Cursor::skip_value`]. Leaves the cursor at the start of the value if it fails.
    pub fn sub_value(&mut self, kind: Kind) -> Result<Self, Error> {
        let start = self.pos;
        if let Err(error) = self.skip_value(kind) {
            self.pos = start;
            return Err(error);
        }
        Ok(Self {
            buf: &self.buf[start..self.pos],
            pos: 0,
            base: self.base + start,
            numbering: self.numbering,
            widths: self.widths,
        })
    }

    // ── reading a value's header ────────────────────────────────────────────

    /// The shape a value of `kind` declares, read from the header bytes ahead of its body.
    ///
    /// Returns the same [`ValueShape`] the resolver's type rule uses, filled by the rules of
    /// [`ValueShape::of`]: a container's or option's item kind, a map's key and value kinds, an
    /// embed's class. A pointer's class is deliberately not recorded, so nothing is read for
    /// one. The cursor does not move.
    ///
    /// # Errors
    ///
    /// [`Error::InvalidPropertyTypePrimitive`] if a header kind byte does not decode,
    /// [`Error::InvalidNesting`] or [`Error::InvalidKeyType`] for a header
    /// [`Cursor::item_kind`] or [`Cursor::key_kind`] refuses, or [`Error::IOError`] if the
    /// bytes end inside the header.
    pub fn value_shape(&self, kind: Kind) -> Result<ValueShape, Error> {
        use Kind as K;
        let mut ahead = *self;
        let (item_kind, key_kind, class) = match kind {
            K::Container | K::UnorderedContainer | K::Optional => {
                (Some(ahead.item_kind()?), None, None)
            }
            K::Map => {
                let key = ahead.key_kind()?;
                let value = ahead.item_kind()?;
                (Some(value), Some(key), None)
            }
            K::Embedded => (None, None, Some(ahead.bin_hash()?)),
            _ => (None, None, None),
        };

        Ok(ValueShape {
            kind,
            item_kind,
            key_kind,
            class,
        })
    }

    /// The kind a container, optional or map declares for what it holds, advancing past it.
    ///
    /// The one place that applies the nesting rule: a container, an optional and a map hold one
    /// value each of a kind that is none of those three. A header declaring one of them makes
    /// every byte after it mean something else, and the value model has no such value to build.
    ///
    /// # Errors
    ///
    /// [`Error::InvalidNesting`] for a container, optional or map item kind,
    /// [`Error::InvalidPropertyTypePrimitive`] if the byte does not decode, or
    /// [`Error::IOError`] at the end of the slice.
    pub fn item_kind(&mut self) -> Result<Kind, Error> {
        let kind = self.kind()?;
        match kind.is_container() {
            true => Err(Error::InvalidNesting(kind)),
            false => Ok(kind),
        }
    }

    /// The kind a map declares for its keys, advancing past it.
    ///
    /// The one place that applies the key rule: [`Kind::is_valid_map_key`] is what the client's
    /// reader accepts as a key.
    ///
    /// # Errors
    ///
    /// [`Error::InvalidKeyType`] for a kind no map is keyed by,
    /// [`Error::InvalidPropertyTypePrimitive`] if the byte does not decode, or
    /// [`Error::IOError`] at the end of the slice.
    pub fn key_kind(&mut self) -> Result<Kind, Error> {
        let kind = self.kind()?;
        match kind.is_valid_map_key() {
            true => Ok(kind),
            false => Err(Error::InvalidKeyType(kind)),
        }
    }

    // ── walking a value by its counts ───────────────────────────────────────

    /// Walks one value of `kind` by its counts, verifying declared sizes along the way.
    ///
    /// This is the parse path's traversal: element and property counts drive it, exactly as
    /// they drive the client's parser. A sized region's declared size is compared against what
    /// the walk consumed after the fact — the two describing different bytes means the file is
    /// internally inconsistent, so the walk fails rather than guessing which one to believe.
    ///
    /// # Errors
    ///
    /// [`Error::InvalidSize`] if a declared size disagrees with what the counts consumed,
    /// [`Error::IOError`] if the walk runs past the end of the slice,
    /// [`Error::InvalidPropertyTypePrimitive`] if a kind byte does not decode, or
    /// [`Error::InvalidNesting`] and [`Error::InvalidKeyType`] for a header
    /// [`Cursor::item_kind`] or [`Cursor::key_kind`] refuses.
    pub fn walk_value(&mut self, kind: Kind) -> Result<(), Error> {
        use Kind as K;
        if let Some(width) = kind.fixed_width() {
            return self.skip(width);
        }
        match kind {
            K::Hash => self.skip(self.hash_width().bytes()),
            K::String => {
                let len = self.u16()? as usize;
                self.skip(len)
            }
            K::Container | K::UnorderedContainer => {
                let item_kind = self.item_kind()?;
                self.sized_region(|cur| {
                    let count = cur.u32()?;
                    for _ in 0..count {
                        cur.walk_value(item_kind)?;
                    }
                    Ok(())
                })
            }
            K::Map => {
                let key_kind = self.key_kind()?;
                let value_kind = self.item_kind()?;
                self.sized_region(|cur| {
                    let count = cur.u32()?;
                    for _ in 0..count {
                        cur.walk_value(key_kind)?;
                        cur.walk_value(value_kind)?;
                    }
                    Ok(())
                })
            }
            K::Struct | K::Embedded => {
                if self.u32()? == 0 {
                    return Ok(());
                }
                self.sized_region(|cur| {
                    let count = cur.u16()?;
                    cur.walk_properties(count)
                })
            }
            K::Optional => {
                let item_kind = self.item_kind()?;
                match self.bool()? {
                    true => self.walk_value(item_kind),
                    false => Ok(()),
                }
            }
            _ => unreachable!("every kind without a fixed width is matched above"),
        }
    }

    /// Walks one whole object — its `u32 size` field included — by the same rules.
    ///
    /// An object's sized region is its path hash, its property count, and that many properties.
    /// This is the walk a buffered object gets as it lands: it settles the numbering latch and
    /// proves the declared size before anything reads inside it.
    ///
    /// # Errors
    ///
    /// The same as [`Cursor::walk_value`].
    pub fn walk_object(&mut self) -> Result<(), Error> {
        self.sized_region(|cur| {
            cur.skip(4)?; // path hash
            let count = cur.u16()?;
            cur.walk_properties(count)
        })
    }

    /// Walks `count` `name_hash`/`kind`/`value` triples.
    fn walk_properties(&mut self, count: u16) -> Result<(), Error> {
        for _ in 0..count {
            self.skip(4)?; // name hash
            let kind = self.kind()?;
            self.walk_value(kind)?;
        }
        Ok(())
    }

    /// Reads a `u32` size field, runs `body`, and checks declared against consumed.
    ///
    /// The one place [`Error::InvalidSize`] comes from, which is why both renderers over this
    /// module raise it on exactly the same inputs.
    ///
    /// # Errors
    ///
    /// [`Error::InvalidSize`] if `body` did not consume exactly the declared size, plus
    /// whatever `body` itself raises.
    pub fn sized_region<T>(
        &mut self,
        body: impl FnOnce(&mut Self) -> Result<T, Error>,
    ) -> Result<T, Error> {
        let declared = self.u32()? as usize;
        let start = self.pos;

        let value = body(self)?;

        let consumed = self.pos - start;
        match consumed == declared {
            true => Ok(value),
            false => Err(Error::InvalidSize(declared as u64, consumed as u64)),
        }
    }
}

pub(crate) fn eof() -> Error {
    Error::IOError(std::io::Error::from(std::io::ErrorKind::UnexpectedEof))
}