| // 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:core' hide RegExp; |
| import 'dart:io' as system; |
| |
| import 'cache.dart'; |
| import 'formatter.dart'; |
| import 'limits.dart'; |
| import 'patterns.dart'; |
| import 'regexp_debug.dart'; |
| |
| class FetchedContentsOf extends Key { FetchedContentsOf(super.value); } |
| |
| enum LicenseType { |
| afl, |
| apache, |
| apacheNotice, |
| apsl, |
| bison, |
| bsd, |
| eclipse, |
| freetype, |
| gpl, |
| icu, |
| defaultTemplate, // metatype: a license that applies to a file without an internal license; only used when searching for a license |
| ietf, |
| ijg, |
| lgpl, |
| libpng, |
| llvm, |
| mit, |
| mpl, |
| openssl, |
| unicode, |
| unknown, |
| vulkan, |
| zlib, |
| } |
| |
| LicenseType convertLicenseNameToType(String? name) { |
| switch (name) { |
| case 'Apache': |
| case 'Apache-2.0.txt': |
| case 'LICENSE-APACHE-2.0.txt': |
| case 'LICENSE.vulkan': |
| case 'APACHE-LICENSE-2.0': |
| return LicenseType.apache; |
| case 'BSD': |
| case 'BSD-3-Clause.txt': |
| case 'BSD.txt': |
| return LicenseType.bsd; |
| case 'COPYING-LGPL-2.1': |
| case 'LICENSE-LGPL-2': |
| case 'LICENSE-LGPL-2.1': |
| return LicenseType.lgpl; |
| case 'COPYING-GPL-3': |
| case 'GPL-3.0-only.txt': |
| case 'GPLv2.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 'COPYING-MPL-1.1': |
| case 'LICENSE.MPLv2': |
| case 'http://mozilla.org/MPL/2.0/': |
| return LicenseType.mpl; |
| case 'COPYRIGHT.vulkan': |
| return LicenseType.vulkan; |
| case 'LICENSE.MIT': |
| case 'MIT': |
| case 'MIT.txt': |
| return LicenseType.mit; |
| // file names that don't say what the type is |
| case 'COPYING': |
| case 'COPYING.LIB': // lgpl usually |
| case 'COPYING.RUNTIME': // gcc exception usually |
| case 'COPYING.txt': |
| case 'COPYRIGHT.musl': |
| case 'Copyright': |
| case 'LICENSE': |
| case 'LICENSE-APPLE': |
| case 'LICENSE.TXT': |
| case 'LICENSE.cssmin': |
| case 'LICENSE.md': |
| case 'LICENSE.rst': |
| case 'LICENSE.txt': |
| case 'License.txt': |
| case 'NOTICE': |
| case 'NOTICE.txt': |
| case 'copyright': |
| 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.html': |
| case 'license.patch': |
| case 'license.txt': |
| 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) && body.contains(lrLLVM)) { |
| return LicenseType.llvm; |
| } |
| 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; |
| } |
| |
| // API exposed by the classes in main.dart |
| abstract class LicenseSource { |
| String get name; |
| String get libraryName; |
| String get officialSourceLocation; |
| List<License>? nearestLicensesFor(String name); |
| License? nearestLicenseOfType(LicenseType type); |
| License? nearestLicenseWithName(String name, { String? authors }); |
| } |
| |
| // Represents a license/file pairing, with metadata saying where the license came from. |
| class Assignment { |
| const Assignment(this.license, this.target, this.source); |
| final License license; |
| final String target; |
| final LicenseSource source; |
| } |
| |
| // Represents a group of files assigned to the same license, so that we can avoid |
| // duplicating licenses in the output. |
| class GroupedLicense { |
| GroupedLicense(this.type, this.body); |
| final LicenseType type; |
| final String body; |
| |
| // The names of files to which this license applies. |
| final Set<String> targets = <String>{}; |
| |
| // The libraries from which those files originate. |
| final Set<String> libraries = <String>{}; |
| |
| // How we determined the license applied to these files. |
| final Set<String> origins = <String>{}; |
| |
| String toStringDebug() { |
| final StringBuffer result = StringBuffer(); |
| result.writeln('=' * 100); |
| (libraries.map((String s) => 'LIBRARY: $s').toList()..sort()).forEach(result.writeln); |
| (origins.map((String s) => 'ORIGIN: $s').toList()..sort()).forEach(result.writeln); |
| result.writeln('TYPE: $type'); |
| (targets.map((String s) => 'FILE: $s').toList()..sort()).forEach(result.writeln); |
| result.writeln('-' * 100); |
| if (body.isEmpty) { |
| result.writeln('<THIS BLOCK INTENTIONALLY LEFT BLANK>'); |
| } else { |
| result.writeln(body); |
| } |
| result.writeln('=' * 100); |
| return result.toString(); |
| } |
| |
| String toStringFormal() { |
| final StringBuffer result = StringBuffer(); |
| (libraries.toList()..sort()).forEach(result.writeln); |
| result.writeln(); |
| assert(body.isNotEmpty); |
| result.write(body); |
| return result.toString(); |
| } |
| } |
| |
| List<GroupedLicense> groupLicenses(Iterable<Assignment> assignments) { |
| final Map<String, GroupedLicense> groups = <String, GroupedLicense>{}; |
| for (final Assignment assignment in assignments) { |
| final String body = assignment.license.toStringBody(assignment.source); |
| final GroupedLicense entry = groups.putIfAbsent(body, () => GroupedLicense(assignment.license.type, body)); |
| entry.targets.add(assignment.target); |
| entry.libraries.add(assignment.source.libraryName); |
| entry.origins.add(assignment.license.origin); |
| } |
| final List<GroupedLicense> results = groups.values.toList(); |
| results.sort((GroupedLicense a, GroupedLicense b) => a.body.compareTo(b.body)); |
| return results; |
| } |
| |
| abstract class License { |
| factory License.unique(String body, LicenseType type, { |
| bool reformatted = false, |
| required String origin, |
| String? authors, |
| bool yesWeKnowWhatItLooksLikeButItIsNot = false |
| }) { |
| if (!reformatted) { |
| body = reformat(body); |
| } |
| final License result = UniqueLicense._(body, type, origin: origin, yesWeKnowWhatItLooksLikeButItIsNot: yesWeKnowWhatItLooksLikeButItIsNot, authors: authors); |
| 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, |
| required String origin, |
| String? authors, |
| }) { |
| if (!reformatted) { |
| body = reformat(body); |
| } |
| final License result = TemplateLicense._autosplit(body, type, origin: origin, authors: authors); |
| 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.multiLicense(String body, LicenseType type, { |
| bool reformatted = false, |
| String? authors, |
| required String origin |
| }) { |
| if (!reformatted) { |
| body = reformat(body); |
| } |
| final License result = MultiLicense._(body, type, origin: origin, authors: authors); |
| assert(() { |
| if (result is! MultiLicense || result.type != type) { |
| throw 'tried to add a MultiLicense $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, |
| required String origin |
| }) { |
| if (!reformatted) { |
| body = reformat(body); |
| } |
| final License result = 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, { required String origin }) { |
| final License result = 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.mozilla(String body, { required String origin }) { |
| body = reformat(body); |
| final License result = MozillaLicense._(body, LicenseType.mpl, origin: origin); |
| assert(() { |
| if (result is! MozillaLicense) { |
| throw 'tried to add a MozillaLicense, but it was a duplicate of a ${result.runtimeType} ${result.type}'; |
| } |
| return true; |
| }()); |
| return result; |
| } |
| |
| factory License.fromMultipleBlocks(List<String> bodies, LicenseType type, { |
| String? authors, |
| required String origin, |
| bool yesWeKnowWhatItLooksLikeButItIsNot = false, |
| }) { |
| final String body = bodies.map((String s) => reformat(s)).join('\n\n'); |
| return MultiLicense._(body, type, authors: authors, origin: origin, yesWeKnowWhatItLooksLikeButItIsNot: yesWeKnowWhatItLooksLikeButItIsNot); |
| } |
| |
| factory License.fromBodyAndType(String body, LicenseType type, { |
| bool reformatted = false, |
| required String origin |
| }) { |
| if (!reformatted) { |
| body = reformat(body); |
| } |
| final License result; |
| switch (type) { |
| case LicenseType.bsd: |
| case LicenseType.mit: |
| result = TemplateLicense._autosplit(body, type, origin: origin); |
| case LicenseType.apache: |
| case LicenseType.freetype: |
| case LicenseType.ijg: |
| case LicenseType.ietf: |
| case LicenseType.libpng: |
| case LicenseType.llvm: // The LLVM license is an Apache variant |
| case LicenseType.unicode: |
| case LicenseType.unknown: |
| case LicenseType.vulkan: |
| case LicenseType.zlib: |
| result = MessageLicense._(body, type, origin: origin); |
| case LicenseType.apacheNotice: |
| result = UniqueLicense._(body, type, origin: origin); |
| case LicenseType.mpl: |
| result = MozillaLicense._(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: |
| result = BlankLicense._(body, type, origin: origin); |
| case LicenseType.icu: |
| case LicenseType.openssl: |
| throw 'Use License.fromMultipleBlocks rather than License.fromBodyAndType for the ICU and OpenSSL licenses.'; |
| case LicenseType.afl: |
| case LicenseType.apsl: |
| case LicenseType.eclipse: |
| case LicenseType.gpl: |
| case LicenseType.lgpl: |
| result = DisallowedLicense._(body, type, origin: origin); |
| case LicenseType.defaultTemplate: |
| throw 'should not be creating a LicenseType.defaultTemplate license, it is not a real type'; |
| } |
| assert(result.type == type); |
| return result; |
| } |
| |
| factory License.fromBodyAndName(String body, String name, { required String origin }) { |
| body = reformat(body); |
| LicenseType type = convertLicenseNameToType(name); |
| if (type == LicenseType.unknown) { |
| type = convertBodyToType(body); |
| } |
| return License.fromBodyAndType(body, type, reformatted: true, origin: origin); |
| } |
| |
| factory License.fromBody(String body, { required String origin, bool reformatted = false }) { |
| if (!reformatted) { |
| 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, { required String origin }) { |
| copyright = reformat(copyright); |
| template = reformat(template); |
| return TemplateLicense._(copyright, template, type, origin: origin); |
| } |
| |
| factory License.fromIdentifyingReference(String identifyingReference, { required String referencer }) { |
| String body; |
| LicenseType type = LicenseType.unknown; |
| switch (identifyingReference) { |
| case 'Apache-2.0 OR MIT': // SPDX ID |
| case 'Apache-2.0': // SPDX ID |
| case 'Apache:2.0': |
| case 'http://www.apache.org/licenses/LICENSE-2.0': |
| case 'https://www.apache.org/licenses/LICENSE-2.0': |
| // If you're wondering why Abseil has what appears to be a duplicate copy of |
| // the Apache license, it's because of this: |
| // https://github.com/abseil/abseil-cpp/pull/270/files#r793181143 |
| body = system.File('data/apache-license-2.0').readAsStringSync(); |
| type = LicenseType.apache; |
| case 'Apache-2.0 WITH LLVM-exception': // SPDX ID |
| case 'https://llvm.org/LICENSE.txt': |
| body = system.File('data/apache-license-2.0-with-llvm-exception').readAsStringSync(); |
| type = LicenseType.llvm; |
| case 'https://developers.google.com/open-source/licenses/bsd': |
| body = system.File('data/google-bsd').readAsStringSync(); |
| type = LicenseType.bsd; |
| case 'http://polymer.github.io/LICENSE.txt': |
| body = system.File('data/polymer-bsd').readAsStringSync(); |
| type = LicenseType.bsd; |
| case 'http://www.eclipse.org/legal/epl-v10.html': |
| body = system.File('data/eclipse-1.0').readAsStringSync(); |
| type = LicenseType.eclipse; |
| case 'COPYING3:3': |
| body = system.File('data/gpl-3.0').readAsStringSync(); |
| type = LicenseType.gpl; |
| case 'COPYING.LIB:2': |
| case 'COPYING.LIother.m_:2': // blame hyatt |
| body = system.File('data/library-gpl-2.0').readAsStringSync(); |
| type = LicenseType.lgpl; |
| 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; |
| case 'COPYING.RUNTIME:3.1': |
| case 'GCC Runtime Library Exception:3.1': |
| body = system.File('data/gpl-gcc-exception-3.1').readAsStringSync(); |
| case 'Academic Free License:3.0': |
| body = system.File('data/academic-3.0').readAsStringSync(); |
| type = LicenseType.afl; |
| case 'Mozilla Public License:1.1': |
| body = system.File('data/mozilla-1.1').readAsStringSync(); |
| type = LicenseType.mpl; |
| case 'http://mozilla.org/MPL/2.0/:2.0': |
| body = system.File('data/mozilla-2.0').readAsStringSync(); |
| type = LicenseType.mpl; |
| case 'MIT': // SPDX ID |
| case 'http://opensource->org/licenses/MIT': // i don't even |
| case 'http://opensource.org/licenses/MIT': |
| case 'https://opensource.org/licenses/MIT': |
| body = system.File('data/mit').readAsStringSync(); |
| type = LicenseType.mit; |
| case 'Unicode-DFS-2016': // SPDX ID |
| case 'http://unicode.org/copyright.html#Exhibit1': |
| case 'http://www.unicode.org/copyright.html#License': |
| case 'http://www.unicode.org/copyright.html': |
| case 'http://www.unicode.org/terms_of_use.html': // redirects to copyright.html |
| case 'https://www.unicode.org/copyright.html': |
| case 'https://www.unicode.org/terms_of_use.html': // redirects to copyright.html |
| body = system.File('data/unicode').readAsStringSync(); |
| type = LicenseType.unicode; |
| case 'http://www.ietf.org/rfc/rfc3454.txt': |
| body = system.File('data/ietf').readAsStringSync(); |
| type = LicenseType.ietf; |
| default: throw 'unknown identifyingReference $identifyingReference'; |
| } |
| return License.fromBodyAndType(body, type, origin: '$identifyingReference referenced by $referencer'); |
| } |
| |
| License._(String body, this.type, { |
| required this.origin, |
| String? authors, |
| bool yesWeKnowWhatItLooksLikeButItIsNot = false |
| }) : authors = authors ?? _readAuthors(body) { |
| assert(() { |
| try { |
| switch (type) { |
| case LicenseType.afl: |
| case LicenseType.apsl: |
| case LicenseType.eclipse: |
| case LicenseType.gpl: |
| case LicenseType.lgpl: |
| // We do not want this kind of license in our build. |
| assert(this is DisallowedLicense); |
| case LicenseType.apache: |
| case LicenseType.freetype: |
| case LicenseType.ijg: |
| case LicenseType.ietf: |
| case LicenseType.libpng: |
| case LicenseType.llvm: |
| case LicenseType.unicode: |
| case LicenseType.vulkan: |
| case LicenseType.zlib: |
| assert(this is MessageLicense); |
| case LicenseType.apacheNotice: |
| assert(this is UniqueLicense); |
| case LicenseType.bison: |
| assert(this is BlankLicense); |
| case LicenseType.bsd: |
| case LicenseType.mit: |
| assert(this is TemplateLicense); |
| case LicenseType.icu: |
| case LicenseType.openssl: |
| assert(this is MultiLicense); |
| case LicenseType.mpl: |
| assert(this is MozillaLicense); |
| case LicenseType.unknown: |
| assert(this is MessageLicense || this is UniqueLicense); |
| case LicenseType.defaultTemplate: |
| assert(false, 'should not be creating LicenseType.defaultTemplate license'); |
| } |
| } 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:\n---------\n$body\n-----------'; |
| } |
| if (type != LicenseType.apache && type != LicenseType.llvm && type != LicenseType.vulkan && type != LicenseType.apacheNotice && 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 (detectedType != LicenseType.unknown && detectedType != type && !yesWeKnowWhatItLooksLikeButItIsNot) { |
| throw 'Created a license of type $type but it looks like $detectedType.'; |
| } |
| if (body.contains(trailingColon)) { |
| throw 'incomplete license detected:\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? authors; |
| final String origin; |
| final LicenseType type; |
| |
| Assignment assignLicenses(String target, LicenseSource source) { |
| return Assignment(this, target, source); |
| } |
| |
| // This takes a second license, which has been pre-split into copyright and |
| // licenseBody, and uses this license to expand it into a new license. How |
| // this works depends on this license; for example BSD licenses typically take |
| // their body and put on the given copyright, (and the licenseBody argument |
| // here in those cases is usually a reference to that license); some other |
| // licenses turn into two, one for the original license and one for this |
| // copyright/body pair. |
| Iterable<License> _expandTemplate(String copyright, String licenseBody, { required String origin }); |
| |
| String toStringBody(LicenseSource source); |
| |
| 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\n\n${StackTrace.current}\n\n'; |
| } |
| return matches[0].group(1); |
| } |
| |
| @override |
| String toString() => '$runtimeType ($type) from $origin'; |
| } |
| |
| 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; |
| String endReason; |
| 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()) { |
| endReason = 'ran out of text after author'; |
| 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] == ' ')) { |
| 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] == ' ') { |
| throw 'unexpectedly ragged author list when looking for copyright'; |
| } |
| end = lines.current.end; |
| } |
| if (!hadMoreLines) { |
| endReason = 'ran out of text while collecting authors'; |
| 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()) { |
| endReason = 'ran out of text after matching halfCopyrightPattern/trailingComma sequence'; |
| break; |
| } |
| } else if (copyrightStatementPatterns.any(line.contains)) { |
| end = lines.current.end; |
| if (!lines.moveNext()) { |
| endReason = 'ran out of text after copyright statement pattern'; |
| break; |
| } |
| } else { |
| endReason = 'line did not match any copyright patterns ("$line")'; |
| 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=========\ntermination reason: $endReason'; |
| } |
| return _SplitLicense(body, end); |
| } |
| |
| class _PartialLicenseMatch { |
| _PartialLicenseMatch(this._body, this.start, this.split, this.end, this._match, { required 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; |
| } |
| |
| // Look for all matches of `pattern` in `body` and return them along with associated copyrights. |
| 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) |
| int firstLineOffset = 0; |
| 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 a blank line |
| if (lastWasBlank && (foundNonBlank || !needsCopyright)) { |
| break; |
| } |
| lastWasBlank = true; |
| } else if (!isBlockCommentLine && line.startsWith('/*')) { |
| start = range.start; |
| firstLineOffset = 2; |
| break; |
| } else if (line.startsWith('<!--')) { |
| start = range.start; |
| firstLineOffset = 4; |
| 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; |
| RegExp? debugFirstPattern; |
| copyrightSearch: for (final _LineRange range in _walkLinesForwards(body, start: start, end: match.start)) { |
| String line = range.value; |
| if (firstLineOffset > 0) { |
| line = line.substring(firstLineOffset); |
| firstLineOffset = 0; |
| } else if (line.startsWith(fullPrefix)) { |
| line = line.substring(fullPrefix.length); |
| } else { |
| assert(line.isEmpty || fullPrefix.startsWith(line), 'invariant violated: expected this to be a blank line but it was "$line" (prefix is "$fullPrefix").'); |
| continue copyrightSearch; |
| } |
| for (final RegExp pattern in copyrightStatementLeadingPatterns) { |
| if (line.contains(pattern)) { |
| start = range.start; |
| foundAny = true; |
| debugFirstPattern = pattern; |
| break copyrightSearch; |
| } |
| } |
| } |
| // At this point we have figured out what might be copyright text before the license (if anything). |
| 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 consistencyCheck = _splitLicense(undecoratedCopyrights, verifyResults: false); |
| final String conditions = consistencyCheck.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 = consistencyCheck.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---\nfirst line matched: $debugFirstPattern\n---\npattern:\n$pattern\n---\n${StackTrace.current}\n============='; |
| } |
| } |
| |
| if (!copyrights.contains(anySlightSignOfCopyrights)) { |
| 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---'; |
| } |
| assert(body[match.start - 1] == '\n', 'match did not start at a newline; match.start = ${match.start}, match.end = ${match.end}, split at: "${body[match.start - 1]}"'); |
| 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.ignoreWhenCheckingOverlappingRegions = false, |
| this.missingCopyrights = false |
| }); |
| final License license; |
| final int start; |
| final int end; |
| final String debug; |
| final bool ignoreWhenCheckingOverlappingRegions; |
| final bool missingCopyrights; |
| |
| @override |
| String toString() { |
| return '$start..$end: $license'; |
| } |
| } |
| |
| Iterable<_LicenseMatch> _expand(License template, String copyright, String body, int start, int end, { String debug = '', required 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, ignoreWhenCheckingOverlappingRegions: true, debug: 'expanding subsequent template for $debug')); |
| } |
| } |
| |
| Iterable<_LicenseMatch> _tryReferenceByFilename(String body, LicenseFileReferencePattern pattern, LicenseSource parentDirectory, { required 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 |
| } |
| // There's also special cases for fuchsia/sdk/linux/dart/zircon/lib/src/fakes/handle_disposition.dart |
| // (which points to "The Flutter Authors" instead of "The Fuchsia Authors" for mysterious reasons) and |
| // third_party/angle/src/common/fuchsia_egl/fuchsia_egl.* (which does something similar), but those |
| // files never reach here because they're marked as binary files in filesystem.dart. |
| 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, { required String origin }) sync* { |
| for (final _PartialLicenseMatch match in _findLicenseBlocks(body, pattern, 1, 2, needsCopyright: false)) { |
| 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'; |
| } |
| if (match.getCopyrights() == '') { |
| yield _LicenseMatch(template, match.start, match.end, debug: '_tryReferenceByType (without copyrights) for type $type'); |
| } else { |
| yield* _expand(template, match.getCopyrights(), match.getEntireLicense(), match.start, match.end, debug: '_tryReferenceByType (with successful copyright search) for type $type', origin: origin); |
| } |
| } |
| } |
| |
| License _dereferenceLicense(int groupIndex, String? Function(int index) group, LicenseReferencePattern pattern, LicenseSource parentDirectory, { required String origin }) { |
| License? result = pattern.checkLocalFirst ? parentDirectory.nearestLicenseWithName(group(groupIndex)!) : null; |
| result ??= License.fromIdentifyingReference(group(groupIndex)!, referencer: origin); |
| return result; |
| } |
| |
| Iterable<_LicenseMatch> _tryReferenceByIdentifyingReference(String body, LicenseReferencePattern pattern, LicenseSource parentDirectory, { required String origin }) sync* { |
| for (final _PartialLicenseMatch match in _findLicenseBlocks(body, pattern.pattern!, 1, 2, needsCopyright: false)) { |
| if (pattern.spdx) { |
| // Per legal advice, we allowlist the use of SPDX headers. Currently, we |
| // recognize SPDX headers in code from Khronos. To identify such code, we |
| // examine the copyright that came with the SPDX header or we look at the |
| // library name. We also accept the headers in some libcxx files that have |
| // their own license and that otherwise would trip up our code checking |
| // for missed licenses and copyrights. To identify those, we use the |
| // library name and the parent directory name or copyright. |
| bool allowSpdx = false; |
| final String copyrights = match.getCopyrights(); |
| if (copyrights.contains('The Khronos Group') || |
| parentDirectory.libraryName == 'spirv-cross' || |
| (parentDirectory.libraryName == 'libcxx' && parentDirectory.name == 'ryu') || |
| (parentDirectory.libraryName == 'libcxx' && copyrights.contains('Microsoft'))) { |
| allowSpdx = true; |
| } |
| if (!allowSpdx) { |
| // Skip this match. |
| continue; |
| } |
| } |
| assert(match.group(3) != null, 'pattern ${pattern.pattern!} did not have three groups when matched against:\n---\n$body\n---\nmatch: $match'); |
| final License template = _dereferenceLicense(3, match.group, pattern, parentDirectory, origin: origin); |
| if (match.getCopyrights() == '') { |
| yield _LicenseMatch(template, match.start, match.end, debug: '_tryReferenceByIdentifyingReference (without copyrights)'); |
| } else { |
| yield* _expand(template, match.getCopyrights(), match.getEntireLicense(), match.start, match.end, debug: '_tryReferenceByIdentifyingReference (with copyright)', origin: origin); |
| } |
| } |
| } |
| |
| Iterable<_LicenseMatch> _tryInline(String body, RegExp pattern, { |
| required bool needsCopyright, |
| required String origin, |
| }) sync* { |
| for (final _PartialLicenseMatch match in _findLicenseBlocks(body, pattern, 1, 2, needsCopyright: needsCopyright)) { |
| yield _LicenseMatch(License.fromBody(match.getEntireLicense(), origin: origin), match.start, match.end, debug: '_tryInline', missingCopyrights: needsCopyright && !match.hasCopyrights); |
| } |
| } |
| |
| Iterable<_LicenseMatch> _tryStray(String body, RegExp pattern, LicenseSource parentDirectory, { required String origin }) sync* { |
| // this one doesn't look for copyrights (that's the point, the patterns _are_ the copyrights) |
| bool gotTemplate = false; |
| License? template; |
| for (final Match match in pattern.allMatches(body)) { |
| if (!gotTemplate) { |
| template = parentDirectory.nearestLicenseOfType(LicenseType.defaultTemplate); |
| gotTemplate = true; |
| } |
| if (template != null) { |
| yield* _expand(template, match.group(0)!, match.group(0)!, match.start, match.end, debug: '_tryStray (with template)', origin: origin); |
| } else { |
| yield _LicenseMatch(License.fromBody(match.group(0)!, origin: origin), match.start, match.end, debug: '_tryStray'); |
| } |
| } |
| } |
| |
| Iterable<_LicenseMatch> _tryForwardReferencePattern(String fileContents, ForwardReferencePattern pattern, License template, LicenseSource source, { required String origin }) sync* { |
| String? body; |
| for (final _PartialLicenseMatch match in _findLicenseBlocks(fileContents, pattern.pattern, pattern.firstPrefixIndex, pattern.indentPrefixIndex)) { |
| body ??= template.toStringBody(source); |
| if (!body.contains(pattern.targetPattern)) { |
| throw |
| 'forward license reference to unexpected license\n' |
| 'license:\n---\n$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, { required 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 = stripAsciiArt(fileContents); |
| 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(csReferencesByIdentifyingReference.expand((LicenseReferencePattern pattern) => _tryReferenceByIdentifyingReference(fileContents, pattern, parentDirectory, origin: origin))); |
| results.addAll(csTemplateLicenses.expand((RegExp pattern) => _tryInline(fileContents, pattern, needsCopyright: true, origin: origin))); |
| results.addAll(csNoticeLicenses.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, parentDirectory, origin: origin))); |
| if (results.length > 1) { |
| usedTemplate = target; |
| } |
| } |
| for (final _LicenseMatch match in results.where((_LicenseMatch match) => match.missingCopyrights)) { |
| throw 'found a license for $filename but could not match its copyright:\n----8<----\n${match.license}\n----8<----'; |
| } |
| if (results.isEmpty) { |
| if ((fileContents.contains(copyrightMentionPattern) && fileContents.contains(licenseMentionPattern)) && !fileContents.contains(copyrightMentionOkPattern)) { |
| throw 'failed to find any license but saw unmatched potential copyright and license statements; first twenty lines:\n----8<----\n${fileContents.split("\n").take(20).join("\n")}\n----8<----'; |
| } |
| } |
| // Some files have the odd copyright that isn't explicitly attached to a |
| // license; we treat those as notice licenses. Only such copyrights |
| // allowlisted in csStrayCopyrights are handled this way, though. For each of |
| // these, we have to make sure they don't overlap any of the actual licenses |
| // matched earlier, so we check for overlaps on each one first. |
| strays: for (final _LicenseMatch stray in csStrayCopyrights.expand((RegExp pattern) => _tryStray(fileContents, pattern, parentDirectory, origin: origin))) { |
| for (final _LicenseMatch full in results) { |
| if (stray.start >= full.start && stray.end <= full.end) { |
| continue strays; |
| } |
| } |
| results.add(stray); |
| } |
| 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.ignoreWhenCheckingOverlappingRegions) { |
| continue; |
| } // some text expanded into multiple licenses, so overlapping is expected |
| if (position > m.start) { |
| system.stderr.writeln('\n\noverlapping licenses:'); |
| for (final _LicenseMatch n in results) { |
| system.stderr.writeln( |
| 'license match: ${n.start}..${n.end}, ${n.license.runtimeType}, ${n.debug}\n' |
| ' first line: ${n.license.toStringBody(parentDirectory).split("\n").first}\n' |
| ' last line: ${n.license.toStringBody(parentDirectory).split("\n").last}' |
| ); |
| } |
| 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"\nmatched licenses: $results'; |
| } |
| if (substring.contains(licenseMentionPattern)) { |
| throw 'there is another unmatched potential license in $filename:\n $position..${m.start}: "$substring"\nmatched licenses: $results'; |
| } |
| } |
| position = m.end; |
| } |
| final String substring = fileContents.substring(position); |
| if (substring.contains(copyrightMentionPattern) && !substring.contains(copyrightMentionOkPattern)) { |
| throw 'there is an unmatched potential copyright statement in $filename:\n $position..end: "$substring"\nmatched licenses: $results'; |
| } |
| if (substring.contains(licenseMentionPattern)) { |
| throw 'there is an unmatched potential license in $filename:\n $position..end: "$substring"\nmatched licenses: $results'; |
| } |
| return results.map((_LicenseMatch entry) => entry.license).toSet().toList(); |
| } |
| |
| License? interpretAsRedirectLicense(String fileContents, LicenseSource parentDirectory, { required String origin }) { |
| _SplitLicense split; |
| try { |
| split = _splitLicense(fileContents); |
| } on String { |
| return null; |
| } |
| final String body = split.getConditions().trim(); |
| License? result; |
| for (final LicenseReferencePattern pattern in csReferencesByIdentifyingReference) { |
| if (pattern.spdx) { |
| // We don't support SPDX headers in files that use _RepositoryLicenseRedirectFile. |
| // Before changing this, obtain legal advice. |
| continue; |
| } |
| final Match? match = pattern.pattern!.matchAsPrefix(body); |
| if (match != null && match.start == 0 && match.end == body.length) { |
| final License candidate = _dereferenceLicense(3, match.group as String? Function(int?), pattern, parentDirectory, origin: origin); |
| if (result != 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._(this.body, LicenseType type, { required String origin }) : super._(body, type, origin: origin); |
| |
| final String body; |
| |
| @override |
| String toStringBody(LicenseSource source) => body; |
| |
| @override |
| Iterable<License> _expandTemplate(String copyright, String licenseBody, { required String origin }) => <License>[this]; |
| } |
| |
| // the kind of license that says to include the copyright and the license text (e.g. BSD) |
| class TemplateLicense extends License { |
| TemplateLicense._(this.defaultCopyright, this.terms, LicenseType type, { String? authors, required String origin }) |
| : assert(!defaultCopyright.endsWith('\n')), |
| assert(!terms.startsWith('\n')), |
| assert(terms.isNotEmpty), |
| super._('$defaultCopyright\n\n$terms', type, origin: origin, authors: authors); |
| |
| factory TemplateLicense._autosplit(String body, LicenseType type, { String? authors, required String origin }) { |
| final _SplitLicense bits = _splitLicense(body); |
| final String copyright = bits.getCopyright(); |
| final String terms = bits.getConditions(); |
| assert((copyright.isEmpty && terms == body) || ('$copyright\n$terms' == body) || (copyright == body && terms.isEmpty), '_splitLicense contract violation.\nCOPYRIGHT:\n===\n$copyright\n===\nTERMS:\n===\n$terms\n===\nBODY:\n===\n$body\n===\n'); |
| int copyrightLength = copyright.length; |
| while (copyrightLength > 0 && copyright[copyrightLength - 1] == '\n') { |
| copyrightLength -= 1; |
| } |
| int termsStart = 0; |
| while (termsStart < terms.length && terms[termsStart] == '\n') { |
| termsStart += 1; |
| } |
| return TemplateLicense._( |
| copyright.substring(0, copyrightLength), |
| terms.substring(termsStart), |
| type, |
| authors: authors, |
| origin: origin, |
| ); |
| } |
| |
| final String defaultCopyright; |
| final String terms; |
| |
| @override |
| String toStringBody(LicenseSource source) { |
| if (defaultCopyright.isEmpty) { |
| return terms; |
| } |
| return '$defaultCopyright\n\n$terms'; |
| } |
| |
| @override |
| Iterable<License> _expandTemplate(String copyright, String licenseBody, { required String origin }) { |
| return <License>[ License.fromCopyrightAndLicense(copyright, terms, 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). |
| // |
| // This is a lawyer-suggested workaround for handling BSD-style licenses where instead of there |
| // being a single license block where it's obvious what is meant by "above copyright notice" and |
| // "this list of conditions", we instead have a bunch of similar licenses, each with their own |
| // copyright, plus there's a copyright in the file that (probably) doesn't exactly match any of the |
| // copyrights in the file, plus some text saying that that license applies to the file. |
| class MultiLicense extends License { |
| MultiLicense._(this.body, LicenseType type, { |
| String? authors, |
| required String origin, |
| bool yesWeKnowWhatItLooksLikeButItIsNot = false, |
| }) : super._(body, type, origin: origin, authors: authors, yesWeKnowWhatItLooksLikeButItIsNot: yesWeKnowWhatItLooksLikeButItIsNot); |
| |
| final String body; |
| |
| @override |
| String toStringBody(LicenseSource source) => body; |
| |
| @override |
| Iterable<License> _expandTemplate(String copyright, String licenseBody, { required String origin }) { |
| // Sometimes a license (e.g. the OpenSSL license in the BoringSSL package) is referenced |
| // from a file that has its own copyright header. When that happens we just print the referenced |
| // license and the reference to that license separately because it's not at all clear how we're |
| // supposed to merge them otherwise. |
| licenseBody = reformat(licenseBody); |
| assert(licenseBody.startsWith(copyright), 'copyright:\n---\n$copyright\n---\nlicenseBody:\n---\n$licenseBody\n---'); |
| return <License>[ |
| this, |
| License.fromBody(licenseBody, origin: '$origin (with ${this.origin})', reformatted: true), |
| ]; |
| } |
| } |
| |
| // the kind of license that should not be combined with separate copyright notices |
| class UniqueLicense extends License { |
| UniqueLicense._(this.body, LicenseType type, { |
| required String origin, |
| String? authors, |
| bool yesWeKnowWhatItLooksLikeButItIsNot = false |
| }) : super._(body, type, origin: origin, yesWeKnowWhatItLooksLikeButItIsNot: yesWeKnowWhatItLooksLikeButItIsNot, authors: authors); |
| |
| final String body; |
| |
| @override |
| String toStringBody(LicenseSource source) => body; |
| |
| @override |
| Iterable<License> _expandTemplate(String copyright, String licenseBody, { required String origin }) { |
| 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._(super.body, super.type, { required super.origin }) : super._(); |
| |
| @override |
| Iterable<License> _expandTemplate(String copyright, String licenseBody, { required String origin }) { |
| // We don't care what copyrights this kind of license has, we don't need |
| // to report them. Just report |this| (which is always blank, see below). |
| return <License>[this]; |
| } |
| |
| @override |
| String toStringBody(LicenseSource source) => ''; |
| } |
| |
| // MPL |
| class MozillaLicense extends License { |
| MozillaLicense._(this.body, LicenseType type, { required String origin }) : assert(type == LicenseType.mpl), super._(body, type, origin: origin); |
| |
| final String body; |
| |
| @override |
| Assignment assignLicenses(String target, LicenseSource source) { |
| if (source.libraryName != 'fallback_root_certificates') { |
| throw 'Only fallack_root_certificates is allowed to use the MPL.'; |
| } |
| return Assignment(this, target, source); |
| } |
| |
| @override |
| Iterable<License> _expandTemplate(String copyright, String licenseBody, { required String origin }) { |
| throw 'attempted to expand non-template license with "$copyright"\ntemplate was: $this'; |
| } |
| |
| @override |
| String toStringBody(LicenseSource source) { |
| final StringBuffer result = StringBuffer(); |
| result.writeln(body); |
| result.writeln(); |
| result.writeln("You may obtain a copy of this library's Source Code Form from: ${source.officialSourceLocation}"); |
| return result.toString(); |
| } |
| } |
| |
| class DisallowedLicense extends License { |
| DisallowedLicense._(this.body, LicenseType type, { required String origin }) : super._(body, type, origin: origin); |
| |
| final String body; |
| |
| @override |
| Assignment assignLicenses(String target, LicenseSource source) { |
| throw '$target (in ${source.libraryName}) attempted to use $origin which is a disallowed license type ($type)'; |
| } |
| |
| @override |
| Iterable<License> _expandTemplate(String copyright, String licenseBody, { required String origin }) { |
| throw 'attempted to use $origin which is a disallowed license type ($type)'; |
| } |
| |
| @override |
| String toStringBody(LicenseSource source) { |
| throw '${source.libraryName} attempted to use $origin which is a disallowed license type ($type)'; |
| } |
| } |