frameassembly 1.5.1

A DSL for creating custom .pcap/.pcapng captures and decompiling captures to .fasm for testing firewalls, IDS rules, SNMP traps, and pentesting.
Documentation
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
394
395
396
397
398
399
400
401
402
403
404
405
//! Structured compiler error definitions and rich diagnostic reporting for FrameAssembly.
//!
//! This module defines the [`CompilerError`] enumeration covering all failure modes across the
//! compilation pipeline: unrecognized character and symbol errors (`LexError`), grammar and
//! keyword errors (`ParseError`), networking rule validation errors (`SemanticError`), and
//! wire-level PCAP emission failures (`BackendError`). Each source-level error integrates with
//! [`miette`] to render file-named source spans and plain-English remediation hints tailored for
//! network technicians and engineers.

use alloc::boxed::Box;
use alloc::string::{String, ToString};
use miette::{Diagnostic, LabeledSpan, SourceSpan};

/// Extracts the offending text slice from `src` for a `(start, len)` byte span.
fn slice_at_span(src: &str, span: (usize, usize)) -> &str {
    let start = span.0.min(src.len());
    let end = span.0.saturating_add(span.1).min(src.len());
    src.get(start..end).unwrap_or("")
}

/// Produces a plain-English explanation of an unrecognized character or symbol for [`CompilerError::LexError`].
fn lex_error_summary(src: &str, span: (usize, usize)) -> String {
    let snippet = slice_at_span(src, span);
    let rest = src.get(span.0.min(src.len())..).unwrap_or("");
    if rest.starts_with('#') {
        "Failed to parse token '#': comments in .fasm start with '//', not '#'".to_string()
    } else if rest.starts_with("/*") || rest.starts_with("*/") {
        "Failed to parse token '/*': block comments are not supported in .fasm (use '//' instead)"
            .to_string()
    } else if rest.starts_with('\'') {
        alloc::format!(
            "Failed to parse token '{}': single quotes are not used for text strings in .fasm (use double quotes \"...\" instead)",
            snippet
        )
    } else if rest.starts_with('/')
        && rest.as_bytes().get(1).is_some_and(|b| b.is_ascii_digit())
    {
        alloc::format!(
            "Failed to parse token '{}': subnet masks (CIDR notation) are not used in HOST or IP declarations",
            snippet
        )
    } else if rest.starts_with('=') || rest.starts_with(':') {
        let ch = rest.chars().next().unwrap_or('=');
        alloc::format!(
            "Failed to parse token '{}': assignment symbol '{}' is not used between keywords and values in .fasm",
            ch,
            ch
        )
    } else if rest.starts_with('[') || rest.starts_with(']') {
        let ch = rest.chars().next().unwrap_or('[');
        alloc::format!(
            "Failed to parse token '{}': square brackets '[' and ']' are not used in .fasm",
            ch
        )
    } else if rest.starts_with(';') {
        "Failed to parse token ';': semicolons are not needed at the end of lines in .fasm"
            .to_string()
    } else if rest.starts_with('+') {
        "Failed to parse token '+': relative delay prefixes like '+5ms' are not used in .fasm"
            .to_string()
    } else if snippet.is_empty() {
        "Failed to parse token".to_string()
    } else {
        alloc::format!("Failed to parse token '{}'", snippet)
    }
}

