Skip to content

Commit 5bb9b81

Browse files
committed
cvss: implement encoding.TextAppender
This implements the new interface, redefines `encoding.TextMarshaler` using it, and adds benchmarks. The goal is to have `AppendText` do no unavoidable allocations, meaning that if the input slice has sufficient capacity there are no additional heap allocations to populate it. Signed-off-by: Hank Donnay <hdonnay@redhat.com> Change-Id: I50157434eb16073a14edb2b4bec253966a6a6964
1 parent 3d17511 commit 5bb9b81

6 files changed

Lines changed: 379 additions & 113 deletions

File tree

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
package cvss
2+
3+
import (
4+
"math/rand/v2"
5+
"testing"
6+
)
7+
8+
func BenchmarkAppend(b *testing.B) {
9+
b.Run("V2", benchAppendV2)
10+
b.Run("V3", benchAppendV3)
11+
b.Run("V4", benchAppendV4)
12+
}
13+
14+
func benchAppendV4(b *testing.B) {
15+
buf := make([]byte, 0, 1024) // Is it cheating to oversize this?
16+
benchOne := func(b *testing.B, vec []byte) {
17+
b.Helper()
18+
b.Attr("input", string(vec))
19+
var v V4
20+
if err := v.UnmarshalText(vec); err != nil {
21+
b.Fatal(err)
22+
}
23+
var err error
24+
var x []byte
25+
b.ReportAllocs()
26+
27+
for b.Loop() {
28+
x, err = v.AppendText(buf)
29+
if err != nil {
30+
b.Error(err)
31+
}
32+
_ = x
33+
}
34+
}
35+
36+
b.Run("List", func(b *testing.B) {
37+
vecs := loadVectorList(b, `v4_roundtrip.list`)
38+
todo := make([][]byte, 10)
39+
for i := range todo {
40+
todo[i] = vecs[rand.N(len(vecs))]
41+
}
42+
for _, vec := range todo {
43+
b.Run("", func(b *testing.B) { benchOne(b, vec) })
44+
}
45+
})
46+
// Each of the following test one fixture plucked from the Spec's examples.
47+
b.Run("B", func(b *testing.B) {
48+
benchOne(b, []byte("CVSS:4.0/AV:A/AC:H/AT:P/PR:L/UI:P/VC:H/VI:H/VA:H/SC:L/SI:L/SA:L"))
49+
})
50+
b.Run("BT", func(b *testing.B) {
51+
benchOne(b, []byte("CVSS:4.0/AV:A/AC:H/AT:P/PR:L/UI:P/VC:H/VI:H/VA:H/SC:L/SI:L/SA:L/E:P"))
52+
})
53+
b.Run("BE", func(b *testing.B) {
54+
benchOne(b, []byte("CVSS:4.0/AV:L/AC:H/AT:N/PR:N/UI:A/VC:N/VI:N/VA:L/SC:H/SI:H/SA:H/CR:H/IR:H/AR:M/MAV:N/MAC:L/MAT:P/MPR:L/MUI:A/MVC:N/MVI:H/MVA:L/MSC:L/MSI:S/MSA:H"))
55+
})
56+
b.Run("BTES", func(b *testing.B) {
57+
benchOne(b, []byte("CVSS:4.0/AV:N/AC:L/AT:N/PR:H/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N/E:U/CR:L/IR:X/AR:L/MAV:A/MAC:H/MAT:N/MPR:N/MUI:P/MVC:X/MVI:N/MVA:H/MSC:N/MSI:L/MSA:S/S:N/AU:N/R:I/V:C/RE:H/U:Green"))
58+
})
59+
}
60+
61+
func benchAppendV3(b *testing.B) {
62+
buf := make([]byte, 0, 1024) // Is it cheating to oversize this?
63+
benchOne := func(b *testing.B, vec []byte) {
64+
b.Helper()
65+
b.Attr("input", string(vec))
66+
var v V3
67+
if err := v.UnmarshalText(vec); err != nil {
68+
b.Fatal(err)
69+
}
70+
var err error
71+
var x []byte
72+
b.ReportAllocs()
73+
74+
for b.Loop() {
75+
x, err = v.AppendText(buf)
76+
if err != nil {
77+
b.Error(err)
78+
}
79+
_ = x
80+
}
81+
}
82+
83+
b.Run("List", func(b *testing.B) {
84+
vecs := loadVectorList(b, `v31_score.list`)
85+
todo := make([][]byte, 10)
86+
for i := range todo {
87+
todo[i] = vecs[rand.N(len(vecs))]
88+
}
89+
for _, vec := range todo {
90+
b.Run("", func(b *testing.B) { benchOne(b, vec) })
91+
}
92+
})
93+
b.Run("Heartbleed", func(b *testing.B) {
94+
benchOne(b, []byte("CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N"))
95+
})
96+
}
97+
98+
func benchAppendV2(b *testing.B) {
99+
buf := make([]byte, 0, 1024) // Is it cheating to oversize this?
100+
benchOne := func(b *testing.B, vec []byte) {
101+
b.Helper()
102+
b.Attr("input", string(vec))
103+
var v V2
104+
if err := v.UnmarshalText(vec); err != nil {
105+
b.Fatal(err)
106+
}
107+
var err error
108+
var x []byte
109+
b.ReportAllocs()
110+
111+
for b.Loop() {
112+
x, err = v.AppendText(buf)
113+
if err != nil {
114+
b.Error(err)
115+
}
116+
_ = x
117+
}
118+
}
119+
120+
b.Run("List", func(b *testing.B) {
121+
vecs := loadVectorList(b, `v2_score.list`)
122+
todo := make([][]byte, 10)
123+
for i := range todo {
124+
todo[i] = vecs[rand.N(len(vecs))]
125+
}
126+
for _, vec := range todo {
127+
b.Run("", func(b *testing.B) { benchOne(b, vec) })
128+
}
129+
})
130+
b.Run("Heartbleed", func(b *testing.B) {
131+
benchOne(b, []byte("AV:N/AC:L/Au:N/C:P/I:N/A:N"))
132+
})
133+
}

