// Unless explicitly stated otherwise all files in this repository are licensed
// under the Apache License Version 2.0.
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2016 Datadog, Inc.

//go:build appsec && cgo && !windows && (amd64 || arm64) && (linux || darwin)
// +build appsec
// +build cgo
// +build !windows
// +build amd64 arm64
// +build linux darwin

package waf

// #include <stdlib.h>
// #include <string.h>
// #include "ddwaf.h"
// // Forward declaration of the Go function go_ddwaf_object_free which is a Go
// // function defined and exported into C by CGO in this file.
// // This allows to reference this symbol with the C wrapper and pass its
// // pointer to ddwaf_context_init.
// void go_ddwaf_object_free(ddwaf_object*);
// #cgo CFLAGS: -I${SRCDIR}/include
// #cgo linux,amd64 LDFLAGS: -L${SRCDIR}/lib/linux-amd64 -lddwaf -lm -ldl -Wl,-rpath=/lib64:/usr/lib64:/usr/local/lib64:/lib:/usr/lib:/usr/local/lib
// #cgo linux,arm64 LDFLAGS: -L${SRCDIR}/lib/linux-arm64 -lddwaf -lm -ldl -Wl,-rpath=/lib64:/usr/lib64:/usr/local/lib64:/lib:/usr/lib:/usr/local/lib
// #cgo darwin,amd64 LDFLAGS: -L${SRCDIR}/lib/darwin-amd64 -lddwaf -lc++
// #cgo darwin,arm64 LDFLAGS: -L${SRCDIR}/lib/darwin-arm64 -lddwaf -lc++
import "C"

import (
	"encoding/json"
	"errors"
	"fmt"
	"math"
	"reflect"
	"strconv"
	"strings"
	"sync"
	"sync/atomic"
	"time"
	"unicode"
	"unsafe"

	rc "github.com/DataDog/datadog-agent/pkg/remoteconfig/state"

	// Do not remove the following imports which allow supporting package
	// vendoring by properly copying all the files needed by CGO: the libddwaf
	// header file and the static libraries.
	_ "gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/waf/include"
	_ "gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/waf/lib/darwin-amd64"
	_ "gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/waf/lib/darwin-arm64"
	_ "gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/waf/lib/linux-amd64"
	_ "gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/waf/lib/linux-arm64"
)

var wafVersion = getWAFVersion()

// Health allows knowing if the WAF can be used. It returns a nil error when the WAF library is healthy.
// Otherwise, it returns an error describing the issue.
func Health() error {
	return nil
}

// Version returns the current version of the WAF
func Version() string {
	return wafVersion
}

// Handle represents an instance of the WAF for a given ruleset.
type Handle struct {
	// Instance of the WAF
	handle C.ddwaf_handle

	// Lock-less reference counter avoiding blocking calls to the Close() method
	// while WAF contexts are still using the WAF handle. Instead, we let the
	// release actually happen only when the reference counter reaches 0.
	// This can happen either from a request handler calling its WAF context's
	// Close() method, or either from the appsec instance calling the WAF
	// handle's Close() method when creating a new WAF handle with new rules.
	// Note that this means several instances of the WAF can exist at the same
	// time with their own set of rules. This choice was done to be able to
	// efficiently update the security rules concurrently, without having to
	// block the request handlers for the time of the security rules update.
	refCounter atomicRefCounter

	// RWMutex protecting the R/W accesses to the internal rules data (stored
	// in the handle).
	mu sync.RWMutex

	// encoder of Go values into ddwaf objects.
	encoder encoder
	// addresses the WAF rule is expecting.
	addresses []string
	// rulesetInfo holds information about rules initialization
	rulesetInfo RulesetInfo
}

