[dart-vfs] implement psuedo-dir.
CF-221: #progress
TEST=fx run-test-component fuchsia_vfs_package_unittests
Change-Id: I75719ed4569de1cba109cbaf2d06564a5910a6af
diff --git a/public/dart/fuchsia_vfs/BUILD.gn b/public/dart/fuchsia_vfs/BUILD.gn
index 384c15c..b6ca1e0 100644
--- a/public/dart/fuchsia_vfs/BUILD.gn
+++ b/public/dart/fuchsia_vfs/BUILD.gn
@@ -14,12 +14,14 @@
sources = [
"src/internal/_error_node.dart",
+ "src/pseudo_dir.dart",
"src/pseudo_file.dart",
"src/vnode.dart",
"vfs.dart",
]
deps = [
+ "//third_party/dart-pkg/pub/quiver",
"//topaz/public/dart/fidl",
"//zircon/public/fidl/fuchsia-io",
]
@@ -34,12 +36,12 @@
]
sources = [
+ "pseudo_dir_test.dart",
"pseudo_file_test.dart",
]
deps = [
":fuchsia_vfs",
- "//third_party/dart-pkg/pub/mockito",
"//third_party/dart-pkg/pub/test",
]
}
diff --git a/public/dart/fuchsia_vfs/lib/src/pseudo_dir.dart b/public/dart/fuchsia_vfs/lib/src/pseudo_dir.dart
new file mode 100644
index 0000000..8b8ce91
--- /dev/null
+++ b/public/dart/fuchsia_vfs/lib/src/pseudo_dir.dart
@@ -0,0 +1,411 @@
+// 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:collection';
+import 'dart:convert';
+import 'dart:typed_data';
+
+import 'package:fidl/fidl.dart' as fidl;
+import 'package:fidl_fuchsia_io/fidl_async.dart';
+import 'package:quiver/collection.dart';
+import 'package:zircon/zircon.dart';
+
+import 'vnode.dart';
+
+/// A [PseudoDir] is a directory-like object whose entries are constructed
+/// by a program at runtime. The client can lookup, enumerate, and watch these
+/// directory entries but it cannot create, remove, or rename them.
+///
+/// This class is designed to allow programs to publish a relatively small number
+/// of entries (up to a few hundreds) such as services, file-system roots,
+/// debugging [PseudoFile].
+///
+/// This version doesn't support watchers, should support watchers if needed.
+class PseudoDir extends Vnode {
+ final HashMap<String, _Entry> _entries = HashMap();
+ final AvlTreeSet<_Entry> _treeEntries =
+ AvlTreeSet(comparator: (v1, v2) => v1.nodeId.compareTo(v2.nodeId));
+ int _nextId = 1;
+ final List<_DirConnection> _connections = [];
+
+ /// Adds a directory entry associating the given [name] with [node].
+ /// It is ok to add the same Vnode multiple times with different names.
+ ///
+ /// Returns `ZX.OK` on success.
+ /// Returns `ZX.ERR_INVALID_ARGS` if name length is more than `maxFilename`.
+ /// Returns `ZX.ERR_ALREADY_EXISTS` if there is already a node with the
+ /// given name.
+ int addNode(String name, Vnode node) {
+ if (name.length > maxFilename) {
+ return ZX.ERR_INVALID_ARGS;
+ }
+ if (_entries.containsKey(name)) {
+ return ZX.ERR_ALREADY_EXISTS;
+ }
+ var id = _nextId++;
+ var e = _Entry(node, name, id);
+ _entries[name] = e;
+ _treeEntries.add(e);
+ return ZX.OK;
+ }
+
+ /// Connects to this instance of [PseudoDir] and serves
+ /// [fushsia.io.Directory] over fidl.
+ @override
+ int connect(int flags, int mode, fidl.InterfaceRequest<Node> request,
+ [int parentFlags = -1]) {
+ // There should be no modeType* flags set, except for, possibly,
+ // modeTypeDirectory when the target is a pseudo dir.
+ if ((mode & ~modeProtectionMask) & ~modeTypeDirectory != 0) {
+ sendErrorEvent(flags, ZX.ERR_INVALID_ARGS, request);
+ return ZX.ERR_INVALID_ARGS;
+ }
+
+ // ignore parentFlags as every directory is readable even if flag is not passed.
+ var status = _validateFlags(flags);
+ if (status != ZX.OK) {
+ sendErrorEvent(flags, status, request);
+ return status;
+ }
+ var connection = _DirConnection(
+ mode, flags, this, fidl.InterfaceRequest(request.passChannel()));
+
+ _connections.add(connection);
+ return ZX.OK;
+ }
+
+ @override
+ int inodeNumber() {
+ return inoUnknown;
+ }
+
+ /// Checks if directory is empty.
+ bool isEmpty() {
+ return _entries.isEmpty;
+ }
+
+ /// Returns names of the the nodes present in this directory.
+ List<String> listNodeNames() {
+ return _treeEntries.map((f) => f.name).toList();
+ }
+
+ /// Looks up a node for given `name`.
+ ///
+ /// Returns `null` if no node if found.
+ Vnode lookup(String name) {
+ var v = _entries[name];
+ if (v != null) {
+ return v.node;
+ }
+ return null;
+ }
+
+ @override
+ void open(
+ int flags, int mode, String path, fidl.InterfaceRequest<Node> request) {
+ var p = path.trim();
+ if (p.startsWith('/')) {
+ sendErrorEvent(flags, ZX.ERR_BAD_PATH, request);
+ return;
+ }
+ if (p == '' || p == '.') {
+ connect(flags, mode, request);
+ }
+ var index = p.indexOf('/');
+ var key = '';
+ if (index == -1) {
+ key = p;
+ } else {
+ key = p.substring(0, index);
+ }
+ if (_entries.containsKey(key)) {
+ var e = _entries[key];
+ // final element, open it
+ if (index == -1) {
+ e.node.connect(flags, mode, request);
+ } else if (index == p.length - 1) {
+ // '/' is at end, should be a directory, add flag
+ e.node.connect(flags | openFlagDirectory, mode, request);
+ } else {
+ // forward request to child Vnode and let it handle rest of path.
+ e.node.open(flags, mode, p.substring(index + 1), request);
+ }
+ }
+ }
+
+ /// Removes all directory entries.
+ void removeAllNodes() {
+ _entries.clear();
+ _treeEntries.clear();
+ }
+
+ /// Removes a directory entry with the given `name`.
+ ///
+ /// Returns `ZX.OK` on success.
+ /// Returns `ZX.RR_NOT_FOUND` if there is no node with the given name.
+ int removeNode(String name) {
+ var e = _entries.remove(name);
+ if (e == null) {
+ return ZX.ERR_NOT_FOUND;
+ }
+ _treeEntries.remove(e);
+ return ZX.OK;
+ }
+
+ @override
+ int type() {
+ return direntTypeDirectory;
+ }
+
+ void _onClose(_DirConnection obj) {
+ assert(_connections.remove(obj));
+ }
+
+ int _validateFlags(int flags) {
+ var allowedFlags = openRightReadable |
+ openFlagDirectory |
+ openFlagStatus |
+ openFlagDescribe;
+ var prohibitedFlags = openFlagCreate |
+ openFlagCreateIfAbsent |
+ openFlagTruncate |
+ openFlagAppend;
+
+ // Pseudo directories do not allow modifications or mounting, at this point.
+ if (flags & openRightWritable != 0 || flags & openRightAdmin != 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;
+ }
+}
+
+/// Implementation of fuchsia.io.Directory for pseudo directory.
+///
+/// This class should not be used directly, but by [fuchsia_vfs.PseudoDirectory].
+class _DirConnection extends Directory {
+ final DirectoryBinding _binding = DirectoryBinding();
+
+ // reference to current Directory object;
+ PseudoDir _dir;
+ int _mode;
+ int _flags;
+
+ /// Position in directory where [#readDirents] should start searching. If less
+ /// than 0, means first entry should be dot('.').
+ ///
+ /// All the entires in [PseudoDir] are greater then 0.
+ /// We will get key after `_seek` and traverse in the TreeMap.
+ int _seek = -1;
+
+ bool _closed = false;
+
+ /// Constructor
+ _DirConnection(this._mode, this._flags, this._dir,
+ fidl.InterfaceRequest<Directory> request)
+ : assert(_dir != null),
+ assert(request != null) {
+ _binding.bind(this, request);
+ _binding.whenClosed.then((_) {
+ return close();
+ });
+ }
+
+ @override
+ Stream<Directory$OnOpen$Response> get onOpen {
+ if (_flags & openFlagStatus != 0) {
+ NodeInfo nodeInfo;
+ if (_flags & openFlagDescribe != 0) {
+ nodeInfo = _describe();
+ }
+ var d = Directory$OnOpen$Response(ZX.OK, nodeInfo);
+ return Stream.fromIterable([d]);
+ }
+ return null;
+ }
+
+ @override
+ Future<void> clone(int flags, fidl.InterfaceRequest<Node> object) async {
+ _dir.connect(flags, _mode, object);
+ }
+
+ @override
+ Future<int> close() async {
+ if (_closed) {
+ return ZX.OK;
+ }
+ _dir._onClose(this);
+ _closed = true;
+
+ return ZX.OK;
+ }
+
+ @override
+ Future<NodeInfo> describe() async {
+ return _describe();
+ }
+
+ @override
+ Future<Directory$GetAttr$Response> getAttr() async {
+ var n = NodeAttributes(
+ mode: modeTypeDirectory | modeProtectionMask,
+ id: inoUnknown,
+ contentSize: 0,
+ storageSize: 0,
+ linkCount: 1,
+ creationTime: 0,
+ modificationTime: 0,
+ );
+ return Directory$GetAttr$Response(ZX.OK, n);
+ }
+
+ @override
+ Future<Directory$GetToken$Response> getToken() async {
+ return Directory$GetToken$Response(ZX.ERR_NOT_SUPPORTED, null);
+ }
+
+ @override
+ Future<Directory$Ioctl$Response> ioctl(
+ int opcode, int maxOut, List<Handle> handles, Uint8List in$) async {
+ return Directory$Ioctl$Response(ZX.ERR_NOT_SUPPORTED, null, null);
+ }
+
+ @override
+ Future<int> link(String src, Handle dstParentToken, String dst) async {
+ return ZX.ERR_NOT_SUPPORTED;
+ }
+
+ @override
+ Future<void> open(int flags, int mode, String path,
+ fidl.InterfaceRequest<Node> object) async {
+ _dir.open(flags, mode, path, object);
+ }
+
+ @override
+ Future<Directory$ReadDirents$Response> readDirents(int maxBytes) async {
+ var buf = Uint8List(maxBytes);
+ var bData = ByteData.view(buf.buffer);
+ var firstOne = true;
+ var index = 0;
+
+ // add dot
+ if (_seek < 0) {
+ var bytes = _encodeDirent(
+ bData, index, maxBytes, inoUnknown, direntTypeDirectory, '.');
+ if (bytes == -1) {
+ return Directory$ReadDirents$Response(
+ ZX.ERR_BUFFER_TOO_SMALL, Uint8List(0));
+ }
+ firstOne = false;
+ index += bytes;
+ _seek = 0;
+ }
+
+ var status = ZX.OK;
+
+ // add entries
+ var entry = _dir._treeEntries.nearest(_Entry(null, '', _seek),
+ nearestOption: TreeSearch.GREATER_THAN);
+
+ if (entry != null) {
+ var iterator = _dir._treeEntries.fromIterator(entry);
+ while (iterator.moveNext()) {
+ entry = iterator.current;
+ // we should only send entries > _seek
+ if (entry.nodeId <= _seek) {
+ continue;
+ }
+ var bytes = _encodeDirent(bData, index, maxBytes,
+ entry.node.inodeNumber(), entry.node.type(), entry.name);
+ if (bytes == -1) {
+ if (firstOne) {
+ status = ZX.ERR_BUFFER_TOO_SMALL;
+ }
+ break;
+ }
+ firstOne = false;
+ index += bytes;
+ status = ZX.OK;
+ _seek = entry.nodeId;
+ }
+ }
+ return Directory$ReadDirents$Response(
+ status, Uint8List.view(buf.buffer, 0, index));
+ }
+
+ @override
+ Future<int> rename(String src, Handle dstParentToken, String dst) async {
+ return ZX.ERR_NOT_SUPPORTED;
+ }
+
+ @override
+ Future<int> rewind() async {
+ _seek = -1;
+ return ZX.OK;
+ }
+
+ @override
+ Future<int> setAttr(int flags, NodeAttributes attributes) async {
+ return ZX.ERR_NOT_SUPPORTED;
+ }
+
+ @override
+ Future<int> sync() async {
+ return ZX.ERR_NOT_SUPPORTED;
+ }
+
+ @override
+ Future<int> unlink(String path) async {
+ return ZX.ERR_NOT_SUPPORTED;
+ }
+
+ @override
+ Future<int> watch(int mask, int options, Channel watcher) async {
+ return ZX.ERR_NOT_SUPPORTED;
+ }
+
+ NodeInfo _describe() {
+ return NodeInfo.withDirectory(DirectoryObject(reserved: 0));
+ }
+
+ /// returns number of bytes written
+ int _encodeDirent(ByteData buf, int startIndex, int maxBytes, int inodeNumber,
+ int type, String name) {
+ List<int> charBytes = utf8.encode(name);
+ var len = 8 /*ino*/ + 1 /*size*/ + 1 /*type*/ + charBytes.length;
+ // cannot fit in buffer
+ if (maxBytes - startIndex < len) {
+ return -1;
+ }
+ var index = startIndex;
+ buf.setUint64(index, inodeNumber, Endian.little);
+ index += 8;
+ buf..setUint8(index++, charBytes.length)..setUint8(index++, type);
+ for (int i = 0; i < charBytes.length; i++) {
+ buf.setUint8(index++, charBytes[i]);
+ }
+ return len;
+ }
+}
+
+/// _Entry class to store in pseudo directory.
+class _Entry {
+ /// Vnode
+ Vnode node;
+
+ /// node name
+ String name;
+
+ /// node id: defines insertion order
+ int nodeId;
+
+ /// Constructor
+ _Entry(this.node, this.name, this.nodeId);
+}
diff --git a/public/dart/fuchsia_vfs/lib/vfs.dart b/public/dart/fuchsia_vfs/lib/vfs.dart
index 34f1460..a328432 100644
--- a/public/dart/fuchsia_vfs/lib/vfs.dart
+++ b/public/dart/fuchsia_vfs/lib/vfs.dart
@@ -2,4 +2,6 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
+export 'src/pseudo_dir.dart';
export 'src/pseudo_file.dart';
+export 'src/vnode.dart';
diff --git a/public/dart/fuchsia_vfs/test/pseudo_dir_test.dart b/public/dart/fuchsia_vfs/test/pseudo_dir_test.dart
new file mode 100644
index 0000000..1cb211e
--- /dev/null
+++ b/public/dart/fuchsia_vfs/test/pseudo_dir_test.dart
@@ -0,0 +1,903 @@
+// 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:convert';
+import 'dart:typed_data';
+
+import 'package:fidl/fidl.dart';
+import 'package:fidl_fuchsia_io/fidl_async.dart';
+import 'package:fuchsia_vfs/vfs.dart';
+import 'package:test/test.dart';
+import 'package:zircon/zircon.dart';
+
+void main() {
+ group('pseudo dir: ', () {
+ test('inode number', () {
+ Vnode dir = PseudoDir();
+ expect(dir.inodeNumber(), inoUnknown);
+ });
+
+ test('type', () {
+ Vnode dir = PseudoDir();
+ expect(dir.type(), direntTypeDirectory);
+ });
+
+ test('basic', () {
+ PseudoDir dir = PseudoDir();
+ var key1 = 'key1';
+ var key2 = 'key2';
+
+ var node1 = _TestVnode();
+ expect(dir.addNode(key1, node1), ZX.OK);
+ expect(dir.lookup(key1), node1);
+
+ var node2 = _TestVnode();
+ expect(dir.addNode(key2, node2), ZX.OK);
+ expect(dir.lookup(key2), node2);
+
+ // make sure key1 is still there
+ expect(dir.lookup(key1), node1);
+ });
+
+ test('duplicate key', () {
+ PseudoDir dir = PseudoDir();
+ var key = 'key';
+ var node = _TestVnode();
+ var dupNode = _TestVnode();
+ expect(dir.addNode(key, node), ZX.OK);
+ expect(dir.addNode(key, dupNode), ZX.ERR_ALREADY_EXISTS);
+
+ // check that key was not replaced
+ expect(dir.lookup(key), node);
+ });
+
+ test('remove node', () {
+ PseudoDir dir = PseudoDir();
+ var key = 'key';
+ var node = _TestVnode();
+ expect(dir.addNode(key, node), ZX.OK);
+ expect(dir.lookup(key), node);
+
+ expect(dir.removeNode(key), ZX.OK);
+ expect(dir.lookup(key), null);
+
+ // add again and check
+ expect(dir.addNode(key, node), ZX.OK);
+ expect(dir.lookup(key), node);
+ });
+
+ test('remove when multiple keys', () {
+ PseudoDir dir = PseudoDir();
+ var key1 = 'key1';
+ var key2 = 'key2';
+ var node1 = _TestVnode();
+ var node2 = _TestVnode();
+ expect(dir.addNode(key1, node1), ZX.OK);
+ expect(dir.addNode(key2, node2), ZX.OK);
+ expect(dir.lookup(key1), node1);
+ expect(dir.lookup(key2), node2);
+
+ expect(dir.removeNode(key1), ZX.OK);
+ expect(dir.lookup(key1), null);
+
+ // check that key2 is still there
+ expect(dir.lookup(key2), node2);
+
+ // add again and check
+ expect(dir.addNode(key1, node1), ZX.OK);
+ expect(dir.lookup(key1), node1);
+ expect(dir.lookup(key2), node2);
+ });
+
+ test('key order is maintained', () {
+ PseudoDir dir = PseudoDir();
+ var key1 = 'key1';
+ var key2 = 'key2';
+ var key3 = 'key3';
+ var node1 = _TestVnode();
+ var node2 = _TestVnode();
+ var node3 = _TestVnode();
+ expect(dir.addNode(key1, node1), ZX.OK);
+ expect(dir.addNode(key2, node2), ZX.OK);
+ expect(dir.addNode(key3, node3), ZX.OK);
+
+ expect(dir.listNodeNames(), [key1, key2, key3]);
+
+ // order maintained after removing node
+ expect(dir.removeNode(key1), ZX.OK);
+ expect(dir.listNodeNames(), [key2, key3]);
+
+ // add again and check
+ expect(dir.addNode(key1, node1), ZX.OK);
+ expect(dir.listNodeNames(), [key2, key3, key1]);
+ });
+
+ test('remove and isEmpty', () {
+ PseudoDir dir = PseudoDir();
+ var key1 = 'key1';
+ var key2 = 'key2';
+ var key3 = 'key3';
+ var node1 = _TestVnode();
+ var node2 = _TestVnode();
+ var node3 = _TestVnode();
+ expect(dir.isEmpty(), true);
+ expect(dir.addNode(key1, node1), ZX.OK);
+ expect(dir.addNode(key2, node2), ZX.OK);
+ expect(dir.addNode(key3, node3), ZX.OK);
+ expect(dir.isEmpty(), false);
+
+ expect(dir.removeNode(key1), ZX.OK);
+ expect(dir.isEmpty(), false);
+ dir.removeAllNodes();
+ expect(dir.isEmpty(), true);
+ expect(dir.listNodeNames(), []);
+ // make sure that keys are really gone
+ expect(dir.lookup(key2), null);
+ expect(dir.lookup(key3), null);
+
+ // add again and check
+ expect(dir.addNode(key1, node1), ZX.OK);
+ expect(dir.isEmpty(), false);
+ expect(dir.lookup(key1), node1);
+ expect(dir.listNodeNames(), [key1]);
+ });
+ });
+
+ group('pseudo dir server: ', () {
+ group('open fails: ', () {
+ test('invalid flags', () async {
+ PseudoDir dir = PseudoDir();
+ var invalidFlags = [
+ openFlagAppend,
+ openFlagCreate,
+ openFlagCreateIfAbsent,
+ openFlagNodeReference,
+ openFlagNoRemote,
+ openFlagTruncate,
+ openRightAdmin,
+ openRightWritable
+ ];
+
+ var i = 0;
+ for (var flag in invalidFlags) {
+ DirectoryProxy proxy = DirectoryProxy();
+ var status = dir.connect(flag | openFlagStatus, 0,
+ InterfaceRequest(proxy.ctrl.request().passChannel()));
+ expect(status, isNot(ZX.OK), reason: 'flagIndex: $i');
+ i++;
+ await proxy.onOpen.first.then((response) {
+ expect(response.s, status);
+ expect(response.info, isNull);
+ }).catchError((err) async {
+ fail(err.toString());
+ });
+ }
+ });
+
+ test('invalid mode', () async {
+ PseudoDir dir = PseudoDir();
+ var invalidModes = [
+ modeTypeBlockDevice,
+ modeTypeFile,
+ modeTypeService,
+ modeTypeService,
+ modeTypeSocket
+ ];
+
+ var i = 0;
+ for (var mode in invalidModes) {
+ DirectoryProxy proxy = DirectoryProxy();
+ var status = dir.connect(openFlagStatus, mode,
+ InterfaceRequest(proxy.ctrl.request().passChannel()));
+ expect(status, ZX.ERR_INVALID_ARGS, reason: 'modeIndex: $i');
+ i++;
+ await proxy.onOpen.first.then((response) {
+ expect(response.s, status);
+ expect(response.info, isNull);
+ }).catchError((err) async {
+ fail(err.toString());
+ });
+ }
+ });
+ });
+
+ DirectoryProxy _getProxyForDir(PseudoDir dir, [int flags = 0]) {
+ DirectoryProxy proxy = DirectoryProxy();
+ var status = dir.connect(
+ flags, 0, InterfaceRequest(proxy.ctrl.request().passChannel()));
+ expect(status, ZX.OK);
+ return proxy;
+ }
+
+ test('open passes', () async {
+ PseudoDir dir = PseudoDir();
+ DirectoryProxy proxy = _getProxyForDir(dir, openFlagStatus);
+
+ await proxy.onOpen.first.then((response) {
+ expect(response.s, ZX.OK);
+ expect(response.info, isNull);
+ }).catchError((err) async {
+ fail(err.toString());
+ });
+ });
+
+ test('open passes with valid mode', () async {
+ PseudoDir dir = PseudoDir();
+ var validModes = [
+ modeProtectionMask,
+ modeTypeDirectory,
+ ];
+
+ var i = 0;
+ for (var mode in validModes) {
+ DirectoryProxy proxy = DirectoryProxy();
+ var status = dir.connect(openFlagStatus, mode,
+ InterfaceRequest(proxy.ctrl.request().passChannel()));
+ expect(status, ZX.OK, reason: 'modeIndex: $i');
+ i++;
+ await proxy.onOpen.first.then((response) {
+ expect(response.s, status);
+ expect(response.info, isNull);
+ }).catchError((err) async {
+ fail(err.toString());
+ });
+ }
+ });
+
+ test('open passes with valid flags', () async {
+ PseudoDir dir = PseudoDir();
+ var validFlags = [
+ openRightReadable,
+ openFlagDirectory,
+ openFlagStatus,
+ openFlagDescribe
+ ];
+
+ for (var flag in validFlags) {
+ DirectoryProxy proxy = _getProxyForDir(dir, flag | openFlagStatus);
+ await proxy.onOpen.first.then((response) {
+ expect(response.s, ZX.OK);
+ if (flag == openFlagDescribe) {
+ expect(response.info, isNotNull);
+ } else {
+ expect(response.info, isNull);
+ }
+ }).catchError((err) async {
+ fail(err.toString());
+ });
+ }
+ });
+
+ test('getattr', () async {
+ PseudoDir dir = PseudoDir();
+ DirectoryProxy proxy = _getProxyForDir(dir);
+
+ var attr = await proxy.getAttr();
+
+ expect(attr.attributes.linkCount, 1);
+ expect(attr.attributes.mode, modeProtectionMask | modeTypeDirectory);
+ });
+
+ _Dirent _createDirentForDot() {
+ return _Dirent(inoUnknown, 1, direntTypeDirectory, '.');
+ }
+
+ _Dirent _createDirent(Vnode vnode, String name) {
+ return _Dirent(vnode.inodeNumber(), name.length, vnode.type(), name);
+ }
+
+ int _expectedDirentSize(List<_Dirent> dirents) {
+ var sum = 0;
+ for (var d in dirents) {
+ sum += d.direntSizeInBytes;
+ }
+ return sum;
+ }
+
+ void _validateExpectedDirents(
+ List<_Dirent> dirents, Directory$ReadDirents$Response response) {
+ expect(response.s, ZX.OK);
+ expect(response.dirents.length, _expectedDirentSize(dirents));
+ var offset = 0;
+ for (var dirent in dirents) {
+ var data = ByteData.view(
+ response.dirents.buffer, response.dirents.offsetInBytes + offset);
+ var actualDirent = _Dirent.fromData(data);
+ expect(actualDirent, dirent);
+ offset += actualDirent.direntSizeInBytes;
+ }
+ }
+
+ group('read dir:', () {
+ test('simple call should work', () async {
+ PseudoDir dir = PseudoDir();
+ PseudoDir subDir = PseudoDir();
+ var file1 = PseudoFile.readOnlyStr(() => 'file1');
+ var file2 = PseudoFile.readOnlyStr(() => 'file2');
+ var file3 = PseudoFile.readOnlyStr(() => 'file3');
+ dir
+ ..addNode('file1', file1)
+ ..addNode('subDir', subDir)
+ ..addNode('file3', file3);
+ subDir.addNode('file2', file2);
+
+ DirectoryProxy proxy = _getProxyForDir(dir);
+
+ var expectedDirents = [
+ _createDirentForDot(),
+ _createDirent(file1, 'file1'),
+ _createDirent(subDir, 'subDir'),
+ _createDirent(file3, 'file3'),
+ ];
+ var response = await proxy.readDirents(1024);
+ _validateExpectedDirents(expectedDirents, response);
+
+ // test that next read call returns length zero buffer
+ response = await proxy.readDirents(1024);
+ expect(response.s, ZX.OK);
+ expect(response.dirents.length, 0);
+
+ // also test sub folder and make sure it was not affected by parent dir.
+ proxy = _getProxyForDir(subDir);
+ expectedDirents = [
+ _createDirentForDot(),
+ _createDirent(file2, 'file2'),
+ ];
+ response = await proxy.readDirents(1024);
+ _validateExpectedDirents(expectedDirents, response);
+ });
+
+ test('passed buffer size is exact', () async {
+ PseudoDir dir = PseudoDir();
+ PseudoDir subDir = PseudoDir();
+ var file1 = PseudoFile.readOnlyStr(() => 'file1');
+ var file3 = PseudoFile.readOnlyStr(() => 'file3');
+ dir
+ ..addNode('file1', file1)
+ ..addNode('subDir', subDir)
+ ..addNode('file3', file3);
+ var proxy = _getProxyForDir(dir);
+
+ var expectedDirents = [
+ _createDirentForDot(),
+ _createDirent(file1, 'file1'),
+ _createDirent(subDir, 'subDir'),
+ _createDirent(file3, 'file3'),
+ ];
+ var response =
+ await proxy.readDirents(_expectedDirentSize(expectedDirents));
+ _validateExpectedDirents(expectedDirents, response);
+
+ // test that next read call returns length zero buffer
+ response = await proxy.readDirents(1024);
+ expect(response.s, ZX.OK);
+ expect(response.dirents.length, 0);
+ });
+
+ test('passed buffer size is exact - 1', () async {
+ PseudoDir dir = PseudoDir();
+ PseudoDir subDir = PseudoDir();
+ var file1 = PseudoFile.readOnlyStr(() => 'file1');
+ var file3 = PseudoFile.readOnlyStr(() => 'file3');
+ dir
+ ..addNode('file1', file1)
+ ..addNode('subDir', subDir)
+ ..addNode('file3', file3);
+
+ var proxy = _getProxyForDir(dir);
+
+ var expectedDirents = [
+ _createDirentForDot(),
+ _createDirent(file1, 'file1'),
+ _createDirent(subDir, 'subDir'),
+ _createDirent(file3, 'file3'),
+ ];
+ var response =
+ await proxy.readDirents(_expectedDirentSize(expectedDirents) - 1);
+ var lastDirent = expectedDirents.removeLast();
+ _validateExpectedDirents(expectedDirents, response);
+
+ // test that next read call returns last dirent
+ response = await proxy.readDirents(1024);
+ _validateExpectedDirents([lastDirent], response);
+
+ // test that next read call returns length zero buffer
+ response = await proxy.readDirents(1024);
+ expect(response.s, ZX.OK);
+ expect(response.dirents.length, 0);
+ });
+
+ test('buffer too small', () async {
+ PseudoDir dir = PseudoDir();
+ PseudoDir subDir = PseudoDir();
+ var file1 = PseudoFile.readOnlyStr(() => 'file1');
+ dir..addNode('file1', file1)..addNode('subDir', subDir);
+
+ var proxy = _getProxyForDir(dir);
+
+ var size = _expectedDirentSize([_createDirentForDot()]) - 1;
+ for (int i = 0; i < size; i++) {
+ var response = await proxy.readDirents(i);
+ expect(response.s, ZX.ERR_BUFFER_TOO_SMALL);
+ expect(response.dirents.length, 0);
+ }
+ });
+
+ test(
+ 'buffer too small after first dot read and subsequent reads with bigger buffer returns correct dirents',
+ () async {
+ PseudoDir dir = PseudoDir();
+ PseudoDir subDir = PseudoDir();
+ var file1 = PseudoFile.readOnlyStr(() => 'file1');
+ dir..addNode('file1', file1)..addNode('subDir', subDir);
+
+ var proxy = _getProxyForDir(dir);
+ var size = _expectedDirentSize([_createDirentForDot()]);
+ var response = await proxy.readDirents(size);
+
+ // make sure that '.' was read
+ _validateExpectedDirents([_createDirentForDot()], response);
+
+ // this should return error
+ response = await proxy.readDirents(size);
+ expect(response.s, ZX.ERR_BUFFER_TOO_SMALL);
+ expect(response.dirents.length, 0);
+
+ var expectedDirents = [
+ _createDirent(file1, 'file1'),
+ _createDirent(subDir, 'subDir'),
+ ];
+ response =
+ await proxy.readDirents(_expectedDirentSize(expectedDirents));
+ _validateExpectedDirents(expectedDirents, response);
+ });
+
+ test('multiple reads with small buffer', () async {
+ PseudoDir dir = PseudoDir();
+ PseudoDir subDir = PseudoDir();
+ var file1 = PseudoFile.readOnlyStr(() => 'file1');
+ dir..addNode('file1', file1)..addNode('subDir', subDir);
+
+ var proxy = _getProxyForDir(dir);
+ var expectedDirents = [
+ _createDirentForDot(),
+ _createDirent(file1, 'file1'),
+ _createDirent(subDir, 'subDir'),
+ ];
+ for (var dirent in expectedDirents) {
+ var dirents = [dirent];
+ var response = await proxy.readDirents(_expectedDirentSize(dirents));
+ _validateExpectedDirents(dirents, response);
+ }
+
+ // test that next read call returns length zero buffer
+ var response = await proxy.readDirents(1024);
+ expect(response.s, ZX.OK);
+ expect(response.dirents.length, 0);
+ });
+
+ test('read two dirents then one', () async {
+ PseudoDir dir = PseudoDir();
+ PseudoDir subDir = PseudoDir();
+ var file1 = PseudoFile.readOnlyStr(() => 'file1');
+ dir..addNode('file1', file1)..addNode('subDir', subDir);
+
+ var proxy = _getProxyForDir(dir);
+
+ var expectedDirents = [
+ _createDirentForDot(),
+ _createDirent(file1, 'file1'),
+ ];
+
+ var response =
+ await proxy.readDirents(_expectedDirentSize(expectedDirents));
+ _validateExpectedDirents(expectedDirents, response);
+
+ expectedDirents = [
+ _createDirent(subDir, 'subDir'),
+ ];
+
+ response = await proxy.readDirents(1024);
+ _validateExpectedDirents(expectedDirents, response);
+ });
+
+ test('buffer size more than first less than 2 dirents', () async {
+ PseudoDir dir = PseudoDir();
+ PseudoDir subDir = PseudoDir();
+ var file1 = PseudoFile.readOnlyStr(() => 'file1');
+ dir..addNode('file1', file1)..addNode('subDir', subDir);
+
+ var proxy = _getProxyForDir(dir);
+
+ var expectedDirents = [
+ _createDirentForDot(),
+ ];
+
+ var response =
+ await proxy.readDirents(_expectedDirentSize(expectedDirents) + 10);
+ _validateExpectedDirents(expectedDirents, response);
+
+ // now test that we are able to get rest
+ expectedDirents = [
+ _createDirent(file1, 'file1'),
+ _createDirent(subDir, 'subDir'),
+ ];
+
+ response = await proxy.readDirents(1024);
+ _validateExpectedDirents(expectedDirents, response);
+ });
+
+ test('rewind works', () async {
+ PseudoDir dir = PseudoDir();
+ PseudoDir subDir = PseudoDir();
+ var file1 = PseudoFile.readOnlyStr(() => 'file1');
+ dir..addNode('file1', file1)..addNode('subDir', subDir);
+
+ var proxy = _getProxyForDir(dir);
+
+ var expectedDirents = [
+ _createDirentForDot(),
+ ];
+
+ var response =
+ await proxy.readDirents(_expectedDirentSize(expectedDirents) + 10);
+ _validateExpectedDirents(expectedDirents, response);
+
+ var rewindResponse = await proxy.rewind();
+ expect(rewindResponse, ZX.OK);
+
+ response =
+ await proxy.readDirents(_expectedDirentSize(expectedDirents) + 10);
+ _validateExpectedDirents(expectedDirents, response);
+ });
+
+ test('rewind works after we reach directory end', () async {
+ PseudoDir dir = PseudoDir();
+ PseudoDir subDir = PseudoDir();
+ var file1 = PseudoFile.readOnlyStr(() => 'file1');
+ dir..addNode('file1', file1)..addNode('subDir', subDir);
+
+ var proxy = _getProxyForDir(dir);
+
+ var expectedDirents = [
+ _createDirentForDot(),
+ _createDirent(file1, 'file1'),
+ _createDirent(subDir, 'subDir'),
+ ];
+
+ var response =
+ await proxy.readDirents(_expectedDirentSize(expectedDirents) + 10);
+ _validateExpectedDirents(expectedDirents, response);
+
+ var rewindResponse = await proxy.rewind();
+ expect(rewindResponse, ZX.OK);
+
+ response =
+ await proxy.readDirents(_expectedDirentSize(expectedDirents) + 10);
+ _validateExpectedDirents(expectedDirents, response);
+ });
+
+ test('readdir works when node removed', () async {
+ PseudoDir dir = PseudoDir();
+ PseudoDir subDir = PseudoDir();
+ var file1 = PseudoFile.readOnlyStr(() => 'file1');
+ dir..addNode('file1', file1)..addNode('subDir', subDir);
+
+ var proxy = _getProxyForDir(dir);
+
+ var expectedDirents = [
+ _createDirentForDot(),
+ ];
+
+ var response =
+ await proxy.readDirents(_expectedDirentSize(expectedDirents));
+ _validateExpectedDirents(expectedDirents, response);
+
+ // remove first node
+ dir.removeNode('file1');
+ expectedDirents = [_createDirent(subDir, 'subDir')];
+ response = await proxy.readDirents(1024);
+ _validateExpectedDirents(expectedDirents, response);
+ });
+
+ test('readdir works when already last node is removed', () async {
+ PseudoDir dir = PseudoDir();
+ PseudoDir subDir = PseudoDir();
+ var file1 = PseudoFile.readOnlyStr(() => 'file1');
+ dir..addNode('file1', file1)..addNode('subDir', subDir);
+
+ var proxy = _getProxyForDir(dir);
+
+ var expectedDirents = [
+ _createDirentForDot(),
+ _createDirent(file1, 'file1')
+ ];
+
+ var response =
+ await proxy.readDirents(_expectedDirentSize(expectedDirents));
+ _validateExpectedDirents(expectedDirents, response);
+
+ // remove first node
+ dir.removeNode('file1');
+ expectedDirents = [_createDirent(subDir, 'subDir')];
+ response = await proxy.readDirents(1024);
+ _validateExpectedDirents(expectedDirents, response);
+ });
+
+ test('readdir works when new node is added', () async {
+ PseudoDir dir = PseudoDir();
+ PseudoDir subDir = PseudoDir();
+ var file1 = PseudoFile.readOnlyStr(() => 'file1');
+ dir..addNode('file1', file1)..addNode('subDir', subDir);
+
+ var proxy = _getProxyForDir(dir);
+
+ var expectedDirents = [
+ _createDirentForDot(),
+ _createDirent(file1, 'file1'),
+ _createDirent(subDir, 'subDir')
+ ];
+
+ var response =
+ await proxy.readDirents(_expectedDirentSize(expectedDirents));
+ _validateExpectedDirents(expectedDirents, response);
+
+ dir.addNode('file2', file1);
+ expectedDirents = [_createDirent(file1, 'file2')];
+ response = await proxy.readDirents(1024);
+ _validateExpectedDirents(expectedDirents, response);
+ });
+
+ test('readdir works when new node is added and only first node was read',
+ () async {
+ PseudoDir dir = PseudoDir();
+ PseudoDir subDir = PseudoDir();
+ var file1 = PseudoFile.readOnlyStr(() => 'file1');
+ dir..addNode('file1', file1)..addNode('subDir', subDir);
+
+ var proxy = _getProxyForDir(dir);
+
+ var expectedDirents = [
+ _createDirentForDot(),
+ ];
+
+ var response =
+ await proxy.readDirents(_expectedDirentSize(expectedDirents));
+ _validateExpectedDirents(expectedDirents, response);
+
+ dir.addNode('file2', file1);
+ expectedDirents = [
+ _createDirent(file1, 'file1'),
+ _createDirent(subDir, 'subDir'),
+ _createDirent(file1, 'file2')
+ ];
+ response = await proxy.readDirents(1024);
+ _validateExpectedDirents(expectedDirents, response);
+ });
+ });
+
+ group('open file/dir in dir:', () {
+ Future<void> _openFileAndAssert(DirectoryProxy proxy, String filePath,
+ int bufferLen, String expectedContent) async {
+ FileProxy fileProxy = FileProxy();
+ await proxy.open(openRightReadable, 0, filePath,
+ InterfaceRequest(fileProxy.ctrl.request().passChannel()));
+
+ var readResonse = await fileProxy.read(bufferLen);
+ expect(readResonse.s, ZX.OK);
+ expect(String.fromCharCodes(readResonse.data), expectedContent);
+ }
+
+ PseudoDir _setUpDir() {
+ PseudoDir dir = PseudoDir();
+ PseudoDir subDir = PseudoDir();
+ var file1 = PseudoFile.readOnlyStr(() => 'file1');
+ var file2 = PseudoFile.readOnlyStr(() => 'file2');
+ var file3 = PseudoFile.readOnlyStr(() => 'file3');
+ var file4 = PseudoFile.readOnlyStr(() => 'file4');
+ dir
+ ..addNode('file1', file1)
+ ..addNode('subDir', subDir)
+ ..addNode('file3', file3);
+ subDir..addNode('file2', file2)..addNode('file4', file4);
+ return dir;
+ }
+
+ test('open self', () async {
+ PseudoDir dir = _setUpDir();
+
+ var proxy = _getProxyForDir(dir);
+ var paths = ['.', ''];
+ for (var path in paths) {
+ DirectoryProxy newProxy = DirectoryProxy();
+ await proxy.open(0, 0, path,
+ InterfaceRequest(newProxy.ctrl.request().passChannel()));
+
+ // open file 1 in proxy and check contents to make sure correct dir was opened.
+ await _openFileAndAssert(newProxy, 'file1', 100, 'file1');
+ }
+ });
+
+ test('open file', () async {
+ PseudoDir dir = _setUpDir();
+
+ var proxy = _getProxyForDir(dir);
+
+ // open file 1 check contents.
+ await _openFileAndAssert(proxy, 'file1', 100, 'file1');
+ });
+
+ test('open file fails for path ending with "/"', () async {
+ PseudoDir dir = _setUpDir();
+
+ var proxy = _getProxyForDir(dir);
+
+ FileProxy fileProxy = FileProxy();
+ await proxy.open(openRightReadable | openFlagStatus, 0, 'file1/',
+ InterfaceRequest(fileProxy.ctrl.request().passChannel()));
+
+ await fileProxy.onOpen.first.then((response) {
+ expect(response.s, ZX.ERR_NOT_DIR);
+ expect(response.info, isNull);
+ }).catchError((err) async {
+ fail(err.toString());
+ });
+ });
+
+ test('open fails for trying to open file within a file', () async {
+ PseudoDir dir = _setUpDir();
+
+ var proxy = _getProxyForDir(dir);
+
+ FileProxy fileProxy = FileProxy();
+ await proxy.open(openRightReadable | openFlagStatus, 0, 'file1/file2',
+ InterfaceRequest(fileProxy.ctrl.request().passChannel()));
+
+ await fileProxy.onOpen.first.then((response) {
+ expect(response.s, ZX.ERR_NOT_DIR);
+ expect(response.info, isNull);
+ }).catchError((err) async {
+ fail(err.toString());
+ });
+ });
+
+ test('open sub dir', () async {
+ PseudoDir dir = _setUpDir();
+
+ var proxy = _getProxyForDir(dir);
+
+ DirectoryProxy dirProxy = DirectoryProxy();
+ await proxy.open(0, 0, 'subDir',
+ InterfaceRequest(dirProxy.ctrl.request().passChannel()));
+
+ // open file 2 check contents to make sure correct dir was opened.
+ await _openFileAndAssert(dirProxy, 'file2', 100, 'file2');
+ });
+
+ test('open sub dir with "/" at end', () async {
+ PseudoDir dir = _setUpDir();
+
+ var proxy = _getProxyForDir(dir);
+
+ DirectoryProxy dirProxy = DirectoryProxy();
+ await proxy.open(0, 0, 'subDir/',
+ InterfaceRequest(dirProxy.ctrl.request().passChannel()));
+
+ // open file 2 check contents to make sure correct dir was opened.
+ await _openFileAndAssert(dirProxy, 'file2', 100, 'file2');
+ });
+
+ test('directly open file in sub dir', () async {
+ PseudoDir dir = _setUpDir();
+
+ var proxy = _getProxyForDir(dir);
+
+ // open file 4 in subDir.
+ await _openFileAndAssert(proxy, 'subDir/file2', 100, 'file2');
+ });
+ });
+
+ test('test clone', () async {
+ PseudoDir dir = PseudoDir();
+
+ var proxy = _getProxyForDir(dir, openFlagStatus);
+
+ DirectoryProxy newProxy = DirectoryProxy();
+ await proxy.clone(openFlagStatus,
+ InterfaceRequest(newProxy.ctrl.request().passChannel()));
+
+ await newProxy.onOpen.first.then((response) {
+ expect(response.s, ZX.OK);
+ expect(response.info, isNull);
+ }).catchError((err) async {
+ fail(err.toString());
+ });
+ });
+
+ test('test clone fails for invalid flags', () async {
+ PseudoDir dir = PseudoDir();
+
+ var proxy = _getProxyForDir(dir, openFlagStatus);
+
+ DirectoryProxy newProxy = DirectoryProxy();
+ await proxy.clone(openFlagTruncate | openFlagStatus,
+ InterfaceRequest(newProxy.ctrl.request().passChannel()));
+
+ await newProxy.onOpen.first.then((response) {
+ expect(response.s, isNot(ZX.OK));
+ expect(response.info, isNull);
+ }).catchError((err) async {
+ fail(err.toString());
+ });
+ });
+ });
+}
+
+class _Dirent {
+ static const int _fixedSize = 10;
+ int ino;
+ int size;
+ int type;
+ String str;
+
+ int direntSizeInBytes;
+ _Dirent(this.ino, this.size, this.type, this.str) {
+ direntSizeInBytes = _fixedSize + size;
+ }
+
+ _Dirent.fromData(ByteData data) {
+ ino = data.getUint64(0, Endian.little);
+ size = data.getUint8(8);
+ type = data.getUint8(9);
+ var offset = _fixedSize;
+ List<int> charBytes = [];
+ direntSizeInBytes = offset + size;
+ expect(data.lengthInBytes, greaterThanOrEqualTo(direntSizeInBytes));
+ for (int i = 0; i < size; i++) {
+ charBytes.add(data.getUint8(offset++));
+ }
+ str = utf8.decode(charBytes);
+ }
+
+ @override
+ int get hashCode =>
+ ino.hashCode + size.hashCode + type.hashCode + str.hashCode;
+
+ @override
+ bool operator ==(Object o) {
+ return o is _Dirent &&
+ o.ino == ino &&
+ o.size == size &&
+ o.type == type &&
+ o.str == str;
+ }
+
+ @override
+ String toString() {
+ return '[ino: $ino, size: $size, type: $type, str: $str]';
+ }
+}
+
+class _TestVnode extends Vnode {
+ String _val;
+ _TestVnode([this._val = '']);
+
+ @override
+ int connect(int flags, int mode, InterfaceRequest<Node> request,
+ [int parentFlags]) {
+ throw UnimplementedError();
+ }
+
+ @override
+ int inodeNumber() {
+ return inoUnknown;
+ }
+
+ @override
+ int type() {
+ return direntTypeUnknown;
+ }
+
+ String value() => _val;
+}