blob: 3998cb8d35da3e348bf4af9eee54fbbe9f4a7ac1 [file] [log] [blame]
// 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.
// @dart=2.10
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.path, 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');
}
}
@override
final String path;
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
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 _node.size;
}
@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();
// TODO(jamesderlin): Implement, https://github.com/google/file.dart/issues/140
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();
// TODO(jamesderlin): Implement, https://github.com/google/file.dart/issues/140
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));
}
}