More implementation of ReplayFileSystem (#32)

Part of #11
diff --git a/lib/src/backends/record_replay/codecs.dart b/lib/src/backends/record_replay/codecs.dart
index 443c337..8b6bb55 100644
--- a/lib/src/backends/record_replay/codecs.dart
+++ b/lib/src/backends/record_replay/codecs.dart
@@ -4,6 +4,7 @@
 
 import 'dart:async';
 import 'dart:convert';
+import 'dart:io' show SYSTEM_ENCODING;
 
 import 'package:file/file.dart';
 import 'package:path/path.dart' as path;
@@ -14,7 +15,9 @@
 import 'replay_file.dart';
 import 'replay_file_stat.dart';
 import 'replay_file_system.dart';
+import 'replay_io_sink.dart';
 import 'replay_link.dart';
+import 'replay_random_access_file.dart';
 import 'result_reference.dart';
 
 /// Converter that leaves object untouched.
@@ -22,7 +25,10 @@
 
 /// Converter that will turn an object into a [Future] of that object.
 const Converter<dynamic, dynamic> kFutureReviver =
-    const _FutureDecoder<dynamic>();
+    const _FutureReviver<dynamic>();
+
+/// Converter that will convert an [Iterable] into a [Stream].
+Converter<dynamic, dynamic> kStreamReviver = const _StreamReviver();
 
 /// Converter that will deserialize a [DateTime].
 const Converter<dynamic, dynamic> kDateTimeReviver = _DateTimeCodec.kDecoder;
@@ -38,17 +44,50 @@
 const Converter<dynamic, dynamic> kPathContextReviver =
     _PathContextCodec.kDecoder;
 
+/// Converter that will deserialize a [Uri].
+const Converter<dynamic, dynamic> kUriReviver = _UriCodec.kDecoder;
+
+/// Converter that will deserialize a [Encoding].
+const Converter<dynamic, dynamic> kEncodingReviver = _EncodingCodec.kDecoder;
+
+/// Converter that will deserialize a [FileSystemEvent].
+const Converter<dynamic, dynamic> kFileSystemEventReviver =
+    _FileSystemEventCodec.kDecoder;
+
+/// Converter that will deserialize each element of a [List] by delegating to
+/// the specified [elementReviver].
+Converter<dynamic, dynamic> listReviver(
+        Converter<dynamic, dynamic> elementReviver) =>
+    new _ListReviver(elementReviver);
+
+/// Converter that will deserialize a blob file reference into the file's bytes.
+Converter<dynamic, dynamic> blobReviver(ReplayFileSystemImpl fileSystem) =>
+    new _BlobReviver(fileSystem);
+
 /// Converter that will deserialize a [ReplayDirectory].
 Converter<dynamic, dynamic> directoryReviver(ReplayFileSystemImpl fileSystem) =>
-    new _DirectoryDecoder(fileSystem);
+    new _DirectoryReviver(fileSystem);
 
 /// Converter that will deserialize a [ReplayFile].
 Converter<dynamic, dynamic> fileReviver(ReplayFileSystemImpl fileSystem) =>
-    new _FileDecoder(fileSystem);
+    new _FileReviver(fileSystem);
 
 /// Converter that will deserialize a [ReplayLink].
 Converter<dynamic, dynamic> linkReviver(ReplayFileSystemImpl fileSystem) =>
-    new _LinkDecoder(fileSystem);
+    new _LinkReviver(fileSystem);
+
+/// Converter that will deserialize an arbitrary [FileSystemEntity].
+Converter<dynamic, dynamic> entityReviver(ReplayFileSystemImpl fileSystem) =>
+    new _FileSystemEntityReviver(fileSystem);
+
+/// Converter that will deserialize a [ReplayRandomAccessFile].
+Converter<dynamic, dynamic> randomAccessFileReviver(
+        ReplayFileSystemImpl fileSystem) =>
+    new _RandomAccessFileReviver(fileSystem);
+
+/// Converter that will deserialize a [ReplayRandomAccessFile].
+Converter<dynamic, dynamic> ioSinkReviver(ReplayFileSystemImpl fileSystem) =>
+    new _IOSinkReviver(fileSystem);
 
 /// Encodes an arbitrary [object] into a JSON-ready representation (a number,
 /// boolean, string, null, list, or map).
@@ -87,16 +126,16 @@
     const TypeMatcher<Map<dynamic, dynamic>>(): const _MapEncoder(),
     const TypeMatcher<Symbol>(): const _SymbolEncoder(),
     const TypeMatcher<DateTime>(): _DateTimeCodec.kEncoder,
-    const TypeMatcher<Uri>(): const _ToStringEncoder(),
+    const TypeMatcher<Uri>(): _UriCodec.kEncoder,
     const TypeMatcher<path.Context>(): _PathContextCodec.kEncoder,
     const TypeMatcher<ResultReference<dynamic>>(): const _ResultEncoder(),
     const TypeMatcher<LiveInvocationEvent<dynamic>>(): const _EventEncoder(),
     const TypeMatcher<ReplayAware>(): const _ReplayAwareEncoder(),
-    const TypeMatcher<Encoding>(): const _EncodingEncoder(),
+    const TypeMatcher<Encoding>(): _EncodingCodec.kEncoder,
     const TypeMatcher<FileMode>(): const _FileModeEncoder(),
     const TypeMatcher<FileStat>(): _FileStatCodec.kEncoder,
     const TypeMatcher<FileSystemEntityType>(): _EntityTypeCodec.kEncoder,
-    const TypeMatcher<FileSystemEvent>(): const _FileSystemEventEncoder(),
+    const TypeMatcher<FileSystemEvent>(): _FileSystemEventCodec.kEncoder,
   };
 
   /// Default encoder (used for types not covered in [_encoders]).
