| // 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 = 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); |
| } |
| } |