// Package alignedbuff implements encoding and decoding aligned data elements
// to/from buffers in native endianess.
//
// # Note
//
// The alignment/padding as implemented in this package must match that of
// kernel's and user space C implementations for a particular architecture (bit
// size). Please see also the "dummy structure" _xt_align
// (https://elixir.bootlin.com/linux/v5.17.7/source/include/uapi/linux/netfilter/x_tables.h#L93)
// as well as the associated XT_ALIGN C preprocessor macro.
//
// In particular, we rely on the Go compiler to follow the same architecture
// alignments as the C compiler(s) on Linux.
package alignedbuff

import (
	"bytes"
	"errors"
	"fmt"
	"unsafe"

	"github.com/sagernet/nftables/binaryutil"
)

// ErrEOF signals trying to read beyond the available payload information.
var ErrEOF = errors.New("not enough data left")

// AlignedBuff implements marshalling and unmarshalling information in
// platform/architecture native endianess and data type alignment. It
// additionally covers some of the nftables-xtables translation-specific
// idiosyncracies to the extend needed in order to properly marshal and
// unmarshal Match and Target expressions, and their Info payload in particular.
type AlignedBuff struct {
	data []byte
	pos  int
}

// New returns a new AlignedBuff for marshalling aligned data in native
// endianess.
func New() AlignedBuff {
	return AlignedBuff{}
}

// NewWithData returns a new AlignedBuff for unmarshalling the passed data in
// native endianess.
func NewWithData(data []byte) AlignedBuff {
	return AlignedBuff{data: data}
}

// Data returns the properly padded info payload data written before by calling
// the various Uint8, Uint16, ... marshalling functions.
func (a *AlignedBuff) Data() []byte {
	// The Linux kernel expects payloads to be padded to the next uint64
	// alignment.
	a.alignWrite(uint64AlignMask)
	return a.data
}

// BytesAligned32 unmarshals the given amount of bytes starting with the native
// alignment for uint32 data types. It returns ErrEOF when trying to read beyond
// the payload.
//
// BytesAligned32 is used to unmarshal IP addresses for different IP versions,
// which are always aligned the same way as the native alignment for uint32.
func (a *AlignedBuff) BytesAligned32(size int) ([]byte, error) {
	if err := a.alignCheckedRead(uint32AlignMask); err != nil {
		return nil, err
	}
	if a.pos > len(a.data)-size {
		return nil, ErrEOF
	}
	data := a.data[a.pos : a.pos+size]
	a.pos += size
	return data, nil
}

// Uint8 unmarshals an uint8 in native endianess and alignment. It returns
// ErrEOF when trying to read beyond the payload.
func (a *AlignedBuff) Uint8() (uint8, error) {
	if a.pos >= len(a.data) {
		return 0, ErrEOF
	}
	v := a.data[a.pos]
	a.pos++
	return v, nil
}

// Uint16 unmarshals an uint16 in native endianess and alignment. It returns
// ErrEOF when trying to read beyond the payload.
func (a *AlignedBuff) Uint16() (uint16, error) {
	if err := a.alignCheckedRead(uint16AlignMask); err != nil {
		return 0, err
	}
	v := binaryutil.NativeEndian.Uint16(a.data[a.pos : a.pos+2])
	a.pos += 2
	return v, nil
}

// Uint16BE unmarshals an uint16 in "network" (=big endian) endianess and native
// uint16 alignment. It returns ErrEOF when trying to read beyond the payload.
func (a *AlignedBuff) Uint16BE() (uint16, error) {
	if err := a.alignCheckedRead(uint16AlignMask); err != nil {
		return 0, err
	}
	v := binaryutil.BigEndian.Uint16(a.data[a.pos : a.pos+2])
	a.pos += 2
	return v, nil
}

// Uint32 unmarshals an uint32 in native endianess and alignment. It returns
// ErrEOF when trying to read beyond the payload.
func (a *AlignedBuff) Uint32() (uint32, error) {
	if err := a.alignCheckedRead(uint32AlignMask); err != nil {
		return 0, err
	}
	v := binaryutil.NativeEndian.Uint32(a.data[a.pos : a.pos+4])
	a.pos += 4
	return v, nil
}

// Uint64 unmarshals an uint64 in native endianess and alignment. It returns
// ErrEOF when trying to read beyond the payload.
func (a *AlignedBuff) Uint64() (uint64, error) {
	if err := a.alignCheckedRead(uint64AlignMask); err != nil {
		return 0, err
	}
	v := binaryutil.NativeEndian.Uint64(a.data[a.pos : a.pos+8])
	a.pos += 8
	return v, nil
}

// Int32 unmarshals an int32 in native endianess and alignment. It returns
// ErrEOF when trying to read beyond the payload.
func (a *AlignedBuff) Int32() (int32, error) {
	if err := a.alignCheckedRead(int32AlignMask); err != nil {
		return 0, err
	}
	v := binaryutil.Int32(a.data[a.pos : a.pos+4])
	a.pos += 4
	return v, nil
}

// String unmarshals a null terminated string
func (a *AlignedBuff) String() (string, error) {
	len := 0
	for {
		if a.data[a.pos+len] == 0x00 {
			break
		}
		len++
	}

	v := binaryutil.String(a.data[a.pos : a.pos+len])
	a.pos += len
	return v, nil
}

