blob: 710d0d8685afa70b35cedfd95cdb259033b482c2 [file] [log] [blame]
// Copyright 2018 The Fuchsia Authors. 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:async';
import 'dart:math';
import 'dart:typed_data';
import 'package:fidl/fidl.dart' as fidl;
import 'package:fidl_fuchsia_io/fidl_async.dart';
import 'package:meta/meta.dart';
import 'package:zircon/zircon.dart';
import 'vnode.dart';
typedef WriteFn = int Function(Uint8List);
typedef WriteFnStr = int Function(String);
typedef ReadFn = Uint8List Function();
typedef ReadFnStr = String Function();
/// A [PseudoFile] is a file-like object whose content is generated and modified
/// dynamically on-the-fly by invoking handler functions rather than being
/// directly persisted as a sequence of bytes.
///
/// This class is designed to allow programs to publish read-only,
/// or read-write properties such as configuration options, debug flags,
/// and dumps of internal state which may change dynamically.
///
/// Although [PseudoFile] usually contain text, they can also be used for binary
/// data.
///
/// Read callback, is called when the connection to the file is opened and
/// pre-populates a buffer that will be used to when serving this file content
/// over this particular connection.
///
/// Write callback, if any, is called when the connection is closed if the file
/// content was ever modified while the connection was open.
/// Modifications are: [fuchsia.io.File#write()] calls or opening a file for
/// writing with the `openFlagTruncate` flag set.
class PseudoFile extends Vnode {
final int _capacity;
ReadFn _readFn;
WriteFn _writeFn;
bool _isClosed = false;
final List<_FileConnection> _connections = [];
/// Creates a new read-only [PseudoFile] backed by the specified read handler.
///
/// The handler is called every time a read operation is performed on the file. It is only allowed
/// to read at offset 0, and all of the content returned by the handler is returned by the read
/// operation. Subsequent reads act the same - there is no seek position, nor ability to read
/// content in chunks.
PseudoFile.readOnly(this._readFn)
: _capacity = 0,
assert(_readFn != null);
/// See [#readOnly()]. Wraps the callback, allowing it to return a String instead of a Uint8List,
/// but otherwise behaves identical to [#readOnly()].
PseudoFile.readOnlyStr(ReadFnStr fn)
: _capacity = 0,
assert(fn != null) {
_readFn = _getReadFn(fn);
}
/// Creates new [PseudoFile] backed by the specified read and write handlers.
///
/// The read handler is called every time a read operation is performed on the file. It is only
/// allowed to read at offset 0, and all of the content returned by the handler is returned by the
/// read operation. Subsequent reads act the same - there is no seek position, nor ability to read
/// content in chunks.
///
/// The write handler is called every time a write operation is performed on the file. It is only
/// allowed to write at offset 0, and all of the new content should be provided to a single write
/// operation. Subsequent writes act the same - there is no seek position, nor ability to write
/// content in chunks.
PseudoFile.readWrite(this._capacity, this._readFn, this._writeFn)
: assert(_writeFn != null),
assert(_readFn != null),
assert(_capacity > 0);
/// See [#readWrite()]. Wraps the read callback, allowing it to return a [String] instead of a
/// [Uint8List]. Wraps the write callback, only allowing valid UTF-8 content to be written into
/// the file. Written bytes are converted into a string instance, and the passed to the handler.
/// In every other aspect behaves just like [#readWrite()].
PseudoFile.readWriteStr(this._capacity, ReadFnStr rfn, WriteFnStr wfn)
: assert(_capacity > 0),
assert(rfn != null),
assert(wfn != null) {
_readFn = _getReadFn(rfn);
_writeFn = _getWriteFn(wfn);
}
/// Connects to this instance of [PseudoFile] and serves [fushsia.io.File] over fidl.
@override
int connect(int flags, int mode, fidl.InterfaceRequest<Node> request,
[int parentFlags = -1]) {
if (_isClosed) {
sendErrorEvent(flags, ZX.ERR_NOT_SUPPORTED, request);
return ZX.ERR_NOT_SUPPORTED;
}
// There should be no MODE_TYPE_* flags set, except for, possibly,
// MODE_TYPE_FILE when the target is a pseudo file.
if ((mode & ~modeProtectionMask) & ~modeTypeFile != 0) {
sendErrorEvent(flags, ZX.ERR_INVALID_ARGS, request);
return ZX.ERR_INVALID_ARGS;
}
var connectFlags = filterForNodeReference(flags);
var status = _validateFlags(parentFlags, connectFlags);
if (status != ZX.OK) {
sendErrorEvent(connectFlags, status, request);
return status;
}
var connection = _FileConnection(
capacity: _capacity,
flags: connectFlags,
file: this,
mode: mode,
request: fidl.InterfaceRequest<File>(request.passChannel()));
// [connection] will also send on_open success event.
_connections.add(connection);
return ZX.OK;
}
@override
int inodeNumber() {
return inoUnknown;
}
@override
int type() {
return direntTypeFile;
}
/// Return the description of this file.
/// This function may return null if describing the node fails. In that case, the connection should be closed.
NodeInfo describe() {
return NodeInfo.withFile(FileObject(event: null));
}
ReadFn _getReadFn(ReadFnStr fn) {
return () => Uint8List.fromList(fn().codeUnits);
}
WriteFn _getWriteFn(WriteFnStr fn) {
return (Uint8List buffer) => fn(String.fromCharCodes(buffer));
}
void _onClose(_FileConnection obj) {
assert(_connections.remove(obj));
}
int _validateFlags(int parentFlags, int flags) {
if (flags & openFlagDirectory != 0) {
return ZX.ERR_NOT_DIR;
}
var allowedFlags = openFlagDescribe |
openFlagNodeReference |
openFlagPosix |
cloneFlagSameRights;
if (_readFn != null) {
allowedFlags |= openRightReadable;
}
if (_writeFn != null) {
allowedFlags |= openRightWritable | openFlagTruncate;
}
// allowedFlags takes precedence over prohibited_flags.
var prohibitedFlags = openFlagAppend;
var flagsDependentOnParentFlags = [openRightReadable, openRightWritable];
for (var flag in flagsDependentOnParentFlags) {
if (flags & flag != 0 && parentFlags & flag == 0) {
return ZX.ERR_ACCESS_DENIED;
}
}
if (flags & prohibitedFlags != 0) {
return ZX.ERR_INVALID_ARGS;
}
if (flags & ~allowedFlags != 0) {
return ZX.ERR_NOT_SUPPORTED;
}
return ZX.OK;
}
@override
void close() {
_isClosed = true;
// schedule a task because if user closes this as soon as
// they open a connection, dart fidl binding throws exception due to
// event on this fidl.
scheduleMicrotask(() {
for (var c in _connections) {
c.closeBinding();
}
_connections.clear();
});
}
}
/// Implementation of fuchsia.io.File for pseudo file.
///
/// This class should not be used directly, but by [fuchsia_vfs.PseudoFile].
class _FileConnection extends File {
final FileBinding _binding = new FileBinding();
/// open file connection flags
final int flags;
/// open file mode
final int mode;
/// seek position in file.
int seekPos = 0;
/// file's maximum capacity.
int capacity;
/// current length of file.
int _currentLen = 0;
// TODO(CF-252): Implement a grow-able buffer.
/// buffer which stores file content
Uint8List _buffer = Uint8List(0);
/// true if client wrote to this file.
bool _wasWritten = false;
/// Reference to PsuedoFile's Vnode.
PseudoFile file;
bool _isClosed = false;
/// Constructor to init _FileConnection
_FileConnection({
@required this.flags,
@required this.mode,
@required this.capacity,
@required this.file,
@required fidl.InterfaceRequest<File> request,
}) : assert(file != null) {
if (file._writeFn != null) {
_buffer = Uint8List(capacity);
}
if (flags & openFlagTruncate != 0) {
// don't call read handler on truncate.
_wasWritten = true;
} else {
var readBuf = file._readFn();
_currentLen = readBuf.lengthInBytes;
if (_currentLen > capacity) {
capacity = _currentLen;
_buffer = Uint8List(capacity);
}
_buffer.setRange(0, _currentLen, readBuf);
}
_binding.bind(this, request);
_binding.whenClosed.then((_) => close());
}
void closeBinding() {
_binding.close();
_isClosed = true;
}
@override
Stream<File$OnOpen$Response> get onOpen {
if ((flags & openFlagDescribe) == 0) {
return null;
}
NodeInfo nodeInfo = _describe();
var d = File$OnOpen$Response(ZX.OK, nodeInfo);
return Stream.fromIterable([d]);
}
@override
Future<void> clone(int flags, fidl.InterfaceRequest<Node> object) async {
file.connect(flags, mode, object, this.flags);
}
@override
Future<int> close() async {
if (_isClosed) {
return ZX.OK;
}
var status = ZX.OK;
if (file._writeFn != null && _wasWritten) {
status = file._writeFn(_buffer.buffer.asUint8List(0, _currentLen));
}
// no more read/write operations should be possible
file._onClose(this);
_isClosed = true;
return status;
}
@override
Future<NodeInfo> describe() async {
return _describe();
}
@override
Future<File$GetAttr$Response> getAttr() async {
return File$GetAttr$Response(
ZX.OK,
NodeAttributes(
mode: modeTypeFile | modeProtectionMask,
id: inoUnknown,
contentSize: 0,
storageSize: 0,
linkCount: 1,
creationTime: 0,
modificationTime: 0));
}
@override
Future<File$GetFlags$Response> getFlags() async {
return File$GetFlags$Response(ZX.OK, flags);
}
@override
Future<File$GetBuffer$Response> getBuffer(int flags) async {
return File$GetBuffer$Response(ZX.OK, null);
}
@override
Future<File$Ioctl$Response> ioctl(
int opcode, int maxOut, List<Handle> handles, Uint8List in$) async {
return File$Ioctl$Response(ZX.ERR_NOT_SUPPORTED, [], Uint8List(0));
}
@override
Future<File$Read$Response> read(int count) async {
var response = _handleRead(count, seekPos);
if (response.s == ZX.OK) {
seekPos += response.data.length;
}
return response;
}
@override
Future<File$ReadAt$Response> readAt(int count, int offset) async {
var response = _handleRead(count, offset);
return File$ReadAt$Response(response.s, response.data);
}
@override
Future<File$Seek$Response> seek(int offset, SeekOrigin seek) async {
var calculatedOffset = offset;
switch (seek) {
case SeekOrigin.start:
calculatedOffset = offset;
break;
case SeekOrigin.current:
calculatedOffset = seekPos + offset;
break;
case SeekOrigin.end:
calculatedOffset = (_currentLen - 1) + offset;
break;
default:
return File$Seek$Response(ZX.ERR_INVALID_ARGS, 0);
}
if (calculatedOffset > _currentLen || calculatedOffset < 0) {
return File$Seek$Response(ZX.ERR_OUT_OF_RANGE, seekPos);
}
seekPos = calculatedOffset;
return File$Seek$Response(ZX.OK, seekPos);
}
@override
Future<int> setAttr(int flags, NodeAttributes attributes) async {
return ZX.ERR_NOT_SUPPORTED;
}
@override
Future<int> setFlags(int flags) async {
return ZX.ERR_NOT_SUPPORTED;
}
@override
Future<int> sync() async {
return ZX.ERR_NOT_SUPPORTED;
}
@override
Future<int> truncate(int length) async {
if ((flags & openRightWritable) == 0) {
return ZX.ERR_ACCESS_DENIED;
}
if (file._writeFn == null) {
return ZX.ERR_NOT_SUPPORTED;
}
if (length > _currentLen) {
return ZX.ERR_OUT_OF_RANGE;
}
_currentLen = length;
seekPos = min(seekPos, _currentLen);
_wasWritten = true;
return ZX.OK;
}
@override
Future<File$Write$Response> write(Uint8List data) async {
var response = _handleWrite(seekPos, data);
if (response.s == ZX.OK) {
seekPos += response.actual;
}
return response;
}
@override
Future<File$WriteAt$Response> writeAt(Uint8List data, int offset) async {
var response = _handleWrite(offset, data);
return File$WriteAt$Response(response.s, response.actual);
}
NodeInfo _describe() {
NodeInfo ret = file.describe();
if (ret == null) {
close();
}
return ret;
}
File$Read$Response _handleRead(int count, int offset) {
if ((flags & openRightReadable) == 0) {
return File$Read$Response(ZX.ERR_ACCESS_DENIED, Uint8List(0));
}
if (file._readFn == null) {
return File$Read$Response(ZX.ERR_NOT_SUPPORTED, Uint8List(0));
}
if (offset == _currentLen) {
return File$Read$Response(ZX.OK, Uint8List(0));
}
if (offset > _currentLen) {
return File$Read$Response(ZX.ERR_OUT_OF_RANGE, Uint8List(0));
}
var c = count;
if (count + offset > _currentLen) {
c = _currentLen - offset;
if (c < 0) {
c = 0;
}
}
var b = Uint8List.view(_buffer.buffer, offset, c);
return File$Read$Response(ZX.OK, b);
}
File$Write$Response _handleWrite(int offset, Uint8List data) {
if ((flags & openRightWritable) == 0) {
return File$Write$Response(ZX.ERR_ACCESS_DENIED, 0);
}
if (file._writeFn == null) {
return File$Write$Response(ZX.ERR_NOT_SUPPORTED, 0);
}
if (offset >= capacity) {
return File$Write$Response(ZX.ERR_OUT_OF_RANGE, 0);
}
if (offset > _currentLen) {
return File$Write$Response(ZX.ERR_OUT_OF_RANGE, 0);
}
var actual = min(data.length, capacity - offset);
_buffer.setRange(offset, offset + actual, data.getRange(0, actual));
_wasWritten = true;
_currentLen = offset + actual;
return File$Write$Response(ZX.OK, actual);
}
}