Add binary serialization
diff --git a/lib/binary_codec.dart b/lib/binary_codec.dart
new file mode 100644
index 0000000..b739114
--- /dev/null
+++ b/lib/binary_codec.dart
@@ -0,0 +1,458 @@
+// Copyright (c) 2015, 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.
+
+/// Info serialization to a binary form.
+///
+/// Unlike the JSON codec, this serialization is designed to be streamed.
+
+import 'dart:convert';
+
+import 'src/binary/sink.dart';
+import 'src/binary/source.dart';
+import 'info.dart';
+
+void encode(AllInfo info, Sink<List<int>> sink) {
+  new BinaryPrinter(new BinarySink(sink)).visitAll(info);
+}
+
+AllInfo decode(List<int> data) {
+  return new BinaryReader(new BinarySource(data)).readAll();
+}
+
+class BinaryPrinter implements InfoVisitor<void> {
+  final BinarySink sink;
+
+  BinaryPrinter(this.sink);
+
+  void writeDate(DateTime date) {
+    sink.writeString(date.toIso8601String());
+  }
+
+  void writeDuration(Duration duration) {
+    sink.writeInt(duration.inMicroseconds);
+  }
+
+  void writeInfoWithKind(Info info) {
+    sink.writeEnum(info.kind);
+    info.accept(this);
+  }
+
+  void visitAll(AllInfo info) {
+    sink.writeInt(info.version);
+    sink.writeInt(info.minorVersion);
+    sink.writeList(info.libraries, visitLibrary);
+    // TODO(sigmund): synthesize the following lists instead of serializing the
+    // values again.
+    sink.writeList(info.classes, visitClass);
+    sink.writeList(info.functions, visitFunction);
+    sink.writeList(info.typedefs, visitTypedef);
+    sink.writeList(info.fields, visitField);
+    sink.writeList(info.constants, visitConstant);
+    sink.writeList(info.closures, visitClosure);
+
+    void writeDependencies(CodeInfo info) {
+      sink.writeList(info.uses, _writeDependencyInfo);
+    }
+
+    info.fields.forEach(writeDependencies);
+    info.functions.forEach(writeDependencies);
+
+    sink.writeInt(info.dependencies.length);
+    info.dependencies.forEach((Info key, List<Info> values) {
+      writeInfoWithKind(key);
+      sink.writeList(values, writeInfoWithKind);
+    });
+    sink.writeList(info.outputUnits, visitOutput);
+    sink.writeString(jsonEncode(info.deferredFiles));
+    visitProgram(info.program);
+    sink.close();
+  }
+
+  void visitProgram(ProgramInfo info) {
+    visitFunction(info.entrypoint);
+    sink.writeInt(info.size);
+    sink.writeString(info.dart2jsVersion);
+    writeDate(info.compilationMoment);
+    writeDuration(info.compilationDuration);
+    // Note: we don't record the 'toJsonDuration' field. Consider deleting it?
+    writeDuration(info.dumpInfoDuration);
+    sink.writeBool(info.noSuchMethodEnabled);
+    sink.writeBool(info.isRuntimeTypeUsed);
+    sink.writeBool(info.isIsolateInUse);
+    sink.writeBool(info.isFunctionApplyUsed);
+    sink.writeBool(info.isMirrorsUsed);
+    sink.writeBool(info.minified);
+  }
+
+  void _visitBasicInfo(BasicInfo info) {
+    sink.writeStringOrNull(info.name);
+    sink.writeInt(info.size);
+    sink.writeStringOrNull(info.coverageId);
+    _writeOutputOrNull(info.outputUnit);
+    // Note: parent-pointers are not serialized, they get deduced during deserialization.
+  }
+
+  void visitLibrary(LibraryInfo library) {
+    sink.writeCached(library, (LibraryInfo info) {
+      sink.writeUri(info.uri);
+      _visitBasicInfo(info);
+      sink.writeList(info.topLevelFunctions, visitFunction);
+      sink.writeList(info.topLevelVariables, visitField);
+      sink.writeList(info.classes, visitClass);
+      sink.writeList(info.typedefs, visitTypedef);
+    });
+  }
+
+  void visitClass(ClassInfo cls) {
+    sink.writeCached(cls, (ClassInfo info) {
+      _visitBasicInfo(info);
+      sink.writeBool(info.isAbstract);
+      sink.writeList(info.fields, visitField);
+      sink.writeList(info.functions, visitFunction);
+    });
+  }
+
+  void visitField(FieldInfo field) {
+    sink.writeCached(field, (FieldInfo info) {
+      _visitBasicInfo(info);
+      sink.writeList(info.closures, visitClosure);
+      sink.writeString(info.inferredType);
+      sink.writeList(info.code, _visitCodeSpan);
+      sink.writeString(info.type);
+      sink.writeBool(info.isConst);
+      if (info.isConst) {
+        _writeConstantOrNull(info.initializer);
+      }
+    });
+  }
+
+  _visitCodeSpan(CodeSpan code) {
+    _writeOutputOrNull(code.outputUnit);
+    sink.writeIntOrNull(code.start);
+    sink.writeIntOrNull(code.end);
+    sink.writeStringOrNull(code.text);
+  }
+
+  void _writeConstantOrNull(ConstantInfo info) {
+    sink.writeBool(info != null);
+    if (info != null) {
+      visitConstant(info);
+    }
+  }
+
+  void visitConstant(ConstantInfo constant) {
+    sink.writeCached(constant, (ConstantInfo info) {
+      _visitBasicInfo(info);
+      sink.writeList(info.code, _visitCodeSpan);
+    });
+  }
+
+  void _visitFunctionModifiers(FunctionModifiers mods) {
+    int value = 0;
+    if (mods.isStatic) value |= _staticMask;
+    if (mods.isConst) value |= _constMask;
+    if (mods.isFactory) value |= _factoryMask;
+    if (mods.isExternal) value |= _externalMask;
+    sink.writeInt(value);
+  }
+
+  void _visitParameterInfo(ParameterInfo info) {
+    sink.writeString(info.name);
+    sink.writeString(info.type);
+    sink.writeString(info.declaredType);
+  }
+
+  void visitFunction(FunctionInfo function) {
+    sink.writeCached(function, (FunctionInfo info) {
+      _visitBasicInfo(info);
+      sink.writeList(info.closures, visitClosure);
+      _visitFunctionModifiers(info.modifiers);
+      sink.writeString(info.returnType);
+      sink.writeString(info.inferredReturnType);
+      sink.writeList(info.parameters, _visitParameterInfo);
+      sink.writeString(info.sideEffects);
+      sink.writeIntOrNull(info.inlinedCount);
+      sink.writeList(info.code, _visitCodeSpan);
+      sink.writeString(info.type);
+    });
+  }
+
+  void _writeDependencyInfo(DependencyInfo info) {
+    writeInfoWithKind(info.target);
+    sink.writeStringOrNull(info.mask);
+  }
+
+  void visitClosure(ClosureInfo closure) {
+    sink.writeCached(closure, (ClosureInfo info) {
+      _visitBasicInfo(info);
+      visitFunction(info.function);
+    });
+  }
+
+  void visitTypedef(TypedefInfo typedef) {
+    sink.writeCached(typedef, (TypedefInfo info) {
+      _visitBasicInfo(info);
+      sink.writeString(info.type);
+    });
+  }
+
+  void _writeOutputOrNull(OutputUnitInfo info) {
+    sink.writeBool(info != null);
+    if (info != null) {
+      visitOutput(info);
+    }
+  }
+
+  void visitOutput(OutputUnitInfo output) {
+    sink.writeCached(output, (OutputUnitInfo info) {
+      _visitBasicInfo(info);
+      sink.writeList(info.imports, sink.writeString);
+    });
+  }
+}
+
+class BinaryReader {
+  final BinarySource source;
+  BinaryReader(this.source);
+
+  DateTime readDate() {
+    return DateTime.parse(source.readString());
+  }
+
+  Duration readDuration() {
+    return new Duration(microseconds: source.readInt());
+  }
+
+  Info readInfoWithKind() {
+    InfoKind kind = source.readEnum(InfoKind.values);
+    switch (kind) {
+      case InfoKind.library:
+        return readLibrary();
+      case InfoKind.clazz:
+        return readClass();
+      case InfoKind.function:
+        return readFunction();
+      case InfoKind.field:
+        return readField();
+      case InfoKind.constant:
+        return readConstant();
+      case InfoKind.outputUnit:
+        return readOutput();
+      case InfoKind.typedef:
+        return readTypedef();
+      case InfoKind.closure:
+        return readClosure();
+    }
+    return null;
+  }
+
+  AllInfo readAll() {
+    var info = new AllInfo();
+    int version = source.readInt();
+    int minorVersion = source.readInt();
+    if (info.version != version || info.minorVersion != minorVersion) {
+      print("warning: data was encoded with format version "
+          "$version.$minorVersion, but decoded with "
+          "${info.version}.${info.minorVersion}");
+    }
+    info.libraries = source.readList(readLibrary);
+    info.classes = source.readList(readClass);
+    info.functions = source.readList(readFunction);
+    info.typedefs = source.readList(readTypedef);
+    info.fields = source.readList(readField);
+    info.constants = source.readList(readConstant);
+    info.closures = source.readList(readClosure);
+
+    void readDependencies(CodeInfo info) {
+      info.uses = source.readList(_readDependencyInfo);
+    }
+
+    info.fields.forEach(readDependencies);
+    info.functions.forEach(readDependencies);
+
+    int dependenciesTotal = source.readInt();
+    while (dependenciesTotal > 0) {
+      Info key = readInfoWithKind();
+      List<Info> values = source.readList(readInfoWithKind);
+      info.dependencies[key] = values;
+      dependenciesTotal--;
+    }
+
+    info.outputUnits = source.readList(readOutput);
+
+    Map<String, Map<String, dynamic>> map =
+        jsonDecode(source.readString()).cast<String, Map<String, dynamic>>();
+    for (final library in map.values) {
+      if (library['imports'] != null) {
+        // The importMap needs to be typed as <String, List<String>>, but the
+        // json parser produces <String, dynamic>.
+        final importMap = library['imports'] as Map<String, dynamic>;
+        importMap.forEach((prefix, files) {
+          importMap[prefix] = (files as List<dynamic>).cast<String>();
+        });
+        library['imports'] = importMap.cast<String, List<String>>();
+      }
+    }
+    info.deferredFiles = map;
+    info.program = readProgram();
+    return info;
+  }
+
+  ProgramInfo readProgram() {
+    var info = new ProgramInfo();
+    info.entrypoint = readFunction();
+    info.size = source.readInt();
+    info.dart2jsVersion = source.readString();
+    info.compilationMoment = readDate();
+    info.compilationDuration = readDuration();
+    info.toJsonDuration = new Duration(microseconds: 0);
+    info.dumpInfoDuration = readDuration();
+    info.noSuchMethodEnabled = source.readBool();
+    info.isRuntimeTypeUsed = source.readBool();
+    info.isIsolateInUse = source.readBool();
+    info.isFunctionApplyUsed = source.readBool();
+    info.isMirrorsUsed = source.readBool();
+    info.minified = source.readBool();
+    return info;
+  }
+
+  void _readBasicInfo(BasicInfo info) {
+    info.name = source.readStringOrNull();
+    info.size = source.readInt();
+    info.coverageId = source.readStringOrNull();
+    info.outputUnit = _readOutputOrNull();
+    // Note: parent pointers are added when deserializing parent nodes.
+  }
+
+  LibraryInfo readLibrary() => source.readCached<LibraryInfo>(() {
+        LibraryInfo info = new LibraryInfo.internal();
+        info.uri = source.readUri();
+        _readBasicInfo(info);
+        info.topLevelFunctions = source.readList(readFunction);
+        info.topLevelVariables = source.readList(readField);
+        info.classes = source.readList(readClass);
+        info.typedefs = source.readList(readTypedef);
+
+        setParent(BasicInfo child) => child.parent = info;
+        info.topLevelFunctions.forEach(setParent);
+        info.topLevelVariables.forEach(setParent);
+        info.classes.forEach(setParent);
+        info.typedefs.forEach(setParent);
+        return info;
+      });
+
+  ClassInfo readClass() => source.readCached<ClassInfo>(() {
+        ClassInfo info = new ClassInfo.internal();
+        _readBasicInfo(info);
+        info.isAbstract = source.readBool();
+        info.fields = source.readList(readField);
+        info.functions = source.readList(readFunction);
+
+        setParent(BasicInfo child) => child.parent = info;
+        info.fields.forEach(setParent);
+        info.functions.forEach(setParent);
+        return info;
+      });
+
+  FieldInfo readField() => source.readCached<FieldInfo>(() {
+        FieldInfo info = new FieldInfo.internal();
+        _readBasicInfo(info);
+        info.closures = source.readList(readClosure);
+        info.inferredType = source.readString();
+        info.code = source.readList(_readCodeSpan);
+        info.type = source.readString();
+        info.isConst = source.readBool();
+        if (info.isConst) {
+          info.initializer = _readConstantOrNull();
+        }
+        info.closures.forEach((c) => c.parent = info);
+        return info;
+      });
+
+  CodeSpan _readCodeSpan() {
+    return new CodeSpan()
+      ..outputUnit = _readOutputOrNull()
+      ..start = source.readIntOrNull()
+      ..end = source.readIntOrNull()
+      ..text = source.readStringOrNull();
+  }
+
+  ConstantInfo _readConstantOrNull() {
+    bool hasOutput = source.readBool();
+    if (hasOutput) return readConstant();
+    return null;
+  }
+
+  ConstantInfo readConstant() => source.readCached<ConstantInfo>(() {
+        ConstantInfo info = new ConstantInfo.internal();
+        _readBasicInfo(info);
+        info.code = source.readList(_readCodeSpan);
+        return info;
+      });
+
+  FunctionModifiers _readFunctionModifiers() {
+    int value = source.readInt();
+    return new FunctionModifiers(
+        isStatic: value & _staticMask != 0,
+        isConst: value & _constMask != 0,
+        isFactory: value & _factoryMask != 0,
+        isExternal: value & _externalMask != 0);
+  }
+
+  ParameterInfo _readParameterInfo() {
+    return new ParameterInfo(
+        source.readString(), source.readString(), source.readString());
+  }
+
+  FunctionInfo readFunction() => source.readCached<FunctionInfo>(() {
+        FunctionInfo info = new FunctionInfo.internal();
+        _readBasicInfo(info);
+        info.closures = source.readList(readClosure);
+        info.modifiers = _readFunctionModifiers();
+        info.returnType = source.readString();
+        info.inferredReturnType = source.readString();
+        info.parameters = source.readList(_readParameterInfo);
+        info.sideEffects = source.readString();
+        info.inlinedCount = source.readIntOrNull();
+        info.code = source.readList(_readCodeSpan);
+        info.type = source.readString();
+        info.closures.forEach((c) => c.parent = info);
+        return info;
+      });
+
+  DependencyInfo _readDependencyInfo() =>
+      new DependencyInfo(readInfoWithKind(), source.readStringOrNull());
+
+  ClosureInfo readClosure() => source.readCached<ClosureInfo>(() {
+        ClosureInfo info = new ClosureInfo.internal();
+        _readBasicInfo(info);
+        info.function = readFunction();
+        info.function.parent = info;
+        return info;
+      });
+
+  TypedefInfo readTypedef() => source.readCached<TypedefInfo>(() {
+        TypedefInfo info = new TypedefInfo.internal();
+        _readBasicInfo(info);
+        info.type = source.readString();
+      });
+
+  OutputUnitInfo _readOutputOrNull() {
+    bool hasOutput = source.readBool();
+    if (hasOutput) return readOutput();
+    return null;
+  }
+
+  OutputUnitInfo readOutput() => source.readCached<OutputUnitInfo>(() {
+        OutputUnitInfo info = new OutputUnitInfo.internal();
+        _readBasicInfo(info);
+        info.imports = source.readList(source.readString);
+        return info;
+      });
+}
+
+const int _staticMask = 1 << 3;
+const int _constMask = 1 << 2;
+const int _factoryMask = 1 << 1;
+const int _externalMask = 1 << 0;
diff --git a/lib/src/binary/sink.dart b/lib/src/binary/sink.dart
new file mode 100644
index 0000000..7cacadc
--- /dev/null
+++ b/lib/src/binary/sink.dart
@@ -0,0 +1,401 @@
+// 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:convert';
+import 'dart:typed_data';
+
+/// Interface for serialization.
+// TODO(sigmund): share this with pkg:compiler/src/serialization/*
+abstract class DataSink {
+  /// The amount of data written to this data sink.
+  ///
+  /// The units is based on the underlying data structure for this data sink.
+  int get length;
+
+  /// Flushes any pending data and closes this data sink.
+  ///
+  /// The data sink can no longer be written to after closing.
+  void close();
+
+  /// Writes a reference to [value] to this data sink. If [value] has not yet
+  /// been serialized, [f] is called to serialize the value itself.
+  void writeCached<E>(E value, void f(E value));
+
+  /// Writes the potentially `null` [value] to this data sink. If [value] is
+  /// non-null [f] is called to write the non-null value to the data sink.
+  ///
+  /// This is a convenience method to be used together with
+  /// [DataSource.readValueOrNull].
+  void writeValueOrNull<E>(E value, void f(E value));
+
+  /// Writes the [values] to this data sink calling [f] to write each value to
+  /// the data sink. If [allowNull] is `true`, [values] is allowed to be `null`.
+  ///
+  /// This is a convenience method to be used together with
+  /// [DataSource.readList].
+  void writeList<E>(Iterable<E> values, void f(E value),
+      {bool allowNull: false});
+
+  /// Writes the boolean [value] to this data sink.
+  void writeBool(bool value);
+
+  /// Writes the non-negative integer [value] to this data sink.
+  void writeInt(int value);
+
+  /// Writes the potentially `null` non-negative [value] to this data sink.
+  ///
+  /// This is a convenience method to be used together with
+  /// [DataSource.readIntOrNull].
+  void writeIntOrNull(int value);
+
+  /// Writes the string [value] to this data sink.
+  void writeString(String value);
+
+  /// Writes the potentially `null` string [value] to this data sink.
+  ///
+  /// This is a convenience method to be used together with
+  /// [DataSource.readStringOrNull].
+  void writeStringOrNull(String value);
+
+  /// Writes the string [values] to this data sink. If [allowNull] is `true`,
+  /// [values] is allowed to be `null`.
+  ///
+  /// This is a convenience method to be used together with
+  /// [DataSource.readStrings].
+  void writeStrings(Iterable<String> values, {bool allowNull: false});
+
+  /// Writes the [map] from string to [V] values to this data sink, calling [f]
+  /// to write each value to the data sink. If [allowNull] is `true`, [map] is
+  /// allowed to be `null`.
+  ///
+  /// This is a convenience method to be used together with
+  /// [DataSource.readStringMap].
+  void writeStringMap<V>(Map<String, V> map, void f(V value),
+      {bool allowNull: false});
+
+  /// Writes the enum value [value] to this data sink.
+  // TODO(johnniwinther): Change the signature to
+  // `void writeEnum<E extends Enum<E>>(E value);` when an interface for enums
+  // is added to the language.
+  void writeEnum(dynamic value);
+
+  /// Writes the URI [value] to this data sink.
+  void writeUri(Uri value);
+}
+
+/// Mixin that implements all convenience methods of [DataSink].
+abstract class DataSinkMixin implements DataSink {
+  @override
+  void writeIntOrNull(int value) {
+    writeBool(value != null);
+    if (value != null) {
+      writeInt(value);
+    }
+  }
+
+  @override
+  void writeStringOrNull(String value) {
+    writeBool(value != null);
+    if (value != null) {
+      writeString(value);
+    }
+  }
+
+  @override
+  void writeStrings(Iterable<String> values, {bool allowNull: false}) {
+    if (values == null) {
+      assert(allowNull);
+      writeInt(0);
+    } else {
+      writeInt(values.length);
+      for (String value in values) {
+        writeString(value);
+      }
+    }
+  }
+
+  @override
+  void writeStringMap<V>(Map<String, V> map, void f(V value),
+      {bool allowNull: false}) {
+    if (map == null) {
+      assert(allowNull);
+      writeInt(0);
+    } else {
+      writeInt(map.length);
+      map.forEach((String key, V value) {
+        writeString(key);
+        f(value);
+      });
+    }
+  }
+
+  @override
+  void writeList<E>(Iterable<E> values, void f(E value),
+      {bool allowNull: false}) {
+    if (values == null) {
+      assert(allowNull);
+      writeInt(0);
+    } else {
+      writeInt(values.length);
+      values.forEach(f);
+    }
+  }
+
+  @override
+  void writeValueOrNull<E>(E value, void f(E value)) {
+    writeBool(value != null);
+    if (value != null) {
+      f(value);
+    }
+  }
+}
+
+/// Data sink helper that canonicalizes [E] values using indices.
+class IndexedSink<E> {
+  final void Function(int) _writeInt;
+  final Map<E, int> _cache = {};
+
+  IndexedSink(this._writeInt);
+
+  /// Write a reference to [value] to the data sink.
+  ///
+  /// If [value] has not been canonicalized yet, [writeValue] is called to
+  /// serialize the [value] itself.
+  void write(E value, void writeValue(E value)) {
+    int index = _cache[value];
+    if (index == null) {
+      index = _cache.length;
+      _cache[value] = index;
+      _writeInt(index);
+      writeValue(value);
+    } else {
+      _writeInt(index);
+    }
+  }
+}
+
+/// Base implementation of [DataSink] using [DataSinkMixin] to implement
+/// convenience methods.
+abstract class AbstractDataSink extends DataSinkMixin implements DataSink {
+  IndexedSink<String> _stringIndex;
+  IndexedSink<Uri> _uriIndex;
+  Map<Type, IndexedSink> _generalCaches = {};
+
+  AbstractDataSink() {
+    _stringIndex = new IndexedSink<String>(_writeIntInternal);
+    _uriIndex = new IndexedSink<Uri>(_writeIntInternal);
+  }
+
+  @override
+  void writeCached<E>(E value, void f(E value)) {
+    IndexedSink sink =
+        _generalCaches[E] ??= new IndexedSink<E>(_writeIntInternal);
+    sink.write(value, (v) => f(v));
+  }
+
+  @override
+  void writeEnum(dynamic value) {
+    _writeEnumInternal(value);
+  }
+
+  @override
+  void writeBool(bool value) {
+    assert(value != null);
+    _writeIntInternal(value ? 1 : 0);
+  }
+
+  @override
+  void writeUri(Uri value) {
+    assert(value != null);
+    _writeUri(value);
+  }
+
+  @override
+  void writeString(String value) {
+    assert(value != null);
+    _writeString(value);
+  }
+
+  @override
+  void writeInt(int value) {
+    assert(value != null);
+    assert(value >= 0 && value >> 30 == 0);
+    _writeIntInternal(value);
+  }
+
+  void _writeString(String value) {
+    _stringIndex.write(value, _writeStringInternal);
+  }
+
+  void _writeUri(Uri value) {
+    _uriIndex.write(value, _writeUriInternal);
+  }
+
+  /// Actual serialization of a URI value, implemented by subclasses.
+  void _writeUriInternal(Uri value);
+
+  /// Actual serialization of a String value, implemented by subclasses.
+  void _writeStringInternal(String value);
+
+  /// Actual serialization of a non-negative integer value, implemented by
+  /// subclasses.
+  void _writeIntInternal(int value);
+
+  /// Actual serialization of an enum value, implemented by subclasses.
+  void _writeEnumInternal(dynamic value);
+}
+
+/// [DataSink] that writes data as a sequence of bytes.
+///
+/// This data sink works together with [BinarySource].
+class BinarySink extends AbstractDataSink {
+  final Sink<List<int>> sink;
+  BufferedSink _bufferedSink;
+  int _length = 0;
+
+  BinarySink(this.sink) : _bufferedSink = new BufferedSink(sink);
+
+  @override
+  void _writeUriInternal(Uri value) {
+    _writeString(value.toString());
+  }
+
+  @override
+  void _writeStringInternal(String value) {
+    List<int> bytes = utf8.encode(value);
+    _writeIntInternal(bytes.length);
+    _bufferedSink.addBytes(bytes);
+    _length += bytes.length;
+  }
+
+  @override
+  void _writeIntInternal(int value) {
+    assert(value >= 0 && value >> 30 == 0);
+    if (value < 0x80) {
+      _bufferedSink.addByte(value);
+      _length += 1;
+    } else if (value < 0x4000) {
+      _bufferedSink.addByte2((value >> 8) | 0x80, value & 0xFF);
+      _length += 2;
+    } else {
+      _bufferedSink.addByte4((value >> 24) | 0xC0, (value >> 16) & 0xFF,
+          (value >> 8) & 0xFF, value & 0xFF);
+      _length += 4;
+    }
+  }
+
+  @override
+  void _writeEnumInternal(dynamic value) {
+    _writeIntInternal(value.index);
+  }
+
+  void close() {
+    _bufferedSink.flushAndDestroy();
+    _bufferedSink = null;
+    sink.close();
+  }
+
+  /// Returns the number of bytes written to this data sink.
+  int get length => _length;
+}
+
+/// Puts a buffer in front of a [Sink<List<int>>].
+// TODO(sigmund): share with the implementation in
+// package:kernel/binary/ast_to_binary.dart
+class BufferedSink {
+  static const int SIZE = 100000;
+  static const int SAFE_SIZE = SIZE - 5;
+  static const int SMALL = 10000;
+  final Sink<List<int>> _sink;
+  Uint8List _buffer = new Uint8List(SIZE);
+  int length = 0;
+  int flushedLength = 0;
+
+  Float64List _doubleBuffer = new Float64List(1);
+  Uint8List _doubleBufferUint8;
+
+  int get offset => length + flushedLength;
+
+  BufferedSink(this._sink);
+
+  void addDouble(double d) {
+    _doubleBufferUint8 ??= _doubleBuffer.buffer.asUint8List();
+    _doubleBuffer[0] = d;
+    addByte4(_doubleBufferUint8[0], _doubleBufferUint8[1],
+        _doubleBufferUint8[2], _doubleBufferUint8[3]);
+    addByte4(_doubleBufferUint8[4], _doubleBufferUint8[5],
+        _doubleBufferUint8[6], _doubleBufferUint8[7]);
+  }
+
+  void addByte(int byte) {
+    _buffer[length++] = byte;
+    if (length == SIZE) {
+      _sink.add(_buffer);
+      _buffer = new Uint8List(SIZE);
+      length = 0;
+      flushedLength += SIZE;
+    }
+  }
+
+  void addByte2(int byte1, int byte2) {
+    if (length < SAFE_SIZE) {
+      _buffer[length++] = byte1;
+      _buffer[length++] = byte2;
+    } else {
+      addByte(byte1);
+      addByte(byte2);
+    }
+  }
+
+  void addByte4(int byte1, int byte2, int byte3, int byte4) {
+    if (length < SAFE_SIZE) {
+      _buffer[length++] = byte1;
+      _buffer[length++] = byte2;
+      _buffer[length++] = byte3;
+      _buffer[length++] = byte4;
+    } else {
+      addByte(byte1);
+      addByte(byte2);
+      addByte(byte3);
+      addByte(byte4);
+    }
+  }
+
+  void addBytes(List<int> bytes) {
+    // Avoid copying a large buffer into the another large buffer. Also, if
+    // the bytes buffer is too large to fit in our own buffer, just emit both.
+    if (length + bytes.length < SIZE &&
+        (bytes.length < SMALL || length < SMALL)) {
+      _buffer.setRange(length, length + bytes.length, bytes);
+      length += bytes.length;
+    } else if (bytes.length < SMALL) {
+      // Flush as much as we can in the current buffer.
+      _buffer.setRange(length, SIZE, bytes);
+      _sink.add(_buffer);
+      // Copy over the remainder into a new buffer. It is guaranteed to fit
+      // because the input byte array is small.
+      int alreadyEmitted = SIZE - length;
+      int remainder = bytes.length - alreadyEmitted;
+      _buffer = new Uint8List(SIZE);
+      _buffer.setRange(0, remainder, bytes, alreadyEmitted);
+      length = remainder;
+      flushedLength += SIZE;
+    } else {
+      flush();
+      _sink.add(bytes);
+      flushedLength += bytes.length;
+    }
+  }
+
+  void flush() {
+    _sink.add(_buffer.sublist(0, length));
+    _buffer = new Uint8List(SIZE);
+    flushedLength += length;
+    length = 0;
+  }
+
+  void flushAndDestroy() {
+    _sink.add(_buffer.sublist(0, length));
+  }
+}
diff --git a/lib/src/binary/source.dart b/lib/src/binary/source.dart
new file mode 100644
index 0000000..5aee23b
--- /dev/null
+++ b/lib/src/binary/source.dart
@@ -0,0 +1,299 @@
+// 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:typed_data';
+import 'dart:convert';
+
+/// Interface for deserialization.
+// TODO(sigmund): share this with pkg:compiler/src/serialization/*
+abstract class DataSource {
+  /// Reads a reference to an [E] value from this data source. If the value has
+  /// not yet been deserialized, [f] is called to deserialize the value itself.
+  E readCached<E>(E f());
+
+  /// Reads a potentially `null` [E] value from this data source, calling [f] to
+  /// read the non-null value from the data source.
+  ///
+  /// This is a convenience method to be used together with
+  /// [DataSink.writeValueOrNull].
+  E readValueOrNull<E>(E f());
+
+  /// Reads a list of [E] values from this data source. If [emptyAsNull] is
+  /// `true`, `null` is returned instead of an empty list.
+  ///
+  /// This is a convenience method to be used together with
+  /// [DataSink.writeList].
+  List<E> readList<E>(E f(), {bool emptyAsNull: false});
+
+  /// Reads a boolean value from this data source.
+  bool readBool();
+
+  /// Reads a non-negative integer value from this data source.
+  int readInt();
+
+  /// Reads a potentially `null` non-negative integer value from this data
+  /// source.
+  ///
+  /// This is a convenience method to be used together with
+  /// [DataSink.writeIntOrNull].
+  int readIntOrNull();
+
+  /// Reads a string value from this data source.
+  String readString();
+
+  /// Reads a potentially `null` string value from this data source.
+  ///
+  /// This is a convenience method to be used together with
+  /// [DataSink.writeStringOrNull].
+  String readStringOrNull();
+
+  /// Reads a list of string values from this data source. If [emptyAsNull] is
+  /// `true`, `null` is returned instead of an empty list.
+  ///
+  /// This is a convenience method to be used together with
+  /// [DataSink.writeStrings].
+  List<String> readStrings({bool emptyAsNull: false});
+
+  /// Reads a map from string values to [V] values from this data source,
+  /// calling [f] to read each value from the data source. If [emptyAsNull] is
+  /// `true`, `null` is returned instead of an empty map.
+  ///
+  /// This is a convenience method to be used together with
+  /// [DataSink.writeStringMap].
+  Map<String, V> readStringMap<V>(V f(), {bool emptyAsNull: false});
+
+  /// Reads an enum value from the list of enum [values] from this data source.
+  ///
+  /// The [values] argument is intended to be the static `.values` field on
+  /// enum classes, for instance:
+  ///
+  ///    enum Foo { bar, baz }
+  ///    ...
+  ///    Foo foo = source.readEnum(Foo.values);
+  ///
+  E readEnum<E>(List<E> values);
+
+  /// Reads a URI value from this data source.
+  Uri readUri();
+}
+
+/// Mixin that implements all convenience methods of [DataSource].
+abstract class DataSourceMixin implements DataSource {
+  @override
+  E readValueOrNull<E>(E f()) {
+    bool hasValue = readBool();
+    if (hasValue) {
+      return f();
+    }
+    return null;
+  }
+
+  @override
+  List<E> readList<E>(E f(), {bool emptyAsNull: false}) {
+    int count = readInt();
+    if (count == 0 && emptyAsNull) return null;
+    List<E> list = new List<E>(count);
+    for (int i = 0; i < count; i++) {
+      list[i] = f();
+    }
+    return list;
+  }
+
+  @override
+  int readIntOrNull() {
+    bool hasValue = readBool();
+    if (hasValue) {
+      return readInt();
+    }
+    return null;
+  }
+
+  @override
+  String readStringOrNull() {
+    bool hasValue = readBool();
+    if (hasValue) {
+      return readString();
+    }
+    return null;
+  }
+
+  @override
+  List<String> readStrings({bool emptyAsNull: false}) {
+    int count = readInt();
+    if (count == 0 && emptyAsNull) return null;
+    List<String> list = new List<String>(count);
+    for (int i = 0; i < count; i++) {
+      list[i] = readString();
+    }
+    return list;
+  }
+
+  @override
+  Map<String, V> readStringMap<V>(V f(), {bool emptyAsNull: false}) {
+    int count = readInt();
+    if (count == 0 && emptyAsNull) return null;
+    Map<String, V> map = {};
+    for (int i = 0; i < count; i++) {
+      String key = readString();
+      V value = f();
+      map[key] = value;
+    }
+    return map;
+  }
+}
+
+/// Data source helper reads canonicalized [E] values through indices.
+class IndexedSource<E> {
+  final int Function() _readInt;
+  final List<E> _cache = [];
+  final Set<int> _pending = new Set();
+
+  IndexedSource(this._readInt);
+
+  /// Reads a reference to an [E] value from the data source.
+  ///
+  /// If the value hasn't yet been read, [readValue] is called to deserialize
+  /// the value itself.
+  E read(E readValue()) {
+    int index = _readInt();
+    if (_pending.contains(index)) throw "serialization cycles not supported";
+    if (index >= _cache.length) {
+      _pending.add(index);
+      _cache.add(null);
+      E value = readValue();
+      _pending.remove(index);
+      _cache[index] = value;
+      return value;
+    } else {
+      return _cache[index];
+    }
+  }
+}
+
+/// Base implementation of [DataSource] using [DataSourceMixin] to implement
+/// convenience methods.
+abstract class AbstractDataSource extends DataSourceMixin
+    implements DataSource {
+  IndexedSource<String> _stringIndex;
+  IndexedSource<Uri> _uriIndex;
+  Map<Type, IndexedSource> _generalCaches = {};
+
+  AbstractDataSource() {
+    _stringIndex = new IndexedSource<String>(_readIntInternal);
+    _uriIndex = new IndexedSource<Uri>(_readIntInternal);
+  }
+
+  @override
+  E readCached<E>(E f()) {
+    IndexedSource source =
+        _generalCaches[E] ??= new IndexedSource<E>(_readIntInternal);
+    return source.read(f);
+  }
+
+  @override
+  E readEnum<E>(List<E> values) {
+    return _readEnumInternal(values);
+  }
+
+  @override
+  Uri readUri() {
+    return _readUri();
+  }
+
+  Uri _readUri() {
+    return _uriIndex.read(_readUriInternal);
+  }
+
+  @override
+  bool readBool() {
+    int value = _readIntInternal();
+    assert(value == 0 || value == 1);
+    return value == 1;
+  }
+
+  @override
+  String readString() {
+    return _readString();
+  }
+
+  String _readString() {
+    return _stringIndex.read(_readStringInternal);
+  }
+
+  @override
+  int readInt() {
+    return _readIntInternal();
+  }
+
+  /// Actual deserialization of a string value, implemented by subclasses.
+  String _readStringInternal();
+
+  /// Actual deserialization of a non-negative integer value, implemented by
+  /// subclasses.
+  int _readIntInternal();
+
+  /// Actual deserialization of a URI value, implemented by subclasses.
+  Uri _readUriInternal();
+
+  /// Actual deserialization of an enum value in [values], implemented by
+  /// subclasses.
+  E _readEnumInternal<E>(List<E> values);
+}
+
+/// [DataSource] that reads data from a sequence of bytes.
+///
+/// This data source works together with [BinarySink].
+class BinarySource extends AbstractDataSource {
+  int _byteOffset = 0;
+  final List<int> _bytes;
+
+  BinarySource(this._bytes);
+  int _readByte() => _bytes[_byteOffset++];
+
+  @override
+  String _readStringInternal() {
+    int length = _readIntInternal();
+    List<int> bytes = new Uint8List(length);
+    bytes.setRange(0, bytes.length, _bytes, _byteOffset);
+    _byteOffset += bytes.length;
+    return utf8.decode(bytes);
+  }
+
+  @override
+  int _readIntInternal() {
+    var byte = _readByte();
+    if (byte & 0x80 == 0) {
+      // 0xxxxxxx
+      return byte;
+    } else if (byte & 0x40 == 0) {
+      // 10xxxxxx
+      return ((byte & 0x3F) << 8) | _readByte();
+    } else {
+      // 11xxxxxx
+      return ((byte & 0x3F) << 24) |
+          (_readByte() << 16) |
+          (_readByte() << 8) |
+          _readByte();
+    }
+  }
+
+  @override
+  Uri _readUriInternal() {
+    String text = _readString();
+    return Uri.parse(text);
+  }
+
+  @override
+  E _readEnumInternal<E>(List<E> values) {
+    int index = _readIntInternal();
+    assert(
+        0 <= index && index < values.length,
+        "Invalid data kind index. "
+        "Expected one of $values, found index $index.");
+    return values[index];
+  }
+
+  @override
+  String get _errorContext => ' Offset $_byteOffset in ${_bytes.length}.';
+}
diff --git a/test/binary_codec_test.dart b/test/binary_codec_test.dart
new file mode 100644
index 0000000..edbacb9
--- /dev/null
+++ b/test/binary_codec_test.dart
@@ -0,0 +1,40 @@
+// Copyright (c) 2015, 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:io';
+
+import 'package:dart2js_info/json_info_codec.dart';
+import 'package:dart2js_info/binary_codec.dart' as binary;
+import 'package:test/test.dart';
+
+class ByteSink implements Sink<List<int>> {
+  BytesBuilder builder = new BytesBuilder();
+
+  add(List<int> data) => builder.add(data);
+  close() {}
+}
+
+main() {
+  group('json to proto conversion with deferred files', () {
+    test('hello_world_deferred', () {
+      var helloWorld = new File(
+          'test/hello_world_deferred/hello_world_deferred.js.info.json');
+      var contents = helloWorld.readAsStringSync();
+      var json = jsonDecode(contents);
+      var info = new AllInfoJsonCodec().decode(json);
+
+      var sink = new ByteSink();
+      binary.encode(info, sink);
+      var info2 = binary.decode(sink.builder.toBytes());
+      var json2 = new AllInfoJsonCodec().encode(info2);
+
+      info.program.toJsonDuration = new Duration(milliseconds: 0);
+      var json1 = new AllInfoJsonCodec().encode(info);
+      var contents1 = const JsonEncoder.withIndent("  ").convert(json1);
+      var contents2 = const JsonEncoder.withIndent("  ").convert(json2);
+      expect(contents1 == contents2, isTrue);
+    });
+  });
+}