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 }