• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/*
2Copyright 2015 Kyle E. Mitchell
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// Common transpositions of license identifier acronyms
29var 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  ['WTH', 'WTF'],
53  ['-License', '']
54]
55
56var TRANSPOSED = 0
57var CORRECT = 1
58
59// Simple corrections to nearly valid identifiers.
60var transforms = [
61  // e.g. 'mit'
62  function (argument) {
63    return argument.toUpperCase()
64  },
65  // e.g. 'MIT '
66  function (argument) {
67    return argument.trim()
68  },
69  // e.g. 'M.I.T.'
70  function (argument) {
71    return argument.replace(/\./g, '')
72  },
73  // e.g. 'Apache- 2.0'
74  function (argument) {
75    return argument.replace(/\s+/g, '')
76  },
77  // e.g. 'CC BY 4.0''
78  function (argument) {
79    return argument.replace(/\s+/g, '-')
80  },
81  // e.g. 'LGPLv2.1'
82  function (argument) {
83    return argument.replace('v', '-')
84  },
85  // e.g. 'Apache 2.0'
86  function (argument) {
87    return argument.replace(/,?\s*(\d)/, '-$1')
88  },
89  // e.g. 'GPL 2'
90  function (argument) {
91    return argument.replace(/,?\s*(\d)/, '-$1.0')
92  },
93  // e.g. 'Apache Version 2.0'
94  function (argument) {
95    return argument
96      .replace(/,?\s*(V\.|v\.|V|v|Version|version)\s*(\d)/, '-$2')
97  },
98  // e.g. 'Apache Version 2'
99  function (argument) {
100    return argument
101      .replace(/,?\s*(V\.|v\.|V|v|Version|version)\s*(\d)/, '-$2.0')
102  },
103  // e.g. 'ZLIB'
104  function (argument) {
105    return argument[0].toUpperCase() + argument.slice(1)
106  },
107  // e.g. 'MPL/2.0'
108  function (argument) {
109    return argument.replace('/', '-')
110  },
111  // e.g. 'Apache 2'
112  function (argument) {
113    return argument
114      .replace(/\s*V\s*(\d)/, '-$1')
115      .replace(/(\d)$/, '$1.0')
116  },
117  // e.g. 'GPL-2.0', 'GPL-3.0'
118  function (argument) {
119    if (argument.indexOf('3.0') !== -1) {
120      return argument + '-or-later'
121    } else {
122      return argument + '-only'
123    }
124  },
125  // e.g. 'GPL-2.0-'
126  function (argument) {
127    return argument + 'only'
128  },
129  // e.g. 'GPL2'
130  function (argument) {
131    return argument.replace(/(\d)$/, '-$1.0')
132  },
133  // e.g. 'BSD 3'
134  function (argument) {
135    return argument.replace(/(-| )?(\d)$/, '-$2-Clause')
136  },
137  // e.g. 'BSD clause 3'
138  function (argument) {
139    return argument.replace(/(-| )clause(-| )(\d)/, '-$3-Clause')
140  },
141  // e.g. 'BY-NC-4.0'
142  function (argument) {
143    return 'CC-' + argument
144  },
145  // e.g. 'BY-NC'
146  function (argument) {
147    return 'CC-' + argument + '-4.0'
148  },
149  // e.g. 'Attribution-NonCommercial'
150  function (argument) {
151    return argument
152      .replace('Attribution', 'BY')
153      .replace('NonCommercial', 'NC')
154      .replace('NoDerivatives', 'ND')
155      .replace(/ (\d)/, '-$1')
156      .replace(/ ?International/, '')
157  },
158  // e.g. 'Attribution-NonCommercial'
159  function (argument) {
160    return 'CC-' +
161      argument
162        .replace('Attribution', 'BY')
163        .replace('NonCommercial', 'NC')
164        .replace('NoDerivatives', 'ND')
165        .replace(/ (\d)/, '-$1')
166        .replace(/ ?International/, '') +
167      '-4.0'
168  }
169]
170
171var licensesWithVersions = spdxLicenseIds
172  .map(function (id) {
173    var match = /^(.*)-\d+\.\d+$/.exec(id)
174    return match
175      ? [match[0], match[1]]
176      : [id, null]
177  })
178  .reduce(function (objectMap, item) {
179    var key = item[1]
180    objectMap[key] = objectMap[key] || []
181    objectMap[key].push(item[0])
182    return objectMap
183  }, {})
184
185var licensesWithOneVersion = Object.keys(licensesWithVersions)
186  .map(function makeEntries (key) {
187    return [key, licensesWithVersions[key]]
188  })
189  .filter(function identifySoleVersions (item) {
190    return (
191      // Licenses has just one valid version suffix.
192      item[1].length === 1 &&
193      item[0] !== null &&
194      // APL will be considered Apache, rather than APL-1.0
195      item[0] !== 'APL'
196    )
197  })
198  .map(function createLastResorts (item) {
199    return [item[0], item[1][0]]
200  })
201
202licensesWithVersions = undefined
203
204// If all else fails, guess that strings containing certain substrings
205// meant to identify certain licenses.
206var lastResorts = [
207  ['UNLI', 'Unlicense'],
208  ['WTF', 'WTFPL'],
209  ['2 CLAUSE', 'BSD-2-Clause'],
210  ['2-CLAUSE', 'BSD-2-Clause'],
211  ['3 CLAUSE', 'BSD-3-Clause'],
212  ['3-CLAUSE', 'BSD-3-Clause'],
213  ['AFFERO', 'AGPL-3.0-or-later'],
214  ['AGPL', 'AGPL-3.0-or-later'],
215  ['APACHE', 'Apache-2.0'],
216  ['ARTISTIC', 'Artistic-2.0'],
217  ['Affero', 'AGPL-3.0-or-later'],
218  ['BEER', 'Beerware'],
219  ['BOOST', 'BSL-1.0'],
220  ['BSD', 'BSD-2-Clause'],
221  ['CDDL', 'CDDL-1.1'],
222  ['ECLIPSE', 'EPL-1.0'],
223  ['FUCK', 'WTFPL'],
224  ['GNU', 'GPL-3.0-or-later'],
225  ['LGPL', 'LGPL-3.0-or-later'],
226  ['GPLV1', 'GPL-1.0-only'],
227  ['GPLV2', 'GPL-2.0-only'],
228  ['GPL', 'GPL-3.0-or-later'],
229  ['MIT +NO-FALSE-ATTRIBS', 'MITNFA'],
230  ['MIT', 'MIT'],
231  ['MPL', 'MPL-2.0'],
232  ['X11', 'X11'],
233  ['ZLIB', 'Zlib']
234].concat(licensesWithOneVersion)
235
236var SUBSTRING = 0
237var IDENTIFIER = 1
238
239var validTransformation = function (identifier) {
240  for (var i = 0; i < transforms.length; i++) {
241    var transformed = transforms[i](identifier).trim()
242    if (transformed !== identifier && valid(transformed)) {
243      return transformed
244    }
245  }
246  return null
247}
248
249var validLastResort = function (identifier) {
250  var upperCased = identifier.toUpperCase()
251  for (var i = 0; i < lastResorts.length; i++) {
252    var lastResort = lastResorts[i]
253    if (upperCased.indexOf(lastResort[SUBSTRING]) > -1) {
254      return lastResort[IDENTIFIER]
255    }
256  }
257  return null
258}
259
260var anyCorrection = function (identifier, check) {
261  for (var i = 0; i < transpositions.length; i++) {
262    var transposition = transpositions[i]
263    var transposed = transposition[TRANSPOSED]
264    if (identifier.indexOf(transposed) > -1) {
265      var corrected = identifier.replace(
266        transposed,
267        transposition[CORRECT]
268      )
269      var checked = check(corrected)
270      if (checked !== null) {
271        return checked
272      }
273    }
274  }
275  return null
276}
277
278module.exports = function (identifier) {
279  var validArugment = (
280    typeof identifier === 'string' &&
281    identifier.trim().length !== 0
282  )
283  if (!validArugment) {
284    throw Error('Invalid argument. Expected non-empty string.')
285  }
286  identifier = identifier.replace(/\+$/, '').trim()
287  if (valid(identifier)) {
288    return upgradeGPLs(identifier)
289  }
290  var transformed = validTransformation(identifier)
291  if (transformed !== null) {
292    return upgradeGPLs(transformed)
293  }
294  transformed = anyCorrection(identifier, function (argument) {
295    if (valid(argument)) {
296      return argument
297    }
298    return validTransformation(argument)
299  })
300  if (transformed !== null) {
301    return upgradeGPLs(transformed)
302  }
303  transformed = validLastResort(identifier)
304  if (transformed !== null) {
305    return upgradeGPLs(transformed)
306  }
307  transformed = anyCorrection(identifier, validLastResort)
308  if (transformed !== null) {
309    return upgradeGPLs(transformed)
310  }
311  return null
312}
313
314function upgradeGPLs (value) {
315  if ([
316    'GPL-1.0', 'LGPL-1.0', 'AGPL-1.0',
317    'GPL-2.0', 'LGPL-2.0', 'AGPL-2.0',
318    'LGPL-2.1'
319  ].indexOf(value) !== -1) {
320    return value + '-only'
321  } else if (['GPL-3.0', 'LGPL-3.0', 'AGPL-3.0'].indexOf(value) !== -1) {
322    return value + '-or-later'
323  } else {
324    return value
325  }
326}
327