// NewHandle creates a new instance of the WAF with the given JSON rule and key/value regexps for obfuscation.
func NewHandle(jsonRule []byte, keyRegex, valueRegex string) (*Handle, error) {
	var rule interface{}
	if err := json.Unmarshal(jsonRule, &rule); err != nil {
		return nil, fmt.Errorf("could not parse the WAF rule: %v", err)
	}

	// Create a temporary unlimited encoder for the rules
	wafRule, err := newMaxEncoder().encode(rule)
	if err != nil {
		return nil, fmt.Errorf("could not encode the JSON WAF rule into a WAF object: %v", err)
	}
	defer freeWO(wafRule)

	// Run-time encoder limiting the size of the encoded values
	encoder := encoder{
		maxDepth:        C.DDWAF_MAX_CONTAINER_DEPTH,
		maxStringLength: C.DDWAF_MAX_STRING_LENGTH,
		maxArrayLength:  C.DDWAF_MAX_CONTAINER_SIZE,
		maxMapLength:    C.DDWAF_MAX_CONTAINER_SIZE,
	}
	var wafRInfo C.ddwaf_ruleset_info
	keyRegexC, _, err := cstring(keyRegex, encoder.maxStringLength)
	if err != nil {
		return nil, fmt.Errorf("could not convert the obfuscator key regexp string to a C string: %v", err)
	}
	defer cFree(unsafe.Pointer(keyRegexC))
	valueRegexC, _, err := cstring(valueRegex, encoder.maxStringLength)
	if err != nil {
		return nil, fmt.Errorf("could not convert the obfuscator value regexp to a C string: %v", err)
	}
	defer cFree(unsafe.Pointer(valueRegexC))
	wafCfg := C.ddwaf_config{
		limits: struct{ max_container_size, max_container_depth, max_string_length C.uint32_t }{
			max_container_size:  C.uint32_t(encoder.maxArrayLength),
			max_container_depth: C.uint32_t(encoder.maxMapLength),
			max_string_length:   C.uint32_t(encoder.maxStringLength),
		},
		obfuscator: struct{ key_regex, value_regex *C.char }{
			key_regex:   keyRegexC,
			value_regex: valueRegexC,
		},
		free_fn: C.ddwaf_object_free_fn(C.go_ddwaf_object_free),
	}
	defer C.ddwaf_ruleset_info_free(&wafRInfo)
	handle := C.ddwaf_init(wafRule.ctype(), &wafCfg, &wafRInfo)
	if handle == nil {
		return nil, errors.New("could not instantiate the waf rule")
	}
	incNbLiveCObjects()

	// Decode the ruleset information returned by the WAF
	errors, err := decodeErrors((*wafObject)(&wafRInfo.errors))
	if err != nil {
		C.ddwaf_destroy(handle)
		decNbLiveCObjects()
		return nil, err
	}
	rInfo := RulesetInfo{
		Failed:  uint16(wafRInfo.failed),
		Loaded:  uint16(wafRInfo.loaded),
		Version: C.GoString(wafRInfo.version),
		Errors:  errors,
	}
	// Get the addresses the rule listens to
	addresses, err := ruleAddresses(handle)
	if err != nil {
		C.ddwaf_destroy(handle)
		decNbLiveCObjects()
		return nil, err
	}
	return &Handle{
		handle:      handle,
		refCounter:  1,
		encoder:     encoder,
		addresses:   addresses,
		rulesetInfo: rInfo,
	}, nil
}

// Increment the ref counter and return true if the handle can be used, false
// otherwise.
func (h *Handle) incrementReferences() bool {
	return h.refCounter.increment() != 0
}

// Decrement the ref counter and release the memory when 0 is reached.
func (h *Handle) decrementReferences() {
	if h.refCounter.decrement() == 0 {
		h.release()
	}
}

// Actual memory release of the WAF handle.
func (h *Handle) release() {
	if h.handle == nil {
		return // already released - only happens if Close() is called more than once
	}
	C.ddwaf_destroy(h.handle)
	decNbLiveCObjects()
	h.handle = nil
}

func ruleAddresses(handle C.ddwaf_handle) ([]string, error) {
	var nbAddresses C.uint32_t
	caddresses := C.ddwaf_required_addresses(handle, &nbAddresses)
	if nbAddresses == 0 {
		return nil, ErrEmptyRuleAddresses
	}
	addresses := make([]string, int(nbAddresses))
	for i := 0; i < len(addresses); i++ {
		addresses[i] = C.GoString(cindexCharPtrArray(caddresses, i))
	}
	return addresses, nil
}

// Addresses returns the list of addresses the WAF rule is expecting.
func (h *Handle) Addresses() []string {
	return h.addresses
}

// RulesetInfo returns the rules initialization metrics for the current WAF handle
func (h *Handle) RulesetInfo() RulesetInfo {
	return h.rulesetInfo
}