/// Detects whether `src` appears to be a foreign `.fasm` file (such as x86 Flat Assembler
/// or FPGA FASM bitstream configuration) rather than a FrameAssembly network packet specification.
///
/// Returns `Some("an x86 Flat Assembler")` or `Some("an FPGA FASM")` if foreign signatures
/// are identified and the input lacks canonical FrameAssembly `FLOW` and `COMPILE` structure.
pub fn detect_foreign_fasm(src: &str) -> Option<&'static str> {
    // If the file already contains clear FrameAssembly host/flow and compile constructs, it is not foreign.
    let has_compile = src.lines().any(|l| {
        let trimmed = l.trim();
        trimmed.to_ascii_uppercase().starts_with("COMPILE")
    });
    let has_flow = src.lines().any(|l| {
        let trimmed = l.trim();
        trimmed.to_ascii_uppercase().starts_with("FLOW")
    });
    let has_host = src.lines().any(|l| {
        let trimmed = l.trim();
        trimmed.to_ascii_uppercase().starts_with("HOST")
    });
    if has_compile && (has_flow || has_host) {
        return None;
    }

    let mut has_semicolon_comment = false;
    let mut has_x86_directive = false;
    let mut has_hash_comment = false;
    let mut has_fpga_tile = false;
    let mut has_dot_tile_feature = false;

    let fpga_tiles = ["CLB", "LUT", "IOB", "INT_L", "INT_R", "BRAM", "DSP", "SLICE"];

    for line in src.lines() {
        let trimmed = line.trim();
        if trimmed.is_empty() {
            continue;
        }
        if trimmed.starts_with(';') {
            has_semicolon_comment = true;
        }
        if trimmed.starts_with('#') {
            has_hash_comment = true;
        }

        let upper = trimmed.to_ascii_uppercase();
        if upper.starts_with("FORMAT ")
            || upper.starts_with("SECTION ")
            || upper.starts_with("SEGMENT ")
            || upper.starts_with("USE16")
            || upper.starts_with("USE32")
            || upper.starts_with("USE64")
            || upper.starts_with("ORG ")
            || upper.starts_with("ENTRY ")
            || (upper.starts_with("INCLUDE ") && (trimmed.contains('\'') || trimmed.contains('"')))
        {
            has_x86_directive = true;
        }

        for tile in &fpga_tiles {
            if upper.contains(tile) {
                has_fpga_tile = true;
                if trimmed.contains('.') && (trimmed.contains('[') || trimmed.contains('=')) {
                    has_dot_tile_feature = true;
                }
                break;
            }
        }
    }

    if has_x86_directive || (has_semicolon_comment && !has_flow && !has_compile && !has_host) {
        return Some("an x86 Flat Assembler");
    }

    if has_fpga_tile
        && (has_hash_comment || has_dot_tile_feature)
        && !has_flow
        && !has_compile
        && !has_host
    {
        return Some("an FPGA FASM");
    }

    None
}

/// Produces a plain-English remediation hint for [`CompilerError::LexError`].
fn lex_error_help(src: &str, span: (usize, usize)) -> String {
    if let Some(kind) = detect_foreign_fasm(src) {
        return alloc::format!(
            "This file appears to be {} file, not a FrameAssembly network packet specification. FrameAssembly files declare HOST {{ IP ... }}, FLOW(...), and COMPILE {{ ... }} blocks to compile network traffic into .pcap/.pcapng captures. See https://github.com/SteveWinschel/FrameAssembly for syntax and examples.",
            kind
        );
    }

    let rest = src.get(span.0.min(src.len())..).unwrap_or("");
    if rest.starts_with('#') {
        "Replace '#' with '//' to write a comment (for example: // Define client and server hosts)."
            .to_string()
    } else if rest.starts_with("/*") || rest.starts_with("*/") {
        "Start each comment line with '//' instead of wrapping comments in '/* ... */'.".to_string()
    } else if rest.starts_with('\'') {
        "Wrap text strings in double quotes instead of single quotes (for example: URI \"/index.html\" or USER \"admin\").".to_string()
    } else if rest.starts_with('/')
        && rest.as_bytes().get(1).is_some_and(|b| b.is_ascii_digit())
    {
        "Remove the CIDR prefix length and write a plain IP address (for example: IP 10.0.0.1 instead of IP 10.0.0.1/24).".to_string()
    } else if rest.starts_with('=') || rest.starts_with(':') {
        "Separate keywords and values with a space instead of '=' or ':' (for example: IP 10.0.0.1 or DSTPORT 443).".to_string()
    } else if rest.starts_with('[') || rest.starts_with(']') {
        "Write TCP flags as space-separated words without brackets (for example: TCP SYN ACK) and place delays at the end of the line (WAIT 5ms).".to_string()
    } else if rest.starts_with(';') {
        "Remove the ';' at the end of the line — statements end automatically at the newline."
            .to_string()
    } else if rest.starts_with('+') {
        "Put 'WAIT <duration>' at the end of the packet statement instead (for example: c -> s TCP SYN ACK WAIT 5ms).".to_string()
    } else {
        "Check for unsupported punctuation or typos. See docs/getting_started.md for valid .fasm syntax and examples.".to_string()
    }
}