toolkit/types/cvss/cvss.go

Lines changed: 65 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import (
3838
"encoding"
3939
"errors"
4040
"fmt"
41+
"slices"
4142
"strings"
4243
)
4344

@@ -59,10 +60,21 @@ var internalDoc = struct{}{}
5960
// ErrMalformedVector is reported when a vector is invalid in some way.
6061
var ErrMalformedVector = errors.New("malformed vector")
6162

62-
// ErrValueUnset is used by [Vector.getString] implementations to signal a
63-
// metric's value is unset.
63+
// ErrValueUnset is used by [Vector] implementations to signal a metric's value
64+
// is unset.
6465
var errValueUnset = errors.New("unset")
6566

67+
// ErrValueDefault is used by [Vector] implementations to signal a metric's value
68+
// is unset, but a default value was used for the requested operation.
69+
var errValueDefault = fmt.Errorf("default: %w", errValueUnset)
70+
71+
// MarshalSize is the initial size of the backing slice for
72+
// [encoding.TextMarshaler] implementations.
73+
//
74+
// This was arrived at by trying sizes until [BenchmarkMarshal] reported a
75+
// single allocation for all but the longest V4 vectors.
76+
const marshalSize = 128
77+
6678
// Value is a "packed" representation of the value of a metric.
6779
//
6880
// When possible, this is the first byte of the abbreviated form in the relevant
@@ -107,55 +119,62 @@ func Version(vec string) (v int) {
107119
return v
108120
}
109121

