| // 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: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(), DirentType.directory); |
| }); |
| |
| test('basic', () { |
| PseudoDir dir = PseudoDir(); |
| const key1 = 'key1'; |
| const key2 = 'key2'; |
| |
| final node1 = _TestVnode(); |
| expect(dir.addNode(key1, node1), ZX.OK); |
| expect(dir.lookup(key1), node1); |
| |
| final 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('legal name', () { |
| PseudoDir dir = PseudoDir(); |
| const maxObjectNameLength = 255; |
| StringBuffer specialsNonPrintablesBuilder = StringBuffer(); |
| // null character is illegal, start at one |
| for (int char = 1; char < maxObjectNameLength; char++) { |
| if (char != 47 /* key seperator */) { |
| specialsNonPrintablesBuilder.writeCharCode(char); |
| } |
| } |
| final legalKeys = <String>[ |
| 'k', |
| 'key', |
| 'longer_key', |
| // dart linter prefers interpolation over cat |
| 'just_shy_of_max_key${'_' * 236}', |
| '.prefix_is_independently_illegal', |
| '..prefix_is_independently_illegal', |
| 'suffix_is_independetly_illegal.', |
| 'suffix_is_independetly_illegal..', |
| 'infix_is_._independetly_illegal', |
| 'infix_is_.._independetly_illegal', |
| '...', |
| '....', |
| 'space is legal', |
| 'which\tmakes\tme\tuncomfortable', |
| 'very\nuncomfortable', |
| 'numbers_0123456789', |
| specialsNonPrintablesBuilder.toString(), |
| ]; |
| for (final key in legalKeys) { |
| final node = _TestVnode(); |
| expect(dir.addNode(key, node), ZX.OK); |
| expect(dir.lookup(key), node); |
| } |
| }); |
| |
| test('illegal name', () { |
| PseudoDir dir = PseudoDir(); |
| final illegalKeys = <String>[ |
| '', |
| 'illegal_length_key${'_' * 238}', |
| '.', |
| '..', |
| '\u{00}', |
| 'null_\u{00}_character', |
| '/', |
| 'key_/_seperator', |
| ]; |
| final node = _TestVnode(); |
| for (final key in illegalKeys) { |
| expect(dir.addNode(key, node), ZX.ERR_INVALID_ARGS); |
| } |
| }); |
| |
| test('duplicate key', () { |
| PseudoDir dir = PseudoDir(); |
| const key = 'key'; |
| final node = _TestVnode(); |
| final 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(); |
| const key = 'key'; |
| final 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(); |
| const key1 = 'key1'; |
| const key2 = 'key2'; |
| final node1 = _TestVnode(); |
| final 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(); |
| const key1 = 'key1'; |
| const key2 = 'key2'; |
| const key3 = 'key3'; |
| final node1 = _TestVnode(); |
| final node2 = _TestVnode(); |
| final 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(); |
| const key1 = 'key1'; |
| const key2 = 'key2'; |
| const key3 = 'key3'; |
| final node1 = _TestVnode(); |
| final node2 = _TestVnode(); |
| final 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(); |
| final invalidFlags = [ |
| OpenFlags.append, |
| OpenFlags.create, |
| OpenFlags.createIfAbsent, |
| OpenFlags.truncate, |
| ]; |
| for (final entry in invalidFlags.asMap().entries) { |
| DirectoryProxy proxy = DirectoryProxy(); |
| final status = dir.connect(entry.value | OpenFlags.describe, 0, |
| InterfaceRequest(proxy.ctrl.request().passChannel())); |
| expect(status, isNot(ZX.OK), reason: 'flagIndex: ${entry.key}'); |
| 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(); |
| final invalidModes = [ |
| modeTypeBlockDevice, |
| modeTypeFile, |
| modeTypeService, |
| modeTypeService, |
| modeTypeSocket |
| ]; |
| |
| for (final entry in invalidModes.asMap().entries) { |
| DirectoryProxy proxy = DirectoryProxy(); |
| final status = dir.connect(OpenFlags.describe, entry.value, |
| InterfaceRequest(proxy.ctrl.request().passChannel())); |
| expect(status, ZX.ERR_INVALID_ARGS, |
| reason: 'modeIndex: ${entry.key}'); |
| await proxy.onOpen.first.then((response) { |
| expect(response.s, status); |
| expect(response.info, isNull); |
| }).catchError((err) async { |
| fail(err.toString()); |
| }); |
| } |
| }); |
| }); |
| |
| DirectoryProxy _getProxyForDir(PseudoDir dir, [OpenFlags? flags]) { |
| DirectoryProxy proxy = DirectoryProxy(); |
| final status = dir.connect( |
| flags ?? OpenFlags.rightReadable | OpenFlags.rightWritable, |
| 0, |
| InterfaceRequest(proxy.ctrl.request().passChannel())); |
| expect(status, ZX.OK); |
| return proxy; |
| } |
| |
| test('open passes', () async { |
| PseudoDir dir = PseudoDir(); |
| DirectoryProxy proxy = |
| _getProxyForDir(dir, OpenFlags.rightReadable | OpenFlags.describe); |
| |
| await proxy.onOpen.first.then((response) { |
| expect(response.s, ZX.OK); |
| expect(response.info, isNotNull); |
| }).catchError((err) async { |
| fail(err.toString()); |
| }); |
| }); |
| |
| test('open passes with valid mode', () async { |
| PseudoDir dir = PseudoDir(); |
| final validModes = [ |
| modeProtectionMask, |
| modeTypeDirectory, |
| ]; |
| |
| for (final entry in validModes.asMap().entries) { |
| DirectoryProxy proxy = DirectoryProxy(); |
| final status = dir.connect(OpenFlags.describe, entry.value, |
| InterfaceRequest(proxy.ctrl.request().passChannel())); |
| expect(status, ZX.OK, reason: 'modeIndex: ${entry.key}'); |
| await proxy.onOpen.first.then((response) { |
| expect(response.s, ZX.OK); |
| expect(response.info, isNotNull); |
| }).catchError((err) async { |
| fail(err.toString()); |
| }); |
| } |
| }); |
| |
| test('open passes with valid flags', () async { |
| PseudoDir dir = PseudoDir(); |
| final validFlags = [ |
| OpenFlags.rightReadable, |
| OpenFlags.rightWritable, |
| OpenFlags.rightReadable | OpenFlags.directory, |
| OpenFlags.nodeReference |
| ]; |
| |
| for (final flag in validFlags) { |
| DirectoryProxy proxy = _getProxyForDir(dir, flag | OpenFlags.describe); |
| await proxy.onOpen.first.then((response) { |
| expect(response.s, ZX.OK); |
| expect(response.info, isNotNull); |
| }).catchError((err) async { |
| fail(err.toString()); |
| }); |
| } |
| }); |
| |
| test('getattr', () async { |
| PseudoDir dir = PseudoDir(); |
| DirectoryProxy proxy = _getProxyForDir(dir); |
| |
| final attr = await proxy.getAttr(); |
| |
| expect(attr.attributes.linkCount, 1); |
| expect(attr.attributes.mode, modeProtectionMask | modeTypeDirectory); |
| }); |
| |
| _Dirent _createDirentForDot() { |
| return _Dirent(inoUnknown, 1, DirentType.directory, '.'); |
| } |
| |
| _Dirent _createDirent(Vnode vnode, String name) { |
| return _Dirent(vnode.inodeNumber(), name.length, vnode.type(), name); |
| } |
| |
| int _expectedDirentSize(List<_Dirent> dirents) { |
| var sum = 0; |
| for (final 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 (final dirent in dirents) { |
| final data = ByteData.view( |
| response.dirents.buffer, response.dirents.offsetInBytes + offset); |
| final 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(); |
| final file1 = PseudoFile.readOnlyStr(() => 'file1'); |
| final file2 = PseudoFile.readOnlyStr(() => 'file2'); |
| final file3 = PseudoFile.readOnlyStr(() => 'file3'); |
| dir |
| ..addNode('file1', file1) |
| ..addNode('subDir', subDir) |
| ..addNode('file3', file3); |
| subDir.addNode('file2', file2); |
| |
| { |
| final proxy = _getProxyForDir(dir); |
| |
| final expectedDirents = [ |
| _createDirentForDot(), |
| _createDirent(file1, 'file1'), |
| _createDirent(subDir, 'subDir'), |
| _createDirent(file3, 'file3'), |
| ]; |
| { |
| final response = await proxy.readDirents(1024); |
| _validateExpectedDirents(expectedDirents, response); |
| } |
| |
| // test that next read call returns length zero buffer |
| { |
| final 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. |
| { |
| final proxy = _getProxyForDir(subDir); |
| final expectedDirents = [ |
| _createDirentForDot(), |
| _createDirent(file2, 'file2'), |
| ]; |
| final response = await proxy.readDirents(1024); |
| _validateExpectedDirents(expectedDirents, response); |
| } |
| }); |
| |
| test('serve function works', () async { |
| PseudoDir dir = PseudoDir(); |
| final file1 = PseudoFile.readOnlyStr(() => 'file1'); |
| |
| dir.addNode('file1', file1); |
| |
| DirectoryProxy proxy = DirectoryProxy(); |
| final status = |
| dir.serve(InterfaceRequest(proxy.ctrl.request().passChannel())); |
| expect(status, ZX.OK); |
| |
| final expectedDirents = [ |
| _createDirentForDot(), |
| _createDirent(file1, 'file1'), |
| ]; |
| final response = await proxy.readDirents(1024); |
| _validateExpectedDirents(expectedDirents, response); |
| }); |
| |
| test('passed buffer size is exact', () async { |
| PseudoDir dir = PseudoDir(); |
| PseudoDir subDir = PseudoDir(); |
| final file1 = PseudoFile.readOnlyStr(() => 'file1'); |
| final file3 = PseudoFile.readOnlyStr(() => 'file3'); |
| dir |
| ..addNode('file1', file1) |
| ..addNode('subDir', subDir) |
| ..addNode('file3', file3); |
| final proxy = _getProxyForDir(dir); |
| |
| final expectedDirents = [ |
| _createDirentForDot(), |
| _createDirent(file1, 'file1'), |
| _createDirent(subDir, 'subDir'), |
| _createDirent(file3, 'file3'), |
| ]; |
| { |
| final response = |
| await proxy.readDirents(_expectedDirentSize(expectedDirents)); |
| _validateExpectedDirents(expectedDirents, response); |
| } |
| |
| // test that next read call returns length zero buffer |
| { |
| final 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(); |
| final file1 = PseudoFile.readOnlyStr(() => 'file1'); |
| final file3 = PseudoFile.readOnlyStr(() => 'file3'); |
| dir |
| ..addNode('file1', file1) |
| ..addNode('subDir', subDir) |
| ..addNode('file3', file3); |
| |
| final proxy = _getProxyForDir(dir); |
| |
| final expectedDirents = [ |
| _createDirentForDot(), |
| _createDirent(file1, 'file1'), |
| _createDirent(subDir, 'subDir'), |
| _createDirent(file3, 'file3'), |
| ]; |
| final size = _expectedDirentSize(expectedDirents) - 1; |
| final lastDirent = expectedDirents.removeLast(); |
| |
| { |
| final response = await proxy.readDirents(size); |
| _validateExpectedDirents(expectedDirents, response); |
| } |
| |
| // test that next read call returns last dirent |
| { |
| final response = await proxy.readDirents(1024); |
| _validateExpectedDirents([lastDirent], response); |
| } |
| |
| // test that next read call returns length zero buffer |
| { |
| final 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(); |
| final file1 = PseudoFile.readOnlyStr(() => 'file1'); |
| dir |
| ..addNode('file1', file1) |
| ..addNode('subDir', subDir); |
| |
| final proxy = _getProxyForDir(dir); |
| |
| final size = _expectedDirentSize([_createDirentForDot()]) - 1; |
| for (int i = 0; i < size; i++) { |
| final 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(); |
| final file1 = PseudoFile.readOnlyStr(() => 'file1'); |
| dir |
| ..addNode('file1', file1) |
| ..addNode('subDir', subDir); |
| |
| final proxy = _getProxyForDir(dir); |
| final size = _expectedDirentSize([_createDirentForDot()]); |
| |
| { |
| final response = await proxy.readDirents(size); |
| // make sure that '.' was read |
| _validateExpectedDirents([_createDirentForDot()], response); |
| } |
| |
| // this should return error |
| { |
| final response = await proxy.readDirents(size); |
| expect(response.s, ZX.ERR_BUFFER_TOO_SMALL); |
| expect(response.dirents.length, 0); |
| } |
| |
| final expectedDirents = [ |
| _createDirent(file1, 'file1'), |
| _createDirent(subDir, 'subDir'), |
| ]; |
| final response = |
| await proxy.readDirents(_expectedDirentSize(expectedDirents)); |
| _validateExpectedDirents(expectedDirents, response); |
| }); |
| |
| test('multiple reads with small buffer', () async { |
| PseudoDir dir = PseudoDir(); |
| PseudoDir subDir = PseudoDir(); |
| final file1 = PseudoFile.readOnlyStr(() => 'file1'); |
| dir |
| ..addNode('file1', file1) |
| ..addNode('subDir', subDir); |
| |
| final proxy = _getProxyForDir(dir); |
| final expectedDirents = [ |
| _createDirentForDot(), |
| _createDirent(file1, 'file1'), |
| _createDirent(subDir, 'subDir'), |
| ]; |
| for (final dirent in expectedDirents) { |
| final dirents = [dirent]; |
| final response = |
| await proxy.readDirents(_expectedDirentSize(dirents)); |
| _validateExpectedDirents(dirents, response); |
| } |
| |
| // test that next read call returns length zero buffer |
| final 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(); |
| final file1 = PseudoFile.readOnlyStr(() => 'file1'); |
| dir |
| ..addNode('file1', file1) |
| ..addNode('subDir', subDir); |
| |
| final proxy = _getProxyForDir(dir); |
| |
| { |
| final expectedDirents = [ |
| _createDirentForDot(), |
| _createDirent(file1, 'file1'), |
| ]; |
| |
| final response = |
| await proxy.readDirents(_expectedDirentSize(expectedDirents)); |
| _validateExpectedDirents(expectedDirents, response); |
| } |
| |
| { |
| final expectedDirents = [ |
| _createDirent(subDir, 'subDir'), |
| ]; |
| |
| final 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(); |
| final file1 = PseudoFile.readOnlyStr(() => 'file1'); |
| dir |
| ..addNode('file1', file1) |
| ..addNode('subDir', subDir); |
| |
| final proxy = _getProxyForDir(dir); |
| |
| { |
| final expectedDirents = [ |
| _createDirentForDot(), |
| ]; |
| |
| final response = await proxy |
| .readDirents(_expectedDirentSize(expectedDirents) + 10); |
| _validateExpectedDirents(expectedDirents, response); |
| } |
| |
| { |
| // now test that we are able to get rest |
| final expectedDirents = [ |
| _createDirent(file1, 'file1'), |
| _createDirent(subDir, 'subDir'), |
| ]; |
| |
| final response = await proxy.readDirents(1024); |
| _validateExpectedDirents(expectedDirents, response); |
| } |
| }); |
| |
| test('rewind works', () async { |
| PseudoDir dir = PseudoDir(); |
| PseudoDir subDir = PseudoDir(); |
| final file1 = PseudoFile.readOnlyStr(() => 'file1'); |
| dir |
| ..addNode('file1', file1) |
| ..addNode('subDir', subDir); |
| |
| final proxy = _getProxyForDir(dir); |
| |
| final expectedDirents = [ |
| _createDirentForDot(), |
| ]; |
| |
| { |
| final response = await proxy |
| .readDirents(_expectedDirentSize(expectedDirents) + 10); |
| _validateExpectedDirents(expectedDirents, response); |
| } |
| |
| { |
| final rewindResponse = await proxy.rewind(); |
| expect(rewindResponse, ZX.OK); |
| } |
| |
| { |
| final 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(); |
| final file1 = PseudoFile.readOnlyStr(() => 'file1'); |
| dir |
| ..addNode('file1', file1) |
| ..addNode('subDir', subDir); |
| |
| final proxy = _getProxyForDir(dir); |
| |
| final expectedDirents = [ |
| _createDirentForDot(), |
| _createDirent(file1, 'file1'), |
| _createDirent(subDir, 'subDir'), |
| ]; |
| |
| { |
| final response = await proxy |
| .readDirents(_expectedDirentSize(expectedDirents) + 10); |
| _validateExpectedDirents(expectedDirents, response); |
| } |
| |
| { |
| final rewindResponse = await proxy.rewind(); |
| expect(rewindResponse, ZX.OK); |
| } |
| |
| { |
| final response = await proxy |
| .readDirents(_expectedDirentSize(expectedDirents) + 10); |
| _validateExpectedDirents(expectedDirents, response); |
| } |
| }); |
| |
| test('readdir works when node removed', () async { |
| PseudoDir dir = PseudoDir(); |
| PseudoDir subDir = PseudoDir(); |
| final file1 = PseudoFile.readOnlyStr(() => 'file1'); |
| dir |
| ..addNode('file1', file1) |
| ..addNode('subDir', subDir); |
| |
| final proxy = _getProxyForDir(dir); |
| |
| { |
| final expectedDirents = [ |
| _createDirentForDot(), |
| ]; |
| final response = |
| await proxy.readDirents(_expectedDirentSize(expectedDirents)); |
| _validateExpectedDirents(expectedDirents, response); |
| } |
| |
| // remove first node |
| dir.removeNode('file1'); |
| |
| { |
| final expectedDirents = [_createDirent(subDir, 'subDir')]; |
| final response = await proxy.readDirents(1024); |
| _validateExpectedDirents(expectedDirents, response); |
| } |
| }); |
| |
| test('readdir works when already last node is removed', () async { |
| PseudoDir dir = PseudoDir(); |
| PseudoDir subDir = PseudoDir(); |
| final file1 = PseudoFile.readOnlyStr(() => 'file1'); |
| dir |
| ..addNode('file1', file1) |
| ..addNode('subDir', subDir); |
| |
| final proxy = _getProxyForDir(dir); |
| |
| { |
| final expectedDirents = [ |
| _createDirentForDot(), |
| _createDirent(file1, 'file1') |
| ]; |
| final response = |
| await proxy.readDirents(_expectedDirentSize(expectedDirents)); |
| _validateExpectedDirents(expectedDirents, response); |
| } |
| |
| // remove first node |
| dir.removeNode('file1'); |
| |
| { |
| final expectedDirents = [_createDirent(subDir, 'subDir')]; |
| final response = await proxy.readDirents(1024); |
| _validateExpectedDirents(expectedDirents, response); |
| } |
| }); |
| |
| test('readdir works when node is added', () async { |
| PseudoDir dir = PseudoDir(); |
| PseudoDir subDir = PseudoDir(); |
| final file1 = PseudoFile.readOnlyStr(() => 'file1'); |
| dir |
| ..addNode('file1', file1) |
| ..addNode('subDir', subDir); |
| |
| final proxy = _getProxyForDir(dir); |
| |
| { |
| final expectedDirents = [ |
| _createDirentForDot(), |
| _createDirent(file1, 'file1'), |
| _createDirent(subDir, 'subDir') |
| ]; |
| final response = |
| await proxy.readDirents(_expectedDirentSize(expectedDirents)); |
| _validateExpectedDirents(expectedDirents, response); |
| } |
| |
| dir.addNode('file2', file1); |
| |
| { |
| final expectedDirents = [_createDirent(file1, 'file2')]; |
| final response = await proxy.readDirents(1024); |
| _validateExpectedDirents(expectedDirents, response); |
| } |
| }); |
| |
| test('readdir works when node is added and only first node was read', |
| () async { |
| PseudoDir dir = PseudoDir(); |
| PseudoDir subDir = PseudoDir(); |
| final file1 = PseudoFile.readOnlyStr(() => 'file1'); |
| dir |
| ..addNode('file1', file1) |
| ..addNode('subDir', subDir); |
| |
| final proxy = _getProxyForDir(dir); |
| |
| { |
| final expectedDirents = [ |
| _createDirentForDot(), |
| ]; |
| final response = |
| await proxy.readDirents(_expectedDirentSize(expectedDirents)); |
| _validateExpectedDirents(expectedDirents, response); |
| } |
| |
| dir.addNode('file2', file1); |
| |
| { |
| final expectedDirents = [ |
| _createDirent(file1, 'file1'), |
| _createDirent(subDir, 'subDir'), |
| _createDirent(file1, 'file2') |
| ]; |
| final response = await proxy.readDirents(1024); |
| _validateExpectedDirents(expectedDirents, response); |
| } |
| }); |
| }); |
| |
| group('open/close file/dir in dir:', () { |
| Future<void> _openFileAndAssert(DirectoryProxy proxy, String filePath, |
| int bufferLen, String expectedContent) async { |
| FileProxy fileProxy = FileProxy(); |
| await proxy.open(OpenFlags.rightReadable, 0, filePath, |
| InterfaceRequest(fileProxy.ctrl.request().passChannel())); |
| |
| final data = await fileProxy.read(bufferLen); |
| expect(String.fromCharCodes(data), expectedContent); |
| } |
| |
| PseudoDir _setUpDir() { |
| PseudoDir dir = PseudoDir(); |
| PseudoDir subDir = PseudoDir(); |
| final file1 = PseudoFile.readOnlyStr(() => 'file1'); |
| final file2 = PseudoFile.readOnlyStr(() => 'file2'); |
| final file3 = PseudoFile.readOnlyStr(() => 'file3'); |
| final 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(); |
| |
| final proxy = _getProxyForDir(dir); |
| final paths = ['.', './', './/', './//', './/.//./']; |
| for (final path in paths) { |
| DirectoryProxy newProxy = DirectoryProxy(); |
| await proxy.open(OpenFlags.rightReadable, 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(); |
| |
| final proxy = _getProxyForDir(dir); |
| |
| // open file 1 check contents. |
| final paths = ['file1', './file1', './/file1', './//file1']; |
| for (final path in paths) { |
| await _openFileAndAssert(proxy, path, 100, 'file1'); |
| } |
| }); |
| |
| test('open fails for illegal path', () async { |
| PseudoDir dir = _setUpDir(); |
| |
| final proxy = _getProxyForDir(dir); |
| final paths = <String>[ |
| '', |
| 'too_long_path${'_' * 242}', |
| 'subDir/too_long_path${'_' * 242}', |
| '..', |
| 'subDir/..', |
| 'invalid_\u{00}_name', |
| 'subDir/invalid_\u{00}_name', |
| 'invalid_\u{00}_name/legal_name', |
| ]; |
| for (final path in paths) { |
| DirectoryProxy newProxy = DirectoryProxy(); |
| await proxy.open(OpenFlags.rightReadable | OpenFlags.describe, 0, |
| path, 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()); |
| }); |
| } |
| }); |
| |
| test('open file fails for path ending with "/"', () async { |
| PseudoDir dir = _setUpDir(); |
| |
| final proxy = _getProxyForDir(dir); |
| |
| FileProxy fileProxy = FileProxy(); |
| await proxy.open(OpenFlags.rightReadable | OpenFlags.describe, 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 file fails for invalid key', () async { |
| PseudoDir dir = _setUpDir(); |
| |
| final proxy = _getProxyForDir(dir); |
| |
| FileProxy fileProxy = FileProxy(); |
| await proxy.open(OpenFlags.rightReadable, 0, 'invalid', |
| InterfaceRequest(fileProxy.ctrl.request().passChannel())); |
| |
| // channel should be closed |
| fileProxy.ctrl.whenClosed.asStream().listen(expectAsync1((_) {})); |
| }); |
| |
| test('open fails for trying to open file within a file', () async { |
| PseudoDir dir = _setUpDir(); |
| |
| final proxy = _getProxyForDir(dir); |
| |
| FileProxy fileProxy = FileProxy(); |
| await proxy.open( |
| OpenFlags.rightReadable | OpenFlags.describe, |
| 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('close works', () async { |
| PseudoDir dir = PseudoDir(); |
| PseudoDir subDir = PseudoDir(); |
| final file1 = PseudoFile.readOnlyStr(() => 'file1'); |
| dir |
| ..addNode('file1', file1) |
| ..addNode('subDir', subDir); |
| |
| final proxy = _getProxyForDir(dir); |
| DirectoryProxy subDirProxy = DirectoryProxy(); |
| await proxy.open(OpenFlags.rightReadable, 0, 'subDir', |
| InterfaceRequest(subDirProxy.ctrl.request().passChannel())); |
| |
| FileProxy fileProxy = FileProxy(); |
| await proxy.open(OpenFlags.rightReadable, 0, 'file1', |
| InterfaceRequest(fileProxy.ctrl.request().passChannel())); |
| dir.close(); |
| proxy.ctrl.whenClosed.asStream().listen(expectAsync1((_) {})); |
| subDirProxy.ctrl.whenClosed.asStream().listen(expectAsync1((_) {})); |
| fileProxy.ctrl.whenClosed.asStream().listen(expectAsync1((_) {})); |
| }); |
| |
| test('open sub dir', () async { |
| PseudoDir dir = _setUpDir(); |
| |
| final proxy = _getProxyForDir(dir); |
| |
| DirectoryProxy dirProxy = DirectoryProxy(); |
| await proxy.open(OpenFlags.rightReadable, 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('directory rights are hierarchical (open dir)', () async { |
| PseudoDir dir = _setUpDir(); |
| |
| final proxy = _getProxyForDir(dir, OpenFlags.rightReadable); |
| |
| final newProxy = DirectoryProxy(); |
| await proxy.open(OpenFlags.rightWritable | OpenFlags.describe, 0, |
| 'subDir', InterfaceRequest(newProxy.ctrl.request().passChannel())); |
| |
| await newProxy.onOpen.first.then((response) { |
| expect(response.s, ZX.ERR_ACCESS_DENIED); |
| expect(response.info, isNull); |
| }).catchError((err) async { |
| fail(err.toString()); |
| }); |
| }); |
| |
| test('directory rights are hierarchical (open file)', () async { |
| PseudoDir dir = _setUpDir(); |
| |
| final proxy = _getProxyForDir(dir, OpenFlags.rightWritable); |
| |
| final newProxy = DirectoryProxy(); |
| await proxy.open(OpenFlags.rightWritable | OpenFlags.describe, 0, |
| 'subDir', InterfaceRequest(newProxy.ctrl.request().passChannel())); |
| |
| await newProxy.onOpen.first.then((response) { |
| expect(response.s, ZX.OK); |
| expect(response.info, isNotNull); |
| }).catchError((err) async { |
| fail(err.toString()); |
| }); |
| |
| FileProxy fileProxy = FileProxy(); |
| await newProxy.open(OpenFlags.rightReadable, 0, 'file2', |
| InterfaceRequest(fileProxy.ctrl.request().passChannel())); |
| |
| // channel should be closed |
| fileProxy.ctrl.whenClosed.asStream().listen(expectAsync1((_) {})); |
| }); |
| |
| test('open sub dir with "/" at end', () async { |
| PseudoDir dir = _setUpDir(); |
| |
| final proxy = _getProxyForDir(dir); |
| |
| DirectoryProxy dirProxy = DirectoryProxy(); |
| await proxy.open(OpenFlags.rightReadable, 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(); |
| |
| final proxy = _getProxyForDir(dir); |
| |
| // open file 2 in subDir. |
| await _openFileAndAssert(proxy, 'subDir/file2', 100, 'file2'); |
| }); |
| |
| test('readdir fails for NodeReference', () async { |
| PseudoDir dir = _setUpDir(); |
| |
| final proxy = _getProxyForDir(dir, OpenFlags.nodeReference); |
| |
| final response = await proxy.readDirents(1024); |
| expect(response.s, ZX.ERR_BAD_HANDLE); |
| }); |
| |
| test('not allowed to open a file for NodeReference', () async { |
| PseudoDir dir = _setUpDir(); |
| |
| final proxy = _getProxyForDir(dir, OpenFlags.nodeReference); |
| |
| // open file 2 in subDir. |
| FileProxy fileProxy = FileProxy(); |
| await proxy.open(OpenFlags.rightReadable, 0, 'subDir/file2', |
| InterfaceRequest(fileProxy.ctrl.request().passChannel())); |
| |
| // channel should be closed |
| fileProxy.ctrl.whenClosed.asStream().listen(expectAsync1((_) {})); |
| }); |
| |
| test('clone with same rights', () async { |
| PseudoDir dir = _setUpDir(); |
| |
| final proxy = _getProxyForDir( |
| dir, OpenFlags.rightReadable | OpenFlags.rightWritable); |
| DirectoryProxy cloneProxy = DirectoryProxy(); |
| await proxy.clone(OpenFlags.cloneSameRights | OpenFlags.describe, |
| InterfaceRequest(cloneProxy.ctrl.request().passChannel())); |
| |
| final subDirProxy = DirectoryProxy(); |
| await cloneProxy.open( |
| OpenFlags.rightReadable | |
| OpenFlags.rightWritable | |
| OpenFlags.describe, |
| 0, |
| 'subDir', |
| InterfaceRequest(subDirProxy.ctrl.request().passChannel())); |
| |
| await subDirProxy.onOpen.first.then((response) { |
| expect(response.s, ZX.OK); |
| expect(response.info, isNotNull); |
| }).catchError((err) async { |
| fail(err.toString()); |
| }); |
| |
| // open file 2 check contents to make sure correct dir was opened. |
| await _openFileAndAssert(subDirProxy, 'file2', 100, 'file2'); |
| }); |
| }); |
| |
| test('test clone', () async { |
| PseudoDir dir = PseudoDir(); |
| |
| final proxy = _getProxyForDir(dir, OpenFlags.rightReadable); |
| |
| DirectoryProxy newProxy = DirectoryProxy(); |
| await proxy.clone(OpenFlags.rightReadable | OpenFlags.describe, |
| InterfaceRequest(newProxy.ctrl.request().passChannel())); |
| |
| await newProxy.onOpen.first.then((response) { |
| expect(response.s, ZX.OK); |
| expect(response.info, isNotNull); |
| }).catchError((err) async { |
| fail(err.toString()); |
| }); |
| }); |
| |
| test('clone should fail if requested rights exceed source rights', |
| () async { |
| PseudoDir dir = PseudoDir(); |
| final proxy = _getProxyForDir(dir, OpenFlags.rightReadable); |
| |
| final clonedProxy = DirectoryProxy(); |
| await proxy.clone(OpenFlags.rightWritable | OpenFlags.describe, |
| InterfaceRequest(clonedProxy.ctrl.request().passChannel())); |
| |
| await clonedProxy.onOpen.first.then((response) { |
| expect(response.s, ZX.ERR_ACCESS_DENIED); |
| expect(response.info, isNull); |
| }).catchError((err) async { |
| fail(err.toString()); |
| }); |
| }); |
| |
| test('test clone fails for invalid flags', () async { |
| PseudoDir dir = PseudoDir(); |
| |
| final proxy = _getProxyForDir(dir, OpenFlags.rightReadable); |
| |
| DirectoryProxy newProxy = DirectoryProxy(); |
| await proxy.clone(OpenFlags.truncate | OpenFlags.describe, |
| 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()); |
| }); |
| }); |
| |
| test( |
| 'test clone disallows both OpenFlags.cloneSameRights and specific rights', |
| () async { |
| PseudoDir dir = PseudoDir(); |
| final proxy = _getProxyForDir(dir, OpenFlags.rightReadable); |
| |
| final clonedProxy = DirectoryProxy(); |
| await proxy.clone( |
| OpenFlags.rightReadable | |
| OpenFlags.cloneSameRights | |
| OpenFlags.describe, |
| InterfaceRequest(clonedProxy.ctrl.request().passChannel())); |
| |
| await clonedProxy.onOpen.first.then((response) { |
| expect(response.s, ZX.ERR_INVALID_ARGS); |
| expect(response.info, isNull); |
| }).catchError((err) async { |
| fail(err.toString()); |
| }); |
| }); |
| }); |
| } |
| |
| class _Dirent { |
| static const int _fixedSize = 10; |
| int? ino; |
| int? size; |
| DirentType? 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 = DirentType(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 { |
| final String _val = ''; |
| _TestVnode(); |
| |
| @override |
| int connect(OpenFlags flags, int mode, InterfaceRequest<Node> request, |
| [OpenFlags? parentFlags]) { |
| throw UnimplementedError(); |
| } |
| |
| @override |
| int inodeNumber() { |
| return inoUnknown; |
| } |
| |
| @override |
| DirentType type() { |
| return DirentType.unknown; |
| } |
| |
| String value() => _val; |
| |
| @override |
| void close() {} |
| } |