Skip to content

Commit 0c2ebff

Browse files
committedSep 25, 2022
refactor: separate custom marshalling logic from model
Signed-off-by: nscuro <nscuro@protonmail.com>
1 parent 2826fe2 commit 0c2ebff

6 files changed

+486
-397
lines changed
 

‎cyclonedx.go

-207
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,7 @@
1818
package cyclonedx
1919

2020
import (
21-
"encoding/json"
2221
"encoding/xml"
23-
"errors"
24-
"fmt"
25-
"io"
2622
"regexp"
2723
)
2824

@@ -98,24 +94,6 @@ func Bool(value bool) *bool {
9894

9995
type BOMReference string
10096

101-
// bomReferenceXML is temporarily used for marshalling and unmarshalling BOMReference instances to and from XML
102-
type bomReferenceXML struct {
103-
Ref string `json:"-" xml:"ref,attr"`
104-
}
105-
106-
func (b BOMReference) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
107-
return e.EncodeElement(bomReferenceXML{Ref: string(b)}, start)
108-
}
109-
110-
func (b *BOMReference) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
111-
bXML := bomReferenceXML{}
112-
if err := d.DecodeElement(&bXML, &start); err != nil {
113-
return err
114-
}
115-
*b = BOMReference(bXML.Ref)
116-
return nil
117-
}
118-
11997
type ComponentType string
12098