// UpdateRulesData updates the data that some rules reference to.
func (h *Handle) UpdateRulesData(data []rc.ASMDataRuleData) error {
	encoded, err := newMaxEncoder().encode(data) // FIXME: double-check with Anil that we are good with the current conversion of integers into strings here
	if err != nil {
		return fmt.Errorf("could not encode the JSON WAF rule data into a WAF object: %v", err)
	}
	defer freeWO(encoded)

	return h.updateRulesData(encoded)
}

// updateRulesData is the critical section of UpdateRulesData
func (h *Handle) updateRulesData(data *wafObject) error {
	// Note about this lock: ddwaf_update_rule_data already is thread-safe to
	// use, but we chose to lock at the goroutine-level instead in order to
	// avoid locking OS threads and therefore prevent many other goroutines from
	// executing during that OS lock. If a goroutine locks due to this handle's
	// RWMutex, another goroutine gets executed on its OS thread.
	h.mu.Lock()
	defer h.mu.Unlock()

	rc := C.ddwaf_update_rule_data(h.handle, data.ctype())
	if rc != C.DDWAF_OK {
		return fmt.Errorf("unexpected error number `%d` while updating the WAF rule data", rc)
	}
	return nil
}

// Close the WAF handle. Note that this call doesn't block until the handle gets
// released but instead let WAF contexts still use it until there's no more (eg.
// when swapping the WAF handle with a new one).
func (h *Handle) Close() {
	h.decrementReferences()
}

// Context is a WAF execution context. It allows to run the WAF incrementally
// by calling it multiple times to run its rules every time new addresses
// become available. Each request must have its own Context.
type Context struct {
	// Instance of the WAF
	handle *Handle
	// Cumulated internal WAF run time - in nanoseconds - for this context.
	totalRuntimeNs AtomicU64
	// Cumulated overall run time - in nanoseconds - for this context.
	totalOverallRuntimeNs AtomicU64
	// Cumulated timeout count for this context.
	timeoutCount AtomicU64

	context C.ddwaf_context
	// Mutex protecting the use of context which is not thread-safe.
	mu sync.Mutex
}

// NewContext creates a new WAF context and increases the number of references
// to the WAF handle.
// A nil value is returned when the WAF handle can no longer be used or the
// WAF context couldn't be created.
func NewContext(handle *Handle) *Context {
	if !handle.incrementReferences() {
		return nil // The WAF handle got released
	}
	context := C.ddwaf_context_init(handle.handle)
	if context == nil {
		handle.decrementReferences()
		return nil
	}
	incNbLiveCObjects()
	return &Context{
		handle:  handle,
		context: context,
	}
}

// Run the WAF with the given Go values and timeout.
func (c *Context) Run(values map[string]interface{}, timeout time.Duration) (matches []byte, actions []string, err error) {
	now := time.Now()
	defer func() {
		dt := time.Since(now)
		c.totalOverallRuntimeNs.Add(uint64(dt.Nanoseconds()))
	}()

	if len(values) == 0 {
		return
	}

	wafValue, err := c.handle.encoder.encode(values)
	if err != nil {
		return nil, nil, err
	}

	return c.run(wafValue, timeout)
}

// run is the critical section of Run
func (c *Context) run(data *wafObject, timeout time.Duration) (matches []byte, actions []string, err error) {
	// Exclusively lock this WAF context for the time of this run
	c.mu.Lock()
	defer c.mu.Unlock()

	// RLock the handle to safely get read access to the WAF handle and prevent concurrent changes of it
	// such as a rules-data update.
	c.handle.mu.RLock()
	defer c.handle.mu.RUnlock()

	var result C.ddwaf_result
	defer freeWAFResult(&result)

	rc := C.ddwaf_run(c.context, data.ctype(), &result, C.uint64_t(timeout/time.Microsecond))

	c.totalRuntimeNs.Add(uint64(result.total_runtime))

	matches, actions, err = goReturnValues(rc, &result)
	if err == ErrTimeout {
		c.timeoutCount.Inc()
	}

	return matches, actions, err
}

// Close the WAF context by releasing its C memory and decreasing the number of
// references to the WAF handle.
func (c *Context) Close() {
	// RUnlock the WAF RWMutex to decrease the count of WAF Contexts using it.
	defer c.handle.decrementReferences()
	C.ddwaf_context_destroy(c.context)
	decNbLiveCObjects()
}

