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
use std::io;

use crate::{
    property::Kind,
    stream::{layout::Numbering, owned},
    traits::{PropertyExt, PropertyValueExt, ReadProperty, WriteProperty, WriterExt},
    Error, PropertyValueEnum, ValueSlot,
};
use byteorder::{WriteBytesExt, LE};
use ltk_hash::HashWidth;
use ltk_io_ext::{measure, window_at};

mod item;
pub use item::ContainerItem;

/// Returns the width of `value` if `value` is a [`Kind::Hash`] value. Returns `None` otherwise.
fn hash_width_of(value: &PropertyValueEnum) -> Option<HashWidth> {
    match value {
        PropertyValueEnum::Hash(hash) => Some(hash.width()),
        _ => None,
    }
}

/// Checks the width of `value` against `expected`. Sets `expected` to the width of `value` if
/// `expected` is `None`. Does not check a `value` that is not a `Hash`.
///
/// # Errors
///
/// Fails with [`Error::MismatchedHashWidths`] if `value` is a `Hash` and its width is not
/// `expected`.
pub(crate) fn track_hash_width(
    expected: &mut Option<HashWidth>,
    value: &PropertyValueEnum,
) -> Result<(), Error> {
    let Some(got) = hash_width_of(value) else {
        return Ok(());
    };
    match *expected {
        None => *expected = Some(got),
        Some(expected) if expected != got => {
            return Err(Error::MismatchedHashWidths { expected, got })
        }
        Some(_) => {}
    }
    Ok(())
}

/// Checks the width of `value` against `expected`. Does not check a `value` that is not a `Hash`.
/// Does not check any `value` if `expected` is `None`.
///
/// # Errors
///
/// Fails with [`Error::MismatchedHashWidths`] if `value` is a `Hash` and its width is not
/// `expected`.
pub(crate) fn check_hash_width(
    expected: Option<HashWidth>,
    value: &PropertyValueEnum,
) -> Result<(), Error> {
    match (expected, hash_width_of(value)) {
        (Some(expected), Some(got)) if expected != got => {
            Err(Error::MismatchedHashWidths { expected, got })
        }
        _ => Ok(()),
    }
}

/// Converts `error` to the [`io::Error`] that a writer returns for a container or a map side with
/// two `Hash` widths.
pub(crate) fn mixed_widths(error: Error) -> io::Error {
    io::Error::new(io::ErrorKind::InvalidInput, error.to_string())
}

/// A list of values that all have the same [`Kind`].
///
/// The kind is declared once, both in the file and in [`Container::item_kind`], and every item
/// matches it. [`Container::push`] and the checked constructors enforce that on the way in, and
/// [`Container::slot`] is the only way to reach an item mutably, which is why it pins the kind.
///
/// The format has no nested containers, so a container, option or map cannot be an item. The
/// checked constructors reject those kinds, and [`ContainerItem`] excludes them at compile time.
///
/// All [`Kind::Hash`] items of a container have the same width. A reader computes the width from
/// the size of the container and the number of its items. [`Container::new`] and
/// [`Container::push`] fail on a second width. [`Container::to_writer`] fails if the container
/// contains two widths.
///
/// [`Container::to_writer`]: crate::traits::WriteProperty::to_writer
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Debug, PartialEq)]
pub struct Container {
    item_kind: Kind,
    items: Vec<PropertyValueEnum>,
}

impl Container {
    /// An empty container holding items of `item_kind`.
    ///
    /// # Errors
    ///
    /// [`Error::InvalidNesting`] if `item_kind` is itself a container kind.
    pub fn empty(item_kind: Kind) -> Result<Self, Error> {
        Self::new(item_kind, Vec::new())
    }

    /// A container of `items`, all of which must be `item_kind`.
    ///
    /// # Errors
    ///
    /// [`Error::InvalidNesting`] if `item_kind` is itself a container kind, or
    /// [`Error::MismatchedContainerTypes`] if an item is not `item_kind`.
    ///
    /// Fails with [`Error::MismatchedHashWidths`] if two `Hash` items have different widths.
    pub fn new(item_kind: Kind, items: Vec<PropertyValueEnum>) -> Result<Self, Error> {
        if item_kind.is_container() {
            return Err(Error::InvalidNesting(item_kind));
        }
        let mut hash_width = None;
        for item in &items {
            if item.kind() != item_kind {
                return Err(Error::MismatchedContainerTypes {
                    expected: item_kind,
                    got: item.kind(),
                });
            }
            track_hash_width(&mut hash_width, item)?;
        }

        Ok(Self { item_kind, items })
    }

