cdragon_rst/
lib.rs

1//! Support of Riot translation files (RST)
2//!
3//! Use [Rst] to open an RST file (`.stringtable`) and access its content.
4//!
5//! An RST file maps hashed translation keys to translation strings.
6//! When an instance is created, the file header is parsed, data is read, but strings are actually
7//! read and parsed (as UTF-8) only on access.
8//!
9//! # Example
10//! ```no_run
11//! # use cdragon_rst::Rst;
12//! # // Use explicit type annotation, required only by rustdoc
13//! # type RstHashMapper = cdragon_rst::RstHashMapper<39>;
14//!
15//! let rst = Rst::open("main_en_us.stringtable").expect("failed to open or read data");
16//! // Get an entry by its key string
17//! assert_eq!(rst.get("item_1001_name"), Some("Boots".into()));
18//! // Or by its key hash
19//! assert_eq!(rst.get(0x3376eae1da), Some("Boots".into()));
20//!
21//! // Entries can be iterated
22//! // Use a mapper to filter on (known) keys
23//! let hmapper = RstHashMapper::from_path("hashes.rst.txt").expect("failed to load hashes");
24//! for (hash, value) in rst.iter() {
25//!     if let Some(key) = hmapper.get(hash) {
26//!         println!("{key} = {value}");
27//!     }
28//! }
29//! ```
30//!
31//! # Older RST versions
32//!
33//! ## Hash bit size
34//!
35//! Hashes from RST files used more bits.
36//! Number of bits used by an RST file can be retrieved with [Rst::hash_bits()].
37//! The default [RstHashMapper] is suitable for the latest RST version.
38//!
39//! ## Encrypted entries
40//!
41//! Older RST versions could have encrypted entries whose data is not valid UTF-8.
42//! Use [Rst::get_raw()] to access both encrypted and non-encrypted entries.
43
44use std::borrow::Cow;
45use std::collections::HashMap;
46use std::io::{Read, Seek, BufReader};
47use std::path::Path;
48use nom::{
49    number::complete::{le_u8, le_u32, le_u64},
50    bytes::complete::tag,
51    sequence::tuple,
52};
53use thiserror::Error;
54use cdragon_hashes::rst::compute_rst_hash_full;
55use cdragon_utils::{
56    parsing::{ParseError, ReadArray},
57    parse_buf,
58};
59pub use cdragon_hashes::rst::RstHashMapper;
60
61
62/// Result type for RST errors
63type Result<T, E = RstError> = std::result::Result<T, E>;
64
65/// A raw RST entry value, possibly encrypted
66#[derive(Debug)]
67pub enum RstRawValue<'a> {
68    String(&'a [u8]),
69    Encrypted(&'a [u8]),
70}
71
72
73/// Riot translation file
74///
75/// String values can be accessed by hash key or string key.
76/// All getters accept non-truncated hashes and will truncate it as needed.
77pub struct Rst {
78    /// RST version
79    pub version: u8,
80    /// Optional font config (obsolete)
81    pub font_config: Option<String>,
82    /// Number of bits per hash
83    hash_bits: u8,
84    /// True if some entries are encrypted
85    has_trenc: bool,
86    /// Entry offsets, indexed by their hash
87    entry_offsets: HashMap<u64, usize>,
88    /// Buffer of entry data (unparsed)
89    entry_data: Vec<u8>,
90}
91
92impl Rst {
93    /// Open an RMAN file from path
94    pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
95        let file = std::fs::File::open(path.as_ref())?;
96        let reader = BufReader::new(file);
97        Rst::read(reader)
98    }
99
100    /// Read an RST file, check header, read entry headers
101    pub fn read<R: Read + Seek>(mut reader: R) -> Result<Self> {
102        let (version, hash_bits, font_config, entry_count) = Self::parse_header(&mut reader)?;
103
104        let entry_offsets = {
105            let mut entry_offsets = HashMap::with_capacity(entry_count as usize);
106            let mut buf = vec![0; 8 * entry_count as usize];
107            reader.read_exact(&mut buf)?;
108
109            let hash_mask = (1 << hash_bits) - 1;
110            let mut it = nom::combinator::iterator(buf.as_slice(), le_u64);
111            entry_offsets.extend(it
112                .take(entry_count as usize)
113                .map(|v: u64| (v & hash_mask, (v >> hash_bits) as usize))
114            );
115            let result: nom::IResult<_, _, ()> = it.finish();
116            let _ = result.map_err(ParseError::from)?;
117            entry_offsets
118        };
119
120        let has_trenc = version < 5 && reader.read_array::<1>()?[0] != 0;
121
122        let mut entry_data = Vec::new();
123        reader.read_to_end(&mut entry_data)?;
124
125        Ok(Self {
126            version,
127            font_config,
128            hash_bits,
129            has_trenc,
130            entry_offsets,
131            entry_data,
132        })
133    }
134
135    /// Parse header, advance to the beginning of entry directory
136    fn parse_header<R: Read + Seek>(reader: &mut R) -> Result<(u8, u8, Option<String>, u32)> {
137        let version = {
138            let buf = reader.read_array::<{3 + 1}>()?;
139            let (_, version) = parse_buf!(buf, tuple((tag("RST"), le_u8)));
140            version
141        };
142
143        let hash_bits: u8 = match version {
144            2 | 3 => 40,
145            4 | 5 => 39,
146            _ => return Err(RstError::UnsupportedVersion(version)),
147        };
148
149        let font_config = if version == 2 && reader.read_array::<1>()?[0] != 0 {
150            let buf = reader.read_array::<4>()?;
151            let n = parse_buf!(buf, le_u32);
152            let mut buf = vec![0; n as usize];
153            reader.read_exact(&mut buf)?;
154            Some(String::from_utf8(buf)?)
155        } else {
156            None
157        };
158
159        let entry_count = {
160            let buf = reader.read_array::<4>()?;
161            parse_buf!(buf, le_u32)
162        };
163
164        Ok((version, hash_bits, font_config, entry_count))
165    }
166
167    /// Get the number of bits used by hash keys
168    pub fn hash_bits(&self) -> u8 {
169        self.hash_bits
170    }
171
172    /// Truncate a hash key to the number of bits used by the file
173    pub fn truncate_hash_key(&self, key: u64) -> u64 {
174        key & ((1 << self.hash_bits) - 1)
175    }
176
177    /// Get a string from its key
178    ///
179    /// `key` is truncated has needed.
180    /// If the entry is encrypted, return `None`.
181    pub fn get<K: IntoRstKey>(&self, key: K) -> Option<Cow<'_, str>> {
182        match self.get_raw_by_hash(key.into_rst_key())? {
183            RstRawValue::String(s) => Some(String::from_utf8_lossy(s)),
184            _ => None
185        }
186    }
187
188    /// Get a raw value from its key
189    pub fn get_raw<K: IntoRstKey>(&self, key: K) -> Option<RstRawValue> {
190        self.get_raw_by_hash(key.into_rst_key())
191    }
192
193    /// Get a raw value from its hash key
194    fn get_raw_by_hash(&self, key: u64) -> Option<RstRawValue> {
195        let key = self.truncate_hash_key(key);
196        let offset = *self.entry_offsets.get(&key)?;
197        self.get_raw_by_offset(offset)
198    }
199
200    /// Get a raw value from its offset
201    fn get_raw_by_offset(&self, offset: usize) -> Option<RstRawValue> {
202        let data = &self.entry_data[offset..];
203        if data[0] == 0xff && self.has_trenc {
204            let size = u16::from_le_bytes(data[1..3].try_into().unwrap());
205            Some(RstRawValue::Encrypted(&data[3..3+size as usize]))
206        } else {
207            let pos = data.iter().position(|&b| b == 0)?;
208            Some(RstRawValue::String(&data[..pos]))
209        }
210    }
211
212    /// Iterate on string entries
213    pub fn iter(&self) -> impl Iterator<Item=(u64, Cow<'_, str>)> {
214        self.entry_offsets.iter().filter_map(|(key, offset)| {
215            match self.get_raw_by_offset(*offset)? {
216                RstRawValue::String(s) => Some(String::from_utf8_lossy(s)),
217                _ => None
218            }.map(|value| (*key, value))
219        })
220    }
221}
222
223impl std::fmt::Debug for Rst {
224    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
225        f.debug_struct("Rst")
226            .field("version", &self.version)
227            .field("font_config", &self.font_config)
228            .field("hash_bits", &self.hash_bits)
229            .field("has_trenc", &self.has_trenc)
230            .field("len", &self.entry_offsets.len())
231            .finish()
232    }
233}
234
235
236pub trait IntoRstKey {
237    fn into_rst_key(self) -> u64;
238}
239
240impl IntoRstKey for u64 {
241    fn into_rst_key(self) -> u64 {
242        self
243    }
244}
245
246impl IntoRstKey for &str {
247    fn into_rst_key(self) -> u64 {
248        compute_rst_hash_full(self)
249    }
250}
251
252
253
254/// Error in an RST file
255#[allow(missing_docs)]
256#[derive(Error, Debug)]
257pub enum RstError {
258    #[error(transparent)]
259    Io(#[from] std::io::Error),
260    #[error(transparent)]
261    Utf8(#[from] std::string::FromUtf8Error),
262    #[error("parsing error")]
263    Parsing(#[from] ParseError),
264    #[error("version not supported: {0}")]
265    UnsupportedVersion(u8),
266}
267