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