From e3a6bc8502a0366aee9040e938789a6016e2fa4a Mon Sep 17 00:00:00 2001
From: Paul Duncan <pabs@pablotron.org>
Date: Wed, 2 Feb 2022 21:45:23 -0500
Subject: internal/cpe: add token.go, part.go, and tests

---
 internal/cpe/cpe.go         |  89 --------------------------
 internal/cpe/cpe_test.go    | 144 ------------------------------------------
 internal/cpe/part.go        |  41 ++++++++++++
 internal/cpe/part_string.go |  41 ++++++++++++
 internal/cpe/part_test.go   | 108 ++++++++++++++++++++++++++++++++
 internal/cpe/token.go       |  99 +++++++++++++++++++++++++++++
 internal/cpe/token_test.go  | 149 ++++++++++++++++++++++++++++++++++++++++++++
 7 files changed, 438 insertions(+), 233 deletions(-)
 create mode 100644 internal/cpe/part.go
 create mode 100644 internal/cpe/part_string.go
 create mode 100644 internal/cpe/part_test.go
 create mode 100644 internal/cpe/token.go
 create mode 100644 internal/cpe/token_test.go

(limited to 'internal/cpe')

diff --git a/internal/cpe/cpe.go b/internal/cpe/cpe.go
index 73930e8..331f315 100644
--- a/internal/cpe/cpe.go
+++ b/internal/cpe/cpe.go
@@ -3,92 +3,3 @@
 // Source: NISTIR 7605, figure 6-3:
 // https://nvlpubs.nist.gov/nistpubs/Legacy/IR/nistir7695.pdf
 package cpe
-
-import (
-  "fmt"
-)
-
-//go:generate stringer -linecomment -type=tokenType
-
-// token type
-type tokenType byte
-
-const (
-  anyToken tokenType = iota // any
-  naToken // na
-  valToken // val
-)
-
-// token
-type token struct {
-  Type tokenType // token type
-  Val string // token value
-}
-
-// parse buffer into token.
-func newToken(val []byte) token {
-  if len(val) > 0 {
-    switch val[0] {
-    case '*':
-      return token { Type: anyToken }
-    case '-':
-      return token { Type: naToken }
-    default:
-      return token { Type: valToken, Val: string(val) }
-    }
-  } else {
-    // empty value
-    return token { Type: valToken, Val: "" }
-  }
-}
-
-// Parse buffer into slice of tokens.
-func tokenize(buf []byte) ([]token, error) {
-  // build result
-  var r []token
-
-  // current token and escape state
-  var curr []byte
-  esc := false
-
-  // build result
-  for _, b := range(buf) {
-    if esc {
-      switch b {
-      // valid escaped characters
-      case '\\', '*', '-', '!', '"', '#', '$', '%', '&', '\'', '(', ')',
-           '+', ',', '/', ':', ';', '<', '=', '>', '@', '[', ']', '^', '`',
-           '{', '|', '}', '~':
-        curr = append(curr, b)
-        esc = false
-      default:
-        return r, fmt.Errorf("invalid escape byte: 0x%02x", b)
-      }
-    } else {
-      switch b {
-      case '\\':
-        esc = true
-      case ':':
-        // push token, clear buffer
-        r = append(r, newToken(curr))
-        curr = nil
-      case 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l',
-           'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x',
-           'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
-           '-', '.', '_', '*', '?':
-        curr = append(curr, b)
-      default:
-        return r, fmt.Errorf("invalid byte: 0x%02x", b)
-      }
-    }
-  }
-
-  if len(curr) > 0 {
-    // push token, clear buffer
-    r = append(r, newToken(curr))
-    curr = nil
-  }
-
-  // return success
-  return r, nil
-}
diff --git a/internal/cpe/cpe_test.go b/internal/cpe/cpe_test.go
index e842c75..0f121b6 100644
--- a/internal/cpe/cpe_test.go
+++ b/internal/cpe/cpe_test.go
@@ -1,145 +1 @@
 package cpe