@@ -163,10 +202,13 @@
 class _DateTimeCodec extends Codec<DateTime, int> {
   const _DateTimeCodec();
 
-  static int _encode(DateTime input) => input.millisecondsSinceEpoch;
+  static int _encode(DateTime input) => input?.millisecondsSinceEpoch;
 
-  static DateTime _decode(int input) =>
-      new DateTime.fromMillisecondsSinceEpoch(input);
+  static DateTime _decode(int input) {
+    return input == null
+        ? null
+        : new DateTime.fromMillisecondsSinceEpoch(input);
+  }
 
   static const Converter<DateTime, int> kEncoder =
       const _ForwardingConverter<DateTime, int>(_encode);
@@ -181,11 +223,24 @@
   Converter<int, DateTime> get decoder => kDecoder;
 }
 
-class _ToStringEncoder extends Converter<Object, String> {
-  const _ToStringEncoder();
+class _UriCodec extends Codec<Uri, String> {
+  const _UriCodec();
+
+  static String _encode(Uri input) => input.toString();
+
+  static Uri _decode(String input) => Uri.parse(input);
+
+  static const Converter<Uri, String> kEncoder =
+      const _ForwardingConverter<Uri, String>(_encode);
+
+  static const Converter<String, Uri> kDecoder =
+      const _ForwardingConverter<String, Uri>(_decode);
 
   @override
-  String convert(Object input) => input.toString();
+  Converter<Uri, String> get encoder => kEncoder;
+
+  @override
+  Converter<String, Uri> get decoder => kDecoder;
 }
 
 class _PathContextCodec extends Codec<path.Context, Map<String, String>> {
@@ -246,11 +301,31 @@
   String convert(ReplayAware input) => input.identifier;
 }
 
-class _EncodingEncoder extends Converter<Encoding, String> {
-  const _EncodingEncoder();
+class _EncodingCodec extends Codec<Encoding, String> {
+  const _EncodingCodec();
+
+  static String _encode(Encoding input) => input.name;
+
+  static Encoding _decode(String input) {
+    if (input == 'system') {
+      return SYSTEM_ENCODING;
+    } else if (input != null) {
+      return Encoding.getByName(input);
+    }
+    return null;
+  }
+
+  static const Converter<Encoding, String> kEncoder =
+      const _ForwardingConverter<Encoding, String>(_encode);
+
+  static const Converter<String, Encoding> kDecoder =
+      const _ForwardingConverter<String, Encoding>(_decode);
 
   @override
-  String convert(Encoding input) => input.name;
+  Converter<Encoding, String> get encoder => kEncoder;
+
+  @override
+  Converter<String, Encoding> get decoder => kDecoder;
 }
 
 class _FileModeEncoder extends Converter<FileMode, String> {
@@ -332,46 +407,143 @@
   Converter<String, FileSystemEntityType> get decoder => kDecoder;
 }
 
-class _FileSystemEventEncoder
-    extends Converter<FileSystemEvent, Map<String, Object>> {
-  const _FileSystemEventEncoder();
+class _FileSystemEventCodec
+    extends Codec<FileSystemEvent, Map<String, Object>> {
+  const _FileSystemEventCodec();
 
-  @override
-  Map<String, Object> convert(FileSystemEvent input) {
+  static Map<String, Object> _encode(FileSystemEvent input) {
     return <String, Object>{
       'type': input.type,
       'path': input.path,
+      'isDirectory': input.isDirectory,
     };
   }
+
+  static FileSystemEvent _decode(Map<String, Object> input) =>
+      new _FileSystemEvent(input);
+
+  static const Converter<FileSystemEvent, Map<String, Object>> kEncoder =
+      const _ForwardingConverter<FileSystemEvent, Map<String, Object>>(_encode);
+
+  static const Converter<Map<String, Object>, FileSystemEvent> kDecoder =
+      const _ForwardingConverter<Map<String, Object>, FileSystemEvent>(_decode);
+
+  @override
+  Converter<FileSystemEvent, Map<String, Object>> get encoder => kEncoder;
+
+  @override
+  Converter<Map<String, Object>, FileSystemEvent> get decoder => kDecoder;
 }
 
-class _FutureDecoder<T> extends Converter<T, Future<T>> {
-  const _FutureDecoder();
+class _FileSystemEvent implements FileSystemEvent {
+  final Map<String, Object> _data;
+
+  const _FileSystemEvent(this._data);
+
+  @override
+  int get type => _data['type'];
+
+  @override
+  String get path => _data['path'];
+
+  @override
+  bool get isDirectory => _data['isDirectory'];
+}
+
+class _FutureReviver<T> extends Converter<T, Future<T>> {
+  const _FutureReviver();
 
   @override
   Future<T> convert(T input) async => input;
 }
 
-class _DirectoryDecoder extends Converter<String, Directory> {
+class _DirectoryReviver extends Converter<String, Directory> {
   final ReplayFileSystemImpl fileSystem;
-  const _DirectoryDecoder(this.fileSystem);
+  const _DirectoryReviver(this.fileSystem);
 
   @override
   Directory convert(String input) => new ReplayDirectory(fileSystem, input);
 }
 
-class _FileDecoder extends Converter<String, File> {
+class _FileReviver extends Converter<String, File> {
   final ReplayFileSystemImpl fileSystem;
-  const _FileDecoder(this.fileSystem);
+  const _FileReviver(this.fileSystem);
 
   @override
   File convert(String input) => new ReplayFile(fileSystem, input);
 }
 
-class _LinkDecoder extends Converter<String, Link> {
+class _LinkReviver extends Converter<String, Link> {
   final ReplayFileSystemImpl fileSystem;
-  const _LinkDecoder(this.fileSystem);
+  const _LinkReviver(this.fileSystem);
 
   @override
   Link convert(String input) => new ReplayLink(fileSystem, input);
 }
+
+class _FileSystemEntityReviver extends Converter<String, FileSystemEntity> {
+  final ReplayFileSystemImpl fileSystem;
+  const _FileSystemEntityReviver(this.fileSystem);
+
+  @override
+  FileSystemEntity convert(String input) {
+    if (input.contains('Directory')) {
+      return new ReplayDirectory(fileSystem, input);
+    } else if (input.contains('File')) {
+      return new ReplayFile(fileSystem, input);
+    } else {
+      return new ReplayLink(fileSystem, input);
+    }
+  }
+}
+
+class _RandomAccessFileReviver extends Converter<String, RandomAccessFile> {
+  final ReplayFileSystemImpl fileSystem;
+  const _RandomAccessFileReviver(this.fileSystem);
+
+  @override
+  RandomAccessFile convert(String input) =>
+      new ReplayRandomAccessFile(fileSystem, input);
+}
+
+class _IOSinkReviver extends Converter<String, IOSink> {
+  final ReplayFileSystemImpl fileSystem;
+  const _IOSinkReviver(this.fileSystem);
+
+  @override
+  IOSink convert(String input) => new ReplayIOSink(fileSystem, input);
+}
+
+class _ListReviver extends Converter<Iterable<dynamic>, List<dynamic>> {
+  final Converter<dynamic, dynamic> elementReviver;
+
+  const _ListReviver(this.elementReviver);
+
+  @override
+  List<dynamic> convert(Iterable<dynamic> input) =>
+      input.map(elementReviver.convert).toList();
+}
+
+class _StreamReviver extends Converter<List<dynamic>, Stream<dynamic>> {
+  const _StreamReviver();
+
+  @override
+  Stream<dynamic> convert(List<dynamic> input) {
+    return new Stream<dynamic>.fromIterable(input);
+  }
+}
+
+class _BlobReviver extends Converter<String, List<int>> {
+  final ReplayFileSystemImpl fileSystem;
+  const _BlobReviver(this.fileSystem);
+
+  @override
+  List<int> convert(String input) {
+    assert(input.startsWith('!'));
+    String basename = input.substring(1);
+    String dirname = fileSystem.recording.path;
+    String path = fileSystem.recording.fileSystem.path.join(dirname, basename);
+    File file = fileSystem.recording.fileSystem.file(path);
+    return file.readAsBytesSync();
+  }
+}
diff --git a/lib/src/backends/record_replay/proxy.dart b/lib/src/backends/record_replay/proxy.dart
index 2266baa..7c0f83f 100644
--- a/lib/src/backends/record_replay/proxy.dart
+++ b/lib/src/backends/record_replay/proxy.dart
@@ -43,6 +43,7 @@
   }
 }
 
+// TODO(tvolkert): remove (https://github.com/dart-lang/sdk/issues/28706)
 class _MethodInvocationProxy extends Invocation {
   _MethodInvocationProxy(
     this.memberName,
diff --git a/lib/src/backends/record_replay/replay_directory.dart b/lib/src/backends/record_replay/replay_directory.dart
index abac3e1..75e3b18 100644
--- a/lib/src/backends/record_replay/replay_directory.dart
+++ b/lib/src/backends/record_replay/replay_directory.dart
@@ -6,6 +6,7 @@
 
 import 'package:file/file.dart';
 
+import 'codecs.dart';
 import 'replay_file_system.dart';
 import 'replay_file_system_entity.dart';
 
@@ -15,14 +16,24 @@
   /// Creates a new `ReplayDirectory`.
   ReplayDirectory(ReplayFileSystemImpl fileSystem, String identifier)
       : super(fileSystem, identifier) {
-    // TODO(tvolkert): fill in resurrectors
+    Converter<dynamic, dynamic> convertThis = directoryReviver(fileSystem);
+    Converter<dynamic, dynamic> convertFutureThis =
+        convertThis.fuse(kFutureReviver);
+
     methods.addAll(<Symbol, Converter<dynamic, dynamic>>{
-      #create: null,
-      #createSync: null,
-      #createTemp: null,
-      #createTempSync: null,
-      #list: null,
-      #listSync: null,
+      #rename: convertFutureThis,
+      #renameSync: convertThis,
+      #delete: convertFutureThis,
+      #create: convertFutureThis,
+      #createSync: kPassthrough,
+      #createTemp: convertFutureThis,
+      #createTempSync: convertThis,
+      #list: listReviver(entityReviver(fileSystem)).fuse(kStreamReviver),
+      #listSync: listReviver(entityReviver(fileSystem)),
+    });
+
+    properties.addAll(<Symbol, Converter<dynamic, dynamic>>{
+      #absolute: convertThis,
     });
   }
 }
diff --git a/lib/src/backends/record_replay/replay_file.dart b/lib/src/backends/record_replay/replay_file.dart
index 931f5a1..149162d 100644
--- a/lib/src/backends/record_replay/replay_file.dart
+++ b/lib/src/backends/record_replay/replay_file.dart
@@ -6,6 +6,7 @@
 
 import 'package:file/file.dart';
 
+import 'codecs.dart';
 import 'replay_file_system.dart';
 import 'replay_file_system_entity.dart';
 
@@ -15,30 +16,40 @@
   /// Creates a new `ReplayFile`.
   ReplayFile(ReplayFileSystemImpl fileSystem, String identifier)
       : super(fileSystem, identifier) {
-    // TODO(tvolkert): fill in resurrectors
+    Converter<dynamic, dynamic> convertThis = fileReviver(fileSystem);
+    Converter<dynamic, dynamic> convertFutureThis =
+        convertThis.fuse(kFutureReviver);
+
     methods.addAll(<Symbol, Converter<dynamic, dynamic>>{
-      #create: null,
-      #createSync: null,
-      #copy: null,
-      #copySync: null,
-      #length: null,
-      #lengthSync: null,
-      #lastModified: null,
-      #lastModifiedSync: null,
-      #open: null,
-      #openSync: null,
-      #openRead: null,
-      #openWrite: null,
-      #readAsBytes: null,
-      #readAsBytesSync: null,
-      #readAsString: null,
-      #readAsStringSync: null,
-      #readAsLines: null,
-      #readAsLinesSync: null,
-      #writeAsBytes: null,
-      #writeAsBytesSync: null,
-      #writeAsString: null,
-      #writeAsStringSync: null,
+      #rename: convertFutureThis,
+      #renameSync: convertThis,
+      #delete: convertFutureThis,
+      #create: convertFutureThis,
+      #createSync: kPassthrough,
+      #copy: convertFutureThis,
+      #copySync: convertThis,
+      #length: kFutureReviver,
+      #lengthSync: kPassthrough,
+      #lastModified: kDateTimeReviver.fuse(kFutureReviver),
+      #lastModifiedSync: kDateTimeReviver,
+      #open: randomAccessFileReviver(fileSystem).fuse(kFutureReviver),
+      #openSync: randomAccessFileReviver(fileSystem),
+      #openRead: kStreamReviver,
+      #openWrite: ioSinkReviver(fileSystem),
+      #readAsBytes: blobReviver(fileSystem).fuse(kFutureReviver),
+      #readAsBytesSync: blobReviver(fileSystem),
+      #readAsString: kFutureReviver,
+      #readAsStringSync: kPassthrough,
+      #readAsLines: kFutureReviver,
+      #readAsLinesSync: kPassthrough,
+      #writeAsBytes: convertFutureThis,
+      #writeAsBytesSync: kPassthrough,
+      #writeAsString: convertFutureThis,
+      #writeAsStringSync: kPassthrough,
+    });
+
+    properties.addAll(<Symbol, Converter<dynamic, dynamic>>{
+      #absolute: convertThis,
     });
   }
 }
diff --git a/lib/src/backends/record_replay/replay_file_system.dart b/lib/src/backends/record_replay/replay_file_system.dart
index df77057..93efa5e 100644
--- a/lib/src/backends/record_replay/replay_file_system.dart
+++ b/lib/src/backends/record_replay/replay_file_system.dart
@@ -60,7 +60,7 @@
     }
     List<Map<String, dynamic>> manifest =
         new JsonDecoder().convert(manifestFile.readAsStringSync());
-    return new ReplayFileSystemImpl(manifest);
+    return new ReplayFileSystemImpl(recording, manifest);
   }
 }
 
@@ -69,7 +69,7 @@
     with ReplayProxyMixin
     implements ReplayFileSystem, ReplayAware {
   /// Creates a new `ReplayFileSystemImpl`.
-  ReplayFileSystemImpl(this.manifest) {
+  ReplayFileSystemImpl(this.recording, this.manifest) {
     methods.addAll(<Symbol, Converter<dynamic, dynamic>>{
       #directory: directoryReviver(this),
       #file: fileReviver(this),
@@ -91,6 +91,9 @@
     });
   }
 
+  /// The location of the recording that's driving this file system
+  final Directory recording;
+
   @override
   String get identifier => kFileSystemEncodedValue;
 
diff --git a/lib/src/backends/record_replay/replay_file_system_entity.dart b/lib/src/backends/record_replay/replay_file_system_entity.dart
index 3dfd39d..da5631d 100644
--- a/lib/src/backends/record_replay/replay_file_system_entity.dart
+++ b/lib/src/backends/record_replay/replay_file_system_entity.dart
@@ -17,30 +17,24 @@
     implements FileSystemEntity {
   /// Creates a new `ReplayFileSystemEntity`.
   ReplayFileSystemEntity(this.fileSystem, this.identifier) {
-    // TODO(tvolkert): fill in resurrectors
     methods.addAll(<Symbol, Converter<dynamic, dynamic>>{
-      #exists: null,
-      #existsSync: null,
-      #rename: null,
-      #renameSync: null,
-      #resolveSymbolicLinks: null,
-      #resolveSymbolicLinksSync: null,
-      #stat: null,
-      #statSync: null,
-      #delete: null,
-      #deleteSync: null,
-      #watch: null,
+      #exists: kPassthrough.fuse(kFutureReviver),
+      #existsSync: kPassthrough,
+      #resolveSymbolicLinks: kPassthrough.fuse(kFutureReviver),
+      #resolveSymbolicLinksSync: kPassthrough,
+      #stat: kFileStatReviver.fuse(kFutureReviver),
+      #statSync: kFileStatReviver,
+      #deleteSync: kPassthrough,
+      #watch: listReviver(kFileSystemEventReviver).fuse(kStreamReviver),
     });
 
-    // TODO(tvolkert): fill in resurrectors
     properties.addAll(<Symbol, Converter<dynamic, dynamic>>{
       #path: kPassthrough,
-      #uri: null,
-      #isAbsolute: null,
-      #absolute: null,
-      #parent: null,
-      #basename: null,
-      #dirname: null,
+      #uri: kUriReviver,
+      #isAbsolute: kPassthrough,
+      #parent: directoryReviver(fileSystem),
+      #basename: kPassthrough,
+      #dirname: kPassthrough,
     });
   }
 
diff --git a/lib/src/backends/record_replay/replay_io_sink.dart b/lib/src/backends/record_replay/replay_io_sink.dart
new file mode 100644
index 0000000..b625810
--- /dev/null
+++ b/lib/src/backends/record_replay/replay_io_sink.dart
@@ -0,0 +1,44 @@
+// Copyright (c) 2017, 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 'package:file/file.dart';
+
+import 'codecs.dart';
+import 'replay_file_system.dart';
+import 'replay_proxy_mixin.dart';
+
+/// [IOSink] implementation that replays all invocation activity from a prior
+/// recording.
+class ReplayIOSink extends Object with ReplayProxyMixin implements IOSink {
+  final ReplayFileSystemImpl _fileSystem;
+
+  /// Creates a new `ReplayIOSink`.
+  ReplayIOSink(this._fileSystem, this.identifier) {
+    methods.addAll(<Symbol, Converter<dynamic, dynamic>>{
+      #add: kPassthrough,
+      #write: kPassthrough,
+      #writeAll: kPassthrough,
+      #writeln: kPassthrough,
+      #writeCharCode: kPassthrough,
+      #addError: kPassthrough,
+      #addStream: kFutureReviver,
+      #flush: kFutureReviver,
+      #close: kFutureReviver,
+    });
+
+    properties.addAll(<Symbol, Converter<dynamic, dynamic>>{
+      #encoding: kEncodingReviver,
+      const Symbol('encoding='): kPassthrough,
+      #done: kPassthrough.fuse(kFutureReviver),
+    });
+  }
+
+  @override
+  final String identifier;
+
+  @override
+  List<Map<String, dynamic>> get manifest => _fileSystem.manifest;
+}
diff --git a/lib/src/backends/record_replay/replay_link.dart b/lib/src/backends/record_replay/replay_link.dart
index d23e826..6094ca5 100644
--- a/lib/src/backends/record_replay/replay_link.dart
+++ b/lib/src/backends/record_replay/replay_link.dart
@@ -6,6 +6,7 @@
 
 import 'package:file/file.dart';
 
+import 'codecs.dart';
 import 'replay_file_system.dart';
 import 'replay_file_system_entity.dart';
 
@@ -15,14 +16,20 @@
   /// Creates a new `ReplayLink`.
   ReplayLink(ReplayFileSystemImpl fileSystem, String identifier)
       : super(fileSystem, identifier) {
-    // TODO(tvolkert): fill in resurrectors
     methods.addAll(<Symbol, Converter<dynamic, dynamic>>{
-      #create: null,
-      #createSync: null,
-      #update: null,
-      #updateSync: null,
-      #target: null,
-      #targetSync: null,
+      #rename: linkReviver(fileSystem).fuse(kFutureReviver),
+      #renameSync: linkReviver(fileSystem),
+      #delete: linkReviver(fileSystem).fuse(kFutureReviver),
+      #create: linkReviver(fileSystem).fuse(kFutureReviver),
+      #createSync: kPassthrough,
+      #update: linkReviver(fileSystem).fuse(kFutureReviver),
+      #updateSync: kPassthrough,
+      #target: kPassthrough.fuse(kFutureReviver),
+      #targetSync: kPassthrough,
+    });
+
+    properties.addAll(<Symbol, Converter<dynamic, dynamic>>{
+      #absolute: linkReviver(fileSystem),
     });
   }
 }
diff --git a/lib/src/backends/record_replay/replay_random_access_file.dart b/lib/src/backends/record_replay/replay_random_access_file.dart
new file mode 100644
index 0000000..41774c4
--- /dev/null
+++ b/lib/src/backends/record_replay/replay_random_access_file.dart
@@ -0,0 +1,66 @@
+// Copyright (c) 2017, 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 'package:file/file.dart';
+
+import 'codecs.dart';
+import 'replay_file_system.dart';
+import 'replay_proxy_mixin.dart';
+
+/// [RandomAccessFile] implementation that replays all invocation activity from a prior
+/// recording.
+class ReplayRandomAccessFile extends Object
+    with ReplayProxyMixin
+    implements RandomAccessFile {
+  final ReplayFileSystemImpl _fileSystem;
+
+  /// Creates a new `ReplayIOSink`.
+  ReplayRandomAccessFile(this._fileSystem, this.identifier) {
+    Converter<dynamic, dynamic> convertFutureThis =
+        randomAccessFileReviver(_fileSystem).fuse(kFutureReviver);
+
+    methods.addAll(<Symbol, Converter<dynamic, dynamic>>{
+      #close: convertFutureThis,
+      #closeSync: kPassthrough,
+      #readByte: kFutureReviver,
+      #readByteSync: kPassthrough,
+      #read: kFutureReviver,
+      #readSync: kPassthrough,
+      #readInto: kFutureReviver,
+      #readIntoSync: kPassthrough,
+      #writeByte: convertFutureThis,
+      #writeByteSync: kPassthrough,
+      #writeFrom: convertFutureThis,
+      #writeFromSync: kPassthrough,
+      #writeString: convertFutureThis,
+      #writeStringSync: kPassthrough,
+      #position: kFutureReviver,
+      #positionSync: kPassthrough,
+      #setPosition: convertFutureThis,
+      #setPositionSync: kPassthrough,
+      #truncate: convertFutureThis,
+      #truncateSync: kPassthrough,
+      #length: kFutureReviver,
+      #lengthSync: kPassthrough,
+      #flush: convertFutureThis,
+      #flushSync: kPassthrough,
+      #lock: convertFutureThis,
+      #lockSync: kPassthrough,
+      #unlock: convertFutureThis,
+      #unlockSync: kPassthrough,
+    });
+
+    properties.addAll(<Symbol, Converter<dynamic, dynamic>>{
+      #path: kPassthrough,
+    });
+  }
+
+  @override
+  final String identifier;
+
+  @override
+  List<Map<String, dynamic>> get manifest => _fileSystem.manifest;
+}
diff --git a/test/replay_test.dart b/test/replay_test.dart
index 65d317b..d647f95 100644
--- a/test/replay_test.dart
+++ b/test/replay_test.dart
@@ -42,11 +42,16 @@
         // ReplayFileSystem does not yet replay exceptions
         '.*(disallows|throws).*',
 
-        // TODO(tvolkert): Enable when ReplayFileSystem is complete.
-        'FileSystem',
-        'Directory',
-        'File',
-        'Link',
+        // TODO(tvolkert): re-enable when these are implemented
+        'File > copy',
+        'File > openRead',
+        'File > openWrite',
+        'File > readAsLines',
+        'File > readAsString',
+        'File > writeAsBytes',
+        'File > writeAsString',
+
+        'File > open', // Not yet implemented in MemoryFileSystem
       ],
     );