Skip to content
Draft
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
84 changes: 84 additions & 0 deletions toolkit/types/cpe/bench_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package cpe

import (
"testing"
)

const (
// Test string where all value strings can be copied directly.
cpeFS = `cpe:2.3:a:foo\\bar:big\$money_2010:*:*:*:*:special:ipod_touch:80gb:*`
// Test string that needs additional escaping for the unbound form.
cpeEscapeFS = `cpe:2.3:a:hp:insight_diagnostics:7.4.0.1570:-:*:*:online:win2003:x64:*`
)

func BenchmarkUnbindFS(b *testing.B) {
inner := func(in string) func(*testing.B) {
return func(b *testing.B) {
b.ReportAllocs()
for b.Loop() {
out, err := UnbindFS(in)
if err != nil {
b.Error(err)
}
_ = out
}
}
}
b.Run("Simple", inner(cpeFS))
b.Run("Escape", inner(cpeEscapeFS))
}

func BenchmarkUnmarshalFS(b *testing.B) {
inner := func(in string) func(*testing.B) {
return func(b *testing.B) {
b.ReportAllocs()
for b.Loop() {
var out WFN
if err := out.UnmarshalFS(in); err != nil {
b.Error(err)
}
}
}
}
b.Run("Simple", inner(cpeFS))
b.Run("Escape", inner(cpeEscapeFS))
}

func BenchmarkBindFS(b *testing.B) {
inner := func(in string) func(*testing.B) {
return func(b *testing.B) {
var out WFN
if err := out.UnmarshalFS(in); err != nil {
b.Fatal(err)
}
b.ReportAllocs()
for b.Loop() {
s := out.BindFS()
_ = s
}
}
}
b.Run("Simple", inner(cpeFS))
b.Run("Escape", inner(cpeEscapeFS))
}

func BenchmarkAppendText(b *testing.B) {
inner := func(in string) func(*testing.B) {
return func(b *testing.B) {
var out WFN
if err := out.UnmarshalFS(in); err != nil {
b.Fatal(err)
}
b.ReportAllocs()
// Is it cheating to make this properly sized? Dunno.
tmp := make([]byte, 0, len(in))
for b.Loop() {
if _, err := out.AppendText(tmp); err != nil {
b.Error(err)
}
}
}
}
b.Run("Simple", inner(cpeFS))
b.Run("Escape", inner(cpeEscapeFS))
}
65 changes: 63 additions & 2 deletions toolkit/types/cpe/bind.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
package cpe

import "strings"
import (
"encoding"
"errors"
"strings"
"unsafe"
)