// TotalRuntime returns the cumulated WAF runtime across various run calls within the same WAF context.
// Returned time is in nanoseconds.
func (c *Context) TotalRuntime() (overallRuntimeNs, internalRuntimeNs uint64) {
	return c.totalOverallRuntimeNs.Load(), c.totalRuntimeNs.Load()
}

// TotalTimeouts returns the cumulated amount of WAF timeouts across various run calls within the same WAF context.
func (c *Context) TotalTimeouts() uint64 {
	return c.timeoutCount.Load()
}

// Translate libddwaf return values into return values suitable to a Go program.
// Note that it is possible to have matches or actions even if err is not nil in
// case of a timeout during the WAF call.
func goReturnValues(rc C.DDWAF_RET_CODE, result *C.ddwaf_result) (matches []byte, actions []string, err error) {
	if bool(result.timeout) {
		err = ErrTimeout
	}

	switch rc {
	case C.DDWAF_OK:
		return nil, nil, err

	case C.DDWAF_MATCH:
		if result.data != nil {
			matches = C.GoBytes(unsafe.Pointer(result.data), C.int(C.strlen(result.data)))
		}
		if size := result.actions.size; size > 0 {
			cactions := result.actions.array
			actions = make([]string, size)
			for i := 0; i < int(size); i++ {
				actions[i] = C.GoString(cindexCharPtrArray(cactions, i))
			}
		}
		return matches, actions, err

	default:
		return nil, nil, goRunError(rc)
	}
}

func goRunError(rc C.DDWAF_RET_CODE) error {
	switch rc {
	case C.DDWAF_ERR_INTERNAL:
		return ErrInternal
	case C.DDWAF_ERR_INVALID_OBJECT:
		return ErrInvalidObject
	case C.DDWAF_ERR_INVALID_ARGUMENT:
		return ErrInvalidArgument
	default:
		return fmt.Errorf("unknown waf return code %d", int(rc))
	}
}

func getWAFVersion() string {
	cversion := C.ddwaf_get_version() // static mem pointer returned - no need to free it
	return C.GoString(cversion)
}

// Errors the encoder and decoder can return.
var (
	errMaxDepth         = errors.New("max depth reached")
	errUnsupportedValue = errors.New("unsupported Go value")
	errOutOfMemory      = errors.New("out of memory")
	errInvalidMapKey    = errors.New("invalid WAF object map key")
	errNilObjectPtr     = errors.New("nil WAF object pointer")
)

// isIgnoredValueError returns true if the error is only about ignored Go values
// (errUnsupportedValue or errMaxDepth).
func isIgnoredValueError(err error) bool {
	return err == errUnsupportedValue || err == errMaxDepth
}

// encoder is allows to encode a Go value to a WAF object
type encoder struct {
	// Maximum depth a WAF object can have. Every Go value further this depth is
	// ignored and not encoded into a WAF object.
	maxDepth int
	// Maximum string length. A string longer than this length is truncated to
	// this length.
	maxStringLength int
	// Maximum string length. Everything further this length is ignored.
	maxArrayLength int
	// Maximum map length. Everything further this length is ignored. Given the
	// fact Go maps are unordered, it means WAF map objects created from Go maps
	// larger than this length will have random keys.
	maxMapLength int
}

func newMaxEncoder() *encoder {
	const intSize = 32 << (^uint(0) >> 63) // copied from recent versions of math.MaxInt
	const maxInt = 1<<(intSize-1) - 1      // copied from recent versions of math.MaxInt
	return &encoder{
		maxDepth:        maxInt,
		maxStringLength: maxInt,
		maxArrayLength:  maxInt,
		maxMapLength:    maxInt,
	}
}

func (e *encoder) encode(v interface{}) (object *wafObject, err error) {
	defer func() {
		if v := recover(); v != nil {
			err = fmt.Errorf("waf panic: %v", v)
		}
		if err != nil && object != nil {
			freeWO(object)
		}
	}()
	wo := &wafObject{}
	err = e.encodeValue(reflect.ValueOf(v), wo, e.maxDepth)
	if err != nil {
		return nil, err
	}
	return wo, nil
}

