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