1// Copyright 2013 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5#include <errno.h> 6#include <fcntl.h> 7 8#include <set> 9#include <string> 10 11#include <gmock/gmock.h> 12#include <ppapi/c/ppb_file_io.h> 13#include <ppapi/c/pp_directory_entry.h> 14#include <ppapi/c/pp_errors.h> 15#include <ppapi/c/pp_instance.h> 16#if defined(WIN32) 17#include <windows.h> // For Sleep() 18#endif 19 20#include "fake_ppapi/fake_pepper_interface_html5_fs.h" 21#include "nacl_io/kernel_handle.h" 22#include "nacl_io/html5fs/html5_fs.h" 23#include "nacl_io/osdirent.h" 24#include "nacl_io/osunistd.h" 25#include "nacl_io/pepper_interface_delegate.h" 26#include "sdk_util/scoped_ref.h" 27#include "mock_util.h" 28#include "pepper_interface_mock.h" 29 30using namespace nacl_io; 31using namespace sdk_util; 32 33using ::testing::_; 34using ::testing::DoAll; 35using ::testing::Invoke; 36using ::testing::Mock; 37using ::testing::Return; 38 39namespace { 40 41class Html5FsForTesting : public Html5Fs { 42 public: 43 Html5FsForTesting(StringMap_t& string_map, PepperInterface* ppapi, 44 int expected_error = 0) { 45 FsInitArgs args; 46 args.string_map = string_map; 47 args.ppapi = ppapi; 48 Error error = Init(args); 49 EXPECT_EQ(expected_error, error); 50 } 51 52 bool Exists(const char* filename) { 53 ScopedNode node; 54 if (Open(Path(filename), O_RDONLY, &node)) 55 return false; 56 57 struct stat buf; 58 return node->GetStat(&buf) == 0; 59 } 60}; 61 62class Html5FsTest : public ::testing::Test { 63 public: 64 Html5FsTest(); 65 66 protected: 67 FakePepperInterfaceHtml5Fs ppapi_html5_; 68 PepperInterfaceMock ppapi_mock_; 69 PepperInterfaceDelegate ppapi_; 70}; 71 72Html5FsTest::Html5FsTest() 73 : ppapi_mock_(ppapi_html5_.GetInstance()), 74 ppapi_(ppapi_html5_.GetInstance()) { 75 // Default delegation to the html5 pepper interface. 76 ppapi_.SetCoreInterfaceDelegate(ppapi_html5_.GetCoreInterface()); 77 ppapi_.SetFileSystemInterfaceDelegate(ppapi_html5_.GetFileSystemInterface()); 78 ppapi_.SetFileRefInterfaceDelegate(ppapi_html5_.GetFileRefInterface()); 79 ppapi_.SetFileIoInterfaceDelegate(ppapi_html5_.GetFileIoInterface()); 80 ppapi_.SetVarInterfaceDelegate(ppapi_html5_.GetVarInterface()); 81} 82 83} // namespace 84 85TEST_F(Html5FsTest, FilesystemType) { 86 const char* filesystem_type_strings[] = {"", "PERSISTENT", "TEMPORARY", NULL}; 87 PP_FileSystemType filesystem_type_values[] = { 88 PP_FILESYSTEMTYPE_LOCALPERSISTENT, // Default to persistent. 89 PP_FILESYSTEMTYPE_LOCALPERSISTENT, PP_FILESYSTEMTYPE_LOCALTEMPORARY}; 90 91 const char* expected_size_strings[] = {"100", "12345", NULL}; 92 const int expected_size_values[] = {100, 12345}; 93 94 FileSystemInterfaceMock* filesystem_mock = 95 ppapi_mock_.GetFileSystemInterface(); 96 97 FakeFileSystemInterface* filesystem_fake = 98 static_cast<FakeFileSystemInterface*>( 99 ppapi_html5_.GetFileSystemInterface()); 100 101 for (int i = 0; filesystem_type_strings[i] != NULL; ++i) { 102 const char* filesystem_type_string = filesystem_type_strings[i]; 103 PP_FileSystemType expected_filesystem_type = filesystem_type_values[i]; 104 105 for (int j = 0; expected_size_strings[j] != NULL; ++j) { 106 const char* expected_size_string = expected_size_strings[j]; 107 int64_t expected_expected_size = expected_size_values[j]; 108 109 ppapi_.SetFileSystemInterfaceDelegate(filesystem_mock); 110 111 ON_CALL(*filesystem_mock, Create(_, _)).WillByDefault( 112 Invoke(filesystem_fake, &FakeFileSystemInterface::Create)); 113 114 EXPECT_CALL(*filesystem_mock, 115 Create(ppapi_.GetInstance(), expected_filesystem_type)); 116 117 EXPECT_CALL(*filesystem_mock, Open(_, expected_expected_size, _)) 118 .WillOnce(DoAll(CallCallback<2>(int32_t(PP_OK)), 119 Return(int32_t(PP_OK_COMPLETIONPENDING)))); 120 121 StringMap_t map; 122 map["type"] = filesystem_type_string; 123 map["expected_size"] = expected_size_string; 124 ScopedRef<Html5FsForTesting> fs(new Html5FsForTesting(map, &ppapi_)); 125 126 Mock::VerifyAndClearExpectations(&filesystem_mock); 127 } 128 } 129} 130 131TEST_F(Html5FsTest, PassFilesystemResource) { 132 // Fail if given a bad resource. 133 { 134 StringMap_t map; 135 map["filesystem_resource"] = "0"; 136 ScopedRef<Html5FsForTesting> fs( 137 new Html5FsForTesting(map, &ppapi_, EINVAL)); 138 } 139 140 { 141 EXPECT_TRUE(ppapi_html5_.filesystem_template()->AddEmptyFile("/foo", NULL)); 142 PP_Resource filesystem = ppapi_html5_.GetFileSystemInterface()->Create( 143 ppapi_html5_.GetInstance(), PP_FILESYSTEMTYPE_LOCALPERSISTENT); 144 145 ASSERT_EQ(int32_t(PP_OK), ppapi_html5_.GetFileSystemInterface()->Open( 146 filesystem, 0, PP_BlockUntilComplete())); 147 148 StringMap_t map; 149 char buffer[30]; 150 snprintf(buffer, 30, "%d", filesystem); 151 map["filesystem_resource"] = buffer; 152 ScopedRef<Html5FsForTesting> fs( 153 new Html5FsForTesting(map, &ppapi_)); 154 155 ASSERT_TRUE(fs->Exists("/foo")); 156 157 ppapi_html5_.GetCoreInterface()->ReleaseResource(filesystem); 158 } 159} 160 161TEST_F(Html5FsTest, MountSubtree) { 162 EXPECT_TRUE(ppapi_html5_.filesystem_template()->AddEmptyFile("/foo/bar", 163 NULL)); 164 StringMap_t map; 165 map["SOURCE"] = "/foo"; 166 ScopedRef<Html5FsForTesting> fs(new Html5FsForTesting(map, &ppapi_)); 167 168 ASSERT_TRUE(fs->Exists("/bar")); 169 ASSERT_FALSE(fs->Exists("/foo/bar")); 170} 171 172TEST_F(Html5FsTest, Mkdir) { 173 StringMap_t map; 174 ScopedRef<Html5FsForTesting> fs(new Html5FsForTesting(map, &ppapi_)); 175 176 // mkdir at the root should return EEXIST, not EACCES. 177 EXPECT_EQ(EEXIST, fs->Mkdir(Path("/"), 0644)); 178 179 Path path("/foo"); 180 ASSERT_FALSE(fs->Exists("/foo")); 181 ASSERT_EQ(0, fs->Mkdir(path, 0644)); 182 183 struct stat stat; 184 ScopedNode node; 185 ASSERT_EQ(0, fs->Open(path, O_RDONLY, &node)); 186 EXPECT_EQ(0, node->GetStat(&stat)); 187 EXPECT_EQ(S_IFDIR, stat.st_mode & S_IFDIR); 188} 189 190TEST_F(Html5FsTest, Remove) { 191 const char* kPath = "/foo"; 192 EXPECT_TRUE(ppapi_html5_.filesystem_template()->AddEmptyFile(kPath, NULL)); 193 194 StringMap_t map; 195 ScopedRef<Html5FsForTesting> fs(new Html5FsForTesting(map, &ppapi_)); 196 197 Path path(kPath); 198 ASSERT_TRUE(fs->Exists(kPath)); 199 ASSERT_EQ(0, fs->Remove(path)); 200 EXPECT_FALSE(fs->Exists(kPath)); 201} 202 203TEST_F(Html5FsTest, Unlink) { 204 EXPECT_TRUE(ppapi_html5_.filesystem_template()->AddEmptyFile("/file", NULL)); 205 EXPECT_TRUE(ppapi_html5_.filesystem_template()->AddDirectory("/dir", NULL)); 206 207 StringMap_t map; 208 ScopedRef<Html5FsForTesting> fs(new Html5FsForTesting(map, &ppapi_)); 209 210 ASSERT_TRUE(fs->Exists("/dir")); 211 ASSERT_TRUE(fs->Exists("/file")); 212 ASSERT_EQ(0, fs->Unlink(Path("/file"))); 213 ASSERT_EQ(EISDIR, fs->Unlink(Path("/dir"))); 214 EXPECT_FALSE(fs->Exists("/file")); 215 EXPECT_TRUE(fs->Exists("/dir")); 216} 217 218TEST_F(Html5FsTest, Rmdir) { 219 EXPECT_TRUE(ppapi_html5_.filesystem_template()->AddEmptyFile("/file", NULL)); 220 EXPECT_TRUE(ppapi_html5_.filesystem_template()->AddDirectory("/dir", NULL)); 221 222 StringMap_t map; 223 ScopedRef<Html5FsForTesting> fs(new Html5FsForTesting(map, &ppapi_)); 224 225 ASSERT_EQ(ENOTDIR, fs->Rmdir(Path("/file"))); 226 EXPECT_EQ(0, fs->Rmdir(Path("/dir"))); 227 EXPECT_FALSE(fs->Exists("/dir")); 228 EXPECT_TRUE(fs->Exists("/file")); 229} 230 231TEST_F(Html5FsTest, Rename) { 232 EXPECT_TRUE(ppapi_html5_.filesystem_template()->AddEmptyFile("/foo", NULL)); 233 234 StringMap_t map; 235 ScopedRef<Html5FsForTesting> fs(new Html5FsForTesting(map, &ppapi_)); 236 237 ASSERT_TRUE(fs->Exists("/foo")); 238 ASSERT_EQ(0, fs->Rename(Path("/foo"), Path("/bar"))); 239 EXPECT_FALSE(fs->Exists("/foo")); 240 EXPECT_TRUE(fs->Exists("/bar")); 241} 242 243TEST_F(Html5FsTest, OpenForCreate) { 244 StringMap_t map; 245 ScopedRef<Html5FsForTesting> fs(new Html5FsForTesting(map, &ppapi_)); 246 247 EXPECT_FALSE(fs->Exists("/foo")); 248 249 Path path("/foo"); 250 ScopedNode node; 251 ASSERT_EQ(0, fs->Open(path, O_CREAT | O_RDWR, &node)); 252 253 // Write some data. 254 char contents[] = "contents"; 255 int bytes_written = 0; 256 EXPECT_EQ(0, node->Write(HandleAttr(), &contents[0], strlen(contents), 257 &bytes_written)); 258 EXPECT_EQ(strlen(contents), bytes_written); 259 260 // Create again. 261 ASSERT_EQ(0, fs->Open(path, O_CREAT, &node)); 262 263 // Check that the file still has data. 264 off_t size; 265 EXPECT_EQ(0, node->GetSize(&size)); 266 EXPECT_EQ(strlen(contents), size); 267 268 // Open exclusively. 269 EXPECT_EQ(EEXIST, fs->Open(path, O_CREAT | O_EXCL, &node)); 270 271 // Try to truncate without write access. 272 EXPECT_EQ(EINVAL, fs->Open(path, O_CREAT | O_TRUNC, &node)); 273 274 // Open and truncate. 275 ASSERT_EQ(0, fs->Open(path, O_CREAT | O_TRUNC | O_WRONLY, &node)); 276 277 // File should be empty. 278 EXPECT_EQ(0, node->GetSize(&size)); 279 EXPECT_EQ(0, size); 280} 281 282TEST_F(Html5FsTest, Read) { 283 const char contents[] = "contents"; 284 ASSERT_TRUE( 285 ppapi_html5_.filesystem_template()->AddFile("/file", contents, NULL)); 286 ASSERT_TRUE(ppapi_html5_.filesystem_template()->AddDirectory("/dir", NULL)); 287 StringMap_t map; 288 ScopedRef<Html5FsForTesting> fs(new Html5FsForTesting(map, &ppapi_)); 289 290 ScopedNode node; 291 ASSERT_EQ(0, fs->Open(Path("/file"), O_RDONLY, &node)); 292 293 char buffer[10] = {0}; 294 int bytes_read = 0; 295 HandleAttr attr; 296 ASSERT_EQ(0, node->Read(attr, &buffer[0], sizeof(buffer), &bytes_read)); 297 ASSERT_EQ(strlen(contents), bytes_read); 298 ASSERT_STREQ(contents, buffer); 299 300 // Read nothing past the end of the file. 301 attr.offs = 100; 302 ASSERT_EQ(0, node->Read(attr, &buffer[0], sizeof(buffer), &bytes_read)); 303 ASSERT_EQ(0, bytes_read); 304 305 // Read part of the data. 306 attr.offs = 4; 307 ASSERT_EQ(0, node->Read(attr, &buffer[0], sizeof(buffer), &bytes_read)); 308 ASSERT_EQ(strlen(contents) - 4, bytes_read); 309 buffer[bytes_read] = 0; 310 ASSERT_STREQ("ents", buffer); 311 312 // Writing should fail. 313 int bytes_written = 1; // Set to a non-zero value. 314 attr.offs = 0; 315 ASSERT_EQ(EACCES, 316 node->Write(attr, &buffer[0], sizeof(buffer), &bytes_written)); 317 ASSERT_EQ(0, bytes_written); 318 319 // Reading from a directory should fail. 320 ASSERT_EQ(0, fs->Open(Path("/dir"), O_RDONLY, &node)); 321 ASSERT_EQ(EISDIR, node->Read(attr, &buffer[0], sizeof(buffer), &bytes_read)); 322} 323 324TEST_F(Html5FsTest, Write) { 325 const char contents[] = "contents"; 326 EXPECT_TRUE( 327 ppapi_html5_.filesystem_template()->AddFile("/file", contents, NULL)); 328 EXPECT_TRUE(ppapi_html5_.filesystem_template()->AddDirectory("/dir", NULL)); 329 330 StringMap_t map; 331 ScopedRef<Html5FsForTesting> fs(new Html5FsForTesting(map, &ppapi_)); 332 333 ScopedNode node; 334 ASSERT_EQ(0, fs->Open(Path("/file"), O_WRONLY, &node)); 335 336 // Reading should fail. 337 char buffer[10]; 338 int bytes_read = 1; // Set to a non-zero value. 339 HandleAttr attr; 340 EXPECT_EQ(EACCES, node->Read(attr, &buffer[0], sizeof(buffer), &bytes_read)); 341 EXPECT_EQ(0, bytes_read); 342 343 // Reopen as read-write. 344 ASSERT_EQ(0, fs->Open(Path("/file"), O_RDWR, &node)); 345 346 int bytes_written = 1; // Set to a non-zero value. 347 attr.offs = 3; 348 EXPECT_EQ(0, node->Write(attr, "struct", 6, &bytes_written)); 349 EXPECT_EQ(6, bytes_written); 350 351 attr.offs = 0; 352 EXPECT_EQ(0, node->Read(attr, &buffer[0], sizeof(buffer), &bytes_read)); 353 EXPECT_EQ(9, bytes_read); 354 buffer[bytes_read] = 0; 355 EXPECT_STREQ("construct", buffer); 356 357 // Writing to a directory should fail. 358 EXPECT_EQ(0, fs->Open(Path("/dir"), O_RDWR, &node)); 359 EXPECT_EQ(EISDIR, node->Write(attr, &buffer[0], sizeof(buffer), &bytes_read)); 360} 361 362TEST_F(Html5FsTest, GetStat) { 363 const int creation_time = 1000; 364 const int access_time = 2000; 365 const int modified_time = 3000; 366 const char contents[] = "contents"; 367 368 // Create fake file. 369 FakeHtml5FsNode* fake_node; 370 EXPECT_TRUE(ppapi_html5_.filesystem_template()->AddFile( 371 "/file", contents, &fake_node)); 372 fake_node->set_creation_time(creation_time); 373 fake_node->set_last_access_time(access_time); 374 fake_node->set_last_modified_time(modified_time); 375 376 // Create fake directory. 377 EXPECT_TRUE( 378 ppapi_html5_.filesystem_template()->AddDirectory("/dir", &fake_node)); 379 fake_node->set_creation_time(creation_time); 380 fake_node->set_last_access_time(access_time); 381 fake_node->set_last_modified_time(modified_time); 382 383 StringMap_t map; 384 ScopedRef<Html5FsForTesting> fs(new Html5FsForTesting(map, &ppapi_)); 385 386 ScopedNode node; 387 ASSERT_EQ(0, fs->Open(Path("/file"), O_RDONLY, &node)); 388 389 struct stat statbuf; 390 EXPECT_EQ(0, node->GetStat(&statbuf)); 391 EXPECT_EQ(S_IFREG, statbuf.st_mode & S_IFMT); 392 EXPECT_EQ(S_IRALL | S_IWALL | S_IXALL, statbuf.st_mode & ~S_IFMT); 393 EXPECT_EQ(strlen(contents), statbuf.st_size); 394 EXPECT_EQ(access_time, statbuf.st_atime); 395 EXPECT_EQ(creation_time, statbuf.st_ctime); 396 EXPECT_EQ(modified_time, statbuf.st_mtime); 397 398 // Test Get* and Isa* methods. 399 off_t size; 400 EXPECT_EQ(0, node->GetSize(&size)); 401 EXPECT_EQ(strlen(contents), size); 402 EXPECT_FALSE(node->IsaDir()); 403 EXPECT_TRUE(node->IsaFile()); 404 EXPECT_EQ(ENOTTY, node->Isatty()); 405 406 // GetStat on a directory... 407 EXPECT_EQ(0, fs->Open(Path("/dir"), O_RDONLY, &node)); 408 EXPECT_EQ(0, node->GetStat(&statbuf)); 409 EXPECT_EQ(S_IFDIR, statbuf.st_mode & S_IFMT); 410 EXPECT_EQ(S_IRALL | S_IWALL | S_IXALL, statbuf.st_mode & ~S_IFMT); 411 EXPECT_EQ(0, statbuf.st_size); 412 EXPECT_EQ(access_time, statbuf.st_atime); 413 EXPECT_EQ(creation_time, statbuf.st_ctime); 414 EXPECT_EQ(modified_time, statbuf.st_mtime); 415 416 // Test Get* and Isa* methods. 417 EXPECT_EQ(0, node->GetSize(&size)); 418 EXPECT_EQ(0, size); 419 EXPECT_TRUE(node->IsaDir()); 420 EXPECT_FALSE(node->IsaFile()); 421 EXPECT_EQ(ENOTTY, node->Isatty()); 422} 423 424TEST_F(Html5FsTest, FTruncate) { 425 const char contents[] = "contents"; 426 EXPECT_TRUE( 427 ppapi_html5_.filesystem_template()->AddFile("/file", contents, NULL)); 428 EXPECT_TRUE(ppapi_html5_.filesystem_template()->AddDirectory("/dir", NULL)); 429 430 StringMap_t map; 431 ScopedRef<Html5FsForTesting> fs(new Html5FsForTesting(map, &ppapi_)); 432 433 ScopedNode node; 434 ASSERT_EQ(0, fs->Open(Path("/file"), O_RDWR, &node)); 435 436 HandleAttr attr; 437 char buffer[10] = {0}; 438 int bytes_read = 0; 439 440 // First make the file shorter... 441 EXPECT_EQ(0, node->FTruncate(4)); 442 EXPECT_EQ(0, node->Read(attr, &buffer[0], sizeof(buffer), &bytes_read)); 443 EXPECT_EQ(4, bytes_read); 444 buffer[bytes_read] = 0; 445 EXPECT_STREQ("cont", buffer); 446 447 // Now make the file longer... 448 EXPECT_EQ(0, node->FTruncate(8)); 449 EXPECT_EQ(0, node->Read(attr, &buffer[0], sizeof(buffer), &bytes_read)); 450 EXPECT_EQ(8, bytes_read); 451 buffer[bytes_read] = 0; 452 EXPECT_STREQ("cont\0\0\0\0", buffer); 453 454 // Ftruncate should fail for a directory. 455 EXPECT_EQ(0, fs->Open(Path("/dir"), O_RDONLY, &node)); 456 EXPECT_EQ(EISDIR, node->FTruncate(4)); 457} 458 459TEST_F(Html5FsTest, GetDents) { 460 const char contents[] = "contents"; 461 EXPECT_TRUE( 462 ppapi_html5_.filesystem_template()->AddFile("/file", contents, NULL)); 463 464 StringMap_t map; 465 ScopedRef<Html5FsForTesting> fs(new Html5FsForTesting(map, &ppapi_)); 466 467 ScopedNode root; 468 ASSERT_EQ(0, fs->Open(Path("/"), O_RDONLY, &root)); 469 470 ScopedNode node; 471 ASSERT_EQ(0, fs->Open(Path("/file"), O_RDWR, &node)); 472 473 struct stat stat; 474 ASSERT_EQ(0, node->GetStat(&stat)); 475 ino_t file1_ino = stat.st_ino; 476 477 // Should fail for regular files. 478 const size_t kMaxDirents = 5; 479 dirent dirents[kMaxDirents]; 480 int bytes_read = 1; // Set to a non-zero value. 481 482 memset(&dirents[0], 0, sizeof(dirents)); 483 EXPECT_EQ(ENOTDIR, 484 node->GetDents(0, &dirents[0], sizeof(dirents), &bytes_read)); 485 EXPECT_EQ(0, bytes_read); 486 487 // Should work with root directory. 488 // +2 to test a size that is not a multiple of sizeof(dirent). 489 // Expect it to round down. 490 memset(&dirents[0], 0, sizeof(dirents)); 491 EXPECT_EQ( 492 0, root->GetDents(0, &dirents[0], sizeof(dirent) * 3 + 2, &bytes_read)); 493 494 { 495 size_t num_dirents = bytes_read / sizeof(dirent); 496 EXPECT_EQ(3, num_dirents); 497 EXPECT_EQ(sizeof(dirent) * num_dirents, bytes_read); 498 499 std::multiset<std::string> dirnames; 500 for (size_t i = 0; i < num_dirents; ++i) { 501 EXPECT_EQ(sizeof(dirent), dirents[i].d_off); 502 EXPECT_EQ(sizeof(dirent), dirents[i].d_reclen); 503 dirnames.insert(dirents[i].d_name); 504 } 505 506 EXPECT_EQ(1, dirnames.count("file")); 507 EXPECT_EQ(1, dirnames.count(".")); 508 EXPECT_EQ(1, dirnames.count("..")); 509 } 510 511 // Add another file... 512 ASSERT_EQ(0, fs->Open(Path("/file2"), O_CREAT, &node)); 513 ASSERT_EQ(0, node->GetStat(&stat)); 514 ino_t file2_ino = stat.st_ino; 515 516 // These files SHOULD not hash to the same value but COULD. 517 EXPECT_NE(file1_ino, file2_ino); 518 519 // Read the root directory again. 520 memset(&dirents[0], 0, sizeof(dirents)); 521 EXPECT_EQ(0, root->GetDents(0, &dirents[0], sizeof(dirents), &bytes_read)); 522 523 { 524 size_t num_dirents = bytes_read / sizeof(dirent); 525 EXPECT_EQ(4, num_dirents); 526 EXPECT_EQ(sizeof(dirent) * num_dirents, bytes_read); 527 528 std::multiset<std::string> dirnames; 529 for (size_t i = 0; i < num_dirents; ++i) { 530 EXPECT_EQ(sizeof(dirent), dirents[i].d_off); 531 EXPECT_EQ(sizeof(dirent), dirents[i].d_reclen); 532 dirnames.insert(dirents[i].d_name); 533 534 if (!strcmp(dirents[i].d_name, "file")) { 535 EXPECT_EQ(dirents[i].d_ino, file1_ino); 536 } 537 if (!strcmp(dirents[i].d_name, "file2")) { 538 EXPECT_EQ(dirents[i].d_ino, file2_ino); 539 } 540 } 541 542 EXPECT_EQ(1, dirnames.count("file")); 543 EXPECT_EQ(1, dirnames.count("file2")); 544 EXPECT_EQ(1, dirnames.count(".")); 545 EXPECT_EQ(1, dirnames.count("..")); 546 } 547} 548