/// Produces a plain-English remediation hint for [`CompilerError::ParseError`].
fn parse_error_help(message: &str, src: &str, span: (usize, usize)) -> String {
    if let Some(kind) = detect_foreign_fasm(src) {
        return alloc::format!(
            "This file appears to be {} file, not a FrameAssembly network packet specification. FrameAssembly files declare HOST {{ IP ... }}, FLOW(...), and COMPILE {{ ... }} blocks to compile network traffic into .pcap/.pcapng captures. See https://github.com/SteveWinschel/FrameAssembly for syntax and examples.",
            kind
        );
    }

    let snippet = slice_at_span(src, span).to_ascii_lowercase();
    if matches!(snippet.as_str(), "sport" | "src_port" | "source_port") {
        "Replace 'sport' with 'SRCPORT <port>' (for example: SRCPORT 51234).".to_string()
    } else if matches!(
        snippet.as_str(),
        "dport" | "dst_port" | "dest_port" | "destination_port"
    ) {
        "Replace 'dport' with 'DSTPORT <port>' (for example: DSTPORT 443).".to_string()
    } else if message.contains("ACKNUM") {
        "Use 'ACK' alone as a TCP flag, and use 'ACKNUM <number>' to set the TCP acknowledgment number (for example: TCP ACK ACKNUM 100).".to_string()
    } else if matches!(snippet.as_str(), "hoplimit" | "hop_limit" | "hlim")
        || message.contains("Hop Limit")
    {
        "Use 'TTL <value>' for both IPv4 Time-to-Live and IPv6 Hop Limit (for example: TTL 64 or TTL 255).".to_string()
    } else if matches!(
        snippet.as_str(),
        "tos" | "traffic_class" | "trafficclass" | "tclass"
    ) {
        "Use 'DSCP <0..63>' and optional 'ECN <0..3>' to set IPv4 TOS / IPv6 Traffic Class bits (for example: DSCP 46).".to_string()
    } else if matches!(snippet.as_str(), "cos" | "dot1p") {
        "Use 'VLAN <id> PCP <0..7>' to set the Layer 2 802.1p Class of Service priority.".to_string()
    } else if matches!(
        snippet.as_str(),
        "vlan_id" | "vlanid" | "vid" | "svlan" | "cvlan" | "outer_vlan"
    ) || message.contains("INNER_VLAN")
    {
        "Use 'VLAN 100' for single 802.1Q tagging, or 'QINQ 100 INNER_VLAN 200' for 802.1ad double tagging.".to_string()
    } else if message.contains("HOST_HDR") {
        "Inside an HTTP statement, use 'HOST_HDR \"example.com\"' to set the HTTP Host header ('HOST' is reserved for declaring network endpoints).".to_string()
    } else if message.contains("missing a required 'IP")
        || message.contains("must specify at least an 'IP <address>' or 'MAC <address>'")
    {
        "Add 'IP <address>' and/or 'MAC <address>' inside the HOST block — for example: HOST client { IP 10.0.0.1 MAC 00:11:22:33:44:55 }.".to_string()
    } else if message.contains("missing its endpoint parameter list") {
        "Add endpoint parameter names in parentheses after the flow name — for example: FLOW my_flow(c, s) { c -> s TCP SYN }.".to_string()
    } else if message.contains("missing 'COMPILE") || message.contains("duplicate 'COMPILE'") {
        "Every .fasm file must contain exactly one COMPILE { ... } block at the end that calls your flows — for example: COMPILE { my_flow(client, server) }.".to_string()
    } else if message.contains("outside a FLOW block") {
        "Move packet statements ('a -> b ...') inside a FLOW name(a, b) { ... } block (then call name(host1, host2) inside COMPILE { ... }) or place inline packet statements directly inside COMPILE { ... }.".to_string()
    } else {
        "Ensure the syntax matches the expected grammar. See docs/getting_started.md for ready-to-use .fasm examples.".to_string()
    }
}