func (e *encoder) encodeValue(v reflect.Value, wo *wafObject, depth int) error {
	switch kind := v.Kind(); kind {
	default:
		return errUnsupportedValue

	case reflect.Bool:
		var b string
		if v.Bool() {
			b = "true"
		} else {
			b = "false"
		}
		return e.encodeString(b, wo)

	case reflect.Ptr, reflect.Interface:
		// The traversal of pointer and interfaces is not accounted in the depth
		// as it has no impact on the WAF object depth
		return e.encodeValue(v.Elem(), wo, depth)

	case reflect.String:
		return e.encodeString(v.String(), wo)

	case reflect.Struct:
		if depth < 0 {
			return errMaxDepth
		}
		return e.encodeStruct(v, wo, depth-1)

	case reflect.Map:
		if depth < 0 {
			return errMaxDepth
		}
		return e.encodeMap(v, wo, depth-1)

	case reflect.Array, reflect.Slice:
		if depth < 0 {
			return errMaxDepth
		}
		if v.Type() == reflect.TypeOf([]byte(nil)) {
			return e.encodeString(string(v.Bytes()), wo)
		}
		return e.encodeArray(v, wo, depth-1)

	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
		return e.encodeInt64(v.Int(), wo)

	case reflect.Float32, reflect.Float64:
		return e.encodeInt64(int64(math.Round(v.Float())), wo)

	case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
		return e.encodeUint64(v.Uint(), wo)
	}
}

func (e *encoder) encodeStruct(v reflect.Value, wo *wafObject, depth int) error {
	// Consider the number of struct fields as the WAF map capacity as some
	// struct fields might not be supported and ignored.
	typ := v.Type()
	nbFields := typ.NumField()
	capacity := nbFields
	if capacity > e.maxMapLength {
		capacity = e.maxMapLength
	}
	if err := wo.setMapContainer(C.size_t(capacity)); err != nil {
		return err
	}
	// Encode struct fields
	length := 0
	for i := 0; length < capacity && i < nbFields; i++ {
		field := typ.Field(i)
		// Skip private fields
		fieldName := field.Name
		if len(fieldName) < 1 || unicode.IsLower(rune(fieldName[0])) {
			continue
		}

		// Use the json tag name as field name if present
		if tag, ok := field.Tag.Lookup("json"); ok {
			if i := strings.IndexByte(tag, byte(',')); i > 0 {
				tag = tag[:i]
			}
			if len(tag) > 0 {
				fieldName = tag
			}
		}

		mapEntry := wo.index(C.uint64_t(length))
		if err := e.encodeMapKey(reflect.ValueOf(fieldName), mapEntry); isIgnoredValueError(err) {
			continue
		}

		if err := e.encodeValue(v.Field(i), mapEntry, depth); err != nil {
			// Free the map entry in order to free the previously allocated map key
			freeWO(mapEntry)
			if isIgnoredValueError(err) {
				continue
			}
			return err
		}
		length++
	}
	// Update the map length to the actual one
	if length != capacity {
		wo.setLength(C.uint64_t(length))
	}
	return nil
}

func (e *encoder) encodeMap(v reflect.Value, wo *wafObject, depth int) error {
	// Consider the Go map value length the WAF map capacity as some map entries
	// might not be supported and ignored.
	// In this case, the actual map length will be lesser than the Go map value
	// length.
	capacity := v.Len()
	if capacity > e.maxMapLength {
		capacity = e.maxMapLength
	}
	if err := wo.setMapContainer(C.size_t(capacity)); err != nil {
		return err
	}
	// Encode map entries
	length := 0
	for iter := v.MapRange(); iter.Next(); {
		if length == capacity {
			break
		}
		mapEntry := wo.index(C.uint64_t(length))
		if err := e.encodeMapKey(iter.Key(), mapEntry); isIgnoredValueError(err) {
			continue
		}
		if err := e.encodeValue(iter.Value(), mapEntry, depth); err != nil {
			// Free the previously allocated map key
			freeWO(mapEntry)
			if isIgnoredValueError(err) {
				continue
			}
			return err
		}
		length++
	}
	// Update the map length to the actual one
	if length != capacity {
		wo.setLength(C.uint64_t(length))
	}
	return nil
}

