Spicy Parser Generator

repository·main·Indexed 43 days ago

https://github.com/zeek/spicy

A domain-specific language and toolchain for generating efficient, robust C++ parsers for network protocols and file formats. It features incremental parsing, concurrency, and flexible compilation via Just-in-Time (JIT) or Ahead-of-Time (AOT) methods, with deep integration into the Zeek network security monitor.

Tokens
55.1K
Snippets
199
Records
275
Agent score
52%

What's inside Spicy

  1. Overview of Spicy parser generator

    main

    Spicy is a parser generator designed to create robust C++ parsers for network protocols, file formats, and other structured data. It uses an attributed grammar approach, allowing developers to define both the syntax and semantics of an input format within a single domain-specific language.

    Key features include:

    • Incremental Parsing: Parsing proceeds incrementally on input streams of arbitrary size.
    • Concurrency: Supports highly concurrent parsing.
    • Flexible Compilation:
      • Just-in-Time (JIT): Parsers are compiled at startup via a C++ compiler.
      • Ahead-of-Time (AOT): Parsers can be pre-compiled into shared libraries or generated as C++ code for direct linking into applications.
    • Zeek Integration: A dedicated Zeek plugin allows adding protocol and file analyzers to Zeek without writing C++ code, simply by defining the grammar and specifying Zeek events.
  2. Spicy Performance Guidelines Overview

    main
    This section provides best practices and patterns for optimizing Spicy parser performance. Currently, the guidelines focus on two primary areas: Runtime Performance (how fast the parser executes during traffic processing) and Compilation Performance (how long it takes to compile the Spicy parser into C++).
  3. What is Spicy and how does it work?

    main

    Spicy is a parser generator used to create robust C++ parsers for network protocols and file formats. It uses an attributed grammar language to describe both the syntax and semantics of an input format in a single unified language.

    Key Characteristics:

    • Toolchain: Converts Spicy grammars into efficient C++ parsing code.
    • Runtime: Supports fully incremental and highly concurrent parsing on input streams of arbitrary size.
    • Compilation Modes:
      • Just-in-time (JIT): Compilation occurs at startup via a C++ compiler.
      • Ahead-of-time (AOT): Creates pre-compiled shared libraries or generates C++ code that can be linked into an application.
    • Zeek Integration: Allows adding new protocol and file analyzers to Zeek without writing C++ code by defining grammars and specifying Zeek events.
    # Example of a Spicy grammar output for an HTTP request
    # echo "GET /index.html HTTP/1.0" | spicy-driver http-request.spicy
    [$method=b"GET", $uri=b"/index.html", $version=[$number=b"1.0"]]
  4. Branch parsing logic with the switch construct

    main

    The switch construct allows branching into different parsing alternatives. There are two primary modes:

    1. Branch by expression

    Evaluates an expression at the time of the switch. If a value matches, parsing continues with the corresponding field.

    switch ( EXPR ) {
        VALUE_1 -> FIELD_1;
        VALUE_2 -> FIELD_2;
        *      -> DEFAULT_FIELD; # Use '*' for a default case
    };

    Features:

    • Multiple values: Use commas to match multiple values: b"A", b"B" -> x: int8;.
    • Multiple fields: Enclose fields in braces: VALUE -> { FIELD_1; FIELD_2; }.
    • Anonymous branches: Use void if you only need to branch without storing data: b"A" -> : void { DoSomething(); }.
    • Field reuse: If alternatives use the same field name and type, the unit will contain only one instance of that member.

    2. Branch by look-ahead

    If no expression is provided, Spicy attempts to identify the correct branch by looking ahead at the upcoming input.

    switch {
        -> a: b"A";
        -> b: b"B";
    };

    3. Switching with common size

    You can apply a size constraint to all branches within a switch using the &size=EXPR attribute on the switch itself. This limits the number of raw bytes available to the switch logic.

    module Test;
    
    public type Foo = unit {
        x: bytes &size=1;
        switch ( self.x ) {
            b"A" -> a8: int8;
            b"B" -> a16: int16;
            b"C" -> a32: int32;
        };
    
        on %done { print self; }
    };
  5. Define data structures using `unit` types

    main

    In Spicy, data structures are defined using the unit type. A unit is an ordered set of named fields, each with a specific type. Units can be instantiated, and their fields can be assigned or retrieved values.

    Key behaviors:

    • Unset Fields: Fields are initially unset. Accessing an unset field triggers a runtime error.
    • Default Values: Use the &default=VALUE attribute to provide a fallback value. If a field is accessed but has no explicitly assigned value, the default is returned. Note that the field remains technically 'unset' even when a default is used.
    • Development Tip: When using spicy-driver, use the --debug (or -d) option. This prevents the optimizer from eliding fields it deems unused, which is helpful during development.
    type Foo = unit {
        version: uint32 &default=42;
    };
    
    module Test;
    
    global f: Foo;
    print f.version; // Prints 42 if unset
  6. Chain units using Sinks

    main

    Sinks allow you to chain multiple units together in a layered stack, where one unit writes data that another unit reads (similar to a Unix pipe).

    module Test;
    
    public type A = unit {
        on %init { self.b.connect(new B); }
    
        length: uint8;
        // Option 1: Explicit write
        data: bytes &size=self.length { self.b.write($$); }
        // Option 2: Syntactic sugar for bytes fields
        // data: bytes &size=self.length -> self.b;
    
        on %done { print "A", self; }
    
        sink b;
    };
    
    public type B = unit {
        : /GET /;
        path: /[^
    ]+/;
        on %done { print "B", self; }
    };
  7. Dynamic unit selection with MIME types and Sinks

    main

    When the appropriate unit to connect to a sink is only known at runtime, you can use MIME types for dynamic selection.

    1. Declare MIME types: A unit can declare which MIME types it handles using metadata.
    2. Connect via MIME type: Use sink::connect_mime_type(type_string) to instantiate and connect a unit that matches the provided string (e.g., "text/html" or a custom convention like "x-port/443").
  8. Implement manual backtracking with &try

    main

    Spicy supports manual backtracking to handle uncertain parsing paths.

    1. Mark a field with the &try attribute.
    2. If parsing fails or an invalid state is reached, call self.backtrack() within the unit (e.g., inside an if block or an %error hook).

    Behavior:

    • Control is immediately transferred to the field following the &try field.
    • The input stream position is reset to where the &try field started.
    • Units along the original path remain in their partially initialized state.
    • If multiple &try fields are involved, control continues after the most recent one.
    module Test;
    
    public type test = unit {
        foo: Foo &try;
        bar: Bar;
    
        on %done { print self; }
    };
    
    type Foo = unit {
        a: int8 {
            if ( $$ != 1 )
                self.backtrack();
        }
        b: int8;
    };
  9. Use Filters to preprocess unit input

    main

    Filters are special unit types marked with the %filter property. They allow you to transform or preprocess input (e.g., decompression, decoding) before it reaches the main unit.

    How to use filters

    1. Define a unit with the %filter property.
    2. Use unit::forward(data) within the filter to pass transformed data to the next stage.
    3. Attach the filter to a target unit using unit::connect_filter(new FilterType).

    Built-in Filters

    Import the filter library module to access:

    • filter::Zlib: For zlib decompression.
    • filter::Base64Decode: For Base64 decoding.
    module Test;
    
    type Filter = unit {
        %filter;
    
        : bytes &eod &chunked {
            self.forward($$.upper()); // Transforms input to uppercase
        }
    };
    
    public type Foo = unit {
        on %init { self.connect_filter(new Filter); }
        x: bytes &size=5 { print self.x; }
    };
  10. Perform random access within a unit

    main

    You can move the current input position backwards to any location between the start of the current unit and the current position using built-in unit methods.

    Note: Using random access requires Spicy to buffer the unit's data, which may incur a performance penalty.

    Key Methods

    • unit::input(): Returns a stream iterator pointing to the current input position.
    • unit::set_input(iterator): Sets the current input position to the location of the specified iterator. The new position must be between the start of the unit and the current position.
    • unit::offset(): Returns the numerical offset of the current position relative to the first byte of the unit.
    • unit::position(): Returns an iterator to the current input position in the stream.
    module Test;
    
    public type Foo = unit {
        on %init() { self.start = self.input(); }
    
        a: A { self.set_input(self.start); } // Reverts to the start of the unit
        b: B;
    
        on %done() { print self; }
    
        var start: iterator<stream>;
    };
    
    type A = unit { x: uint32; };
    type B = unit { y: bytes &size=4; };
  11. Understand the scope of Spicy global variables

    main

    In Spicy, global variables are truly global. Changes made to a global variable are visible to all Spicy code across the entire execution. Their values are not tied to specific connections or dynamic state. If a variable is public, it can even be accessed from unrelated modules.

    Best Practice: Because globals affect everything, they are rarely the correct tool for parser state. If you need to maintain state that is specific to the current connection being parsed, use the unit_context mechanism instead.

  12. Pass parameters to Unit types

    main

    Unit types can be parameterized upon instantiation. This is useful for passing configuration or context down to subunits.

    Parameter Passing Rules

    • Read-only by default: Like functions, parameters are read-only unless specified otherwise.
    • Passing Units (inout): If a parameter is a unit type, you can declare it as inout to allow the subunit to modify the parent unit's state.
    • Passing Other Types (References): To allow a subunit to modify non-unit types (like a string), you must pass the parameter as a reference using the & suffix (e.g., s: string&).

    Common Pattern: Passing self

    A common idiom is passing the parent unit's self into a subunit so the subunit can access or modify the parent's state.

    type Bar = unit(foo: Foo) {
        # Access to 'foo' state
    }
    
    type Foo = unit {
        b: Bar(self);
    }
    type Bar = unit(msg: string, mult: int8) {
        x: int8 &convert=($$ * mult);
        on %done { print "%s: %d" % (msg, self.x); }
    };
    
    public type Foo = unit {
        y: Bar("My multiplied integer", 5);
    };