110-
// MarshalVector is a generic function to marshal vectors.
111-
//
112-
// The [Vector.getString] method is used here.
113-
func marshalVector[M Metric, V Vector[M]](prefix string, v V) ([]byte, error) {
114-
text := append(make([]byte, 0, 64), prefix...) // Guess at an initial capacity.
122+
// AppendVector is a generic function to marshal vectors via appending to the
123+
// provided byte slice.
124+
func appendVector[M Metric, V Vector[M]](b []byte, prefix string, v V) ([]byte, error) {
125+
start := len(b)
126+
b = append(b, prefix...)
115127
var err error
116-
// This is a rangefunc-style iterator.
117-
v.groups(func(b [2]int) bool {
128+
meta := v.meta()
129+
g := meta.Groups
130+
for s, e := 0, 1; e < len(g); s, e = s+2, e+2 {
118131
var set bool
119-
orig := len(text)
120-
for i := b[0]; i < b[1]; i++ {
132+
i, lim := g[s], g[e]
133+
skipGroup := len(b)
134+
for ; i < lim; i++ {
135+
skipMetric := len(b)
121136
m := M(i)
122-
val, err := v.getString(m)
137+
138+
b = append(b, '/')
139+
b, err = m.AppendText(b)
140+
if err != nil {
141+
return nil, fmt.Errorf("invalid cvss vector: %w", err)
142+
}
143+
b = append(b, ':')
144+
145+
b, err = v.appendValue(b, m)
123146
switch {
124147
case errors.Is(err, nil):
125148
set = true
126-
case errors.Is(err, errValueUnset) && val == "":
127-
continue
149+
case errors.Is(err, errValueDefault):
128150
case errors.Is(err, errValueUnset):
151+
b = b[:skipMetric]
129152
default:
130-
err = errors.New("invalid cvss vector")
131-
return false
153+
return nil, fmt.Errorf("invalid cvss vector: %w", err)
132154
}
133-
134-
text = append(text, '/')
135-
text = append(text, m.String()...)
136-
text = append(text, ':')
137-
text = append(text, val...)
138155
}
139156
if !set {
140-
text = text[:orig]
157+
b = b[:skipGroup]
141158
}
142-
return true
143-
})
144-
if err != nil {
145-
return nil, err
146159
}
147-
// v2 hack
148-
if prefix == "" {
149-
text = text[1:]
160+
// v2 hack: remove the leading slash.
161+
switch {
162+
case prefix == "" && start == 0:
163+
b = b[1:]
164+
case prefix == "" && start != 0:
165+
// Handle the case where this function was passed a slice with a
166+
// non-zero length.
167+
b = slices.Delete(b, start, start+1)
150168
}
151-
return text, nil
169+
return b, nil
152170
}
153171

154172
// Metric is a CVSS metric.
155173
//
156174
// The set of types this describes is namespaced per-version.
157175
type Metric interface {
158176
~int
177+
encoding.TextAppender
159178
fmt.Stringer
160179

161180
// Valid returns the concatenation of valid values for the metric.
@@ -166,6 +185,7 @@ type Metric interface {
166185

167186
// Vector is a CVSS vector of any version.
168187
type Vector[M Metric] interface {
188+
encoding.TextAppender
169189
encoding.TextUnmarshaler
170190
encoding.TextMarshaler
171191
fmt.Stringer
@@ -181,20 +201,16 @@ type Vector[M Metric] interface {
181201
// Environmental reports if the vector contains environmental metrics.
182202
Environmental() bool
183203

184-
// GetString is a hook for returning the stringified version of the metric
185-
// value. If the value is unset, implementations should return err
186-
// [errValueUnset] rather than a specified default, as defaults are omitted
187-
// from the string representation.
188-
//
189-
// CVSSv2 notably does not use names that are identifiable by a single byte,
190-
// so they need to be packed and unpacked.
191-
getString(M) (string, error)
204+
// AppendValue is a hook for appending the stringified version of the metric
205+
// value. If the value is unset, implementations should return the input
206+
// slice and err == [errValueUnset] rather than a specified default, as
207+
// defaults are omitted from the string representation.
208+
appendValue([]byte, M) ([]byte, error)
192209
// GetScore returns the "packed" value representation after any default
193210
// rules are applied.
194211
getScore(M) byte
195-
// Groups is a rangefunc-style iterator returning the bounds for groups of metrics.
196-
// For a returned value "b", it represents the interval "[b[0], b[1])".
197-
groups(func([2]int) bool)
212+
// Meta returns the static metadata for this vector.
213+
meta() *vectorMetadata
198214
}
199215

200216
var (
@@ -203,6 +219,14 @@ var (
203219
_ Vector[V2Metric] = (*V2)(nil)
204220
)
205221

222+
// VectorMetadata is static metadata about a vector.
223+
type vectorMetadata struct {
224+
// Groups is a slice of boundaries for the groups of the vector.
225+
//
226+
// The pairs of ints are [lower, upper).
227+
Groups []int
228+
}
229+
206230
// Qualitative is the "Qualitative Severity" of a Vector.
207231
type Qualitative int
208232

0 commit comments

Comments
 (0)