func (e *encoder) encodeMapKey(v reflect.Value, wo *wafObject) error {
	for {
		switch v.Kind() {
		default:
			return errUnsupportedValue

		case reflect.Ptr, reflect.Interface:
			if v.IsNil() {
				return errUnsupportedValue
			}
			v = v.Elem()

		case reflect.String:
			ckey, length, err := cstring(v.String(), e.maxStringLength)
			if err != nil {
				return err
			}
			wo.setMapKey(ckey, C.uint64_t(length))
			return nil
		}
	}
}

func (e *encoder) encodeArray(v reflect.Value, wo *wafObject, depth int) error {
	// Consider the array length as a capacity as some array values might not be supported and ignored. In this case,
	// the actual length will be lesser than the Go value length.
	length := v.Len()
	capacity := length
	if capacity > e.maxArrayLength {
		capacity = e.maxArrayLength
	}
	if err := wo.setArrayContainer(C.size_t(capacity)); err != nil {
		return err
	}
	// Walk the array until we successfully added up to "cap" elements or the Go array length was reached
	currIndex := 0
	for i := 0; currIndex < capacity && i < length; i++ {
		if err := e.encodeValue(v.Index(i), wo.index(C.uint64_t(currIndex)), depth); err != nil {
			if isIgnoredValueError(err) {
				continue
			}
			return err
		}
		// The value has been successfully encoded and added to the array
		currIndex++
	}
	// Update the array length to its actual value in case some array values where ignored
	if currIndex != capacity {
		wo.setLength(C.uint64_t(currIndex))
	}
	return nil
}

func (e *encoder) encodeString(str string, wo *wafObject) error {
	cstr, length, err := cstring(str, e.maxStringLength)
	if err != nil {
		return err
	}
	wo.setString(cstr, C.uint64_t(length))
	return nil
}

func (e *encoder) encodeInt64(n int64, wo *wafObject) error {
	// As of libddwaf v1.0.16, it currently expects numbers as strings
	// TODO(Julio-Guerra): clarify with libddwaf when should it be an actual
	//   int64
	return e.encodeString(strconv.FormatInt(n, 10), wo)
}

func (e *encoder) encodeUint64(n uint64, wo *wafObject) error {
	// As of libddwaf v1.0.16, it currently expects numbers as strings
	// TODO(Julio-Guerra): clarify with libddwaf when should it be an actual
	//   uint64
	return e.encodeString(strconv.FormatUint(n, 10), wo)
}

func decodeErrors(wo *wafObject) (map[string]interface{}, error) {
	v, err := decodeMap(wo)
	if err != nil {
		return nil, err
	}
	if len(v) == 0 {
		v = nil // enforce a nil map when the ddwaf map was empty
	}
	return v, nil
}

func decodeObject(wo *wafObject) (v interface{}, err error) {
	if wo == nil {
		return nil, errNilObjectPtr
	}
	switch wo._type {
	case wafUintType:
		return uint64(*wo.uint64ValuePtr()), nil
	case wafIntType:
		return int64(*wo.int64ValuePtr()), nil
	case wafStringType:
		return gostring(*wo.stringValuePtr(), wo.length())
	case wafArrayType:
		return decodeArray(wo)
	case wafMapType: // could be a map or a struct, no way to differentiate
		return decodeMap(wo)
	default:
		return nil, errUnsupportedValue
	}
}

func decodeArray(wo *wafObject) ([]interface{}, error) {
	if wo == nil {
		return nil, errNilObjectPtr
	}
	var err error
	len := wo.length()
	arr := make([]interface{}, len)
	for i := C.uint64_t(0); i < len && err == nil; i++ {
		arr[i], err = decodeObject(wo.index(i))
	}
	return arr, err
}

func decodeMap(wo *wafObject) (map[string]interface{}, error) {
	if wo == nil {
		return nil, errNilObjectPtr
	}
	length := wo.length()
	decodedMap := make(map[string]interface{}, length)
	for i := C.uint64_t(0); i < length; i++ {
		obj := wo.index(i)
		key, err := decodeMapKey(obj)
		if err != nil {
			return nil, err
		}
		val, err := decodeObject(obj)
		if err != nil {
			return nil, err
		}
		decodedMap[key] = val
	}
	return decodedMap, nil
}

func decodeMapKey(wo *wafObject) (string, error) {
	if wo == nil {
		return "", errNilObjectPtr
	}
	if wo.parameterNameLength == 0 || wo.mapKey() == nil {
		return "", errInvalidMapKey
	}
	return gostring(wo.mapKey(), wo.parameterNameLength)
}

