@@ -255,13 +255,17 @@ const (
255255
256256// parser parses SPDX expressions.
257257type parser struct {
258- lexer * lexer
259- current token
260- depth int
258+ lexer * lexer
259+ current token
260+ depth int
261+ allowUnknownIdentifiers bool
261262}
262263
263- func newParser (input string ) (* parser , error ) {
264- p := & parser {lexer : newLexer (input )}
264+ func newParser (input string , allowUnknownIdentifiers bool ) (* parser , error ) {
265+ p := & parser {
266+ lexer : newLexer (input ),
267+ allowUnknownIdentifiers : allowUnknownIdentifiers ,
268+ }
265269 tok , err := p .lexer .next ()
266270 if err != nil {
267271 return nil , err
@@ -306,21 +310,12 @@ func Parse(expression string) (Expression, error) {
306310 return nil , err
307311 }
308312
309- p , err := newParser (normalized )
310- if err != nil {
311- return nil , err
312- }
313-
314- expr , err := p .parseExpression ()
313+ p , err := newParser (normalized , false )
315314 if err != nil {
316315 return nil , err
317316 }
318317
319- if p .current .typ != tokenEOF {
320- return nil , fmt .Errorf ("%w: %s" , ErrUnexpectedToken , p .current .value )
321- }
322-
323- return expr , nil
318+ return p .parse ()
324319}
325320
326321// ParseStrict parses an SPDX expression requiring strict SPDX identifiers.
@@ -333,6 +328,30 @@ func Parse(expression string) (Expression, error) {
333328// ParseStrict("MIT OR Apache-2.0") // succeeds
334329// ParseStrict("mit OR apache 2") // fails - "apache 2" is not a valid SPDX ID
335330func ParseStrict (expression string ) (Expression , error ) {
331+ return parseWithoutNormalization (expression , false )
332+ }
333+
334+ // ParseSyntax parses an SPDX expression without requiring bare license and
335+ // exception identifiers to exist in the bundled SPDX identifier list. It
336+ // validates identifier syntax, operators, modifiers, and grouping. Known
337+ // identifiers are returned in their canonical form; unknown identifiers are
338+ // preserved as written.
339+ //
340+ // Use ParseStrict when identifiers must also be present in the bundled SPDX
341+ // list.
342+ //
343+ // Example:
344+ //
345+ // ParseSyntax("Future-License-1.0 OR MIT") // succeeds
346+ // ParseStrict("Future-License-1.0 OR MIT") // fails
347+ func ParseSyntax (expression string ) (Expression , error ) {
348+ return parseWithoutNormalization (expression , true )
349+ }
350+
351+ func parseWithoutNormalization (
352+ expression string ,
353+ allowUnknownIdentifiers bool ,
354+ ) (Expression , error ) {
336355 expression = strings .TrimSpace (expression )
337356 if expression == "" {
338357 return nil , ErrEmptyExpression
@@ -341,11 +360,15 @@ func ParseStrict(expression string) (Expression, error) {
341360 return nil , ErrExpressionTooLarge
342361 }
343362
344- p , err := newParser (expression )
363+ p , err := newParser (expression , allowUnknownIdentifiers )
345364 if err != nil {
346365 return nil , err
347366 }
348367
368+ return p .parse ()
369+ }
370+
371+ func (p * parser ) parse () (Expression , error ) {
349372 expr , err := p .parseExpression ()
350373 if err != nil {
351374 return nil , err
@@ -426,9 +449,9 @@ func (p *parser) parseWith() (Expression, error) {
426449 return nil , fmt .Errorf ("%w: expected exception after WITH" , ErrMissingOperand )
427450 }
428451
429- exception := lookupException (p .current .value )
430- if exception == "" {
431- return nil , fmt . Errorf ( "%w: %s" , ErrInvalidException , p . current . value )
452+ exception , err := p . resolveExceptionIdentifier (p .current .value )
453+ if err != nil {
454+ return nil , err
432455 }
433456
434457 license .Exception = exception
@@ -471,59 +494,116 @@ func (p *parser) parseAtom() (Expression, error) {
471494 return expr , nil
472495
473496 case tokenLicense :
474- value := p .current .value
475- upper := strings .ToUpper (value )
476-
477- // Handle special values
478- if upper == "NONE" || upper == "NOASSERTION" {
479- if err := p .advance (); err != nil {
480- return nil , err
481- }
482- return & SpecialValue {Value : upper }, nil
483- }
497+ return p .parseLicenseAtom ()
484498
485- // Look up the canonical license ID
486- id := lookupLicense (value )
487- if id == "" {
488- return nil , fmt .Errorf ("%w: %s" , ErrInvalidLicenseID , value )
489- }
490-
491- license := & License {ID : id }
499+ case tokenLicenseRef :
500+ return p .parseLicenseReference (false )
492501
493- if err := p .advance (); err != nil {
494- return nil , err
495- }
502+ case tokenDocumentRef :
503+ return p .parseLicenseReference (true )
496504
497- // Check for +
498- if p .current .typ == tokenPlus {
499- license .Plus = true
500- if err := p .advance (); err != nil {
501- return nil , err
502- }
503- }
505+ case tokenEOF :
506+ return nil , ErrMissingOperand
504507
505- return license , nil
508+ default :
509+ return nil , fmt .Errorf ("%w: %s" , ErrUnexpectedToken , p .current .value )
510+ }
511+ }
506512
507- case tokenLicenseRef :
508- ref := parseLicenseRef (p .current .value )
513+ func (p * parser ) parseLicenseAtom () (Expression , error ) {
514+ value := p .current .value
515+ upper := strings .ToUpper (value )
516+ if upper == "NONE" || upper == "NOASSERTION" {
509517 if err := p .advance (); err != nil {
510518 return nil , err
511519 }
512- return ref , nil
520+ return & SpecialValue {Value : upper }, nil
521+ }
513522
514- case tokenDocumentRef :
515- ref := parseDocumentRef (p .current .value )
523+ id , err := p .resolveLicenseIdentifier (value )
524+ if err != nil {
525+ return nil , err
526+ }
527+ license := & License {ID : id }
528+ if err := p .advance (); err != nil {
529+ return nil , err
530+ }
531+ if p .current .typ == tokenPlus {
532+ license .Plus = true
516533 if err := p .advance (); err != nil {
517534 return nil , err
518535 }
519- return ref , nil
536+ }
537+ return license , nil
538+ }
520539
521- case tokenEOF :
522- return nil , ErrMissingOperand
540+ func (p * parser ) resolveLicenseIdentifier (identifier string ) (string , error ) {
541+ if id := lookupLicense (identifier ); id != "" {
542+ return id , nil
543+ }
544+ if p .allowUnknownIdentifiers && validIdentifier (identifier ) {
545+ return identifier , nil
546+ }
547+ return "" , fmt .Errorf ("%w: %s" , ErrInvalidLicenseID , identifier )
548+ }
523549
524- default :
525- return nil , fmt .Errorf ("%w: %s" , ErrUnexpectedToken , p .current .value )
550+ func (p * parser ) resolveExceptionIdentifier (identifier string ) (string , error ) {
551+ if exception := lookupException (identifier ); exception != "" {
552+ return exception , nil
526553 }
554+ if p .allowUnknownIdentifiers && validIdentifier (identifier ) {
555+ return identifier , nil
556+ }
557+ return "" , fmt .Errorf ("%w: %s" , ErrInvalidException , identifier )
558+ }
559+
560+ func (p * parser ) parseLicenseReference (document bool ) (Expression , error ) {
561+ value := p .current .value
562+ var reference * LicenseRef
563+ if document {
564+ reference = parseDocumentRef (value )
565+ } else {
566+ reference = parseLicenseRef (value )
567+ }
568+ if ! validLicenseReference (reference , document ) {
569+ return nil , fmt .Errorf ("%w: %s" , ErrInvalidLicenseID , value )
570+ }
571+ if err := p .advance (); err != nil {
572+ return nil , err
573+ }
574+ return reference , nil
575+ }
576+
577+ func validLicenseReference (reference * LicenseRef , document bool ) bool {
578+ if reference == nil || ! validIdentifier (reference .LicenseRef ) {
579+ return false
580+ }
581+ if document {
582+ return validIdentifier (reference .DocumentRef )
583+ }
584+ return reference .DocumentRef == ""
585+ }
586+
587+ func validIdentifier (identifier string ) bool {
588+ if identifier == "" || ! isIdentifierAlphanumeric (rune (identifier [0 ])) ||
589+ ! isIdentifierAlphanumeric (rune (identifier [len (identifier )- 1 ])) {
590+ return false
591+ }
592+ for _ , character := range identifier {
593+ switch {
594+ case isIdentifierAlphanumeric (character ):
595+ case character == '-' , character == '.' :
596+ default :
597+ return false
598+ }
599+ }
600+ return true
601+ }
602+
603+ func isIdentifierAlphanumeric (character rune ) bool {
604+ return character >= 'a' && character <= 'z' ||
605+ character >= 'A' && character <= 'Z' ||
606+ character >= '0' && character <= '9'
527607}
528608
529609// parseLicenseRef parses "LicenseRef-xxx" into a LicenseRef.
0 commit comments