| // Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file | |
| // for details. 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:typed_data'; | |
| import 'package:analyzer/dart/analysis/features.dart'; | |
| import 'package:analyzer/src/binary/binary_reader.dart'; | |
| import 'package:analyzer/src/binary/binary_writer.dart'; | |
| import 'package:analyzer/src/dart/analysis/experiments.dart'; | |
| import 'package:collection/collection.dart'; | |
| import 'package:convert/convert.dart'; | |
| import 'package:crypto/crypto.dart'; | |
| import 'package:pub_semver/pub_semver.dart'; | |
| /// An instance of [ApiSignature] collects data in the form of primitive types | |
| /// (strings, ints, bools, etc.) from a summary "builder" object, and uses them | |
| /// to generate an MD5 signature of a the non-informative parts of the summary | |
| /// (i.e. those parts representing the API of the code being summarized). | |
| /// | |
| /// Note that the data passed to the MD5 signature algorithm is untyped. So, | |
| /// for instance, an API signature built from a sequence of `false` booleans is | |
| /// likely to match an API signature built from a sequence of zeros. The caller | |
| /// should take this into account; e.g. if a data structure may be represented | |
| /// either by a boolean or an int, the caller should encode a tag distinguishing | |
| /// the two representations before encoding the data. | |
| class ApiSignature { | |
| /// Version number of the code in this class. Any time this class is changed | |
| /// in a way that affects the data collected in [_data], this version number | |
| /// should be incremented, so that a summary signed by a newer version of the | |
| /// signature algorithm won't accidentally have the same signature as a | |
| /// summary signed by an older version. | |
| static const int _version = 0; | |
| /// Data accumulated so far. | |
| ByteData _data = ByteData(4096); | |
| /// Offset into [_data] where the next byte should be written. | |
| int _offset = 0; | |
| /// Create an [ApiSignature] which is ready to accept data. | |
| ApiSignature() { | |
| addInt(_version); | |
| } | |
| /// For testing only: create an [ApiSignature] which doesn't include any | |
| /// version information. This makes it easier to unit tests, since the data | |
| /// is stable even if [_version] is changed. | |
| ApiSignature.unversioned(); | |
| /// Collect a boolean value. | |
| void addBool(bool b) { | |
| _makeRoom(1); | |
| _data.setUint8(_offset, b ? 1 : 0); | |
| _offset++; | |
| } | |
| /// Collect a sequence of arbitrary bytes. Note that the length is not | |
| /// collected, so for example `addBytes([1, 2]);` will have the same effect as | |
| /// `addBytes([1]); addBytes([2]);`. | |
| void addBytes(List<int> bytes) { | |
| int length = bytes.length; | |
| _makeRoom(length); | |
| for (int i = 0; i < length; i++) { | |
| _data.setUint8(_offset + i, bytes[i]); | |
| } | |
| _offset += length; | |
| } | |
| /// Collect a double-precision floating point value. | |
| void addDouble(double d) { | |
| _makeRoom(8); | |
| _data.setFloat64(_offset, d, Endian.little); | |
| _offset += 8; | |
| } | |
| /// Collect a [FeatureSet]. | |
| void addFeatureSet(FeatureSet featureSet) { | |
| var knownFeatures = ExperimentStatus.knownFeatures; | |
| addInt(knownFeatures.length); | |
| for (var feature in knownFeatures.values) { | |
| addBool(featureSet.isEnabled(feature)); | |
| } | |
| } | |
| /// Collect a 32-bit unsigned integer value. | |
| void addInt(int i) { | |
| _makeRoom(4); | |
| _data.setUint32(_offset, i, Endian.little); | |
| _offset += 4; | |
| } | |
| /// Collect a language version. | |
| void addLanguageVersion(Version version) { | |
| addInt(version.major); | |
| addInt(version.minor); | |
| } | |
| void addList<T>(List<T> elements, void Function(T) addElement) { | |
| addInt(elements.length); | |
| for (var element in elements) { | |
| addElement(element); | |
| } | |
| } | |
| void addMapEntryList<K, V>( | |
| List<MapEntry<K, V>> entries, | |
| void Function(K, V) addEntry, | |
| ) { | |
| addInt(entries.length); | |
| for (var entry in entries) { | |
| addEntry(entry.key, entry.value); | |
| } | |
| } | |
| /// Collect a string. | |
| void addString(String s) { | |
| bool isAscii = true; | |
| for (int i = 0; i < s.length; i++) { | |
| if (s.codeUnitAt(i) >= 128) { | |
| isAscii = false; | |
| break; | |
| } | |
| } | |
| if (isAscii) { | |
| int length = s.length; | |
| _makeRoom(length + 4); | |
| _data.setUint32(_offset, length, Endian.little); | |
| _offset += 4; | |
| // Calling addBytes is slow because addBytes will have to be more general. | |
| for (int i = 0; i < length; i++) { | |
| _data.setUint8(_offset + i, s.codeUnitAt(i)); | |
| } | |
| _offset += length; | |
| } else { | |
| var bytes = const Utf8Encoder().convert(s); | |
| addInt(bytes.length); | |
| addBytes(bytes); | |
| } | |
| } | |
| /// Collect a string list, with the length. | |
| void addStringList(List<String> values) { | |
| addInt(values.length); | |
| for (var value in values) { | |
| addString(value); | |
| } | |
| } | |
| /// Collect the given [Uint32List]. | |
| void addUint32List(Uint32List data) { | |
| addBytes(data.buffer.asUint8List()); | |
| } | |
| /// For testing only: retrieve the internal representation of the data that | |
| /// has been collected. | |
| Uint8List getBytes_forDebug() { | |
| return Uint8List.view(_data.buffer, 0, _offset); | |
| } | |
| /// Return the bytes of the MD5 hash of the data collected so far. | |
| Uint8List toByteList() { | |
| var data = _data.buffer.asUint8List(0, _offset); | |
| var bytes = md5.convert(data).bytes; | |
| return bytes is Uint8List ? bytes : Uint8List.fromList(bytes); | |
| } | |
| /// Returns the MD5 hash of the data collected so far. | |
| Hash toHash() { | |
| var bytes = toByteList(); | |
| return Hash._(bytes); | |
| } | |
| /// Return a hex-encoded MD5 signature of the data collected so far. | |
| String toHex() { | |
| return hex.encode(toByteList()); | |
| } | |
| /// Return the MD5 hash of the data collected so far as [Uint32List]. | |
| Uint32List toUint32List() { | |
| var hash = toHash(); | |
| return hash._bytes.buffer.asUint32List(); | |
| } | |
| /// Ensure that [spaceNeeded] bytes can be added to [_data] at [_offset] | |
| /// (copying it to a larger object if necessary). | |
| void _makeRoom(int spaceNeeded) { | |
| int oldLength = _data.lengthInBytes; | |
| if (_offset + spaceNeeded > oldLength) { | |
| int newLength = 2 * (_offset + spaceNeeded); | |
| ByteData newData = ByteData(newLength); | |
| Uint8List.view( | |
| newData.buffer, | |
| ).setRange(0, oldLength, Uint8List.view(_data.buffer)); | |
| _data = newData; | |
| } | |
| } | |
| } | |
| /// Hash value computed by [ApiSignature]. | |
| class Hash { | |
| static final Hash empty = Hash._(Uint8List(0)); | |
| final Uint8List _bytes; | |
| late final int _hashCode = const ListEquality<int>().hash(_bytes); | |
| factory Hash.read(BinaryReader reader) { | |
| return Hash._(reader.readUint8List()); | |
| } | |
| Hash._(this._bytes); | |
| @override | |
| int get hashCode => _hashCode; | |
| @override | |
| bool operator ==(Object other) { | |
| return other is Hash && | |
| const ListEquality<int>().equals(_bytes, other._bytes); | |
| } | |
| @override | |
| String toString() { | |
| return hex.encode(_bytes); | |
| } | |
| void write(BinaryWriter writer) { | |
| writer.writeUint8List(_bytes); | |
| } | |
| } |