Skip to content

funvibe/funbit

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Funbit - Erlang/OTP Bit Syntax for Go

Funbit is a Go library providing Erlang/OTP bit syntax compatibility for bitstring and binary data manipulation. It includes interfaces for constructing and pattern matching bitstrings.

Features

  • Erlang Compatibility: 1:1 mapping of Erlang bit syntax to a Go API.
  • Bit-Level Operations: Operates on a bit stream, not restricted to byte-aligned segments.
  • Data Types: Integer, float (16/32/64-bit), binary, bitstring, UTF-8/16/32.
  • Dynamic Sizing: Supports variables and arithmetic expressions in size fields (e.g., total-6).
  • Unit Multipliers: Allows size multiplication with unit:N (e.g., 32/float-unit:2 for a 64-bit double).
  • Endianness Support: Big, little, and native byte ordering.
  • Compound Specifiers: Supports combinations of specifiers (e.g., 32/big-unsigned-integer-unit:8).
  • String Literals in Patterns: Allows constant string matching for protocol validation (e.g., "IHDR":4/binary).
  • Builder API Pattern: Operations are chained, with a single error check.
  • Type Semantics: Differentiates between integer and binary representations.
  • Protocol Parsing: Designed for parsing protocols such as IPv4, TCP, and PNG.
  • Performance: Optimized for construction and pattern matching.

Core Semantics

Binary vs. Integer Size

// CRITICAL DIFFERENCE:
funbit.Integer(m, &val, funbit.WithSize(32))  // 32 BITS
funbit.Binary(m, &data, funbit.WithSize(32))  // 32 BYTES = 256 BITS!

// For binary segments: WithSize(N) means N UNITS (default: bytes)
funbit.Binary(m, &data, funbit.WithSize(4))   // 4 bytes
funbit.Binary(m, &data, funbit.WithSize(32))  // 32 bytes

Unit Multipliers for Dynamic Sizing

// WITHOUT WithUnit(1): size*8 interpreted as BYTES, multiplied by 8!
funbit.Binary(m, &data, funbit.WithDynamicSizeExpression("size*8"))
// size=5 → 5*8=40, but binary interprets as 40*8=320 bits!

// WITH WithUnit(1): size*8 interpreted as exact BITS
funbit.Binary(m, &data, funbit.WithDynamicSizeExpression("size*8"), funbit.WithUnit(1))
// size=5 → 5*8=40 bits exactly

UTF Extraction Semantics

// Erlang UTF supports BOTH approaches:

// 1. String encoding (entire strings)
funbit.AddUTF8(builder, "Hello")  // Encodes full string

// 2. Code point extraction (individual characters)
funbit.UTF8(matcher, &codepoint)  // Single code point

// 3. Binary extraction (for full strings)
var bytes []byte
funbit.RestBinary(matcher, &bytes)
text := string(bytes)  // Full string

Quick Start

Builder API Pattern

Funbit uses a builder pattern for deferred error handling:

// Chain operations, check error once
builder := funbit.NewBuilder()
funbit.AddInteger(builder, 42, funbit.WithSize(8))
funbit.AddUTF8Codepoint(builder, 0x1F680) // rocket emoji
funbit.AddFloat(builder, 3.14, funbit.WithSize(32))

bitstring, err := funbit.Build(builder) // Error checked once
if err != nil {
    return err
}

// Traditional approach:
// if err := AddInteger(...); err != nil { return err }
// if err := AddUTF8Codepoint(...); err != nil { return err }
// if err := AddFloat(...); err != nil { return err }

Characteristics:

  • Chainable: Add* functions do not return errors.
  • Efficient: The first error halts processing; subsequent calls are ignored.
  • Clean: A single error check is performed at Build() time.
  • Consistent: The pattern is used throughout the API.

Installation

go get github.com/funvibe/funbit

Basic Construction and Matching

package main

import (
    "fmt"
    "github.com/funvibe/funbit/pkg/funbit"
)

