Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
6 changes: 3 additions & 3 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@ jobs:
steps:

- name: Check out code into the Go module directory
uses: actions/checkout@v4
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

- name: Set up Go
uses: actions/setup-go@v5
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
with:
go-version-file: go.mod

Expand All @@ -32,7 +32,7 @@ jobs:
RICHGO_FORCE_COLOR: 1

- name: golangci-lint
uses: golangci/golangci-lint-action@v7
uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9 # v8.0.0
with:
version: v2.1
args: --issues-exit-code=1 --timeout 10m
Expand Down
8 changes: 6 additions & 2 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ linters:
- inamedparam # reports interfaces with unnamed method parameters
- wrapcheck # Checks that errors returned from external packages are wrapped
- err113 # Go linter to check the errors handling expressions
#- noinlineerr
- paralleltest # Detects missing usage of t.Parallel() method in your Go test
- testpackage # linter that makes you use a separate _test package
- exhaustruct # Checks if all structure fields are initialized
Expand All @@ -27,21 +28,22 @@ linters:
- gocognit # revive
- gocyclo # revive
- lll # revive
- wsl # wsl_v5

#
# Formatting only, useful in IDE but should not be forced on CI?
#

- nlreturn # nlreturn checks for a new line before return and branch statements to increase code clarity
- wsl # add or remove empty lines
#- wsl_v5 # add or remove empty lines

settings:

depguard:
rules:
yaml:
files:
- 'yamlpatch/patcher.go'
- '!**/yamlpatch/patcher.go'
deny:
- pkg: gopkg.in/yaml.v2
desc: yaml.v2 is deprecated for new code in favor of yaml.v3
Expand Down Expand Up @@ -102,6 +104,8 @@ linters:
- 43
- name: defer
disabled: true
#- name: enforce-switch-style
# disabled: true
- name: flag-parameter
disabled: true
- name: function-length
Expand Down
44 changes: 44 additions & 0 deletions csyaml/empty.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package csyaml

import (
"errors"
"io"

yaml "github.com/goccy/go-yaml"
"github.com/goccy/go-yaml/parser"
)

// IsEmptyYAML reads one or more YAML documents from r and returns true
// if they are all empty or contain only comments.
// It will reports errors if the input is not valid YAML.
func IsEmptyYAML(r io.Reader) (bool, error) {
src, err := io.ReadAll(r)
if err != nil {
return false, err
}

if len(src) == 0 {
return true, nil
}

file, err := parser.ParseBytes(src, 0)
if err != nil {
if errors.Is(err, io.EOF) {
return true, nil
}

return false, errors.New(yaml.FormatError(err, false, false))
}

if file == nil || len(file.Docs) == 0 {
return true, nil
}

for _, doc := range file.Docs {
if doc.Body != nil {
return false, nil
}
}

return true, nil
}
114 changes: 114 additions & 0 deletions csyaml/empty_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package csyaml

import (
"strings"
"testing"

"github.com/stretchr/testify/assert"

"github.com/crowdsecurity/go-cs-lib/cstest" // adjust this import to your package
)

func TestIsEmptyYAML(t *testing.T) {
tests := []struct {
name string
input string
want bool
wantErr string
}{
{
name: "empty document",
input: ``,
want: true,
},
{
name: "just a key",
input: "foo:",
want: false,
},
{
name: "just newline",
input: "\n",
want: true,
},
{
name: "just comment",
input: "# only a comment",
want: true,
},
{
name: "comments and empty lines",
input: "# only a comment\n\n# another one\n\n",
want: true,
},
{
name: "empty doc with separator",
input: "---",
want: true,
},
{
name: "empty mapping",
input: "{}",
want: false,
},
{
name: "empty sequence",
input: "[]",
want: false,
},
{
name: "non-empty mapping",
input: "foo: bar",
want: false,
},
{
name: "non-empty sequence",
input: "- 1\n- 2",
want: false,
},
{
name: "non-empty scalar",
input: "hello",
want: false,
},
{
name: "empty scalar",
input: "''",
want: false,
},
{
name: "explicit nil",
input: "null",
want: false,
},
{
name: "malformed YAML",
input: "foo: [1,",
wantErr: "[1:6] sequence end token ']' not found",
},
{
name: "multiple empty documents",
input: "---\n---\n---\n#comment",
want: true,
},
{
name: "second document is not empty",
input: "---\nfoo: bar\n---\n#comment",
want: false,
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
got, err := IsEmptyYAML(strings.NewReader(tc.input))

cstest.RequireErrorContains(t, err, tc.wantErr)

if tc.wantErr != "" {
return
}

assert.Equal(t, tc.want, got)
})
}
}
54 changes: 54 additions & 0 deletions csyaml/keys.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package csyaml

import (
"errors"
"fmt"
"io"

"github.com/goccy/go-yaml"
)

// GetDocumentKeys reads all YAML documents from r and for each one
// returns a slice of its top-level keys, in order.
//
// Non-mapping documents yield an empty slice. Duplicate keys
// are not allowed and return an error.
func GetDocumentKeys(r io.Reader) ([][]string, error) {
// Decode into Go types, but force mappings into MapSlice
dec := yaml.NewDecoder(r, yaml.UseOrderedMap())

allKeys := make([][]string, 0)

idx := -1

for {
var raw any

idx++

if err := dec.Decode(&raw); err != nil {
if errors.Is(err, io.EOF) {
break
}
return nil, fmt.Errorf("position %d: %s", idx, yaml.FormatError(err, false, false))
}
keys := []string{}

// Only mapping nodes become MapSlice with UseOrderedMap()
if ms, ok := raw.(yaml.MapSlice); ok {
for _, item := range ms {
// Key is interface{}—here we expect strings
if ks, ok := item.Key.(string); ok {
keys = append(keys, ks)
} else {
// fallback to string form of whatever it is
keys = append(keys, fmt.Sprint(item.Key))
}
}
}

allKeys = append(allKeys, keys)
}

return allKeys, nil
}
67 changes: 67 additions & 0 deletions csyaml/keys_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package csyaml_test

import (
"strings"
"testing"

"github.com/stretchr/testify/assert"

"github.com/crowdsecurity/go-cs-lib/cstest"
"github.com/crowdsecurity/go-cs-lib/csyaml"
)

func TestCollectTopLevelKeys(t *testing.T) {
tests := []struct {
name string
input string
want [][]string
wantErr string
}{
{
name: "single mapping",
input: "a: 1\nb: 2\n",
want: [][]string{{"a", "b"}},
},
{
name: "duplicate keys mapping",
input: "a: 1\nb: 2\na: 3\n",
want: nil,
wantErr: `position 0: [3:1] mapping key "a" already defined at [1:1]`,
},
{
name: "multiple documents",
input: `---
a: 1
b: 2
---
- 1
---
c: 1
b: 2
---
"scalar"
`,
want: [][]string{{"a", "b"}, {}, {"c", "b"}, {}},
},
{
name: "empty input",
input: "",
want: [][]string{},
},
{
name: "invalid YAML",
input: "list: [1, 2,",
want: nil,
wantErr: "position 0: [1:7] sequence end token ']' not found",
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
r := strings.NewReader(tc.input)
got, err := csyaml.GetDocumentKeys(r)
cstest.RequireErrorContains(t, err, tc.wantErr)
assert.Equal(t, tc.want, got)
})
}
}
Loading