Skip to content

Commit 578a186

Browse files
marc-gremilioalvap
andauthored
[osquerybeat] Implement encoding package in extension (#47169)
* feat: Implement encoding package with MarshalToMap and EncodingFlag support * fix: Update EncodingFlag constants and enhance tests for zero value handling * fix: Enhance float handling in convertValueToString to respect zero value flag * fix: Remove unused EncodingFlagParseUnexported and related test cases --------- Co-authored-by: Emilio Alvarez Piñeiro <[email protected]>
1 parent c1ed885 commit 578a186

File tree

2 files changed

+368
-0
lines changed

2 files changed

+368
-0
lines changed
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
2+
// or more contributor license agreements. Licensed under the Elastic License;
3+
// you may not use this file except in compliance with the Elastic License.
4+
5+
package encoding
6+
7+
import (
8+
"fmt"
9+
"reflect"
10+
"strconv"
11+
)
12+
13+
type EncodingFlag int
14+
15+
const (
16+
// EncodingFlagUseNumbersZeroValues forces numeric zero values to be rendered as "0"
17+
// instead of empty strings. By default, zero values for int, uint, and float types
18+
// are converted to empty strings, but this flag preserves them as "0".
19+
EncodingFlagUseNumbersZeroValues EncodingFlag = 1 << iota
20+
)
21+
22+
func (f EncodingFlag) has(option EncodingFlag) bool {
23+
return f&option != 0
24+
}
25+
26+
// MarshalToMap converts a struct, a single-level map (like map[string]string
27+
// or map[string]any), or a pointer to these, into a map[string]string.
28+
// It prioritizes the "osquery" tag for struct fields.
29+
func MarshalToMap(in any) (map[string]string, error) {
30+
return MarshalToMapWithFlags(in, 0)
31+
}
32+
33+
func MarshalToMapWithFlags(in any, flags EncodingFlag) (map[string]string, error) {
34+
if in == nil {
35+
return nil, fmt.Errorf("input cannot be nil")
36+
}
37+
result := make(map[string]string)
38+
39+
v := reflect.ValueOf(in)
40+
t := reflect.TypeOf(in)
41+
42+
if v.Kind() == reflect.Ptr {
43+
if v.IsNil() {
44+
return nil, fmt.Errorf("input pointer is nil")
45+
}
46+
v = v.Elem()
47+
t = t.Elem()
48+
}
49+
50+
if v.Kind() == reflect.Map {
51+
if t.Key().Kind() != reflect.String {
52+
return nil, fmt.Errorf("map keys must be strings, got %s", t.Key().Kind())
53+
}
54+
55+
for _, k := range v.MapKeys() {
56+
key := k.String()
57+
fieldValue := v.MapIndex(k)
58+
59+
value, err := convertValueToString(fieldValue, flags)
60+
if err != nil {
61+
return nil, fmt.Errorf("failed to convert field %s: %w", key, err)
62+
}
63+
result[key] = value
64+
}
65+
return result, nil
66+
}
67+
68+
if v.Kind() != reflect.Struct {
69+
return nil, fmt.Errorf("unsupported type: %s, must be a struct, map, or pointer to one of them", v.Kind())
70+
}
71+
72+
for i := 0; i < v.NumField(); i++ {
73+
fieldValue := v.Field(i)
74+
fieldType := t.Field(i)
75+
76+
if !fieldType.IsExported() {
77+
continue
78+
}
79+
80+
key := fieldType.Tag.Get("osquery")
81+
switch key {
82+
case "-":
83+
continue
84+
case "":
85+
key = fieldType.Name
86+
}
87+
88+
value, err := convertValueToString(fieldValue, flags)
89+
if err != nil {
90+
return nil, fmt.Errorf("failed to convert field %s: %w", key, err)
91+
}
92+
93+
result[key] = value
94+
}
95+
96+
return result, nil
97+
}
98+
99+
func convertValueToString(fieldValue reflect.Value, flag EncodingFlag) (string, error) {
100+
// Handle pointers first
101+
if fieldValue.Kind() == reflect.Ptr {
102+
if fieldValue.IsNil() {
103+
return "", nil
104+
}
105+
return convertValueToString(fieldValue.Elem(), flag)
106+
}
107+
108+
switch fieldValue.Kind() {
109+
case reflect.String:
110+
return fieldValue.String(), nil
111+
112+
case reflect.Bool:
113+
// osquery often expects boolean values as "0" or "1"
114+
if fieldValue.Bool() {
115+
return "1", nil
116+
}
117+
return "0", nil
118+
119+
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
120+
// osquery often expects empty string for 0 values unless necessary
121+
val := fieldValue.Int()
122+
if !flag.has(EncodingFlagUseNumbersZeroValues) && val == 0 {
123+
return "", nil
124+
}
125+
return strconv.FormatInt(val, 10), nil
126+
127+
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
128+
val := fieldValue.Uint()
129+
if !flag.has(EncodingFlagUseNumbersZeroValues) && val == 0 {
130+
return "", nil
131+
}
132+
return strconv.FormatUint(val, 10), nil
133+
134+
case reflect.Float32:
135+
val := fieldValue.Float()
136+
if !flag.has(EncodingFlagUseNumbersZeroValues) && val == 0 {
137+
return "", nil
138+
}
139+
// Use -1 for precision to format the smallest number of digits necessary
140+
return strconv.FormatFloat(val, 'f', -1, 32), nil
141+
case reflect.Float64:
142+
val := fieldValue.Float()
143+
if !flag.has(EncodingFlagUseNumbersZeroValues) && val == 0 {
144+
return "", nil
145+
}
146+
return strconv.FormatFloat(val, 'f', -1, 64), nil
147+
148+
// Default: use Sprintf for unsupported types
149+
default:
150+
if fieldValue.CanInterface() {
151+
return fmt.Sprintf("%v", fieldValue.Interface()), nil
152+
}
153+
return "", fmt.Errorf("unsupported type (%s)", fieldValue.Kind())
154+
}
155+
}
Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
2+
// or more contributor license agreements. Licensed under the Elastic License;
3+
// you may not use this file except in compliance with the Elastic License.
4+
5+
package encoding
6+
7+
import (
8+
"reflect"
9+
"testing"
10+
)
11+
12+
func TestEncodingFlagHas(t *testing.T) {
13+
tests := []struct {
14+
flag EncodingFlag
15+
option EncodingFlag
16+
expected bool
17+
}{
18+
{EncodingFlagUseNumbersZeroValues, EncodingFlagUseNumbersZeroValues, true},
19+
}
20+
21+
for _, test := range tests {
22+
result := test.flag.has(test.option)
23+
if result != test.expected {
24+
t.Errorf("has(%v) = %v; expected %v", test.option, result, test.expected)
25+
}
26+
}
27+
}
28+
29+
func TestMarshalToMapWithFlags(t *testing.T) {
30+
tests := []struct {
31+
input any
32+
flags EncodingFlag
33+
expected map[string]string
34+
err bool
35+
}{
36+
{
37+
input: nil,
38+
flags: 0,
39+
expected: nil,
40+
err: true,
41+
},
42+
{
43+
input: &struct {
44+
Name string `osquery:"name"`
45+
}{Name: "test"},
46+
flags: 0,
47+
expected: map[string]string{"name": "test"},
48+
err: false,
49+
},
50+
{
51+
input: map[string]any{"key1": "value1", "key2": "value2", "key3": 1},
52+
flags: 0,
53+
expected: map[string]string{"key1": "value1", "key2": "value2", "key3": "1"},
54+
err: false,
55+
},
56+
{
57+
input: &struct {
58+
HiddenField int `osquery:"-"`
59+
}{HiddenField: 42},
60+
flags: 0,
61+
expected: map[string]string{},
62+
err: false,
63+
},
64+
{
65+
input: &struct {
66+
InvalidType map[int]string
67+
}{InvalidType: map[int]string{1: "value"}},
68+
flags: 0,
69+
expected: map[string]string{"InvalidType": "map[1:value]"},
70+
err: false,
71+
},
72+
{
73+
input: &struct {
74+
ZeroVal int
75+
}{ZeroVal: 0},
76+
flags: 0,
77+
expected: map[string]string{"ZeroVal": ""},
78+
err: false,
79+
},
80+
{
81+
input: &struct {
82+
ZeroVal int
83+
}{ZeroVal: 0},
84+
flags: EncodingFlagUseNumbersZeroValues,
85+
expected: map[string]string{"ZeroVal": "0"},
86+
err: false,
87+
},
88+
// Test bool type
89+
{
90+
input: &struct {
91+
IsActive bool
92+
}{IsActive: true},
93+
flags: 0,
94+
expected: map[string]string{"IsActive": "1"},
95+
err: false,
96+
},
97+
{
98+
input: &struct {
99+
IsActive bool
100+
}{IsActive: false},
101+
flags: 0,
102+
expected: map[string]string{"IsActive": "0"},
103+
err: false,
104+
},
105+
// Test uint type
106+
{
107+
input: &struct {
108+
Count uint
109+
}{Count: 42},
110+
flags: 0,
111+
expected: map[string]string{"Count": "42"},
112+
err: false,
113+
},
114+
{
115+
input: &struct {
116+
Count uint
117+
}{Count: 0},
118+
flags: 0,
119+
expected: map[string]string{"Count": ""},
120+
err: false,
121+
},
122+
{
123+
input: &struct {
124+
Count uint
125+
}{Count: 0},
126+
flags: EncodingFlagUseNumbersZeroValues,
127+
expected: map[string]string{"Count": "0"},
128+
err: false,
129+
},
130+
// Test float type
131+
{
132+
input: &struct {
133+
Price float64
134+
}{Price: 99.99},
135+
flags: 0,
136+
expected: map[string]string{"Price": "99.99"},
137+
err: false,
138+
},
139+
{
140+
input: &struct {
141+
Price float32
142+
}{Price: 12.5},
143+
flags: 0,
144+
expected: map[string]string{"Price": "12.5"},
145+
err: false,
146+
},
147+
// Test non-pointer struct
148+
{
149+
input: struct {
150+
Name string
151+
}{Name: "test"},
152+
flags: 0,
153+
expected: map[string]string{"Name": "test"},
154+
err: false,
155+
},
156+
// Test pointer maps
157+
{
158+
input: &map[string]string{
159+
"key1": "value1",
160+
"key2": "value2",
161+
},
162+
flags: 0,
163+
expected: map[string]string{"key1": "value1", "key2": "value2"},
164+
err: false,
165+
},
166+
// Test pointer fields
167+
{
168+
input: &struct {
169+
StrPtr *string
170+
IntPtr *int
171+
}{
172+
StrPtr: stringPtr("hello"),
173+
IntPtr: intPtr(123),
174+
},
175+
flags: 0,
176+
expected: map[string]string{"StrPtr": "hello", "IntPtr": "123"},
177+
err: false,
178+
},
179+
// Test nil pointer fields
180+
{
181+
input: &struct {
182+
StrPtr *string
183+
IntPtr *int
184+
}{
185+
StrPtr: nil,
186+
IntPtr: nil,
187+
},
188+
flags: 0,
189+
expected: map[string]string{"StrPtr": "", "IntPtr": ""},
190+
err: false,
191+
},
192+
}
193+
194+
for _, test := range tests {
195+
result, err := MarshalToMapWithFlags(test.input, test.flags)
196+
if (err != nil) != test.err {
197+
t.Errorf("MarshalToMapWithFlags(%v, %v) error = %v; expected error = %v", test.input, test.flags, err, test.err)
198+
continue
199+
}
200+
if !reflect.DeepEqual(result, test.expected) {
201+
t.Errorf("MarshalToMapWithFlags(%v, %v) = %v; expected %v", test.input, test.flags, result, test.expected)
202+
}
203+
}
204+
}
205+
206+
// Helper functions for creating pointers
207+
func stringPtr(s string) *string {
208+
return &s
209+
}
210+
211+
func intPtr(i int) *int {
212+
return &i
213+
}

0 commit comments

Comments
 (0)