func main() {
    // Construction: <<42:8, "hello":5/binary>>
    builder := funbit.NewBuilder()
    funbit.AddInteger(builder, 42, funbit.WithSize(8))
    funbit.AddBinary(builder, []byte("hello"))
    
    bitstring, _ := funbit.Build(builder)
    
    // Pattern Matching: <<value:8, text:5/binary>>
    matcher := funbit.NewMatcher()
    var value int
    var text []byte
    
    funbit.Integer(matcher, &value, funbit.WithSize(8))
    // CRITICAL: Binary size in BYTES! WithSize(5) = 5 bytes = 40 bits
    funbit.Binary(matcher, &text, funbit.WithSize(5)) // 5 bytes
    
    results, err := funbit.Match(matcher, bitstring)
    if err == nil && len(results) > 0 {
        fmt.Printf("Value: %d, Text: %s\n", value, string(text))
        // Output: Value: 42, Text: hello
    }
}

Core Concepts

Construction vs Pattern Matching

Construction builds bitstrings from values:

builder := funbit.NewBuilder()
funbit.AddInteger(builder, 1000, funbit.WithSize(16))
funbit.AddFloat(builder, 3.14, funbit.WithSize(32))
bitstring, _ := funbit.Build(builder)

Pattern Matching extracts values from bitstrings:

matcher := funbit.NewMatcher()
var num int
var pi float32
funbit.Integer(matcher, &num, funbit.WithSize(16))
funbit.Float(matcher, &pi, funbit.WithSize(32))
results, _ := funbit.Match(matcher, bitstring)

Type Semantics

Funbit follows Erlang semantics where the default type is integer:

// Construction
funbit.AddInteger(builder, 42, funbit.WithSize(8))     // Integer: displays as 42
funbit.AddBinary(builder, []byte("A"), funbit.WithSize(1))  // Binary: displays as 'A' (1 byte)

// Pattern Matching
funbit.Integer(matcher, &num, funbit.WithSize(8))      // Extract as number: 42
funbit.Binary(matcher, &char, funbit.WithSize(1))      // Extract as character: 'A' (1 byte)

The interpretation of a bit sequence depends on the type specifier.

Advanced Features

Non-byte-aligned Bitstrings

The library supports bit-level operations that are not restricted to byte boundaries:

// Build 7-bit value (not byte-aligned)
builder := funbit.NewBuilder()
funbit.AddInteger(builder, 0b101, funbit.WithSize(3))   // 3 bits
funbit.AddInteger(builder, 0b1111, funbit.WithSize(4))  // 4 bits
// Total: 7 bits (not a full byte)

bitstring, _ := funbit.Build(builder)

// Pattern matching bit-level
matcher := funbit.NewMatcher()
var part1, part2 int

funbit.Integer(matcher, &part1, funbit.WithSize(3))  // Extract 3 bits
funbit.Integer(matcher, &part2, funbit.WithSize(4))  // Extract 4 bits

results, _ := funbit.Match(matcher, bitstring)
// part1 = 5 (0b101), part2 = 15 (0b1111)

UTF Codepoint API

The API provides functions for handling single codepoints:

// Encoding single codepoints
builder := funbit.NewBuilder()
funbit.AddUTF8Codepoint(builder, 0x1F680)  // emoji
funbit.AddUTF16Codepoint(builder, 0x1F31F, funbit.WithEndianness("big"))
funbit.AddUTF32Codepoint(builder, 65)  // 'A'

bitstring, err := funbit.Build(builder)
if err != nil {
    // Handles invalid codepoints (e.g., surrogate pairs)
    fmt.Printf("Error: %v\n", err)
}

// Extract as INTEGER (Erlang spec!)
matcher := funbit.NewMatcher()
var codepoint int
funbit.UTF8(matcher, &codepoint)  // Returns 0x1F680 (integer)

results, _ := funbit.Match(matcher, bitstring)
// codepoint = 128640 (0x1F680)

Signed vs Unsigned Integers

// Signed interpretation
builder := funbit.NewBuilder()
funbit.AddInteger(builder, -50, funbit.WithSize(8), funbit.WithSigned(true))

// Unsigned interpretation (default)
funbit.AddInteger(builder, 200, funbit.WithSize(8), funbit.WithSigned(false))

bitstring, _ := funbit.Build(builder)

// Pattern matching with signedness
matcher := funbit.NewMatcher()
var signedVal, unsignedVal int

