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();
+ }
+ });
}