    /// Returns the width of the [`Kind::Hash`] items. Returns `None` if the items are not `Hash`
    /// values or if the container is empty.
    #[must_use]
    pub fn hash_width(&self) -> Option<HashWidth> {
        self.items.first().and_then(hash_width_of)
    }

    /// The kind every item in this container has.
    #[inline(always)]
    #[must_use]
    pub fn item_kind(&self) -> Kind {
        self.item_kind
    }

    /// The items, in order.
    #[inline(always)]
    #[must_use]
    pub fn items(&self) -> &[PropertyValueEnum] {
        &self.items
    }

    /// The item at `index`, if there is one.
    #[inline(always)]
    #[must_use]
    pub fn get(&self, index: usize) -> Option<&PropertyValueEnum> {
        self.items.get(index)
    }

    /// A mutable handle on item `index`, pinned to [`Container::item_kind`].
    ///
    /// There is no plain `&mut` to an item, because writing one of a different kind would leave
    /// the container inhomogeneous and [`Container::to_writer`] emitting a file the game cannot
    /// read. [`ValueSlot`] edits in place freely and checks the kind only on a whole-value
    /// replace.
    #[inline(always)]
    #[must_use]
    pub fn slot(&mut self, index: usize) -> Option<ValueSlot<'_>> {
        let item_kind = self.item_kind;
        Some(ValueSlot::pinned(item_kind, self.items.get_mut(index)?))
    }

    /// The items, mutably, for walking further down inside the crate.
    ///
    /// A whole-item replace through one of these skips the kind check [`Container::slot`] makes.
    /// A caller edits inside an item and never replaces one.
    pub(crate) fn items_mut(&mut self) -> std::slice::IterMut<'_, PropertyValueEnum> {
        self.items.iter_mut()
    }

    /// How many items this container holds.
    #[inline(always)]
    #[must_use]
    pub fn len(&self) -> usize {
        self.items.len()
    }

    /// Whether this container holds no items.
    #[inline(always)]
    #[must_use]
    pub fn is_empty(&self) -> bool {
        self.items.is_empty()
    }

    /// Appends `value`.
    ///
    /// # Errors
    ///
    /// [`Error::MismatchedContainerTypes`] if `value` is not [`Container::item_kind`].
    ///
    /// Fails with [`Error::MismatchedHashWidths`] if `value` is a `Hash` and its width differs
    /// from the width of the items.
    pub fn push(&mut self, value: PropertyValueEnum) -> Result<(), Error> {
        if value.kind() != self.item_kind {
            return Err(Error::MismatchedContainerTypes {
                expected: self.item_kind,
                got: value.kind(),
            });
        }
        check_hash_width(self.hash_width(), &value)?;

        self.items.push(value);
        Ok(())
    }

    /// The items, in order, by value.
    #[inline(always)]
    #[must_use]
    pub fn into_items(self) -> Vec<PropertyValueEnum> {
        self.items
    }
}

impl Default for Container {
    fn default() -> Self {
        Self {
            item_kind: Kind::None,
            items: Vec::new(),
        }
    }
}

impl<T: ContainerItem + Into<PropertyValueEnum>> From<Vec<T>> for Container {
    fn from(items: Vec<T>) -> Self {
        items.into_iter().collect()
    }
}

impl<T: ContainerItem + Into<PropertyValueEnum>> FromIterator<T> for Container {
    fn from_iter<I: IntoIterator<Item = T>>(iter: I) -> Self {
        Self {
            item_kind: T::KIND,
            items: iter.into_iter().map(Into::into).collect(),
        }
    }
}

impl TryFrom<Vec<PropertyValueEnum>> for Container {
    type Error = Error;

    /// The item kind comes from the first item, so an empty vector has none to take.
    ///
    /// # Errors
    ///
    /// [`Error::EmptyContainer`] if `items` is empty, otherwise whatever [`Container::new`]
    /// returns.
    fn try_from(items: Vec<PropertyValueEnum>) -> Result<Self, Self::Error> {
        let item_kind = items.first().ok_or(Error::EmptyContainer)?.kind();
        Self::new(item_kind, items)
    }
}

impl PropertyExt for Container {
    fn size_no_header(&self) -> usize {
        9 + self.items.iter().map(|i| i.size_no_header()).sum::<usize>()
    }
}

impl PropertyValueExt for Container {
    const KIND: Kind = Kind::Container;
}

impl ReadProperty for Container {
    fn from_reader<R: io::Read + io::Seek + ?Sized>(
        reader: &mut R,
        legacy: bool,
    ) -> Result<Self, Error> {
        owned::read_from(
            reader,
            Kind::Container,
            Numbering::from_legacy(legacy),
            owned::read_container,
        )
    }
}