funbit.Integer(matcher, &signedVal, funbit.WithSize(8), funbit.WithSigned(true))
funbit.Integer(matcher, &unsignedVal, funbit.WithSize(8), funbit.WithSigned(false))

results, _ := funbit.Match(matcher, bitstring)
// signedVal = -50, unsignedVal = 200

Dynamic Sizing with Expressions

// Create matcher and register variables
matcher := funbit.NewMatcher()
var headerSize int = 32
var total int = 96
funbit.RegisterVariable(matcher, "headerSize", &headerSize)
funbit.RegisterVariable(matcher, "total", &total)

// Use in expressions
funbit.AddBinary(builder, data, funbit.WithDynamicSizeExpression("total-headerSize"))

String Literals in Patterns

String literals can be used in patterns for protocol field validation:

// Validate PNG header: expect exactly "IHDR"
matcher := funbit.NewMatcher()
var length int
expectedType := "IHDR"

funbit.Integer(matcher, &length, funbit.WithSize(32))
funbit.Binary(matcher, &expectedType, funbit.WithSize(4)) // Must match "IHDR" (4 bytes)

Endianness Control

// Big-endian (default)
funbit.AddInteger(builder, 0x1234, funbit.WithSize(16), funbit.WithEndianness("big"))

// Little-endian  
funbit.AddInteger(builder, 0x1234, funbit.WithSize(16), funbit.WithEndianness("little"))

Unit Multipliers

Unit multipliers allow size multiplication for precise control:

// 32-bit float with unit:2 = 64-bit IEEE 754 double precision
funbit.AddFloat(builder, 3.14159265359, funbit.WithSize(32), funbit.WithUnit(2))

// 8-bit size with unit:16 = 128 effective bits
funbit.AddInteger(builder, 8, funbit.WithSize(8), funbit.WithUnit(16))

// Pattern matching with unit multipliers
funbit.Float(matcher, &doubleValue, funbit.WithSize(32), funbit.WithUnit(2))

Compound Specifiers

Combine multiple specifiers for complex data layouts:

// 32/big-unsigned-integer-unit:8
funbit.AddInteger(builder, 0xDEADBEEF,
    funbit.WithSize(32),
    funbit.WithEndianness("big"),
    funbit.WithUnit(8))

// 16/little-unsigned-integer
funbit.AddInteger(builder, 0x1234,
    funbit.WithSize(16),
    funbit.WithEndianness("little"))

Bit-Level Precision

// Individual flag bits
funbit.AddInteger(builder, 1, funbit.WithSize(1))  // Single bit
funbit.AddInteger(builder, 0, funbit.WithSize(1))  // Another bit
funbit.AddInteger(builder, 3, funbit.WithSize(2))  // 2-bit value

Examples

TCP Header Parsing

builder := funbit.NewBuilder()
funbit.AddInteger(builder, 0x1234, funbit.WithSize(16))    // Source port
funbit.AddInteger(builder, 0x5678, funbit.WithSize(16))    // Dest port
funbit.AddInteger(builder, 1, funbit.WithSize(1))          // URG flag
funbit.AddInteger(builder, 0, funbit.WithSize(1))          // ACK flag
// ... more flags
funbit.AddBinary(builder, []byte("payload"))

// Pattern matching extracts all fields with proper types

PNG Header Validation

// Pattern: <<length:32, "IHDR":4/binary, width:32, height:32>>
matcher := funbit.NewMatcher()
var length, width, height int
expectedChunk := "IHDR"

funbit.Integer(matcher, &length, funbit.WithSize(32))
funbit.Binary(matcher, &expectedChunk, funbit.WithSize(4))  // Validates "IHDR" (4 bytes)
funbit.Integer(matcher, &width, funbit.WithSize(32))
funbit.Integer(matcher, &height, funbit.WithSize(32))

Usage Guidelines

1. Understand Type Semantics

  • Use funbit.Integer() for numeric values (displays as numbers)
  • Use funbit.Binary() for text/character data (displays as characters)
  • Default type is integer, not binary

2. Handle Dynamic Sizes Properly

// Register all variables before use
funbit.RegisterVariable("size", 32)
funbit.RegisterVariable("total", 128)

// Use expressions for complex sizing
funbit.WithDynamicSizeExpression("total-size-8")

