// Copyright 2016 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'dart:convert'; import 'dart:io' as system; import 'cache.dart'; import 'patterns.dart'; import 'limits.dart'; class FetchedContentsOf extends Key { FetchedContentsOf(dynamic value) : super(value); } enum LicenseType { unknown, bsd, gpl, lgpl, mpl, afl, mit, freetype, apache, apacheNotice, eclipse, ijg, zlib, icu, apsl, libpng, openssl } LicenseType convertLicenseNameToType(String name) { switch (name) { case 'Apache': case 'apache-license-2.0': case 'LICENSE-APACHE-2.0.txt': return LicenseType.apache; case 'BSD': case 'BSD.txt': return LicenseType.bsd; case 'LICENSE-LGPL-2': case 'LICENSE-LGPL-2.1': case 'COPYING-LGPL-2.1': return LicenseType.lgpl; case 'COPYING-GPL-3': return LicenseType.gpl; case 'FTL.TXT': return LicenseType.freetype; case 'zlib.h': return LicenseType.zlib; case 'png.h': return LicenseType.libpng; case 'ICU': return LicenseType.icu; case 'Apple Public Source License': return LicenseType.apsl; case 'OpenSSL': return LicenseType.openssl; case 'LICENSE.MPLv2': case 'COPYING-MPL-1.1': return LicenseType.mpl; // common file names that don't say what the type is case 'COPYING': case 'COPYING.txt': case 'COPYING.LIB': // lgpl usually case 'COPYING.RUNTIME': // gcc exception usually case 'LICENSE': case 'LICENSE.md': case 'license.html': case 'LICENSE.txt': case 'LICENSE.TXT': case 'LICENSE.cssmin': case 'NOTICE': case 'NOTICE.txt': case 'Copyright': case 'copyright': case 'license.txt': return LicenseType.unknown; // particularly weird file names case 'LICENSE-APPLE': case 'extreme.indiana.edu.license.TXT': case 'extreme.indiana.edu.license.txt': case 'javolution.license.TXT': case 'javolution.license.txt': case 'libyaml-license.txt': case 'license.patch': case 'license.rst': case 'LICENSE.rst': case 'mh-bsd-gcc': case 'pivotal.labs.license.txt': return LicenseType.unknown; } throw 'unknown license type: $name'; } LicenseType convertBodyToType(String body) { if (body.startsWith(lrApache)) return LicenseType.apache; if (body.startsWith(lrMPL)) return LicenseType.mpl; if (body.startsWith(lrGPL)) return LicenseType.gpl; if (body.startsWith(lrAPSL)) return LicenseType.apsl; if (body.contains(lrOpenSSL)) return LicenseType.openssl; if (body.contains(lrBSD)) return LicenseType.bsd; if (body.contains(lrMIT)) return LicenseType.mit; if (body.contains(lrZlib)) return LicenseType.zlib; if (body.contains(lrPNG)) return LicenseType.libpng; return LicenseType.unknown; } abstract class LicenseSource { List nearestLicensesFor(String name); License nearestLicenseOfType(LicenseType type); License nearestLicenseWithName(String name, { String authors }); } abstract class License implements Comparable { factory License.unique(String body, LicenseType type, { bool reformatted: false, String origin, bool yesWeKnowWhatItLooksLikeButItIsNot: false }) { if (!reformatted) body = _reformat(body); License result = _registry.putIfAbsent(body, () => new UniqueLicense._(body, type, origin: origin, yesWeKnowWhatItLooksLikeButItIsNot: yesWeKnowWhatItLooksLikeButItIsNot)); assert(() { if (result is! UniqueLicense || result.type != type) throw 'tried to add a UniqueLicense $type, but it was a duplicate of a ${result.runtimeType} ${result.type}'; return true; }()); return result; } factory License.template(String body, LicenseType type, { bool reformatted: false, String origin }) { if (!reformatted) body = _reformat(body); License result = _registry.putIfAbsent(body, () => new TemplateLicense._(body, type, origin: origin)); assert(() { if (result is! TemplateLicense || result.type != type) throw 'tried to add a TemplateLicense $type, but it was a duplicate of a ${result.runtimeType} ${result.type}'; return true; }()); return result; } factory License.message(String body, LicenseType type, { bool reformatted: false, String origin }) { if (!reformatted) body = _reformat(body); License result = _registry.putIfAbsent(body, () => new MessageLicense._(body, type, origin: origin)); assert(() { if (result is! MessageLicense || result.type != type) throw 'tried to add a MessageLicense $type, but it was a duplicate of a ${result.runtimeType} ${result.type}'; return true; }()); return result; } factory License.blank(String body, LicenseType type, { String origin }) { License result = _registry.putIfAbsent(body, () => new BlankLicense._(_reformat(body), type, origin: origin)); assert(() { if (result is! BlankLicense || result.type != type) throw 'tried to add a BlankLicense $type, but it was a duplicate of a ${result.runtimeType} ${result.type}'; return true; }()); return result; } factory License.fromMultipleBlocks(List bodies, LicenseType type) { final String body = bodies.map((String s) => _reformat(s)).join('\n\n'); return _registry.putIfAbsent(body, () => new UniqueLicense._(body, type)); } factory License.fromBodyAndType(String body, LicenseType type, { bool reformatted: false, String origin }) { if (!reformatted) body = _reformat(body); License result = _registry.putIfAbsent(body, () { switch (type) { case LicenseType.bsd: case LicenseType.mit: case LicenseType.zlib: case LicenseType.icu: return new TemplateLicense._(body, type, origin: origin); case LicenseType.unknown: case LicenseType.apacheNotice: return new UniqueLicense._(body, type, origin: origin); case LicenseType.afl: case LicenseType.mpl: case LicenseType.gpl: case LicenseType.lgpl: case LicenseType.freetype: case LicenseType.apache: case LicenseType.eclipse: case LicenseType.ijg: case LicenseType.apsl: return new MessageLicense._(body, type, origin: origin); case LicenseType.openssl: return new MultiLicense._(body, type, origin: origin); case LicenseType.libpng: return new BlankLicense._(body, type, origin: origin); } }); assert(result.type == type); return result; } factory License.fromBodyAndName(String body, String name, { String origin }) { body = _reformat(body); LicenseType type = convertLicenseNameToType(name); if (type == LicenseType.unknown) type = convertBodyToType(body); return new License.fromBodyAndType(body, type, origin: origin); } factory License.fromBody(String body, { String origin }) { body = _reformat(body); LicenseType type = convertBodyToType(body); return new License.fromBodyAndType(body, type, reformatted: true, origin: origin); } factory License.fromCopyrightAndLicense(String copyright, String template, LicenseType type, { String origin }) { String body = '$copyright\n\n$template'; return _registry.putIfAbsent(body, () => new TemplateLicense._(body, type, origin: origin)); } factory License.fromUrl(String url, { String origin }) { String body; LicenseType type = LicenseType.unknown; switch (url) { case 'Apache:2.0': case 'http://www.apache.org/licenses/LICENSE-2.0': body = new system.File('data/apache-license-2.0').readAsStringSync(); type = LicenseType.apache; break; case 'https://developers.google.com/open-source/licenses/bsd': body = new system.File('data/google-bsd').readAsStringSync(); type = LicenseType.bsd; break; case 'http://polymer.github.io/LICENSE.txt': body = new system.File('data/polymer-bsd').readAsStringSync(); type = LicenseType.bsd; break; case 'http://www.eclipse.org/legal/epl-v10.html': body = new system.File('data/eclipse-1.0').readAsStringSync(); type = LicenseType.eclipse; break; case 'COPYING3:3': body = new system.File('data/gpl-3.0').readAsStringSync(); type = LicenseType.gpl; break; case 'COPYING.LIB:2': case 'COPYING.LIother.m_:2': // blame hyatt body = new system.File('data/library-gpl-2.0').readAsStringSync(); type = LicenseType.lgpl; break; case 'GNU Lesser:2': // there has never been such a license, but the authors said they meant the LGPL2.1 case 'GNU Lesser:2.1': body = new system.File('data/lesser-gpl-2.1').readAsStringSync(); type = LicenseType.lgpl; break; case 'COPYING.RUNTIME:3.1': case 'GCC Runtime Library Exception:3.1': body = new system.File('data/gpl-gcc-exception-3.1').readAsStringSync(); break; case 'Academic Free License:3.0': body = new system.File('data/academic-3.0').readAsStringSync(); type = LicenseType.afl; break; case 'http://mozilla.org/MPL/2.0/:2.0': body = new system.File('data/mozilla-2.0').readAsStringSync(); type = LicenseType.mpl; break; case 'http://opensource.org/licenses/MIT': case 'http://opensource->org/licenses/MIT': // i don't even body = new system.File('data/mit').readAsStringSync(); type = LicenseType.mit; break; case 'http://www.unicode.org/copyright.html': body = new system.File('data/unicode').readAsStringSync(); type = LicenseType.icu; break; default: throw 'unknown url $url'; } return _registry.putIfAbsent(body, () => new License.fromBodyAndType(body, type, origin: origin)); } License._(String body, this.type, { this.origin, bool yesWeKnowWhatItLooksLikeButItIsNot: false }) : body = body, authors = _readAuthors(body) { assert(_reformat(body) == body); assert(() { try { switch (type) { case LicenseType.bsd: case LicenseType.mit: case LicenseType.zlib: case LicenseType.icu: assert(this is TemplateLicense); break; case LicenseType.unknown: assert(this is UniqueLicense || this is BlankLicense); break; case LicenseType.apacheNotice: assert(this is UniqueLicense); break; case LicenseType.afl: case LicenseType.mpl: case LicenseType.gpl: case LicenseType.lgpl: case LicenseType.freetype: case LicenseType.apache: case LicenseType.eclipse: case LicenseType.ijg: case LicenseType.apsl: assert(this is MessageLicense); break; case LicenseType.libpng: assert(this is BlankLicense); break; case LicenseType.openssl: assert(this is MultiLicense); break; } } on AssertionError { throw 'incorrectly created a $runtimeType for a $type'; } return true; }()); final LicenseType detectedType = convertBodyToType(body); if (detectedType != LicenseType.unknown && detectedType != type && !yesWeKnowWhatItLooksLikeButItIsNot) throw 'Created a license of type $type but it looks like $detectedType\.'; if (type != LicenseType.apache && type != LicenseType.apacheNotice) { if (!yesWeKnowWhatItLooksLikeButItIsNot && body.contains('Apache')) throw 'Non-Apache license (type=$type, detectedType=$detectedType) contains the word "Apache"; maybe it\'s a notice?:\n---\n$body\n---'; } if (body.contains(trailingColon)) throw 'incomplete license detected:\n---\n$body\n---'; // if (type == LicenseType.unknown) // print('need detector for:\n----\n$body\n----'); bool isUTF8 = true; List latin1Encoded; try { latin1Encoded = LATIN1.encode(body); isUTF8 = false; } on ArgumentError { } if (!isUTF8) { bool isAscii = false; try { ASCII.decode(latin1Encoded); isAscii = true; } on FormatException { } if (isAscii) return; try { UTF8.decode(latin1Encoded); isUTF8 = true; } on FormatException { } if (isUTF8) throw 'tried to create a License object with text that appears to have been misdecoded as Latin1 instead of as UTF-8:\n$body'; } } final String body; final String authors; final String origin; final LicenseType type; Iterable get licensees => _licensees; List _licensees = []; Set _libraries = new Set(); bool get isUsed => _licensees.isNotEmpty; void markUsed(String filename, String libraryName) { assert(libraryName != null); assert(libraryName != ''); filename != null; _licensees.add(filename); _libraries.add(libraryName); } Iterable expandTemplate(String copyright, String licenseBody, { String origin }); @override int compareTo(License other) => toString().compareTo(other.toString()); @override String toString() { final List prefixes = _libraries.toList(); prefixes.sort(); _licensees.sort(); final List header = []; header.addAll(prefixes.map((String s) => 'LIBRARY: $s')); header.add('ORIGIN: $origin'); header.add('TYPE: $type'); header.addAll(licensees.map((String s) => 'FILE: $s')); return ('=' * 100) + '\n' + header.join('\n') + '\n' + ('-' * 100) + '\n' + toStringBody() + '\n' + ('=' * 100); } String toStringBody() => body; String toStringFormal() { final List prefixes = _libraries.toList(); prefixes.sort(); return prefixes.join('\n') + '\n\n' + body; } static final RegExp _copyrightForAuthors = new RegExp( r'Copyright [-0-9 ,(cC)©]+\b(The .+ Authors)\.', caseSensitive: false ); static String _readAuthors(String body) { final List matches = _copyrightForAuthors.allMatches(body).toList(); if (matches.isEmpty) return null; if (matches.length > 1) throw 'found too many authors for this copyright:\n$body'; return matches[0].group(1); } } final Map _registry = {}; void clearLicenseRegistry() { _registry.clear(); } final License missingLicense = new UniqueLicense._('', LicenseType.unknown); String _reformat(String body) { // TODO(ianh): ensure that we're stripping the same amount of leading text on each line final List lines = body.split('\n'); while (lines.isNotEmpty && lines.first == '') lines.removeAt(0); while (lines.isNotEmpty && lines.last == '') lines.removeLast(); if (lines.length > 2) { if (lines[0].startsWith(beginLicenseBlock) && lines.last.startsWith(endLicenseBlock)) { lines.removeAt(0); lines.removeLast(); } } else if (lines.isEmpty) { return ''; } final List output = []; int lastGood; String previousPrefix; bool lastWasEmpty = true; for (String line in lines) { final Match match = stripDecorations.firstMatch(line); final String prefix = match.group(1); String s = match.group(2); if (!lastWasEmpty || s != '') { if (s != '') { if (previousPrefix != null) { if (previousPrefix.length > prefix.length) { // TODO(ianh): Spot check files that hit this. At least one just // has a corrupt license block, which is why this is commented out. //if (previousPrefix.substring(prefix.length).contains(nonSpace)) // throw 'inconsistent line prefix: was "$previousPrefix", now "$prefix"\nfull body was:\n---8<---\n$body\n---8<---'; previousPrefix = prefix; } else if (previousPrefix.length < prefix.length) { s = '${prefix.substring(previousPrefix.length)}$s'; } } else { previousPrefix = prefix; } lastWasEmpty = false; lastGood = output.length + 1; } else { lastWasEmpty = true; } output.add(s); } } if (lastGood == null) { print('_reformatted to nothing:\n----\n|${body.split("\n").join("|\n|")}|\n----'); assert(lastGood != null); throw 'reformatted to nothing:\n$body'; } return output.take(lastGood).join('\n'); } class _LineRange { _LineRange(this.start, this.end, this._body); final int start; final int end; final String _body; String _value; String get value { _value ??= _body.substring(start, end); return _value; } } Iterable<_LineRange> _walkLinesBackwards(String body, int start) sync* { int end; while (start > 0) { start -= 1; if (body[start] == '\n') { if (end != null) yield new _LineRange(start + 1, end, body); end = start; } } if (end != null) yield new _LineRange(start, end, body); } Iterable<_LineRange> _walkLinesForwards(String body, { int start: 0, int end }) sync* { int startIndex = start == 0 || body[start-1] == '\n' ? start : null; int endIndex = startIndex ?? start; end ??= body.length; while (endIndex < end) { if (body[endIndex] == '\n') { if (startIndex != null) yield new _LineRange(startIndex, endIndex, body); startIndex = endIndex + 1; } endIndex += 1; } if (startIndex != null) yield new _LineRange(startIndex, endIndex, body); } class _SplitLicense { _SplitLicense(this._body, this._split) { assert(this._split == 0 || this._split == this._body.length || this._body[this._split] == '\n'); } final String _body; final int _split; String getCopyright() => _body.substring(0, _split); String getConditions() => _split >= _body.length ? '' : _body.substring(_split == 0 ? 0 : _split + 1); } _SplitLicense _splitLicense(String body, { bool verifyResults: true }) { Iterator<_LineRange> lines = _walkLinesForwards(body).iterator; if (!lines.moveNext()) throw 'tried to split empty license'; int end = 0; while (true) { final String line = lines.current.value; if (line == 'Author:' || line == 'This code is derived from software contributed to Berkeley by' || line == 'The Initial Developer of the Original Code is') { if (!lines.moveNext()) throw 'unexpected end of block instead of author when looking for copyright'; if (lines.current.value.trim() == '') throw 'unexpectedly blank line instead of author when looking for copyright'; end = lines.current.end; if (!lines.moveNext()) break; } else if (line.startsWith('Authors:') || line == 'Other contributors:') { if (line != 'Authors:') { // assume this line contained an author as well end = lines.current.end; } if (!lines.moveNext()) throw 'unexpected end of license when reading list of authors while looking for copyright'; final String firstAuthor = lines.current.value; int subindex = 0; while (subindex < firstAuthor.length && (firstAuthor[subindex] == ' ' || firstAuthor[subindex] == '\t')) subindex += 1; if (subindex == 0 || subindex > firstAuthor.length) throw 'unexpected blank line instead of authors found when looking for copyright'; end = lines.current.end; final String prefix = firstAuthor.substring(0, subindex); while (lines.moveNext() && lines.current.value.startsWith(prefix)) { final String nextAuthor = lines.current.value.substring(prefix.length); if (nextAuthor == '' || nextAuthor[0] == ' ' || nextAuthor[0] == '\t') throw 'unexpectedly ragged author list when looking for copyright'; end = lines.current.end; } if (lines.current == null) break; } else if (line.contains(halfCopyrightPattern)) { do { if (!lines.moveNext()) throw 'unexpected end of block instead of copyright holder when looking for copyright'; if (lines.current.value.trim() == '') throw 'unexpectedly blank line instead of copyright holder when looking for copyright'; end = lines.current.end; } while (lines.current.value.contains(trailingComma)); if (!lines.moveNext()) break; } else if (copyrightStatementPatterns.every((RegExp pattern) => !line.contains(pattern))) { break; } else { end = lines.current.end; if (!lines.moveNext()) break; } } if (verifyResults && 'Copyright ('.allMatches(body, end).isNotEmpty && !body.startsWith('If you modify libpng')) throw 'the license seems to contain a copyright:\n===copyright===\n${body.substring(0, end)}\n===license===\n${body.substring(end)}\n========='; return new _SplitLicense(body, end); } class _PartialLicenseMatch { _PartialLicenseMatch(this._body, this.start, this.split, this.end, this._match, { this.hasCopyrights }) { assert(split >= start); assert(split == start || _body[split] == '\n'); } final String _body; final int start; final int split; final int end; final Match _match; String group(int index) => _match.group(index); String getAuthors() { final Match match = authorPattern.firstMatch(getCopyrights()); if (match != null) return match.group(1); return null; } String getCopyrights() => _body.substring(start, split); String getConditions() => _body.substring(split + 1, end); String getEntireLicense() => _body.substring(start, end); final bool hasCopyrights; } Iterable<_PartialLicenseMatch> _findLicenseBlocks(String body, RegExp pattern, int firstPrefixIndex, int indentPrefixIndex, { bool needsCopyright: true }) sync* { // I tried doing this with one big RegExp initially, but that way lay madness. for (Match match in pattern.allMatches(body)) { assert(match.groupCount >= firstPrefixIndex); assert(match.groupCount >= indentPrefixIndex); int start = match.start; final String fullPrefix = '${match.group(firstPrefixIndex)}${match.group(indentPrefixIndex)}'; // first we walk back to the start of the block that has the same prefix (e.g. // the start of this comment block) bool firstLineSpecialComment = false; bool lastWasBlank = false; bool foundNonBlank = false; for (_LineRange range in _walkLinesBackwards(body, start)) { String line = range.value; bool isBlockCommentLine; if (line.length > 3 && line.endsWith('*/')) { int index = line.length - 3; while (line[index] == ' ') index -= 1; line = line.substring(0, index + 1); isBlockCommentLine = true; } else { isBlockCommentLine = false; } if (line.isEmpty || fullPrefix.startsWith(line)) { // this is blank line if (lastWasBlank && (foundNonBlank || !needsCopyright)) break; lastWasBlank = true; } else if (((!isBlockCommentLine && line.startsWith('/*')) || line.startsWith('