/// Produces a plain-English remediation hint for [`CompilerError::SemanticError`].
fn semantic_error_help(message: &str) -> String {
    if message.contains("Mixed IPv4 and IPv6") {
        "Use either two IPv4 hosts or two IPv6 hosts in the same flow call — a single packet cannot have an IPv4 source and an IPv6 destination.".to_string()
    } else if message.contains("requires an 'IP <address>'") {
        "Add 'IP <address>' to the HOST declaration (for example: HOST r1 { IP 10.0.0.1 MAC 00:11:22:33:44:55 }), or use a pure Layer 2 protocol (ARP, VLAN, QINQ, MPLS, LLDP, STP, LACP, EAPOL).".to_string()
    } else if message.contains("Undefined host") {
        "Declare the host at the top of the file with 'HOST <name> { IP <address> }' before passing it into a flow call in COMPILE.".to_string()
    } else if message.contains("Undefined flow") {
        "Declare the flow with 'FLOW <name>(a, b) { ... }' before calling it inside COMPILE.".to_string()
    } else if message.contains("Undefined endpoint") {
        "Make sure the endpoint name on the packet line matches one of the parameter names listed in 'FLOW name(param1, param2)'.".to_string()
    } else if message.contains("expects") && message.contains("arguments") {
        "Pass the exact number of host names in 'COMPILE { flow_name(host1, host2) }' to match the parameters in 'FLOW flow_name(a, b)'.".to_string()
    } else {
        "Check the highlighted field value or endpoint name against protocol limits.".to_string()
    }
}

/// Represents all possible errors that can occur during the compilation process.
///
/// This enum implements [`miette::Diagnostic`] for rich terminal diagnostics with filename spans
/// and plain-English troubleshooting hints for network technicians.
#[derive(Debug)]
pub enum CompilerError {
    /// Occurs when the lexer encounters an invalid sequence of characters that cannot be tokenized.
    LexError {
        /// The source code snippet.
        src: String,
        /// The byte offset and byte length `(start, len)` of the error.
        span: (usize, usize),
    },

    /// Occurs when the parser encounters a syntax error or unexpected token.
    ParseError {
        /// The parser error message.
        message: String,
        /// The source code snippet.
        src: String,
        /// The byte offset and byte length `(start, len)` of the error.
        span: (usize, usize),
    },

    /// Occurs when the specification is syntactically valid but violates networking or semantic rules.
    SemanticError {
        /// The semantic error message.
        message: String,
        /// The source code snippet.
        src: String,
        /// The byte offset and byte length `(start, len)` of the error.
        span: (usize, usize),
    },

    /// Occurs during the backend PCAP generation (e.g. invalid IP address formatting, missing file access).
    BackendError(String),
}