const (
	wafUintType    = C.DDWAF_OBJ_UNSIGNED
	wafIntType     = C.DDWAF_OBJ_SIGNED
	wafStringType  = C.DDWAF_OBJ_STRING
	wafArrayType   = C.DDWAF_OBJ_ARRAY
	wafMapType     = C.DDWAF_OBJ_MAP
	wafInvalidType = C.DDWAF_OBJ_INVALID
)

// wafObject is a Go wrapper allowing to create, access and destroy a WAF object
// C structure.
type wafObject C.ddwaf_object

func (v *wafObject) ctype() *C.ddwaf_object { return (*C.ddwaf_object)(v) }

// Return the pointer to the union field. It can be cast to the union type that needs to be accessed.
func (v *wafObject) valuePtr() unsafe.Pointer        { return unsafe.Pointer(&v.anon0[0]) }
func (v *wafObject) arrayValuePtr() **C.ddwaf_object { return (**C.ddwaf_object)(v.valuePtr()) }
func (v *wafObject) int64ValuePtr() *C.int64_t       { return (*C.int64_t)(v.valuePtr()) }
func (v *wafObject) uint64ValuePtr() *C.uint64_t     { return (*C.uint64_t)(v.valuePtr()) }
func (v *wafObject) stringValuePtr() **C.char        { return (**C.char)(v.valuePtr()) }

func (v *wafObject) setUint64(n C.uint64_t) {
	v._type = wafUintType
	*v.uint64ValuePtr() = n
}

func (v *wafObject) setInt64(n C.int64_t) {
	v._type = wafIntType
	*v.int64ValuePtr() = n
}

func (v *wafObject) setString(str *C.char, length C.uint64_t) {
	v._type = wafStringType
	v.nbEntries = C.uint64_t(length)
	*v.stringValuePtr() = str
}

func (v *wafObject) string() *C.char {
	return *v.stringValuePtr()
}

func (v *wafObject) setInvalid() {
	*v = wafObject{}
}

func (v *wafObject) setContainer(typ C.DDWAF_OBJ_TYPE, length C.size_t) error {
	// Allocate the zero'd array.
	var a *C.ddwaf_object
	if length > 0 {
		a = (*C.ddwaf_object)(C.calloc(length, C.sizeof_ddwaf_object))
		if a == nil {
			return ErrOutOfMemory
		}
		incNbLiveCObjects()
		*v.arrayValuePtr() = a
		v.setLength(C.uint64_t(length))
	}
	v._type = typ
	return nil
}

func (v *wafObject) setArrayContainer(length C.size_t) error {
	return v.setContainer(wafArrayType, length)
}

func (v *wafObject) setMapContainer(length C.size_t) error {
	return v.setContainer(wafMapType, length)
}

func (v *wafObject) setMapKey(key *C.char, length C.uint64_t) {
	v.parameterName = key
	v.parameterNameLength = length
}

func (v *wafObject) mapKey() *C.char {
	return v.parameterName
}

func (v *wafObject) setLength(length C.uint64_t) {
	v.nbEntries = length
}

func (v *wafObject) length() C.uint64_t {
	return v.nbEntries
}

func (v *wafObject) index(i C.uint64_t) *wafObject {
	if C.uint64_t(i) >= v.nbEntries {
		panic(errors.New("out of bounds access to waf array"))
	}
	// Go pointer arithmetic equivalent to the C expression `a->value.array[i]`
	base := uintptr(unsafe.Pointer(*v.arrayValuePtr()))
	return (*wafObject)(unsafe.Pointer(base + C.sizeof_ddwaf_object*uintptr(i)))
}

// Helper functions for testing, where direct cgo import is not allowed
func toCInt64(v int) C.int64_t {
	return C.int64_t(v)
}
func toCUint64(v uint) C.uint64_t {
	return C.uint64_t(v)
}

// nbLiveCObjects is a simple monitoring of the number of C allocations.
// Tests can read the value to check the count is back to 0.
var nbLiveCObjects uint64

func incNbLiveCObjects() {
	atomic.AddUint64(&nbLiveCObjects, 1)
}

func decNbLiveCObjects() {
	atomic.AddUint64(&nbLiveCObjects, ^uint64(0))
}

