diff --git a/cyclonedx.go b/cyclonedx.go index 6c2b0b1..0bb3ee3 100644 --- a/cyclonedx.go +++ b/cyclonedx.go @@ -20,7 +20,9 @@ package cyclonedx import ( "encoding/json" "encoding/xml" + "errors" "fmt" + "io" ) const ( @@ -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"` @@ -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 { @@ -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"` } diff --git a/cyclonedx_test.go b/cyclonedx_test.go index 1d80004..dfb4010 100644 --- a/cyclonedx_test.go +++ b/cyclonedx_test.go @@ -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, "licenseIDlicenseNamelicenseURL", 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, "expressionValue", string(xmlBytes)) - - // Should return error when both license and expression are set - choice = LicenseChoice{ - License: &License{ - ID: "licenseID", + 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", }, - 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(` + + expressionValue1 + + licenseID + licenseURL + + expressionValue2 +`), 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("licenseIDlicenseNamelicenseURL"), choice) - assert.NoError(t, err) - assert.NotNil(t, choice.License) - assert.Equal(t, "", choice.Expression) - - // Unmarshal expression - choice = new(LicenseChoice) - err = xml.Unmarshal([]byte("expressionValue"), 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) 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("expressionValue"), choice) + // Should return error when an element is neither license nor expression + licenses = new(Licenses) + err = xml.Unmarshal([]byte("expressionValue"), licenses) assert.Error(t, err) } diff --git a/testdata/snapshots/cyclonedx-go-TestRoundTripXML-valid-bom-1.2.xml b/testdata/snapshots/cyclonedx-go-TestRoundTripXML-valid-bom-1.2.xml index 8d974e5..a2ea590 100644 --- a/testdata/snapshots/cyclonedx-go-TestRoundTripXML-valid-bom-1.2.xml +++ b/testdata/snapshots/cyclonedx-go-TestRoundTripXML-valid-bom-1.2.xml @@ -131,6 +131,9 @@ 708f1f53b41f11f02d12a11b1a38d2905d47b099afc71a0f1124ef8582ec7313 387b7ae16b9cae45f830671541539bf544202faae5aac544a93b7b0a04f5f846fa2f4e81ef3f1677e13aed7496408a441f5657ab6d54423e56bf6f38da124aef + + EPL-2.0 OR GPL-2.0-with-classpath-exception + Copyright Example Inc. All rights reserved. cpe:/a:example:myapplication:1.0.0 pkg:maven/com.example/myapplication@1.0.0?packaging=war diff --git a/testdata/snapshots/cyclonedx-go-TestRoundTripXML-valid-license-expression-1.2.xml b/testdata/snapshots/cyclonedx-go-TestRoundTripXML-valid-license-expression-1.2.xml index 590d0ff..aee8304 100644 --- a/testdata/snapshots/cyclonedx-go-TestRoundTripXML-valid-license-expression-1.2.xml +++ b/testdata/snapshots/cyclonedx-go-TestRoundTripXML-valid-license-expression-1.2.xml @@ -14,6 +14,9 @@ f498a8ff2dd007e29c2074f5e4b01a9a01775c3ff3aeaf6906ea503bc5791b7b e8f33e424f3f4ed6db76a482fde1a5298970e442c531729119e37991884bdffab4f9426b7ee11fccd074eeda0634d71697d6f88a460dce0ac8d627a29f7d1282 + + EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + pkg:maven/com.acme/tomcat-catalina@9.0.14?packaging=jar diff --git a/testdata/snapshots/cyclonedx-go-TestRoundTripXML-valid-random-attributes-1.2.xml b/testdata/snapshots/cyclonedx-go-TestRoundTripXML-valid-random-attributes-1.2.xml index 79e4338..ff737b9 100644 --- a/testdata/snapshots/cyclonedx-go-TestRoundTripXML-valid-random-attributes-1.2.xml +++ b/testdata/snapshots/cyclonedx-go-TestRoundTripXML-valid-random-attributes-1.2.xml @@ -69,6 +69,9 @@ 708f1f53b41f11f02d12a11b1a38d2905d47b099afc71a0f1124ef8582ec7313 387b7ae16b9cae45f830671541539bf544202faae5aac544a93b7b0a04f5f846fa2f4e81ef3f1677e13aed7496408a441f5657ab6d54423e56bf6f38da124aef + + EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + Copyright Example Inc. All rights reserved. cpe:/a:example:myapplication:1.0.0 pkg:maven/com.example/myapplication@1.0.0?packaging=war diff --git a/testdata/snapshots/cyclonedx-go-TestRoundTripXML-valid-xml-signature-1.2.xml b/testdata/snapshots/cyclonedx-go-TestRoundTripXML-valid-xml-signature-1.2.xml deleted file mode 100644 index 2b9b2a2..0000000 --- a/testdata/snapshots/cyclonedx-go-TestRoundTripXML-valid-xml-signature-1.2.xml +++ /dev/null @@ -1,107 +0,0 @@ - - - - - Acme Inc - com.acme - tomcat-catalina - 9.0.14 - - 3942447fac867ae5cdb3229b658f4d48 - e6b1000b94e835ffd37f4c6dcbdad43f4b48a02a - f498a8ff2dd007e29c2074f5e4b01a9a01775c3ff3aeaf6906ea503bc5791b7b - e8f33e424f3f4ed6db76a482fde1a5298970e442c531729119e37991884bdffab4f9426b7ee11fccd074eeda0634d71697d6f88a460dce0ac8d627a29f7d1282 - - - - Apache-2.0 - - - pkg:maven/com.acme/tomcat-catalina@9.0.14?packaging=jar - - - - Apache - org.apache.tomcat - tomcat-catalina - 9.0.14 - - - Apache-2.0 - - - pkg:maven/org.apache.tomcat/tomcat-catalina@9.0.14?packaging=jar - - - - - 7638417db6d59f3c431d3e1f261cc637155684cd - https://location/to/7638417db6d59f3c431d3e1f261cc637155684cd - - 2018-11-07T22:01:45Z - John Doe - jdoe@example.com - - - 2018-11-07T22:01:45Z - John Doe - jdoe@example.com - - Initial commit - - - - - - org.example - mylibrary - 1.0.0 - required - - 2342c2eaf1feb9a80195dbaddf2ebaa3 - 68b78babe00a053f9e35ec6a2d9080f5b90122b0 - 708f1f53b41f11f02d12a11b1a38d2905d47b099afc71a0f1124ef8582ec7313 - 387b7ae16b9cae45f830671541539bf544202faae5aac544a93b7b0a04f5f846fa2f4e81ef3f1677e13aed7496408a441f5657ab6d54423e56bf6f38da124aef - - - - Apache-2.0 - blah - fdaf - - - Copyright Example Inc. All rights reserved. - cpe:/a:example:myapplication:1.0.0 - pkg:maven/com.example/myapplication@1.0.0?packaging=war - false - - - com.example - myframework - 1.0.0 - Example Inc, enterprise framework - required - - cfcb0b64aacd2f81c1cd546543de965a - 7fbeef2346c45d565c3341f037bce4e088af8a52 - 0384db3cec55d86a6898c489fdb75a8e75fe66b26639634983d2f3c3558493d1 - 854909cdb9e3ca183056837144aab6d8069b377bd66445087cc7157bf0c3f620418705dd0b83bdc2f73a508c2bdb316ca1809d75ee6972d02023a3e7dd655c79 - - - - Apache-2.0 - - - pkg:maven/com.example/myframework@1.0.0?packaging=war - false - - - http://example.com/myframework - - - http://example.com/security - - - - - diff --git a/testdata/valid-xml-signature-1.2.xml b/testdata/valid-xml-signature-1.2.xml deleted file mode 100644 index 9605144..0000000 --- a/testdata/valid-xml-signature-1.2.xml +++ /dev/null @@ -1,177 +0,0 @@ - - - - - Acme Inc - com.acme - tomcat-catalina - 9.0.14 - - 3942447fac867ae5cdb3229b658f4d48 - e6b1000b94e835ffd37f4c6dcbdad43f4b48a02a - f498a8ff2dd007e29c2074f5e4b01a9a01775c3ff3aeaf6906ea503bc5791b7b - e8f33e424f3f4ed6db76a482fde1a5298970e442c531729119e37991884bdffab4f9426b7ee11fccd074eeda0634d71697d6f88a460dce0ac8d627a29f7d1282 - - - - Apache-2.0 - - - pkg:maven/com.acme/tomcat-catalina@9.0.14?packaging=jar - - - - Apache - org.apache.tomcat - tomcat-catalina - 9.0.14 - - - Apache-2.0 - - - pkg:maven/org.apache.tomcat/tomcat-catalina@9.0.14?packaging=jar - - - - - - 7638417db6d59f3c431d3e1f261cc637155684cd - https://location/to/7638417db6d59f3c431d3e1f261cc637155684cd - - 2018-11-07T22:01:45Z - John Doe - jdoe@example.com - - - 2018-11-07T22:01:45Z - John Doe - jdoe@example.com - - Initial commit - - - - - - org.example - mylibrary - 1.0.0 - required - - 2342c2eaf1feb9a80195dbaddf2ebaa3 - 68b78babe00a053f9e35ec6a2d9080f5b90122b0 - 708f1f53b41f11f02d12a11b1a38d2905d47b099afc71a0f1124ef8582ec7313 - 387b7ae16b9cae45f830671541539bf544202faae5aac544a93b7b0a04f5f846fa2f4e81ef3f1677e13aed7496408a441f5657ab6d54423e56bf6f38da124aef - - - - Apache-2.0 - blah - fdaf - - - Copyright Example Inc. All rights reserved. - cpe:/a:example:myapplication:1.0.0 - pkg:maven/com.example/myapplication@1.0.0?packaging=war - false - - - com.example - myframework - 1.0.0 - Example Inc, enterprise framework - required - - cfcb0b64aacd2f81c1cd546543de965a - 7fbeef2346c45d565c3341f037bce4e088af8a52 - 0384db3cec55d86a6898c489fdb75a8e75fe66b26639634983d2f3c3558493d1 - 854909cdb9e3ca183056837144aab6d8069b377bd66445087cc7157bf0c3f620418705dd0b83bdc2f73a508c2bdb316ca1809d75ee6972d02023a3e7dd655c79 - - - - Apache-2.0 - - - pkg:maven/com.example/myframework@1.0.0?packaging=war - false - - - http://example.com/myframework - - - http://example.com/security - - - - - - - - - - - - - - PrB8/rofGs34XwIX5OIdYSjV2aKSe5VaztJKBvsgjIk= - - - - ePGNg30Zl9CW7RZdcRn8gFCp1AlWncjudA9pQDXyqZOvyj9RC2YtkI688WdfDOdVRZs6mflJFXr7 - IKA9wY6jVrEqZmlef55Qp/8iGwOjOjWbwYsm2AhrdkUi9gaFSWEd8uITYHOpWbiPFSsnimiK9+ft - 56dkg/oJMLdXzlaukzq9iGkRcafRkW433OQcZIXwD2K8lg4cdD0pNNNqBa+PgIvzbxA5H84TyQDB - HBcQiw/j1edRBJgPOwlqzZDUawOJaFhAPUQ+GGKMetIJH2FqqrHXGuV1NIwnbWTCg40RdOcBdCrl - PDtDVjFh34uZ4dYBpJBIlM4daD2N4B6WPB5iHRyuZTczF2q03ObabuTgkpK6EeadFVqFNsEOOPPt - MDDyda+Lwff5KjvUHvRRtUDIOm2rNIQKzaseulwYcA9UWQHAFcupJmWcLLM4zzY7F/uOdZuSurzh - U6h5kdb76Juepof6ee4Q5YpwNOGNL5JfB4C3sc/Dbbv8dZ8OuXFYSZN7reUGZzCNksByqERPEbAe - n1ldJu1HnRXRQpwaon8Asy9CuNmPfFCfDwOs2B4p4tb+tLNIKFHdRlpd19Zr9vCMCbltXeqq0Cpq - OejSyLYGqSWzzzUh449dJrg6KTevrTNEln5GAlLBFSdjM5JA7KV2u/GyDVFwSEW7UKooGN4CtgU= - - - - CN=bomsigner,OU=development,O=cyclonedx - - MIIE+DCCAuCgAwIBAgIEXGzayTANBgkqhkiG9w0BAQsFADA+MRIwEAYDVQQKDAljeWNsb25lZHgx - FDASBgNVBAsMC2RldmVsb3BtZW50MRIwEAYDVQQDDAlib21zaWduZXIwHhcNMTkwMjIwMDQ0MjQ5 - WhcNNDkwMjIwMDQ0MjQ5WjA+MRIwEAYDVQQKDAljeWNsb25lZHgxFDASBgNVBAsMC2RldmVsb3Bt - ZW50MRIwEAYDVQQDDAlib21zaWduZXIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCo - 5JZsM4ZLfWW/dpRlU6CpnItWspddF+bEVDETKVwVj9tGpqR5jURgKS/BOQP2TGUsR3/ZJJBhYRll - ONhrUQrVKV/I6wp3Z40qPEa1RJLE+QlG9iL8qBV52CnXkLmnUSax3dspSzmSct5vDiTnvpHG9jr0 - AKFeTjy7U9rv8GJybz0ijwlpBoO9JRdYPX2PrrzoSeJLoxKq+GwuyCZ5LhXRN0p1a+NAirTAmY+c - G1ZTLkMmfeCUy1t6H/bG4RnYOSSPOvk7Rb68lQpUqb+pbbNuB2o/b9cDwtLLCtGVlu+5Wj8mrytY - 3FGFQM20j3yVeRInmGqTTDBelQa/CO4JKqBlmaeYEIvNYbFs9+AlqadivwDO51RpdPo9fPSpsBpy - ZMv6S2bXNuUML+Rk99WyKJTPM0PTZhRLZ64ZXEhlz3kQWVoSlrcwwim6sj6LRUb5IRqA3lxRFUI6 - NXKyiQLamQp+t3/9OGW9L1rLCcw7yFo0s8LhMTPMiv4ol9/hQViT+8ICzDsr0OM9ZiF4/UagFRlt - IClV70cjh1DpsZjzQIRVGaj8uQ/JdtfRz4E43Ki7U0a2Vpho/t6poLVndv46tkX5nYGtMW4WfMoD - ZflQ9pajvvKtr2jB1wob6nsU+VTmAcWZy4BCPH+XyfDw/0SFBdUceJJJtPWIeYFDUY7onptf+wID - AQABMA0GCSqGSIb3DQEBCwUAA4ICAQCOVariNgK+9OF/5T9ZaSvZbkk45RTmzgQNXtFc5xfRvqwP - s+pu/DFXm1R+ltjyS5j3w6NBZUFUI5MqLQr6JEEDrbu8BvfBO57wJNAEATj1JIHEfDfh7BxnBF8f - oYFOwbrh4jOt0wz0FW2obsSVmF4GSvS7tTlWqTcsxjdZVmwP40RWu18B9jzv7M61adrWD3ksDA5O - amSOsZi3Nt0aacDkyGRdCIEFi0fplxQInXMtD1z3RhXu2JSTAIr54Cei49Bh71kAXSWHMCog/f8a - lSrZyqZBty/ACfU9DqlPIM+giHePKm4z2bcdpUdKZk6wcKDn4CvuBOqsMBMg7L05UEyyqTPD/4dk - 2GwJ8Nv0E5gsYHCIXF2cZ3OUVsw0mB/ozleEJVDE02uZZN/1wW1Xq028LsMdgN0Wk1WvWyF5MEdh - nPWuhqp6tNaDI/kK6XQF+LjYJUzua3AQFOHfYNLKhO6d+bJ4rr0833v4v3cLW34kbXkKb6U3Yv8X - SK3jBGCACiPgnc0N6awkh1kDlrZQ7GMsl14c+2+vpl9Lf0sL0mRUIyICfSC8MjlsP/BZH3emyfsk - iWivPALomycKqP+PSkt1WaWApGENZWk1wNN99FYSYlt6LViW2p6T97fRx4jPRlHu+wecfD2k9RP4 - bt5W2HWfOP0zNAS7SnAVLEl2QZxXKw== - - - - - - qOSWbDOGS31lv3aUZVOgqZyLVrKXXRfmxFQxEylcFY/bRqakeY1EYCkvwTkD9kxlLEd/2SSQYWEZ - ZTjYa1EK1SlfyOsKd2eNKjxGtUSSxPkJRvYi/KgVedgp15C5p1Emsd3bKUs5knLebw4k576RxvY6 - 9AChXk48u1Pa7/Bicm89Io8JaQaDvSUXWD19j6686EniS6MSqvhsLsgmeS4V0TdKdWvjQIq0wJmP - nBtWUy5DJn3glMtbeh/2xuEZ2Dkkjzr5O0W+vJUKVKm/qW2zbgdqP2/XA8LSywrRlZbvuVo/Jq8r - WNxRhUDNtI98lXkSJ5hqk0wwXpUGvwjuCSqgZZmnmBCLzWGxbPfgJamnYr8AzudUaXT6PXz0qbAa - cmTL+ktm1zblDC/kZPfVsiiUzzND02YUS2euGVxIZc95EFlaEpa3MMIpurI+i0VG+SEagN5cURVC - OjVysokC2pkKfrd//ThlvS9aywnMO8haNLPC4TEzzIr+KJff4UFYk/vCAsw7K9DjPWYheP1GoBUZ - bSApVe9HI4dQ6bGY80CEVRmo/LkPyXbX0c+BONyou1NGtlaYaP7eqaC1Z3b+OrZF+Z2BrTFuFnzK - A2X5UPaWo77yra9owdcKG+p7FPlU5gHFmcuAQjx/l8nw8P9EhQXVHHiSSbT1iHmBQ1GO6J6bX/s= - - AQAB - - - -