-
-import (
-  "reflect"
-  "testing"
-)
-
-func TestTokenTypeString(t *testing.T) {
-  tests := []struct {
-    val tokenType
-    exp string
-  } {
-    { anyToken, "any" },
-    { naToken, "na" },
-    { valToken, "val" },
-    { tokenType(255), "tokenType(255)" },
-  }
-
-  for _, test := range(tests) {
-    t.Run(test.exp, func(t *testing.T) {
-      got := test.val.String()
-      if got != test.exp {
-        t.Errorf("got \"%s\", exp \"%s\"", got, test.exp)
-      }
-    })
-  }
-}
-
-func TestNewToken(t *testing.T) {
-  passTests := []struct {
-    name string
-    val string
-    exp token
-  } {
-    { "any", "*", token { Type: anyToken } },
-    { "na", "-", token { Type: naToken } },
-    { "empty", "", token { Type: valToken } },
-    { "foo", "foo", token { Type: valToken, Val: "foo" } },
-  }
-
-  for _, test := range(passTests) {
-    t.Run(test.name, func(t *testing.T) {
-      got := newToken([]byte(test.val))
-      if got.Type != test.exp.Type {
-        t.Errorf("token: got %s, exp %s", got.Type, test.exp.Type)
-      } else if got.Type == valToken && got.Val != test.exp.Val {
-        t.Errorf("value: got \"%s\", exp \"%s\"", got.Val, test.exp.Val)
-      }
-    })
-  }
-}
-
-func TestTokenize(t *testing.T) {
-  passTests := []struct {
-    val string
-    exp []token
-  } {{
-    val: "foo",
-    exp: []token { token { Type: valToken, Val: "foo" } },
-  }, {
-    val: "foo:bar",
-    exp: []token {
-      token { Type: valToken, Val: "foo" },
-      token { Type: valToken, Val: "bar" },
-    },
-  }, {
-    val: "*",
-    exp: []token { token { Type: anyToken } },
-  }, {
-    val: "-",
-    exp: []token { token { Type: naToken } },
-  }, {
-    val: "*:bar",
-    exp: []token {
-      token { Type: anyToken },
-      token { Type: valToken, Val: "bar" },
-    },
-  }, {
-    val: "foo:*",
-    exp: []token {
-      token { Type: valToken, Val: "foo" },
-      token { Type: anyToken },
-    },
-  }, {
-    val: "-:bar",
-    exp: []token {
-      token { Type: naToken },
-      token { Type: valToken, Val: "bar" },
-    },
-  }, {
-    val: "foo:-",
-    exp: []token {
-      token { Type: valToken, Val: "foo" },
-      token { Type: naToken },
-    },
-  }, {
-    val: "foo\\*:-",
-    exp: []token {
-      token { Type: valToken, Val: "foo*" },
-      token { Type: naToken },
-    },
-  }}
-
-  for _, test := range(passTests) {
-    t.Run(test.val, func(t *testing.T) {
-      // tokenize, check for error
-      got, err := tokenize([]byte(test.val))
-      if err != nil {
-        t.Error(err)
-        return
-      }
-
-      if !reflect.DeepEqual(got, test.exp) {
-        t.Errorf("token: got %v, exp %v", got, test.exp)
-        return
-      }
-    })
-  }
-
-  failTests := []struct {
-    id  string
-    val string
-    exp string
-  } {{
-    id:  "invalid escape",
-    val: "foo\\.",
-    exp: "invalid escape byte: 0x2e",
-  }, {
-    id:  "invalid byte",
-    val: "\n",
-    exp: "invalid byte: 0x0a",
-  }}
-
-  for _, test := range(failTests) {
-    t.Run(test.id, func(t *testing.T) {
-      // tokenize, check for error
-      got, err := tokenize([]byte(test.val))
-      if err == nil {
-        t.Errorf("got %v, exp error", got)
-      } else if err.Error() != test.exp {
-        t.Errorf("got \"%s\", exp \"%s\"", err.Error(), test.exp)
-      }
-    })
-  }
-}
diff --git a/internal/cpe/part.go b/internal/cpe/part.go
new file mode 100644
index 0000000..ef91f7c
--- /dev/null
+++ b/internal/cpe/part.go
@@ -0,0 +1,41 @@
+package cpe
+
+//go:generate stringer -linecomment -type=Part
+
+import (
+  "fmt"
+)
+
+// CPE part
+type Part byte
+
+const (
+  ApplicationPart Part = 'a' // a
+  OperatingSystemPart Part = 'o' // o
+  HardwarePart Part = 'h' // h
+  AnyPart Part = '*' // *
+  NAPart Part = '-' // -
+)
+
+// create new part from token
+func newPart(t token) (Part, error) {
+  switch t.Type {
+  case anyToken:
+    return AnyPart, nil
+  case naToken:
+    return NAPart, nil
+  case valToken:
+    switch t.Val {
+    case "a":
+      return ApplicationPart, nil
+    case "o":
+      return OperatingSystemPart, nil
+    case "h":
+      return HardwarePart, nil
+    default:
+      return 0, fmt.Errorf("unknown part: \"%s\"", t.Val)
+    }
+  default:
+    return 0, fmt.Errorf("unknown token type: 0x%02x", byte(t.Type))
+  }
+}
diff --git a/internal/cpe/part_string.go b/internal/cpe/part_string.go
new file mode 100644
index 0000000..98b9fd3
--- /dev/null
+++ b/internal/cpe/part_string.go
@@ -0,0 +1,41 @@
+// Code generated by "stringer -linecomment -type=Part"; DO NOT EDIT.
+
+package cpe
+
+import "strconv"
+
+func _() {
+	// An "invalid array index" compiler error signifies that the constant values have changed.
+	// Re-run the stringer command to generate them again.
+	var x [1]struct{}
+	_ = x[ApplicationPart-97]
+	_ = x[OperatingSystemPart-111]
+	_ = x[HardwarePart-104]
+	_ = x[AnyPart-42]
+	_ = x[NAPart-45]
+}
+
+const (
+	_Part_name_0 = "*"
+	_Part_name_1 = "-"
+	_Part_name_2 = "a"
+	_Part_name_3 = "h"
+	_Part_name_4 = "o"
+)
+
+func (i Part) String() string {
+	switch {
+	case i == 42:
+		return _Part_name_0
+	case i == 45:
+		return _Part_name_1
+	case i == 97:
+		return _Part_name_2
+	case i == 104:
+		return _Part_name_3
+	case i == 111:
+		return _Part_name_4
+	default:
+		return "Part(" + strconv.FormatInt(int64(i), 10) + ")"
+	}
+}
diff --git a/internal/cpe/part_test.go b/internal/cpe/part_test.go
new file mode 100644
index 0000000..269d16e
--- /dev/null
+++ b/internal/cpe/part_test.go
@@ -0,0 +1,108 @@
+package cpe
+
+import (
+  "testing"
+)
+
+func TestNewPart(t *testing.T) {
+  passTests := []struct {
+    name string
+    val token
+    exp Part
+  } {{
+    name: "any",
+    val:  token { Type: anyToken },
+    exp:  AnyPart,
+  }, {
+    name: "na",
+    val:  token { Type: naToken },
+    exp:  NAPart,
+  }, {
+    name: "a",
+    val:  token { Type: valToken, Val: "a" },
+    exp:  ApplicationPart,
+  }, {
+    name: "h",
+    val:  token { Type: valToken, Val: "h" },
+    exp:  HardwarePart,
+  }, {
+    name: "o",
+    val:  token { Type: valToken, Val: "o" },
+    exp:  OperatingSystemPart,
+  }}
+
+  for _, test := range(passTests) {
+    t.Run(test.name, func(t *testing.T) {
+      if got, err := newPart(test.val); err != nil {
+        t.Error(err)
+      } else if got != test.exp {
+        t.Errorf("got %s, exp %s", got, test.exp)
+      }
+    })
+  }
+
+  failTests := []struct {
+    name string
+    val token
+    exp string
+  } {{
+    name: "invalid token",
+    val:  token { Type: valToken, Val: "foo" },
+    exp:  "unknown part: \"foo\"",
+  }, {
+    name: "empty token",
+    val:  token { Type: valToken },
+    exp:  "unknown part: \"\"",
+  }, {
+    name: "unknown token type",
+    val:  token { Type: tokenType(255) },
+    exp:  "unknown token type: 0xff",
+  }}
+
+  for _, test := range(failTests) {
+    t.Run(test.name, func(t *testing.T) {
+      // tokenize, check for error
+      got, err := newPart(test.val)
+      if err == nil {
+        t.Errorf("got %v, exp error", got)
+      } else if err.Error() != test.exp {
+        t.Errorf("got \"%s\", exp \"%s\"", err.Error(), test.exp)
+      }
+    })
+  }
+}
+
+func TestPartString(t *testing.T) {
+  tests := []struct {
+    val Part
+    exp string
+  } {{
+    val:  AnyPart,
+    exp:  "*",
+  }, {
+    val:  NAPart,
+    exp:  "-",
+  }, {
+    val:  ApplicationPart,
+    exp:  "a",
+  }, {
+    val:  HardwarePart,
+    exp:  "h",
+  }, {
+    val:  OperatingSystemPart,
+    exp:  "o",
+  }, {
+    val:  Part(255),
+    exp:  "Part(255)",
+  }}
+
+  for _, test := range(tests) {
+    t.Run(test.exp, func(t *testing.T) {
+      got := test.val.String()
+
+      if got != test.exp {
+        t.Errorf("got \"%s\", exp \"%s\"", got, test.exp)
+      }
+    })
+  }
+}
diff --git a/internal/cpe/token.go b/internal/cpe/token.go
new file mode 100644
index 0000000..d88e773
--- /dev/null
+++ b/internal/cpe/token.go
@@ -0,0 +1,99 @@
+package cpe
+
+//go:generate stringer -linecomment -type=tokenType
+
+import (
+  "errors"
+  "fmt"
+)
+
+// token type
+type tokenType byte
+
+const (
+  anyToken tokenType = iota // any
+  naToken // na
+  valToken // val
+)
+
+// token
+type token struct {
+  Type tokenType // token type
+  Val string // token value
+}
+
+// parse buffer into token.
+func newToken(val []byte) token {
+  if len(val) > 0 {
+    switch val[0] {
+    case '*':
+      return token { Type: anyToken }
+    case '-':
+      return token { Type: naToken }
+    default:
+      return token { Type: valToken, Val: string(val) }
+    }
+  } else {
+    // empty value
+    return token { Type: valToken }
+  }
+}
+
+// unterminated escape error
+var unterminatedEsc = errors.New("unterminated escape at end of buffer")
+
+// Parse buffer into list of tokens.
+func tokenize(buf []byte) ([]token, error) {
+  // build result
+  var r []token
+
+  // current token and escape state
+  var curr []byte
+  esc := false
+
+  // build result
+  for _, b := range(buf) {
+    if esc {
+      switch b {
+      // valid escaped characters
+      case '\\', '*', '-', '!', '"', '#', '$', '%', '&', '\'', '(', ')',
+           '+', ',', '/', ':', ';', '<', '=', '>', '@', '[', ']', '^', '`',
+           '{', '|', '}', '~':
+        curr = append(curr, b)
+        esc = false
+      default:
+        return r, fmt.Errorf("invalid escape byte: 0x%02x", b)
+      }
+    } else {
+      switch b {
+      case '\\':
+        esc = true
+      case ':':
+        // push token, clear buffer
+        r = append(r, newToken(curr))
+        curr = nil
+      case 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l',
+           'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x',
+           'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
+           '-', '.', '_', '*', '?':
+        curr = append(curr, b)
+      default:
+        return r, fmt.Errorf("invalid byte: 0x%02x", b)
+      }
+    }
+  }
+
+  // check for unterminated escape
+  if esc {
+    return r, unterminatedEsc
+  }
+
+  if len(curr) > 0 {
+    // push token, clear buffer
+    r = append(r, newToken(curr))
+    curr = nil
+  }
+
+  // return success
+  return r, nil
+}
diff --git a/internal/cpe/token_test.go b/internal/cpe/token_test.go
new file mode 100644
index 0000000..595df2c
--- /dev/null
+++ b/internal/cpe/token_test.go
@@ -0,0 +1,149 @@
+package cpe
+
+import (
+  "reflect"
+  "testing"
+)
+
+func TestTokenTypeString(t *testing.T) {
+  tests := []struct {
+    val tokenType
+    exp string
+  } {
+    { anyToken, "any" },
+    { naToken, "na" },
+    { valToken, "val" },
+    { tokenType(255), "tokenType(255)" },
+  }
+
+  for _, test := range(tests) {
+    t.Run(test.exp, func(t *testing.T) {
+      got := test.val.String()
+      if got != test.exp {
+        t.Errorf("got \"%s\", exp \"%s\"", got, test.exp)
+      }
+    })
+  }
+}
+
+func TestNewToken(t *testing.T) {
+  passTests := []struct {
+    name string
+    val string
+    exp token
+  } {
+    { "any", "*", token { Type: anyToken } },
+    { "na", "-", token { Type: naToken } },
+    { "empty", "", token { Type: valToken } },
+    { "foo", "foo", token { Type: valToken, Val: "foo" } },
+  }
+
+  for _, test := range(passTests) {
+    t.Run(test.name, func(t *testing.T) {
+      got := newToken([]byte(test.val))
+      if got.Type != test.exp.Type {
+        t.Errorf("token: got %s, exp %s", got.Type, test.exp.Type)
+      } else if got.Type == valToken && got.Val != test.exp.Val {
+        t.Errorf("value: got \"%s\", exp \"%s\"", got.Val, test.exp.Val)
+      }
+    })
+  }
+}
+
+func TestTokenize(t *testing.T) {
+  passTests := []struct {
+    val string
+    exp []token
+  } {{
+    val: "foo",
+    exp: []token { token { Type: valToken, Val: "foo" } },
+  }, {
+    val: "foo:bar",
+    exp: []token {
+      token { Type: valToken, Val: "foo" },
+      token { Type: valToken, Val: "bar" },
+    },
+  }, {
+    val: "*",
+    exp: []token { token { Type: anyToken } },
+  }, {
+    val: "-",
+    exp: []token { token { Type: naToken } },
+  }, {
+    val: "*:bar",
+    exp: []token {
+      token { Type: anyToken },
+      token { Type: valToken, Val: "bar" },
+    },
+  }, {
+    val: "foo:*",
+    exp: []token {
+      token { Type: valToken, Val: "foo" },
+      token { Type: anyToken },
+    },
+  }, {
+    val: "-:bar",
+    exp: []token {
+      token { Type: naToken },
+      token { Type: valToken, Val: "bar" },
+    },
+  }, {
+    val: "foo:-",
+    exp: []token {
+      token { Type: valToken, Val: "foo" },
+      token { Type: naToken },
+    },
+  }, {
+    val: "foo\\*:-",
+    exp: []token {
+      token { Type: valToken, Val: "foo*" },
+      token { Type: naToken },
+    },
+  }}
+
+  for _, test := range(passTests) {
+    t.Run(test.val, func(t *testing.T) {
+      // tokenize, check for error
+      got, err := tokenize([]byte(test.val))
+      if err != nil {
+        t.Error(err)
+        return
+      }
+
+      if !reflect.DeepEqual(got, test.exp) {
+        t.Errorf("token: got %v, exp %v", got, test.exp)
+        return
+      }
+    })
+  }
+
+  failTests := []struct {
+    id  string
+    val string
+    exp string
+  } {{
+    id:  "invalid escape",
+    val: "foo\\.",
+    exp: "invalid escape byte: 0x2e",
+  }, {
+    id:  "invalid byte",
+    val: "\n",
+    exp: "invalid byte: 0x0a",
+  }, {
+    id:  "unterminated escape",
+    val: "\\",
+    exp: "unterminated escape at end of buffer",
+  }}
+
+  for _, test := range(failTests) {
+    t.Run(test.id, func(t *testing.T) {
+      // tokenize, check for error
+      got, err := tokenize([]byte(test.val))
+      if err == nil {
+        t.Errorf("got %v, exp error", got)
+      } else if err.Error() != test.exp {
+        t.Errorf("got \"%s\", exp \"%s\"", err.Error(), test.exp)
+      }
+    })
+  }
+}
-- 
cgit v1.2.3