1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
//! Enumerates IDA's string list through the [`StringLiteral`] view and [`Strings`].
//!
//! The natural singular name `String` collides with [`std::string::String`], so the view is
//! [`StringLiteral`] while the iterator and the [`Database::strings`] method keep the ergonomic
//! `strings` stem.
use idakit_sys as sys;
use crate::Database;
use crate::address::Address;
impl Database {
/// Iterate every string literal IDA located in the database.
///
/// This (re)builds IDA's string list first, an O(database) scan, then walks it. Collect the
/// result once if you iterate repeatedly, rather than calling this again.
#[must_use]
#[doc(alias("build_strlist"))]
pub fn strings(&self) -> Strings<'_> {
self.strlist_build();
Strings::new(self)
}
}
/// A borrowed view of one string IDA located, keyed by address.
///
/// Carries the [`address`](Self::address), octet [`len`](Self::len), and decoded
/// [`text`](Self::text). The raw type fields are read once at iteration; the text is decoded
/// on demand.
#[derive(Clone, Copy)]
#[doc(alias("string_info_t"))]
pub struct StringLiteral<'db> {
address: Address,
length: usize,
raw_type: i32,
db: &'db Database,
}
impl<'db> StringLiteral<'db> {
#[inline]
pub(crate) fn new(address: Address, length: usize, raw_type: i32, db: &'db Database) -> Self {
Self {
address,
length,
raw_type,
db,
}
}
/// The string's address.
#[inline]
#[must_use]
pub const fn address(&self) -> Address {
self.address
}
/// The string's length in octets (raw bytes), excluding any terminator. Divide by
/// [`char_width`](Self::char_width) for a character count.
#[inline]
#[must_use]
pub const fn len(&self) -> usize {
self.length
}
/// Whether the string is empty.
#[inline]
#[must_use]
pub const fn is_empty(&self) -> bool {
self.length == 0
}
/// Bytes per character: `1` (byte / UTF-8), `2` (UTF-16), or `4` (UTF-32).
#[inline]
#[must_use]
pub fn char_width(&self) -> u8 {
char_width_of(self.raw_type)
}
/// Whether the string is length-prefixed (Pascal-style) rather than terminated.
#[inline]
#[must_use]
pub fn is_pascal(&self) -> bool {
is_pascal_of(self.raw_type)
}
/// The decoded string as UTF-8, or `None` if the bytes can't be read.
///
/// This is the semantic form, for search, matching, and analysis: undecodable units become the
/// Unicode replacement character (U+FFFD) rather than failing. For the pseudocode-faithful
/// rendering with control bytes shown as C escapes, use [`escaped`](Self::escaped).
#[must_use]
#[doc(alias("get_strlit_contents"))]
pub fn text(&self) -> Option<String> {
self.db
.strlit_contents(self.address, self.length, self.raw_type)
}
/// The string in its C-escaped display form, or `None` if the bytes can't be read.
///
/// This is what the decompiler renders in pseudocode: non-printable and undecodable bytes show
/// as C escapes (`\n`, `\xNN`, `\uNNNN`). Use [`text`](Self::text) for the semantic form.
#[must_use]
#[doc(alias("get_strlit_contents"))]
pub fn escaped(&self) -> Option<String> {
self.db
.strlit_escaped(self.address, self.length, self.raw_type)
}
}
impl std::fmt::Debug for StringLiteral<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("StringLiteral")
.field("address", &self.address)
.field("len", &self.length)
.field("char_width", &self.char_width())
.field("text", &self.text())
.finish()
}
}
key_identity!(StringLiteral, address, ord);
/// Bytes per character encoded in the raw string-type code: 1, 2, or 4.
///
/// Only the low byte carries width/layout; the high byte's encoding index is irrelevant here.
fn char_width_of(raw_type: i32) -> u8 {
1u8 << ((raw_type & sys::STRWIDTH_MASK) as u8)
}
/// Whether a string's layout is Pascal (length-prefixed) rather than terminated.
fn is_pascal_of(raw_type: i32) -> bool {
let layout = (raw_type & sys::STRLYT_MASK) >> sys::STRLYT_SHIFT;
(1..=3).contains(&layout)
}
/// A lazy iterator over IDA's string list, in list order, from [`Database::strings`].
///
/// Borrows `&Database`, so it can't coexist with a write. `size_hint`'s lower bound is `0`: a
/// list entry with no readable address is skipped.
#[doc(alias("get_strlist_qty", "get_strlist_item"))]
pub struct Strings<'db> {
db: &'db Database,
next: usize,
count: usize,
}
impl<'db> Strings<'db> {
#[inline]
pub(crate) fn new(db: &'db Database) -> Self {
Self {
db,
next: 0,
count: db.strlist_qty(),
}
}
/// Read list entry `n` into a view, or `None` if it is out of range or has no valid address.
fn item(&self, n: usize) -> Option<StringLiteral<'db>> {
let item = self.db.strlist_item(n)?;
let address = Address::try_new(item.ea)?;
Some(StringLiteral::new(
address,
item.length.max(0) as usize,
item.type_,
self.db,
))
}
}
impl<'db> Iterator for Strings<'db> {
type Item = StringLiteral<'db>;
fn next(&mut self) -> Option<Self::Item> {
while self.next < self.count {
let n = self.next;
self.next += 1;
if let Some(string) = self.item(n) {
return Some(string);
}
}
None
}
#[inline]
fn size_hint(&self) -> (usize, Option<usize>) {
(0, Some(self.count - self.next))
}
}
#[cfg(test)]
mod tests {
use assert2::assert;
use rstest::rstest;
use super::*;
// Raw string-type codes: width in bits 0-1, layout in bits 2+.
const STRTYPE_C: i32 = 0x00; // 1-byte, terminated
const STRTYPE_C_16: i32 = 0x01; // 2-byte, terminated
const STRTYPE_C_32: i32 = 0x02; // 4-byte, terminated
const STRTYPE_PASCAL: i32 = 0x04; // 1-byte, 1-byte length prefix
const STRTYPE_PASCAL_16: i32 = 0x05; // 2-byte, 1-byte length prefix
const STRTYPE_LEN2: i32 = 0x08; // 1-byte, 2-byte length prefix
/// Width comes from the low bits, and a nonzero encoding index (high byte) does not
/// disturb it.
#[rstest]
#[case(STRTYPE_C, 1)]
#[case(STRTYPE_C_16, 2)]
#[case(STRTYPE_C_32, 4)]
#[case(STRTYPE_PASCAL, 1)]
#[case(STRTYPE_PASCAL_16, 2)]
#[case(STRTYPE_C | 0x5500_0000, 1)] // encoding index in the high byte is ignored
fn char_width_reads_the_strwidth_field(#[case] raw: i32, #[case] width: u8) {
assert!(char_width_of(raw) == width);
}
/// Terminated layouts are not Pascal; the three length-prefixed layouts are.
#[rstest]
#[case(STRTYPE_C, false)]
#[case(STRTYPE_C_16, false)]
#[case(STRTYPE_PASCAL, true)]
#[case(STRTYPE_PASCAL_16, true)]
#[case(STRTYPE_LEN2, true)]
fn is_pascal_reads_the_strlyt_field(#[case] raw: i32, #[case] pascal: bool) {
assert!(is_pascal_of(raw) == pascal);
}
/// `len`/`is_empty` read the stored length back verbatim, zero or not.
#[rstest]
#[case(0, true)]
#[case(1, false)]
#[case(5, false)]
fn len_and_is_empty_reflect_the_stored_length(#[case] length: usize, #[case] empty: bool) {
let db = Database::new();
let literal = StringLiteral::new(Address::new_const(0x1000), length, STRTYPE_C, &db);
assert!(literal.len() == length);
assert!(literal.is_empty() == empty);
}
/// `char_width`/`is_pascal` forward to the free functions above over the stored raw type.
#[rstest]
#[case(STRTYPE_C, 1, false)]
#[case(STRTYPE_C_16, 2, false)]
#[case(STRTYPE_PASCAL, 1, true)]
fn char_width_and_is_pascal_forward_the_raw_type(
#[case] raw: i32,
#[case] width: u8,
#[case] pascal: bool,
) {
let db = Database::new();
let literal = StringLiteral::new(Address::new_const(0x1000), 0, raw, &db);
assert!(literal.char_width() == width);
assert!(literal.is_pascal() == pascal);
}
/// Identity is the address alone: two entries at the same address are equal even with
/// different lengths/types, and a different address is never equal.
#[test]
fn string_literal_identity_compares_by_address() {
let db = Database::new();
let a = Address::new_const(0x1000);
let b = Address::new_const(0x2000);
assert!(
StringLiteral::new(a, 4, STRTYPE_C, &db)
== StringLiteral::new(a, 8, STRTYPE_PASCAL, &db)
);
assert!(
StringLiteral::new(a, 4, STRTYPE_C, &db) != StringLiteral::new(b, 4, STRTYPE_C, &db)
);
}
#[test]
fn string_literal_ord_sorts_by_address() {
let db = Database::new();
let hi = Address::new_const(0x2000);
let lo = Address::new_const(0x1000);
let mut strings = [
StringLiteral::new(hi, 1, STRTYPE_C, &db),
StringLiteral::new(lo, 1, STRTYPE_C, &db),
];
strings.sort();
assert!(strings[0].address() == lo);
assert!(strings[1].address() == hi);
}
/// `size_hint`'s upper bound is the remaining `count - next` span, with no live kernel
/// involved: it never walks the list itself.
#[test]
fn strings_size_hint_reports_the_remaining_span() {
let db = Database::new();
let strings = Strings {
db: &db,
next: 2,
count: 6,
};
assert!(strings.size_hint() == (0, Some(4)));
}
mod proptests {
use proptest::prelude::*;
use super::*;
proptest! {
// Across the full raw STRTYPE domain: width comes only from the low two bits, so it
// is always a power of two in {1, 2, 4, 8} (8 is STRWIDTH's unassigned reserved
// value; no real IDA database emits it, but the parser must not panic on it either),
// and it is never disturbed by any bit outside STRWIDTH_MASK.
#[test]
fn char_width_of_never_panics_and_is_a_power_of_two(raw in any::<i32>()) {
let width = char_width_of(raw);
prop_assert!(matches!(width, 1 | 2 | 4 | 8));
prop_assert_eq!(width, char_width_of(raw & sys::STRWIDTH_MASK));
}
// is_pascal_of never panics, and only depends on the STRLYT field (bits outside
// STRWIDTH_MASK).
#[test]
fn is_pascal_of_never_panics_and_ignores_width_bits(raw in any::<i32>(), width_bits in 0i32..4) {
prop_assert_eq!(
is_pascal_of(raw),
is_pascal_of((raw & !sys::STRWIDTH_MASK) | width_bits)
);
}
}
}
}