3. Validate Protocol Constants

// Use string literals to validate protocol headers
expectedType := "IHDR"
funbit.Binary(matcher, &expectedType, funbit.WithSize(4))  // 4 bytes = "IHDR"

4. Use Unit Multipliers for Precision

// For IEEE 754 double precision floats
funbit.AddFloat(builder, value, funbit.WithSize(32), funbit.WithUnit(2))  // 64-bit

// For size fields that represent bit counts
funbit.AddInteger(builder, 8, funbit.WithSize(8), funbit.WithUnit(16))    // 8*16 bits

5. Combine Specifiers for Complex Layouts

// Full compound specifier
funbit.AddInteger(builder, value,
    funbit.WithSize(32),
    funbit.WithEndianness("big"),
    funbit.WithUnit(8))

6. Endianness Format

// Use short forms
funbit.WithEndianness("big")     // Supported
funbit.WithEndianness("little")  // Supported  
funbit.WithEndianness("native")  // Supported

7. Pattern Size Validation

Funbit automatically validates pattern sizes unless:

  • Pattern contains rest patterns (funbit.RestBinary())
  • Pattern contains dynamic sizes
  • Pattern contains string literals (for validation)
  • Pattern contains unit multipliers (calculated dynamically)

Erlang to Funbit Syntax Reference

Erlang Syntax Funbit Equivalent Description
<<42:8>> funbit.AddInteger(b, 42, funbit.WithSize(8)) 8-bit integer
<<42:8/big>> funbit.AddInteger(b, 42, funbit.WithSize(8), funbit.WithEndianness("big")) Big-endian integer
<<3.14:32/float>> funbit.AddFloat(b, 3.14, funbit.WithSize(32)) 32-bit float
<<"hello world"/binary>> funbit.AddBinary(b, []byte("hello world")) Binary data (full)
<<"hello world":5/binary>> funbit.AddBinary(b, []byte("hello world"), funbit.WithSize(5)) Binary data (truncated to 5 bytes: "hello")
<<Size:8, Data:Size/binary>> funbit.Integer(m, &size, funbit.WithSize(8))
funbit.Binary(m, &data, funbit.WithDynamicSizeExpression("size"))
Dynamic sizing
<<Value:16/unit:8>> funbit.AddInteger(b, value, funbit.WithSize(16), funbit.WithUnit(8)) Unit multiplier
<<Codepoint/utf8>> funbit.AddUTF8Codepoint(b, codepoint) UTF-8 codepoint
<<"text"/utf8>> funbit.AddUTF8(b, "text") UTF-8 string
<<-50:8/signed>> funbit.AddInteger(b, -50, funbit.WithSize(8), funbit.WithSigned(true)) Signed integer
<<Rest/binary>> funbit.RestBinary(m, &rest) Rest pattern
<<1:3, 15:4>> funbit.AddInteger(b, 1, funbit.WithSize(3))
funbit.AddInteger(b, 15, funbit.WithSize(4))
Non-byte-aligned

API Differences from Erlang:

  • Funbit uses an explicit builder pattern vs Erlang's literal syntax.
  • Funbit requires variable registration for dynamic sizes.
  • Funbit supports method chaining and error accumulation.
  • Funbit provides stronger type safety with Go's type system.

Performance

  • Construction: O(n) where n is number of segments
  • Pattern Matching: O(n) where n is number of segments
  • Memory: Bitstrings are immutable and memory-efficient
  • Threading: Thread-safe for concurrent reads; builders are not thread-safe.

Integration with Runtimes

When integrating with language runtimes (like Lua, JavaScript):

// For integers extracted from patterns
if intValue >= 0 && intValue <= 255 {
    // In mixed binary context, might display as character
    // In pure integer context, display as number
}

// For binary data
string(binaryData) // Always display as characters

Additional Examples

Refer to funbit/examples/public_api_example.go for examples covering:

  • Basic construction and matching
  • Data types and specifiers
  • Endianness support
  • Dynamic sizing
  • String literals in patterns
  • Complex protocol parsing
  • Unit multipliers
  • Compound specifiers
  • Advanced float handling
  • Type semantics
  • Integration patterns

Contributing

Contributions are accepted.

License

MIT License. See the LICENSE file for details.