Skip to content

jsmn-go/config.go

:material-github: View on GitHub · 192 lines · live source, included at build time

// Package jsmngo provides a fast JSON tokenizer with parallel processing capabilities.
package jsmngo

import (
    "context"
    "errors"
)

// Common errors.
var (
    // ErrInputTooLarge is returned when input exceeds MaxInputSize.
    ErrInputTooLarge = errors.New("input size exceeds maximum allowed")

    // ErrTooManyTokens is returned when token count exceeds MaxTokens.
    ErrTooManyTokens = errors.New("token count exceeds maximum allowed")

    // ErrEmptyInput is returned when input is empty.
    ErrEmptyInput = errors.New("empty input")
)

// Config holds parsing configuration and limits.
type Config struct {
    // MaxInputSize limits the maximum JSON input size in bytes.
    // Default: 100MB. Set to 0 for unlimited (not recommended).
    MaxInputSize int

    // MaxTokens limits the maximum number of tokens that can be parsed.
    // Default: 1,000,000. Set to 0 for unlimited (not recommended).
    MaxTokens int

    // InitialTokenCapacity is the initial capacity for the token slice.
    // Default: inputSize / 4. The slice will grow automatically if needed.
    InitialTokenCapacity int

    // ParallelThreshold is the minimum input size (in bytes) to enable parallel parsing.
    // Default: 4KB. Parallel parsing has overhead, so small inputs are faster serially.
    ParallelThreshold int
}

// DefaultConfig returns the default configuration with sensible limits.
func DefaultConfig() *Config {
    return &Config{
        MaxInputSize:         100 * 1024 * 1024, // 100MB
        MaxTokens:            1_000_000,         // 1 million tokens
        InitialTokenCapacity: 0,                 // Auto-calculated
        ParallelThreshold:    4 * 1024,          // 4KB
    }
}

// StrictConfig returns a stricter configuration suitable for untrusted input.
func StrictConfig() *Config {
    return &Config{
        MaxInputSize:         10 * 1024 * 1024, // 10MB
        MaxTokens:            100_000,          // 100k tokens
        InitialTokenCapacity: 0,                // Auto-calculated
        ParallelThreshold:    4 * 1024,         // 4KB
    }
}

// UnlimitedConfig returns a configuration with no limits (use with caution).
func UnlimitedConfig() *Config {
    return &Config{
        MaxInputSize:         0,        // Unlimited
        MaxTokens:            0,        // Unlimited
        InitialTokenCapacity: 0,        // Auto-calculated
        ParallelThreshold:    4 * 1024, // 4KB
    }
}

// Validate checks if the configuration is valid.
func (c *Config) Validate() error {
    if c.MaxInputSize < 0 {
        return errors.New("MaxInputSize cannot be negative")
    }
    if c.MaxTokens < 0 {
        return errors.New("MaxTokens cannot be negative")
    }
    if c.InitialTokenCapacity < 0 {
        return errors.New("InitialTokenCapacity cannot be negative")
    }
    if c.ParallelThreshold < 0 {
        return errors.New("ParallelThreshold cannot be negative")
    }
    return nil
}

// validateInput checks if the input meets the configuration constraints.
func (c *Config) validateInput(data []byte) error {
    if len(data) == 0 {
        return ErrEmptyInput
    }

    if c.MaxInputSize > 0 && len(data) > c.MaxInputSize {
        return ErrInputTooLarge
    }

    return nil
}

// getInitialCapacity returns the initial token capacity based on input size.
func (c *Config) getInitialCapacity(inputSize int) int {
    if c.InitialTokenCapacity > 0 {
        return c.InitialTokenCapacity
    }

    // Heuristic: JSON typically has ~1 token per 4 bytes
    capacity := inputSize / 4
    if capacity < 10 {
        capacity = 10
    }
    if c.MaxTokens > 0 && capacity > c.MaxTokens {
        capacity = c.MaxTokens
    }
    return capacity
}

// shouldUseParallel determines if parallel parsing should be used.
func (c *Config) shouldUseParallel(inputSize int) bool {
    return inputSize >= c.ParallelThreshold
}

// ParserWithConfig creates a new parser with the given configuration.
type ParserWithConfig struct {
    *Parser
    config *Config
}

// NewParserWithConfig creates a new parser with the given configuration.
func NewParserWithConfig(config *Config) (*ParserWithConfig, error) {
    if err := config.Validate(); err != nil {
        return nil, err
    }

    return &ParserWithConfig{
        Parser: &Parser{
            tokens: make([]Token, 0, 100), // Will grow as needed
        },
        config: config,
    }, nil
}

// Parse tokenizes the JSON input with validation.
func (p *ParserWithConfig) Parse(json []byte) (int, error) {
    if err := p.config.validateInput(json); err != nil {
        return 0, err
    }

    return p.Parser.Parse(json)
}

// ParseWithConfig parses JSON with the given configuration.
func ParseWithConfig(ctx context.Context, data []byte, config *Config) ([]Token, error) {
    if config == nil {
        config = DefaultConfig()
    }

    if err := config.Validate(); err != nil {
        return nil, err
    }

    if err := config.validateInput(data); err != nil {
        return nil, err
    }

    // Check context before starting
    select {
    case <-ctx.Done():
        return nil, ctx.Err()
    default:
    }

    // Choose parsing strategy
    if config.shouldUseParallel(len(data)) {
        return parseParallelWithConfig(ctx, data, config)
    }

    // Serial parsing
    capacity := config.getInitialCapacity(len(data))
    p := NewParser(capacity)

    // Check token limit during parsing
    count, err := p.Parse(data)
    if err != nil {
        return nil, err
    }

    if config.MaxTokens > 0 && count > config.MaxTokens {
        return nil, ErrTooManyTokens
    }

    return p.Tokens(), nil
}