12199
const (
@@ -185,19 +163,6 @@ type Copyright struct {
185163
Text string `json:"text" xml:"-"`
186164
}
187165

188-
func (c Copyright) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
189-
return e.EncodeElement(c.Text, start)
190-
}
191-
192-
func (c *Copyright) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
193-
var text string
194-
if err := d.DecodeElement(&text, &start); err != nil {
195-
return err
196-
}
197-
(*c).Text = text
198-
return nil
199-
}
200-
201166
type Credits struct {
202167
Organizations *[]OrganizationalEntity `json:"organizations,omitempty" xml:"organizations>organization,omitempty"`
203168
Individuals *[]OrganizationalContact `json:"individuals,omitempty" xml:"individuals>individual,omitempty"`
@@ -222,52 +187,6 @@ type Dependency struct {
222187
Dependencies *[]Dependency `xml:"dependency,omitempty"`
223188
}
224189

225-
// dependencyJSON is temporarily used for marshalling and unmarshalling Dependency instances to and from JSON
226-
type dependencyJSON struct {
227-
Ref string `json:"ref"`
228-
DependsOn []string `json:"dependsOn,omitempty"`
229-
}
230-
231-
func (d Dependency) MarshalJSON() ([]byte, error) {
232-
if d.Dependencies == nil || len(*d.Dependencies) == 0 {
233-
return json.Marshal(&dependencyJSON{
234-
Ref: d.Ref,
235-
})
236-
}
237-
238-
dependencyRefs := make([]string, len(*d.Dependencies))
239-
for i, dependency := range *d.Dependencies {
240-
dependencyRefs[i] = dependency.Ref
241-
}
242-
243-
return json.Marshal(&dependencyJSON{
244-
Ref: d.Ref,
245-
DependsOn: dependencyRefs,
246-
})
247-
}
248-
249-
func (d *Dependency) UnmarshalJSON(bytes []byte) error {
250-
dependency := new(dependencyJSON)
251-
if err := json.Unmarshal(bytes, dependency); err != nil {
252-
return err
253-
}
254-
d.Ref = dependency.Ref
255-
256-
if len(dependency.DependsOn) == 0 {
257-
return nil
258-
}
259-
260-
dependencies := make([]Dependency, len(dependency.DependsOn))
261-
for i, dep := range dependency.DependsOn {
262-
dependencies[i] = Dependency{
263-
Ref: dep,
264-
}
265-
}
266-
d.Dependencies = &dependencies
267-
268-
return nil
269-
}
270-
271190
type Diff struct {
272191
Text *AttachedText `json:"text,omitempty" xml:"text,omitempty"`
273192
URL string `json:"url,omitempty" xml:"url,omitempty"`
@@ -394,70 +313,6 @@ type License struct {
394313

395314
type Licenses []LicenseChoice
396315

397-
func (l Licenses) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
398-
if len(l) == 0 {
399-
return nil
400-
}
401-
402-
if err := e.EncodeToken(start); err != nil {
403-
return err
404-
}
405-
406-
for _, choice := range l {
407-
if choice.License != nil && choice.Expression != "" {
408-
return fmt.Errorf("either license or expression must be set, but not both")
409-
}
410-
411-
if choice.License != nil {
412-
if err := e.EncodeElement(choice.License, xml.StartElement{Name: xml.Name{Local: "license"}}); err != nil {
413-
return err
414-
}
415-
} else if choice.Expression != "" {
416-
if err := e.EncodeElement(choice.Expression, xml.StartElement{Name: xml.Name{Local: "expression"}}); err != nil {
417-
return err
418-
}
419-
}
420-
}
421-
422-
return e.EncodeToken(start.End())
423-
}
424-
425-
func (l *Licenses) UnmarshalXML(d *xml.Decoder, _ xml.StartElement) error {
426-
licenses := make([]LicenseChoice, 0)
427-
428-
for {
429-
token, err := d.Token()
430-
if err != nil {
431-
if errors.Is(err, io.EOF) {
432-
break
433-
}
434-
return err
435-
}
436-
437-
switch tokenType := token.(type) {
438-
case xml.StartElement:
439-
if tokenType.Name.Local == "expression" {
440-
var expression string
441-
if err = d.DecodeElement(&expression, &tokenType); err != nil {
442-
return err
443-
}
444-
licenses = append(licenses, LicenseChoice{Expression: expression})
445-
} else if tokenType.Name.Local == "license" {
446-
var license License
447-
if err = d.DecodeElement(&license, &tokenType); err != nil {
448-
return err
449-
}
450-
licenses = append(licenses, LicenseChoice{License: &license})
451-
} else {
452-
return fmt.Errorf("unknown element: %s", tokenType.Name.Local)
453-
}
454-
}
455-
}
456-
457-
*l = licenses
458-
return nil
459-
}
460-
461316
type LicenseChoice struct {
462317
License *License `json:"license,omitempty" xml:"-"`
463318
Expression string `json:"expression,omitempty" xml:"-"`
@@ -599,60 +454,6 @@ const (
599454
SpecVersion1_4 // 1.4
600455
)
601456

602-
func (sv SpecVersion) MarshalJSON() ([]byte, error) {
603-
return json.Marshal(sv.String())
604-
}
605-
606-
func (sv *SpecVersion) UnmarshalJSON(bytes []byte) error {
607-
var v string
608-
err := json.Unmarshal(bytes, &v)
609-
if err != nil {
610-
return err
611-
}
612-
613-
switch v {
614-
case SpecVersion1_0.String():
615-
*sv = SpecVersion1_0
616-
case SpecVersion1_1.String():
617-
*sv = SpecVersion1_1
618-
case SpecVersion1_2.String():
619-
*sv = SpecVersion1_2
620-
case SpecVersion1_3.String():
621-
*sv = SpecVersion1_3
622-
case SpecVersion1_4.String():
623-
*sv = SpecVersion1_4
624-
}
625-
626-
return nil
627-
}
628-
629-
func (sv SpecVersion) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
630-
return e.EncodeElement(sv.String(), start)
631-
}
632-
633-
func (sv *SpecVersion) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
634-
var v string
635-
err := d.DecodeElement(&v, &start)
636-
if err != nil {
637-
return err
638-
}
639-
640-
switch v {
641-
case SpecVersion1_0.String():
642-
*sv = SpecVersion1_0
643-
case SpecVersion1_1.String():
644-
*sv = SpecVersion1_1
645-
case SpecVersion1_2.String():
646-
*sv = SpecVersion1_2
647-
case SpecVersion1_3.String():
648-
*sv = SpecVersion1_3
649-
case SpecVersion1_4.String():
650-
*sv = SpecVersion1_4
651-
}
652-
653-
return nil
654-
}
655-
656457
type SWID struct {
657458
Text *AttachedText `json:"text,omitempty" xml:"text,omitempty"`
658459
URL string `json:"url,omitempty" xml:"url,attr,omitempty"`
@@ -720,11 +521,3 @@ const (
720521
VulnerabilityStatusAffected VulnerabilityStatus = "affected"
721522
VulnerabilityStatusNotAffected VulnerabilityStatus = "unaffected"
722523
)
723-
724-
var xmlNamespaces = map[SpecVersion]string{
725-
SpecVersion1_0: "http://cyclonedx.org/schema/bom/1.0",
726-
SpecVersion1_1: "http://cyclonedx.org/schema/bom/1.1",
727-
SpecVersion1_2: "http://cyclonedx.org/schema/bom/1.2",
728-
SpecVersion1_3: "http://cyclonedx.org/schema/bom/1.3",
729-
SpecVersion1_4: "http://cyclonedx.org/schema/bom/1.4",
730-
}

‎cyclonedx_json.go

+93
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
// This file is part of CycloneDX Go
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the “License”);
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an “AS IS” BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
//
15+
// SPDX-License-Identifier: Apache-2.0
16+
// Copyright (c) OWASP Foundation. All Rights Reserved.
17+
18+
package cyclonedx
19+
20+
import "encoding/json"
21+
22+
// dependencyJSON is temporarily used for marshalling and unmarshalling Dependency instances to and from JSON
23+
type dependencyJSON struct {
24+
Ref string `json:"ref"`
25+
DependsOn []string `json:"dependsOn,omitempty"`
26+
}
27+
28+
func (d Dependency) MarshalJSON() ([]byte, error) {
29+
if d.Dependencies == nil || len(*d.Dependencies) == 0 {
30+
return json.Marshal(&dependencyJSON{
31+
Ref: d.Ref,
32+
})
33+
}
34+
35+
dependencyRefs := make([]string, len(*d.Dependencies))
36+
for i, dependency := range *d.Dependencies {
37+
dependencyRefs[i] = dependency.Ref
38+
}
39+
40+
return json.Marshal(&dependencyJSON{
41+
Ref: d.Ref,
42+
DependsOn: dependencyRefs,
43+
})
44+
}
45+
46+
func (d *Dependency) UnmarshalJSON(bytes []byte) error {
47+
dependency := new(dependencyJSON)
48+
if err := json.Unmarshal(bytes, dependency); err != nil {
49+
return err
50+
}
51+
d.Ref = dependency.Ref
52+
53+
if len(dependency.DependsOn) == 0 {
54+
return nil
55+
}
56+
57+
dependencies := make([]Dependency, len(dependency.DependsOn))
58+
for i, dep := range dependency.DependsOn {
59+
dependencies[i] = Dependency{
60+
Ref: dep,
61+
}
62+
}
63+
d.Dependencies = &dependencies
64+
65+
return nil
66+
}
67+
68+
func (sv SpecVersion) MarshalJSON() ([]byte, error) {
69+
return json.Marshal(sv.String())
70+
}
71+
72+
func (sv *SpecVersion) UnmarshalJSON(bytes []byte) error {
73+
var v string
74+
err := json.Unmarshal(bytes, &v)
75+
if err != nil {
76+
return err
77+
}
78+
79+
switch v {
80+
case SpecVersion1_0.String():
81+
*sv = SpecVersion1_0
82+
case SpecVersion1_1.String():
83+
*sv = SpecVersion1_1
84+
case SpecVersion1_2.String():
85+
*sv = SpecVersion1_2
86+
case SpecVersion1_3.String():
87+
*sv = SpecVersion1_3
88+
case SpecVersion1_4.String():
89+
*sv = SpecVersion1_4
90+
}
91+
92+
return nil
93+
}

‎cyclonedx_json_test.go

+77
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
// This file is part of CycloneDX Go
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the “License”);
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an “AS IS” BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
//
15+
// SPDX-License-Identifier: Apache-2.0
16+
// Copyright (c) OWASP Foundation. All Rights Reserved.
17+
18+
package cyclonedx
19+
20+
import (
21+
"encoding/json"
22+
"testing"
23+
24+
"github.com/stretchr/testify/assert"
25+
)
26+
27+
func TestDependency_MarshalJSON(t *testing.T) {
28+
// Marshal empty dependency
29+
dependency := Dependency{}
30+
jsonBytes, err := json.Marshal(dependency)
31+
assert.NoError(t, err)
32+
assert.Equal(t, "{\"ref\":\"\"}", string(jsonBytes))
33+
34+
// Marshal dependency with empty dependencies
35+
dependency = Dependency{
36+
Ref: "dependencyRef",
37+
Dependencies: &[]Dependency{},
38+
}
39+
jsonBytes, err = json.Marshal(dependency)
40+
assert.NoError(t, err)
41+
assert.Equal(t, "{\"ref\":\"dependencyRef\"}", string(jsonBytes))
42+
43+
// Marshal dependency with dependencies
44+
dependency = Dependency{
45+
Ref: "dependencyRef",
46+
Dependencies: &[]Dependency{
47+
{Ref: "transitiveDependencyRef"},
48+
},
49+
}
50+
jsonBytes, err = json.Marshal(dependency)
51+
assert.NoError(t, err)
52+
assert.Equal(t, "{\"ref\":\"dependencyRef\",\"dependsOn\":[\"transitiveDependencyRef\"]}", string(jsonBytes))
53+
}
54+
55+
func TestDependency_UnmarshalJSON(t *testing.T) {
56+
// Unmarshal empty dependency
57+
dependency := new(Dependency)
58+
err := json.Unmarshal([]byte("{}"), dependency)
59+
assert.NoError(t, err)
60+
assert.Equal(t, "", dependency.Ref)
61+
assert.Nil(t, dependency.Dependencies)
62+
63+
// Unmarshal dependency with empty dependencies
64+
dependency = new(Dependency)
65+
err = json.Unmarshal([]byte("{\"ref\":\"dependencyRef\",\"dependsOn\":[]}"), dependency)
66+
assert.NoError(t, err)
67+
assert.Equal(t, "dependencyRef", dependency.Ref)
68+
assert.Nil(t, dependency.Dependencies)
69+
70+
// Unmarshal dependency with dependencies
71+
dependency = new(Dependency)
72+
err = json.Unmarshal([]byte("{\"ref\":\"dependencyRef\",\"dependsOn\":[\"transitiveDependencyRef\"]}"), dependency)
73+
assert.NoError(t, err)
74+
assert.Equal(t, "dependencyRef", dependency.Ref)
75+
assert.Equal(t, 1, len(*dependency.Dependencies))
76+
assert.Equal(t, "transitiveDependencyRef", (*dependency.Dependencies)[0].Ref)
77+
}