impl WriteProperty for Container {
    // TODO: legacy writing
    fn to_writer<R: io::Write + io::Seek + ?Sized>(
        &self,
        writer: &mut R,
        legacy: bool,
    ) -> Result<(), io::Error> {
        if legacy {
            unimplemented!("legacy container writing");
        }
        writer.write_property_kind(self.item_kind)?;
        let size_pos = writer.stream_position()?;
        writer.write_u32::<LE>(0)?;

        let (size, _) = measure(writer, |writer| {
            writer.write_u32::<LE>(self.items.len() as _)?;
            let mut hash_width = None;
            for item in &self.items {
                track_hash_width(&mut hash_width, item).map_err(mixed_widths)?;
                item.to_writer(writer)?;
            }
            Ok::<_, io::Error>(())
        })?;

        window_at(writer, size_pos, |writer| writer.write_u32::<LE>(size as _))?;

        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::property::values;

    #[test]
    fn takes_its_item_kind_from_the_type() {
        let list = Container::from(vec![values::I32::new(1), values::I32::new(2)]);
        assert_eq!(list.item_kind(), Kind::I32);
        assert_eq!(list.len(), 2);

        // An empty iterator still knows what it would have held.
        let list: Container = std::iter::empty::<values::String>().collect();
        assert_eq!(list.item_kind(), Kind::String);
        assert!(list.is_empty());
    }

    #[test]
    fn infers_its_item_kind_from_the_first_item() {
        let items: Vec<PropertyValueEnum> =
            vec![values::U8::new(1).into(), values::U8::new(2).into()];
        let list: Container = Container::try_from(items).unwrap();
        assert_eq!(list.item_kind(), Kind::U8);

        // Nothing to infer from.
        let empty: Result<Container, _> = Container::try_from(Vec::<PropertyValueEnum>::new());
        assert!(matches!(empty, Err(Error::EmptyContainer)));
    }

    #[test]
    fn rejects_an_item_that_is_not_its_item_kind() {
        assert!(matches!(
            Container::new(Kind::I32, vec![values::String::from("no").into()]),
            Err(Error::MismatchedContainerTypes {
                expected: Kind::I32,
                got: Kind::String
            })
        ));

        let mut list = Container::empty(Kind::I32).unwrap();
        assert!(matches!(
            list.push(values::String::from("no").into()),
            Err(Error::MismatchedContainerTypes {
                expected: Kind::I32,
                got: Kind::String
            })
        ));
        assert!(list.push(values::I32::new(1).into()).is_ok());
        assert_eq!(list.len(), 1);
    }

    #[test]
    fn rejects_a_nested_container() {
        // `ContainerItem` keeps the same kinds out of `from`/`from_iter` at compile time.
        for kind in [
            Kind::Container,
            Kind::UnorderedContainer,
            Kind::Optional,
            Kind::Map,
        ] {
            assert!(matches!(
                Container::empty(kind),
                Err(Error::InvalidNesting(k)) if k == kind
            ));
        }
    }

    #[test]
    fn borrows_its_items() {
        let list = Container::from(vec![values::I32::new(1), values::I32::new(2)]);

        assert_eq!(list.get(0), Some(&values::I32::new(1).into()));
        assert_eq!(list.get(2), None);
        assert_eq!(list.items().len(), 2);
        assert_eq!(list.into_items().len(), 2);
    }

    /// An item can be edited in place or replaced by its own kind, and by nothing else.
    #[test]
    fn pins_its_item_kind_to_the_slots_it_hands_out() {
        use crate::property::ValueMut;

        let mut list = Container::from(vec![values::I32::new(1), values::I32::new(2)]);
        assert!(list.slot(2).is_none());

        let mut slot = list.slot(1).unwrap();
        assert_eq!(slot.pinned_kind(), Some(Kind::I32));

        assert!(matches!(
            slot.set(values::String::from("no").into()),
            Err(Error::MismatchedContainerTypes {
                expected: Kind::I32,
                got: Kind::String
            })
        ));
        assert_eq!(
            slot.set(values::I32::new(7).into()).unwrap(),
            values::I32::new(2).into()
        );

        let ValueMut::I32(item) = slot.as_mut() else {
            panic!("the slot holds an i32");
        };
        item.value += 1;

        assert_eq!(list.item_kind(), Kind::I32);
        assert_eq!(
            list.items(),
            [values::I32::new(1).into(), values::I32::new(8).into()]
        );
    }
}