[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;
+}