diff --git a/cyclonedx.go b/cyclonedx.go
index 5eb9393..69c2b18 100644
--- a/cyclonedx.go
+++ b/cyclonedx.go
@@ -18,11 +18,7 @@
package cyclonedx
import (
- "encoding/json"
"encoding/xml"
- "errors"
- "fmt"
- "io"
"regexp"
)
@@ -98,24 +94,6 @@ func Bool(value bool) *bool {
type BOMReference string
-// bomReferenceXML is temporarily used for marshalling and unmarshalling BOMReference instances to and from XML
-type bomReferenceXML struct {
- Ref string `json:"-" xml:"ref,attr"`
-}
-
-func (b BOMReference) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
- return e.EncodeElement(bomReferenceXML{Ref: string(b)}, start)
-}
-
-func (b *BOMReference) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
- bXML := bomReferenceXML{}
- if err := d.DecodeElement(&bXML, &start); err != nil {
- return err
- }
- *b = BOMReference(bXML.Ref)
- return nil
-}
-
type ComponentType string
const (
@@ -185,19 +163,6 @@ type Copyright struct {
Text string `json:"text" xml:"-"`
}
-func (c Copyright) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
- return e.EncodeElement(c.Text, start)
-}
-
-func (c *Copyright) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
- var text string
- if err := d.DecodeElement(&text, &start); err != nil {
- return err
- }
- (*c).Text = text
- return nil
-}
-
type Credits struct {
Organizations *[]OrganizationalEntity `json:"organizations,omitempty" xml:"organizations>organization,omitempty"`
Individuals *[]OrganizationalContact `json:"individuals,omitempty" xml:"individuals>individual,omitempty"`
@@ -222,52 +187,6 @@ type Dependency struct {
Dependencies *[]Dependency `xml:"dependency,omitempty"`
}
-// dependencyJSON is temporarily used for marshalling and unmarshalling Dependency instances to and from JSON
-type dependencyJSON struct {
- Ref string `json:"ref"`
- DependsOn []string `json:"dependsOn,omitempty"`
-}
-
-func (d Dependency) MarshalJSON() ([]byte, error) {
- if d.Dependencies == nil || len(*d.Dependencies) == 0 {
- return json.Marshal(&dependencyJSON{
- Ref: d.Ref,
- })
- }
-
- dependencyRefs := make([]string, len(*d.Dependencies))
- for i, dependency := range *d.Dependencies {
- dependencyRefs[i] = dependency.Ref
- }
-
- return json.Marshal(&dependencyJSON{
- Ref: d.Ref,
- DependsOn: dependencyRefs,
- })
-}
-
-func (d *Dependency) UnmarshalJSON(bytes []byte) error {
- dependency := new(dependencyJSON)
- if err := json.Unmarshal(bytes, dependency); err != nil {
- return err
- }
- d.Ref = dependency.Ref
-
- if len(dependency.DependsOn) == 0 {
- return nil
- }
-
- dependencies := make([]Dependency, len(dependency.DependsOn))
- for i, dep := range dependency.DependsOn {
- dependencies[i] = Dependency{
- Ref: dep,
- }
- }
- d.Dependencies = &dependencies
-
- return nil
-}
-
type Diff struct {
Text *AttachedText `json:"text,omitempty" xml:"text,omitempty"`
URL string `json:"url,omitempty" xml:"url,omitempty"`
@@ -394,70 +313,6 @@ type License struct {
type Licenses []LicenseChoice
-func (l Licenses) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
- if len(l) == 0 {
- return nil
- }
-
- if err := e.EncodeToken(start); err != nil {
- return err
- }
-
- for _, choice := range l {
- if choice.License != nil && choice.Expression != "" {
- return fmt.Errorf("either license or expression must be set, but not both")
- }
-
- if choice.License != nil {
- if err := e.EncodeElement(choice.License, xml.StartElement{Name: xml.Name{Local: "license"}}); err != nil {
- return err
- }
- } else if choice.Expression != "" {
- if err := e.EncodeElement(choice.Expression, xml.StartElement{Name: xml.Name{Local: "expression"}}); err != nil {
- return err
- }
- }
- }
-
- return e.EncodeToken(start.End())
-}
-
-func (l *Licenses) UnmarshalXML(d *xml.Decoder, _ xml.StartElement) error {
- licenses := make([]LicenseChoice, 0)
-
- for {
- token, err := d.Token()
- if err != nil {
- if errors.Is(err, io.EOF) {
- break
- }
- return err
- }
-
- switch tokenType := token.(type) {
- case xml.StartElement:
- if tokenType.Name.Local == "expression" {
- var expression string
- if err = d.DecodeElement(&expression, &tokenType); err != nil {
- return err
- }
- licenses = append(licenses, LicenseChoice{Expression: expression})
- } else if tokenType.Name.Local == "license" {
- var license License
- if err = d.DecodeElement(&license, &tokenType); err != nil {
- return err
- }
- licenses = append(licenses, LicenseChoice{License: &license})
- } else {
- return fmt.Errorf("unknown element: %s", tokenType.Name.Local)
- }
- }
- }
-
- *l = licenses
- return nil
-}
-
type LicenseChoice struct {
License *License `json:"license,omitempty" xml:"-"`
Expression string `json:"expression,omitempty" xml:"-"`
@@ -599,60 +454,6 @@ const (
SpecVersion1_4 // 1.4
)
-func (sv SpecVersion) MarshalJSON() ([]byte, error) {
- return json.Marshal(sv.String())
-}
-
-func (sv *SpecVersion) UnmarshalJSON(bytes []byte) error {
- var v string
- err := json.Unmarshal(bytes, &v)
- if err != nil {
- return err
- }
-
- switch v {
- case SpecVersion1_0.String():
- *sv = SpecVersion1_0
- case SpecVersion1_1.String():
- *sv = SpecVersion1_1
- case SpecVersion1_2.String():
- *sv = SpecVersion1_2
- case SpecVersion1_3.String():
- *sv = SpecVersion1_3
- case SpecVersion1_4.String():
- *sv = SpecVersion1_4
- }
-
- return nil
-}
-
-func (sv SpecVersion) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
- return e.EncodeElement(sv.String(), start)
-}
-
-func (sv *SpecVersion) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
- var v string
- err := d.DecodeElement(&v, &start)
- if err != nil {
- return err
- }
-
- switch v {
- case SpecVersion1_0.String():
- *sv = SpecVersion1_0
- case SpecVersion1_1.String():
- *sv = SpecVersion1_1
- case SpecVersion1_2.String():
- *sv = SpecVersion1_2
- case SpecVersion1_3.String():
- *sv = SpecVersion1_3
- case SpecVersion1_4.String():
- *sv = SpecVersion1_4
- }
-
- return nil
-}
-
type SWID struct {
Text *AttachedText `json:"text,omitempty" xml:"text,omitempty"`
URL string `json:"url,omitempty" xml:"url,attr,omitempty"`
@@ -720,11 +521,3 @@ const (
VulnerabilityStatusAffected VulnerabilityStatus = "affected"
VulnerabilityStatusNotAffected VulnerabilityStatus = "unaffected"
)
-
-var xmlNamespaces = map[SpecVersion]string{
- SpecVersion1_0: "http://cyclonedx.org/schema/bom/1.0",
- SpecVersion1_1: "http://cyclonedx.org/schema/bom/1.1",
- SpecVersion1_2: "http://cyclonedx.org/schema/bom/1.2",
- SpecVersion1_3: "http://cyclonedx.org/schema/bom/1.3",
- SpecVersion1_4: "http://cyclonedx.org/schema/bom/1.4",
-}
diff --git a/cyclonedx_json.go b/cyclonedx_json.go
new file mode 100644
index 0000000..3efdbde
--- /dev/null
+++ b/cyclonedx_json.go
@@ -0,0 +1,93 @@
+// This file is part of CycloneDX Go
+//
+// Licensed under the Apache License, Version 2.0 (the “License”);
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an “AS IS” BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// SPDX-License-Identifier: Apache-2.0
+// Copyright (c) OWASP Foundation. All Rights Reserved.
+
+package cyclonedx
+
+import "encoding/json"
+
+// dependencyJSON is temporarily used for marshalling and unmarshalling Dependency instances to and from JSON
+type dependencyJSON struct {
+ Ref string `json:"ref"`
+ DependsOn []string `json:"dependsOn,omitempty"`
+}
+
+func (d Dependency) MarshalJSON() ([]byte, error) {
+ if d.Dependencies == nil || len(*d.Dependencies) == 0 {
+ return json.Marshal(&dependencyJSON{
+ Ref: d.Ref,
+ })
+ }
+
+ dependencyRefs := make([]string, len(*d.Dependencies))
+ for i, dependency := range *d.Dependencies {
+ dependencyRefs[i] = dependency.Ref
+ }
+
+ return json.Marshal(&dependencyJSON{
+ Ref: d.Ref,
+ DependsOn: dependencyRefs,
+ })
+}
+
+func (d *Dependency) UnmarshalJSON(bytes []byte) error {
+ dependency := new(dependencyJSON)
+ if err := json.Unmarshal(bytes, dependency); err != nil {
+ return err
+ }
+ d.Ref = dependency.Ref
+
+ if len(dependency.DependsOn) == 0 {
+ return nil
+ }
+
+ dependencies := make([]Dependency, len(dependency.DependsOn))
+ for i, dep := range dependency.DependsOn {
+ dependencies[i] = Dependency{
+ Ref: dep,
+ }
+ }
+ d.Dependencies = &dependencies
+
+ return nil
+}
+
+func (sv SpecVersion) MarshalJSON() ([]byte, error) {
+ return json.Marshal(sv.String())
+}
+
+func (sv *SpecVersion) UnmarshalJSON(bytes []byte) error {
+ var v string
+ err := json.Unmarshal(bytes, &v)
+ if err != nil {
+ return err
+ }
+
+ switch v {
+ case SpecVersion1_0.String():
+ *sv = SpecVersion1_0
+ case SpecVersion1_1.String():
+ *sv = SpecVersion1_1
+ case SpecVersion1_2.String():
+ *sv = SpecVersion1_2
+ case SpecVersion1_3.String():
+ *sv = SpecVersion1_3
+ case SpecVersion1_4.String():
+ *sv = SpecVersion1_4
+ }
+
+ return nil
+}
diff --git a/cyclonedx_json_test.go b/cyclonedx_json_test.go
new file mode 100644
index 0000000..7d0099e
--- /dev/null
+++ b/cyclonedx_json_test.go
@@ -0,0 +1,77 @@
+// This file is part of CycloneDX Go
+//
+// Licensed under the Apache License, Version 2.0 (the “License”);
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an “AS IS” BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// SPDX-License-Identifier: Apache-2.0
+// Copyright (c) OWASP Foundation. All Rights Reserved.
+
+package cyclonedx
+
+import (
+ "encoding/json"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestDependency_MarshalJSON(t *testing.T) {
+ // Marshal empty dependency
+ dependency := Dependency{}
+ jsonBytes, err := json.Marshal(dependency)
+ assert.NoError(t, err)
+ assert.Equal(t, "{\"ref\":\"\"}", string(jsonBytes))
+
+ // Marshal dependency with empty dependencies
+ dependency = Dependency{
+ Ref: "dependencyRef",
+ Dependencies: &[]Dependency{},
+ }
+ jsonBytes, err = json.Marshal(dependency)
+ assert.NoError(t, err)
+ assert.Equal(t, "{\"ref\":\"dependencyRef\"}", string(jsonBytes))
+
+ // Marshal dependency with dependencies
+ dependency = Dependency{
+ Ref: "dependencyRef",
+ Dependencies: &[]Dependency{
+ {Ref: "transitiveDependencyRef"},
+ },
+ }
+ jsonBytes, err = json.Marshal(dependency)
+ assert.NoError(t, err)
+ assert.Equal(t, "{\"ref\":\"dependencyRef\",\"dependsOn\":[\"transitiveDependencyRef\"]}", string(jsonBytes))
+}
+
+func TestDependency_UnmarshalJSON(t *testing.T) {
+ // Unmarshal empty dependency
+ dependency := new(Dependency)
+ err := json.Unmarshal([]byte("{}"), dependency)
+ assert.NoError(t, err)
+ assert.Equal(t, "", dependency.Ref)
+ assert.Nil(t, dependency.Dependencies)
+
+ // Unmarshal dependency with empty dependencies
+ dependency = new(Dependency)
+ err = json.Unmarshal([]byte("{\"ref\":\"dependencyRef\",\"dependsOn\":[]}"), dependency)
+ assert.NoError(t, err)
+ assert.Equal(t, "dependencyRef", dependency.Ref)
+ assert.Nil(t, dependency.Dependencies)
+
+ // Unmarshal dependency with dependencies
+ dependency = new(Dependency)
+ err = json.Unmarshal([]byte("{\"ref\":\"dependencyRef\",\"dependsOn\":[\"transitiveDependencyRef\"]}"), dependency)
+ assert.NoError(t, err)
+ assert.Equal(t, "dependencyRef", dependency.Ref)
+ assert.Equal(t, 1, len(*dependency.Dependencies))
+ assert.Equal(t, "transitiveDependencyRef", (*dependency.Dependencies)[0].Ref)
+}
diff --git a/cyclonedx_test.go b/cyclonedx_test.go
index 7605d47..bfcb8fd 100644
--- a/cyclonedx_test.go
+++ b/cyclonedx_test.go
@@ -18,8 +18,6 @@
package cyclonedx
import (
- "encoding/json"
- "encoding/xml"
"fmt"
"os/exec"
"strings"
@@ -27,7 +25,6 @@ import (
"github.com/bradleyjkemp/cupaloy/v2"
"github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
)
var snapShooter = cupaloy.NewDefaultConfig().
@@ -38,193 +35,6 @@ func TestBool(t *testing.T) {
assert.Equal(t, false, *Bool(false))
}
-func TestBOMReference_MarshalXML(t *testing.T) {
- // Marshal empty bomRef
- bomRef := BOMReference("")
- xmlBytes, err := xml.Marshal(bomRef)
- assert.NoError(t, err)
- assert.Equal(t, "", string(xmlBytes))
-
- // Marshal bomRef
- bomRef = "bomRef"
- xmlBytes, err = xml.Marshal(bomRef)
- assert.NoError(t, err)
- assert.Equal(t, "", string(xmlBytes))
-}
-
-func TestBOMReference_UnmarshalXML(t *testing.T) {
- // Unmarshal empty bomRef
- bomRef := new(BOMReference)
- err := xml.Unmarshal([]byte(""), bomRef)
- require.NoError(t, err)
- require.Equal(t, "", string(*bomRef))
-
- // Unmarshal bomRef
- err = xml.Unmarshal([]byte(""), bomRef)
- require.NoError(t, err)
- require.Equal(t, "bomRef", string(*bomRef))
-}
-
-func TestCopyright_MarshalXML(t *testing.T) {
- // Marshal empty copyright
- copyright := Copyright{}
- xmlBytes, err := xml.Marshal(copyright)
- require.NoError(t, err)
- require.Equal(t, "", string(xmlBytes))
-
- // Marshal copyright
- copyright.Text = "copyright"
- xmlBytes, err = xml.Marshal(copyright)
- require.NoError(t, err)
- require.Equal(t, "copyright", string(xmlBytes))
-}
-
-func TestCopyright_UnmarshalXML(t *testing.T) {
- // Unmarshal empty copyright
- copyright := new(Copyright)
- err := xml.Unmarshal([]byte(""), copyright)
- require.NoError(t, err)
- require.Equal(t, "", copyright.Text)
-
- // Unmarshal copyright
- err = xml.Unmarshal([]byte("copyright"), copyright)
- require.NoError(t, err)
- require.Equal(t, "copyright", copyright.Text)
-}
-
-func TestDependency_MarshalJSON(t *testing.T) {
- // Marshal empty dependency
- dependency := Dependency{}
- jsonBytes, err := json.Marshal(dependency)
- assert.NoError(t, err)
- assert.Equal(t, "{\"ref\":\"\"}", string(jsonBytes))
-
- // Marshal dependency with empty dependencies
- dependency = Dependency{
- Ref: "dependencyRef",
- Dependencies: &[]Dependency{},
- }
- jsonBytes, err = json.Marshal(dependency)
- assert.NoError(t, err)
- assert.Equal(t, "{\"ref\":\"dependencyRef\"}", string(jsonBytes))
-
- // Marshal dependency with dependencies
- dependency = Dependency{
- Ref: "dependencyRef",
- Dependencies: &[]Dependency{
- {Ref: "transitiveDependencyRef"},
- },
- }
- jsonBytes, err = json.Marshal(dependency)
- assert.NoError(t, err)
- assert.Equal(t, "{\"ref\":\"dependencyRef\",\"dependsOn\":[\"transitiveDependencyRef\"]}", string(jsonBytes))
-}
-
-func TestDependency_UnmarshalJSON(t *testing.T) {
- // Unmarshal empty dependency
- dependency := new(Dependency)
- err := json.Unmarshal([]byte("{}"), dependency)
- assert.NoError(t, err)
- assert.Equal(t, "", dependency.Ref)
- assert.Nil(t, dependency.Dependencies)
-
- // Unmarshal dependency with empty dependencies
- dependency = new(Dependency)
- err = json.Unmarshal([]byte("{\"ref\":\"dependencyRef\",\"dependsOn\":[]}"), dependency)
- assert.NoError(t, err)
- assert.Equal(t, "dependencyRef", dependency.Ref)
- assert.Nil(t, dependency.Dependencies)
-
- // Unmarshal dependency with dependencies
- dependency = new(Dependency)
- err = json.Unmarshal([]byte("{\"ref\":\"dependencyRef\",\"dependsOn\":[\"transitiveDependencyRef\"]}"), dependency)
- assert.NoError(t, err)
- assert.Equal(t, "dependencyRef", dependency.Ref)
- assert.Equal(t, 1, len(*dependency.Dependencies))
- assert.Equal(t, "transitiveDependencyRef", (*dependency.Dependencies)[0].Ref)
-}
-
-func TestLicenses_MarshalXML(t *testing.T) {
- // Marshal license and expressions
- licenses := Licenses{
- LicenseChoice{
- Expression: "expressionValue1",
- },
- LicenseChoice{
- License: &License{
- ID: "licenseID",
- URL: "licenseURL",
- },
- },
- LicenseChoice{
- Expression: "expressionValue2",
- },
- }
- xmlBytes, err := xml.MarshalIndent(licenses, "", " ")
- assert.NoError(t, err)
- assert.Equal(t, `
- expressionValue1
-
- licenseID
- licenseURL
-
- expressionValue2
-`, string(xmlBytes))
-
- // Should return error when both license and expression are set on an element
- licenses = Licenses{
- LicenseChoice{
- License: &License{
- ID: "licenseID",
- },
- Expression: "expressionValue",
- },
- }
- _, err = xml.Marshal(licenses)
- assert.Error(t, err)
-
- // Should encode nothing when empty
- licenses = Licenses{}
- xmlBytes, err = xml.Marshal(licenses)
- assert.NoError(t, err)
- assert.Nil(t, xmlBytes)
-}
-
-func TestLicenses_UnmarshalXML(t *testing.T) {
- // Unmarshal license and expressions
- licenses := new(Licenses)
- err := xml.Unmarshal([]byte(`
-
- expressionValue1
-
- licenseID
- licenseURL
-
- expressionValue2
-`), licenses)
- assert.NoError(t, err)
- assert.Len(t, *licenses, 3)
- assert.Nil(t, (*licenses)[0].License)
- assert.Equal(t, "expressionValue1", (*licenses)[0].Expression)
- assert.NotNil(t, (*licenses)[1].License)
- assert.Equal(t, "licenseID", (*licenses)[1].License.ID)
- assert.Equal(t, "licenseURL", (*licenses)[1].License.URL)
- assert.Empty(t, (*licenses)[1].Expression)
- assert.Nil(t, (*licenses)[2].License)
- assert.Equal(t, "expressionValue2", (*licenses)[2].Expression)
-
- // Unmarshal empty licenses
- licenses = new(Licenses)
- err = xml.Unmarshal([]byte(""), licenses)
- assert.NoError(t, err)
- assert.Empty(t, *licenses)
-
- // Should return error when an element is neither license nor expression
- licenses = new(Licenses)
- err = xml.Unmarshal([]byte("expressionValue"), licenses)
- assert.Error(t, err)
-}
-
func TestVulnerability_Properties(t *testing.T) {
// GIVEN
properties := []Property{}
diff --git a/cyclonedx_xml.go b/cyclonedx_xml.go
new file mode 100644
index 0000000..83db3c5
--- /dev/null
+++ b/cyclonedx_xml.go
@@ -0,0 +1,155 @@
+// This file is part of CycloneDX Go
+//
+// Licensed under the Apache License, Version 2.0 (the “License”);
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an “AS IS” BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// SPDX-License-Identifier: Apache-2.0
+// Copyright (c) OWASP Foundation. All Rights Reserved.
+
+package cyclonedx
+
+import (
+ "encoding/xml"
+ "errors"
+ "fmt"
+ "io"
+)
+
+// bomReferenceXML is temporarily used for marshalling and unmarshalling BOMReference instances to and from XML
+type bomReferenceXML struct {
+ Ref string `json:"-" xml:"ref,attr"`
+}
+
+func (b BOMReference) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
+ return e.EncodeElement(bomReferenceXML{Ref: string(b)}, start)
+}
+
+func (b *BOMReference) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
+ bXML := bomReferenceXML{}
+ if err := d.DecodeElement(&bXML, &start); err != nil {
+ return err
+ }
+ *b = BOMReference(bXML.Ref)
+ return nil
+}
+
+func (c Copyright) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
+ return e.EncodeElement(c.Text, start)
+}
+
+func (c *Copyright) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
+ var text string
+ if err := d.DecodeElement(&text, &start); err != nil {
+ return err
+ }
+ (*c).Text = text
+ return nil
+}
+
+func (l Licenses) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
+ if len(l) == 0 {
+ return nil
+ }
+
+ if err := e.EncodeToken(start); err != nil {
+ return err
+ }
+
+ for _, choice := range l {
+ if choice.License != nil && choice.Expression != "" {
+ return fmt.Errorf("either license or expression must be set, but not both")
+ }
+
+ if choice.License != nil {
+ if err := e.EncodeElement(choice.License, xml.StartElement{Name: xml.Name{Local: "license"}}); err != nil {
+ return err
+ }
+ } else if choice.Expression != "" {
+ if err := e.EncodeElement(choice.Expression, xml.StartElement{Name: xml.Name{Local: "expression"}}); err != nil {
+ return err
+ }
+ }
+ }
+
+ return e.EncodeToken(start.End())
+}
+
+func (l *Licenses) UnmarshalXML(d *xml.Decoder, _ xml.StartElement) error {
+ licenses := make([]LicenseChoice, 0)
+
+ for {
+ token, err := d.Token()
+ if err != nil {
+ if errors.Is(err, io.EOF) {
+ break
+ }
+ return err
+ }
+
+ switch tokenType := token.(type) {
+ case xml.StartElement:
+ if tokenType.Name.Local == "expression" {
+ var expression string
+ if err = d.DecodeElement(&expression, &tokenType); err != nil {
+ return err
+ }
+ licenses = append(licenses, LicenseChoice{Expression: expression})
+ } else if tokenType.Name.Local == "license" {
+ var license License
+ if err = d.DecodeElement(&license, &tokenType); err != nil {
+ return err
+ }
+ licenses = append(licenses, LicenseChoice{License: &license})
+ } else {
+ return fmt.Errorf("unknown element: %s", tokenType.Name.Local)
+ }
+ }
+ }
+
+ *l = licenses
+ return nil
+}
+
+func (sv SpecVersion) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
+ return e.EncodeElement(sv.String(), start)
+}
+
+func (sv *SpecVersion) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
+ var v string
+ err := d.DecodeElement(&v, &start)
+ if err != nil {
+ return err
+ }
+
+ switch v {
+ case SpecVersion1_0.String():
+ *sv = SpecVersion1_0
+ case SpecVersion1_1.String():
+ *sv = SpecVersion1_1
+ case SpecVersion1_2.String():
+ *sv = SpecVersion1_2
+ case SpecVersion1_3.String():
+ *sv = SpecVersion1_3
+ case SpecVersion1_4.String():
+ *sv = SpecVersion1_4
+ }
+
+ return nil
+}
+
+var xmlNamespaces = map[SpecVersion]string{
+ SpecVersion1_0: "http://cyclonedx.org/schema/bom/1.0",
+ SpecVersion1_1: "http://cyclonedx.org/schema/bom/1.1",
+ SpecVersion1_2: "http://cyclonedx.org/schema/bom/1.2",
+ SpecVersion1_3: "http://cyclonedx.org/schema/bom/1.3",
+ SpecVersion1_4: "http://cyclonedx.org/schema/bom/1.4",
+}
diff --git a/cyclonedx_xml_test.go b/cyclonedx_xml_test.go
new file mode 100644
index 0000000..a7f332e
--- /dev/null
+++ b/cyclonedx_xml_test.go
@@ -0,0 +1,161 @@
+// This file is part of CycloneDX Go
+//
+// Licensed under the Apache License, Version 2.0 (the “License”);
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an “AS IS” BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// SPDX-License-Identifier: Apache-2.0
+// Copyright (c) OWASP Foundation. All Rights Reserved.
+
+package cyclonedx
+
+import (
+ "encoding/xml"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestBOMReference_MarshalXML(t *testing.T) {
+ // Marshal empty bomRef
+ bomRef := BOMReference("")
+ xmlBytes, err := xml.Marshal(bomRef)
+ assert.NoError(t, err)
+ assert.Equal(t, "", string(xmlBytes))
+
+ // Marshal bomRef
+ bomRef = "bomRef"
+ xmlBytes, err = xml.Marshal(bomRef)
+ assert.NoError(t, err)
+ assert.Equal(t, "", string(xmlBytes))
+}
+
+func TestBOMReference_UnmarshalXML(t *testing.T) {
+ // Unmarshal empty bomRef
+ bomRef := new(BOMReference)
+ err := xml.Unmarshal([]byte(""), bomRef)
+ require.NoError(t, err)
+ require.Equal(t, "", string(*bomRef))
+
+ // Unmarshal bomRef
+ err = xml.Unmarshal([]byte(""), bomRef)
+ require.NoError(t, err)
+ require.Equal(t, "bomRef", string(*bomRef))
+}
+
+func TestCopyright_MarshalXML(t *testing.T) {
+ // Marshal empty copyright
+ copyright := Copyright{}
+ xmlBytes, err := xml.Marshal(copyright)
+ require.NoError(t, err)
+ require.Equal(t, "", string(xmlBytes))
+
+ // Marshal copyright
+ copyright.Text = "copyright"
+ xmlBytes, err = xml.Marshal(copyright)
+ require.NoError(t, err)
+ require.Equal(t, "copyright", string(xmlBytes))
+}
+
+func TestCopyright_UnmarshalXML(t *testing.T) {
+ // Unmarshal empty copyright
+ copyright := new(Copyright)
+ err := xml.Unmarshal([]byte(""), copyright)
+ require.NoError(t, err)
+ require.Equal(t, "", copyright.Text)
+
+ // Unmarshal copyright
+ err = xml.Unmarshal([]byte("copyright"), copyright)
+ require.NoError(t, err)
+ require.Equal(t, "copyright", copyright.Text)
+}
+
+func TestLicenses_MarshalXML(t *testing.T) {
+ // Marshal license and expressions
+ licenses := Licenses{
+ LicenseChoice{
+ Expression: "expressionValue1",
+ },
+ LicenseChoice{
+ License: &License{
+ ID: "licenseID",
+ URL: "licenseURL",
+ },
+ },
+ LicenseChoice{
+ Expression: "expressionValue2",
+ },
+ }
+ xmlBytes, err := xml.MarshalIndent(licenses, "", " ")
+ assert.NoError(t, err)
+ assert.Equal(t, `
+ expressionValue1
+
+ licenseID
+ licenseURL
+
+ expressionValue2
+`, string(xmlBytes))
+
+ // Should return error when both license and expression are set on an element
+ licenses = Licenses{
+ LicenseChoice{
+ License: &License{
+ ID: "licenseID",
+ },
+ Expression: "expressionValue",
+ },
+ }
+ _, err = xml.Marshal(licenses)
+ assert.Error(t, err)
+
+ // Should encode nothing when empty
+ licenses = Licenses{}
+ xmlBytes, err = xml.Marshal(licenses)
+ assert.NoError(t, err)
+ assert.Nil(t, xmlBytes)
+}
+
+func TestLicenses_UnmarshalXML(t *testing.T) {
+ // Unmarshal license and expressions
+ licenses := new(Licenses)
+ err := xml.Unmarshal([]byte(`
+
+ expressionValue1
+
+ licenseID
+ licenseURL
+
+ expressionValue2
+`), licenses)
+ assert.NoError(t, err)
+ assert.Len(t, *licenses, 3)
+ assert.Nil(t, (*licenses)[0].License)
+ assert.Equal(t, "expressionValue1", (*licenses)[0].Expression)
+ assert.NotNil(t, (*licenses)[1].License)
+ assert.Equal(t, "licenseID", (*licenses)[1].License.ID)
+ assert.Equal(t, "licenseURL", (*licenses)[1].License.URL)
+ assert.Empty(t, (*licenses)[1].Expression)
+ assert.Nil(t, (*licenses)[2].License)
+ assert.Equal(t, "expressionValue2", (*licenses)[2].Expression)
+
+ // Unmarshal empty licenses
+ licenses = new(Licenses)
+ err = xml.Unmarshal([]byte(""), licenses)
+ assert.NoError(t, err)
+ assert.Empty(t, *licenses)
+
+ // Should return error when an element is neither license nor expression
+ licenses = new(Licenses)
+ err = xml.Unmarshal([]byte("expressionValue"), licenses)
+ assert.Error(t, err)
+}