PcapFileEx.Flows.Decoder behaviour (pcap_file_ex v0.5.5)

View Source

Behaviour and types for custom flow decoders.

Decoders transform raw binary payloads into structured data based on matching criteria (port, content-type, etc.).

Binary-Only Decoding

IMPORTANT: Custom decoders only apply to binary content. Built-in JSON/text decoding and multipart parsing run first. Custom decoders are invoked only when the decoded value would be {:binary, payload}.

If JSON parsing fails (invalid JSON), the result is {:binary, payload} and IS eligible for custom decoders.

Decoder Types

Decoders can be either:

  • Arity-1 functions: Receive only the payload, return any term or {:error, reason}
  • Arity-2 functions: Receive context and payload, return decode_result()
  • Modules: Implement this behaviour with decode/2 callback

Returning :skip vs error

  • :skip - Decoder declines to handle; continue to next matching decoder
  • {:error, reason} - Decoder tried and failed; terminal, no further decoders tried

Example Usage

# Simple arity-1 decoder
decoder = %{
  protocol: :udp,
  match: %{port: 5005},
  decoder: &MyTelemetry.decode/1
}

# Context-aware arity-2 decoder
decoder = %{
  protocol: :http1,
  match: %{scope: :multipart_part, content_type: "application/vnd.3gpp.ngap"},
  decoder: fn %{content_id: id}, payload ->
    {:ok, {:ngap, id, NGAP.parse(payload)}}
  end
}

{:ok, result} = PcapFileEx.Flows.analyze("capture.pcap",
  decoders: [decoder]
)

Summary

Types

Result of decoding a payload.

Decoder function.

Decoder specification for registration.

Field descriptor for display filter support.

Context passed to decoders with all available metadata.

Matcher for determining if a decoder applies.

Callbacks

Decode a binary payload given the match context.

Optional: Return field descriptors for display filter support.

Types

decode_result()

@type decode_result() :: {:ok, term()} | {:error, term()} | :skip

Result of decoding a payload.

  • {:ok, term} - Successfully decoded, wrapped as {:custom, term} in result
  • {:error, term} - Decoding failed (terminal), stored as {:decode_error, reason}
  • :skip - This decoder declines; continue to next matching decoder

decoder_fn()

@type decoder_fn() ::
  (binary() -> {:error, term()} | term())
  | (match_context(), binary() -> decode_result())

Decoder function.

Arity-1

Receives only payload. Returns:

  • {:error, reason} - Stored as {:decode_error, reason} (terminal)
  • Any other term - Wrapped as {:custom, term}

Note: Arity-1 cannot return :skip; use arity-2 if you need to decline.

Arity-2

Receives context and payload. Must return decode_result().

decoder_spec()

@type decoder_spec() :: %{
  protocol: :udp | :http1 | :http2,
  match: matcher(),
  decoder: decoder_fn() | module()
}

Decoder specification for registration.

  • :protocol - Required. Filter by protocol (:udp, :http1, :http2)
  • :match - Required. Matcher for determining if decoder applies
  • :decoder - Required. Decoder function or module implementing this behaviour

Module Invocation

When decoder is a module, module.decode(ctx, payload) is invoked (arity-2). Modules MUST implement the Decoder behaviour with decode/2. Arity-1 module functions are not supported; use &Module.decode/1 explicitly.

field_descriptor()

@type field_descriptor() :: %{
  id: String.t(),
  type: :string | :integer | :boolean | :binary,
  extractor: (term() -> term())
}

Field descriptor for display filter support.

Allows extracted fields to be used in display filters.

match_context()

@type match_context() :: %{
  :protocol => :udp | :http1 | :http2,
  :direction => :request | :response | :datagram,
  optional(:port) => non_neg_integer(),
  optional(:from) => PcapFileEx.Endpoint.t(),
  optional(:to) => PcapFileEx.Endpoint.t(),
  optional(:scope) => :body | :multipart_part,
  optional(:content_type) => String.t(),
  optional(:content_id) => String.t() | nil,
  optional(:headers) => %{required(String.t()) => String.t()},
  optional(:method) => String.t(),
  optional(:path) => String.t(),
  optional(:status) => non_neg_integer()
}

Context passed to decoders with all available metadata.

Common Fields

  • :protocol - Protocol identifier (:udp, :http1, :http2)
  • :direction - Direction (:request, :response, :datagram)

UDP Fields (direction: :datagram)

  • :port - Destination port
  • :from - Source endpoint
  • :to - Destination endpoint

HTTP Fields (direction: :request | :response)

  • :scope - :body or :multipart_part
  • :content_type - Content-Type header (normalized, lowercase)
  • :headers - Regular headers as %{"lowercase-key" => "value"} (excludes HTTP/2 pseudo-headers; use :method, :path, :status fields instead)
  • :method - HTTP method
  • :path - Request path
  • :status - Response status (if response)
  • :content_id - Part's Content-ID (multipart only)

matcher()

@type matcher() ::
  (match_context() -> boolean())
  | %{
      optional(:scope) => :body | :multipart_part,
      optional(:port) => non_neg_integer() | Range.t() | [non_neg_integer()],
      optional(:content_type) => String.t() | Regex.t() | [String.t()],
      optional(:content_id) => String.t() | Regex.t(),
      optional(:method) => String.t() | [String.t()],
      optional(:path) => String.t() | Regex.t()
    }

Matcher for determining if a decoder applies.

Can be either:

  • A function receiving match_context() and returning boolean
  • A map with optional criteria (all specified must match)

Note: protocol is NOT in matcher; use decoder_spec.protocol instead. If you add :protocol to a match map, it will be ignored.

Callbacks

decode(match_context, binary)

@callback decode(match_context(), binary()) :: decode_result()

Decode a binary payload given the match context.

Return {:ok, decoded} for successful decoding (wrapped as {:custom, decoded}), {:error, reason} for failures (terminal, stored as {:decode_error, reason}), or :skip to skip this decoder (try next matching decoder).

fields()

(optional)
@callback fields() :: [field_descriptor()]

Optional: Return field descriptors for display filter support.