Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix license expression unmarshalling #5

Merged
merged 2 commits into from
Jun 24, 2021
Merged
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
94 changes: 59 additions & 35 deletions cyclonedx.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ package cyclonedx
import (
"encoding/json"
"encoding/xml"
"errors"
"fmt"
"io"
)

const (
Expand Down Expand Up @@ -109,7 +111,7 @@ type Component struct {
Description string `json:"description,omitempty" xml:"description,omitempty"`
Scope Scope `json:"scope,omitempty" xml:"scope,omitempty"`
Hashes *[]Hash `json:"hashes,omitempty" xml:"hashes>hash,omitempty"`
Licenses *[]LicenseChoice `json:"licenses,omitempty" xml:"licenses>license,omitempty"`
Licenses *Licenses `json:"licenses,omitempty" xml:"licenses,omitempty"`
Copyright string `json:"copyright,omitempty" xml:"copyright,omitempty"`
CPE string `json:"cpe,omitempty" xml:"cpe,omitempty"`
PackageURL string `json:"purl,omitempty" xml:"purl,omitempty"`
Expand Down Expand Up @@ -267,53 +269,75 @@ type License struct {
URL string `json:"url,omitempty" xml:"url,omitempty"`
}

type LicenseChoice struct {
License *License `json:"license,omitempty" xml:"-"`
Expression string `json:"expression,omitempty" xml:"-"`
}
type Licenses []LicenseChoice

func (l LicenseChoice) MarshalXML(e *xml.Encoder, _ xml.StartElement) error {
if l.License != nil && l.Expression != "" {
return fmt.Errorf("either license or expression must be set, but not both")
func (l Licenses) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
if len(l) == 0 {
return nil
}

if l.License != nil {
return e.EncodeElement(l.License, xml.StartElement{Name: xml.Name{Local: "license"}})
} else if l.Expression != "" {
expressionElement := xml.StartElement{Name: xml.Name{Local: "expression"}}
if err := e.EncodeToken(expressionElement); err != nil {
return err
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 err := e.EncodeToken(xml.CharData(l.Expression)); err != nil {
return err

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(xml.EndElement{Name: expressionElement.Name})
}

// Neither license nor expression set - don't write anything
return nil
return e.EncodeToken(start.End())
}

func (l *LicenseChoice) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
if start.Name.Local == "license" {
license := new(License)
if err := d.DecodeElement(license, &start); err != nil {
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
}
l.License = license
l.Expression = ""
return nil
} else if start.Name.Local == "expression" {
expression := new(string)
if err := d.DecodeElement(expression, &start); err != nil {
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.License = nil
l.Expression = *expression
return nil
}

return xml.UnmarshalError(fmt.Sprintf("cannot unmarshal element %#v", start))
*l = licenses
return nil
}

type LicenseChoice struct {
License *License `json:"license,omitempty" xml:"-"`
Expression string `json:"expression,omitempty" xml:"-"`
}

type Metadata struct {
Expand Down Expand Up @@ -380,7 +404,7 @@ type Service struct {
Authenticated *bool `json:"authenticated,omitempty" xml:"authenticated,omitempty"`
CrossesTrustBoundary *bool `json:"x-trust-boundary,omitempty" xml:"x-trust-boundary,omitempty"`
Data *[]DataClassification `json:"data,omitempty" xml:"data>classification,omitempty"`
Licenses *[]LicenseChoice `json:"licenses,omitempty" xml:"licenses>license,omitempty"`
Licenses *Licenses `json:"licenses,omitempty" xml:"licenses,omitempty"`
ExternalReferences *[]ExternalReference `json:"externalReferences,omitempty" xml:"externalReferences>reference,omitempty"`
Services *[]Service `json:"services,omitempty" xml:"services>service,omitempty"`
}
Expand Down
142 changes: 63 additions & 79 deletions cyclonedx_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,99 +82,83 @@ func TestDependency_UnmarshalJSON(t *testing.T) {
assert.Equal(t, "transitiveDependencyRef", (*dependency.Dependencies)[0].Ref)
}

func TestLicenseChoice_MarshalJSON(t *testing.T) {
// Marshal license
choice := LicenseChoice{
License: &License{
ID: "licenseID",
Name: "licenseName",
URL: "licenseURL",
func TestLicenses_MarshalXML(t *testing.T) {
// Marshal license and expressions
licenses := Licenses{
LicenseChoice{
Expression: "expressionValue1",
},
}
jsonBytes, err := json.Marshal(choice)
assert.NoError(t, err)
assert.Equal(t, "{\"license\":{\"id\":\"licenseID\",\"name\":\"licenseName\",\"url\":\"licenseURL\"}}", string(jsonBytes))

// Marshal expression
choice = LicenseChoice{
Expression: "expressionValue",
}
jsonBytes, err = json.Marshal(choice)
assert.NoError(t, err)
assert.Equal(t, "{\"expression\":\"expressionValue\"}", string(jsonBytes))
}

func TestLicenseChoice_MarshalXML(t *testing.T) {
// Marshal license
choice := LicenseChoice{
License: &License{
ID: "licenseID",
Name: "licenseName",
URL: "licenseURL",
LicenseChoice{
License: &License{
ID: "licenseID",
URL: "licenseURL",
},
},
LicenseChoice{
Expression: "expressionValue2",
},
}
xmlBytes, err := xml.Marshal(choice)
assert.NoError(t, err)
assert.Equal(t, "<license><id>licenseID</id><name>licenseName</name><url>licenseURL</url></license>", string(xmlBytes))

// Marshal expression
choice = LicenseChoice{
Expression: "expressionValue",
}
xmlBytes, err = xml.Marshal(choice)
xmlBytes, err := xml.MarshalIndent(licenses, "", " ")
assert.NoError(t, err)
assert.Equal(t, "<expression>expressionValue</expression>", string(xmlBytes))

// Should return error when both license and expression are set
choice = LicenseChoice{
License: &License{
ID: "licenseID",
assert.Equal(t, `<Licenses>
<expression>expressionValue1</expression>
<license>
<id>licenseID</id>
<url>licenseURL</url>
</license>
<expression>expressionValue2</expression>
</Licenses>`, string(xmlBytes))

// Should return error when both license and expression are set on an element
licenses = Licenses{
LicenseChoice{
License: &License{
ID: "licenseID",
},
Expression: "expressionValue",
},
Expression: "expressionValue",
}
_, err = xml.Marshal(choice)
_, err = xml.Marshal(licenses)
assert.Error(t, err)

// Should encode nothing when neither license nor expression are set
choice = LicenseChoice{}
xmlBytes, err = xml.Marshal(choice)
// Should encode nothing when empty
licenses = Licenses{}
xmlBytes, err = xml.Marshal(licenses)
assert.NoError(t, err)
assert.Nil(t, xmlBytes)
}

func TestLicenseChoice_UnmarshalJSON(t *testing.T) {
// Unmarshal license
choice := new(LicenseChoice)
err := json.Unmarshal([]byte("{\"license\":{\"id\":\"licenseID\",\"name\":\"licenseName\",\"url\":\"licenseURL\"}}"), choice)
assert.NoError(t, err)
assert.NotNil(t, choice.License)
assert.Equal(t, "", choice.Expression)

// Unmarshal expression
choice = new(LicenseChoice)
err = json.Unmarshal([]byte("{\"expression\":\"expressionValue\"}"), choice)
func TestLicenses_UnmarshalXML(t *testing.T) {
// Unmarshal license and expressions
licenses := new(Licenses)
err := xml.Unmarshal([]byte(`
<Licenses>
<expression>expressionValue1</expression>
<license>
<id>licenseID</id>
<url>licenseURL</url>
</license>
<expression>expressionValue2</expression>
</Licenses>`), licenses)
assert.NoError(t, err)
assert.Nil(t, choice.License)
assert.Equal(t, "expressionValue", choice.Expression)
}

func TestLicenseChoice_UnmarshalXML(t *testing.T) {
// Unmarshal license
choice := new(LicenseChoice)
err := xml.Unmarshal([]byte("<license><id>licenseID</id><name>licenseName</name><url>licenseURL</url></license>"), choice)
assert.NoError(t, err)
assert.NotNil(t, choice.License)
assert.Equal(t, "", choice.Expression)

// Unmarshal expression
choice = new(LicenseChoice)
err = xml.Unmarshal([]byte("<expression>expressionValue</expression>"), choice)
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></Licenses>"), licenses)
assert.NoError(t, err)
assert.Nil(t, choice.License)
assert.Equal(t, "expressionValue", choice.Expression)
assert.Empty(t, *licenses)

// Should return error when input is neither license nor expression
choice = new(LicenseChoice)
err = xml.Unmarshal([]byte("<somethingElse>expressionValue</somethingElse>"), choice)
// Should return error when an element is neither license nor expression
licenses = new(Licenses)
err = xml.Unmarshal([]byte("<Licenses><somethingElse>expressionValue</somethingElse></Licenses>"), licenses)
assert.Error(t, err)
}
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,9 @@
<hash alg="SHA-256">708f1f53b41f11f02d12a11b1a38d2905d47b099afc71a0f1124ef8582ec7313</hash>
<hash alg="SHA-512">387b7ae16b9cae45f830671541539bf544202faae5aac544a93b7b0a04f5f846fa2f4e81ef3f1677e13aed7496408a441f5657ab6d54423e56bf6f38da124aef</hash>
</hashes>
<licenses>
<expression>EPL-2.0 OR GPL-2.0-with-classpath-exception</expression>
</licenses>
<copyright>Copyright Example Inc. All rights reserved.</copyright>
<cpe>cpe:/a:example:myapplication:1.0.0</cpe>
<purl>pkg:maven/com.example/[email protected]?packaging=war</purl>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
<hash alg="SHA-256">f498a8ff2dd007e29c2074f5e4b01a9a01775c3ff3aeaf6906ea503bc5791b7b</hash>
<hash alg="SHA-512">e8f33e424f3f4ed6db76a482fde1a5298970e442c531729119e37991884bdffab4f9426b7ee11fccd074eeda0634d71697d6f88a460dce0ac8d627a29f7d1282</hash>
</hashes>
<licenses>
<expression>EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0</expression>
</licenses>
<purl>pkg:maven/com.acme/[email protected]?packaging=jar</purl>
</component>
</components>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@
<hash alg="SHA-256">708f1f53b41f11f02d12a11b1a38d2905d47b099afc71a0f1124ef8582ec7313</hash>
<hash alg="SHA-512">387b7ae16b9cae45f830671541539bf544202faae5aac544a93b7b0a04f5f846fa2f4e81ef3f1677e13aed7496408a441f5657ab6d54423e56bf6f38da124aef</hash>
</hashes>
<licenses>
<expression>EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0</expression>
</licenses>
<copyright>Copyright Example Inc. All rights reserved.</copyright>
<cpe>cpe:/a:example:myapplication:1.0.0</cpe>
<purl>pkg:maven/com.example/[email protected]?packaging=war</purl>
Expand Down
Loading