impl Diagnostic for CompilerError {
    fn code<'a>(&'a self) -> Option<Box<dyn core::fmt::Display + 'a>> {
        let code_str = match self {
            Self::LexError { .. } => "frameassembly::lex_error",
            Self::ParseError { .. } => "frameassembly::parse_error",
            Self::SemanticError { .. } => "frameassembly::semantic_error",
            Self::BackendError(_) => "frameassembly::backend_error",
        };
        Some(Box::new(code_str))
    }

    fn help<'a>(&'a self) -> Option<Box<dyn core::fmt::Display + 'a>> {
        match self {
            Self::LexError { src, span } => Some(Box::new(lex_error_help(src, *span))),
            Self::ParseError { message, src, span } => {
                Some(Box::new(parse_error_help(message, src, *span)))
            }
            Self::SemanticError { message, .. } => Some(Box::new(semantic_error_help(message))),
            Self::BackendError(_) => None,
        }
    }

    fn labels(&self) -> Option<Box<dyn Iterator<Item = LabeledSpan> + '_>> {
        match self {
            Self::LexError { span, .. } => Some(Box::new(core::iter::once(
                LabeledSpan::new_with_span(
                    Some("Invalid token".to_string()),
                    SourceSpan::from(*span),
                ),
            ))),
            Self::ParseError { span, .. } => Some(Box::new(core::iter::once(
                LabeledSpan::new_with_span(Some("Here".to_string()), SourceSpan::from(*span)),
            ))),
            Self::SemanticError { span, .. } => Some(Box::new(core::iter::once(
                LabeledSpan::new_with_span(
                    Some("Error occurred here".to_string()),
                    SourceSpan::from(*span),
                ),
            ))),
            Self::BackendError(_) => None,
        }
    }
}

impl core::fmt::Display for CompilerError {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        match self {
            Self::LexError { src, span } => write!(f, "{}", lex_error_summary(src, *span)),
            Self::ParseError { message, .. } => write!(f, "Failed to parse input: {}", message),
            Self::SemanticError { message, .. } => write!(f, "Semantic error: {}", message),
            Self::BackendError(msg) => write!(f, "Backend generation error: {}", msg),
        }
    }
}

impl std::error::Error for CompilerError {}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_detect_x86_flat_assembler() {
        let sample1 = "format PE GUI 4.0\nentry start\nsection '.text' code readable executable\nstart:\n  ret\n";
        assert_eq!(detect_foreign_fasm(sample1), Some("an x86 Flat Assembler"));

        let sample2 = "; Flat Assembler source\nuse32\norg 0x100\nmov ax, 0x4c00\nint 0x21\n";
        assert_eq!(detect_foreign_fasm(sample2), Some("an x86 Flat Assembler"));

        let sample3 = "format ELF64 executable\nsegment readable executable\nentry $\n";
        assert_eq!(detect_foreign_fasm(sample3), Some("an x86 Flat Assembler"));
    }

    #[test]
    fn test_detect_fpga_fasm() {
        let sample1 = "# Generated by Project X-Ray\nCLBLL_L_X12Y34.SLICEL_X0.ALUT.INIT[31:0] = 32'h00000000\nIOB_X0Y1.ENABLE 1\n";
        assert_eq!(detect_foreign_fasm(sample1), Some("an FPGA FASM"));

        let sample2 = "CLBLL_L_X12Y34.SLICEL_X0.ALUT.INIT[31:0] = 32'h00000000\n";
        assert_eq!(detect_foreign_fasm(sample2), Some("an FPGA FASM"));
    }

    #[test]
    fn test_valid_frameassembly_is_not_foreign() {
        let sample = "HOST client { IP 10.0.0.1 }\nFLOW ping(c, s) { c -> s ICMP }\nCOMPILE { ping(client, client) }\n";
        assert_eq!(detect_foreign_fasm(sample), None);
    }

    #[test]
    fn test_foreign_fasm_lex_error_help_hint() {
        let sample = "; x86 assembly file\nformat PE\n";
        let help = lex_error_help(sample, (0, 1));
        assert!(help.contains("x86 Flat Assembler"));
        assert!(help.contains("FrameAssembly files declare HOST { IP ... }, FLOW(...), and COMPILE { ... } blocks"));
        assert!(help.contains("https://github.com/SteveWinschel/FrameAssembly"));

        let fpga_sample = "# FPGA bitstream\nCLBLL_L_X0Y0.ENABLE 1\n";
        let fpga_help = lex_error_help(fpga_sample, (0, 1));
        assert!(fpga_help.contains("FPGA FASM"));
    }

    #[test]
    fn test_foreign_fasm_parse_error_help_hint() {
        let sample = "format PE GUI 4.0\nentry start\n";
        let help = parse_error_help("unexpected token", sample, (0, 6));
        assert!(help.contains("x86 Flat Assembler"));
    }
}