// Copyright (c) 2019, 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:math" as math; import "package:kernel/binary/tag.dart" show numberOfFixedFields; class BinaryMdDillReader { final String _binaryMdContent; /// The actual binary content. final List _dillContent; String _currentlyUnparsed = ""; late Map> readingInstructions; late Map> _generics; late Set _abstractTypes; late Map tagToName; late Map constantTagToName; int? version; late Map _extends; late int _binaryMdNestingDepth; late String _binaryMdCurrentClass; /// The offset in the binary where we're supposed to read next. late int _binaryOffset; late int _depth; late Map _dillStringsPointer; int verboseLevel = 0; bool _ranSetup = false; List readingStack = []; BinaryMdDillReader(this._binaryMdContent, this._dillContent); void setup() { if (!_ranSetup) { _setupFields(); _readBinaryMd(); _ranSetup = true; } } Map attemptRead() { setup(); return _readDill(); } /// Initialize the bare essentials, e.g. that a double is 8 bytes. void _setupFields() { readingInstructions = { "Byte": ["byte"], "UInt32": ["byte", "byte", "byte", "byte"], "Double": ["byte", "byte", "byte", "byte", "byte", "byte", "byte", "byte"] }; _generics = {}; _abstractTypes = {}; tagToName = {}; constantTagToName = {}; _extends = {}; _binaryMdNestingDepth = 0; _binaryMdCurrentClass = ""; } /// Read the binary.md text and put the data into the various tables. void _readBinaryMd() { List lines = _binaryMdContent.split("\n"); bool inComment = false; for (String s in lines) { if (s.trim().startsWith("//") || s.trim() == "") { continue; } else if (s.trim().startsWith("/*")) { inComment = true; continue; } else if (s.trim().startsWith("*/")) { inComment = false; continue; } else if (inComment) { continue; } else if (s.trim().startsWith("type ") || s.trim().startsWith("abstract type ") || s.trim().startsWith("enum ")) { _binaryMdHandlePossibleClassStart(s); } else if (s.trim() == "if name begins with '_' {" && _binaryMdCurrentClass == "Name") { // Special-case if sentence in Name. _binaryMdNestingDepth++; } else if (s.trim().endsWith("{")) { throw "Unhandled case: $s"; } else if (s.trim() == "}") { _binaryMdNestingDepth--; _binaryMdCurrentClass = ""; } else if (_binaryMdNestingDepth > 0 && _binaryMdCurrentClass != "") { _binaryMdHandleContent(s); } } _binaryMdCheckHasAllTypes(); if (verboseLevel > 0) { print("Seems to find all types."); } } late int numLibs; late int binaryOffsetForSourceTable; late int binaryOffsetForConstantTable; late int binaryOffsetForConstantTableIndex; late int binaryOffsetForCanonicalNames; late int binaryOffsetForMetadataPayloads; late int binaryOffsetForMetadataMappings; late int binaryOffsetForStringTable; late int binaryOffsetForStartOfComponentIndex; late int mainMethodReference; /// Read the dill file data, parsing it into a Map. Map _readDill() { _binaryOffset = 0; _depth = 0; // Hack start: Read ComponentIndex first. _binaryOffset = _dillContent.length - (4 * 2); numLibs = _peekUint32(); // Skip to the start of the index. _binaryOffset = _dillContent.length - numberOfFixedFields(numLibs) * 4; // Read index. binaryOffsetForSourceTable = _peekUint32(); _binaryOffset += 4; binaryOffsetForConstantTable = _peekUint32(); _binaryOffset += 4; binaryOffsetForConstantTableIndex = _peekUint32(); _binaryOffset += 4; binaryOffsetForCanonicalNames = _peekUint32(); _binaryOffset += 4; binaryOffsetForMetadataPayloads = _peekUint32(); _binaryOffset += 4; binaryOffsetForMetadataMappings = _peekUint32(); _binaryOffset += 4; binaryOffsetForStringTable = _peekUint32(); _binaryOffset += 4; binaryOffsetForStartOfComponentIndex = _peekUint32(); _binaryOffset += 4; mainMethodReference = _peekUint32(); _binaryOffset += 4; _binaryOffset = binaryOffsetForStringTable; var saved = readingInstructions["ComponentFile"]!; readingInstructions["ComponentFile"] = ["StringTable strings;"]; _readBinary("ComponentFile", ""); readingInstructions["ComponentFile"] = saved; _binaryOffset = 0; _depth = 0; // Hack end. Map componentFile = _readBinary("ComponentFile", ""); if (_binaryOffset != _dillContent.length) { throw "Didn't read the entire binary: " "Only read $_binaryOffset of ${_dillContent.length} bytes."; } if (verboseLevel > 0) { print("Successfully read the dill file."); } return componentFile; } /// Initial setup of a "class definition" in the binary.md file. /// This includes parsing the name, setting up any "extends"-relationship, /// generics etc. void _binaryMdHandlePossibleClassStart(String s) { if (s.startsWith("type Byte =")) return; if (s.startsWith("type UInt32 =")) return; if (_binaryMdNestingDepth != 0 || _binaryMdCurrentClass != "") { throw "Cannot handle nesting: " "'$s', $_binaryMdNestingDepth, $_binaryMdCurrentClass"; } if (s.contains("{")) _binaryMdNestingDepth++; if (s.contains("}")) _binaryMdNestingDepth--; String name = s.trim(); bool isAbstract = name.startsWith("abstract "); if (isAbstract) { name = name.substring("abstract ".length); } if (name.startsWith("type ")) name = name.substring("type ".length); bool isEnum = false; if (name.startsWith("enum ")) { name = name.substring("enum ".length); isEnum = true; } String? nameExtends = null; Match? extendsMatch = (new RegExp("extends (.+)[ \{]")).firstMatch(name); if (extendsMatch != null) { nameExtends = extendsMatch.group(1); } name = _getType(name); if (name.contains("<")) { List types = _getGenerics(name); name = name.substring(0, name.indexOf("<")) + "<${types.length}>"; _generics[name] ??= types; } if (_binaryMdNestingDepth != 0) _binaryMdCurrentClass = name; if (nameExtends != null) { _extends[name] = nameExtends.trim(); } if (isEnum) { readingInstructions[name] ??= ["byte"]; } else { readingInstructions[name] ??= []; } if (isAbstract) { _abstractTypes.add(name); } } Map _typeCache = {}; /// Extract the type/name of an input string, e.g. turns /// /// * "ClassLevel { Type = 0, [...], }" into "ClassLevel" /// * "Class extends Node {" into "Class" /// * "Byte tag = 97;" into "Byte" /// * "List {" into "List" String _getType(final String inputString) { String? cached = _typeCache[inputString]; if (cached != null) return cached; int end = math.max( math.max(inputString.indexOf(" "), inputString.lastIndexOf(">") + 1), inputString.lastIndexOf("]") + 1); if (end <= 0) end = inputString.length; String result = inputString.substring(0, end); if (result.contains(" extends")) { result = result.substring(0, result.indexOf(" extends ")); } _typeCache[inputString] = result; return result; } static const int $COMMA = 44; static const int $LT = 60; static const int $GT = 62; /// Extract the generics used in an input type, e.g. turns /// /// * "Pair" into ["A", "B"]. /// * "List" into ["Expression"]. /// * "Foo>" into ["Bar"]. /// * "Foo, E>" into ["A", "B", "E"]. /// /// Note that the input string *has* to use generics, i.e. have '<' and '>' /// in it. /// /// DartDocTest( /// BinaryMdDillReader._getGenerics("Pair"), ["A", "B"] /// ) /// DartDocTest( /// BinaryMdDillReader._getGenerics("List"), ["Expression"] /// ) /// DartDocTest( /// BinaryMdDillReader._getGenerics("List>"), /// ["Pair"] /// ) /// DartDocTest( /// BinaryMdDillReader._getGenerics("RList>"), /// ["Pair"] /// ) /// DartDocTest( /// BinaryMdDillReader._getGenerics( /// "List>"), /// ["Pair"] /// ) /// DartDocTest( /// BinaryMdDillReader._getGenerics( /// "List>"), /// ["Pair"] /// ) /// DartDocTest( /// BinaryMdDillReader._getGenerics( /// "List>"), /// ["Pair"] /// ) /// DartDocTest( /// BinaryMdDillReader._getGenerics("Option>"), /// ["List"] /// ) /// DartDocTest( /// BinaryMdDillReader._getGenerics("Foo>"), ["Bar"] /// ) /// DartDocTest( /// BinaryMdDillReader._getGenerics("Foo, E>"), /// ["A", "B", "E"] /// ) /// DartDocTest( /// BinaryMdDillReader._getGenerics("Foo>>>, H>"), /// ["A", "B>>>", "H"] /// ) /// /// Expected failing run (expects to fail with unbalanced < >). /// TODO: Support this more elegantly. /// DartDocTest(() { /// try { /// BinaryMdDillReader._getGenerics("Foo"); /// return false; /// } catch(e) { /// return true; /// } /// }(), /// true /// ) /// static List _getGenerics(String s) { s = s.substring(s.indexOf("<") + 1, s.lastIndexOf(">")); // Check that any '<' and '>' are balanced and split entries on comma for // the outermost parameters. int ltCount = 0; int gtCount = 0; int depth = 0; int lastPos = 0; List codeUnits = s.codeUnits; List result = []; for (int i = 0; i < codeUnits.length; i++) { int codeUnit = codeUnits[i]; if (codeUnit == $LT) { ltCount++; depth++; } else if (codeUnit == $GT) { gtCount++; depth--; } else if (codeUnit == $COMMA && depth == 0) { result.add(s.substring(lastPos, i).trim()); lastPos = i + 1; } } if (ltCount != gtCount) { throw "Unbalanced '<' and '>': $s"; } assert(depth == 0); result.add(s.substring(lastPos, codeUnits.length).trim()); return result; } /// Parses a line of binary.md content for a "current class" into the /// reading-instructions for that class. /// There is special handling around tags, and around lines that are split, /// i.e. not yet finished (not ending in a semi-colon). void _binaryMdHandleContent(String s) { if (s.trim().startsWith("UInt32 formatVersion = ")) { String versionString = s.trim().substring("UInt32 formatVersion = ".length); if (versionString.endsWith(";")) { versionString = versionString.substring(0, versionString.length - 1); } if (version != null) { throw "Already have a version set ($version), " "now trying to set $versionString"; } version = int.parse(versionString); } if (s.trim().startsWith("Byte tag = ")) { String tag = s.trim().substring("Byte tag = ".length); if (tag.endsWith(";")) tag = tag.substring(0, tag.length - 1); if (tag == "224 + N; // Where 0 <= N < 8.") { for (int n = 0; n < 8; ++n) { tagToName[224 + n] = _binaryMdCurrentClass; } } else if (tag == "232 + N; // Where 0 <= N < 8.") { for (int n = 0; n < 8; ++n) { tagToName[232 + n] = _binaryMdCurrentClass; } } else if (tag == "240 + N; // Where 0 <= N < 8.") { for (int n = 0; n < 8; ++n) { tagToName[240 + n] = _binaryMdCurrentClass; } } else { if (tag.contains("; // Note: tag is out of order")) { tag = tag.substring(0, tag.indexOf("; // Note: tag is out of order")); } Map tagMap; if (isA(_binaryMdCurrentClass, "Constant")) { tagMap = constantTagToName; } else { tagMap = tagToName; } if (tagMap[int.parse(tag)] != null) { throw "Two tags with same name!: " "$tag (${tagMap[int.parse(tag)]} and ${_binaryMdCurrentClass})"; } tagMap[int.parse(tag)] = _binaryMdCurrentClass; } } { var line = _currentlyUnparsed + s.trim(); if (line.contains("//")) line = line.substring(0, line.indexOf("//")); if (!line.trim().endsWith(";")) { _currentlyUnparsed = line; return; } s = line; _currentlyUnparsed = ""; } readingInstructions[_binaryMdCurrentClass]!.add(s.trim()); } /// Check the all types referenced by reading instructions are types we know /// about. void _binaryMdCheckHasAllTypes() { for (String key in readingInstructions.keys) { for (String s in readingInstructions[key]!) { String type = _getType(s); if (!_isKnownType(type, key)) { throw "Unknown type: $type (used in $key)"; } } } } /// Check that we know about the specific type, i.e. know something about how /// to read it. bool _isKnownType(String type, String parent) { if (type == "byte") return true; if (readingInstructions[type] != null) return true; if (type.contains("[") && readingInstructions[type.substring(0, type.indexOf("["))] != null) { return true; } if (parent.contains("<")) { Set types = _generics[parent]!.toSet(); if (types.contains(type)) return true; if (type.contains("[") && types.contains(type.substring(0, type.indexOf("[")))) { return true; } } if (type.contains("<")) { List types = _getGenerics(type); String renamedType = type.substring(0, type.indexOf("<")) + "<${types.length}>"; if (readingInstructions[renamedType] != null) { bool ok = true; for (String type in types) { if (!_isKnownType(type, renamedType)) { ok = false; break; } } if (ok) return true; } } return false; } /// Get a string from the string table after the string table has been read /// from the dill file. String getDillString(int num) { List endOffsets = (_dillStringsPointer["endOffsets"]["items"] as List).cast(); List utf8 = (_dillStringsPointer["utf8Bytes"] as List).cast(); return new String.fromCharCodes( utf8.sublist(num == 0 ? 0 : endOffsets[num - 1], endOffsets[num])); } RegExp regExpSplit = new RegExp(r"[\. ]"); /// Actually read the binary dill file. Read type [what] at the current /// binary position as specified by field [_binaryOffset]. dynamic _readBinary(String what, String callerInstruction) { ++_depth; what = _remapWhat(what); // Read any 'base types'. if (what == "UInt") { return _readUint(); } if (what == "UInt32") { return _readUint32(); } if (what == "byte" || what == "Byte") { int value = _dillContent[_binaryOffset]; ++_binaryOffset; --_depth; return value; } // Not a 'base type'. Read according to [readingInstructions] field. List types = []; List typeNames = []; String orgWhat = what; int orgPosition = _binaryOffset; if (what.contains("<")) { types = _getGenerics(what); what = what.substring(0, what.indexOf("<")) + "<${types.length}>"; typeNames = _generics[what]!; } if (readingInstructions[what] == null) { throw "Didn't find instructions for '$what'"; } readingStack.add(orgWhat); readingStack.add(callerInstruction); Map vars = {}; if (verboseLevel > 1) { print("".padLeft(_depth * 2) + " -> $what ($orgWhat @ $orgPosition)"); } for (String instruction in readingInstructions[what]!) { // Special-case a few things that aren't (easily) described in the // binary.md file. if (what == "Name" && instruction == "LibraryReference library;") { // Special-case if sentence in Name. String name = getDillString(vars["name"]["index"]); if (!name.startsWith("_")) continue; } else if (what == "ComponentFile" && instruction == "MetadataPayload[] metadataPayloads;") { // Special-case skipping metadata payloads. _binaryOffset = binaryOffsetForMetadataMappings; continue; } else if (what == "ComponentFile" && instruction == "RList metadataMappings;") { // Special-case skipping metadata mappings. _binaryOffset = binaryOffsetForStringTable; continue; } else if (what == "ComponentIndex" && instruction == "Byte[] 8bitAlignment;") { // Special-case 8-byte alignment. int sizeWithoutPadding = _binaryOffset + numberOfFixedFields(numLibs) * 4; int padding = 8 - sizeWithoutPadding % 8; if (padding == 8) padding = 0; _binaryOffset += padding; continue; } String type = _getType(instruction); String name = instruction.substring(type.length).trim(); if (name.contains("//")) { name = name.substring(0, name.indexOf("//")).trim(); } if (name.contains("=")) { name = name.substring(0, name.indexOf("=")).trim(); } if (name.endsWith(";")) name = name.substring(0, name.length - 1); int oldOffset = _binaryOffset; if (verboseLevel > 1) { print("".padLeft(_depth * 2 + 1) + " -> $instruction ($type) (@ $_binaryOffset) " "($orgWhat @ $orgPosition)"); } bool readNothingIsOk = false; if (type.contains("[")) { // The type is an array. Read into a List. // Note that we need to know the length of that list. String count = type.substring(type.indexOf("[") + 1, type.indexOf("]")); type = type.substring(0, type.indexOf("[")); type = _lookupGenericType(typeNames, type, types); int intCount = int.tryParse(count) ?? -1; if (intCount == -1) { if (vars[count] != null && vars[count] is int) { intCount = vars[count]; } else if (count.contains(".")) { List countData = count.split(regExpSplit).map((s) => s.trim()).toList(); if (vars[countData[0]] != null) { dynamic v = vars[countData[0]]; if (v is Map && countData[1] == "last" && v["items"] is List && v["items"].last is int) { intCount = v["items"].last; } else if (v is Map && v[countData[1]] != null) { v = v[countData[1]]; if (v is Map && v[countData[2]] != null) { v = v[countData[2]]; if (v is int) intCount = v; } else if (v is int && countData.length == 4 && countData[2] == "+") { intCount = v + int.parse(countData[3]); } } else { throw "Unknown dot to int ($count)"; } } } } // Special-case that we know how many libraries we have. if (intCount < 0 && type == "Library" && _depth == 1) { intCount = numLibs; } if (intCount < 0 && type == "UInt32" && _depth == 2 && count == "libraryCount + 1") { intCount = numLibs + 1; } if (intCount < 0 && type == "UInt32" && _depth == 2 && count == "length" && readingStack.last == "RList constantsMapping;") { int prevBinaryOffset = _binaryOffset; // Hack: Use the ComponentIndex to go to the end and read the length. _binaryOffset = binaryOffsetForCanonicalNames - 4; intCount = _peekUint32(); _binaryOffset = prevBinaryOffset; } if (intCount >= 0) { readNothingIsOk = intCount == 0; List value = new List.filled(intCount, null); for (int i = 0; i < intCount; ++i) { int oldOffset2 = _binaryOffset; value[i] = _readBinary(type, instruction); if (_binaryOffset <= oldOffset2) { throw "Didn't read anything for $type @ $_binaryOffset"; } } vars[name] = value; } else { throw "Array of unknown size ($instruction, $type, $count)"; } } else { // Not an array, read the single field recursively. type = _lookupGenericType(typeNames, type, types); dynamic value = _readBinary(type, instruction); vars[name] = value; _checkTag(instruction, value); } if (_binaryOffset <= oldOffset && !readNothingIsOk) { throw "Didn't read anything for $type @ $_binaryOffset"; } // Special case that when we read the string table we need to remember it // to be able to lookup strings to read names properly later // (private names has a library, public names does not). if (what == "ComponentFile") { if (name == "strings") _dillStringsPointer = vars[name]; } } --_depth; readingStack.removeLast(); readingStack.removeLast(); return vars; } /// Verify, that if the instruction was a tag with a value /// (e.g. "Byte tag = 5;"), then the value read was indeed the expected value /// (5 in this example). void _checkTag(String instruction, dynamic value) { if (instruction.trim().startsWith("Byte tag = ")) { String tag = instruction.trim().substring("Byte tag = ".length); if (tag.contains("//")) { tag = tag.substring(0, tag.indexOf("//")).trim(); } if (tag.endsWith(";")) tag = tag.substring(0, tag.length - 1).trim(); int tagAsInt = int.tryParse(tag) ?? -1; if (tagAsInt >= 0) { if (tagAsInt != value) { throw "Unexpected tag. " "Expected $tagAsInt but got $value (around $_binaryOffset)."; } } } } /// Looks up any generics used, replacing the generic-name (if any) with the /// actual type, e.g. /// * ([], "UInt", []) into "UInt" /// * (["T"], "T", ["Expression"]) into "Expression" /// * (["T0", "T1"], "T0", ["FileOffset", "Expression"]) into "FileOffset" String _lookupGenericType( List typeNames, String type, List types) { for (int i = 0; i < typeNames.length; ++i) { if (typeNames[i] == type) { type = types[i]; break; } } return type; } bool isAbstract(String clazz) => _abstractTypes.contains(clazz); /// Check if [what] is an [a], i.e. if [what] extends [a]. /// This method uses the [_extends] map and it is thus risky to use it before /// the binary.md file has been read in entirety (because the field isn't /// completely filled out yet). bool isA(String what, String a) { String? parent = what; while (parent != null) { if (parent == a) return true; parent = _extends[parent]; } return false; } /// Remaps the type by looking at tags, e.g. if asked to read an "Expression" /// and the tag actually says "Block", return "Block" after checking that a /// "Block" is actually an "Expression". String _remapWhat(String what) { Map tagMap; if (isA(what, "Constant")) { tagMap = constantTagToName; } else { tagMap = tagToName; } if (what == "Expression") { if (tagMap[_dillContent[_binaryOffset]] != null) { what = tagMap[_dillContent[_binaryOffset]]!; if (!isA(what, "Expression")) { throw "Expected Expression but found $what"; } } else { throw "Unknown expression"; } } if (what == "IntegerLiteral") { if (tagMap[_dillContent[_binaryOffset]] != null) { what = tagMap[_dillContent[_binaryOffset]]!; if (!isA(what, "IntegerLiteral")) { throw "Expected IntegerLiteral but found $what"; } } else { throw "Unknown IntegerLiteral"; } } if (what == "Statement") { if (tagMap[_dillContent[_binaryOffset]] != null) { what = tagMap[_dillContent[_binaryOffset]]!; if (!isA(what, "Statement")) { throw "Expected Statement but found $what"; } } else { throw "Unknown Statement"; } } if (what == "Initializer") { if (tagMap[_dillContent[_binaryOffset]] != null) { what = tagMap[_dillContent[_binaryOffset]]!; if (!isA(what, "Initializer")) { throw "Expected Initializer but found $what"; } } else { throw "Unknown Initializer"; } } if (what == "DartType") { if (tagMap[_dillContent[_binaryOffset]] != null) { what = tagMap[_dillContent[_binaryOffset]]!; if (!isA(what, "DartType")) { throw "Expected DartType but found $what"; } } else { throw "Unknown DartType at $_binaryOffset " "(${_dillContent[_binaryOffset]})"; } } if (what.startsWith("Option<")) { if (tagMap[_dillContent[_binaryOffset]] != null && tagMap[_dillContent[_binaryOffset]]!.startsWith("Something<")) { what = what.replaceFirst("Option<", "Something<"); } } if (what == "Constant") { if (tagMap[_dillContent[_binaryOffset]] != null) { what = tagMap[_dillContent[_binaryOffset]]!; if (!isA(what, "Constant")) { throw "Expected Constant but found $what"; } } else { throw "Unknown Constant"; } } return what; } /// Read the "UInt" type as used in kernel. This is hard-coded. /// Note that this decrements the [_depth] and increments the /// [_binaryOffset] correctly. int _readUint() { int b = _dillContent[_binaryOffset]; if (b & 128 == 0) { ++_binaryOffset; --_depth; return b; } if (b & 192 == 128) { int value = (_dillContent[_binaryOffset] & 63) << 8 | _dillContent[_binaryOffset + 1]; _binaryOffset += 2; --_depth; return value; } if (b & 192 == 192) { int value = (_dillContent[_binaryOffset] & 63) << 24 | _dillContent[_binaryOffset + 1] << 16 | _dillContent[_binaryOffset + 2] << 8 | _dillContent[_binaryOffset + 3]; _binaryOffset += 4; --_depth; return value; } throw "Unexpected UInt"; } /// Read the "UInt43" type as used in kernel. This is hard-coded. /// Note that this decrements the [_depth] and increments the /// [_binaryOffset] correctly. int _readUint32() { int value = (_dillContent[_binaryOffset] & 63) << 24 | _dillContent[_binaryOffset + 1] << 16 | _dillContent[_binaryOffset + 2] << 8 | _dillContent[_binaryOffset + 3]; _binaryOffset += 4; --_depth; return value; } /// Read the "UInt32" type as used in kernel. This is hard-coded. /// This does not change any state. int _peekUint32() { return (_dillContent[_binaryOffset] & 63) << 24 | _dillContent[_binaryOffset + 1] << 16 | _dillContent[_binaryOffset + 2] << 8 | _dillContent[_binaryOffset + 3]; } } class DillComparer { Map? tagToName; StringBuffer? outputTo; bool compare(List a, List b, String binaryMd, [StringBuffer? outputTo]) { this.outputTo = outputTo; bool printOnExit = false; if (this.outputTo == null) { this.outputTo = new StringBuffer(); printOnExit = true; } BinaryMdDillReader readerA = new BinaryMdDillReader(binaryMd, a); dynamic aResult = readerA.attemptRead(); tagToName = readerA.tagToName; BinaryMdDillReader readerB = new BinaryMdDillReader(binaryMd, b); dynamic bResult = readerB.attemptRead(); bool result = _compareInternal(aResult, bResult); if (printOnExit) print(outputTo); return result; } List stack = []; int outputLines = 0; void printDifference(String s) { outputTo!.writeln("----------"); outputTo!.writeln(s); outputTo!.writeln("'Stacktrace':"); stack.forEach(outputTo!.writeln); outputLines += 3 + stack.length; } bool _compareInternal(dynamic a, dynamic b) { if (a.runtimeType != b.runtimeType) { printDifference( "Different runtime types (${a.runtimeType} and ${b.runtimeType})"); return false; } bool result = true; if (a is List) { List listA = a; List listB = b; int length = listA.length; if (listA.length != listB.length) { printDifference( "Lists have different length (${listA.length} vs ${listB.length})"); result = false; if (listB.length < listA.length) length = listB.length; } for (int i = 0; i < length; i++) { stack.add("Lists at index $i ${_getTag(a)}"); if (!_compareInternal(listA[i], listB[i])) { result = false; } stack.removeLast(); if (outputLines > 1000) return result; } return result; } if (a is Map) { Map mapA = a; Map mapB = b; for (String key in mapA.keys) { dynamic valueA = mapA[key]; dynamic valueB = mapB[key]; stack.add("Map with key '$key' ${_getTag(a)}"); if (!_compareInternal(valueA, valueB)) { result = false; } stack.removeLast(); if (outputLines > 1000) return result; } if (mapA.length != mapB.length) { printDifference("Maps have different number of entries " "(${mapA.length} vs ${mapB.length}). ${_getTag(a)}"); result = false; } return result; } if (a is int) { if (a != b) { printDifference("Integers differ: $a vs $b"); return false; } return true; } throw "Unsupported: ${a.runtimeType}"; } String _getTag(dynamic input) { if (input is Map) { dynamic tag = input["tag"]; if (tag != null) { if (tagToName![tag] != null) { return "(tag $tag, likely '${tagToName![tag]}')"; } return "(tag $tag)"; } } return ""; } }