index.js
  1  /*
  2  Copyright spdx-correct.js contributors
  3  
  4  Licensed under the Apache License, Version 2.0 (the "License");
  5  you may not use this file except in compliance with the License.
  6  You may obtain a copy of the License at
  7  
  8     http://www.apache.org/licenses/LICENSE-2.0
  9  
 10  Unless required by applicable law or agreed to in writing, software
 11  distributed under the License is distributed on an "AS IS" BASIS,
 12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 13  See the License for the specific language governing permissions and
 14  limitations under the License.
 15  */
 16  var parse = require('spdx-expression-parse')
 17  var spdxLicenseIds = require('spdx-license-ids')
 18  
 19  function valid (string) {
 20    try {
 21      parse(string)
 22      return true
 23    } catch (error) {
 24      return false
 25    }
 26  }
 27  
 28  // Common transpositions of license identifier acronyms
 29  var transpositions = [
 30    ['APGL', 'AGPL'],
 31    ['Gpl', 'GPL'],
 32    ['GLP', 'GPL'],
 33    ['APL', 'Apache'],
 34    ['ISD', 'ISC'],
 35    ['GLP', 'GPL'],
 36    ['IST', 'ISC'],
 37    ['Claude', 'Clause'],
 38    [' or later', '+'],
 39    [' International', ''],
 40    ['GNU', 'GPL'],
 41    ['GUN', 'GPL'],
 42    ['+', ''],
 43    ['GNU GPL', 'GPL'],
 44    ['GNU/GPL', 'GPL'],
 45    ['GNU GLP', 'GPL'],
 46    ['GNU General Public License', 'GPL'],
 47    ['Gnu public license', 'GPL'],
 48    ['GNU Public License', 'GPL'],
 49    ['GNU GENERAL PUBLIC LICENSE', 'GPL'],
 50    ['MTI', 'MIT'],
 51    ['Mozilla Public License', 'MPL'],
 52    ['Universal Permissive License', 'UPL'],
 53    ['WTH', 'WTF'],
 54    ['-License', '']
 55  ]
 56  
 57  var TRANSPOSED = 0
 58  var CORRECT = 1
 59  
 60  // Simple corrections to nearly valid identifiers.
 61  var transforms = [
 62    // e.g. 'mit'
 63    function (argument) {
 64      return argument.toUpperCase()
 65    },
 66    // e.g. 'MIT '
 67    function (argument) {
 68      return argument.trim()
 69    },
 70    // e.g. 'M.I.T.'
 71    function (argument) {
 72      return argument.replace(/\./g, '')
 73    },
 74    // e.g. 'Apache- 2.0'
 75    function (argument) {
 76      return argument.replace(/\s+/g, '')
 77    },
 78    // e.g. 'CC BY 4.0''
 79    function (argument) {
 80      return argument.replace(/\s+/g, '-')
 81    },
 82    // e.g. 'LGPLv2.1'
 83    function (argument) {
 84      return argument.replace('v', '-')
 85    },
 86    // e.g. 'Apache 2.0'
 87    function (argument) {
 88      return argument.replace(/,?\s*(\d)/, '-$1')
 89    },
 90    // e.g. 'GPL 2'
 91    function (argument) {
 92      return argument.replace(/,?\s*(\d)/, '-$1.0')
 93    },
 94    // e.g. 'Apache Version 2.0'
 95    function (argument) {
 96      return argument
 97        .replace(/,?\s*(V\.|v\.|V|v|Version|version)\s*(\d)/, '-$2')
 98    },
 99    // e.g. 'Apache Version 2'
100    function (argument) {
101      return argument
102        .replace(/,?\s*(V\.|v\.|V|v|Version|version)\s*(\d)/, '-$2.0')
103    },
104    // e.g. 'ZLIB'
105    function (argument) {
106      return argument[0].toUpperCase() + argument.slice(1)
107    },
108    // e.g. 'MPL/2.0'
109    function (argument) {
110      return argument.replace('/', '-')
111    },
112    // e.g. 'Apache 2'
113    function (argument) {
114      return argument
115        .replace(/\s*V\s*(\d)/, '-$1')
116        .replace(/(\d)$/, '$1.0')
117    },
118    // e.g. 'GPL-2.0', 'GPL-3.0'
119    function (argument) {
120      if (argument.indexOf('3.0') !== -1) {
121        return argument + '-or-later'
122      } else {
123        return argument + '-only'
124      }
125    },
126    // e.g. 'GPL-2.0-'
127    function (argument) {
128      return argument + 'only'
129    },
130    // e.g. 'GPL2'
131    function (argument) {
132      return argument.replace(/(\d)$/, '-$1.0')
133    },
134    // e.g. 'BSD 3'
135    function (argument) {
136      return argument.replace(/(-| )?(\d)$/, '-$2-Clause')
137    },
138    // e.g. 'BSD clause 3'
139    function (argument) {
140      return argument.replace(/(-| )clause(-| )(\d)/, '-$3-Clause')
141    },
142    // e.g. 'New BSD license'
143    function (argument) {
144      return argument.replace(/\b(Modified|New|Revised)(-| )?BSD((-| )License)?/i, 'BSD-3-Clause')
145    },
146    // e.g. 'Simplified BSD license'
147    function (argument) {
148      return argument.replace(/\bSimplified(-| )?BSD((-| )License)?/i, 'BSD-2-Clause')
149    },
150    // e.g. 'Free BSD license'
151    function (argument) {
152      return argument.replace(/\b(Free|Net)(-| )?BSD((-| )License)?/i, 'BSD-2-Clause-$1BSD')
153    },
154    // e.g. 'Clear BSD license'
155    function (argument) {
156      return argument.replace(/\bClear(-| )?BSD((-| )License)?/i, 'BSD-3-Clause-Clear')
157    },
158    // e.g. 'Old BSD License'
159    function (argument) {
160      return argument.replace(/\b(Old|Original)(-| )?BSD((-| )License)?/i, 'BSD-4-Clause')
161    },
162    // e.g. 'BY-NC-4.0'
163    function (argument) {
164      return 'CC-' + argument
165    },
166    // e.g. 'BY-NC'
167    function (argument) {
168      return 'CC-' + argument + '-4.0'
169    },
170    // e.g. 'Attribution-NonCommercial'
171    function (argument) {
172      return argument
173        .replace('Attribution', 'BY')
174        .replace('NonCommercial', 'NC')
175        .replace('NoDerivatives', 'ND')
176        .replace(/ (\d)/, '-$1')
177        .replace(/ ?International/, '')
178    },
179    // e.g. 'Attribution-NonCommercial'
180    function (argument) {
181      return 'CC-' +
182        argument
183          .replace('Attribution', 'BY')
184          .replace('NonCommercial', 'NC')
185          .replace('NoDerivatives', 'ND')
186          .replace(/ (\d)/, '-$1')
187          .replace(/ ?International/, '') +
188        '-4.0'
189    }
190  ]
191  
192  var licensesWithVersions = spdxLicenseIds
193    .map(function (id) {
194      var match = /^(.*)-\d+\.\d+$/.exec(id)
195      return match
196        ? [match[0], match[1]]
197        : [id, null]
198    })
199    .reduce(function (objectMap, item) {
200      var key = item[1]
201      objectMap[key] = objectMap[key] || []
202      objectMap[key].push(item[0])
203      return objectMap
204    }, {})
205  
206  var licensesWithOneVersion = Object.keys(licensesWithVersions)
207    .map(function makeEntries (key) {
208      return [key, licensesWithVersions[key]]
209    })
210    .filter(function identifySoleVersions (item) {
211      return (
212        // Licenses has just one valid version suffix.
213        item[1].length === 1 &&
214        item[0] !== null &&
215        // APL will be considered Apache, rather than APL-1.0
216        item[0] !== 'APL'
217      )
218    })
219    .map(function createLastResorts (item) {
220      return [item[0], item[1][0]]
221    })
222  
223  licensesWithVersions = undefined
224  
225  // If all else fails, guess that strings containing certain substrings
226  // meant to identify certain licenses.
227  var lastResorts = [
228    ['UNLI', 'Unlicense'],
229    ['WTF', 'WTFPL'],
230    ['2 CLAUSE', 'BSD-2-Clause'],
231    ['2-CLAUSE', 'BSD-2-Clause'],
232    ['3 CLAUSE', 'BSD-3-Clause'],
233    ['3-CLAUSE', 'BSD-3-Clause'],
234    ['AFFERO', 'AGPL-3.0-or-later'],
235    ['AGPL', 'AGPL-3.0-or-later'],
236    ['APACHE', 'Apache-2.0'],
237    ['ARTISTIC', 'Artistic-2.0'],
238    ['Affero', 'AGPL-3.0-or-later'],
239    ['BEER', 'Beerware'],
240    ['BOOST', 'BSL-1.0'],
241    ['BSD', 'BSD-2-Clause'],
242    ['CDDL', 'CDDL-1.1'],
243    ['ECLIPSE', 'EPL-1.0'],
244    ['FUCK', 'WTFPL'],
245    ['GNU', 'GPL-3.0-or-later'],
246    ['LGPL', 'LGPL-3.0-or-later'],
247    ['GPLV1', 'GPL-1.0-only'],
248    ['GPL-1', 'GPL-1.0-only'],
249    ['GPLV2', 'GPL-2.0-only'],
250    ['GPL-2', 'GPL-2.0-only'],
251    ['GPL', 'GPL-3.0-or-later'],
252    ['MIT +NO-FALSE-ATTRIBS', 'MITNFA'],
253    ['MIT', 'MIT'],
254    ['MPL', 'MPL-2.0'],
255    ['X11', 'X11'],
256    ['ZLIB', 'Zlib']
257  ].concat(licensesWithOneVersion)
258  
259  var SUBSTRING = 0
260  var IDENTIFIER = 1
261  
262  var validTransformation = function (identifier) {
263    for (var i = 0; i < transforms.length; i++) {
264      var transformed = transforms[i](identifier).trim()
265      if (transformed !== identifier && valid(transformed)) {
266        return transformed
267      }
268    }
269    return null
270  }
271  
272  var validLastResort = function (identifier) {
273    var upperCased = identifier.toUpperCase()
274    for (var i = 0; i < lastResorts.length; i++) {
275      var lastResort = lastResorts[i]
276      if (upperCased.indexOf(lastResort[SUBSTRING]) > -1) {
277        return lastResort[IDENTIFIER]
278      }
279    }
280    return null
281  }
282  
283  var anyCorrection = function (identifier, check) {
284    for (var i = 0; i < transpositions.length; i++) {
285      var transposition = transpositions[i]
286      var transposed = transposition[TRANSPOSED]
287      if (identifier.indexOf(transposed) > -1) {
288        var corrected = identifier.replace(
289          transposed,
290          transposition[CORRECT]
291        )
292        var checked = check(corrected)
293        if (checked !== null) {
294          return checked
295        }
296      }
297    }
298    return null
299  }
300  
301  module.exports = function (identifier, options) {
302    options = options || {}
303    var upgrade = options.upgrade === undefined ? true : !!options.upgrade
304    function postprocess (value) {
305      return upgrade ? upgradeGPLs(value) : value
306    }
307    var validArugment = (
308      typeof identifier === 'string' &&
309      identifier.trim().length !== 0
310    )
311    if (!validArugment) {
312      throw Error('Invalid argument. Expected non-empty string.')
313    }
314    identifier = identifier.trim()
315    if (valid(identifier)) {
316      return postprocess(identifier)
317    }
318    var noPlus = identifier.replace(/\+$/, '').trim()
319    if (valid(noPlus)) {
320      return postprocess(noPlus)
321    }
322    var transformed = validTransformation(identifier)
323    if (transformed !== null) {
324      return postprocess(transformed)
325    }
326    transformed = anyCorrection(identifier, function (argument) {
327      if (valid(argument)) {
328        return argument
329      }
330      return validTransformation(argument)
331    })
332    if (transformed !== null) {
333      return postprocess(transformed)
334    }
335    transformed = validLastResort(identifier)
336    if (transformed !== null) {
337      return postprocess(transformed)
338    }
339    transformed = anyCorrection(identifier, validLastResort)
340    if (transformed !== null) {
341      return postprocess(transformed)
342    }
343    return null
344  }
345  
346  function upgradeGPLs (value) {
347    if ([
348      'GPL-1.0', 'LGPL-1.0', 'AGPL-1.0',
349      'GPL-2.0', 'LGPL-2.0', 'AGPL-2.0',
350      'LGPL-2.1'
351    ].indexOf(value) !== -1) {
352      return value + '-only'
353    } else if ([
354      'GPL-1.0+', 'GPL-2.0+', 'GPL-3.0+',
355      'LGPL-2.0+', 'LGPL-2.1+', 'LGPL-3.0+',
356      'AGPL-1.0+', 'AGPL-3.0+'
357    ].indexOf(value) !== -1) {
358      return value.replace(/\+$/, '-or-later')
359    } else if (['GPL-3.0', 'LGPL-3.0', 'AGPL-3.0'].indexOf(value) !== -1) {
360      return value + '-or-later'
361    } else {
362      return value
363    }
364  }