blob: 2a6e8ee190e975a8acab190b35c5cc5c61b1623a [file] [log] [blame]
// Copyright 2013 The Flutter 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 'limits.dart';
import 'patterns.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, vulkan, bison }
LicenseType convertLicenseNameToType(String? name) {
switch (name) {
case 'Apache':
case 'apache-license-2.0':
case 'LICENSE-APACHE-2.0.txt':
case 'Apache-2.0.txt':
case 'LICENSE.vulkan':
return LicenseType.apache;
case 'BSD':
case 'BSD.txt':
case 'BSD-3-Clause.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':
case 'GPL-3.0-only.txt':
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;
case 'COPYRIGHT.vulkan':
return LicenseType.vulkan;
case 'LICENSE.MIT':
case 'MIT.txt':
return LicenseType.mit;
// 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 'COPYRIGHT.musl':
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;
}
if (body.contains(lrBison)) {
return LicenseType.bison;
}
return LicenseType.unknown;
}
abstract class LicenseSource {
List<License>? nearestLicensesFor(String name);
License? nearestLicenseOfType(LicenseType type);
License? nearestLicenseWithName(String name, { String? authors });
}
abstract class License implements Comparable<License> {
factory License.unique(String body, LicenseType type, {
bool reformatted = false,
String? origin,
bool yesWeKnowWhatItLooksLikeButItIsNot = false
}) {
if (!reformatted) {
body = _reformat(body);
}
final License result = _registry.putIfAbsent(body, () => 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);
}
final License result = _registry.putIfAbsent(body, () => 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);
}
final License result = _registry.putIfAbsent(body, () => 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 }) {
final License result = _registry.putIfAbsent(body, () => 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<String> bodies, LicenseType type) {
final String body = bodies.map((String s) => _reformat(s)).join('\n\n');
return _registry.putIfAbsent(body, () => UniqueLicense._(body, type));
}
factory License.fromBodyAndType(String body, LicenseType type, {
bool reformatted = false,
String? origin
}) {
if (!reformatted) {
body = _reformat(body);
}
final License result = _registry.putIfAbsent(body, () {
assert(type != null);
switch (type) {
case LicenseType.bsd:
case LicenseType.mit:
case LicenseType.zlib:
case LicenseType.icu:
return TemplateLicense._(body, type, origin: origin);
case LicenseType.unknown:
case LicenseType.apacheNotice:
return 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 MessageLicense._(body, type, origin: origin);
case LicenseType.vulkan:
case LicenseType.openssl:
return MultiLicense._(body, type, origin: origin);
case LicenseType.libpng:
return BlankLicense._(body, type, origin: origin);
// The exception in the license of Bison allows redistributing larger
// works "under terms of your choice"; we choose terms that don't require
// any notice in the binary distribution.
case LicenseType.bison:
return 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 License.fromBodyAndType(body, type, origin: origin);
}
factory License.fromBody(String body, { String? origin }) {
body = _reformat(body);
final LicenseType type = convertBodyToType(body);
return License.fromBodyAndType(body, type, reformatted: true, origin: origin);
}
factory License.fromCopyrightAndLicense(String copyright, String? template, LicenseType type, { String? origin }) {
final String body = '$copyright\n\n$template';
return _registry.putIfAbsent(body, () => 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 'Apache-2.0': // SPDX ID
case 'http://www.apache.org/licenses/LICENSE-2.0':
case 'https://www.apache.org/licenses/LICENSE-2.0':
// If we start seeing more OR options, we can parse them out and write
// a generic utility to pick according so some ranking; for now just
// hard-code a choice for this option set.
case 'Apache-2.0 OR MIT': // SPDX ID
body = system.File('data/apache-license-2.0').readAsStringSync();
type = LicenseType.apache;
break;
case 'Apache-2.0 WITH LLVM-exception': // SPDX ID
body = system.File('data/apache-license-2.0-with-llvm-exception').readAsStringSync();
type = LicenseType.apache;
break;
case 'https://developers.google.com/open-source/licenses/bsd':
body = system.File('data/google-bsd').readAsStringSync();
type = LicenseType.bsd;
break;
case 'http://polymer.github.io/LICENSE.txt':
body = system.File('data/polymer-bsd').readAsStringSync();
type = LicenseType.bsd;
break;
case 'http://www.eclipse.org/legal/epl-v10.html':
body = system.File('data/eclipse-1.0').readAsStringSync();
type = LicenseType.eclipse;
break;
case 'COPYING3:3':
body = system.File('data/gpl-3.0').readAsStringSync();
type = LicenseType.gpl;
break;
case 'COPYING.LIB:2':
case 'COPYING.LIother.m_:2': // blame hyatt
body = 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 = 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 = system.File('data/gpl-gcc-exception-3.1').readAsStringSync();
break;
case 'Academic Free License:3.0':
body = system.File('data/academic-3.0').readAsStringSync();
type = LicenseType.afl;
break;
case 'Mozilla Public License:1.1':
body = system.File('data/mozilla-1.1').readAsStringSync();
type = LicenseType.mpl;
break;
case 'http://mozilla.org/MPL/2.0/:2.0':
body = system.File('data/mozilla-2.0').readAsStringSync();
type = LicenseType.mpl;
break;
case 'MIT': // SPDX ID
case 'http://opensource.org/licenses/MIT':
case 'https://opensource.org/licenses/MIT':
case 'http://opensource->org/licenses/MIT': // i don't even
body = system.File('data/mit').readAsStringSync();
type = LicenseType.mit;
break;
case 'Unicode-DFS-2016': // SPDX ID
case 'https://www.unicode.org/copyright.html':
case 'http://www.unicode.org/copyright.html':
body = system.File('data/unicode').readAsStringSync();
type = LicenseType.icu;
break;
default: throw 'unknown url $url';
}
return _registry.putIfAbsent(body, () => License.fromBodyAndType(body, type, origin: origin));
}
License._(this.body, this.type, {
this.origin,
bool yesWeKnowWhatItLooksLikeButItIsNot = false
}) : 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:
case LicenseType.bison:
assert(this is BlankLicense);
break;
case LicenseType.openssl:
case LicenseType.vulkan:
assert(this is MultiLicense);
break;
}
} on AssertionError {
throw 'incorrectly created a $runtimeType for a $type';
}
return true;
}());
final LicenseType detectedType = convertBodyToType(body);
// Fuchsia SDK Vulkan license is Apache 2.0 with some additional BSD-matching copyrights.
if (type == LicenseType.vulkan) {
yesWeKnowWhatItLooksLikeButItIsNot = true;
}
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;
late List<int> latin1Encoded;
try {
latin1Encoded = latin1.encode(body);
isUTF8 = false;
} on ArgumentError { /* Fall through to next encoding check. */ }
if (!isUTF8) {
bool isAscii = false;
try {
ascii.decode(latin1Encoded);
isAscii = true;
} on FormatException { /* Fall through to next encoding check */ }
if (isAscii) {
return;
}
try {
utf8.decode(latin1Encoded);
isUTF8 = true;
} on FormatException { /* We check isUTF8 below and throw if necessary */ }
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;
final Set<String> _licensees = <String>{};
final Set<String> _libraries = <String>{};
bool get isUsed => _licensees.isNotEmpty;
void markUsed(String filename, String libraryName) {
assert(filename != null);
assert(filename != '');
assert(libraryName != null);
assert(libraryName != '');
_licensees.add(filename);
_libraries.add(libraryName);
}
Iterable<License> expandTemplate(String copyright, String licenseBody, { String? origin });
@override
int compareTo(License other) => toString().compareTo(other.toString());
@override
String toString() {
final List<String> prefixes = _libraries.toList();
prefixes.sort();
final List<String> licensees = _licensees.toList();
licensees.sort();
final List<String> header = <String>[];
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<String> prefixes = _libraries.toList();
prefixes.sort();
return '${prefixes.join('\n')}\n\n$body';
}
static final RegExp _copyrightForAuthors = RegExp(
r'Copyright [-0-9 ,(cC)©]+\b(The .+ Authors)\.',
caseSensitive: false
);
static String? _readAuthors(String body) {
final List<Match> 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<String, License> _registry = <String, License>{};
void clearLicenseRegistry() {
_registry.clear();
}
final License missingLicense = UniqueLicense._('<missing>', LicenseType.unknown);
String _reformat(String body) {
// TODO(ianh): ensure that we're stripping the same amount of leading text on each line
final List<String> 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<String?> output = <String?>[];
int? lastGood;
String? previousPrefix;
bool lastWasEmpty = true;
for (final 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 _LineRange(start + 1, end, body);
}
end = start;
}
}
if (end != null) {
yield _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 _LineRange(startIndex, endIndex, body);
}
startIndex = endIndex + 1;
}
endIndex += 1;
}
if (startIndex != null) {
yield _LineRange(startIndex, endIndex, body);
}
}
class _SplitLicense {
_SplitLicense(this._body, this._split) : assert(_split == 0 || _split == _body.length || _body[_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 }) {
final 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);
bool hadMoreLines;
while ((hadMoreLines = 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 (!hadMoreLines) {
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 _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 (final 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 (final _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('<!--')
|| (range.start == 0 && line.startsWith(' $fullPrefix'))) {
start = range.start;
firstLineSpecialComment = true;
break;
} else if (fullPrefix.isNotEmpty && !line.startsWith(fullPrefix)) {
break;
} else if (licenseFragments.any((RegExp pattern) => line.contains(pattern))) {
// we're running into another license, abort, abort!
break;
} else {
lastWasBlank = false;
foundNonBlank = true;
}
start = range.start;
}
// then we walk forward dropping anything until the first line that matches what
// we think might be part of a copyright statement
bool foundAny = false;
for (final _LineRange range in _walkLinesForwards(body, start: start, end: match.start)) {
final String line = range.value;
if (firstLineSpecialComment || line.startsWith(fullPrefix)) {
String? data;
if (firstLineSpecialComment) {
data = stripDecorations.firstMatch(line)!.group(2);
} else {
data = line.substring(fullPrefix.length);
}
if (copyrightStatementLeadingPatterns.any((RegExp pattern) => data!.contains(pattern))) {
start = range.start;
foundAny = true;
break;
}
}
firstLineSpecialComment = false;
}
// At this point we have figured out what might be copyright text before the license.
int split;
if (!foundAny) {
if (needsCopyright) {
throw 'could not find copyright before license\nlicense body was:\n---\n${body.substring(match.start, match.end)}\n---\nfile was:\n---\n$body\n---';
}
start = match.start;
split = match.start;
} else {
final String copyrights = body.substring(start, match.start);
final String undecoratedCopyrights = _reformat(copyrights);
// Let's try splitting the copyright block as if it was a license.
// This will tell us if we collected something in the copyright block
// that was more license than copyright and that therefore should be
// examined closer.
final _SplitLicense sanityCheck = _splitLicense(undecoratedCopyrights, verifyResults: false);
final String conditions = sanityCheck.getConditions();
if (conditions != '') {
// Copyright lines long enough to spill to a second line can create
// false positives; try to weed those out.
final String resplitCopyright = sanityCheck.getCopyright();
if (resplitCopyright.trim().contains('\n') ||
conditions.trim().contains('\n') ||
resplitCopyright.length < 70 ||
conditions.length > 15) {
throw 'potential license text caught in _findLicenseBlocks copyright dragnet:\n---\n$conditions\n---\nundecorated copyrights was:\n---\n$undecoratedCopyrights\n---\ncopyrights was:\n---\n$copyrights\n---\nblock was:\n---\n${body.substring(start, match.end)}\n---';
}
}
if (!copyrights.contains(copyrightMentionPattern)) {
throw 'could not find copyright before license block:\n---\ncopyrights was:\n---\n$copyrights\n---\nblock was:\n---\n${body.substring(start, match.end)}\n---';
}
if (body[match.start - 1] != '\n') {
print('about to assert; match.start = ${match.start}, match.end = ${match.end}, split at: "${body[match.start - 1]}"');
}
assert(body[match.start - 1] == '\n');
split = match.start - 1;
}
yield _PartialLicenseMatch(body, start, split, match.end, match, hasCopyrights: foundAny);
}
}
class _LicenseMatch {
_LicenseMatch(this.license, this.start, this.end, {
this.debug = '',
this.isDuplicate = false,
this.missingCopyrights = false
});
final License license;
final int start;
final int end;
final String debug;
final bool isDuplicate;
final bool missingCopyrights;
}
Iterable<_LicenseMatch> _expand(License template, String copyright, String body, int start, int end, { String debug = '', String? origin }) sync* {
final List<License> results = template.expandTemplate(_reformat(copyright), body, origin: origin).toList();
if (results.isEmpty) {
throw 'license could not be expanded';
}
yield _LicenseMatch(results.first, start, end, debug: 'expanding template for $debug');
if (results.length > 1) {
yield* results.skip(1).map((License license) => _LicenseMatch(license, start, end, isDuplicate: true, debug: 'expanding subsequent template for $debug'));
}
}
Iterable<_LicenseMatch> _tryNone(String body, String filename, RegExp pattern, LicenseSource parentDirectory) sync* {
for (final Match match in pattern.allMatches(body)) {
final List<License>? results = parentDirectory.nearestLicensesFor(filename);
if (results == null || results.isEmpty) {
throw 'no default license file found';
}
// TODO(ianh): use _expand if the license asks for the copyright to be included (e.g. BSD)
yield _LicenseMatch(results.first, match.start, match.end, debug: '_tryNone');
if (results.length > 1) {
yield* results.skip(1).map((License license) => _LicenseMatch(license, match.start, match.end, isDuplicate: true, debug: 'subsequent _tryNone'));
}
}
}
Iterable<_LicenseMatch> _tryAttribution(String body, RegExp pattern, { String? origin }) sync* {
for (final Match match in pattern.allMatches(body)) {
assert(match.groupCount == 2);
yield _LicenseMatch(License.unique('Thanks to ${match.group(2)}.', LicenseType.unknown, origin: origin), match.start, match.end, debug: '_tryAttribution');
}
}
Iterable<_LicenseMatch> _tryReferenceByFilename(String body, LicenseFileReferencePattern pattern, LicenseSource parentDirectory, { String? origin }) sync* {
if (pattern.copyrightIndex != null) {
for (final Match match in pattern.pattern!.allMatches(body)) {
final String copyright = match.group(pattern.copyrightIndex!)!;
final String? authors = pattern.authorIndex != null ? match.group(pattern.authorIndex!) : null;
final String filename = match.group(pattern.fileIndex!)!;
final License? template = parentDirectory.nearestLicenseWithName(filename, authors: authors);
if (template == null) {
throw 'failed to find template $filename in $parentDirectory (authors=$authors)';
}
assert(_reformat(copyright) != '');
final String entireLicense = body.substring(match.start, match.end);
yield* _expand(template, copyright, entireLicense, match.start, match.end, debug: '_tryReferenceByFilename (with explicit copyright) looking for $filename', origin: origin);
}
} else {
for (final _PartialLicenseMatch match in _findLicenseBlocks(body, pattern.pattern!, pattern.firstPrefixIndex, pattern.indentPrefixIndex, needsCopyright: pattern.needsCopyright)) {
final String? authors = match.getAuthors();
String? filename = match.group(pattern.fileIndex);
if (filename == 'modp_b64.c') {
filename = 'modp_b64.cc';
} // it was renamed but other files reference the old name
final License? template = parentDirectory.nearestLicenseWithName(filename!, authors: authors);
if (template == null) {
throw
'failed to find accompanying "$filename" in $parentDirectory\n'
'block:\n---\n${match.getEntireLicense()}\n---';
}
if (match.getCopyrights() == '') {
yield _LicenseMatch(template, match.start, match.end, debug: '_tryReferenceByFilename (with failed copyright search) looking for $filename');
} else {
yield* _expand(template, match.getCopyrights(), match.getEntireLicense(), match.start, match.end, debug: '_tryReferenceByFilename (with successful copyright search) looking for $filename', origin: origin);
}
}
}
}
Iterable<_LicenseMatch> _tryReferenceByType(String body, RegExp pattern, LicenseSource parentDirectory, { String? origin, bool needsCopyright = true }) sync* {
for (final _PartialLicenseMatch match in _findLicenseBlocks(body, pattern, 1, 2, needsCopyright: needsCopyright)) {
final LicenseType type = convertLicenseNameToType(match.group(3));
final License? template = parentDirectory.nearestLicenseOfType(type);
if (template == null) {
throw 'failed to find accompanying $type license in $parentDirectory';
}
assert(() {
final String copyrights = _reformat(match.getCopyrights());
assert(needsCopyright && copyrights.isNotEmpty || !needsCopyright && copyrights.isEmpty);
return true;
}());
if (needsCopyright) {
yield* _expand(template, match.getCopyrights(), match.getEntireLicense(), match.start, match.end, debug: '_tryReferenceByType', origin: origin);
} else {
yield _LicenseMatch(template, match.start, match.end, debug: '_tryReferenceByType (without copyrights) for type $type');
}
}
}
License _dereferenceLicense(int groupIndex, String? Function(int index) group, MultipleVersionedLicenseReferencePattern pattern, LicenseSource parentDirectory, { String? origin }) {
License? result = pattern.checkLocalFirst ? parentDirectory.nearestLicenseWithName(group(groupIndex)!) : null;
if (result == null) {
String suffix = '';
if (pattern.versionIndices != null && pattern.versionIndices!.containsKey(groupIndex)) {
suffix = ':${group(pattern.versionIndices![groupIndex]!)}';
}
result = License.fromUrl('${group(groupIndex)}$suffix', origin: origin);
}
return result;
}
Iterable<_LicenseMatch> _tryReferenceByUrl(String body, MultipleVersionedLicenseReferencePattern pattern, LicenseSource parentDirectory, { String? origin }) sync* {
for (final _PartialLicenseMatch match in _findLicenseBlocks(body, pattern.pattern!, 1, 2, needsCopyright: false)) {
bool isDuplicate = false;
for (final int index in pattern.licenseIndices!) {
final License result = _dereferenceLicense(index, match.group, pattern, parentDirectory, origin: origin);
yield _LicenseMatch(result, match.start, match.end, isDuplicate: isDuplicate, debug: '_tryReferenceByUrl');
isDuplicate = true;
}
}
}
Iterable<_LicenseMatch> _tryInline(String body, RegExp pattern, {
required bool needsCopyright,
String? origin,
}) sync* {
assert(needsCopyright != null);
for (final _PartialLicenseMatch match in _findLicenseBlocks(body, pattern, 1, 2, needsCopyright: false)) {
// We search with "needsCopyright: false" but then create a _LicenseMatch with
// "missingCopyrights: true" if our own "needsCopyright" argument is true.
// We use a template license here (not unique) because it's not uncommon for files
// to reference license blocks in other files, but with their own copyrights.
yield _LicenseMatch(License.fromBody(match.getEntireLicense(), origin: origin), match.start, match.end, debug: '_tryInline', missingCopyrights: needsCopyright && !match.hasCopyrights!);
}
}
Iterable<_LicenseMatch> _tryForwardReferencePattern(String fileContents, ForwardReferencePattern pattern, License template, { String? origin }) sync* {
for (final _PartialLicenseMatch match in _findLicenseBlocks(fileContents, pattern.pattern!, pattern.firstPrefixIndex, pattern.indentPrefixIndex)) {
if (!template.body.contains(pattern.targetPattern!)) {
throw
'forward license reference to unexpected license\n'
'license:\n---\n${template.body}\n---\nexpected pattern:\n---\n${pattern.targetPattern}\n---';
}
yield* _expand(template, match.getCopyrights(), match.getEntireLicense(), match.start, match.end, debug: '_tryForwardReferencePattern', origin: origin);
}
}
List<License> determineLicensesFor(String fileContents, String filename, LicenseSource? parentDirectory, { String? origin }) {
if (parentDirectory == null) {
throw 'Fatal error: determineLicensesFor was called with parentDirectory=null!';
}
if (fileContents.length > kMaxSize) {
fileContents = fileContents.substring(0, kMaxSize);
}
final List<_LicenseMatch> results = <_LicenseMatch>[];
fileContents = fileContents.replaceAll('\t', ' ');
fileContents = fileContents.replaceAll(newlinePattern, '\n');
results.addAll(csNoCopyrights.expand((RegExp pattern) => _tryNone(fileContents, filename, pattern, parentDirectory)));
results.addAll(csAttribution.expand((RegExp pattern) => _tryAttribution(fileContents, pattern, origin: origin)));
results.addAll(csReferencesByFilename.expand((LicenseFileReferencePattern pattern) => _tryReferenceByFilename(fileContents, pattern, parentDirectory, origin: origin)));
results.addAll(csReferencesByType.expand((RegExp pattern) => _tryReferenceByType(fileContents, pattern, parentDirectory, origin: origin)));
results.addAll(csReferencesByTypeNoCopyright.expand((RegExp pattern) => _tryReferenceByType(fileContents, pattern, parentDirectory, origin: origin, needsCopyright: false)));
results.addAll(csReferencesByUrl.expand((MultipleVersionedLicenseReferencePattern pattern) => _tryReferenceByUrl(fileContents, pattern, parentDirectory, origin: origin)));
results.addAll(csLicenses.expand((RegExp pattern) => _tryInline(fileContents, pattern, needsCopyright: true, origin: origin)));
results.addAll(csNotices.expand((RegExp pattern) => _tryInline(fileContents, pattern, needsCopyright: false, origin: origin)));
_LicenseMatch? usedTemplate;
if (results.length == 1) {
final _LicenseMatch target = results.single;
results.addAll(csForwardReferenceLicenses.expand((ForwardReferencePattern pattern) => _tryForwardReferencePattern(fileContents, pattern, target.license, origin: origin)));
if (results.length > 1) {
usedTemplate = target;
}
}
// It's good to manually sanity check that these are all being correctly used
// to expand later licenses every now and then:
// for (_LicenseMatch match in results.where((_LicenseMatch match) => match.missingCopyrights)) {
// print('Found a license for $filename but it was missing a copyright, so ignoring it:\n----8<----\n${match.license}\n----8<----');
// }
results.removeWhere((_LicenseMatch match) => match.missingCopyrights);
if (results.isEmpty) {
// we failed to find a license, so let's look for some corner cases
results.addAll(csFallbacks.expand((RegExp pattern) => _tryNone(fileContents, filename, pattern, parentDirectory)));
if (results.isEmpty) {
if ((fileContents.contains(copyrightMentionPattern) && fileContents.contains(licenseMentionPattern)) && !fileContents.contains(copyrightMentionOkPattern)) {
throw 'unmatched potential copyright and license statements; first twenty lines:\n----8<----\n${fileContents.split("\n").take(20).join("\n")}\n----8<----';
}
}
}
final List<_LicenseMatch> verificationList = results.toList();
if (usedTemplate != null && !verificationList.contains(usedTemplate)) {
verificationList.add(usedTemplate);
}
verificationList.sort((_LicenseMatch a, _LicenseMatch b) {
final int result = a.start - b.start;
if (result != 0) {
return result;
}
return a.end - b.end;
});
int position = 0;
for (final _LicenseMatch m in verificationList) {
if (m.isDuplicate) {
continue;
} // some text expanded into multiple licenses, so overlapping is expected
if (position > m.start) {
for (final _LicenseMatch n in results) {
print('license match: ${n.start}..${n.end}, ${n.debug}, first line: ${n.license.body.split("\n").first}');
}
throw 'overlapping licenses in $filename (one ends at $position, another starts at ${m.start})';
}
if (position < m.start) {
final String substring = fileContents.substring(position, m.start);
if (substring.contains(copyrightMentionPattern) && !substring.contains(copyrightMentionOkPattern)) {
throw 'there is another unmatched potential copyright statement in $filename:\n $position..${m.start}: "$substring"';
}
}
position = m.end;
}
return results.map((_LicenseMatch entry) => entry.license).toList();
}
License? interpretAsRedirectLicense(String fileContents, LicenseSource parentDirectory, { String? origin }) {
_SplitLicense split;
try {
split = _splitLicense(fileContents);
} on String {
return null;
}
final String body = split.getConditions().trim();
License? result;
for (final MultipleVersionedLicenseReferencePattern pattern in csReferencesByUrl) {
final Match? match = pattern.pattern!.matchAsPrefix(body);
if (match != null && match.start == 0 && match.end == body.length) {
for (final int index in pattern.licenseIndices!) {
final License candidate = _dereferenceLicense(index, match.group as String? Function(int?), pattern, parentDirectory, origin: origin);
if (result != null && candidate != null) {
throw 'Multiple potential matches in interpretAsRedirectLicense in $parentDirectory; body was:\n------8<------\n$fileContents\n------8<------';
}
result = candidate;
}
}
}
return result;
}
// the kind of license that just wants to show a message (e.g. the JPEG one)
class MessageLicense extends License {
MessageLicense._(String body, LicenseType type, { String? origin }) : super._(body, type, origin: origin);
@override
Iterable<License> expandTemplate(String copyright, String licenseBody, { String? origin }) sync* {
yield this;
}
}
// the kind of license that says to include the copyright and the license text (e.g. BSD)
class TemplateLicense extends License {
TemplateLicense._(String body, LicenseType type, { String? origin })
: assert(!body.startsWith('Apache License')),
super._(body, type, origin: origin);
String? _conditions;
@override
Iterable<License> expandTemplate(String copyright, String licenseBody, { String? origin }) sync* {
_conditions ??= _splitLicense(body).getConditions();
yield License.fromCopyrightAndLicense(copyright, _conditions, type, origin: '$origin + ${this.origin}');
}
}
// the kind of license that expands to two license blocks a main license and the referring block (e.g. OpenSSL)
class MultiLicense extends License {
MultiLicense._(String body, LicenseType type, { String? origin }) : super._(body, type, origin: origin);
@override
Iterable<License> expandTemplate(String copyright, String licenseBody, { String? origin }) sync* {
yield License.fromBody(body, origin: '$origin + ${this.origin}');
yield License.fromBody(licenseBody, origin: '$origin + ${this.origin}');
}
}
// the kind of license that should not be combined with separate copyright notices
class UniqueLicense extends License {
UniqueLicense._(String body, LicenseType type, {
String? origin,
bool yesWeKnowWhatItLooksLikeButItIsNot = false
}) : super._(body, type, origin: origin, yesWeKnowWhatItLooksLikeButItIsNot: yesWeKnowWhatItLooksLikeButItIsNot);
@override
Iterable<License> expandTemplate(String copyright, String licenseBody, { String? origin }) sync* {
throw 'attempted to expand non-template license with "$copyright"\ntemplate was: $this';
}
}
// the kind of license that doesn't need to be reported anywhere
class BlankLicense extends License {
BlankLicense._(String body, LicenseType type, { String? origin }) : super._(body, type, origin: origin);
@override
Iterable<License> expandTemplate(String copyright, String licenseBody, { String? origin }) sync* {
yield this;
}
@override
String toStringBody() => '<THIS BLOCK INTENTIONALLY LEFT BLANK>';
@override
String? toStringFormal() => null;
}