‎cyclonedx_test.go

-190
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,13 @@
1818
package cyclonedx
1919

2020
import (
21-
"encoding/json"
22-
"encoding/xml"
2321
"fmt"
2422
"os/exec"
2523
"strings"
2624
"testing"
2725

2826
"github.com/bradleyjkemp/cupaloy/v2"
2927
"github.com/stretchr/testify/assert"
30-
"github.com/stretchr/testify/require"
3128
)
3229

3330
var snapShooter = cupaloy.NewDefaultConfig().
@@ -38,193 +35,6 @@ func TestBool(t *testing.T) {
3835
assert.Equal(t, false, *Bool(false))
3936
}
4037

41-
func TestBOMReference_MarshalXML(t *testing.T) {
42-
// Marshal empty bomRef
43-
bomRef := BOMReference("")
44-
xmlBytes, err := xml.Marshal(bomRef)
45-
assert.NoError(t, err)
46-
assert.Equal(t, "<BOMReference ref=\"\"></BOMReference>", string(xmlBytes))
47-
48-
// Marshal bomRef
49-
bomRef = "bomRef"
50-
xmlBytes, err = xml.Marshal(bomRef)
51-
assert.NoError(t, err)
52-
assert.Equal(t, "<BOMReference ref=\"bomRef\"></BOMReference>", string(xmlBytes))
53-
}
54-
55-
func TestBOMReference_UnmarshalXML(t *testing.T) {
56-
// Unmarshal empty bomRef
57-
bomRef := new(BOMReference)
58-
err := xml.Unmarshal([]byte("<BOMReference ref=\"\"></BOMReference>"), bomRef)
59-
require.NoError(t, err)
60-
require.Equal(t, "", string(*bomRef))
61-
62-
// Unmarshal bomRef
63-
err = xml.Unmarshal([]byte("<BOMReference ref=\"bomRef\"></BOMReference>"), bomRef)
64-
require.NoError(t, err)
65-
require.Equal(t, "bomRef", string(*bomRef))
66-
}
67-
68-
func TestCopyright_MarshalXML(t *testing.T) {
69-
// Marshal empty copyright
70-
copyright := Copyright{}
71-
xmlBytes, err := xml.Marshal(copyright)
72-
require.NoError(t, err)
73-
require.Equal(t, "<Copyright></Copyright>", string(xmlBytes))
74-
75-
// Marshal copyright
76-
copyright.Text = "copyright"
77-
xmlBytes, err = xml.Marshal(copyright)
78-
require.NoError(t, err)
79-
require.Equal(t, "<Copyright>copyright</Copyright>", string(xmlBytes))
80-
}
81-
82-
func TestCopyright_UnmarshalXML(t *testing.T) {
83-
// Unmarshal empty copyright
84-
copyright := new(Copyright)
85-
err := xml.Unmarshal([]byte("<Copyright></Copyright>"), copyright)
86-
require.NoError(t, err)
87-
require.Equal(t, "", copyright.Text)
88-
89-
// Unmarshal copyright
90-
err = xml.Unmarshal([]byte("<Copyright>copyright</Copyright>"), copyright)
91-
require.NoError(t, err)
92-
require.Equal(t, "copyright", copyright.Text)
93-
}
94-
95-
func TestDependency_MarshalJSON(t *testing.T) {
96-
// Marshal empty dependency
97-
dependency := Dependency{}
98-
jsonBytes, err := json.Marshal(dependency)
99-
assert.NoError(t, err)
100-
assert.Equal(t, "{\"ref\":\"\"}", string(jsonBytes))
101-
102-
// Marshal dependency with empty dependencies
103-
dependency = Dependency{
104-
Ref: "dependencyRef",
105-
Dependencies: &[]Dependency{},
106-
}
107-
jsonBytes, err = json.Marshal(dependency)
108-
assert.NoError(t, err)
109-
assert.Equal(t, "{\"ref\":\"dependencyRef\"}", string(jsonBytes))
110-
111-
// Marshal dependency with dependencies
112-
dependency = Dependency{
113-
Ref: "dependencyRef",
114-
Dependencies: &[]Dependency{
115-
{Ref: "transitiveDependencyRef"},
116-
},
117-
}
118-
jsonBytes, err = json.Marshal(dependency)
119-
assert.NoError(t, err)
120-
assert.Equal(t, "{\"ref\":\"dependencyRef\",\"dependsOn\":[\"transitiveDependencyRef\"]}", string(jsonBytes))
121-
}
122-
123-
func TestDependency_UnmarshalJSON(t *testing.T) {
124-
// Unmarshal empty dependency
125-
dependency := new(Dependency)
126-
err := json.Unmarshal([]byte("{}"), dependency)
127-
assert.NoError(t, err)
128-
assert.Equal(t, "", dependency.Ref)
129-
assert.Nil(t, dependency.Dependencies)
130-
131-
// Unmarshal dependency with empty dependencies
132-
dependency = new(Dependency)
133-
err = json.Unmarshal([]byte("{\"ref\":\"dependencyRef\",\"dependsOn\":[]}"), dependency)
134-
assert.NoError(t, err)
135-
assert.Equal(t, "dependencyRef", dependency.Ref)
136-
assert.Nil(t, dependency.Dependencies)
137-
138-
// Unmarshal dependency with dependencies
139-
dependency = new(Dependency)
140-
err = json.Unmarshal([]byte("{\"ref\":\"dependencyRef\",\"dependsOn\":[\"transitiveDependencyRef\"]}"), dependency)
141-
assert.NoError(t, err)
142-
assert.Equal(t, "dependencyRef", dependency.Ref)
143-
assert.Equal(t, 1, len(*dependency.Dependencies))
144-
assert.Equal(t, "transitiveDependencyRef", (*dependency.Dependencies)[0].Ref)
145-
}
146-
147-
func TestLicenses_MarshalXML(t *testing.T) {
148-
// Marshal license and expressions
149-
licenses := Licenses{
150-
LicenseChoice{
151-
Expression: "expressionValue1",
152-
},
153-
LicenseChoice{
154-
License: &License{
155-
ID: "licenseID",
156-
URL: "licenseURL",
157-
},
158-
},
159-
LicenseChoice{
160-
Expression: "expressionValue2",
161-
},
162-
}
163-
xmlBytes, err := xml.MarshalIndent(licenses, "", " ")
164-
assert.NoError(t, err)
165-
assert.Equal(t, `<Licenses>
166-
<expression>expressionValue1</expression>
167-
<license>
168-
<id>licenseID</id>
169-
<url>licenseURL</url>
170-
</license>
171-
<expression>expressionValue2</expression>
172-
</Licenses>`, string(xmlBytes))
173-
174-
// Should return error when both license and expression are set on an element
175-
licenses = Licenses{
176-
LicenseChoice{
177-
License: &License{
178-
ID: "licenseID",
179-
},
180-
Expression: "expressionValue",
181-
},
182-
}
183-
_, err = xml.Marshal(licenses)
184-
assert.Error(t, err)
185-
186-
// Should encode nothing when empty
187-
licenses = Licenses{}
188-
xmlBytes, err = xml.Marshal(licenses)
189-
assert.NoError(t, err)
190-
assert.Nil(t, xmlBytes)
191-
}
192-
193-
func TestLicenses_UnmarshalXML(t *testing.T) {
194-
// Unmarshal license and expressions
195-
licenses := new(Licenses)
196-
err := xml.Unmarshal([]byte(`
197-
<Licenses>
198-
<expression>expressionValue1</expression>
199-
<license>
200-
<id>licenseID</id>
201-
<url>licenseURL</url>
202-
</license>
203-
<expression>expressionValue2</expression>
204-
</Licenses>`), licenses)
205-
assert.NoError(t, err)
206-
assert.Len(t, *licenses, 3)
207-
assert.Nil(t, (*licenses)[0].License)
208-
assert.Equal(t, "expressionValue1", (*licenses)[0].Expression)
209-
assert.NotNil(t, (*licenses)[1].License)
210-
assert.Equal(t, "licenseID", (*licenses)[1].License.ID)
211-
assert.Equal(t, "licenseURL", (*licenses)[1].License.URL)
212-
assert.Empty(t, (*licenses)[1].Expression)
213-
assert.Nil(t, (*licenses)[2].License)
214-
assert.Equal(t, "expressionValue2", (*licenses)[2].Expression)
215-
216-
// Unmarshal empty licenses
217-
licenses = new(Licenses)
218-
err = xml.Unmarshal([]byte("<Licenses></Licenses>"), licenses)
219-
assert.NoError(t, err)
220-
assert.Empty(t, *licenses)
221-
222-
// Should return error when an element is neither license nor expression
223-
licenses = new(Licenses)
224-
err = xml.Unmarshal([]byte("<Licenses><somethingElse>expressionValue</somethingElse></Licenses>"), licenses)
225-
assert.Error(t, err)
226-
}
227-
22838
func TestVulnerability_Properties(t *testing.T) {
22939
// GIVEN
23040
properties := []Property{}

‎cyclonedx_xml.go

+155
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
// This file is part of CycloneDX Go
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the “License”);
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an “AS IS” BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
//
15+
// SPDX-License-Identifier: Apache-2.0
16+
// Copyright (c) OWASP Foundation. All Rights Reserved.
17+
18+
package cyclonedx
19+
20+
import (
21+
"encoding/xml"
22+
"errors"
23+
"fmt"
24+
"io"
25+
)
26+
27+
// bomReferenceXML is temporarily used for marshalling and unmarshalling BOMReference instances to and from XML
28+
type bomReferenceXML struct {
29+
Ref string `json:"-" xml:"ref,attr"`
30+
}
31+
32+
func (b BOMReference) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
33+
return e.EncodeElement(bomReferenceXML{Ref: string(b)}, start)
34+
}
35+
36+
func (b *BOMReference) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
37+
bXML := bomReferenceXML{}
38+
if err := d.DecodeElement(&bXML, &start); err != nil {
39+
return err
40+
}
41+
*b = BOMReference(bXML.Ref)
42+
return nil
43+
}
44+
45+
func (c Copyright) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
46+
return e.EncodeElement(c.Text, start)
47+
}
48+
49+
func (c *Copyright) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
50+
var text string
51+
if err := d.DecodeElement(&text, &start); err != nil {
52+
return err
53+
}
54+
(*c).Text = text
55+
return nil
56+
}
57+
58+
func (l Licenses) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
59+
if len(l) == 0 {
60+
return nil
61+
}
62+
63+
if err := e.EncodeToken(start); err != nil {
64+
return err
65+
}
66+
67+
for _, choice := range l {
68+
if choice.License != nil && choice.Expression != "" {
69+
return fmt.Errorf("either license or expression must be set, but not both")
70+
}
71+
72+
if choice.License != nil {
73+
if err := e.EncodeElement(choice.License, xml.StartElement{Name: xml.Name{Local: "license"}}); err != nil {
74+
return err
75+
}
76+
} else if choice.Expression != "" {
77+
if err := e.EncodeElement(choice.Expression, xml.StartElement{Name: xml.Name{Local: "expression"}}); err != nil {
78+
return err
79+
}
80+
}
81+
}
82+
83+
return e.EncodeToken(start.End())
84+
}
85+
86+
func (l *Licenses) UnmarshalXML(d *xml.Decoder, _ xml.StartElement) error {
87+
licenses := make([]LicenseChoice, 0)
88+
89+
for {
90+
token, err := d.Token()
91+
if err != nil {
92+
if errors.Is(err, io.EOF) {
93+
break
94+
}
95+
return err
96+
}
97+
98+
switch tokenType := token.(type) {
99+
case xml.StartElement:
100+
if tokenType.Name.Local == "expression" {
101+
var expression string
102+
if err = d.DecodeElement(&expression, &tokenType); err != nil {
103+
return err
104+
}
105+
licenses = append(licenses, LicenseChoice{Expression: expression})
106+
} else if tokenType.Name.Local == "license" {
107+
var license License
108+
if err = d.DecodeElement(&license, &tokenType); err != nil {
109+
return err
110+
}
111+
licenses = append(licenses, LicenseChoice{License: &license})
112+
} else {
113+
return fmt.Errorf("unknown element: %s", tokenType.Name.Local)
114+
}
115+
}
116+
}
117+
118+
*l = licenses
119+
return nil
120+
}
121+
122+
func (sv SpecVersion) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
123+
return e.EncodeElement(sv.String(), start)
124+
}
125+
126+
func (sv *SpecVersion) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
127+
var v string
128+
err := d.DecodeElement(&v, &start)
129+
if err != nil {
130+
return err
131+
}
132+
133+
switch v {
134+
case SpecVersion1_0.String():
135+
*sv = SpecVersion1_0
136+
case SpecVersion1_1.String():
137+
*sv = SpecVersion1_1
138+
case SpecVersion1_2.String():
139+
*sv = SpecVersion1_2
140+
case SpecVersion1_3.String():
141+
*sv = SpecVersion1_3
142+
case SpecVersion1_4.String():
143+
*sv = SpecVersion1_4
144+
}
145+
146+
return nil
147+
}
148+
149+
var xmlNamespaces = map[SpecVersion]string{
150+
SpecVersion1_0: "http://cyclonedx.org/schema/bom/1.0",
151+
SpecVersion1_1: "http://cyclonedx.org/schema/bom/1.1",
152+
SpecVersion1_2: "http://cyclonedx.org/schema/bom/1.2",
153+
SpecVersion1_3: "http://cyclonedx.org/schema/bom/1.3",
154+
SpecVersion1_4: "http://cyclonedx.org/schema/bom/1.4",
155+
}

