Repository: zmap/zlint
Pull request #297 (2019-07-30 20:49:26)

lints: implement json.Unmarshaler for LintStatus.
The `lints.LintStatus` type implements `json.Marshaler` to marshal to a human readable string (e.g. `"error"` instead of `6`). Without providing a compatible `json.Unmarshaler` implementation downstream users that marshal a `lints.LintStatus` to JSON will encounter an error when they try to unmarshal it due to a mismatch of types (`int` vs `string`).

Notably this is causing me problems because when I added pre-issuance linting to [CFSSL](https://github.com/cloudflare/cfssl/pull/1015) I made the configuration value on the `cfssl.config.SigningProfile` type [a `zlint.lints.LintStatus`](https://github.com/cloudflare/cfssl/blob/633726f6bcb7574626ae05ae72ca3c8dbc51810f/config/config.go#L94-L102) after a suggestion by the maintainers. That seemed fine at the time but I later discovered in Boulder we expect to be able to [round-trip our CFSSL config to/from JSON without error](https://github.com/letsencrypt/boulder/blob/bb005e1c79f3972ef8c7f24e3bd44779bbced9e5/ca/ca.go#L209-L218).

Without the changes in this branch the de-serialization of the CFSSL config fails with an error like:

> Failed to create CA: {"code":5200,"message":"failed to unmarshal configuration: json: cannot unmarshal string into Go struct field SigningProfile.lint_error_level of type lints.LintStatus"}




Patch:
diff --git a/lints/result.go b/lints/result.go
index 92a1e590..6ea447c3 100644
--- a/lints/result.go
+++ b/lints/result.go
@@ -14,7 +14,11 @@ package lints
  * permissions and limitations under the License.
  */
 
-import "encoding/json"
+import (
+	"encoding/json"
+	"fmt"
+	"strings"
+)
 
 // LintStatus is an enum returned by lints inside of a LintResult.
 type LintStatus int
@@ -37,6 +41,22 @@ const (
 	Fatal  LintStatus = 7
 )
 
+var (
+	// statusLabelToLintStatus is used to work backwards from
+	// a LintStatus.String() to the LintStatus. This is used by
+	// LintStatus.Unmarshal.
+	statusLabelToLintStatus = map[string]LintStatus{
+		Reserved.String(): Reserved,
+		NA.String():       NA,
+		NE.String():       NE,
+		Pass.String():     Pass,
+		Notice.String():   Notice,
+		Warn.String():     Warn,
+		Error.String():    Error,
+		Fatal.String():    Fatal,
+	}
+)
+
 // LintResult contains a LintStatus, and an optional human-readable description.
 // The output of a lint is a LintResult.
 type LintResult struct {
@@ -50,9 +70,22 @@ func (e LintStatus) MarshalJSON() ([]byte, error) {
 	return json.Marshal(s)
 }
 
+// UnmarshalJSON implements the json.Unmarshaler interface.
+func (e *LintStatus) UnmarshalJSON(data []byte) error {
+	key := strings.ReplaceAll(string(data), `"`, "")
+	if status, ok := statusLabelToLintStatus[key]; ok {
+		*e = status
+	} else {
+		return fmt.Errorf("bad LintStatus JSON value: %s", string(data))
+	}
+	return nil
+}
+
 // String returns the canonical representation of a LintStatus as a string.
 func (e LintStatus) String() string {
 	switch e {
+	case Reserved:
+		return "reserved"
 	case NA:
 		return "NA"
 	case NE:


Test patch:
diff --git a/lints/result_test.go b/lints/result_test.go
new file mode 100644
index 00000000..66bb3ef0
--- /dev/null
+++ b/lints/result_test.go
@@ -0,0 +1,68 @@
+package lints
+
+import (
+	"encoding/json"
+	"testing"
+)
+
+func TestMarshalingLintStatus(t *testing.T) {
+	testCases := []struct {
+		result       LintStatus
+		expectedJSON string
+	}{
+		{
+			result:       Reserved,
+			expectedJSON: `"reserved"`,
+		},
+		{
+			result:       NA,
+			expectedJSON: `"NA"`,
+		},
+		{
+			result:       NE,
+			expectedJSON: `"NE"`,
+		},
+		{
+			result:       Pass,
+			expectedJSON: `"pass"`,
+		},
+		{
+			result:       Notice,
+			expectedJSON: `"info"`,
+		},
+		{
+			result:       Warn,
+			expectedJSON: `"warn"`,
+		},
+		{
+			result:       Error,
+			expectedJSON: `"error"`,
+		},
+		{
+			result:       Fatal,
+			expectedJSON: `"fatal"`,
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.result.String(), func(t *testing.T) {
+			j, err := json.Marshal(tc.result)
+			if err != nil {
+				t.Error("Failed to marshal LintStatus")
+			}
+			if string(j) != tc.expectedJSON {
+				t.Errorf("Expected LintStatus to marshal to JSON %q, got %q",
+					tc.expectedJSON,
+					j)
+			}
+			var in LintStatus
+			if err := json.Unmarshal(j, &in); err != nil {
+				t.Errorf("Expected to unmarshal %q without error. Got %v", j, err)
+			}
+			if in != tc.result {
+				t.Errorf("Expected to unmarshal %q to %#v, got %#v", j, tc.result, in)
+			}
+		})
+	}
+
+}