// BindFS returns the WFN bound as CPE 2.3 formatted string.
//
// Deprecated: use [WFN.AppendText].
func (w WFN) BindFS() string {
b := strings.Builder{}
b.WriteString(`cpe:2.3`)
for i := 0; i < NumAttr; i++ {
for i := range NumAttr {
b.WriteByte(':')
w.Attr[i].bind(&b)
}
Expand All @@ -33,3 +40,57 @@ var valueString = strings.NewReplacer(
`\-`, `-`,
`\_`, `_`,
)

var (
_ encoding.TextAppender = (*Value)(nil)
_ encoding.TextAppender = (*WFN)(nil)
)

// AppendText implements [encoding.TextAppender].
func (w *WFN) AppendText(b []byte) ([]byte, error) {
switch err := w.Valid(); {
case err == nil:
case errors.Is(err, ErrUnset):
return []byte{}, nil
default:
return nil, err
}
b = append(b, 'c', 'p', 'e', ':', '2', '.', '3')
for i := range NumAttr {
// Cannot error
b, _ = (&w.Attr[i]).AppendText(b)
}
return b, nil
}

// AppendText implements [encoding.TextAppender].
func (v *Value) AppendText(b []byte) ([]byte, error) {
b = append(b, ':')
switch v.Kind {
case ValueUnset, ValueAny:
return append(b, '*'), nil
case ValueNA:
return append(b, '-'), nil
case ValueSet:
default:
panic("unreachable")
}

esc := false
// SAFETY: This is all read-only.
for _, c := range unsafe.Slice(unsafe.StringData(v.V), len(v.V)) {
switch {
case !esc && c == '\\':
esc = true
continue
case esc && (c != '.' && c != '-' && c != '_'):
b = append(b, '\\')
fallthrough
case esc:
esc = false
default:
}
b = append(b, c)
}
return b, nil
}
1 change: 1 addition & 0 deletions toolkit/types/cpe/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ package cpe
//go:generate go tool stringer -type ValueKind
//go:generate go tool stringer -type Relation -linecomment
//go:generate go tool cpedict
//go:generate go tool mkragel unbind_fs.rl
24 changes: 11 additions & 13 deletions toolkit/types/cpe/marshaling.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,13 @@ import (

// MarshalText implements [encoding.TextMarshaler].
func (w *WFN) MarshalText() ([]byte, error) {
switch err := w.Valid(); {
case err == nil:
case errors.Is(err, ErrUnset):
return []byte{}, nil
default:
return nil, err
}
return []byte(w.BindFS()), nil
// Guess at a good initial size. Calculated via finding the mean size across
// the CPE Name dictionary and then rounding it up.
//
// zcat testdata/dictionary.list.gz | awk '/^#/{next}/^$/{next}{ct++;sum+=length($0)}END{print sum/ct}'
// 55.9444 = 64
b := make([]byte, 0, 64)
return w.AppendText(b)
}

// UnmarshalText implements [encoding.TextUnmarshaler].
Expand All @@ -31,14 +30,13 @@ func (w *WFN) UnmarshalText(b []byte) (err error) {
// Scan implements [sql.Scanner].
//
// Passing an empty string does not error and leaves the WFN in its current state.
func (w *WFN) Scan(src interface{}) (err error) {
func (w *WFN) Scan(src any) (err error) {
var s string
switch src.(type) {
switch src := src.(type) {
case []byte:
s = string(src.([]byte))
s = strings.ToValidUTF8(s, "�")
s = strings.ToValidUTF8(string(src), "�")
case string:
s = src.(string)
s = src
default:
return fmt.Errorf("cpe: unable to Scan from type %T", src)
}
Expand Down
21 changes: 20 additions & 1 deletion toolkit/types/cpe/marshaling_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (

func TestMarshal(t *testing.T) {
t.Parallel()
var names = []string{
names := []string{
`cpe:2.3:a:foo\\bar:big\$money:2010:*:*:*:special:ipod_touch:80gb:*`,
`cpe:2.3:a:foo\\bar:big\$money_2010:*:*:*:*:special:ipod_touch:80gb:*`,
`cpe:2.3:a:hp:insight:7.4.0.1570:-:*:*:online:win2003:x64:*`,
Expand Down Expand Up @@ -125,4 +125,23 @@ func TestMarshal(t *testing.T) {
}
}
})
t.Run("Equivalent", func(t *testing.T) {
for _, n := range names {
if n == "" {
continue
}
var wfn WFN
if err := wfn.UnmarshalFS(n); err != nil {
t.Error(err)
}
s := wfn.BindFS()
b, err := wfn.AppendText(nil)
if err != nil {
t.Error(err)
}
if got, want := string(b), s; got != want {
t.Error(cmp.Diff(got, want))
}
}
})
}
74 changes: 38 additions & 36 deletions toolkit/types/cpe/unbind.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package cpe
import (
"fmt"
"strings"
"unicode"
)

const (
Expand All @@ -17,7 +18,8 @@ func Unbind(s string) (WFN, error) {
case strings.HasPrefix(s, cpe22Prefix):
return UnbindURI(s)
case strings.HasPrefix(s, cpe23Prefix):
return UnbindFS(s)
var wfn WFN
return wfn, wfn.UnmarshalFS(s)
default:
}
return WFN{}, fmt.Errorf("cpe: string does not appear to be a bound WFN: %q", s)
Expand Down Expand Up @@ -153,21 +155,41 @@ var valueURI = strings.NewReplacer(
)

// UnbindFS attempts to unbind a string as CPE 2.3 formatted string into a WFN.
//
// Deprecated: Use [WFN.UnmarshalFS].
func UnbindFS(s string) (WFN, error) {
r := WFN{}
wfn := WFN{}
if !strings.HasPrefix(s, cpe23Prefix) {
return r, fmt.Errorf("cpe: malformed CPE formatted string: bad prefix")
}
fs := splitFS(s)
if l := len(fs); l != 13 {
return r, fmt.Errorf("cpe: malformed CPE formatted string: bad components: %d != 13", l)
return wfn, fmt.Errorf("cpe: malformed CPE formatted string: bad prefix")
}
fs = fs[2:13] // Skip the first two segments, "cpe" and "2.3".
s = s[len(cpe23Prefix):]
var b strings.Builder
for i, c := range fs {
r.Attr[i].unbindFS(&b, c)
a := 0
prev, esc := 0, false
for i, r := range s {
switch {
case r >= unicode.MaxASCII:
return wfn, fmt.Errorf("cpe: malformed CPE formatted string: invalid character %q @ %d", r, i)
case r == '\\':
esc = true
continue
case r == ':':
if esc {
break
}
wfn.Attr[a].unbindFS(&b, s[prev:i])
a++
if a == NumAttr {
return wfn, fmt.Errorf("cpe: malformed CPE formatted string: bad components: >13")
}
prev = i + 1
default:
}
esc = false
}
return r, r.Valid()
wfn.Attr[a].unbindFS(&b, s[prev:])

return wfn, wfn.Valid()
}

// UnbindFS undoes the FS binding and assigns it to v.
Expand All @@ -185,34 +207,14 @@ func (v *Value) unbindFS(b *strings.Builder, s string) {
}
}

// SplitFS splits a string in to unquoted-colon separated segments.
func splitFS(s string) []string {
var fs []string
prev, esc := 0, false
for i, r := range s {
switch r {
case '\\':
esc = true
continue
case ':':
if esc {
break
}
fs = append(fs, s[prev:i])
prev = i + 1
default:
}
esc = false
}
fs = append(fs, s[prev:])
return fs
}

// UnbindFSValue does what it says on the tin.
//
// Caller provides scratch space for the return construction via the passed
// strings.Builder.
func unbindFSValue(b *strings.Builder, s string) string {
if !strings.ContainsFunc(s, reserved) {
return s
}
b.Reset()
esc := false
for _, r := range s {
Expand All @@ -221,14 +223,14 @@ func unbindFSValue(b *strings.Builder, s string) string {
switch {
case r == '\\':
esc = true
b.WriteRune('\\')
b.WriteByte('\\')
continue
case r == '*' || r == '?':
fallthrough
case esc || !reserved(r):
b.WriteRune(r)
default:
b.WriteRune('\\')
b.WriteByte('\\')
b.WriteRune(r)
}
esc = false
Expand Down
Loading
Loading