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