Skip to main content

rakata_formats/gff/
mod.rs

1//! GFF V3.2 binary reader and writer.
2//!
3//! GFF is a typed, table-backed binary graph format used for most KotOR game
4//! data objects (`UTC`, `UTI`, `ARE`, `DLG`, and others).
5//!
6//! ## Format Layout
7//! ```text
8//! +------------------------------+ 0x0000
9//! | Header (56 bytes)            |
10//! | file_type + version +        |
11//! | offsets/counts for tables    |
12//! +------------------------------+ struct_offset
13//! | Struct table                 |
14//! | 12 bytes * struct_count      |
15//! +------------------------------+ field_offset
16//! | Field table                  |
17//! | 12 bytes * field_count       |
18//! +------------------------------+ label_offset
19//! | Label table                  |
20//! | 16 bytes * label_count       |
21//! +------------------------------+ field_data_offset
22//! | Field data blob              |
23//! +------------------------------+ field_indices_offset
24//! | Field indices array (u32)    |
25//! +------------------------------+ list_indices_offset
26//! | List indices array (u32)     |
27//! +------------------------------+
28//! ```
29//!
30//! ## Logical Data Model
31//! ```text
32//! Gff
33//!  `-- root GffStruct
34//!       `-- [GffField(label, GffValue)]
35//!            `-- nested structs/lists recursively reference tables
36//! ```
37//!
38//! Offsets in struct and field records reference table locations or blob
39//! offsets depending on field type. This module keeps those rules explicit and
40//! validates all ranges before decoding.
41
42mod label;
43mod reader;
44mod writer;
45
46pub use label::*;
47pub use reader::{read_gff, read_gff_from_bytes};
48pub use writer::{write_gff, write_gff_to_vec};
49
50use num_enum::{IntoPrimitive, TryFromPrimitive};
51use thiserror::Error;
52
53use rakata_core::{DecodeTextError, EncodeTextError, ResRef, StrRef, TextEncoding};
54
55use crate::binary::{self, DecodeBinary, EncodeBinary};
56
57/// GFF V3.2 header size.
58const GFF_HEADER_SIZE: usize = 56;
59/// Size of one struct-table entry.
60const STRUCT_ENTRY_SIZE: usize = 12;
61/// Size of one field-table entry.
62const FIELD_ENTRY_SIZE: usize = 12;
63/// Fixed width for field labels in the label table.
64const LABEL_SIZE: usize = 16;
65/// Binary GFF version supported by KotOR.
66const GFF_VERSION_V32: [u8; 4] = *b"V3.2";
67/// Default encoding used for non-localized GFF text values.
68const DEFAULT_TEXT_ENCODING: TextEncoding = TextEncoding::Windows1252;
69
70/// In-memory representation of a binary GFF file.
71#[derive(Debug, Clone, PartialEq)]
72pub struct Gff {
73    /// Four-byte file type (`GFF `, `UTC `, `UTI `, ...).
74    pub file_type: [u8; 4],
75    /// Root structure.
76    pub root: GffStruct,
77}
78
79impl Gff {
80    /// Creates a GFF value with the provided file type and root structure.
81    pub fn new(file_type: [u8; 4], root: GffStruct) -> Self {
82        Self { file_type, root }
83    }
84
85    /// Creates a generic `GFF ` container.
86    pub fn generic(root: GffStruct) -> Self {
87        Self {
88            file_type: *b"GFF ",
89            root,
90        }
91    }
92}
93
94impl DecodeBinary for Gff {
95    type Error = GffBinaryError;
96
97    fn decode_binary(bytes: &[u8]) -> Result<Self, Self::Error> {
98        read_gff_from_bytes(bytes)
99    }
100}
101
102impl EncodeBinary for Gff {
103    type Error = GffBinaryError;
104
105    fn encode_binary(&self) -> Result<Vec<u8>, Self::Error> {
106        write_gff_to_vec(self)
107    }
108}
109
110/// One GFF struct node.
111#[derive(Debug, Clone, PartialEq)]
112pub struct GffStruct {
113    /// Struct ID from the binary table.
114    pub struct_id: i32,
115    /// Ordered fields for this struct.
116    pub fields: Vec<GffField>,
117}
118
119impl GffStruct {
120    /// Creates an empty struct with the given `struct_id`.
121    pub fn new(struct_id: i32) -> Self {
122        Self {
123            struct_id,
124            fields: Vec::new(),
125        }
126    }
127
128    /// Creates a struct with pre-populated fields.
129    pub fn with_fields(struct_id: i32, fields: Vec<GffField>) -> Self {
130        Self { struct_id, fields }
131    }
132
133    /// Appends a new field.
134    pub fn push_field(&mut self, label: impl TryInto<GffLabel>, value: GffValue) {
135        self.fields.push(GffField {
136            label: label
137                .try_into()
138                .unwrap_or_else(|_| panic!("failed to push field with invalid label")),
139            value,
140        });
141    }
142
143    /// Returns the first field value that matches `label`.
144    pub fn field(&self, label: &str) -> Option<&GffValue> {
145        self.fields
146            .iter()
147            .find(|field| field.label == label)
148            .map(|field| &field.value)
149    }
150}
151
152/// One labeled GFF field.
153#[derive(Debug, Clone, PartialEq)]
154pub struct GffField {
155    /// Field label.
156    pub label: GffLabel,
157    /// Field payload.
158    pub value: GffValue,
159}
160
161/// Runtime value for one GFF field.
162#[derive(Debug, Clone, PartialEq)]
163pub enum GffValue {
164    /// Unsigned 8-bit integer.
165    UInt8(u8),
166    /// Signed 8-bit integer.
167    Int8(i8),
168    /// Unsigned 16-bit integer.
169    UInt16(u16),
170    /// Signed 16-bit integer.
171    Int16(i16),
172    /// Unsigned 32-bit integer.
173    UInt32(u32),
174    /// Signed 32-bit integer.
175    Int32(i32),
176    /// Unsigned 64-bit integer.
177    UInt64(u64),
178    /// Signed 64-bit integer.
179    Int64(i64),
180    /// 32-bit floating point.
181    Single(f32),
182    /// 64-bit floating point.
183    Double(f64),
184    /// CExoString.
185    String(String),
186    /// CResRef canonicalized to the typed resource reference.
187    ResRef(ResRef),
188    /// CExoLocString payload.
189    LocalizedString(GffLocalizedString),
190    /// Arbitrary binary blob.
191    Binary(Vec<u8>),
192    /// Nested struct.
193    Struct(Box<GffStruct>),
194    /// Struct list.
195    List(Vec<GffStruct>),
196    /// Vector4 / orientation.
197    Vector4([f32; 4]),
198    /// Vector3 / position.
199    Vector3([f32; 3]),
200    /// StrRef extension field (type id 18).
201    StrRef(StrRef),
202}
203
204impl GffValue {
205    /// Test/fixture helper: constructs a [`GffValue::ResRef`] from a string
206    /// literal, panicking on invalid input.
207    ///
208    /// Production code should construct `GffValue::ResRef(resref)` directly
209    /// with a validated [`ResRef`]. This helper exists so fixture generators
210    /// and unit tests can avoid boilerplate when the input is a known-valid
211    /// literal.
212    #[doc(hidden)]
213    pub fn resref_lit(value: &str) -> Self {
214        GffValue::ResRef(ResRef::new(value).expect("valid resref literal"))
215    }
216}
217
218/// CExoLocString payload.
219#[derive(Debug, Clone, PartialEq, Eq)]
220pub struct GffLocalizedString {
221    /// TLK string reference (`StrRef::invalid()` means use substrings).
222    pub string_ref: StrRef,
223    /// Embedded localized substrings.
224    pub substrings: Vec<GffLocalizedSubstring>,
225}
226
227impl Default for GffLocalizedString {
228    /// An empty localized string: no TLK reference and no substrings.
229    ///
230    /// `StrRef::invalid()` is the engine's own "no TLK entry" marker rather
231    /// than a stand-in, so this is the genuine empty value for the type.
232    fn default() -> Self {
233        Self::new(StrRef::invalid())
234    }
235}
236
237impl GffLocalizedString {
238    /// Creates an empty localized string.
239    pub fn new(string_ref: impl Into<StrRef>) -> Self {
240        Self {
241            string_ref: string_ref.into(),
242            substrings: Vec::new(),
243        }
244    }
245}
246
247/// One localized substring entry.
248#[derive(Debug, Clone, PartialEq, Eq)]
249pub struct GffLocalizedSubstring {
250    /// Packed string ID (`language_id * 2 + gender`).
251    pub string_id: u32,
252    /// Decoded text payload.
253    pub text: String,
254}
255
256impl GffLocalizedSubstring {
257    /// Returns the language ID portion (`string_id / 2`).
258    pub fn language_id(&self) -> u32 {
259        self.string_id / 2
260    }
261
262    /// Returns `true` for feminine entries (`string_id % 2 == 1`).
263    pub fn is_feminine(&self) -> bool {
264        self.string_id % 2 == 1
265    }
266}
267
268/// Errors produced while parsing or writing binary GFF data.
269#[derive(Debug, Error)]
270pub enum GffBinaryError {
271    /// I/O read/write failure.
272    #[error(transparent)]
273    Io(#[from] std::io::Error),
274    /// Header/body layout is invalid or truncated.
275    #[error("invalid GFF header: {0}")]
276    InvalidHeader(String),
277    /// GFF version is unsupported.
278    #[error("invalid GFF version: {0:?}")]
279    InvalidVersion([u8; 4]),
280    /// Encountered an unknown field type ID.
281    #[error("invalid GFF field type id: {0}")]
282    InvalidFieldType(u32),
283    /// In-memory data is not valid for binary serialization.
284    #[error("invalid GFF data: {0}")]
285    InvalidData(String),
286    /// Value cannot fit the target on-disk width.
287    #[error("value overflow while writing `{0}`")]
288    ValueOverflow(&'static str),
289    /// Label exceeds 16 bytes after encoding.
290    #[error("label `{label}` encoded length {len} exceeds maximum {max}")]
291    LabelTooLong {
292        /// Label text.
293        label: String,
294        /// Encoded byte length.
295        len: usize,
296        /// Maximum allowed byte length.
297        max: usize,
298    },
299    /// Text cannot be represented in the target encoding.
300    #[error("GFF text encoding failed for {context}: {source}")]
301    TextEncoding {
302        /// Context path for error reporting.
303        context: String,
304        /// Source encoding error.
305        #[source]
306        source: EncodeTextError,
307    },
308    /// Text bytes cannot be decoded losslessly.
309    #[error("GFF text decoding failed for {context}: {source}")]
310    TextDecoding {
311        /// Context path for error reporting.
312        context: String,
313        /// Source decoding error.
314        #[source]
315        source: DecodeTextError,
316    },
317    /// Language ID maps to an unsupported encoding.
318    #[error("unsupported language id {0} for localized string encoding")]
319    UnsupportedLanguageEncoding(u32),
320}
321
322impl From<binary::BinaryLayoutError> for GffBinaryError {
323    fn from(error: binary::BinaryLayoutError) -> Self {
324        Self::InvalidHeader(error.to_string())
325    }
326}
327
328#[derive(Debug, Clone, Copy, PartialEq, Eq, IntoPrimitive, TryFromPrimitive)]
329#[repr(u32)]
330pub(super) enum FieldType {
331    UInt8 = 0,
332    Int8 = 1,
333    UInt16 = 2,
334    Int16 = 3,
335    UInt32 = 4,
336    Int32 = 5,
337    UInt64 = 6,
338    Int64 = 7,
339    Single = 8,
340    Double = 9,
341    String = 10,
342    ResRef = 11,
343    LocalizedString = 12,
344    Binary = 13,
345    Struct = 14,
346    List = 15,
347    Vector4 = 16,
348    Vector3 = 17,
349    StrRef = 18,
350}
351
352pub(super) fn to_u32(value: usize, name: &'static str) -> Result<u32, GffBinaryError> {
353    u32::try_from(value).map_err(|_| GffBinaryError::ValueOverflow(name))
354}
355
356pub(super) fn to_usize(value: u32, name: &'static str) -> Result<usize, GffBinaryError> {
357    binary::checked_to_usize(value, name).map_err(|_| {
358        GffBinaryError::InvalidData(format!("{name} does not fit target platform usize"))
359    })
360}
361
362//
363// Serde Support (JSON)
364//
365
366#[cfg(feature = "serde")]
367/// JSON serialization support for GFF.
368pub mod serde_json_fmt {
369    use super::*;
370    use serde::{Deserialize, Serialize};
371    use serde_json::{from_slice, from_str, to_string_pretty, to_vec};
372    use std::collections::BTreeMap;
373
374    /// Serializes a GFF to JSON.
375    pub fn write_gff_to_json(gff: &Gff) -> Result<String, GffBinaryError> {
376        let dto = GffDto::from(gff);
377        to_string_pretty(&dto).map_err(|e| GffBinaryError::InvalidData(e.to_string()))
378    }
379
380    /// Serializes a GFF to JSON bytes.
381    pub fn write_gff_to_json_vec(gff: &Gff) -> Result<Vec<u8>, GffBinaryError> {
382        let dto = GffDto::from(gff);
383        to_vec(&dto).map_err(|e| GffBinaryError::InvalidData(e.to_string()))
384    }
385
386    /// Deserializes a GFF from JSON.
387    pub fn read_gff_from_json(json: &str) -> Result<Gff, GffBinaryError> {
388        let dto: GffDto = from_str(json).map_err(|e| GffBinaryError::InvalidData(e.to_string()))?;
389        Gff::try_from(dto)
390    }
391
392    /// Deserializes a GFF from JSON bytes.
393    pub fn read_gff_from_json_bytes(bytes: &[u8]) -> Result<Gff, GffBinaryError> {
394        let dto: GffDto =
395            from_slice(bytes).map_err(|e| GffBinaryError::InvalidData(e.to_string()))?;
396        Gff::try_from(dto)
397    }
398
399    /// Serializable DTO for the root GFF file.
400    #[derive(Serialize, Deserialize)]
401    pub struct GffDto {
402        /// File type signature (e.g. "UTC ").
403        pub file_type: String,
404        /// Root struct data.
405        pub root: GffStructDto,
406    }
407
408    /// Serializable DTO for a GFF struct.
409    #[derive(Serialize, Deserialize)]
410    pub struct GffStructDto {
411        /// Struct ID (usually -1 for root, or specific ID for list items).
412        pub struct_id: i32,
413        /// Field map (sorted by label).
414        #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
415        pub fields: BTreeMap<String, GffValueDto>,
416    }
417
418    /// Serializable DTO for a GFF field value.
419    #[derive(Serialize, Deserialize)]
420    #[serde(tag = "type", content = "value")]
421    pub enum GffValueDto {
422        /// Unsigned 8-bit integer.
423        UInt8(u8),
424        /// Signed 8-bit integer.
425        Int8(i8),
426        /// Unsigned 16-bit integer.
427        UInt16(u16),
428        /// Signed 16-bit integer.
429        Int16(i16),
430        /// Unsigned 32-bit integer.
431        UInt32(u32),
432        /// Signed 32-bit integer.
433        Int32(i32),
434        /// Unsigned 64-bit integer.
435        UInt64(u64),
436        /// Signed 64-bit integer.
437        Int64(i64),
438        /// 32-bit float.
439        Single(f32),
440        /// 64-bit float.
441        Double(f64),
442        /// String value.
443        String(String),
444        /// Resource reference string.
445        ResRef(String),
446        /// Localized string object.
447        LocalizedString(GffLocalizedStringDto),
448        /// Binary blob (serialized as hex string).
449        #[serde(with = "hex_bytes")]
450        Binary(Vec<u8>),
451        /// Nested struct.
452        Struct(Box<GffStructDto>),
453        /// List of structs.
454        List(Vec<GffStructDto>),
455        /// 4-component vector.
456        Vector4([f32; 4]),
457        /// 3-component vector.
458        Vector3([f32; 3]),
459        /// String reference ID.
460        StrRef(i32),
461    }
462
463    /// Serializable DTO for a localized string.
464    #[derive(Serialize, Deserialize)]
465    pub struct GffLocalizedStringDto {
466        /// Reference into `dialog.tlk`.
467        pub str_ref: i32,
468        /// Map of language ID to localized text.
469        #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
470        pub substrings: BTreeMap<u32, String>,
471    }
472
473    impl From<&Gff> for GffDto {
474        fn from(gff: &Gff) -> Self {
475            let file_type = rakata_core::text::decode_text(
476                &gff.file_type,
477                rakata_core::text::TextEncoding::Windows1252,
478            );
479            Self {
480                file_type,
481                root: GffStructDto::from(&gff.root),
482            }
483        }
484    }
485
486    impl TryFrom<GffDto> for Gff {
487        type Error = GffBinaryError;
488
489        fn try_from(dto: GffDto) -> Result<Self, Self::Error> {
490            let mut file_type = [0u8; 4];
491            let bytes = dto.file_type.as_bytes();
492            if bytes.len() > 4 {
493                return Err(GffBinaryError::InvalidHeader("file_type too long".into()));
494            }
495            file_type[..bytes.len()].copy_from_slice(bytes);
496
497            Ok(Self {
498                file_type,
499                root: GffStruct::try_from(dto.root)?,
500            })
501        }
502    }
503
504    impl From<&GffStruct> for GffStructDto {
505        fn from(s: &GffStruct) -> Self {
506            let mut fields = BTreeMap::new();
507            for field in &s.fields {
508                fields.insert(field.label.to_string(), GffValueDto::from(&field.value));
509            }
510            Self {
511                struct_id: s.struct_id,
512                fields,
513            }
514        }
515    }
516
517    impl TryFrom<GffStructDto> for GffStruct {
518        type Error = GffBinaryError;
519
520        fn try_from(dto: GffStructDto) -> Result<Self, Self::Error> {
521            let mut fields = Vec::with_capacity(dto.fields.len());
522            for (label, value_dto) in dto.fields {
523                fields.push(GffField {
524                    label: label.try_into().map_err(|_| {
525                        GffBinaryError::InvalidData("Invalid GFF label in JSON".into())
526                    })?,
527                    value: GffValue::try_from(value_dto)?,
528                });
529            }
530            Ok(Self {
531                struct_id: dto.struct_id,
532                fields,
533            })
534        }
535    }
536
537    impl From<&GffValue> for GffValueDto {
538        fn from(v: &GffValue) -> Self {
539            match v {
540                GffValue::UInt8(x) => Self::UInt8(*x),
541                GffValue::Int8(x) => Self::Int8(*x),
542                GffValue::UInt16(x) => Self::UInt16(*x),
543                GffValue::Int16(x) => Self::Int16(*x),
544                GffValue::UInt32(x) => Self::UInt32(*x),
545                GffValue::Int32(x) => Self::Int32(*x),
546                GffValue::UInt64(x) => Self::UInt64(*x),
547                GffValue::Int64(x) => Self::Int64(*x),
548                GffValue::Single(x) => Self::Single(*x),
549                GffValue::Double(x) => Self::Double(*x),
550                GffValue::String(x) => Self::String(x.clone()),
551                GffValue::ResRef(x) => Self::ResRef(x.to_string()),
552                GffValue::LocalizedString(x) => {
553                    Self::LocalizedString(GffLocalizedStringDto::from(x))
554                }
555                GffValue::Binary(x) => Self::Binary(x.clone()),
556                GffValue::Struct(x) => Self::Struct(Box::new(GffStructDto::from(x.as_ref()))),
557                GffValue::List(x) => Self::List(x.iter().map(GffStructDto::from).collect()),
558                GffValue::Vector4(x) => Self::Vector4(*x),
559                GffValue::Vector3(x) => Self::Vector3(*x),
560                GffValue::StrRef(x) => Self::StrRef(x.raw()),
561            }
562        }
563    }
564
565    impl TryFrom<GffValueDto> for GffValue {
566        type Error = GffBinaryError;
567
568        fn try_from(dto: GffValueDto) -> Result<Self, Self::Error> {
569            Ok(match dto {
570                GffValueDto::UInt8(x) => Self::UInt8(x),
571                GffValueDto::Int8(x) => Self::Int8(x),
572                GffValueDto::UInt16(x) => Self::UInt16(x),
573                GffValueDto::Int16(x) => Self::Int16(x),
574                GffValueDto::UInt32(x) => Self::UInt32(x),
575                GffValueDto::Int32(x) => Self::Int32(x),
576                GffValueDto::UInt64(x) => Self::UInt64(x),
577                GffValueDto::Int64(x) => Self::Int64(x),
578                GffValueDto::Single(x) => Self::Single(x),
579                GffValueDto::Double(x) => Self::Double(x),
580                GffValueDto::String(x) => Self::String(x),
581                GffValueDto::ResRef(x) => Self::ResRef(
582                    ResRef::new(&x)
583                        .map_err(|e| GffBinaryError::InvalidData(format!("resref `{x}`: {e}")))?,
584                ),
585                GffValueDto::LocalizedString(x) => {
586                    Self::LocalizedString(GffLocalizedString::from(x))
587                }
588                GffValueDto::Binary(x) => Self::Binary(x),
589                GffValueDto::Struct(x) => Self::Struct(Box::new(GffStruct::try_from(*x)?)),
590                GffValueDto::List(x) => {
591                    let mut list = Vec::with_capacity(x.len());
592                    for item in x {
593                        list.push(GffStruct::try_from(item)?);
594                    }
595                    Self::List(list)
596                }
597                GffValueDto::Vector4(x) => Self::Vector4(x),
598                GffValueDto::Vector3(x) => Self::Vector3(x),
599                GffValueDto::StrRef(x) => Self::StrRef(StrRef::from_raw(x)),
600            })
601        }
602    }
603
604    impl From<&GffLocalizedString> for GffLocalizedStringDto {
605        fn from(s: &GffLocalizedString) -> Self {
606            let mut substrings = BTreeMap::new();
607            for sub in &s.substrings {
608                substrings.insert(sub.string_id, sub.text.clone());
609            }
610            Self {
611                str_ref: s.string_ref.raw(),
612                substrings,
613            }
614        }
615    }
616
617    impl From<GffLocalizedStringDto> for GffLocalizedString {
618        fn from(dto: GffLocalizedStringDto) -> Self {
619            let mut substrings = Vec::with_capacity(dto.substrings.len());
620            for (string_id, text) in dto.substrings {
621                substrings.push(GffLocalizedSubstring { string_id, text });
622            }
623            Self {
624                string_ref: StrRef::from_raw(dto.str_ref),
625                substrings,
626            }
627        }
628    }
629
630    mod hex_bytes {
631        use serde::{Deserialize, Deserializer, Serializer};
632
633        pub fn serialize<S>(bytes: &[u8], serializer: S) -> Result<S::Ok, S::Error>
634        where
635            S: Serializer,
636        {
637            let hex = hex_encode(bytes);
638            serializer.serialize_str(&hex)
639        }
640
641        pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
642        where
643            D: Deserializer<'de>,
644        {
645            let s = String::deserialize(deserializer)?;
646            hex_decode(&s).map_err(serde::de::Error::custom)
647        }
648
649        fn hex_encode(bytes: &[u8]) -> String {
650            use std::fmt::Write;
651            let mut s = String::with_capacity(bytes.len() * 2);
652            for b in bytes {
653                write!(&mut s, "{b:02X}").expect("writing to a String cannot fail");
654            }
655            s
656        }
657
658        fn hex_decode(s: &str) -> Result<Vec<u8>, String> {
659            if !s.len().is_multiple_of(2) {
660                return Err("odd length hex string".into());
661            }
662            let mut bytes = Vec::with_capacity(s.len() / 2);
663            for i in (0..s.len()).step_by(2) {
664                let byte_str = &s[i..i + 2];
665                let byte = u8::from_str_radix(byte_str, 16)
666                    .map_err(|e| format!("invalid hex byte {}: {}", byte_str, e))?;
667                bytes.push(byte);
668            }
669            Ok(bytes)
670        }
671    }
672}
673
674#[cfg(feature = "serde")]
675pub use serde_json_fmt::{
676    read_gff_from_json, read_gff_from_json_bytes, write_gff_to_json, write_gff_to_json_vec,
677};
678
679#[cfg(test)]
680mod tests {
681    use super::GffLocalizedString;
682    use rakata_core::StrRef;
683
684    #[test]
685    fn a_default_localized_string_is_empty_and_unreferenced() {
686        let value = GffLocalizedString::default();
687
688        assert_eq!(value.string_ref, StrRef::invalid());
689        assert!(value.substrings.is_empty());
690        assert_eq!(value, GffLocalizedString::new(StrRef::invalid()));
691    }
692}