// gostring returns the Go version of the C string `str`, copying at most `len` bytes from the original string.
func gostring(str *C.char, len C.uint64_t) (string, error) {
	if str == nil {
		return "", ErrInvalidArgument
	}
	goLen := C.int(len)
	if C.uint64_t(goLen) != len {
		return "", ErrInvalidArgument
	}
	return C.GoStringN(str, goLen), nil
}

// cstring returns the C string of the given Go string `str` with up to maxWAFStringSize bytes, along with the string
// size that was allocated and copied.
func cstring(str string, maxLength int) (*C.char, int, error) {
	// Limit the maximum string size to copy
	l := len(str)
	if l > maxLength {
		l = maxLength
	}
	// Copy the string up to l.
	// The copy is required as the pointer will be stored into the C structures,
	// so using a Go pointer is impossible.
	cstr := C.CString(str[:l])
	if cstr == nil {
		return nil, 0, errOutOfMemory
	}
	incNbLiveCObjects()
	return cstr, l, nil
}

func freeWO(v *wafObject) {
	if v == nil {
		return
	}
	// Free the map key if any
	if key := v.mapKey(); key != nil {
		cFree(unsafe.Pointer(v.parameterName))
	}
	// Free allocated values
	switch v._type {
	case wafInvalidType:
		return
	case wafStringType:
		cFree(unsafe.Pointer(v.string()))
	case wafMapType, wafArrayType:
		freeWOContainer(v)
	}
	// Make the value invalid to make it unusable
	v.setInvalid()
}

func freeWOContainer(v *wafObject) {
	length := v.length()
	for i := C.uint64_t(0); i < length; i++ {
		freeWO(v.index(i))
	}
	if a := *v.arrayValuePtr(); a != nil {
		cFree(unsafe.Pointer(a))
	}
}

func cFree(ptr unsafe.Pointer) {
	C.free(ptr)
	decNbLiveCObjects()
}

// Exported Go function to free ddwaf objects by using freeWO in order to keep
// its dummy but efficient memory kallocation monitoring.
//
//export go_ddwaf_object_free
func go_ddwaf_object_free(v *C.ddwaf_object) {
	freeWO((*wafObject)(v))
}

// Go reimplementation of ddwaf_result_free to avoid yet another CGO call in the
// request hot-path and avoiding it when there are no results to free.
func freeWAFResult(result *C.ddwaf_result) {
	if data := result.data; data != nil {
		C.free(unsafe.Pointer(data))
	}

	if array := result.actions.array; array != nil {
		for i := 0; i < int(result.actions.size); i++ {
			C.free(unsafe.Pointer(cindexCharPtrArray(array, i)))
		}
		C.free(unsafe.Pointer(array))
	}
}

// Helper function to access to i-th element of the given **C.char array.
func cindexCharPtrArray(array **C.char, i int) *C.char {
	// Go pointer arithmetic equivalent to the C expression `array[i]`
	base := uintptr(unsafe.Pointer(array))
	return *(**C.char)(unsafe.Pointer(base + unsafe.Sizeof((*C.char)(nil))*uintptr(i)))
}

// Atomic reference counter helper initialized at 1 so that 0 is the special
// value meaning that the object was released and is no longer usable.
type atomicRefCounter uint32

func (c *atomicRefCounter) unwrap() *uint32 {
	return (*uint32)(c)
}

// Add delta to reference counter.
// It relies on a CAS spin-loop implementation in order to avoid changing the
// counter when 0 has been reached.
func (c *atomicRefCounter) add(delta uint32) uint32 {
	addr := c.unwrap()
	for {
		current := atomic.LoadUint32(addr)
		if current == 0 {
			// The object was released
			return 0
		}
		new := current + delta
		if swapped := atomic.CompareAndSwapUint32(addr, current, new); swapped {
			return new
		}
	}
}

// Increment the reference counter.
// CAS spin-loop implementation in order to enforce +1 cannot happen when 0 has
// been reached.
func (c *atomicRefCounter) increment() uint32 {
	return c.add(1)
}

// Decrement the reference counter.
// CAS spin-loop implementation in order to enforce +1 cannot happen when 0 has
// been reached.
func (c *atomicRefCounter) decrement() uint32 {
	const d = ^uint32(0)
	return c.add(d)
}
