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//! ## Shape of the container
7//!
8//! A 56-byte header carrying an offset-and-count pair for each of six sections:
9//! structs, fields, labels, the field data blob, and two index arrays. Every
10//! section is located by its own header entry.
11//!
12//! One trap worth knowing before reading the header: the last three counts are
13//! byte lengths while the first three are element counts.
14//!
15//! ## The overloaded word
16//!
17//! Both struct and field records carry a `data_or_offset` that means different
18//! things by context, with no discriminator stored beside it. On a field it is
19//! the value itself when the type fits in four bytes and an offset into the data
20//! blob or an index array otherwise. On a struct it is a field index directly
21//! when `field_count` is exactly one, and a byte offset into the field indices
22//! array for any other count.
23//!
24//! Getting that wrong does not fail loudly, it reads a plausible number out of
25//! the wrong region, which is why this module validates every range before
26//! decoding rather than trusting the arithmetic.
27//!
28//! ## Logical Data Model
29//! ```text
30//! Gff
31//!  `-- root GffStruct
32//!       `-- [GffField(label, GffValue)]
33//!            `-- nested structs/lists recursively reference tables
34//! ```
35//!
36//! Offsets in struct and field records reference table locations or blob
37//! offsets depending on field type. This module keeps those rules explicit and
38//! validates all ranges before decoding.
39
40mod coerce;
41mod document;
42mod label;
43mod path;
44mod reader;
45mod walk;
46mod writer;
47
48pub use coerce::*;
49pub use document::{Change, GffDocument, GffDocumentError};
50pub use label::*;
51pub use path::{GffPath, GffPathError, GffPathParseError, GffPathSegment};
52pub use reader::{read_gff, read_gff_from_bytes};
53pub use walk::GffWalk;
54pub use writer::{write_gff, write_gff_to_vec};
55
56use num_enum::{IntoPrimitive, TryFromPrimitive};
57use thiserror::Error;
58
59use rakata_core::{DecodeTextError, EncodeTextError, ResRef, StrRef, TextEncoding};
60
61use crate::binary::{self, DecodeBinary, EncodeBinary};
62
63/// GFF V3.2 header size.
64const GFF_HEADER_SIZE: usize = 56;
65/// Size of one struct-table entry.
66const STRUCT_ENTRY_SIZE: usize = 12;
67/// Size of one field-table entry.
68const FIELD_ENTRY_SIZE: usize = 12;
69/// Fixed width for field labels in the label table.
70const LABEL_SIZE: usize = 16;
71/// Binary GFF version supported by KotOR.
72const GFF_VERSION_V32: [u8; 4] = *b"V3.2";
73/// Default encoding used for non-localized GFF text values.
74const DEFAULT_TEXT_ENCODING: TextEncoding = TextEncoding::Windows1252;
75
76/// The magic a GFF carries when it claims no format of its own.
77///
78/// Every typed reader in the workspace accepts this alongside the magic it is
79/// looking for, which makes it the one four-byte value that appears in all of
80/// them and means the same thing each time. Named so those guards cite it
81/// rather than spelling it out, and so the meaning sits somewhere a reader can
82/// find: a container carrying this says only that it is a GFF, and which view
83/// it should be read as has to come from somewhere other than the bytes.
84///
85/// A `.res` save sidecar and a `.bic` both present as this, which is the same
86/// point [`ResourceType::Gff`](rakata_core::ResourceType::Gff) makes about
87/// resource types.
88pub const GENERIC_FILE_TYPE: [u8; 4] = *b"GFF ";
89
90/// In-memory representation of a binary GFF file.
91#[derive(Debug, Clone, PartialEq)]
92pub struct Gff {
93    /// Four-byte file type (`GFF `, `UTC `, `UTI `, ...).
94    pub file_type: [u8; 4],
95    /// Root structure.
96    pub root: GffStruct,
97}
98
99impl Gff {
100    /// Creates a GFF value with the provided file type and root structure.
101    pub fn new(file_type: [u8; 4], root: GffStruct) -> Self {
102        Self { file_type, root }
103    }
104
105    /// Creates a generic `GFF ` container.
106    pub fn generic(root: GffStruct) -> Self {
107        Self {
108            file_type: GENERIC_FILE_TYPE,
109            root,
110        }
111    }
112}
113
114impl DecodeBinary for Gff {
115    type Error = GffBinaryError;
116
117    fn decode_binary(bytes: &[u8]) -> Result<Self, Self::Error> {
118        read_gff_from_bytes(bytes)
119    }
120}
121
122impl EncodeBinary for Gff {
123    type Error = GffBinaryError;
124
125    fn encode_binary(&self) -> Result<Vec<u8>, Self::Error> {
126        write_gff_to_vec(self)
127    }
128}
129
130/// One GFF struct node.
131#[derive(Debug, Clone, PartialEq)]
132pub struct GffStruct {
133    /// Struct ID from the binary table.
134    pub struct_id: i32,
135    /// Ordered fields for this struct.
136    pub fields: Vec<GffField>,
137}
138
139impl GffStruct {
140    /// Creates an empty struct with the given `struct_id`.
141    pub fn new(struct_id: i32) -> Self {
142        Self {
143            struct_id,
144            fields: Vec::new(),
145        }
146    }
147
148    /// Creates a struct with pre-populated fields.
149    pub fn with_fields(struct_id: i32, fields: Vec<GffField>) -> Self {
150        Self { struct_id, fields }
151    }
152
153    /// Appends a new field.
154    ///
155    /// Takes a built [`GffLabel`] rather than converting one, so the decision
156    /// about an unrepresentable label belongs to whoever supplied it. A source
157    /// literal goes through [`gff_label!`](crate::gff_label) and fails the
158    /// build; a label read out of a file or a user's input goes through
159    /// [`GffLabel::new`] and carries a `Result` the caller has to answer.
160    pub fn push_field(&mut self, label: GffLabel, value: GffValue) {
161        self.fields.push(GffField { label, value });
162    }
163
164    /// Returns the first field value that matches `label`, or `None` when the
165    /// struct carries no field under that label.
166    ///
167    /// A struct can hold one label more than once, and vanilla content does:
168    /// every `EntryList` and `ReplyList` node in a `.dlg` carries `SoundExists`
169    /// six times over. Across every GFF file in the install the copies agree in
170    /// value, so taking the first is not a choice between different answers.
171    /// It is still a choice, and a caller that wants to see the rest has to
172    /// walk [`Self::fields`] itself.
173    pub fn field(&self, label: &str) -> Option<&GffValue> {
174        self.fields
175            .iter()
176            .find(|field| field.label == label)
177            .map(|field| &field.value)
178    }
179}
180
181/// One labeled GFF field.
182#[derive(Debug, Clone, PartialEq)]
183pub struct GffField {
184    /// Field label.
185    pub label: GffLabel,
186    /// Field payload.
187    pub value: GffValue,
188}
189
190/// The GFF V3.2 field type set, declared once for every enum that mirrors it.
191///
192/// Four types describe this one closed set: the wire codes the reader decodes,
193/// the runtime value, the serde DTO, and the schema's expected type. They were
194/// maintained by hand and had already drifted, and the drift was silent because
195/// nothing matches a schema type against a wire code. Passing the set through
196/// this macro is what makes a variant present in one and absent from another
197/// unrepresentable.
198///
199/// Each row is a variant name, its wire code, the name the format gives it, a
200/// description, the runtime payload, and the DTO payload. The set is fixed by
201/// the format, so this list does not grow.
202///
203/// Code `18` is deliberately absent. `docs/src/formats/gff/index.md` records it
204/// as defined by the format with no established storage rule, and a reader that
205/// meets one is supposed to surface it rather than guess which branch to take.
206/// Leaving it out of the wire enum is what makes the reader do that: it reports
207/// the code it could not decode instead of reading four bytes out of a region
208/// nothing attests.
209macro_rules! gff_field_types {
210    ($emit:ident) => {
211        $emit! {
212            UInt8 = 0, "BYTE", "unsigned 8-bit integer", u8, u8;
213            Int8 = 1, "CHAR", "signed 8-bit integer", i8, i8;
214            UInt16 = 2, "WORD", "unsigned 16-bit integer", u16, u16;
215            Int16 = 3, "SHORT", "signed 16-bit integer", i16, i16;
216            UInt32 = 4, "DWORD", "unsigned 32-bit integer", u32, u32;
217            Int32 = 5, "INT", "signed 32-bit integer", i32, i32;
218            UInt64 = 6, "DWORD64", "unsigned 64-bit integer", u64, u64;
219            Int64 = 7, "INT64", "signed 64-bit integer", i64, i64;
220            Single = 8, "FLOAT", "32-bit floating point", f32, f32;
221            Double = 9, "DOUBLE", "64-bit floating point", f64, f64;
222            String = 10, "CExoString", "variable-length string", String, String;
223            ResRef = 11, "CResRef", "resource reference, at most 16 bytes", ResRef, String;
224            LocalizedString = 12, "CExoLocString",
225                "localized string with an optional TLK reference",
226                GffLocalizedString, GffLocalizedStringDto;
227            Binary = 13, "VOID", "raw binary blob", Vec<u8>,
228                #[serde(with = "hex_bytes")] Vec<u8>;
229            Struct = 14, "Struct", "nested struct", Box<GffStruct>, Box<GffStructDto>;
230            List = 15, "List", "struct list", Vec<GffStruct>, Vec<GffStructDto>;
231            Vector4 = 16, "Quaternion", "four packed `f32`, scalar first", [f32; 4], [f32; 4];
232            Vector3 = 17, "Vector", "three packed `f32`", [f32; 3], [f32; 3];
233        }
234    };
235}
236
237pub(crate) use gff_field_types;
238
239macro_rules! emit_gff_value {
240    ($($variant:ident = $id:literal, $wire:literal, $description:literal, $value:ty,
241        $(#[$dto_attr:meta])* $dto:ty;)*) => {
242        /// Runtime value for one GFF field.
243        #[derive(Debug, Clone, PartialEq)]
244        pub enum GffValue {
245            $(
246                #[doc = concat!("`", $wire, "`: ", $description, ".")]
247                $variant($value),
248            )*
249        }
250    };
251}
252
253gff_field_types!(emit_gff_value);
254
255impl GffValue {
256    /// Test/fixture helper: constructs a [`GffValue::ResRef`] from a string
257    /// literal, panicking on invalid input.
258    ///
259    /// Production code should construct `GffValue::ResRef(resref)` directly
260    /// with a validated [`ResRef`]. This helper exists so fixture generators
261    /// and unit tests can avoid boilerplate when the input is a known-valid
262    /// literal.
263    #[doc(hidden)]
264    pub fn resref_lit(value: &str) -> Self {
265        GffValue::ResRef(ResRef::new(value).expect("valid resref literal"))
266    }
267}
268
269/// CExoLocString payload.
270#[derive(Debug, Clone, PartialEq, Eq)]
271pub struct GffLocalizedString {
272    /// TLK string reference (`StrRef::invalid()` means use substrings).
273    pub string_ref: StrRef,
274    /// Embedded localized substrings.
275    pub substrings: Vec<GffLocalizedSubstring>,
276}
277
278impl Default for GffLocalizedString {
279    /// An empty localized string: no TLK reference and no substrings.
280    ///
281    /// `StrRef::invalid()` is the engine's own "no TLK entry" marker rather
282    /// than a stand-in, so this is the genuine empty value for the type.
283    fn default() -> Self {
284        Self::new(StrRef::invalid())
285    }
286}
287
288impl GffLocalizedString {
289    /// Creates an empty localized string.
290    pub fn new(string_ref: impl Into<StrRef>) -> Self {
291        Self {
292            string_ref: string_ref.into(),
293            substrings: Vec::new(),
294        }
295    }
296}
297
298/// One localized substring entry.
299#[derive(Debug, Clone, PartialEq, Eq)]
300pub struct GffLocalizedSubstring {
301    /// Packed string ID (`language_id * 2 + gender`).
302    pub string_id: u32,
303    /// Decoded text payload.
304    pub text: String,
305}
306
307impl GffLocalizedSubstring {
308    /// Returns the language ID portion (`string_id / 2`).
309    pub fn language_id(&self) -> u32 {
310        self.string_id / 2
311    }
312
313    /// Returns `true` for feminine entries (`string_id % 2 == 1`).
314    pub fn is_feminine(&self) -> bool {
315        self.string_id % 2 == 1
316    }
317}
318
319/// Errors produced while parsing or writing binary GFF data.
320#[derive(Debug, Error)]
321pub enum GffBinaryError {
322    /// I/O read/write failure.
323    #[error(transparent)]
324    Io(#[from] std::io::Error),
325    /// Header/body layout is invalid or truncated.
326    #[error("invalid GFF header: {0}")]
327    InvalidHeader(String),
328    /// GFF version is unsupported.
329    #[error("invalid GFF version: {0:?}")]
330    InvalidVersion([u8; 4]),
331    /// Encountered an unknown field type ID.
332    #[error("invalid GFF field type id: {0}")]
333    InvalidFieldType(u32),
334    /// In-memory data is not valid for binary serialization.
335    #[error("invalid GFF data: {0}")]
336    InvalidData(String),
337    /// Value cannot fit the target on-disk width.
338    #[error("value overflow while writing `{0}`")]
339    ValueOverflow(&'static str),
340    /// Label exceeds 16 bytes after encoding.
341    #[error("label `{label}` encoded length {len} exceeds maximum {max}")]
342    LabelTooLong {
343        /// Label text.
344        label: String,
345        /// Encoded byte length.
346        len: usize,
347        /// Maximum allowed byte length.
348        max: usize,
349    },
350    /// Text cannot be represented in the target encoding.
351    #[error("GFF text encoding failed for {context}: {source}")]
352    TextEncoding {
353        /// Context path for error reporting.
354        context: String,
355        /// Source encoding error.
356        #[source]
357        source: EncodeTextError,
358    },
359    /// Text bytes cannot be decoded losslessly.
360    #[error("GFF text decoding failed for {context}: {source}")]
361    TextDecoding {
362        /// Context path for error reporting.
363        context: String,
364        /// Source decoding error.
365        #[source]
366        source: DecodeTextError,
367    },
368    /// Language ID maps to an unsupported encoding.
369    #[error("unsupported language id {0} for localized string encoding")]
370    UnsupportedLanguageEncoding(u32),
371}
372
373impl From<binary::BinaryLayoutError> for GffBinaryError {
374    fn from(error: binary::BinaryLayoutError) -> Self {
375        Self::InvalidHeader(error.to_string())
376    }
377}
378
379macro_rules! emit_field_type {
380    ($($variant:ident = $id:literal, $wire:literal, $description:literal, $value:ty,
381        $(#[$dto_attr:meta])* $dto:ty;)*) => {
382        #[derive(Debug, Clone, Copy, PartialEq, Eq, IntoPrimitive, TryFromPrimitive)]
383        #[repr(u32)]
384        pub(super) enum FieldType {
385            $($variant = $id,)*
386        }
387    };
388}
389
390gff_field_types!(emit_field_type);
391
392pub(super) fn to_u32(value: usize, name: &'static str) -> Result<u32, GffBinaryError> {
393    u32::try_from(value).map_err(|_| GffBinaryError::ValueOverflow(name))
394}
395
396pub(super) fn to_usize(value: u32, name: &'static str) -> Result<usize, GffBinaryError> {
397    binary::checked_to_usize(value, name).map_err(|_| {
398        GffBinaryError::InvalidData(format!("{name} does not fit target platform usize"))
399    })
400}
401
402//
403// Serde Support (JSON)
404//
405
406#[cfg(feature = "serde")]
407/// JSON serialization support for GFF.
408pub mod serde_json_fmt {
409    use super::*;
410    use serde::{Deserialize, Serialize};
411    use serde_json::{from_slice, from_str, to_string_pretty, to_vec};
412
413    /// Serializes a GFF to JSON.
414    ///
415    /// # Errors
416    ///
417    /// [`GffBinaryError::InvalidData`] carrying the `serde_json` message. The
418    /// DTO is built from the tree first and holds nothing that resists
419    /// serialization, so this arm is not reachable in practice.
420    pub fn write_gff_to_json(gff: &Gff) -> Result<String, GffBinaryError> {
421        let dto = GffDto::from(gff);
422        to_string_pretty(&dto).map_err(|e| GffBinaryError::InvalidData(e.to_string()))
423    }
424
425    /// Serializes a GFF to JSON bytes.
426    ///
427    /// # Errors
428    ///
429    /// The same as [`write_gff_to_json`].
430    pub fn write_gff_to_json_vec(gff: &Gff) -> Result<Vec<u8>, GffBinaryError> {
431        let dto = GffDto::from(gff);
432        to_vec(&dto).map_err(|e| GffBinaryError::InvalidData(e.to_string()))
433    }
434
435    /// Deserializes a GFF from JSON.
436    ///
437    /// # Errors
438    ///
439    /// [`GffBinaryError::InvalidData`] carrying the `serde_json` message, for
440    /// JSON that is malformed or does not have the DTO's shape, and
441    /// [`GffBinaryError::InvalidHeader`] when the DTO reads and does not
442    /// convert to a tree.
443    ///
444    /// The JSON form keys fields by label, so a struct carrying one label
445    /// several times does not survive the round trip. A vanilla `.dlg` node
446    /// carries `SoundExists` six times over.
447    pub fn read_gff_from_json(json: &str) -> Result<Gff, GffBinaryError> {
448        let dto: GffDto = from_str(json).map_err(|e| GffBinaryError::InvalidData(e.to_string()))?;
449        Gff::try_from(dto)
450    }
451
452    /// Deserializes a GFF from JSON bytes.
453    ///
454    /// # Errors
455    ///
456    /// The same as [`read_gff_from_json`].
457    pub fn read_gff_from_json_bytes(bytes: &[u8]) -> Result<Gff, GffBinaryError> {
458        let dto: GffDto =
459            from_slice(bytes).map_err(|e| GffBinaryError::InvalidData(e.to_string()))?;
460        Gff::try_from(dto)
461    }
462
463    /// Serializable DTO for the root GFF file.
464    #[derive(Serialize, Deserialize)]
465    pub struct GffDto {
466        /// File type signature (e.g. "UTC ").
467        pub file_type: String,
468        /// Root struct data.
469        pub root: GffStructDto,
470    }
471
472    /// Serializable DTO for a GFF struct.
473    #[derive(Serialize, Deserialize)]
474    pub struct GffStructDto {
475        /// Struct ID (usually -1 for root, or specific ID for list items).
476        pub struct_id: i32,
477        /// Fields in the order the file carries them.
478        ///
479        /// A sequence rather than a map keyed by label, because a struct can
480        /// carry one label more than once. Keying by label dropped five of
481        /// every `.dlg` node's six `SoundExists` copies, which is 565 of one
482        /// vanilla file's 4,473 fields, and the JSON looked complete.
483        #[serde(default, skip_serializing_if = "Vec::is_empty")]
484        pub fields: Vec<GffFieldDto>,
485    }
486
487    /// Serializable DTO for one labelled field.
488    #[derive(Serialize, Deserialize)]
489    pub struct GffFieldDto {
490        /// Field label.
491        pub label: String,
492        /// Field payload, inlined beside the label rather than nested under
493        /// a second `value` key.
494        #[serde(flatten)]
495        pub value: GffValueDto,
496    }
497
498    macro_rules! emit_value_dto {
499        ($($variant:ident = $id:literal, $wire:literal, $description:literal, $value:ty,
500            $(#[$dto_attr:meta])* $dto:ty;)*) => {
501            /// Serializable DTO for a GFF field value.
502            ///
503            /// A `Binary` payload is a hex string in JSON rather than an array
504            /// of numbers, which is the one place the DTO does not mirror the
505            /// runtime value's shape.
506            #[derive(Serialize, Deserialize)]
507            #[serde(tag = "type", content = "value")]
508            pub enum GffValueDto {
509                $(
510                    #[doc = concat!("`", $wire, "`: ", $description, ".")]
511                    $(#[$dto_attr])*
512                    $variant($dto),
513                )*
514            }
515        };
516    }
517
518    gff_field_types!(emit_value_dto);
519
520    /// Serializable DTO for a localized string.
521    #[derive(Serialize, Deserialize)]
522    pub struct GffLocalizedStringDto {
523        /// Reference into `dialog.tlk`.
524        pub str_ref: i32,
525        /// Substrings in the order the file carries them.
526        #[serde(default, skip_serializing_if = "Vec::is_empty")]
527        pub substrings: Vec<GffSubstringDto>,
528    }
529
530    /// Serializable DTO for one localized substring.
531    ///
532    /// A sequence entry rather than a map keyed by the id. No install file was
533    /// seen carrying an id twice, so unlike [`GffStructDto::fields`] this is
534    /// not fixing observed loss; the map had to go because a numeric key does
535    /// not survive `serde`'s flattening, which left the field DTO readable in
536    /// one direction only.
537    #[derive(Serialize, Deserialize)]
538    pub struct GffSubstringDto {
539        /// Packed language-and-gender id.
540        pub string_id: u32,
541        /// Decoded text payload.
542        pub text: String,
543    }
544
545    impl From<&Gff> for GffDto {
546        fn from(gff: &Gff) -> Self {
547            let file_type = rakata_core::text::decode_text(
548                &gff.file_type,
549                rakata_core::text::TextEncoding::Windows1252,
550            );
551            Self {
552                file_type,
553                root: GffStructDto::from(&gff.root),
554            }
555        }
556    }
557
558    impl TryFrom<GffDto> for Gff {
559        type Error = GffBinaryError;
560
561        fn try_from(dto: GffDto) -> Result<Self, Self::Error> {
562            let mut file_type = [0u8; 4];
563            let bytes = dto.file_type.as_bytes();
564            if bytes.len() > 4 {
565                return Err(GffBinaryError::InvalidHeader("file_type too long".into()));
566            }
567            file_type[..bytes.len()].copy_from_slice(bytes);
568
569            Ok(Self {
570                file_type,
571                root: GffStruct::try_from(dto.root)?,
572            })
573        }
574    }
575
576    impl From<&GffStruct> for GffStructDto {
577        fn from(s: &GffStruct) -> Self {
578            Self {
579                struct_id: s.struct_id,
580                fields: s
581                    .fields
582                    .iter()
583                    .map(|field| GffFieldDto {
584                        label: field.label.to_string(),
585                        value: GffValueDto::from(&field.value),
586                    })
587                    .collect(),
588            }
589        }
590    }
591
592    impl TryFrom<GffStructDto> for GffStruct {
593        type Error = GffBinaryError;
594
595        fn try_from(dto: GffStructDto) -> Result<Self, Self::Error> {
596            let mut fields = Vec::with_capacity(dto.fields.len());
597            for field in dto.fields {
598                fields.push(GffField {
599                    label: field.label.try_into().map_err(|_| {
600                        GffBinaryError::InvalidData("Invalid GFF label in JSON".into())
601                    })?,
602                    value: GffValue::try_from(field.value)?,
603                });
604            }
605            Ok(Self {
606                struct_id: dto.struct_id,
607                fields,
608            })
609        }
610    }
611
612    impl From<&GffValue> for GffValueDto {
613        fn from(v: &GffValue) -> Self {
614            match v {
615                GffValue::UInt8(x) => Self::UInt8(*x),
616                GffValue::Int8(x) => Self::Int8(*x),
617                GffValue::UInt16(x) => Self::UInt16(*x),
618                GffValue::Int16(x) => Self::Int16(*x),
619                GffValue::UInt32(x) => Self::UInt32(*x),
620                GffValue::Int32(x) => Self::Int32(*x),
621                GffValue::UInt64(x) => Self::UInt64(*x),
622                GffValue::Int64(x) => Self::Int64(*x),
623                GffValue::Single(x) => Self::Single(*x),
624                GffValue::Double(x) => Self::Double(*x),
625                GffValue::String(x) => Self::String(x.clone()),
626                GffValue::ResRef(x) => Self::ResRef(x.to_string()),
627                GffValue::LocalizedString(x) => {
628                    Self::LocalizedString(GffLocalizedStringDto::from(x))
629                }
630                GffValue::Binary(x) => Self::Binary(x.clone()),
631                GffValue::Struct(x) => Self::Struct(Box::new(GffStructDto::from(x.as_ref()))),
632                GffValue::List(x) => Self::List(x.iter().map(GffStructDto::from).collect()),
633                GffValue::Vector4(x) => Self::Vector4(*x),
634                GffValue::Vector3(x) => Self::Vector3(*x),
635            }
636        }
637    }
638
639    impl TryFrom<GffValueDto> for GffValue {
640        type Error = GffBinaryError;
641
642        fn try_from(dto: GffValueDto) -> Result<Self, Self::Error> {
643            Ok(match dto {
644                GffValueDto::UInt8(x) => Self::UInt8(x),
645                GffValueDto::Int8(x) => Self::Int8(x),
646                GffValueDto::UInt16(x) => Self::UInt16(x),
647                GffValueDto::Int16(x) => Self::Int16(x),
648                GffValueDto::UInt32(x) => Self::UInt32(x),
649                GffValueDto::Int32(x) => Self::Int32(x),
650                GffValueDto::UInt64(x) => Self::UInt64(x),
651                GffValueDto::Int64(x) => Self::Int64(x),
652                GffValueDto::Single(x) => Self::Single(x),
653                GffValueDto::Double(x) => Self::Double(x),
654                GffValueDto::String(x) => Self::String(x),
655                GffValueDto::ResRef(x) => Self::ResRef(
656                    ResRef::new(&x)
657                        .map_err(|e| GffBinaryError::InvalidData(format!("resref `{x}`: {e}")))?,
658                ),
659                GffValueDto::LocalizedString(x) => {
660                    Self::LocalizedString(GffLocalizedString::from(x))
661                }
662                GffValueDto::Binary(x) => Self::Binary(x),
663                GffValueDto::Struct(x) => Self::Struct(Box::new(GffStruct::try_from(*x)?)),
664                GffValueDto::List(x) => {
665                    let mut list = Vec::with_capacity(x.len());
666                    for item in x {
667                        list.push(GffStruct::try_from(item)?);
668                    }
669                    Self::List(list)
670                }
671                GffValueDto::Vector4(x) => Self::Vector4(x),
672                GffValueDto::Vector3(x) => Self::Vector3(x),
673            })
674        }
675    }
676
677    impl From<&GffLocalizedString> for GffLocalizedStringDto {
678        fn from(s: &GffLocalizedString) -> Self {
679            Self {
680                str_ref: s.string_ref.raw(),
681                substrings: s
682                    .substrings
683                    .iter()
684                    .map(|sub| GffSubstringDto {
685                        string_id: sub.string_id,
686                        text: sub.text.clone(),
687                    })
688                    .collect(),
689            }
690        }
691    }
692
693    impl From<GffLocalizedStringDto> for GffLocalizedString {
694        fn from(dto: GffLocalizedStringDto) -> Self {
695            let substrings = dto
696                .substrings
697                .into_iter()
698                .map(|sub| GffLocalizedSubstring {
699                    string_id: sub.string_id,
700                    text: sub.text,
701                })
702                .collect();
703            Self {
704                string_ref: StrRef::from_raw(dto.str_ref),
705                substrings,
706            }
707        }
708    }
709
710    mod hex_bytes {
711        use serde::{Deserialize, Deserializer, Serializer};
712
713        pub fn serialize<S>(bytes: &[u8], serializer: S) -> Result<S::Ok, S::Error>
714        where
715            S: Serializer,
716        {
717            let hex = hex_encode(bytes);
718            serializer.serialize_str(&hex)
719        }
720
721        pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
722        where
723            D: Deserializer<'de>,
724        {
725            let s = String::deserialize(deserializer)?;
726            hex_decode(&s).map_err(serde::de::Error::custom)
727        }
728
729        fn hex_encode(bytes: &[u8]) -> String {
730            use std::fmt::Write;
731            let mut s = String::with_capacity(bytes.len() * 2);
732            for b in bytes {
733                write!(&mut s, "{b:02X}").expect("writing to a String cannot fail");
734            }
735            s
736        }
737
738        fn hex_decode(s: &str) -> Result<Vec<u8>, String> {
739            if !s.len().is_multiple_of(2) {
740                return Err("odd length hex string".into());
741            }
742            let mut bytes = Vec::with_capacity(s.len() / 2);
743            for i in (0..s.len()).step_by(2) {
744                let byte_str = &s[i..i + 2];
745                let byte = u8::from_str_radix(byte_str, 16)
746                    .map_err(|e| format!("invalid hex byte {}: {}", byte_str, e))?;
747                bytes.push(byte);
748            }
749            Ok(bytes)
750        }
751    }
752}
753
754#[cfg(feature = "serde")]
755pub use serde_json_fmt::{
756    read_gff_from_json, read_gff_from_json_bytes, write_gff_to_json, write_gff_to_json_vec,
757};
758
759#[cfg(test)]
760mod tests {
761    use super::GffLocalizedString;
762    use rakata_core::StrRef;
763
764    #[test]
765    fn a_default_localized_string_is_empty_and_unreferenced() {
766        let value = GffLocalizedString::default();
767
768        assert_eq!(value.string_ref, StrRef::invalid());
769        assert!(value.substrings.is_empty());
770        assert_eq!(value, GffLocalizedString::new(StrRef::invalid()));
771    }
772}