Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add bitmath inspector #128

Merged
merged 5 commits into from
Aug 9, 2023
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions build/config/substation.libsonnet
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
{
defaults: {
inspector: {
bitmath: {
options: { type: null, value: null },
},
content: {
options: { type: null },
},
Expand Down Expand Up @@ -209,6 +212,16 @@
},
inspector: {
settings: { key: null, negate: null },
bitmath(options=$.defaults.inspector.bitmath.options,
settings=$.interfaces.inspector.settings): {
local opt = std.mergePatch($.defaults.inspector.bitmath.options, options),

assert $.helpers.inspector.validate(settings) : 'invalid inspector settings',
local s = std.mergePatch($.interfaces.inspector.settings, settings),

type: 'bitmath',
settings: std.mergePatch({ options: opt }, s),
},
condition(options=null,
settings=$.interfaces.inspector.settings): {
assert options != null : 'invalid inspector options',
Expand Down
103 changes: 103 additions & 0 deletions condition/bitmath.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package condition

import (
"context"
"fmt"
"strconv"

"golang.org/x/exp/slices"

"github.com/brexhq/substation/config"
"github.com/brexhq/substation/internal/errors"
)

// bitmath evaluates data using bitwith math operations.
//
// This inspector supports the data and object handling patterns.
type inspBitmath struct {
condition
Options inspBitmathOptions `json:"options"`
}

type inspBitmathOptions struct {
shellcromancer marked this conversation as resolved.
Show resolved Hide resolved
// Type is the string evaluation Type used during inspection.
//
// Must be one of:
//
// - and
//
// - or
//
// - not
//
// - xor
Type string `json:"type"`
// Value is the length that is used for comparison during inspection.
Value int64 `json:"value"`
}

// Creates a new bitmath inspector.
func newInspBitmath(_ context.Context, cfg config.Config) (c inspBitmath, err error) {
if err = config.Decode(cfg.Settings, &c); err != nil {
return inspBitmath{}, err
}

// validate option.type
if !slices.Contains(
[]string{
"and",
"or",
"not",
"xor",
},
c.Options.Type) {
return inspBitmath{}, fmt.Errorf("condition: bitmath: type %q: %v", c.Options.Type, errors.ErrInvalidOption)
}

return c, nil
}

func (c inspBitmath) String() string {
return toString(c)
}

// Inspect evaluates encapsulated data with the bitmath inspector.
func (c inspBitmath) Inspect(ctx context.Context, capsule config.Capsule) (output bool, err error) {
var check int64
if c.Key == "" {
check, err = strconv.ParseInt(string(capsule.Data()), 10, 64)
if err != nil {
return false, fmt.Errorf("condition: bitmath: invalid data processing value: %v", err)
shellcromancer marked this conversation as resolved.
Show resolved Hide resolved
}
} else {
check = capsule.Get(c.Key).Int()
}

var matched bool
switch c.Options.Type {
case "and":
if check&c.Options.Value != 0 {
matched = true
}
case "or":
if check|c.Options.Value != 0 {
matched = true
}
case "not":
if ^check != 0 {
matched = true
}
case "xor":
if check^c.Options.Value != 0 {
matched = true
}
default:
return false, fmt.Errorf("condition: bitmath: type %s: %v", c.Options.Type, errors.ErrInvalidOption)
}

if c.Negate {
return !matched, nil
}

return matched, nil
}
188 changes: 188 additions & 0 deletions condition/bitmath_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
package condition

import (
"context"
"testing"

"github.com/brexhq/substation/config"
)

var _ Inspector = inspBitmath{}

var bitmathTests = []struct {
name string
cfg config.Config
test []byte
expected bool
}{
{
"pass xor",
config.Config{
Type: "bitmath",
Settings: map[string]interface{}{
"key": "foo",
"options": map[string]interface{}{
"type": "xor",
"value": 3,
},
},
},
[]byte(`{"foo":"0"}`),
true,
},
{
"fail xor",
config.Config{
Type: "bitmath",
Settings: map[string]interface{}{
"key": "foo",
"options": map[string]interface{}{
"type": "xor",
"value": 42,
},
},
},
[]byte(`{"foo":"42"}`),
false,
},
{
"!fail xor",
config.Config{
Type: "bitmath",
Settings: map[string]interface{}{
"key": "foo",
"negate": true,
"options": map[string]interface{}{
"type": "xor",
"value": 42,
},
},
},
[]byte(`{"foo":"42"}`),
true,
},
{
"pass or",
config.Config{
Type: "bitmath",
Settings: map[string]interface{}{
"key": "foo",
"options": map[string]interface{}{
"type": "or",
"value": -1,
},
},
},
[]byte(`{"foo":"0"}`),
true,
},
{
"!pass or",
config.Config{
Type: "bitmath",
Settings: map[string]interface{}{
"key": "foo",
"negate": true,
"options": map[string]interface{}{
"type": "or",
"value": 1,
},
},
},
[]byte(`{"foo":"0"}`),
false,
},
{
"pass and",
config.Config{
Type: "bitmath",
Settings: map[string]interface{}{
"key": "foo",
"options": map[string]interface{}{
"type": "and",
"value": 0x0001,
},
},
},
[]byte(`{"foo":"570506001"}`),
true,
},
{
"fail and",
config.Config{
Type: "bitmath",
Settings: map[string]interface{}{
"key": "foo",
"options": map[string]interface{}{
"type": "and",
"value": 0x0002,
},
},
},
[]byte(`{"foo":"570506001"}`),
false,
},
{
"pass data",
config.Config{
Type: "bitmath",
Settings: map[string]interface{}{
"options": map[string]interface{}{
"type": "or",
"value": 1,
},
},
},
[]byte(`0001`),
true,
},
}

func TestBitmath(t *testing.T) {
ctx := context.TODO()
capsule := config.NewCapsule()

for _, test := range bitmathTests {
t.Run(test.name, func(t *testing.T) {
capsule.SetData(test.test)

insp, err := newInspBitmath(ctx, test.cfg)
if err != nil {
t.Fatal(err)
}

check, err := insp.Inspect(ctx, capsule)
if err != nil {
t.Error(err)
}

if test.expected != check {
t.Errorf("expected %v, got %v", test.expected, check)
}
})
}
}

func benchmarkBitmathByte(b *testing.B, inspector inspBitmath, capsule config.Capsule) {
ctx := context.TODO()
for i := 0; i < b.N; i++ {
_, _ = inspector.Inspect(ctx, capsule)
}
}

func BenchmarkBitmathByte(b *testing.B) {
capsule := config.NewCapsule()
for _, test := range bitmathTests {
insp, err := newInspBitmath(context.TODO(), test.cfg)
if err != nil {
b.Fatal(err)
}

b.Run(test.name,
func(b *testing.B) {
capsule.SetData(test.test)
benchmarkBitmathByte(b, insp, capsule)
},
)
}
}
2 changes: 2 additions & 0 deletions condition/condition.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ type Inspector interface {
// NewInspector returns a configured Inspector from an Inspector configuration.
func NewInspector(ctx context.Context, cfg config.Config) (Inspector, error) {
switch cfg.Type {
case "bitmath":
return newInspBitmath(ctx, cfg)
case "condition":
return newInspCondition(ctx, cfg)
case "content":
Expand Down
Loading