1// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later 2 3package common 4 5import ( 6 "encoding/json" 7 "fmt" 8 "strings" 9) 10 11const ( 12 spdxRefPrefix = "SPDXRef-" 13 documentRefPrefix = "DocumentRef-" 14) 15 16// ElementID represents the identifier string portion of an SPDX element 17// identifier. DocElementID should be used for any attributes which can 18// contain identifiers defined in a different SPDX document. 19// ElementIDs should NOT contain the mandatory 'SPDXRef-' portion. 20type ElementID string 21 22// MarshalJSON returns an SPDXRef- prefixed JSON string 23func (d ElementID) MarshalJSON() ([]byte, error) { 24 return json.Marshal(prefixElementId(d)) 25} 26 27// UnmarshalJSON validates SPDXRef- prefixes and removes them when processing ElementIDs 28func (d *ElementID) UnmarshalJSON(data []byte) error { 29 // SPDX identifier will simply be a string 30 idStr := string(data) 31 idStr = strings.Trim(idStr, "\"") 32 33 e, err := trimElementIdPrefix(idStr) 34 if err != nil { 35 return err 36 } 37 *d = e 38 return nil 39} 40 41// prefixElementId adds the SPDXRef- prefix to an element ID if it does not have one 42func prefixElementId(id ElementID) string { 43 val := string(id) 44 if !strings.HasPrefix(val, spdxRefPrefix) { 45 return spdxRefPrefix + val 46 } 47 return val 48} 49 50// trimElementIdPrefix removes the SPDXRef- prefix from an element ID string or returns an error if it 51// does not start with SPDXRef- 52func trimElementIdPrefix(id string) (ElementID, error) { 53 // handle SPDXRef- 54 idFields := strings.SplitN(id, spdxRefPrefix, 2) 55 if len(idFields) != 2 { 56 return "", fmt.Errorf("failed to parse SPDX identifier '%s'", id) 57 } 58 59 e := ElementID(idFields[1]) 60 return e, nil 61} 62 63// DocElementID represents an SPDX element identifier that could be defined 64// in a different SPDX document, and therefore could have a "DocumentRef-" 65// portion, such as Relationships and Annotations. 66// ElementID is used for attributes in which a "DocumentRef-" portion cannot 67// appear, such as a Package or File definition (since it is necessarily 68// being defined in the present document). 69// DocumentRefID will be the empty string for elements defined in the 70// present document. 71// DocElementIDs should NOT contain the mandatory 'DocumentRef-' or 72// 'SPDXRef-' portions. 73// SpecialID is used ONLY if the DocElementID matches a defined set of 74// permitted special values for a particular field, e.g. "NONE" or 75// "NOASSERTION" for the right-hand side of Relationships. If SpecialID 76// is set, DocumentRefID and ElementRefID should be empty (and vice versa). 77type DocElementID struct { 78 DocumentRefID string 79 ElementRefID ElementID 80 SpecialID string 81} 82 83// MarshalJSON converts the receiver into a slice of bytes representing a DocElementID in string form. 84// This function is also used when marshalling to YAML 85func (d DocElementID) MarshalJSON() ([]byte, error) { 86 if d.DocumentRefID != "" && d.ElementRefID != "" { 87 idStr := prefixElementId(d.ElementRefID) 88 return json.Marshal(fmt.Sprintf("%s%s:%s", documentRefPrefix, d.DocumentRefID, idStr)) 89 } else if d.ElementRefID != "" { 90 return json.Marshal(prefixElementId(d.ElementRefID)) 91 } else if d.SpecialID != "" { 92 return json.Marshal(d.SpecialID) 93 } 94 95 return []byte{}, fmt.Errorf("failed to marshal empty DocElementID") 96} 97 98// UnmarshalJSON takes a SPDX Identifier string parses it into a DocElementID struct. 99// This function is also used when unmarshalling YAML 100func (d *DocElementID) UnmarshalJSON(data []byte) (err error) { 101 // SPDX identifier will simply be a string 102 idStr := string(data) 103 idStr = strings.Trim(idStr, "\"") 104 105 // handle special cases 106 if idStr == "NONE" || idStr == "NOASSERTION" { 107 d.SpecialID = idStr 108 return nil 109 } 110 111 var idFields []string 112 // handle DocumentRef- if present 113 if strings.HasPrefix(idStr, documentRefPrefix) { 114 // strip out the "DocumentRef-" so we can get the value 115 idFields = strings.SplitN(idStr, documentRefPrefix, 2) 116 idStr = idFields[1] 117 118 // an SPDXRef can appear after a DocumentRef, separated by a colon 119 idFields = strings.SplitN(idStr, ":", 2) 120 d.DocumentRefID = idFields[0] 121 122 if len(idFields) == 2 { 123 idStr = idFields[1] 124 } else { 125 return nil 126 } 127 } 128 129 d.ElementRefID, err = trimElementIdPrefix(idStr) 130 return err 131} 132 133// TODO: add equivalents for LicenseRef- identifiers 134 135// MakeDocElementID takes strings (without prefixes) for the DocumentRef- 136// and SPDXRef- identifiers, and returns a DocElementID. An empty string 137// should be used for the DocumentRef- portion if it is referring to the 138// present document. 139func MakeDocElementID(docRef string, eltRef string) DocElementID { 140 return DocElementID{ 141 DocumentRefID: docRef, 142 ElementRefID: ElementID(eltRef), 143 } 144} 145 146// MakeDocElementSpecial takes a "special" string (e.g. "NONE" or 147// "NOASSERTION" for the right side of a Relationship), nd returns 148// a DocElementID with it in the SpecialID field. Other fields will 149// be empty. 150func MakeDocElementSpecial(specialID string) DocElementID { 151 return DocElementID{SpecialID: specialID} 152} 153 154// RenderElementID takes an ElementID and returns the string equivalent, 155// with the SPDXRef- prefix reinserted. 156func RenderElementID(eID ElementID) string { 157 return spdxRefPrefix + string(eID) 158} 159 160// RenderDocElementID takes a DocElementID and returns the string equivalent, 161// with the SPDXRef- prefix (and, if applicable, the DocumentRef- prefix) 162// reinserted. If a SpecialID is present, it will be rendered verbatim and 163// DocumentRefID and ElementRefID will be ignored. 164func RenderDocElementID(deID DocElementID) string { 165 if deID.SpecialID != "" { 166 return deID.SpecialID 167 } 168 prefix := "" 169 if deID.DocumentRefID != "" { 170 prefix = documentRefPrefix + deID.DocumentRefID + ":" 171 } 172 return prefix + spdxRefPrefix + string(deID.ElementRefID) 173} 174