Skip to content

Commit

Permalink
Merge pull request #5 from CycloneDX/fix-license-expression-unmarshal…
Browse files Browse the repository at this point in the history
…ling

Fix license expression unmarshalling
  • Loading branch information
nscuro committed Jun 24, 2021
2 parents c1fdab5 + 7508273 commit a20be9f
Show file tree
Hide file tree
Showing 7 changed files with 131 additions and 398 deletions.
94 changes: 59 additions & 35 deletions cyclonedx.go
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
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)
}
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/myapplication@1.0.0?packaging=war</purl>
Expand Down
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/tomcat-catalina@9.0.14?packaging=jar</purl>
</component>
</components>
Expand Down
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/myapplication@1.0.0?packaging=war</purl>
Expand Down

0 comments on commit a20be9f

Please sign in to comment.