Skip to main content

rakata_formats/gff/
reader.rs

1//! GFF V3.2 binary reader.
2
3use std::io::Read;
4
5use rakata_core::{decode_text_strict, text_encoding_for_language, ResRef, StrRef};
6
7use super::{
8    binary, to_usize, FieldType, Gff, GffBinaryError, GffField, GffLocalizedString,
9    GffLocalizedSubstring, GffStruct, GffValue, DEFAULT_TEXT_ENCODING, FIELD_ENTRY_SIZE,
10    GFF_HEADER_SIZE, GFF_VERSION_V32, LABEL_SIZE, STRUCT_ENTRY_SIZE,
11};
12
13/// Reads a binary GFF file from a reader.
14///
15/// The stream is consumed from its current position.
16///
17/// # Errors
18///
19/// [`GffBinaryError::Io`] when the stream will not read to end, and whatever
20/// [`read_gff_from_bytes`] reports for the bytes it collected.
21#[cfg_attr(
22    feature = "tracing",
23    tracing::instrument(level = "debug", skip(reader))
24)]
25pub fn read_gff<R: Read>(reader: &mut R) -> Result<Gff, GffBinaryError> {
26    let mut bytes = Vec::new();
27    reader.read_to_end(&mut bytes)?;
28    crate::trace_debug!(bytes_len = bytes.len(), "read gff bytes from reader");
29    read_gff_from_bytes(&bytes)
30}
31
32/// Reads a binary GFF file from bytes.
33///
34/// # Errors
35///
36/// [`GffBinaryError::InvalidVersion`] for a version other than `V3.2`. There
37/// is no magic check: a GFF's first four bytes are its *file type*, which
38/// varies per format, so a file that is not a GFF fails on its version or its
39/// layout rather than on a signature.
40///
41/// [`GffBinaryError::InvalidHeader`] when the header is truncated or a table
42/// offset overflows, and [`GffBinaryError::InvalidData`] for a tree that does
43/// not hang together: a struct or field index past its table, a field-indices
44/// run outside the index array, or a nesting depth past twice the struct
45/// count. That bound is the cycle guard, and it is deliberately loose, since a
46/// legitimate file can need more depth than it has structs.
47///
48/// [`GffBinaryError::InvalidFieldType`] for a type code this reader will not
49/// guess at, [`GffBinaryError::TextDecoding`] for a string that is not valid
50/// text in its declared encoding, and
51/// [`GffBinaryError::UnsupportedLanguageEncoding`] for a localized string
52/// whose language id maps to no codepage.
53#[cfg_attr(
54    feature = "tracing",
55    tracing::instrument(level = "debug", skip(bytes), fields(bytes_len = bytes.len()))
56)]
57pub fn read_gff_from_bytes(bytes: &[u8]) -> Result<Gff, GffBinaryError> {
58    let header = parse_header(bytes)?;
59    let labels = read_labels(bytes, &header)?;
60    let parser = GffParser {
61        bytes,
62        header,
63        labels,
64    };
65    let root = parser.read_struct(0, 0)?;
66    crate::trace_debug!(
67        file_type = ?parser.header.file_type,
68        struct_count = parser.header.struct_count,
69        field_count = parser.header.field_count,
70        label_count = parser.header.label_count,
71        root_field_count = root.fields.len(),
72        "parsed gff from bytes"
73    );
74    Ok(Gff {
75        file_type: parser.header.file_type,
76        root,
77    })
78}
79
80#[derive(Debug, Clone, Copy)]
81struct GffHeader {
82    file_type: [u8; 4],
83    struct_offset: usize,
84    struct_count: usize,
85    field_offset: usize,
86    field_count: usize,
87    label_offset: usize,
88    label_count: usize,
89    field_data_offset: usize,
90    field_data_count: usize,
91    field_indices_offset: usize,
92    field_indices_count: usize,
93    list_indices_offset: usize,
94    list_indices_count: usize,
95}
96
97struct GffParser<'a> {
98    bytes: &'a [u8],
99    header: GffHeader,
100    labels: Vec<super::GffLabel>,
101}
102
103impl<'a> GffParser<'a> {
104    fn read_struct(&self, struct_index: usize, depth: usize) -> Result<GffStruct, GffBinaryError> {
105        // Depth alternates struct -> field -> nested struct, growing by
106        // one per hop. The longest legitimate acyclic path visits each
107        // struct once and reads its fields, so max depth is
108        // 2 * struct_count - 1. Use 2 * struct_count as the trip
109        // bound (saturating for paranoia against malformed headers).
110        let depth_limit = self.header.struct_count.saturating_mul(2);
111        if depth > depth_limit {
112            return Err(GffBinaryError::InvalidData(
113                "detected nested struct cycle while reading".into(),
114            ));
115        }
116        if struct_index >= self.header.struct_count {
117            return Err(GffBinaryError::InvalidData(format!(
118                "struct index {struct_index} out of range"
119            )));
120        }
121
122        let base = self
123            .header
124            .struct_offset
125            .checked_add(
126                struct_index
127                    .checked_mul(STRUCT_ENTRY_SIZE)
128                    .ok_or_else(|| GffBinaryError::InvalidHeader("struct index overflow".into()))?,
129            )
130            .ok_or_else(|| GffBinaryError::InvalidHeader("struct base overflow".into()))?;
131        let struct_id = read_i32(self.bytes, base)?;
132        let data_or_offset = binary::read_u32(self.bytes, base + 4)?;
133        let field_count = usize::try_from(binary::read_u32(self.bytes, base + 8)?)
134            .map_err(|_| GffBinaryError::InvalidData("field count does not fit usize".into()))?;
135
136        let mut fields = Vec::with_capacity(field_count);
137        match field_count {
138            0 => {}
139            1 => {
140                let field_index = usize::try_from(data_or_offset).map_err(|_| {
141                    GffBinaryError::InvalidData("single field index does not fit usize".into())
142                })?;
143                fields.push(self.read_field(field_index, depth + 1)?);
144            }
145            _ => {
146                let indices_rel_offset = usize::try_from(data_or_offset).map_err(|_| {
147                    GffBinaryError::InvalidData("field indices offset does not fit usize".into())
148                })?;
149                let indices_size = field_count.checked_mul(4).ok_or_else(|| {
150                    GffBinaryError::InvalidData("field indices size overflow".into())
151                })?;
152                if indices_rel_offset
153                    .checked_add(indices_size)
154                    .is_none_or(|end| end > self.header.field_indices_count)
155                {
156                    return Err(GffBinaryError::InvalidData(format!(
157                        "field indices block out of range for struct {struct_index}"
158                    )));
159                }
160                let indices_base = self
161                    .header
162                    .field_indices_offset
163                    .checked_add(indices_rel_offset)
164                    .ok_or_else(|| {
165                        GffBinaryError::InvalidData("field indices absolute offset overflow".into())
166                    })?;
167                for i in 0..field_count {
168                    let field_index =
169                        usize::try_from(binary::read_u32(self.bytes, indices_base + i * 4)?)
170                            .map_err(|_| {
171                                GffBinaryError::InvalidData(
172                                    "field index from field-indices table does not fit usize"
173                                        .into(),
174                                )
175                            })?;
176                    fields.push(self.read_field(field_index, depth + 1)?);
177                }
178            }
179        }
180
181        Ok(GffStruct { struct_id, fields })
182    }
183
184    fn read_field(&self, field_index: usize, depth: usize) -> Result<GffField, GffBinaryError> {
185        if field_index >= self.header.field_count {
186            return Err(GffBinaryError::InvalidData(format!(
187                "field index {field_index} out of range"
188            )));
189        }
190        let base = self
191            .header
192            .field_offset
193            .checked_add(
194                field_index
195                    .checked_mul(FIELD_ENTRY_SIZE)
196                    .ok_or_else(|| GffBinaryError::InvalidHeader("field index overflow".into()))?,
197            )
198            .ok_or_else(|| GffBinaryError::InvalidHeader("field base overflow".into()))?;
199
200        let field_type_raw = binary::read_u32(self.bytes, base)?;
201        let field_type = FieldType::try_from(field_type_raw)
202            .map_err(|_| GffBinaryError::InvalidFieldType(field_type_raw))?;
203        let label_index = usize::try_from(binary::read_u32(self.bytes, base + 4)?)
204            .map_err(|_| GffBinaryError::InvalidData("label index does not fit usize".into()))?;
205        let data_or_offset = binary::read_u32(self.bytes, base + 8)?;
206        let label = *self.labels.get(label_index).ok_or_else(|| {
207            GffBinaryError::InvalidData(format!("label index {label_index} out of range"))
208        })?;
209
210        let value = match field_type {
211            FieldType::UInt8 => {
212                GffValue::UInt8(u8::try_from(data_or_offset & 0xFF).expect("masked to 8 bits"))
213            }
214            FieldType::Int8 => {
215                GffValue::Int8(i8::from_le_bytes([
216                    u8::try_from(data_or_offset & 0xFF).expect("masked to 8 bits")
217                ]))
218            }
219            FieldType::UInt16 => {
220                GffValue::UInt16(u16::try_from(data_or_offset & 0xFFFF).expect("masked to 16 bits"))
221            }
222            FieldType::Int16 => GffValue::Int16(i16::from_le_bytes(
223                u16::try_from(data_or_offset & 0xFFFF)
224                    .expect("masked to 16 bits")
225                    .to_le_bytes(),
226            )),
227            FieldType::UInt32 => GffValue::UInt32(data_or_offset),
228            FieldType::Int32 => GffValue::Int32(i32::from_le_bytes(data_or_offset.to_le_bytes())),
229            FieldType::Single => GffValue::Single(f32::from_bits(data_or_offset)),
230            FieldType::UInt64 => GffValue::UInt64(read_u64_at_field_data(self, data_or_offset)?),
231            FieldType::Int64 => GffValue::Int64(i64::from_le_bytes(
232                read_u64_at_field_data(self, data_or_offset)?.to_le_bytes(),
233            )),
234            FieldType::Double => GffValue::Double(f64::from_bits(read_u64_at_field_data(
235                self,
236                data_or_offset,
237            )?)),
238            FieldType::String => GffValue::String(read_sized_string(
239                self,
240                data_or_offset,
241                format!("field[{field_index}] string"),
242            )?),
243            FieldType::ResRef => {
244                let raw = read_sized_string_u8(
245                    self,
246                    data_or_offset,
247                    format!("field[{field_index}] resref"),
248                )?;
249                let resref = ResRef::new(&raw).map_err(|err| {
250                    GffBinaryError::InvalidData(format!(
251                        "field[{field_index}] resref `{raw}`: {err}"
252                    ))
253                })?;
254                GffValue::ResRef(resref)
255            }
256            FieldType::LocalizedString => {
257                GffValue::LocalizedString(read_localized_string(self, data_or_offset, field_index)?)
258            }
259            FieldType::Binary => GffValue::Binary(read_binary_blob(self, data_or_offset)?),
260            FieldType::Struct => {
261                let struct_index = usize::try_from(data_or_offset).map_err(|_| {
262                    GffBinaryError::InvalidData("nested struct index does not fit usize".into())
263                })?;
264                GffValue::Struct(Box::new(self.read_struct(struct_index, depth + 1)?))
265            }
266            FieldType::List => GffValue::List(read_struct_list(self, data_or_offset, depth + 1)?),
267            FieldType::Vector4 => GffValue::Vector4(read_vector4(self, data_or_offset)?),
268            FieldType::Vector3 => GffValue::Vector3(read_vector3(self, data_or_offset)?),
269        };
270
271        Ok(GffField { label, value })
272    }
273}
274
275fn parse_header(bytes: &[u8]) -> Result<GffHeader, GffBinaryError> {
276    if bytes.len() < GFF_HEADER_SIZE {
277        return Err(GffBinaryError::InvalidHeader(
278            "file smaller than GFF header".into(),
279        ));
280    }
281    let file_type = binary::read_fourcc(bytes, 0)?;
282    let version = binary::read_fourcc(bytes, 4)?;
283    binary::expect_fourcc(version, GFF_VERSION_V32).map_err(GffBinaryError::InvalidVersion)?;
284
285    let struct_offset = to_usize(binary::read_u32(bytes, 8)?, "struct_offset")?;
286    let struct_count = to_usize(binary::read_u32(bytes, 12)?, "struct_count")?;
287    let field_offset = to_usize(binary::read_u32(bytes, 16)?, "field_offset")?;
288    let field_count = to_usize(binary::read_u32(bytes, 20)?, "field_count")?;
289    let label_offset = to_usize(binary::read_u32(bytes, 24)?, "label_offset")?;
290    let label_count = to_usize(binary::read_u32(bytes, 28)?, "label_count")?;
291    let field_data_offset = to_usize(binary::read_u32(bytes, 32)?, "field_data_offset")?;
292    let field_data_count = to_usize(binary::read_u32(bytes, 36)?, "field_data_count")?;
293    let field_indices_offset = to_usize(binary::read_u32(bytes, 40)?, "field_indices_offset")?;
294    let field_indices_count = to_usize(binary::read_u32(bytes, 44)?, "field_indices_count")?;
295    let list_indices_offset = to_usize(binary::read_u32(bytes, 48)?, "list_indices_offset")?;
296    let list_indices_count = to_usize(binary::read_u32(bytes, 52)?, "list_indices_count")?;
297
298    check_table_bounds(
299        bytes.len(),
300        struct_offset,
301        struct_count
302            .checked_mul(STRUCT_ENTRY_SIZE)
303            .ok_or(GffBinaryError::InvalidHeader(
304                "struct table size overflow".into(),
305            ))?,
306        "struct table",
307    )?;
308    check_table_bounds(
309        bytes.len(),
310        field_offset,
311        field_count
312            .checked_mul(FIELD_ENTRY_SIZE)
313            .ok_or(GffBinaryError::InvalidHeader(
314                "field table size overflow".into(),
315            ))?,
316        "field table",
317    )?;
318    check_table_bounds(
319        bytes.len(),
320        label_offset,
321        label_count
322            .checked_mul(LABEL_SIZE)
323            .ok_or(GffBinaryError::InvalidHeader(
324                "label table size overflow".into(),
325            ))?,
326        "label table",
327    )?;
328    check_table_bounds(
329        bytes.len(),
330        field_data_offset,
331        field_data_count,
332        "field data",
333    )?;
334    check_table_bounds(
335        bytes.len(),
336        field_indices_offset,
337        field_indices_count,
338        "field indices",
339    )?;
340    check_table_bounds(
341        bytes.len(),
342        list_indices_offset,
343        list_indices_count,
344        "list indices",
345    )?;
346
347    if struct_count == 0 {
348        return Err(GffBinaryError::InvalidHeader(
349            "struct table is empty (missing root struct)".into(),
350        ));
351    }
352
353    Ok(GffHeader {
354        file_type,
355        struct_offset,
356        struct_count,
357        field_offset,
358        field_count,
359        label_offset,
360        label_count,
361        field_data_offset,
362        field_data_count,
363        field_indices_offset,
364        field_indices_count,
365        list_indices_offset,
366        list_indices_count,
367    })
368}
369
370fn read_labels(bytes: &[u8], header: &GffHeader) -> Result<Vec<super::GffLabel>, GffBinaryError> {
371    let mut labels = Vec::with_capacity(header.label_count);
372    for label_index in 0..header.label_count {
373        let offset = header
374            .label_offset
375            .checked_add(
376                label_index
377                    .checked_mul(LABEL_SIZE)
378                    .ok_or_else(|| GffBinaryError::InvalidHeader("label offset overflow".into()))?,
379            )
380            .ok_or_else(|| GffBinaryError::InvalidHeader("label base overflow".into()))?;
381        let raw = bytes
382            .get(offset..offset + LABEL_SIZE)
383            .ok_or_else(|| GffBinaryError::InvalidHeader("label slice out of range".into()))?;
384        let end = raw.iter().position(|byte| *byte == 0).unwrap_or(LABEL_SIZE);
385        let label = decode_text_strict(&raw[..end], DEFAULT_TEXT_ENCODING).map_err(|source| {
386            GffBinaryError::TextDecoding {
387                context: format!("label[{label_index}]"),
388                source,
389            }
390        })?;
391        let gff_label = super::GffLabel::new(&label).map_err(|err| {
392            GffBinaryError::InvalidData(format!("label[{label_index}] `{label}` is invalid: {err}"))
393        })?;
394        labels.push(gff_label);
395    }
396    Ok(labels)
397}
398
399fn read_u64_at_field_data(parser: &GffParser<'_>, offset: u32) -> Result<u64, GffBinaryError> {
400    let offset = to_usize(offset, "field_data_offset")?;
401    let absolute = parser
402        .header
403        .field_data_offset
404        .checked_add(offset)
405        .ok_or_else(|| GffBinaryError::InvalidData("field data absolute offset overflow".into()))?;
406    let end_rel = offset
407        .checked_add(8)
408        .ok_or_else(|| GffBinaryError::InvalidData("field data u64 end overflow".into()))?;
409    if end_rel > parser.header.field_data_count {
410        return Err(GffBinaryError::InvalidData(
411            "field data u64 read out of range".into(),
412        ));
413    }
414    Ok(binary::read_u64(parser.bytes, absolute)?)
415}
416
417fn read_sized_string(
418    parser: &GffParser<'_>,
419    offset: u32,
420    context: String,
421) -> Result<String, GffBinaryError> {
422    let offset = to_usize(offset, "field_data_offset")?;
423    let base = parser
424        .header
425        .field_data_offset
426        .checked_add(offset)
427        .ok_or_else(|| GffBinaryError::InvalidData("string base overflow".into()))?;
428    let len = to_usize(binary::read_u32(parser.bytes, base)?, "string_length")?;
429    let data_rel_end = offset
430        .checked_add(4)
431        .and_then(|v| v.checked_add(len))
432        .ok_or_else(|| GffBinaryError::InvalidData("string end overflow".into()))?;
433    if data_rel_end > parser.header.field_data_count {
434        return Err(GffBinaryError::InvalidData(
435            "string payload exceeds field data bounds".into(),
436        ));
437    }
438    let data_start = base + 4;
439    let raw = parser
440        .bytes
441        .get(data_start..data_start + len)
442        .ok_or_else(|| GffBinaryError::InvalidData("string bytes out of range".into()))?;
443    decode_text_strict(raw, DEFAULT_TEXT_ENCODING)
444        .map_err(|source| GffBinaryError::TextDecoding { context, source })
445}
446
447fn read_sized_string_u8(
448    parser: &GffParser<'_>,
449    offset: u32,
450    context: String,
451) -> Result<String, GffBinaryError> {
452    let offset = to_usize(offset, "field_data_offset")?;
453    let base = parser
454        .header
455        .field_data_offset
456        .checked_add(offset)
457        .ok_or_else(|| GffBinaryError::InvalidData("resref base overflow".into()))?;
458    let len =
459        usize::from(*parser.bytes.get(base).ok_or_else(|| {
460            GffBinaryError::InvalidData("resref length byte out of range".into())
461        })?);
462    let data_rel_end = offset
463        .checked_add(1)
464        .and_then(|v| v.checked_add(len))
465        .ok_or_else(|| GffBinaryError::InvalidData("resref end overflow".into()))?;
466    if data_rel_end > parser.header.field_data_count {
467        return Err(GffBinaryError::InvalidData(
468            "resref payload exceeds field data bounds".into(),
469        ));
470    }
471    let data_start = base + 1;
472    let raw = parser
473        .bytes
474        .get(data_start..data_start + len)
475        .ok_or_else(|| GffBinaryError::InvalidData("resref bytes out of range".into()))?;
476    decode_text_strict(raw, DEFAULT_TEXT_ENCODING)
477        .map_err(|source| GffBinaryError::TextDecoding { context, source })
478}
479
480fn read_binary_blob(parser: &GffParser<'_>, offset: u32) -> Result<Vec<u8>, GffBinaryError> {
481    let offset = to_usize(offset, "field_data_offset")?;
482    let base = parser
483        .header
484        .field_data_offset
485        .checked_add(offset)
486        .ok_or_else(|| GffBinaryError::InvalidData("binary base overflow".into()))?;
487    let len = to_usize(binary::read_u32(parser.bytes, base)?, "binary_length")?;
488    let data_rel_end = offset
489        .checked_add(4)
490        .and_then(|v| v.checked_add(len))
491        .ok_or_else(|| GffBinaryError::InvalidData("binary end overflow".into()))?;
492    if data_rel_end > parser.header.field_data_count {
493        return Err(GffBinaryError::InvalidData(
494            "binary payload exceeds field data bounds".into(),
495        ));
496    }
497    let data_start = base + 4;
498    let raw = parser
499        .bytes
500        .get(data_start..data_start + len)
501        .ok_or_else(|| GffBinaryError::InvalidData("binary bytes out of range".into()))?;
502    Ok(raw.to_vec())
503}
504
505fn read_vector4(parser: &GffParser<'_>, offset: u32) -> Result<[f32; 4], GffBinaryError> {
506    let offset = to_usize(offset, "field_data_offset")?;
507    let base = parser
508        .header
509        .field_data_offset
510        .checked_add(offset)
511        .ok_or_else(|| GffBinaryError::InvalidData("vector4 base overflow".into()))?;
512    let end_rel = offset
513        .checked_add(16)
514        .ok_or_else(|| GffBinaryError::InvalidData("vector4 end overflow".into()))?;
515    if end_rel > parser.header.field_data_count {
516        return Err(GffBinaryError::InvalidData(
517            "vector4 exceeds field data bounds".into(),
518        ));
519    }
520    Ok([
521        binary::read_f32(parser.bytes, base)?,
522        binary::read_f32(parser.bytes, base + 4)?,
523        binary::read_f32(parser.bytes, base + 8)?,
524        binary::read_f32(parser.bytes, base + 12)?,
525    ])
526}
527
528fn read_vector3(parser: &GffParser<'_>, offset: u32) -> Result<[f32; 3], GffBinaryError> {
529    let offset = to_usize(offset, "field_data_offset")?;
530    let base = parser
531        .header
532        .field_data_offset
533        .checked_add(offset)
534        .ok_or_else(|| GffBinaryError::InvalidData("vector3 base overflow".into()))?;
535    let end_rel = offset
536        .checked_add(12)
537        .ok_or_else(|| GffBinaryError::InvalidData("vector3 end overflow".into()))?;
538    if end_rel > parser.header.field_data_count {
539        return Err(GffBinaryError::InvalidData(
540            "vector3 exceeds field data bounds".into(),
541        ));
542    }
543    Ok([
544        binary::read_f32(parser.bytes, base)?,
545        binary::read_f32(parser.bytes, base + 4)?,
546        binary::read_f32(parser.bytes, base + 8)?,
547    ])
548}
549
550fn read_struct_list(
551    parser: &GffParser<'_>,
552    offset: u32,
553    depth: usize,
554) -> Result<Vec<GffStruct>, GffBinaryError> {
555    let offset = to_usize(offset, "list_indices_offset")?;
556    let count_base = parser
557        .header
558        .list_indices_offset
559        .checked_add(offset)
560        .ok_or_else(|| GffBinaryError::InvalidData("list base overflow".into()))?;
561    if offset
562        .checked_add(4)
563        .is_none_or(|end| end > parser.header.list_indices_count)
564    {
565        return Err(GffBinaryError::InvalidData(
566            "list count read out of range".into(),
567        ));
568    }
569    let count = to_usize(binary::read_u32(parser.bytes, count_base)?, "list_count")?;
570    let list_entries_size = count
571        .checked_mul(4)
572        .ok_or_else(|| GffBinaryError::InvalidData("list entries size overflow".into()))?;
573    if offset
574        .checked_add(4)
575        .and_then(|v| v.checked_add(list_entries_size))
576        .is_none_or(|end| end > parser.header.list_indices_count)
577    {
578        return Err(GffBinaryError::InvalidData(
579            "list entries out of range".into(),
580        ));
581    }
582    let mut out = Vec::with_capacity(count);
583    let entries_base = count_base + 4;
584    for index in 0..count {
585        let struct_index = to_usize(
586            binary::read_u32(parser.bytes, entries_base + index * 4)?,
587            "list_struct_index",
588        )?;
589        out.push(parser.read_struct(struct_index, depth + 1)?);
590    }
591    Ok(out)
592}
593
594fn read_localized_string(
595    parser: &GffParser<'_>,
596    offset: u32,
597    field_index: usize,
598) -> Result<GffLocalizedString, GffBinaryError> {
599    let offset = to_usize(offset, "field_data_offset")?;
600    let base = parser
601        .header
602        .field_data_offset
603        .checked_add(offset)
604        .ok_or_else(|| GffBinaryError::InvalidData("locstring base overflow".into()))?;
605    let total_size = to_usize(
606        binary::read_u32(parser.bytes, base)?,
607        "locstring_total_size",
608    )?;
609    let payload_rel_start = offset
610        .checked_add(4)
611        .ok_or_else(|| GffBinaryError::InvalidData("locstring payload start overflow".into()))?;
612    let payload_rel_end = payload_rel_start
613        .checked_add(total_size)
614        .ok_or_else(|| GffBinaryError::InvalidData("locstring payload end overflow".into()))?;
615    if payload_rel_end > parser.header.field_data_count {
616        return Err(GffBinaryError::InvalidData(format!(
617            "locstring payload out of range for field[{field_index}]"
618        )));
619    }
620
621    let payload_start = base + 4;
622    let string_ref = StrRef::from_raw(read_i32(parser.bytes, payload_start)?);
623    let substring_count = to_usize(
624        binary::read_u32(parser.bytes, payload_start + 4)?,
625        "substring_count",
626    )?;
627    let mut cursor = payload_start + 8;
628    let payload_end = payload_start + total_size;
629    let mut substrings = Vec::with_capacity(substring_count);
630
631    for substring_index in 0..substring_count {
632        if cursor.checked_add(8).is_none_or(|end| end > payload_end) {
633            return Err(GffBinaryError::InvalidData(format!(
634                "locstring header truncated at substring {substring_index}"
635            )));
636        }
637        let string_id = binary::read_u32(parser.bytes, cursor)?;
638        let length = to_usize(
639            binary::read_u32(parser.bytes, cursor + 4)?,
640            "substring_length",
641        )?;
642        cursor += 8;
643        if cursor
644            .checked_add(length)
645            .is_none_or(|end| end > payload_end)
646        {
647            return Err(GffBinaryError::InvalidData(format!(
648                "locstring substring bytes out of range at index {substring_index}"
649            )));
650        }
651        let bytes = parser.bytes.get(cursor..cursor + length).ok_or_else(|| {
652            GffBinaryError::InvalidData("locstring substring slice invalid".into())
653        })?;
654        let language_id = string_id / 2;
655        let encoding = text_encoding_for_language(language_id)
656            // Optional enhancement track: language IDs 70..=72 remain
657            // unsupported by default because they are not required for
658            // vanilla K1/K2 parity.
659            .map_err(|err| GffBinaryError::UnsupportedLanguageEncoding(err.language_id.raw()))?;
660        let text =
661            decode_text_strict(bytes, encoding).map_err(|source| GffBinaryError::TextDecoding {
662                context: format!("field[{field_index}] locstring[{substring_index}]"),
663                source,
664            })?;
665        substrings.push(GffLocalizedSubstring { string_id, text });
666        cursor += length;
667    }
668
669    if cursor != payload_end {
670        return Err(GffBinaryError::InvalidData(format!(
671            "locstring payload has {} trailing bytes",
672            payload_end - cursor
673        )));
674    }
675
676    Ok(GffLocalizedString {
677        string_ref,
678        substrings,
679    })
680}
681
682fn check_table_bounds(
683    total_len: usize,
684    offset: usize,
685    size: usize,
686    table_name: &str,
687) -> Result<(), GffBinaryError> {
688    binary::check_range_in_bounds(total_len, offset, size, table_name)?;
689    Ok(())
690}
691
692fn read_i32(bytes: &[u8], offset: usize) -> Result<i32, GffBinaryError> {
693    let bits = binary::read_u32(bytes, offset)?;
694    Ok(i32::from_le_bytes(bits.to_le_bytes()))
695}
696
697#[cfg(test)]
698mod tests {
699    use super::*;
700    use crate::gff::write_gff_to_vec;
701    use crate::gff_label;
702
703    const TEST_GFF: &[u8] = include_bytes!(concat!(
704        env!("CARGO_MANIFEST_DIR"),
705        "/../../fixtures/test.gff"
706    ));
707    const TEST_UTC: &[u8] = include_bytes!(concat!(
708        env!("CARGO_MANIFEST_DIR"),
709        "/../../fixtures/test.utc"
710    ));
711
712    #[test]
713    fn roundtrip_gff_binary_with_all_core_field_variants() {
714        let mut root = GffStruct::new(-1);
715        root.push_field(gff_label!("uint8"), GffValue::UInt8(255));
716        root.push_field(gff_label!("int8"), GffValue::Int8(-127));
717        root.push_field(gff_label!("uint16"), GffValue::UInt16(65535));
718        root.push_field(gff_label!("int16"), GffValue::Int16(-32768));
719        root.push_field(gff_label!("uint32"), GffValue::UInt32(u32::MAX));
720        root.push_field(gff_label!("int32"), GffValue::Int32(i32::MIN));
721        root.push_field(gff_label!("uint64"), GffValue::UInt64(4_294_967_296));
722        root.push_field(gff_label!("int64"), GffValue::Int64(2_147_483_647));
723        root.push_field(gff_label!("single"), GffValue::Single(12.34567));
724        root.push_field(gff_label!("double"), GffValue::Double(12.345678901234));
725        root.push_field(
726            gff_label!("string"),
727            GffValue::String("abcdefghij123456789".into()),
728        );
729        root.push_field(gff_label!("resref"), GffValue::resref_lit("resref01"));
730        root.push_field(
731            gff_label!("locstring"),
732            GffValue::LocalizedString(GffLocalizedString {
733                string_ref: StrRef::invalid(),
734                substrings: vec![
735                    GffLocalizedSubstring {
736                        string_id: 0,
737                        text: "male_eng".into(),
738                    },
739                    GffLocalizedSubstring {
740                        string_id: 5,
741                        text: "fem_german".into(),
742                    },
743                ],
744            }),
745        );
746        root.push_field(
747            gff_label!("binary"),
748            GffValue::Binary(b"binarydata".to_vec()),
749        );
750        root.push_field(
751            gff_label!("orientation"),
752            GffValue::Vector4([1.0, 2.0, 3.0, 4.0]),
753        );
754        root.push_field(
755            gff_label!("position"),
756            GffValue::Vector3([11.0, 22.0, 33.0]),
757        );
758
759        let mut child = GffStruct::new(0);
760        child.push_field(gff_label!("child_uint8"), GffValue::UInt8(4));
761        root.push_field(
762            gff_label!("child_struct"),
763            GffValue::Struct(Box::new(child)),
764        );
765        root.push_field(
766            gff_label!("list"),
767            GffValue::List(vec![GffStruct::new(1), GffStruct::new(2)]),
768        );
769
770        let original = Gff::generic(root);
771        let bytes = write_gff_to_vec(&original).expect("write should succeed");
772        let parsed = read_gff_from_bytes(&bytes).expect("read should succeed");
773        assert_eq!(parsed, original);
774    }
775
776    #[test]
777    fn parses_gff_fixture() {
778        let gff = read_gff_from_bytes(TEST_GFF).expect("fixture should parse");
779        assert_eq!(gff.file_type, *b"GFF ");
780
781        assert_eq!(find_field(&gff.root, "uint8"), &GffValue::UInt8(255));
782        assert_eq!(find_field(&gff.root, "int8"), &GffValue::Int8(-127));
783        assert_eq!(find_field(&gff.root, "uint16"), &GffValue::UInt16(65535));
784        assert_eq!(find_field(&gff.root, "int16"), &GffValue::Int16(-32768));
785        assert_eq!(find_field(&gff.root, "uint32"), &GffValue::UInt32(u32::MAX));
786        assert_eq!(find_field(&gff.root, "int32"), &GffValue::Int32(i32::MIN));
787        assert_eq!(
788            find_field(&gff.root, "uint64"),
789            &GffValue::UInt64(4_294_967_296)
790        );
791        assert_eq!(
792            find_field(&gff.root, "string"),
793            &GffValue::String("abcdefghij123456789".into())
794        );
795        assert_eq!(
796            find_field(&gff.root, "resref"),
797            &GffValue::resref_lit("resref01")
798        );
799        match find_field(&gff.root, "locstring") {
800            GffValue::LocalizedString(loc) => {
801                assert_eq!(loc.string_ref, StrRef::invalid());
802                assert_eq!(loc.substrings.len(), 2);
803                assert_eq!(loc.substrings[0].text, "male_eng");
804                assert_eq!(loc.substrings[1].text, "fem_german");
805            }
806            other => panic!("expected localized string, got {other:?}"),
807        }
808    }
809
810    #[test]
811    fn read_write_roundtrip_preserves_fixture_semantics() {
812        let parsed = read_gff_from_bytes(TEST_GFF).expect("read should succeed");
813        let bytes = write_gff_to_vec(&parsed).expect("write should succeed");
814        let reparsed = read_gff_from_bytes(&bytes).expect("re-read should succeed");
815        assert_eq!(reparsed, parsed);
816    }
817
818    #[test]
819    fn writer_is_deterministic_for_parsed_fixture() {
820        let parsed = read_gff_from_bytes(TEST_GFF).expect("fixture should parse");
821        let first = write_gff_to_vec(&parsed).expect("first write should succeed");
822        let second = write_gff_to_vec(&parsed).expect("second write should succeed");
823        assert_eq!(first, second, "canonical GFF writer output drifted");
824    }
825
826    #[test]
827    fn roundtrip_preserves_list_order_and_struct_ids() {
828        let mut first = GffStruct::new(500);
829        first.push_field(gff_label!("marker"), GffValue::UInt16(11));
830
831        let mut second = GffStruct::new(2);
832        second.push_field(gff_label!("marker"), GffValue::UInt16(22));
833
834        let mut third = GffStruct::new(9_999);
835        third.push_field(gff_label!("marker"), GffValue::UInt16(33));
836
837        let mut root = GffStruct::new(-1);
838        root.push_field(
839            gff_label!("ordered"),
840            GffValue::List(vec![first, second, third]),
841        );
842
843        let gff = Gff::generic(root);
844        let bytes = write_gff_to_vec(&gff).expect("write should succeed");
845        let reparsed = read_gff_from_bytes(&bytes).expect("read should succeed");
846
847        let list = find_list(&reparsed.root, "ordered");
848        assert_eq!(list_struct_ids(list), vec![500, 2, 9_999]);
849        assert_eq!(list_u16_field(list, "marker"), vec![11, 22, 33]);
850    }
851
852    #[test]
853    fn utc_fixture_roundtrip_preserves_list_indices_and_values() {
854        let parsed = read_gff_from_bytes(TEST_UTC).expect("fixture should parse");
855        let bytes = write_gff_to_vec(&parsed).expect("write should succeed");
856        let reparsed = read_gff_from_bytes(&bytes).expect("re-read should succeed");
857
858        for label in ["FeatList", "Equip_ItemList", "ItemList", "ClassList"] {
859            assert_eq!(
860                list_struct_ids(find_list(&parsed.root, label)),
861                list_struct_ids(find_list(&reparsed.root, label)),
862                "list struct_id order changed for {label}"
863            );
864        }
865
866        assert_eq!(
867            list_u16_field(find_list(&parsed.root, "FeatList"), "Feat"),
868            list_u16_field(find_list(&reparsed.root, "FeatList"), "Feat")
869        );
870        assert_eq!(
871            list_resref_field(find_list(&parsed.root, "Equip_ItemList"), "EquippedRes"),
872            list_resref_field(find_list(&reparsed.root, "Equip_ItemList"), "EquippedRes")
873        );
874        assert_eq!(
875            list_resref_field(find_list(&parsed.root, "ItemList"), "InventoryRes"),
876            list_resref_field(find_list(&reparsed.root, "ItemList"), "InventoryRes")
877        );
878    }
879
880    #[test]
881    fn rejects_invalid_version() {
882        let mut bytes = vec![0_u8; GFF_HEADER_SIZE];
883        bytes[0..4].copy_from_slice(b"GFF ");
884        bytes[4..8].copy_from_slice(b"V9.9");
885        let err = read_gff_from_bytes(&bytes).expect_err("must fail");
886        assert!(matches!(err, GffBinaryError::InvalidVersion(_)));
887    }
888
889    #[test]
890    fn rejects_truncated_header() {
891        let bytes = vec![0_u8; GFF_HEADER_SIZE - 1];
892        let err = read_gff_from_bytes(&bytes).expect_err("must fail");
893        assert!(matches!(err, GffBinaryError::InvalidHeader(_)));
894    }
895
896    #[test]
897    fn rejects_unknown_field_type() {
898        let mut bytes = TEST_GFF.to_vec();
899        let field_offset = usize::try_from(u32::from_le_bytes(
900            bytes[16..20].try_into().expect("field offset bytes"),
901        ))
902        .expect("offset fits in usize");
903        bytes[field_offset..field_offset + 4].copy_from_slice(&99_u32.to_le_bytes());
904
905        let err = read_gff_from_bytes(&bytes).expect_err("must fail");
906        assert!(matches!(err, GffBinaryError::InvalidFieldType(99)));
907    }
908
909    #[test]
910    fn surfaces_field_type_eighteen_rather_than_guessing() {
911        // Code 18 is defined by the format but no file attests whether it
912        // holds its value immediately or points into the field data, so
913        // either branch would read a plausible number out of a region
914        // nothing licenses.
915        let mut bytes = TEST_GFF.to_vec();
916        let field_offset = usize::try_from(u32::from_le_bytes(
917            bytes[16..20].try_into().expect("field offset bytes"),
918        ))
919        .expect("offset fits in usize");
920        bytes[field_offset..field_offset + 4].copy_from_slice(&18_u32.to_le_bytes());
921
922        let err = read_gff_from_bytes(&bytes).expect_err("must fail");
923        assert!(matches!(err, GffBinaryError::InvalidFieldType(18)));
924    }
925
926    #[test]
927    fn accepts_two_struct_gff_with_nested_list_entry() {
928        // Regression for a false-positive cycle-detection trip. A GFF
929        // with only two structs total (root plus one list entry)
930        // needs depth 3 to read the list entry's fields, so a depth
931        // bound of `struct_count` rejects it with a bogus "detected
932        // nested struct cycle". About a quarter of vanilla K1 .uti
933        // blueprints hit this shape.
934        let mut entry = GffStruct::new(0);
935        entry.push_field(gff_label!("PropertyName"), GffValue::UInt16(11));
936        entry.push_field(gff_label!("Subtype"), GffValue::UInt16(5));
937
938        let mut root = GffStruct::new(-1);
939        root.push_field(gff_label!("PropertiesList"), GffValue::List(vec![entry]));
940
941        let gff = Gff::generic(root);
942        let bytes = write_gff_to_vec(&gff).expect("write should succeed");
943        let parsed = read_gff_from_bytes(&bytes).expect("two-struct GFF should parse");
944
945        let GffValue::List(list) = find_field(&parsed.root, "PropertiesList") else {
946            panic!("PropertiesList should be a list");
947        };
948        assert_eq!(list.len(), 1);
949        assert_eq!(list[0].field("PropertyName"), Some(&GffValue::UInt16(11)));
950    }
951
952    #[test]
953    fn writer_rejects_unsupported_locstring_language_ids() {
954        let mut root = GffStruct::new(-1);
955        root.push_field(
956            gff_label!("locstring"),
957            GffValue::LocalizedString(GffLocalizedString {
958                string_ref: StrRef::invalid(),
959                substrings: vec![GffLocalizedSubstring {
960                    string_id: 140,
961                    text: "test".into(),
962                }],
963            }),
964        );
965        let gff = Gff::generic(root);
966
967        let err = write_gff_to_vec(&gff).expect_err("must fail");
968        assert!(matches!(
969            err,
970            GffBinaryError::UnsupportedLanguageEncoding(70)
971        ));
972    }
973
974    fn find_field<'a>(structure: &'a GffStruct, label: &str) -> &'a GffValue {
975        structure
976            .field(label)
977            .unwrap_or_else(|| panic!("missing field {label}"))
978    }
979
980    fn find_list<'a>(structure: &'a GffStruct, label: &str) -> &'a [GffStruct] {
981        match structure.field(label) {
982            Some(GffValue::List(values)) => values.as_slice(),
983            Some(other) => panic!("field {label} is not a list: {other:?}"),
984            None => panic!("missing list field {label}"),
985        }
986    }
987
988    fn list_struct_ids(list: &[GffStruct]) -> Vec<i32> {
989        list.iter().map(|entry| entry.struct_id).collect::<Vec<_>>()
990    }
991
992    fn list_u16_field(list: &[GffStruct], label: &str) -> Vec<u16> {
993        list.iter()
994            .map(|entry| match entry.field(label) {
995                Some(GffValue::UInt16(value)) => *value,
996                Some(other) => panic!("field {label} is not UInt16: {other:?}"),
997                None => panic!("missing field {label}"),
998            })
999            .collect::<Vec<_>>()
1000    }
1001
1002    fn list_resref_field(list: &[GffStruct], label: &str) -> Vec<String> {
1003        list.iter()
1004            .map(|entry| match entry.field(label) {
1005                Some(GffValue::ResRef(value)) => value.to_string(),
1006                Some(other) => panic!("field {label} is not ResRef: {other:?}"),
1007                None => panic!("missing field {label}"),
1008            })
1009            .collect::<Vec<_>>()
1010    }
1011}