// StringWithLength unmarshals a string of a given length (for non-null
// terminated strings)
func (a *AlignedBuff) StringWithLength(len int) (string, error) {
	v := binaryutil.String(a.data[a.pos : a.pos+len])
	a.pos += len
	return v, nil
}

// Uint unmarshals an uint in native endianess and alignment for the C "unsigned
// int" type. It returns ErrEOF when trying to read beyond the payload. Please
// note that on 64bit platforms, the size and alignment of C's and Go's unsigned
// integer data types differ, so we encapsulate this difference here.
func (a *AlignedBuff) Uint() (uint, error) {
	switch uintSize {
	case 2:
		v, err := a.Uint16()
		return uint(v), err
	case 4:
		v, err := a.Uint32()
		return uint(v), err
	case 8:
		v, err := a.Uint64()
		return uint(v), err
	default:
		panic(fmt.Sprintf("unsupported uint size %d", uintSize))
	}
}

// PutBytesAligned32 marshals the given bytes starting with the native alignment
// for uint32 data types. It additionaly adds padding to reach the specified
// size.
//
// PutBytesAligned32 is used to marshal IP addresses for different IP versions,
// which are always aligned the same way as the native alignment for uint32.
func (a *AlignedBuff) PutBytesAligned32(data []byte, size int) {
	a.alignWrite(uint32AlignMask)
	a.data = append(a.data, data...)
	a.pos += len(data)
	if len(data) < size {
		padding := size - len(data)
		a.data = append(a.data, bytes.Repeat([]byte{0}, padding)...)
		a.pos += padding
	}
}

// PutUint8 marshals an uint8 in native endianess and alignment.
func (a *AlignedBuff) PutUint8(v uint8) {
	a.data = append(a.data, v)
	a.pos++
}

// PutUint16 marshals an uint16 in native endianess and alignment.
func (a *AlignedBuff) PutUint16(v uint16) {
	a.alignWrite(uint16AlignMask)
	a.data = append(a.data, binaryutil.NativeEndian.PutUint16(v)...)
	a.pos += 2
}

// PutUint16BE marshals an uint16 in "network" (=big endian) endianess and
// native uint16 alignment.
func (a *AlignedBuff) PutUint16BE(v uint16) {
	a.alignWrite(uint16AlignMask)
	a.data = append(a.data, binaryutil.BigEndian.PutUint16(v)...)
	a.pos += 2
}

// PutUint32 marshals an uint32 in native endianess and alignment.
func (a *AlignedBuff) PutUint32(v uint32) {
	a.alignWrite(uint32AlignMask)
	a.data = append(a.data, binaryutil.NativeEndian.PutUint32(v)...)
	a.pos += 4
}

// PutUint64 marshals an uint64 in native endianess and alignment.
func (a *AlignedBuff) PutUint64(v uint64) {
	a.alignWrite(uint64AlignMask)
	a.data = append(a.data, binaryutil.NativeEndian.PutUint64(v)...)
	a.pos += 8
}

// PutInt32 marshals an int32 in native endianess and alignment.
func (a *AlignedBuff) PutInt32(v int32) {
	a.alignWrite(int32AlignMask)
	a.data = append(a.data, binaryutil.PutInt32(v)...)
	a.pos += 4
}

// PutString marshals a string.
func (a *AlignedBuff) PutString(v string) {
	a.data = append(a.data, binaryutil.PutString(v)...)
	a.pos += len(v)
}

// PutUint marshals an uint in native endianess and alignment for the C
// "unsigned int" type. Please note that on 64bit platforms, the size and
// alignment of C's and Go's unsigned integer data types differ, so we
// encapsulate this difference here.
func (a *AlignedBuff) PutUint(v uint) {
	switch uintSize {
	case 2:
		a.PutUint16(uint16(v))
	case 4:
		a.PutUint32(uint32(v))
	case 8:
		a.PutUint64(uint64(v))
	default:
		panic(fmt.Sprintf("unsupported uint size %d", uintSize))
	}
}

// alignCheckedRead aligns the (read) position if necessary and suitable
// according to the specified alignment mask. alignCheckedRead returns an error
// if after any necessary alignment there isn't enough data left to be read into
// a value of the size corresponding to the specified alignment mask.
func (a *AlignedBuff) alignCheckedRead(m int) error {
	a.pos = (a.pos + m) & ^m
	if a.pos > len(a.data)-(m+1) {
		return ErrEOF
	}
	return nil
}

// alignWrite aligns the (write) position if necessary and suitable according to
// the specified alignment mask. It doubles as final payload padding helpmate in
// order to keep the kernel happy.
func (a *AlignedBuff) alignWrite(m int) {
	pos := (a.pos + m) & ^m
	if pos != a.pos {
		a.data = append(a.data, padding[:pos-a.pos]...)
		a.pos = pos
	}
}

// This is ... ugly.
var (
	uint16AlignMask = int(unsafe.Alignof(uint16(0)) - 1)
	uint32AlignMask = int(unsafe.Alignof(uint32(0)) - 1)
	uint64AlignMask = int(unsafe.Alignof(uint64(0)) - 1)
	padding         = bytes.Repeat([]byte{0}, uint64AlignMask)
)

var int32AlignMask = int(unsafe.Alignof(int32(0)) - 1)

// And this even worse.
var uintSize = unsafe.Sizeof(uint32(0))
