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
// Copyright (c) 2025-2026 the libmagic-rs contributors
// SPDX-License-Identifier: Apache-2.0
//! GNU `file`-compatible `getstr` escape resolution for `regex` pattern values.
//!
//! `TypeKind::Regex` patterns are captured here as a getstr-**resolved**
//! [`Value::String`] instead of falling through the generic
//! `parse_hex_bytes`/`parse_bare_string_value` bareword path used by the
//! other string-family types. Two problems make this its own module:
//!
//! 1. `parse_value`'s `alt(...)` tries `parse_hex_bytes` (see
//! `super::value`) before any string interpretation, so a `regex`
//! pattern beginning with a magic(5) escape (`\^`, `\040`, `\t`, `\x..`)
//! was previously captured as `Value::Bytes` instead of `Value::String`
//! -- the evaluator's `Regex` arms only accepted `Value::String`,
//! producing a fatal `UnsupportedType` error for every such rule.
//! 2. Rust's `regex` crate does not interpret octal escapes (`\040`) the
//! way GNU `file`'s `getstr` does, so handing the RAW escape text to
//! `regex::bytes::Regex::new` is not an option -- the pattern must be
//! getstr-resolved (escapes replaced by their resolved bytes) *before*
//! it reaches the regex compiler, exactly as GNU `file` does before
//! calling `regcomp`.
//!
//! # Escape table (verified against GNU `file` source, not inferred)
//!
//! The table below was read directly from `file/file`'s `src/apprentice.c`,
//! function `getstr` (`master` branch, fetched during this fix; the
//! function body starts with the comment "Convert a string containing C
//! character escapes. Stop at an unescaped space or tab."). The relevant
//! `switch (c = *s++)` after a backslash is seen:
//!
//! - `\a`, `\b`, `\f`, `\n`, `\r`, `\t`, `\v` resolve to the corresponding
//! C control-character byte: `0x07`, `0x08`, `0x0C`, `0x0A`, `0x0D`,
//! `0x09`, `0x0B`.
//! - `\0`-`\7` starts a 1-3 digit **octal** escape (`\NNN`); the resolved
//! value is truncated to a byte exactly like getstr's `CAST(char, val)`.
//! - `\x` starts a 0-2 digit **hex** escape (`\xNN`). If no valid hex
//! digit follows, getstr's `val` variable keeps its initialized default
//! of the literal character `'x'` (`0x78`) -- an intentional getstr
//! quirk, replicated here for fidelity.
//! - **Every other escaped character** -- the "relation" characters
//! `> < & ^ = !`, backslash itself, space, an escaped `.`, an escaped
//! literal tab character, and any genuinely unrecognized escape such as
//! `\^` -- falls through getstr's `default:` case into the shared
//! `case ' ':` body, whose only effect is `*p++ = c;`. In other words:
//! **the backslash is dropped and the following character is kept
//! literally.** This is not special-cased per character in the C
//! source; it is the general fallback for anything not in the named
//! list above -- the bullets above name the notable members, not an
//! exhaustive enumeration of this bucket.
//!
//! Note also: this resolver deliberately does **not** reproduce getstr's
//! `file_magwarn` lint diagnostics (the "unnecessary escape" / "no such
//! escape" warnings `apprentice.c` emits while *compiling* a magic file).
//! Those are magic-file-authoring diagnostics, orthogonal to escape
//! *resolution* -- this module's contract is producing the correct
//! resolved byte sequence for a pattern that already parsed, not linting
//! the source `.magic` file's style.
//!
//! ## Resolved open question: control-char / regex-shorthand collision
//!
//! GNU `file`'s getstr resolves `\b` to the raw control byte `0x08`
//! (backspace) -- Rust's `regex` crate assigns `\b` (the two-character
//! ASCII sequence backslash + `b`) the *different* meaning "word boundary
//! assertion". Confirmed from the `apprentice.c` source above that this is
//! genuinely how getstr behaves (case `'b'`: `*p++ = '\b';`), so the
//! collision the plan flagged is real *at the character level* -- but it
//! does **not** manifest as an actual bug here, because this resolver
//! never re-emits the two-character sequence `\` + `b`. It appends the
//! single resolved byte `0x08` directly to the pattern text. A lone raw
//! `0x08` byte, unescaped, has no special meaning to the `regex` crate --
//! it is matched as an ordinary literal byte, not reinterpreted as the
//! `\b` escape (that requires the literal two-character source sequence
//! `\b`, which this resolver by construction never produces for a
//! getstr-resolved byte `< 0x80`). The same reasoning applies to any
//! other named control escape (`\a`, `\f`, `\n`, `\r`, `\t`, `\v`): each
//! resolves to a single raw byte, not to escape *syntax*, so it cannot be
//! reinterpreted by the downstream regex compiler. No `\xHH` re-encoding
//! is needed for these -- only bytes `>= 0x80` require it (see
//! [`push_resolved_byte`]), because those cannot be represented as a
//! single-byte Rust `char`/UTF-8 sequence at all.
//!
//! Unrelated but worth noting for anyone reading a `.magic` file with
//! `\d`, `\s`, or `\w` in a `regex` pattern expecting PCRE-style character
//! classes: getstr does **not** special-case those letters, so they fall
//! into the "everything else" bucket above and are demoted to the bare
//! literal character (`\d` -> `d`, `\s` -> `s`, `\w` -> `w`). That is
//! genuine upstream libmagic behavior, not a bug in this resolver.
use IResult;
use multispace0;
use Error as NomError;
use crateValue;
/// Parse a single (unquoted) `regex` pattern token, resolving magic(5)
/// `getstr` escapes into the byte sequence GNU `file` would produce, and
/// return it as a [`Value::String`] ready for `regex::bytes::Regex::new`.
///
/// Token boundary: consumes non-whitespace characters, stopping at the
/// first *unescaped* whitespace character -- mirroring getstr's
/// `isspace(c)` early-exit (see module docs). Quoted (`"..."`) regex
/// values are **not** handled here; callers should keep using
/// [`super::value::parse_value`] for those (see `GOTCHAS.md` S3.6 and the
/// existing quoted-regex round-trip test for why quoting is preserved
/// as-is rather than getstr-resolved).
///
/// # Errors
/// Returns a nom parsing error if the input is empty, starts with
/// whitespace, or (after escape resolution) yields no characters at all.
pub
/// Resolve a single getstr escape sequence, appending its resolved
/// representation to `resolved`. `rest` is the input immediately
/// following the triggering backslash. Returns the remaining input after
/// the escape, or `None` if `rest` is empty (an incomplete trailing
/// escape -- see [`parse_regex_getstr_value`]).
/// Resolve a 1-3 digit octal escape (`\NNN`), matching getstr's
/// look-ahead-up-to-2-more-digits logic. `first` is the digit already
/// consumed by the caller's dispatch (guaranteed `'0'..='7'`).
/// Resolve a 0-2 digit hex escape (`\xNN`), matching getstr's quirky
/// "no digits following `\x`" default of the literal character `'x'`.
/// Append a resolved getstr byte to the in-progress pattern text.
///
/// Bytes `< 0x80` are valid ASCII / single-byte UTF-8 and are pushed
/// directly as a `char`. Bytes `>= 0x80` cannot be pushed as a raw `char`
/// (`0x80`-`0xFF` are not valid standalone Unicode scalar values, and
/// `String` must stay valid UTF-8 -- pushing one would panic or require
/// lossy reinterpretation that corrupts the intended byte value). Per the
/// plan's KTD3, such bytes are instead re-encoded as a regex-native
/// `\xHH` hex escape *in the output text*, so the regex engine -- not the
/// `String` -- carries the byte. This is a Rust-`String`-vs-C-`char*`
/// type-system accommodation, not a getstr behavior: GNU `file`'s C
/// implementation just writes the raw byte into an untyped buffer handed
/// to `regcomp`.