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
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
//! Preprocessed source lines (FAS.TXT table 3).
use crate::error::{FasError, Result};
use crate::fas::bytes;
use crate::fas::header::Header;
use std::collections::{BTreeMap, BTreeSet};
/// One preprocessed line, keyed by its offset inside the preprocessed blob.
#[derive(Debug, Clone)]
pub struct PrepLine {
/// Offset of this record inside the preprocessed source.
pub offset: u32,
/// Zero = main input file; otherwise offset of an ASCIIZ file name inside
/// the preprocessed blob. Ignored when [`Self::generated_by_macro`].
pub file_or_macro: u32,
/// 1-based line number in the originating source file (high bit stripped).
pub line_number: u32,
/// Token stream was produced by a macro rather than loaded from a file.
pub generated_by_macro: bool,
/// Byte position of this line in the source file, or the invoking line
/// offset when this line is macro-generated.
pub source_pos_or_invoke: u32,
/// Offset of the macro-definition line when macro-generated.
pub macro_def_offset: u32,
/// Offset of the first token byte in the preprocessed blob.
pub tokens_start: u32,
/// Offset immediately after the terminating zero token.
pub tokens_end: u32,
}
impl PrepLine {
/// Parse a single table-3 header at `offset` inside `prep`.
///
/// Token bytes after offset 16 are skipped until a 0 terminator so the
/// caller can find the next line. Quoted tokens (`22h`) carry a 32-bit
/// length; symbol tokens (`1Ah` / `3Bh`) carry a 8-bit length.
pub fn parse_at(prep: &[u8], offset: u32) -> Result<(Self, u32)> {
let o = offset as usize;
if o + 16 > prep.len() {
return Err(FasError::Truncated("preprocessed line header"));
}
let file_or_macro = bytes::u32_at(prep, o)?;
let line_field = bytes::u32_at(prep, o + 4)?;
let source_pos_or_invoke = bytes::u32_at(prep, o + 8)?;
let macro_def_offset = bytes::u32_at(prep, o + 12)?;
let generated_by_macro = (line_field & 0x8000_0000) != 0;
let line_number = line_field & 0x7FFF_FFFF;
let mut i = o + 16;
while i < prep.len() {
match prep[i] {
0 => {
let next = (i + 1) as u32;
return Ok((
Self {
offset,
file_or_macro,
line_number,
generated_by_macro,
source_pos_or_invoke,
macro_def_offset,
tokens_start: (o + 16) as u32,
tokens_end: next,
},
next,
));
}
0x1A | 0x3B => {
let n = *prep.get(i + 1).ok_or(FasError::Truncated("sym token"))? as usize;
i = i
.checked_add(2 + n)
.ok_or(FasError::Truncated("sym token"))?;
}
0x22 => {
let n = bytes::u32_at(prep, i + 1)? as usize;
i = i
.checked_add(5 + n)
.ok_or(FasError::Truncated("quote token"))?;
}
_ => i += 1,
}
}
Err(FasError::Truncated("unterminated preprocessed line"))
}
/// Token bytes including the terminating zero byte.
pub fn token_bytes<'a>(&self, prep: &'a [u8]) -> Result<&'a [u8]> {
prep.get(self.tokens_start as usize..self.tokens_end as usize)
.ok_or(FasError::Truncated("preprocessed tokens"))
}
/// Decode the tokenized line into structured tokens.
pub fn tokens(&self, prep: &[u8]) -> Result<Vec<SourceToken>> {
decode_tokens(self.token_bytes(prep)?)
}
/// Deterministic textual rendering used when original source is absent.
pub fn detokenize(&self, prep: &[u8]) -> Result<String> {
Ok(render_tokens(&self.tokens(prep)?))
}
}
/// One token from a preprocessed source line.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SourceToken {
/// Normal symbol token (`1Ah`).
Symbol(Vec<u8>),
/// Symbol token already interpreted by the preprocessor (`3Bh`).
InterpretedSymbol(Vec<u8>),
/// Quoted byte sequence (`22h`).
Quoted(Vec<u8>),
/// One special-character token.
Character(u8),
}
/// One macro provenance hop.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MacroFrame {
/// Generated line offset.
pub generated_line: u32,
/// Macro name, when its Pascal string is valid.
pub macro_name: Option<String>,
/// Invoking line offset.
pub invocation_line: u32,
/// Macro-definition line offset.
pub definition_line: u32,
}
/// Complete macro chain and its physical-source origin.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Provenance {
/// Generated macro frames, outermost traversal order.
pub frames: Vec<MacroFrame>,
/// Physical-source line reached by following invocation links.
pub origin_offset: u32,
/// Why traversal stopped early, when the chain was incomplete.
pub diagnostic: Option<ProvenanceDiagnostic>,
}
/// Macro-chain traversal issue.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ProvenanceDiagnostic {
/// An invocation offset did not resolve to a parsed source row.
MissingLine(u32),
/// An invocation link repeated a row already visited.
Cycle(u32),
}
/// Index of every preprocessed line keyed by blob offset.
#[derive(Debug, Clone, Default)]
pub struct PrepSource {
/// Offset → line.
pub lines: BTreeMap<u32, PrepLine>,
}
impl PrepSource {
/// Walk the whole preprocessed blob.
pub fn parse(prep: &[u8]) -> Result<Self> {
let mut lines = BTreeMap::new();
let mut off = 0u32;
while (off as usize) < prep.len() {
// File names stored *inside* the blob as ASCIIZ are not line
// records. A line always starts with a 16-byte header; if we
// cannot parse, stop. FASM packs names at offsets referenced by
// lines, typically overlapping the stream. In practice the blob
// is a sequence of lines only — names live in the same blob at
// offsets that point *into* a previous line's token area or at
// dedicated spots. We parse strictly as a line sequence.
match PrepLine::parse_at(prep, off) {
Ok((line, next)) => {
if next <= off {
return Err(FasError::Geometry("preprocessed line did not advance"));
}
lines.insert(off, line);
off = next;
}
Err(_) => break,
}
}
Ok(Self { lines })
}
/// Look up a line by preprocessed-blob offset.
pub fn get(&self, offset: u32) -> Option<&PrepLine> {
self.lines.get(&offset)
}
/// File name for a non-macro line. `input_name` is used when the line
/// belongs to the main source file (`file_or_macro == 0`).
pub fn file_name<'a>(
&'a self,
line: &PrepLine,
prep: &'a [u8],
input_name: &'a str,
) -> Result<&'a str> {
if line.generated_by_macro {
return Ok(input_name);
}
if line.file_or_macro == 0 {
return Ok(input_name);
}
bytes::cstring_at(prep, line.file_or_macro as usize)
}
/// Walk macro invocation links until a source-file line is found.
pub fn origin<'a>(&'a self, mut line: &'a PrepLine) -> &'a PrepLine {
let mut guard = 0u32;
while line.generated_by_macro && guard < 64 {
guard += 1;
match self.get(line.source_pos_or_invoke) {
Some(invoker) => line = invoker,
None => break,
}
}
line
}
/// Pascal-style macro name stored at `file_or_macro` when the line is
/// macro-generated. `None` if the pointer is invalid.
pub fn macro_name<'a>(&self, line: &PrepLine, prep: &'a [u8]) -> Option<&'a str> {
if !line.generated_by_macro {
return None;
}
bytes::pascal_at(prep, line.file_or_macro as usize).ok()
}
/// Preserve every macro invocation/definition hop and report broken chains.
pub fn provenance(&self, line: &PrepLine, prep: &[u8]) -> Provenance {
let mut current = line;
let mut frames = Vec::new();
let mut seen = BTreeSet::new();
while current.generated_by_macro {
if !seen.insert(current.offset) {
return Provenance {
frames,
origin_offset: current.offset,
diagnostic: Some(ProvenanceDiagnostic::Cycle(current.offset)),
};
}
frames.push(MacroFrame {
generated_line: current.offset,
macro_name: self.macro_name(current, prep).map(str::to_owned),
invocation_line: current.source_pos_or_invoke,
definition_line: current.macro_def_offset,
});
let Some(invoker) = self.get(current.source_pos_or_invoke) else {
return Provenance {
frames,
origin_offset: current.offset,
diagnostic: Some(ProvenanceDiagnostic::MissingLine(
current.source_pos_or_invoke,
)),
};
};
current = invoker;
}
Provenance {
frames,
origin_offset: current.offset,
diagnostic: None,
}
}
}
fn decode_tokens(raw: &[u8]) -> Result<Vec<SourceToken>> {
let mut tokens = Vec::new();
let mut index = 0usize;
while index < raw.len() {
match raw[index] {
0 => return Ok(tokens),
kind @ (0x1A | 0x3B) => {
let length =
*raw.get(index + 1)
.ok_or(FasError::Truncated("symbol token"))? as usize;
let bytes = raw
.get(index + 2..index + 2 + length)
.ok_or(FasError::Truncated("symbol token"))?
.to_vec();
tokens.push(if kind == 0x1A {
SourceToken::Symbol(bytes)
} else {
SourceToken::InterpretedSymbol(bytes)
});
index += 2 + length;
}
0x22 => {
let length = bytes::u32_at(raw, index + 1)? as usize;
let quoted = raw
.get(index + 5..index + 5 + length)
.ok_or(FasError::Truncated("quoted token"))?
.to_vec();
tokens.push(SourceToken::Quoted(quoted));
index += 5 + length;
}
character => {
tokens.push(SourceToken::Character(character));
index += 1;
}
}
}
Err(FasError::Truncated("unterminated token stream"))
}
fn render_tokens(tokens: &[SourceToken]) -> String {
let mut output = String::new();
let mut previous_word = false;
for token in tokens {
let (text, word) = match token {
SourceToken::Symbol(bytes) | SourceToken::InterpretedSymbol(bytes) => {
(String::from_utf8_lossy(bytes).into_owned(), true)
}
SourceToken::Quoted(bytes) => {
let mut quoted = String::from("\"");
for byte in bytes {
match byte {
b'\\' => quoted.push_str("\\\\"),
b'\"' => quoted.push_str("\\\""),
0x20..=0x7e => quoted.push(char::from(*byte)),
_ => quoted.push_str(&format!("\\x{byte:02x}")),
}
}
quoted.push('\"');
(quoted, true)
}
SourceToken::Character(character) => (char::from(*character).to_string(), false),
};
if word && previous_word {
output.push(' ');
}
output.push_str(&text);
previous_word = word;
}
output
}
/// Convenience wrapper used by [`crate::fas::FasFile::parse`].
pub fn parse_preprocessed(header: &Header, data: &[u8]) -> Result<PrepSource> {
PrepSource::parse(header.preprocessed(data)?)
}
#[cfg(test)]
mod tests {
use super::*;
fn line(offset: u32, generated: bool, invocation: u32) -> PrepLine {
PrepLine {
offset,
file_or_macro: 0,
line_number: 1,
generated_by_macro: generated,
source_pos_or_invoke: invocation,
macro_def_offset: 0,
tokens_start: 0,
tokens_end: 1,
}
}
#[test]
fn decodes_and_renders_token_streams() {
let raw = [
0x1a, 3, b'm', b'o', b'v', 0x1a, 3, b'e', b'a', b'x', b',', 0x22, 4, 0, 0, 0, b'A',
b'B', b'C', b'D', 0,
];
let tokens = decode_tokens(&raw).unwrap();
assert_eq!(
tokens,
[
SourceToken::Symbol(b"mov".to_vec()),
SourceToken::Symbol(b"eax".to_vec()),
SourceToken::Character(b','),
SourceToken::Quoted(b"ABCD".to_vec()),
]
);
assert_eq!(render_tokens(&tokens), "mov eax,\"ABCD\"");
}
#[test]
fn provenance_reports_cycles_and_missing_invocations() {
let mut source = PrepSource::default();
source.lines.insert(0, line(0, true, 10));
source.lines.insert(10, line(10, true, 0));
let cycle = source.provenance(source.get(0).unwrap(), b"");
assert_eq!(cycle.diagnostic, Some(ProvenanceDiagnostic::Cycle(0)));
assert_eq!(cycle.frames.len(), 2);
let missing_line = line(20, true, 99);
let missing = source.provenance(&missing_line, b"");
assert_eq!(
missing.diagnostic,
Some(ProvenanceDiagnostic::MissingLine(99))
);
}
}