Add a basic implementation for MemoryRandomAccessFile (#136)

* Add a basic implementation for MemoryRandomAccessFile

Add a basic implementation for `MemoryRandomAccessFile` so that
`MemoryFile.open`/`openSync` can work.  Currently all functions
related to `lock`/`unlock` remain unimplemented.

* Make MemoryRandomAccessFile.readIntoSync and writeFromSync more efficient
diff --git a/packages/file/CHANGELOG.md b/packages/file/CHANGELOG.md
index 9929c89..d44e91a 100644
--- a/packages/file/CHANGELOG.md
+++ b/packages/file/CHANGELOG.md
@@ -1,3 +1,8 @@
+#### 5.2.0
+
+* Added a `MemoryRandomAccessFile` class and implemented
+  `MemoryFile.open()`/`openSync()`.
+
 #### 5.1.0
 
 * Added a new `MemoryFileSystem` constructor to use a test clock
@@ -12,7 +17,7 @@
 
 #### 5.0.8
 
-* Return Uint8List rather than List<int>.
+* Return `Uint8List` rather than `List<int>`.
 
 #### 5.0.7
 
diff --git a/packages/file/lib/src/backends/memory/memory_file.dart b/packages/file/lib/src/backends/memory/memory_file.dart
index 90419b6..5d75dbf 100644
--- a/packages/file/lib/src/backends/memory/memory_file.dart
+++ b/packages/file/lib/src/backends/memory/memory_file.dart
@@ -4,7 +4,7 @@
 
 import 'dart:async';
 import 'dart:convert';
-import 'dart:math' show min;
+import 'dart:math' as math show min;
 import 'dart:typed_data';
 
 import 'package:file/file.dart';
@@ -14,6 +14,7 @@
 
 import 'common.dart';
 import 'memory_file_system_entity.dart';
+import 'memory_random_access_file.dart';
 import 'node.dart';
 import 'utils.dart' as utils;
 
@@ -173,8 +174,15 @@
       openSync(mode: mode);
 
   @override
-  io.RandomAccessFile openSync({io.FileMode mode = io.FileMode.read}) =>
-      throw UnimplementedError('TODO');
+  io.RandomAccessFile openSync({io.FileMode mode = io.FileMode.read}) {
+    if (utils.isWriteMode(mode) && !existsSync()) {
+      // [resolvedBacking] requires that the file already exists, so we must
+      // create it here first.
+      createSync();
+    }
+
+    return MemoryRandomAccessFile(this, resolvedBacking as FileNode, mode);
+  }
 
   @override
   Stream<Uint8List> openRead([int start, int end]) {
@@ -184,7 +192,7 @@
       if (start != null) {
         content = end == null
             ? content.sublist(start)
-            : content.sublist(start, min(end, content.length));
+            : content.sublist(start, math.min(end, content.length));
       }
       return Stream<Uint8List>.fromIterable(<Uint8List>[content]);
     } catch (e) {
diff --git a/packages/file/lib/src/backends/memory/memory_random_access_file.dart b/packages/file/lib/src/backends/memory/memory_random_access_file.dart
new file mode 100644
index 0000000..fcd4227
--- /dev/null
+++ b/packages/file/lib/src/backends/memory/memory_random_access_file.dart
@@ -0,0 +1,389 @@
+// Copyright (c) 2020, 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:math' as math show min;
+import 'dart:typed_data';
+
+import 'package:file/src/common.dart' as common;
+import 'package:file/src/io.dart' as io;
+
+import 'memory_file.dart';
+import 'node.dart';
+import 'utils.dart' as utils;
+
+/// A [MemoryFileSystem]-backed implementation of [io.RandomAccessFile].
+class MemoryRandomAccessFile implements io.RandomAccessFile {
+  /// Constructs a [MemoryRandomAccessFile].
+  ///
+  /// This should be used only by [MemoryFile.open] or [MemoryFile.openSync].
+  MemoryRandomAccessFile(this._memoryFile, this._node, this._mode) {
+    switch (_mode) {
+      case io.FileMode.read:
+        break;
+      case io.FileMode.write:
+      case io.FileMode.writeOnly:
+        truncateSync(0);
+        break;
+      case io.FileMode.append:
+      case io.FileMode.writeOnlyAppend:
+        _position = lengthSync();
+        break;
+      default:
+        // [FileMode] provides no way of retrieving its value or name.
+        throw UnimplementedError('Unsupported FileMode');
+    }
+  }
+
+  final MemoryFile _memoryFile;
+  final FileNode _node;
+  final io.FileMode _mode;
+
+  bool _isOpen = true;
+  int _position = 0;
+
+  /// Whether an asynchronous operation is pending.
+  ///
+  /// See [_asyncWrapper] for details.
+  bool get _asyncOperationPending => __asyncOperationPending;
+
+  set _asyncOperationPending(bool value) {
+    assert(__asyncOperationPending != value);
+    __asyncOperationPending = value;
+  }
+
+  bool __asyncOperationPending = false;
+
+  /// Throws a [io.FileSystemException] if an operation is attempted on a file
+  /// that is not open.
+  void _checkOpen() {
+    if (!_isOpen) {
+      throw io.FileSystemException('File closed', path);
+    }
+  }
+
+  /// Throws a [io.FileSystemException] if attempting to read from a file that
+  /// has not been opened for reading.
+  void _checkReadable(String operation) {
+    switch (_mode) {
+      case io.FileMode.read:
+      case io.FileMode.write:
+      case io.FileMode.append:
+        return;
+      case io.FileMode.writeOnly:
+      case io.FileMode.writeOnlyAppend:
+      default:
+        throw io.FileSystemException(
+            '$operation failed', path, common.badFileDescriptor(path).osError);
+    }
+  }
+
+  /// Throws a [io.FileSystemException] if attempting to read from a file that
+  /// has not been opened for writing.
+  void _checkWritable(String operation) {
+    if (utils.isWriteMode(_mode)) {
+      return;
+    }
+
+    throw io.FileSystemException(
+        '$operation failed', path, common.badFileDescriptor(path).osError);
+  }
+
+  /// Throws a [io.FileSystemException] if attempting to perform an operation
+  /// while an asynchronous operation is already in progress.
+  ///
+  /// See [_asyncWrapper] for details.
+  void _checkAsync() {
+    if (_asyncOperationPending) {
+      throw io.FileSystemException(
+          'An async operation is currently pending', path);
+    }
+  }
+
+  /// Wraps a synchronous function to make it appear asynchronous.
+  ///
+  /// [_asyncOperationPending], [_checkAsync], and [_asyncWrapper] are used to
+  /// mimic [RandomAccessFile]'s enforcement that only one asynchronous
+  /// operation is pending for a [RandomAccessFile] instance.  Since
+  /// [MemoryFileSystem]-based classes are likely to be used in tests, fidelity
+  /// is important to catch errors that might occur in production.
+  ///
+  /// [_asyncWrapper] does not call [f] directly since setting and unsetting
+  /// [_asyncOperationPending] synchronously would not be meaningful.  We
+  /// instead execute [f] through a [Future.delayed] callback to better simulate
+  /// asynchrony.
+  Future<R> _asyncWrapper<R>(R Function() f) async {
+    _checkAsync();
+
+    _asyncOperationPending = true;
+    try {
+      return await Future<R>.delayed(
+        Duration.zero,
+        () {
+          // Temporarily reset [_asyncOpPending] in case [f]'s has its own
+          // checks for pending asynchronous operations.
+          _asyncOperationPending = false;
+          try {
+            return f();
+          } finally {
+            _asyncOperationPending = true;
+          }
+        },
+      );
+    } finally {
+      _asyncOperationPending = false;
+    }
+  }
+
+  @override
+  String get path => _memoryFile.path;
+
+  @override
+  Future<void> close() async => _asyncWrapper(closeSync);
+
+  @override
+  void closeSync() {
+    _checkOpen();
+    _isOpen = false;
+  }
+
+  @override
+  Future<io.RandomAccessFile> flush() async {
+    await _asyncWrapper(flushSync);
+    return this;
+  }
+
+  @override
+  void flushSync() {
+    _checkOpen();
+    _checkAsync();
+  }
+
+  @override
+  Future<int> length() => _asyncWrapper(lengthSync);
+
+  @override
+  int lengthSync() {
+    _checkOpen();
+    _checkAsync();
+    return _memoryFile.lengthSync();
+  }
+
+  @override
+  Future<io.RandomAccessFile> lock([
+    io.FileLock mode = io.FileLock.exclusive,
+    int start = 0,
+    int end = -1,
+  ]) async {
+    await _asyncWrapper(() => lockSync(mode, start, end));
+    return this;
+  }
+
+  @override
+  void lockSync([
+    io.FileLock mode = io.FileLock.exclusive,
+    int start = 0,
+    int end = -1,
+  ]) {
+    _checkOpen();
+    _checkAsync();
+    throw UnimplementedError('TODO');
+  }
+
+  @override
+  Future<int> position() => _asyncWrapper(positionSync);
+
+  @override
+  int positionSync() {
+    _checkOpen();
+    _checkAsync();
+    return _position;
+  }
+
+  @override
+  Future<Uint8List> read(int bytes) => _asyncWrapper(() => readSync(bytes));
+
+  @override
+  Uint8List readSync(int bytes) {
+    _checkOpen();
+    _checkAsync();
+    _checkReadable('read');
+    // TODO(jamesderlin): Check for integer overflow.
+    final int end = math.min(_position + bytes, lengthSync());
+    final Uint8List copy = _node.content.sublist(_position, end);
+    _position = end;
+    return copy;
+  }
+
+  @override
+  Future<int> readByte() => _asyncWrapper(readByteSync);
+
+  @override
+  int readByteSync() {
+    _checkOpen();
+    _checkAsync();
+    _checkReadable('readByte');
+
+    if (_position >= lengthSync()) {
+      return -1;
+    }
+    return _node.content[_position++];
+  }
+
+  @override
+  Future<int> readInto(List<int> buffer, [int start = 0, int end]) =>
+      _asyncWrapper(() => readIntoSync(buffer, start, end));
+
+  @override
+  int readIntoSync(List<int> buffer, [int start = 0, int end]) {
+    _checkOpen();
+    _checkAsync();
+    _checkReadable('readInto');
+
+    end = RangeError.checkValidRange(start, end, buffer.length);
+
+    final int length = lengthSync();
+    int i;
+    for (i = start; i < end && _position < length; i += 1, _position += 1) {
+      buffer[i] = _node.content[_position];
+    }
+    return i - start;
+  }
+
+  @override
+  Future<io.RandomAccessFile> setPosition(int position) async {
+    await _asyncWrapper(() => setPositionSync(position));
+    return this;
+  }
+
+  @override
+  void setPositionSync(int position) {
+    _checkOpen();
+    _checkAsync();
+
+    if (position < 0) {
+      throw io.FileSystemException(
+          'setPosition failed', path, common.invalidArgument(path).osError);
+    }
+
+    // Empirical testing indicates that setting the position to be beyond the
+    // end of the file is legal and will zero-fill upon the next write.
+    _position = position;
+  }
+
+  @override
+  Future<io.RandomAccessFile> truncate(int length) async {
+    await _asyncWrapper(() => truncateSync(length));
+    return this;
+  }
+
+  @override
+  void truncateSync(int length) {
+    _checkOpen();
+    _checkAsync();
+
+    if (length < 0 || !utils.isWriteMode(_mode)) {
+      throw io.FileSystemException(
+          'truncate failed', path, common.invalidArgument(path).osError);
+    }
+
+    final int oldLength = lengthSync();
+    if (length < oldLength) {
+      _node.truncate(length);
+
+      // [_position] is intentionally left untouched to match the observed
+      // behavior of [RandomAccessFile].
+    } else if (length > oldLength) {
+      _node.write(Uint8List(length - oldLength));
+    }
+    assert(lengthSync() == length);
+  }
+
+  @override
+  Future<io.RandomAccessFile> unlock([int start = 0, int end = -1]) async {
+    await _asyncWrapper(() => unlockSync(start, end));
+    return this;
+  }
+
+  @override
+  void unlockSync([int start = 0, int end = -1]) {
+    _checkOpen();
+    _checkAsync();
+    throw UnimplementedError('TODO');
+  }
+
+  @override
+  Future<io.RandomAccessFile> writeByte(int value) async {
+    await _asyncWrapper(() => writeByteSync(value));
+    return this;
+  }
+
+  @override
+  int writeByteSync(int value) {
+    _checkOpen();
+    _checkAsync();
+    _checkWritable('writeByte');
+
+    // [Uint8List] will truncate values to 8-bits automatically, so we don't
+    // need to check [value].
+
+    int length = lengthSync();
+    if (_position >= length) {
+      // If [_position] is out of bounds, [RandomAccessFile] zero-fills the
+      // file.
+      truncateSync(_position + 1);
+      length = lengthSync();
+    }
+    assert(_position < length);
+    _node.content[_position++] = value;
+
+    // Despite what the documentation states, [RandomAccessFile.writeByteSync]
+    // always seems to return 1, even if we had to extend the file for an out of
+    // bounds write.  See https://github.com/dart-lang/sdk/issues/42298.
+    return 1;
+  }
+
+  @override
+  Future<io.RandomAccessFile> writeFrom(
+    List<int> buffer, [
+    int start = 0,
+    int end,
+  ]) async {
+    await _asyncWrapper(() => writeFromSync(buffer, start, end));
+    return this;
+  }
+
+  @override
+  void writeFromSync(List<int> buffer, [int start = 0, int end]) {
+    _checkOpen();
+    _checkAsync();
+    _checkWritable('writeFrom');
+
+    end = RangeError.checkValidRange(start, end, buffer.length);
+
+    final int writeByteCount = end - start;
+    final int endPosition = _position + writeByteCount;
+
+    if (endPosition > lengthSync()) {
+      truncateSync(endPosition);
+    }
+
+    _node.content.setRange(_position, endPosition, buffer, start);
+    _position = endPosition;
+  }
+
+  @override
+  Future<io.RandomAccessFile> writeString(
+    String string, {
+    Encoding encoding = utf8,
+  }) async {
+    await _asyncWrapper(() => writeStringSync(string, encoding: encoding));
+    return this;
+  }
+
+  @override
+  void writeStringSync(String string, {Encoding encoding = utf8}) {
+    writeFromSync(encoding.encode(string));
+  }
+}
diff --git a/packages/file/lib/src/backends/memory/node.dart b/packages/file/lib/src/backends/memory/node.dart
index cc4ba14..e7a4029 100644
--- a/packages/file/lib/src/backends/memory/node.dart
+++ b/packages/file/lib/src/backends/memory/node.dart
@@ -255,6 +255,15 @@
     _content.setRange(existing.length, _content.length, bytes);
   }
 
+  /// Truncates this node's [content] to the specified length.
+  ///
+  /// [length] must be in the range \[0, [size]\].
+  void truncate(int length) {
+    assert(length >= 0);
+    assert(length <= _content.length);
+    _content = _content.sublist(0, length);
+  }
+
   /// Clears the [content] of the node.
   void clear() {
     _content = Uint8List(0);
diff --git a/packages/file/pubspec.yaml b/packages/file/pubspec.yaml
index 0486a25..77a45d7 100644
--- a/packages/file/pubspec.yaml
+++ b/packages/file/pubspec.yaml
@@ -1,5 +1,5 @@
 name: file
-version: 5.1.0
+version: 5.2.0
 authors:
 - Matan Lurey <matanl@google.com>
 - Yegor Jbanov <yjbanov@google.com>
diff --git a/packages/file/test/common_tests.dart b/packages/file/test/common_tests.dart
index 44c7712..ceeef74 100644
--- a/packages/file/test/common_tests.dart
+++ b/packages/file/test/common_tests.dart
@@ -1769,6 +1769,18 @@
               test('lengthIsResetToZeroIfOpened', () {
                 expect(raf.lengthSync(), equals(0));
               });
+
+              test('throwsIfAsyncUnawaited', () async {
+                try {
+                  final Future<void> future = raf.flush();
+                  expectFileSystemException(null, () => raf.flush());
+                  expectFileSystemException(null, () => raf.flushSync());
+                  await expectLater(future, completes);
+                  raf.flushSync();
+                } finally {
+                  raf.closeSync();
+                }
+              });
             } else {
               test('lengthIsNotModifiedIfOpened', () {
                 expect(raf.lengthSync(), isNot(equals(0)));
diff --git a/packages/file/test/memory_test.dart b/packages/file/test/memory_test.dart
index dfa76bf..3433ac7 100644
--- a/packages/file/test/memory_test.dart
+++ b/packages/file/test/memory_test.dart
@@ -2,7 +2,10 @@
 // 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:io' as io;
+
 import 'package:file/memory.dart';
+import 'package:file/src/backends/memory/memory_random_access_file.dart';
 import 'package:test/test.dart';
 
 import 'common_tests.dart';
@@ -15,12 +18,7 @@
       fs = MemoryFileSystem();
     });
 
-    runCommonTests(
-      () => fs,
-      skip: <String>[
-        'File > open', // Not yet implemented
-      ],
-    );
+    runCommonTests(() => fs);
 
     group('toString', () {
       test('File', () {
@@ -47,9 +45,6 @@
     runCommonTests(
       () => fs,
       root: () => fs.style.root,
-      skip: <String>[
-        'File > open', // Not yet implemented
-      ],
     );
 
     group('toString', () {
@@ -96,4 +91,23 @@
     expect(
         fs.file('/test2.txt').statSync().modified, DateTime(2000, 1, 1, 0, 6));
   });
+
+  test('MemoryFile.openSync returns a MemoryRandomAccessFile', () async {
+    final MemoryFileSystem fs = MemoryFileSystem.test();
+    final io.File file = fs.file('/test1')..createSync();
+
+    io.RandomAccessFile raf = file.openSync();
+    try {
+      expect(raf, isA<MemoryRandomAccessFile>());
+    } finally {
+      raf.closeSync();
+    }
+
+    raf = await file.open();
+    try {
+      expect(raf, isA<MemoryRandomAccessFile>());
+    } finally {
+      raf.closeSync();
+    }
+  });
 }