Implement Comparison operation (parsing, interpretation, evaluation, tests).

Also improves the conciseness and consistency of the nftinterp_test file.

PiperOrigin-RevId: 663200389
This commit is contained in:
Jayden Nyamiaka
2024-08-15 00:16:41 -07:00
committed by gVisor bot
parent be54c87aaf
commit b508258e39
4 changed files with 766 additions and 159 deletions
+137 -28
View File
@@ -42,6 +42,8 @@ import (
"fmt"
"slices"
"encoding/binary"
"gvisor.dev/gvisor/pkg/abi/linux"
"gvisor.dev/gvisor/pkg/tcpip/stack"
)
@@ -554,9 +556,6 @@ type Rule struct {
// Operation represents a single operation in a rule.
type Operation interface {
// TypeString returns the string representation of the type of the operation.
TypeString() string
// evaluate evaluates the operation on the given packet and register set,
// changing the register set and possibly the packet in place.
evaluate(regs *RegisterSet, pkt *stack.PacketBuffer)
@@ -565,6 +564,7 @@ type Operation interface {
// Ensures all operations implement the Operation interface at compile time.
var (
_ Operation = (*Immediate)(nil)
_ Operation = (*Comparison)(nil)
)
// Immediate is an operation that sets the data in a register.
@@ -581,14 +581,119 @@ func NewImmediate(dreg uint8, data RegisterData) (*Immediate, error) {
return &Immediate{dreg: dreg, data: data}, nil
}
// TypeString for Immediate returns "Immediate" as the string operation type.
func (op *Immediate) TypeString() string { return "Immediate" }
// evaluate for Immediate sets the data in the destination register.
func (op Immediate) evaluate(regs *RegisterSet, pkt *stack.PacketBuffer) {
op.data.StoreData(regs, op.dreg)
}
// Comparison is an operation that compares the data in a register to a given
// value and breaks (by setting the verdict register to NFT_BREAK) from the rule
// if the comparison is false.
// Note: comparison operations are not supported for the verdict register.
type Comparison struct {
data RegisterData // Data to compare the source register to.
sreg uint8 // Number of the source register.
cop cmpOp // Comparison operator.
}
// cmpOp is the comparison operator for a Comparison operation.
// Note: corresponds to enum nft_cmp_op from
// include/uapi/linux/netfilter/nf_tables.h and uses the same constants.
type cmpOp int
// String for NftCmpOp returns the string representation of the comparison
// operator.
func (cop cmpOp) String() string {
switch cop {
case linux.NFT_CMP_EQ:
return "=="
case linux.NFT_CMP_NEQ:
return "!="
case linux.NFT_CMP_LT:
return "<"
case linux.NFT_CMP_LTE:
return "<="
case linux.NFT_CMP_GT:
return ">"
case linux.NFT_CMP_GTE:
return ">="
default:
panic(fmt.Sprintf("invalid comparison operator: %d", int(cop)))
}
}
// validateComparisonOp ensures the comparison operator is valid.
func validateComparisonOp(cop cmpOp) error {
switch cop {
case linux.NFT_CMP_EQ, linux.NFT_CMP_NEQ, linux.NFT_CMP_LT, linux.NFT_CMP_LTE, linux.NFT_CMP_GT, linux.NFT_CMP_GTE:
return nil
default:
return fmt.Errorf("invalid comparison operator: %d", int(cop))
}
}
// NewComparison creates a new Comparison operation.
func NewComparison(sreg uint8, op int, data RegisterData) (*Comparison, error) {
if sreg == linux.NFT_REG_VERDICT {
return nil, fmt.Errorf("comparison operation cannot use verdict register as source")
}
if err := data.ValidateRegister(sreg); err != nil {
return nil, err
}
cop := cmpOp(op)
if err := validateComparisonOp(cop); err != nil {
return nil, err
}
return &Comparison{sreg: sreg, cop: cop, data: data}, nil
}
// evaluate for Comparison compares the data in the source register to the given
// data and breaks from the rule if the comparison is false.
func (op Comparison) evaluate(regs *RegisterSet, pkt *stack.PacketBuffer) {
// Gets the data from the source register.
regBuf := getRegisterData(regs, op.sreg, op.data.Type())
// Gets the data to compare to.
bytesData, ok := op.data.(BytesData)
if !ok {
panic("comparison operation data is not BytesData")
}
// Compares from left to right in 4-byte chunks starting with the rightmost
// byte of every 4-byte chunk since the data is little endian.
// For example, 16-byte IPv6 address 2001:000a:130f:0000:0000:09c0:876a:130b
// is represented as 0x0a000120 0x00000f13 0xc0090000 0x0b136a87 in operations
// and as [0a|00|01|20|00|00|0f|13|c0|09|00|00|0b|13|6a|87] in the byte slice,
// so we compare right to left in the first 4 bytes and then go to the next 4.
dif := 0
for i := 0; i < len(bytesData.data) && dif == 0; i += 4 {
regVal := binary.LittleEndian.Uint32(regBuf[i : i+4])
opVal := binary.LittleEndian.Uint32(bytesData.data[i : i+4])
if regVal < opVal {
dif = -1
} else if regVal > opVal {
dif = 1
}
}
var result bool
switch op.cop {
case linux.NFT_CMP_EQ:
result = dif == 0
case linux.NFT_CMP_NEQ:
result = dif != 0
case linux.NFT_CMP_LT:
result = dif < 0
case linux.NFT_CMP_LTE:
result = dif <= 0
case linux.NFT_CMP_GT:
result = dif > 0
case linux.NFT_CMP_GTE:
result = dif >= 0
}
if !result {
// Comparison is false, so break from the rule.
regs.verdict = Verdict{Code: VC(linux.NFT_BREAK)}
}
}
//
// Register and Register-Related Implementations.
// Note: Registers are represented by type uint8 for the register number.
@@ -735,29 +840,33 @@ func (rd BytesData) ValidateRegister(reg uint8) error {
return nil
}
// getRegisterData is a helper function that gets the appropriate slice of
// register data from the register set.
// Note: does not support verdict data and assumes the register is valid for the
// given data type.
func getRegisterData(regs *RegisterSet, reg uint8, dataType RegisterDataType) []byte {
// 4-byte data in a 4-byte register.
if is4ByteRegister(reg) {
start := (reg - linux.NFT_REG32_00) * linux.NFT_REG32_SIZE
return regs.data[start : start+linux.NFT_REG32_SIZE]
}
// 16-byte data in a 16-byte register.
if dataType == Data16Bytes {
start := (reg - linux.NFT_REG_1) * linux.NFT_REG_SIZE
return regs.data[start : start+linux.NFT_REG_SIZE]
}
// 4-byte data in a 16-byte register
// Leaves excess space on the left (bc the data is little endian).
start := (reg-linux.NFT_REG_1)*linux.NFT_REG_SIZE + linux.NFT_REG_SIZE - linux.NFT_REG32_SIZE
return regs.data[start : start+linux.NFT_REG32_SIZE]
}
// StoreData sets the data in the destination register to the uint32.
func (rd BytesData) StoreData(regs *RegisterSet, reg uint8) {
if err := rd.ValidateRegister(reg); err != nil {
panic(err)
}
var start uint8
var regBuf []byte
// Stores 4-byte data in a 4-byte register.
if is4ByteRegister(reg) {
start = (reg - linux.NFT_REG32_00) * linux.NFT_REG32_SIZE
regBuf = regs.data[start : start+linux.NFT_REG32_SIZE]
} else {
// Stores 16-byte data in a 16-byte register.
if rd.Type() == Data16Bytes {
start = (reg - linux.NFT_REG_1) * linux.NFT_REG_SIZE
regBuf = regs.data[start : start+linux.NFT_REG_SIZE]
} else {
// Stores 4-byte data in a 16-byte register, leaving excess space on the
// left (bc the data is little endian).
start = (reg-linux.NFT_REG_1)*linux.NFT_REG_SIZE + linux.NFT_REG_SIZE - linux.NFT_REG32_SIZE
regBuf = regs.data[start : start+linux.NFT_REG32_SIZE]
}
}
regBuf := getRegisterData(regs, reg, rd.Type())
copy(regBuf, rd.data)
}
@@ -806,7 +915,7 @@ type Verdict struct {
// String returns a string representation of the verdict.
func (v Verdict) String() string {
out := VerdictToString(v.Code)
out := VerdictCodeToString(v.Code)
if v.ChainName != "" {
out += fmt.Sprintf(" -> %s", v.ChainName)
}
@@ -818,8 +927,8 @@ func VC(v int32) uint32 {
return uint32(v)
}
// VerdictToString prints names for the supported verdicts.
func VerdictToString(v uint32) string {
// VerdictCodeToString prints names for the supported verdicts.
func VerdictCodeToString(v uint32) string {
switch v {
// Netfilter (External) Verdicts:
case VC(linux.NF_DROP):
@@ -916,7 +1025,7 @@ func (nf *NFTables) EvaluateHook(family AddressFamily, hook Hook, pkt *stack.Pac
return Verdict{Code: VC(linux.NF_ACCEPT)}, nil
}
panic(fmt.Sprintf("unexpected verdict from hook evaluation: %s", VerdictToString(regs.Verdict().Code)))
panic(fmt.Sprintf("unexpected verdict from hook evaluation: %s", VerdictCodeToString(regs.Verdict().Code)))
}
// evaluateFromRule is a helper function for Chain.evaluate that evaluates the
+287
View File
@@ -290,6 +290,284 @@ func TestEvaluateImmediate(t *testing.T) {
}
}
// TestEvaluateComparison tests that the Comparison operation correctly compares
// the data in the source register to the given data.
// Note: Relies on expected behavior of the Immediate operation.
func TestEvaluateComparison(t *testing.T) {
for _, test := range []struct {
tname string
op1 Operation // will be nil if unused
op2 Operation // will be nil if unused
res bool // should be true if we reach end of the rule (no breaks)
}{
{
tname: "compare register == 4-byte data, true",
op1: mustCreateImmediate(t, linux.NFT_REG_1, NewBytesData([]byte{0, 0, 0, 0})),
op2: mustCreateComparison(t, linux.NFT_REG_1, linux.NFT_CMP_EQ, NewBytesData([]byte{0, 0, 0, 0})),
res: true,
},
{
tname: "compare register == 4-byte data, false",
op1: mustCreateImmediate(t, linux.NFT_REG32_11, NewBytesData([]byte{1, 0, 0, 0})),
op2: mustCreateComparison(t, linux.NFT_REG32_11, linux.NFT_CMP_EQ, NewBytesData([]byte{0, 0, 0, 0})),
res: false,
},
{
tname: "compare register != 4-byte data, true",
op1: mustCreateImmediate(t, linux.NFT_REG32_03, NewBytesData([]byte{1, 7, 0, 0})),
op2: mustCreateComparison(t, linux.NFT_REG32_03, linux.NFT_CMP_NEQ, NewBytesData([]byte{1, 98, 0, 56})),
res: true,
},
{
tname: "compare register != 4-byte data, false",
op1: mustCreateImmediate(t, linux.NFT_REG_3, NewBytesData([]byte{1, 98, 0, 56})),
op2: mustCreateComparison(t, linux.NFT_REG_3, linux.NFT_CMP_NEQ, NewBytesData([]byte{1, 98, 0, 56})),
res: false,
},
{
tname: "compare register < 4-byte data, true",
op1: mustCreateImmediate(t, linux.NFT_REG_4, NewBytesData([]byte{29, 0, 0, 0})),
op2: mustCreateComparison(t, linux.NFT_REG_4, linux.NFT_CMP_LT, NewBytesData([]byte{100, 0, 0, 0})),
res: true,
},
{
tname: "compare register < 4-byte data, false eq",
op1: mustCreateImmediate(t, linux.NFT_REG32_04, NewBytesData([]byte{100, 0, 0, 0})),
op2: mustCreateComparison(t, linux.NFT_REG32_04, linux.NFT_CMP_LT, NewBytesData([]byte{100, 0, 0, 0})),
res: false,
},
{
tname: "compare register < 4-byte data, false gt",
op1: mustCreateImmediate(t, linux.NFT_REG32_14, NewBytesData([]byte{200, 0, 0, 0})),
op2: mustCreateComparison(t, linux.NFT_REG32_14, linux.NFT_CMP_LT, NewBytesData([]byte{100, 0, 0, 0})),
res: false,
},
{
tname: "compare register > 4-byte data, true",
op1: mustCreateImmediate(t, linux.NFT_REG32_15, NewBytesData([]byte{0, 0, 0, 1})),
op2: mustCreateComparison(t, linux.NFT_REG32_15, linux.NFT_CMP_GT, NewBytesData([]byte{29, 76, 230, 0})),
res: true,
},
{
tname: "compare register > 4-byte data, false eq",
op1: mustCreateImmediate(t, linux.NFT_REG32_07, NewBytesData([]byte{29, 76, 230, 0})),
op2: mustCreateComparison(t, linux.NFT_REG32_07, linux.NFT_CMP_GT, NewBytesData([]byte{29, 76, 230, 0})),
res: false,
},
{
tname: "compare register > 4-byte data, false lt",
op1: mustCreateImmediate(t, linux.NFT_REG32_05, NewBytesData([]byte{28, 76, 230, 0})),
op2: mustCreateComparison(t, linux.NFT_REG32_05, linux.NFT_CMP_GT, NewBytesData([]byte{29, 76, 230, 0})),
res: false,
},
{
tname: "compare register <= 4-byte data, true lt",
op1: mustCreateImmediate(t, linux.NFT_REG_2, NewBytesData([]byte{29, 0, 0, 0})),
op2: mustCreateComparison(t, linux.NFT_REG_2, linux.NFT_CMP_LTE, NewBytesData([]byte{100, 0, 0, 0})),
res: true,
},
{
tname: "compare register <= 4-byte data, true eq",
op1: mustCreateImmediate(t, linux.NFT_REG32_09, NewBytesData([]byte{100, 0, 0, 0})),
op2: mustCreateComparison(t, linux.NFT_REG32_09, linux.NFT_CMP_LTE, NewBytesData([]byte{100, 0, 0, 0})),
res: true,
},
{
tname: "compare register <= 4-byte data, false",
op1: mustCreateImmediate(t, linux.NFT_REG32_06, NewBytesData([]byte{200, 0, 0, 0})),
op2: mustCreateComparison(t, linux.NFT_REG32_06, linux.NFT_CMP_LTE, NewBytesData([]byte{100, 0, 0, 0})),
res: false,
},
{
tname: "compare register >= 4-byte data, true gt",
op1: mustCreateImmediate(t, linux.NFT_REG32_12, NewBytesData([]byte{0, 0, 0, 1})),
op2: mustCreateComparison(t, linux.NFT_REG32_12, linux.NFT_CMP_GTE, NewBytesData([]byte{29, 76, 230, 0})),
res: true,
},
{
tname: "compare register >= 4-byte data, true eq",
op1: mustCreateImmediate(t, linux.NFT_REG_1, NewBytesData([]byte{29, 76, 230, 0})),
op2: mustCreateComparison(t, linux.NFT_REG_1, linux.NFT_CMP_GTE, NewBytesData([]byte{29, 76, 230, 0})),
res: true,
},
{
tname: "compare register >= 4-byte data, false",
op1: mustCreateImmediate(t, linux.NFT_REG_3, NewBytesData([]byte{28, 76, 230, 0})),
op2: mustCreateComparison(t, linux.NFT_REG_3, linux.NFT_CMP_GTE, NewBytesData([]byte{29, 76, 230, 0})),
res: false,
},
{
tname: "compare register == 16-byte data, true",
op1: mustCreateImmediate(t, linux.NFT_REG_1, NewBytesData([]byte{1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0})),
op2: mustCreateComparison(t, linux.NFT_REG_1, linux.NFT_CMP_EQ, NewBytesData([]byte{1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0})),
res: true,
},
{
tname: "compare register == 16-byte data, false",
op1: mustCreateImmediate(t, linux.NFT_REG_2, NewBytesData([]byte{1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0})),
op2: mustCreateComparison(t, linux.NFT_REG_2, linux.NFT_CMP_EQ, NewBytesData([]byte{1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1})),
res: false,
},
{
tname: "compare register != 16-byte data, true",
op1: mustCreateImmediate(t, linux.NFT_REG_3, NewBytesData([]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16})),
op2: mustCreateComparison(t, linux.NFT_REG_3, linux.NFT_CMP_NEQ, NewBytesData([]byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15})),
res: true,
},
{
tname: "compare register != 16-byte data, false",
op1: mustCreateImmediate(t, linux.NFT_REG_4, NewBytesData([]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16})),
op2: mustCreateComparison(t, linux.NFT_REG_4, linux.NFT_CMP_NEQ, NewBytesData([]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16})),
res: false,
},
{
tname: "compare register < 16-byte data, true",
op1: mustCreateImmediate(t, linux.NFT_REG_1, NewBytesData([]byte{0x0a, 0x00, 0x01, 0x1f, 0x00, 0x00, 0x0f, 0x13, 0xc0, 0x09, 0x00, 0x00, 0x0b, 0x13, 0x6a, 0xaa})),
op2: mustCreateComparison(t, linux.NFT_REG_1, linux.NFT_CMP_LT, NewBytesData([]byte{0x0a, 0x00, 0x01, 0x20, 0x00, 0x00, 0x0f, 0x13, 0xc0, 0x09, 0x00, 0x00, 0x0b, 0x13, 0x6a, 0x87})),
res: true,
},
{
tname: "compare register < 16-byte data, false eq",
op1: mustCreateImmediate(t, linux.NFT_REG_2, NewBytesData([]byte{0x0a, 0x00, 0x01, 0x20, 0x00, 0x00, 0x0f, 0x13, 0xc0, 0x09, 0x00, 0x00, 0x0b, 0x13, 0x6a, 0x87})),
op2: mustCreateComparison(t, linux.NFT_REG_2, linux.NFT_CMP_LT, NewBytesData([]byte{0x0a, 0x00, 0x01, 0x20, 0x00, 0x00, 0x0f, 0x13, 0xc0, 0x09, 0x00, 0x00, 0x0b, 0x13, 0x6a, 0x87})),
res: false,
},
{
tname: "compare register < 16-byte data, false gt",
op1: mustCreateImmediate(t, linux.NFT_REG_3, NewBytesData([]byte{0x0a, 0x00, 0x01, 0x21, 0x00, 0x00, 0x0f, 0x13, 0xc0, 0x09, 0x00, 0x00, 0x0b, 0x13, 0x6a, 0xaa})),
op2: mustCreateComparison(t, linux.NFT_REG_3, linux.NFT_CMP_LT, NewBytesData([]byte{0x0a, 0x00, 0x01, 0x20, 0x00, 0x00, 0x0f, 0x13, 0xc0, 0x09, 0x00, 0x00, 0x0b, 0x13, 0x6a, 0x87})),
res: false,
},
{
tname: "compare register > 16-byte data, true",
op1: mustCreateImmediate(t, linux.NFT_REG_4, NewBytesData([]byte{0x0a, 0x00, 0x01, 0x21, 0xaa, 0xaa, 0xaa, 0xaa, 0xc0, 0x09, 0x00, 0x00, 0x0b, 0x13, 0x6a, 0x87})),
op2: mustCreateComparison(t, linux.NFT_REG_4, linux.NFT_CMP_GT, NewBytesData([]byte{0x0a, 0x00, 0x01, 0x20, 0xcc, 0xcc, 0xcc, 0xcc, 0xc0, 0x09, 0x00, 0x00, 0x0b, 0x13, 0x6a, 0x87})),
res: true,
},
{
tname: "compare register > 16-byte data, false eq",
op1: mustCreateImmediate(t, linux.NFT_REG_1, NewBytesData([]byte{0x0a, 0x00, 0x01, 0x20, 0x00, 0x00, 0x0f, 0x13, 0xc0, 0x09, 0x00, 0x00, 0x0b, 0x13, 0x6a, 0x87})),
op2: mustCreateComparison(t, linux.NFT_REG_1, linux.NFT_CMP_GT, NewBytesData([]byte{0x0a, 0x00, 0x01, 0x20, 0x00, 0x00, 0x0f, 0x13, 0xc0, 0x09, 0x00, 0x00, 0x0b, 0x13, 0x6a, 0x87})),
res: false,
},
{
tname: "compare register > 16-byte data, false lt",
op1: mustCreateImmediate(t, linux.NFT_REG_2, NewBytesData([]byte{0x0a, 0x00, 0x01, 0x1f, 0x00, 0x00, 0x0f, 0x13, 0xc0, 0x09, 0x00, 0x00, 0x0b, 0x13, 0x6a, 0x90})),
op2: mustCreateComparison(t, linux.NFT_REG_2, linux.NFT_CMP_GT, NewBytesData([]byte{0x0a, 0x00, 0x01, 0x20, 0x00, 0x00, 0x0f, 0x13, 0xc0, 0x09, 0x00, 0x00, 0x0b, 0x13, 0x6a, 0x87})),
res: false,
},
{
tname: "compare register <= 16-byte data, true lt",
op1: mustCreateImmediate(t, linux.NFT_REG_1, NewBytesData([]byte{0x0a, 0x00, 0x01, 0x20, 0x00, 0x00, 0x0f, 0x13, 0xc0, 0x09, 0x00, 0x00, 0x0b, 0x13, 0x6a, 0x86})),
op2: mustCreateComparison(t, linux.NFT_REG_1, linux.NFT_CMP_LTE, NewBytesData([]byte{0x0a, 0x00, 0x01, 0x20, 0x00, 0x00, 0x0f, 0x13, 0xc0, 0x09, 0x00, 0x00, 0x0b, 0x13, 0x6a, 0x87})),
res: true,
},
{
tname: "compare register <= 16-byte data, true eq",
op1: mustCreateImmediate(t, linux.NFT_REG_2, NewBytesData([]byte{0x0a, 0x00, 0x01, 0x20, 0x00, 0x00, 0x0f, 0x13, 0xc0, 0x09, 0x00, 0x00, 0x0b, 0x13, 0x6a, 0x87})),
op2: mustCreateComparison(t, linux.NFT_REG_2, linux.NFT_CMP_LTE, NewBytesData([]byte{0x0a, 0x00, 0x01, 0x20, 0x00, 0x00, 0x0f, 0x13, 0xc0, 0x09, 0x00, 0x00, 0x0b, 0x13, 0x6a, 0x87})),
res: true,
},
{
tname: "compare register <= 16-byte data, false",
op1: mustCreateImmediate(t, linux.NFT_REG_3, NewBytesData([]byte{0x0a, 0x00, 0x01, 0x20, 0x00, 0x00, 0x0f, 0x13, 0xc0, 0x09, 0xaa, 0x00, 0x0b, 0x13, 0x6a, 0x88})),
op2: mustCreateComparison(t, linux.NFT_REG_3, linux.NFT_CMP_LTE, NewBytesData([]byte{0x0a, 0x00, 0x01, 0x20, 0x00, 0x00, 0x0f, 0x13, 0xc0, 0x09, 0x00, 0x00, 0x0b, 0x13, 0x6a, 0x87})),
res: false,
},
{
tname: "compare register >= 16-byte data, true gt",
op1: mustCreateImmediate(t, linux.NFT_REG_4, NewBytesData([]byte{0xaa, 0xaa, 0xaa, 0x20, 0xaa, 0xaa, 0xaa, 0x13, 0xc0, 0x09, 0x00, 0x00, 0x0b, 0x13, 0x6a, 0x87})),
op2: mustCreateComparison(t, linux.NFT_REG_4, linux.NFT_CMP_GTE, NewBytesData([]byte{0x0a, 0x00, 0x01, 0x20, 0x00, 0x00, 0x0f, 0x13, 0xc0, 0x09, 0x00, 0x00, 0x0b, 0x13, 0x6a, 0x87})),
res: true,
},
{
tname: "compare register >= 16-byte data, true eq",
op1: mustCreateImmediate(t, linux.NFT_REG_3, NewBytesData([]byte{0xab, 0xbc, 0xcd, 0xde, 0xef, 0x00, 0x01, 0x12, 0x23, 0x34, 0x45, 0x56, 0x67, 0x78, 0x89, 0x90})),
op2: mustCreateComparison(t, linux.NFT_REG_3, linux.NFT_CMP_GTE, NewBytesData([]byte{0xab, 0xbc, 0xcd, 0xde, 0xef, 0x00, 0x01, 0x12, 0x23, 0x34, 0x45, 0x56, 0x67, 0x78, 0x89, 0x90})),
res: true,
},
{
tname: "compare register >= 16-byte data, false",
op1: mustCreateImmediate(t, linux.NFT_REG_4, NewBytesData([]byte{0x0a, 0x00, 0x01, 0x20, 0x00, 0x00, 0x0f, 0x13, 0xc0, 0x09, 0x00, 0x00, 0x0a, 0x13, 0x6a, 0x85})),
op2: mustCreateComparison(t, linux.NFT_REG_4, linux.NFT_CMP_GTE, NewBytesData([]byte{0x0a, 0x00, 0x01, 0x20, 0x00, 0x00, 0x0f, 0x13, 0xc0, 0x09, 0x00, 0x00, 0x0b, 0x13, 0x6a, 0x87})),
res: false,
},
{
tname: "compare empty 4-byte register, true",
op1: mustCreateComparison(t, linux.NFT_REG32_10, linux.NFT_CMP_EQ, NewBytesData([]byte{0, 0, 0, 0})),
res: true,
},
{
tname: "compare empty 4-byte register, false",
op1: mustCreateComparison(t, linux.NFT_REG32_11, linux.NFT_CMP_EQ, NewBytesData([]byte{1, 0, 0, 0})),
res: false,
},
{
tname: "compare empty 16-byte register, true",
op1: mustCreateComparison(t, linux.NFT_REG_1, linux.NFT_CMP_LT, NewBytesData([]byte{1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0})),
res: true,
},
{
tname: "compare empty 16-byte register, false",
op1: mustCreateComparison(t, linux.NFT_REG_4, linux.NFT_CMP_GTE, NewBytesData([]byte{1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0})),
res: false,
},
} {
t.Run(test.tname, func(t *testing.T) {
// Sets up an NFTables object with a single table, chain, and rule.
nf := NewNFTables()
tab, err := nf.AddTable(arbitraryFamily, "test", "test table", false)
if err != nil {
t.Fatalf("unexpected error for AddTable: %v", err)
}
bc, err := tab.AddChain("base_chain", nil, "test chain", false)
if err != nil {
t.Fatalf("unexpected error for AddChain: %v", err)
}
bc.SetBaseChainInfo(arbitraryInfoPolicyAccept)
rule := &Rule{}
// Adds testing operations.
if test.op1 != nil {
rule.AddOperation(test.op1)
}
if test.op2 != nil {
rule.AddOperation(test.op2)
}
// Add an operation that drops. This is what the final verdict should be
// if all the comparisons are true (res = true).
rule.AddOperation(mustCreateImmediate(t, linux.NFT_REG_VERDICT, NewVerdictData(Verdict{Code: VC(linux.NF_DROP)})))
// Registers the rule to the base chain.
if err := bc.RegisterRule(rule, -1); err != nil {
t.Fatalf("unexpected error for RegisterRule: %v", err)
}
// Runs evaluation and checks verdict.
pkt := makeTestingPacket()
v, err := nf.EvaluateHook(arbitraryFamily, arbitraryHook, pkt)
if err != nil {
t.Fatalf("unexpected error for EvaluateHook: %v", err)
}
// If all comparisons are true, the packet will get to the end of the rule
// and the last operation above will set the final verdict to oppose the
// base chain policy. If any comparison is false, the comparison operation
// will break from the rule and the final verdict will default to the base
// chain policy.
if test.res {
if v.Code != VC(linux.NF_DROP) {
t.Fatalf("expected verdict Drop for %t result, got %v", test.res, v)
}
} else {
if v.Code != VC(linux.NF_ACCEPT) {
t.Fatalf("expected base chain policy verdict Accept for %t result, got %v", test.res, v)
}
}
})
}
}
// TestLoopCheckOnRegisterAndUnregister tests the loop checking and accompanying
// logic on registering and unregistering rules.
func TestLoopCheckOnRegisterAndUnregister(t *testing.T) {
@@ -882,3 +1160,12 @@ func mustCreateImmediate(t *testing.T, dreg uint8, data RegisterData) *Immediate
}
return imm
}
// mustCreateComparison wraps the NewComparison function for brevity.
func mustCreateComparison(t *testing.T, sreg uint8, cop int, data RegisterData) *Comparison {
cmp, err := NewComparison(sreg, cop, data)
if err != nil {
t.Fatalf("failed to create comparison: %v", err)
}
return cmp
}
+85
View File
@@ -138,6 +138,8 @@ func InterpretOperation(line string, lnIdx int) (Operation, error) {
switch tokens[1] {
case "immediate":
return InterpretImmediate(line, lnIdx)
case "cmp":
return InterpretComparison(line, lnIdx)
default:
return nil, &SyntaxError{lnIdx, 1, fmt.Sprintf("unrecognized operation type: %s", tokens[1])}
}
@@ -199,6 +201,69 @@ func InterpretImmediate(line string, lnIdx int) (Operation, error) {
return imm, nil
}
// InterpretComparison creates a new Comparison operation from the given string.
func InterpretComparison(line string, lnIdx int) (Operation, error) {
tokens := strings.Fields(line)
// Requires at least 7 tokens:
// "[", "cmp", op, "reg", register index, register value, "]".
if len(tokens) < 7 {
return nil, &SyntaxError{lnIdx, 0, fmt.Sprintf("incorrect number of tokens for cmp operation, should be at least 7, got %d", len(tokens))}
}
if err := checkOperationBrackets(tokens, lnIdx); err != nil {
return nil, err
}
tkIdx := 1
// First token should be "cmp".
if err := consumeToken("cmp", tokens, lnIdx, tkIdx); err != nil {
return nil, err
}
tkIdx++
// Second token should be the comparison operator.
cop, err := parseCmpOp(tokens[tkIdx], lnIdx, tkIdx)
if err != nil {
return nil, err
}
tkIdx++
// Third token should be "reg".
if err := consumeToken("reg", tokens, lnIdx, tkIdx); err != nil {
return nil, err
}
tkIdx++
// Fourth token should be the uint8 representing the register index.
reg, err := parseRegister(tokens[tkIdx], lnIdx, tkIdx)
if err != nil {
return nil, err
}
tkIdx++
// Fifth token should be the value.
nextIdx, data, err := parseRegisterData(reg, tokens, lnIdx, tkIdx)
if err != nil {
return nil, err
}
tkIdx = nextIdx
// Done parsing tokens.
if tkIdx != len(tokens)-1 {
return nil, &SyntaxError{lnIdx, tkIdx, "unexpected token after comparison operation"}
}
// Create the operation with the specified arguments.
cmp, err := NewComparison(reg, cop, data)
if err != nil {
return nil, &LogicError{lnIdx, tkIdx, err}
}
return cmp, nil
}
//
// Interpreter Helper Functions.
//
@@ -326,6 +391,26 @@ func parseHexData(tokens []string, lnIdx int, tkIdx int) (int, RegisterData, err
return 0, nil, &SyntaxError{lnIdx, tkIdx, fmt.Sprintf("incorrect number of bytes for hexadecimal data, should be 4 or 16, got %d", len(bytes))}
}
// parseCmpOp parses the int representing the cmpOp from the given string.
func parseCmpOp(copString string, lnIdx int, tkIdx int) (int, error) {
switch copString {
case "eq":
return linux.NFT_CMP_EQ, nil
case "neq":
return linux.NFT_CMP_NEQ, nil
case "lt":
return linux.NFT_CMP_LT, nil
case "lte":
return linux.NFT_CMP_LTE, nil
case "gt":
return linux.NFT_CMP_GT, nil
case "gte":
return linux.NFT_CMP_GTE, nil
default:
return 0, &SyntaxError{lnIdx, tkIdx, fmt.Sprintf("invalid comparison operator: '%s'", copString)}
}
}
// consumeToken is a helper function that checks if the token at the given index
// matches the expected string, returning a SyntaxError if not.
func consumeToken(expected string, tokens []string, lnIdx int, tkIdx int) error {
+257 -131
View File
@@ -21,145 +21,264 @@ import (
"gvisor.dev/gvisor/pkg/abi/linux"
)
func TestInterpretImmediateOps(t *testing.T) {
for _, test := range []struct {
tname string
opStr string
op *Immediate // will be nil if an error is expected
}{
{
tname: "verdict register with accept verdict",
opStr: "[ immediate reg 0 accept ]",
op: mustCreateImmediate(t, linux.NFT_REG_VERDICT, NewVerdictData(Verdict{Code: VC(linux.NF_ACCEPT)})),
},
{
tname: "verdict register with drop verdict",
opStr: "[ immediate reg 0 drop ]",
op: mustCreateImmediate(t, linux.NFT_REG_VERDICT, NewVerdictData(Verdict{Code: VC(linux.NF_DROP)})),
},
{
tname: "verdict register with continue verdict",
opStr: "[ immediate reg 0 continue ]",
op: mustCreateImmediate(t, linux.NFT_REG_VERDICT, NewVerdictData(Verdict{Code: VC(linux.NFT_CONTINUE)})),
},
{
tname: "verdict register with return verdict",
opStr: "[ immediate reg 0 return ]",
op: mustCreateImmediate(t, linux.NFT_REG_VERDICT, NewVerdictData(Verdict{Code: VC(linux.NFT_RETURN)})),
},
{
tname: "verdict register with jump verdict",
opStr: "[ immediate reg 0 jump -> next_chain ]",
op: mustCreateImmediate(t, linux.NFT_REG_VERDICT, NewVerdictData(Verdict{Code: VC(linux.NFT_JUMP), ChainName: "next_chain"})),
},
{
tname: "verdict register with goto verdict",
opStr: "[ immediate reg 0 goto -> next_chain ]",
op: mustCreateImmediate(t, linux.NFT_REG_VERDICT, NewVerdictData(Verdict{Code: VC(linux.NFT_GOTO), ChainName: "next_chain"})),
},
{
tname: "verdict register with 4-byte data",
opStr: "[ immediate reg 0 0x0201a8c0 ]",
op: nil,
},
{
tname: "verdict register with 16-byte data",
opStr: "[ immediate reg 0 0xb80d0120 0x00000000 0x00000000 0x02000000 ]",
op: nil,
},
{
tname: "16-byte register with verdict data",
opStr: "[ immediate reg 1 accept ]",
op: nil,
},
{
tname: "16-byte register with verdict data with target",
opStr: "[ immediate reg 2 jump -> next_chain ]",
op: nil,
},
{
tname: "16-byte register with 4-byte data",
opStr: "[ immediate reg 3 0x0201a8c0 ]",
op: mustCreateImmediate(t, linux.NFT_REG_3, NewBytesData([]byte{0x02, 0x01, 0xa8, 0xc0})),
},
{
tname: "16-byte register with 16-byte data",
opStr: "[ immediate reg 4 0xb80d0120 0x00000000 0x00000000 0x02000000 ]",
op: mustCreateImmediate(t, linux.NFT_REG_4, NewBytesData([]byte{0xb8, 0x0d, 0x01, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00})),
},
{
tname: "16-byte register with 8-byte data",
opStr: "[ immediate reg 4 0xb80d0120 0x00000050 ]",
op: nil,
},
{
tname: "4-byte register with verdict data",
opStr: "[ immediate reg 8 return ]",
op: nil,
},
{
tname: "4-byte register with verdict data with target",
opStr: "[ immediate reg 9 goto -> next_chain ]",
op: nil,
},
{
tname: "4-byte register with 4-byte data",
opStr: "[ immediate reg 10 0x0201a8c0 ]",
op: mustCreateImmediate(t, linux.NFT_REG32_02, NewBytesData([]byte{0x02, 0x01, 0xa8, 0xc0})),
},
{
tname: "4-byte register with 16-byte data",
opStr: "[ immediate reg 9 0xb80d0120 0x00000000 0x00000000 0x02000000 ]",
op: nil,
},
} {
t.Run(test.tname, func(t *testing.T) {
rule, err := InterpretRule(test.opStr)
if err != nil {
if test.op == nil {
return
}
t.Fatalf("unexpected interpretation error for %s: %v", test.tname, err)
}
// interpretOperationTestAction is a generic action for testing the
// interpretation of an operation.
type interpretOperationTestAction struct {
tname string
opStr string
expected Operation // will be nil if an error is expected
}
if len(rule.ops) != 1 {
t.Fatalf("expected single operation for %s, got %d", test.tname, len(rule.ops))
}
op := rule.ops[0]
if err := checkImmediateOp(test.tname, test.op, op); err != nil {
t.Fatalf(err.Error())
}
})
// checkOp is a generic operation validation function used for testing that
// the interpretation of an operation matches the expected operation.
func checkOp(t *testing.T, test interpretOperationTestAction, checkFunc func(string, Operation, Operation) error) {
rule, err := InterpretRule(test.opStr)
if test.expected == nil {
if err == nil {
t.Fatalf("unexpected interpretation success for %s", test.tname)
}
return
}
if err != nil {
t.Fatalf("unexpected interpretation error for %s: %v", test.tname, err)
}
if len(rule.ops) != 1 {
t.Fatalf("expected single operation for %s, got %d", test.tname, len(rule.ops))
}
actual := rule.ops[0]
if actual == nil {
t.Fatalf("expected non-nil operation for %s, got nil", test.tname)
}
if err := checkFunc(test.tname, test.expected, actual); err != nil {
t.Fatalf(err.Error())
}
}
func checkImmediateOp(tname string, expected *Immediate, actual Operation) error {
if actual == nil {
return fmt.Errorf("expected non-nil operation for %s, got nil", tname)
// TestInterpretImmediateOps tests the interpretation of immediate operations.
func TestInterpretImmediateOps(t *testing.T) {
for _, test := range []interpretOperationTestAction{
{
tname: "verdict register with accept verdict",
opStr: "[ immediate reg 0 accept ]",
expected: mustCreateImmediate(t, linux.NFT_REG_VERDICT, NewVerdictData(Verdict{Code: VC(linux.NF_ACCEPT)})),
},
{
tname: "verdict register with drop verdict",
opStr: "[ immediate reg 0 drop ]",
expected: mustCreateImmediate(t, linux.NFT_REG_VERDICT, NewVerdictData(Verdict{Code: VC(linux.NF_DROP)})),
},
{
tname: "verdict register with continue verdict",
opStr: "[ immediate reg 0 continue ]",
expected: mustCreateImmediate(t, linux.NFT_REG_VERDICT, NewVerdictData(Verdict{Code: VC(linux.NFT_CONTINUE)})),
},
{
tname: "verdict register with return verdict",
opStr: "[ immediate reg 0 return ]",
expected: mustCreateImmediate(t, linux.NFT_REG_VERDICT, NewVerdictData(Verdict{Code: VC(linux.NFT_RETURN)})),
},
{
tname: "verdict register with jump verdict",
opStr: "[ immediate reg 0 jump -> next_chain ]",
expected: mustCreateImmediate(t, linux.NFT_REG_VERDICT, NewVerdictData(Verdict{Code: VC(linux.NFT_JUMP), ChainName: "next_chain"})),
},
{
tname: "verdict register with goto verdict",
opStr: "[ immediate reg 0 goto -> next_chain ]",
expected: mustCreateImmediate(t, linux.NFT_REG_VERDICT, NewVerdictData(Verdict{Code: VC(linux.NFT_GOTO), ChainName: "next_chain"})),
},
{
tname: "verdict register with 4-byte data",
opStr: "[ immediate reg 0 0x0201a8c0 ]",
expected: nil,
},
{
tname: "verdict register with 16-byte data",
opStr: "[ immediate reg 0 0xb80d0120 0x00000000 0x00000000 0x02000000 ]",
expected: nil,
},
{
tname: "16-byte register with verdict data",
opStr: "[ immediate reg 1 accept ]",
expected: nil,
},
{
tname: "16-byte register with verdict data with target",
opStr: "[ immediate reg 2 jump -> next_chain ]",
expected: nil,
},
{
tname: "16-byte register with 4-byte data",
opStr: "[ immediate reg 3 0x0201a8c0 ]",
expected: mustCreateImmediate(t, linux.NFT_REG_3, NewBytesData([]byte{0x02, 0x01, 0xa8, 0xc0})),
},
{
tname: "16-byte register with 16-byte data",
opStr: "[ immediate reg 4 0xb80d0120 0x00000000 0x00000000 0x02000000 ]",
expected: mustCreateImmediate(t, linux.NFT_REG_4, NewBytesData([]byte{0xb8, 0x0d, 0x01, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00})),
},
{
tname: "16-byte register with 8-byte data",
opStr: "[ immediate reg 4 0xb80d0120 0x00000050 ]",
expected: nil,
},
{
tname: "4-byte register with verdict data",
opStr: "[ immediate reg 8 return ]",
expected: nil,
},
{
tname: "4-byte register with verdict data with target",
opStr: "[ immediate reg 9 goto -> next_chain ]",
expected: nil,
},
{
tname: "4-byte register with 4-byte data",
opStr: "[ immediate reg 10 0x0201a8c0 ]",
expected: mustCreateImmediate(t, linux.NFT_REG32_02, NewBytesData([]byte{0x02, 0x01, 0xa8, 0xc0})),
},
{
tname: "4-byte register with 16-byte data",
opStr: "[ immediate reg 9 0xb80d0120 0x00000000 0x00000000 0x02000000 ]",
expected: nil,
},
} {
t.Run(test.tname, func(t *testing.T) { checkOp(t, test, checkImmediateOp) })
}
}
// checkImmediateOp checks that the given operation is an immediate operation
// and that it matches the expected immediate operation.
func checkImmediateOp(tname string, expected Operation, actual Operation) error {
expectedImm := expected.(*Immediate)
imm, ok := actual.(*Immediate)
if !ok {
return fmt.Errorf("expected operation type to be Immediate for %s, got %s", tname, actual.TypeString())
return fmt.Errorf("expected operation type to be Immediate for %s, got %T", tname, actual)
}
if imm.dreg != expected.dreg {
return fmt.Errorf("expected register to be %d for %s, got %d", expected.dreg, tname, imm.dreg)
if imm.dreg != expectedImm.dreg {
return fmt.Errorf("expected register to be %d for %s, got %d", expectedImm.dreg, tname, imm.dreg)
}
if !imm.data.Equal(expected.data) {
return fmt.Errorf("expected data to be %s for %s, got %s", expected.data.String(), tname, imm.data.String())
if !imm.data.Equal(expectedImm.data) {
return fmt.Errorf("expected data to be %v for %s, got %v", expectedImm.data, tname, imm.data)
}
return nil
}
// TestInterpretComparisonOps tests the interpretation of comparison operations.
func TestInterpretComparisonOps(t *testing.T) {
for _, test := range []interpretOperationTestAction{
{
tname: "verdict register with 4-byte data comparison",
opStr: "[ cmp eq reg 0 0x00000002 ]",
expected: nil,
},
{
tname: "register == 4-byte data",
opStr: "[ cmp eq reg 1 0x0302010a ]",
expected: mustCreateComparison(t, linux.NFT_REG_1, linux.NFT_CMP_EQ, NewBytesData([]byte{0x03, 0x02, 0x01, 0x0a})),
},
{
tname: "register != 4-byte data",
opStr: "[ cmp neq reg 2 0x00000064 ]",
expected: mustCreateComparison(t, linux.NFT_REG_2, linux.NFT_CMP_NEQ, NewBytesData([]byte{0x00, 0x00, 0x00, 0x64})),
},
{
tname: "register < 4-byte data",
opStr: "[ cmp lt reg 3 0x00000000 ]",
expected: mustCreateComparison(t, linux.NFT_REG_3, linux.NFT_CMP_LT, NewBytesData([]byte{0x00, 0x00, 0x00, 0x00})),
},
{
tname: "register <= 4-byte data",
opStr: "[ cmp lte reg 4 0x00000164 ]",
expected: mustCreateComparison(t, linux.NFT_REG_4, linux.NFT_CMP_LTE, NewBytesData([]byte{0x00, 0x00, 0x01, 0x64})),
},
{
tname: "register > 4-byte data",
opStr: "[ cmp gt reg 8 0xe8030000 ]",
expected: mustCreateComparison(t, linux.NFT_REG32_00, linux.NFT_CMP_GT, NewBytesData([]byte{0xe8, 0x03, 0x00, 0x00})),
},
{
tname: "register >= 4-byte data",
opStr: "[ cmp gte reg 9 0xc02b0000 ]",
expected: mustCreateComparison(t, linux.NFT_REG32_01, linux.NFT_CMP_GTE, NewBytesData([]byte{0xc0, 0x2b, 0x00, 0x00})),
},
{
tname: "verdict register with 16-byte data comparison",
opStr: "[ cmp gt reg 0 0xb80d0120 0x00000000 0x00000000 0x02000000 ]",
expected: nil,
},
{
tname: "4-byte register with 16-byte data comparison",
opStr: "[ cmp lte reg 8 0x0302010a 0x00000000 0x00000000 0x02000001 ]",
expected: nil,
},
{
tname: "register == 16-byte data",
opStr: "[ cmp eq reg 1 0x0302010a 0x00000000 0x00000000 0x02000002 ]",
expected: mustCreateComparison(t, linux.NFT_REG_1, linux.NFT_CMP_EQ, NewBytesData([]byte{0x03, 0x02, 0x01, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02})),
},
{
tname: "register != 16-byte data",
opStr: "[ cmp neq reg 2 0x00000064 0x00000000 0x00000000 0x02000000 ]",
expected: mustCreateComparison(t, linux.NFT_REG_2, linux.NFT_CMP_NEQ, NewBytesData([]byte{0x00, 0x00, 0x00, 0x64, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00})),
},
{
tname: "register < 16-byte data",
opStr: "[ cmp lt reg 3 0x00000000 0x00000000 0x00000000 0x00000000 ]",
expected: mustCreateComparison(t, linux.NFT_REG_3, linux.NFT_CMP_LT, NewBytesData([]byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00})),
},
{
tname: "register <= 16-byte data",
opStr: "[ cmp lte reg 4 0x00000164 0x00000164 0x00000164 0x00000164 ]",
expected: mustCreateComparison(t, linux.NFT_REG_4, linux.NFT_CMP_LTE, NewBytesData([]byte{0x00, 0x00, 0x01, 0x64, 0x00, 0x00, 0x01, 0x64, 0x00, 0x00, 0x01, 0x64, 0x00, 0x00, 0x01, 0x64})),
},
{
tname: "register > 16-byte data",
opStr: "[ cmp gt reg 2 0xe8030000 0x00000f13 0xc0090000 0x0b136a87 ]",
expected: mustCreateComparison(t, linux.NFT_REG_2, linux.NFT_CMP_GT, NewBytesData([]byte{0xe8, 0x03, 0x00, 0x00, 0x00, 0x00, 0x0f, 0x13, 0xc0, 0x09, 0x00, 0x00, 0x0b, 0x13, 0x6a, 0x87})),
},
{
tname: "register >= 16-byte data",
opStr: "[ cmp gte reg 3 0x0a000120 0x00000f13 0xc0090000 0x0b136a87 ]",
expected: mustCreateComparison(t, linux.NFT_REG_3, linux.NFT_CMP_GTE, NewBytesData([]byte{0x0a, 0x00, 0x01, 0x20, 0x00, 0x00, 0x0f, 0x13, 0xc0, 0x09, 0x00, 0x00, 0x0b, 0x13, 0x6a, 0x87})),
},
} {
t.Run(test.tname, func(t *testing.T) { checkOp(t, test, checkComparisonOp) })
}
}
// checkComparisonOp checks that the given operation is an comparison operation
// and that it matches the expected comparison operation.
func checkComparisonOp(tname string, expected Operation, actual Operation) error {
expectedCmp := expected.(*Comparison)
cmp, ok := actual.(*Comparison)
if !ok {
return fmt.Errorf("expected operation type to be Comparison for %s, got %T", tname, actual)
}
if cmp.sreg != expectedCmp.sreg {
return fmt.Errorf("expected register to be %d for %s, got %d", expectedCmp.sreg, tname, cmp.sreg)
}
if cmp.cop != expectedCmp.cop {
return fmt.Errorf("expected comparison operator to be %v for %s, got %v", expectedCmp.cop, tname, cmp.cop)
}
if !cmp.data.Equal(expectedCmp.data) {
return fmt.Errorf("expected data to be %v for %s, got %v", expectedCmp.data, tname, cmp.data)
}
return nil
}
// TestInterpretRule tests the interpretation of basic and general rules as a
// list of operations.
func TestInterpretRule(t *testing.T) {
for _, test := range []struct {
tname string
ruleStr string
rule *Rule // will be nil if an error is expected
tname string
ruleStr string
expected *Rule // will be nil if an error is expected
}{
{
tname: "empty ruleset",
ruleStr: ``,
rule: &Rule{},
tname: "empty ruleset",
ruleStr: ``,
expected: &Rule{},
},
{
tname: "empty ruleset with excess whitespace",
@@ -167,33 +286,40 @@ func TestInterpretRule(t *testing.T) {
`,
rule: &Rule{},
expected: &Rule{},
},
} {
t.Run(test.tname, func(t *testing.T) {
rule, err := InterpretRule(test.ruleStr)
if err != nil {
if test.rule == nil {
return
if test.expected == nil {
if err == nil {
t.Fatalf("unexpected interpretation success for %s", test.tname)
}
return
}
if err != nil {
t.Fatalf("unexpected interpretation error for %s: %v", test.tname, err)
}
if len(rule.ops) != len(test.rule.ops) {
t.Fatalf("expected %d operations for %s, got %d", len(test.rule.ops), test.tname, len(rule.ops))
if len(rule.ops) != len(test.expected.ops) {
t.Fatalf("expected %d operations for %s, got %d", len(test.expected.ops), test.tname, len(rule.ops))
}
// Checks each operation in the rule with the appropriate check function.
for i, op := range rule.ops {
testOp := test.rule.ops[i]
testOp := test.expected.ops[i]
switch testOp.(type) {
case *Immediate:
if err := checkImmediateOp(test.tname, testOp.(*Immediate), op); err != nil {
if err := checkImmediateOp(test.tname, testOp, op); err != nil {
t.Fatalf(err.Error())
}
case *Comparison:
if err := checkComparisonOp(test.tname, testOp, op); err != nil {
t.Fatalf(err.Error())
}
// TODO(b/345684870): cases will be added here as more types are supported.
default:
t.Fatalf("unexpected operation type for %s: %s", test.tname, testOp.TypeString())
t.Fatalf("unexpected operation type for %s: %T", test.tname, testOp)
}
}
})