Skip to main content

rakata_core/
strref.rs

1use std::fmt::{Display, Formatter};
2use thiserror::Error;
3
4/// Sentinel value used by KotOR formats to indicate an unset string reference.
5pub const INVALID_STRREF_RAW: i32 = -1;
6
7/// Error returned when converting a value into [`StrRef`] fails.
8#[derive(Debug, Clone, PartialEq, Eq, Error)]
9pub enum StrRefError {
10    /// Unsigned index cannot fit the signed on-disk width.
11    #[error("string reference index {0} exceeds i32 range")]
12    IndexOverflow(u32),
13}
14
15/// TLK string-reference wrapper shared across KotOR formats.
16///
17/// Most formats store string references as signed 32-bit integers where `-1`
18/// means "unset/no string".
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
20#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
21pub struct StrRef(i32);
22
23impl StrRef {
24    /// Creates a string reference from a raw on-disk value.
25    pub const fn from_raw(raw: i32) -> Self {
26        Self(raw)
27    }
28
29    /// Returns the canonical "unset" string-reference value (`-1`).
30    pub const fn invalid() -> Self {
31        Self(INVALID_STRREF_RAW)
32    }
33
34    /// Creates a string reference from a non-negative TLK index.
35    ///
36    /// # Errors
37    ///
38    /// [`StrRefError::IndexOverflow`] above [`i32::MAX`]. The on-disk value is
39    /// signed because `-1` is the unset sentinel, so the top half of the `u32`
40    /// range has nowhere to go.
41    pub fn from_index(index: u32) -> Result<Self, StrRefError> {
42        let raw = i32::try_from(index).map_err(|_| StrRefError::IndexOverflow(index))?;
43        Ok(Self(raw))
44    }
45
46    /// Returns the raw on-disk value.
47    pub const fn raw(self) -> i32 {
48        self.0
49    }
50
51    /// Returns `true` if this value is the unset sentinel (`-1`).
52    pub const fn is_invalid(self) -> bool {
53        self.0 == INVALID_STRREF_RAW
54    }
55
56    /// Returns `true` for non-negative TLK indexes.
57    pub const fn is_valid(self) -> bool {
58        self.0 >= 0
59    }
60
61    /// Returns this value as a TLK index when it is non-negative.
62    pub fn index(self) -> Option<u32> {
63        if self.0 < 0 {
64            None
65        } else {
66            u32::try_from(self.0).ok()
67        }
68    }
69}
70
71impl From<i32> for StrRef {
72    fn from(value: i32) -> Self {
73        Self::from_raw(value)
74    }
75}
76
77impl From<StrRef> for i32 {
78    fn from(value: StrRef) -> Self {
79        value.raw()
80    }
81}
82
83impl TryFrom<u32> for StrRef {
84    type Error = StrRefError;
85
86    fn try_from(value: u32) -> Result<Self, Self::Error> {
87        Self::from_index(value)
88    }
89}
90
91impl Display for StrRef {
92    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
93        write!(f, "{}", self.0)
94    }
95}
96
97impl Default for StrRef {
98    fn default() -> Self {
99        Self::invalid()
100    }
101}
102
103#[cfg(test)]
104mod tests {
105    use super::*;
106
107    #[test]
108    fn invalid_default_is_minus_one() {
109        let strref = StrRef::default();
110        assert_eq!(strref.raw(), -1);
111        assert!(strref.is_invalid());
112        assert!(!strref.is_valid());
113        assert_eq!(strref.index(), None);
114    }
115
116    #[test]
117    fn non_negative_values_are_valid_indexes() {
118        let strref = StrRef::from_raw(123_456);
119        assert!(strref.is_valid());
120        assert!(!strref.is_invalid());
121        assert_eq!(strref.index(), Some(123_456));
122    }
123
124    #[test]
125    fn from_index_rejects_overflow() {
126        let err = StrRef::from_index(u32::MAX).expect_err("must fail");
127        assert_eq!(err, StrRefError::IndexOverflow(u32::MAX));
128    }
129}