‎cyclonedx_xml_test.go

+161
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
// This file is part of CycloneDX Go
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the “License”);
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an “AS IS” BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
//
15+
// SPDX-License-Identifier: Apache-2.0
16+
// Copyright (c) OWASP Foundation. All Rights Reserved.
17+
18+
package cyclonedx
19+
20+
import (
21+
"encoding/xml"
22+
"testing"
23+
24+
"github.com/stretchr/testify/assert"
25+
"github.com/stretchr/testify/require"
26+
)
27+
28+
func TestBOMReference_MarshalXML(t *testing.T) {
29+
// Marshal empty bomRef
30+
bomRef := BOMReference("")
31+
xmlBytes, err := xml.Marshal(bomRef)
32+
assert.NoError(t, err)
33+
assert.Equal(t, "<BOMReference ref=\"\"></BOMReference>", string(xmlBytes))
34+
35+
// Marshal bomRef
36+
bomRef = "bomRef"
37+
xmlBytes, err = xml.Marshal(bomRef)
38+
assert.NoError(t, err)
39+
assert.Equal(t, "<BOMReference ref=\"bomRef\"></BOMReference>", string(xmlBytes))
40+
}
41+
42+
func TestBOMReference_UnmarshalXML(t *testing.T) {
43+
// Unmarshal empty bomRef
44+
bomRef := new(BOMReference)
45+
err := xml.Unmarshal([]byte("<BOMReference ref=\"\"></BOMReference>"), bomRef)
46+
require.NoError(t, err)
47+
require.Equal(t, "", string(*bomRef))
48+
49+
// Unmarshal bomRef
50+
err = xml.Unmarshal([]byte("<BOMReference ref=\"bomRef\"></BOMReference>"), bomRef)
51+
require.NoError(t, err)
52+
require.Equal(t, "bomRef", string(*bomRef))
53+
}
54+
55+
func TestCopyright_MarshalXML(t *testing.T) {
56+
// Marshal empty copyright
57+
copyright := Copyright{}
58+
xmlBytes, err := xml.Marshal(copyright)
59+
require.NoError(t, err)
60+
require.Equal(t, "<Copyright></Copyright>", string(xmlBytes))
61+
62+
// Marshal copyright
63+
copyright.Text = "copyright"
64+
xmlBytes, err = xml.Marshal(copyright)
65+
require.NoError(t, err)
66+
require.Equal(t, "<Copyright>copyright</Copyright>", string(xmlBytes))
67+
}
68+
69+
func TestCopyright_UnmarshalXML(t *testing.T) {
70+
// Unmarshal empty copyright
71+
copyright := new(Copyright)
72+
err := xml.Unmarshal([]byte("<Copyright></Copyright>"), copyright)
73+
require.NoError(t, err)
74+
require.Equal(t, "", copyright.Text)
75+
76+
// Unmarshal copyright
77+
err = xml.Unmarshal([]byte("<Copyright>copyright</Copyright>"), copyright)
78+
require.NoError(t, err)
79+
require.Equal(t, "copyright", copyright.Text)
80+
}
81+
82+
func TestLicenses_MarshalXML(t *testing.T) {
83+
// Marshal license and expressions
84+
licenses := Licenses{
85+
LicenseChoice{
86+
Expression: "expressionValue1",
87+
},
88+
LicenseChoice{
89+
License: &License{
90+
ID: "licenseID",
91+
URL: "licenseURL",
92+
},
93+
},
94+
LicenseChoice{
95+
Expression: "expressionValue2",
96+
},
97+
}
98+
xmlBytes, err := xml.MarshalIndent(licenses, "", " ")
99+
assert.NoError(t, err)
100+
assert.Equal(t, `<Licenses>
101+
<expression>expressionValue1</expression>
102+
<license>
103+
<id>licenseID</id>
104+
<url>licenseURL</url>
105+
</license>
106+
<expression>expressionValue2</expression>
107+
</Licenses>`, string(xmlBytes))
108+
109+
// Should return error when both license and expression are set on an element
110+
licenses = Licenses{
111+
LicenseChoice{
112+
License: &License{
113+
ID: "licenseID",
114+
},
115+
Expression: "expressionValue",
116+
},
117+
}
118+
_, err = xml.Marshal(licenses)
119+
assert.Error(t, err)
120+
121+
// Should encode nothing when empty
122+
licenses = Licenses{}
123+
xmlBytes, err = xml.Marshal(licenses)
124+
assert.NoError(t, err)
125+
assert.Nil(t, xmlBytes)
126+
}
127+
128+
func TestLicenses_UnmarshalXML(t *testing.T) {
129+
// Unmarshal license and expressions
130+
licenses := new(Licenses)
131+
err := xml.Unmarshal([]byte(`
132+
<Licenses>
133+
<expression>expressionValue1</expression>
134+
<license>
135+
<id>licenseID</id>
136+
<url>licenseURL</url>
137+
</license>
138+
<expression>expressionValue2</expression>
139+
</Licenses>`), licenses)
140+
assert.NoError(t, err)
141+
assert.Len(t, *licenses, 3)
142+
assert.Nil(t, (*licenses)[0].License)
143+
assert.Equal(t, "expressionValue1", (*licenses)[0].Expression)
144+
assert.NotNil(t, (*licenses)[1].License)
145+
assert.Equal(t, "licenseID", (*licenses)[1].License.ID)
146+
assert.Equal(t, "licenseURL", (*licenses)[1].License.URL)
147+
assert.Empty(t, (*licenses)[1].Expression)
148+
assert.Nil(t, (*licenses)[2].License)
149+
assert.Equal(t, "expressionValue2", (*licenses)[2].Expression)
150+
151+
// Unmarshal empty licenses
152+
licenses = new(Licenses)
153+
err = xml.Unmarshal([]byte("<Licenses></Licenses>"), licenses)
154+
assert.NoError(t, err)
155+
assert.Empty(t, *licenses)
156+
157+
// Should return error when an element is neither license nor expression
158+
licenses = new(Licenses)
159+
err = xml.Unmarshal([]byte("<Licenses><somethingElse>expressionValue</somethingElse></Licenses>"), licenses)
160+
assert.Error(t, err)
161+
}

0 commit comments

Comments
 (0)
Please sign in to comment.