gvisor.dev/gvisor@v0.0.0-20240520182842-f9d4d51c7e0f/test/util/fs_util.cc (about) 1 // Copyright 2018 The gVisor Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 #include "test/util/fs_util.h" 16 17 #include <dirent.h> 18 #ifdef __linux__ 19 #include <linux/magic.h> 20 #endif // __linux__ 21 #include <sys/stat.h> 22 #include <sys/statfs.h> 23 #include <sys/types.h> 24 #include <unistd.h> 25 26 #include "gmock/gmock.h" 27 #include "absl/strings/match.h" 28 #include "absl/strings/str_cat.h" 29 #include "absl/strings/str_split.h" 30 #include "absl/strings/string_view.h" 31 #include "absl/time/clock.h" 32 #include "absl/time/time.h" 33 #include "test/util/cleanup.h" 34 #include "test/util/file_descriptor.h" 35 #include "test/util/posix_error.h" 36 37 namespace gvisor { 38 namespace testing { 39 40 namespace { 41 PosixError WriteContentsToFD(int fd, absl::string_view contents) { 42 int written = 0; 43 while (static_cast<absl::string_view::size_type>(written) < contents.size()) { 44 int wrote = write(fd, contents.data() + written, contents.size() - written); 45 if (wrote < 0) { 46 if (errno == EINTR) { 47 continue; 48 } 49 return PosixError( 50 errno, absl::StrCat("WriteContentsToFD fd: ", fd, " write failure.")); 51 } 52 written += wrote; 53 } 54 return NoError(); 55 } 56 } // namespace 57 58 namespace internal { 59 60 // Given a collection of file paths, append them all together, 61 // ensuring that the proper path separators are inserted between them. 62 std::string JoinPathImpl(std::initializer_list<absl::string_view> paths) { 63 std::string result; 64 65 if (paths.size() != 0) { 66 // This size calculation is worst-case: it assumes one extra "/" for every 67 // path other than the first. 68 size_t total_size = paths.size() - 1; 69 for (const absl::string_view path : paths) total_size += path.size(); 70 result.resize(total_size); 71 72 auto begin = result.begin(); 73 auto out = begin; 74 bool trailing_slash = false; 75 for (absl::string_view path : paths) { 76 if (path.empty()) continue; 77 if (path.front() == '/') { 78 if (trailing_slash) { 79 path.remove_prefix(1); 80 } 81 } else { 82 if (!trailing_slash && out != begin) *out++ = '/'; 83 } 84 const size_t this_size = path.size(); 85 memcpy(&*out, path.data(), this_size); 86 out += this_size; 87 trailing_slash = out[-1] == '/'; 88 } 89 result.erase(out - begin); 90 } 91 return result; 92 } 93 } // namespace internal 94 95 // Returns a status or the current working directory. 96 PosixErrorOr<std::string> GetCWD() { 97 char buffer[PATH_MAX + 1] = {}; 98 if (getcwd(buffer, PATH_MAX) == nullptr) { 99 return PosixError(errno, "GetCWD() failed"); 100 } 101 102 return std::string(buffer); 103 } 104 105 PosixErrorOr<struct stat> Stat(absl::string_view path) { 106 struct stat stat_buf; 107 int res = stat(std::string(path).c_str(), &stat_buf); 108 if (res < 0) { 109 return PosixError(errno, absl::StrCat("stat ", path)); 110 } 111 return stat_buf; 112 } 113 114 PosixErrorOr<struct stat> Lstat(absl::string_view path) { 115 struct stat stat_buf; 116 int res = lstat(std::string(path).c_str(), &stat_buf); 117 if (res < 0) { 118 return PosixError(errno, absl::StrCat("lstat ", path)); 119 } 120 return stat_buf; 121 } 122 123 PosixErrorOr<struct stat> Fstat(int fd) { 124 struct stat stat_buf; 125 int res = fstat(fd, &stat_buf); 126 if (res < 0) { 127 return PosixError(errno, absl::StrCat("fstat ", fd)); 128 } 129 return stat_buf; 130 } 131 132 PosixErrorOr<bool> Exists(absl::string_view path) { 133 struct stat stat_buf; 134 int res = lstat(std::string(path).c_str(), &stat_buf); 135 if (res < 0) { 136 if (errno == ENOENT) { 137 return false; 138 } 139 return PosixError(errno, absl::StrCat("lstat ", path)); 140 } 141 return true; 142 } 143 144 PosixErrorOr<bool> IsDirectory(absl::string_view path) { 145 ASSIGN_OR_RETURN_ERRNO(struct stat stat_buf, Lstat(path)); 146 if (S_ISDIR(stat_buf.st_mode)) { 147 return true; 148 } 149 150 return false; 151 } 152 153 PosixError Delete(absl::string_view path) { 154 int res = unlink(std::string(path).c_str()); 155 if (res < 0) { 156 return PosixError(errno, absl::StrCat("unlink ", path)); 157 } 158 159 return NoError(); 160 } 161 162 PosixError Truncate(absl::string_view path, int length) { 163 int res = truncate(std::string(path).c_str(), length); 164 if (res < 0) { 165 return PosixError(errno, 166 absl::StrCat("truncate ", path, " to length ", length)); 167 } 168 169 return NoError(); 170 } 171 172 PosixError Chmod(absl::string_view path, int mode) { 173 int res = chmod(std::string(path).c_str(), mode); 174 if (res < 0) { 175 return PosixError(errno, absl::StrCat("chmod ", path)); 176 } 177 178 return NoError(); 179 } 180 181 PosixError MknodAt(const FileDescriptor& dfd, absl::string_view path, int mode, 182 dev_t dev) { 183 int res = mknodat(dfd.get(), std::string(path).c_str(), mode, dev); 184 if (res < 0) { 185 return PosixError(errno, absl::StrCat("mknod ", path)); 186 } 187 188 return NoError(); 189 } 190 191 PosixError Unlink(absl::string_view path) { 192 int res = unlink(std::string(path).c_str()); 193 if (res < 0) { 194 return PosixError(errno, absl::StrCat("unlink ", path)); 195 } 196 return NoError(); 197 } 198 199 PosixError UnlinkAt(const FileDescriptor& dfd, absl::string_view path, 200 int flags) { 201 int res = unlinkat(dfd.get(), std::string(path).c_str(), flags); 202 if (res < 0) { 203 return PosixError(errno, absl::StrCat("unlink ", path)); 204 } 205 206 return NoError(); 207 } 208 209 PosixError Mkdir(absl::string_view path, int mode) { 210 int res = mkdir(std::string(path).c_str(), mode); 211 if (res < 0) { 212 return PosixError(errno, 213 absl::StrFormat("mkdir \"%s\" mode %#o", path, mode)); 214 } 215 216 return NoError(); 217 } 218 219 PosixError Rmdir(absl::string_view path) { 220 int res = rmdir(std::string(path).c_str()); 221 if (res < 0) { 222 return PosixError(errno, absl::StrCat("rmdir ", path)); 223 } 224 225 return NoError(); 226 } 227 228 PosixError SetContents(absl::string_view path, absl::string_view contents) { 229 ASSIGN_OR_RETURN_ERRNO(bool exists, Exists(path)); 230 if (!exists) { 231 return PosixError( 232 ENOENT, absl::StrCat("SetContents file ", path, " doesn't exist.")); 233 } 234 235 ASSIGN_OR_RETURN_ERRNO(auto fd, Open(std::string(path), O_WRONLY | O_TRUNC)); 236 return WriteContentsToFD(fd.get(), contents); 237 } 238 239 // Create a file with the given contents (if it does not already exist with the 240 // given mode) and then set the contents. 241 PosixError CreateWithContents(absl::string_view path, 242 absl::string_view contents, int mode) { 243 ASSIGN_OR_RETURN_ERRNO( 244 auto fd, Open(std::string(path), O_WRONLY | O_CREAT | O_TRUNC, mode)); 245 return WriteContentsToFD(fd.get(), contents); 246 } 247 248 PosixError GetContents(absl::string_view path, std::string* output) { 249 ASSIGN_OR_RETURN_ERRNO(auto fd, Open(std::string(path), O_RDONLY)); 250 output->clear(); 251 252 // Keep reading until we hit an EOF or an error. 253 return GetContentsFD(fd.get(), output); 254 } 255 256 PosixErrorOr<std::string> GetContents(absl::string_view path) { 257 std::string ret; 258 RETURN_IF_ERRNO(GetContents(path, &ret)); 259 return ret; 260 } 261 262 PosixErrorOr<std::string> GetContentsFD(int fd) { 263 std::string ret; 264 RETURN_IF_ERRNO(GetContentsFD(fd, &ret)); 265 return ret; 266 } 267 268 PosixError GetContentsFD(int fd, std::string* output) { 269 // Keep reading until we hit an EOF or an error. 270 while (true) { 271 char buf[16 * 1024] = {}; // Read in 16KB chunks. 272 int bytes_read = read(fd, buf, sizeof(buf)); 273 if (bytes_read < 0) { 274 if (errno == EINTR) { 275 continue; 276 } 277 return PosixError(errno, "GetContentsFD read failure."); 278 } 279 280 if (bytes_read == 0) { 281 break; // EOF. 282 } 283 284 output->append(buf, bytes_read); 285 } 286 return NoError(); 287 } 288 289 PosixErrorOr<std::string> ReadLink(absl::string_view path) { 290 char buf[PATH_MAX + 1] = {}; 291 int ret = readlink(std::string(path).c_str(), buf, PATH_MAX); 292 if (ret < 0) { 293 return PosixError(errno, absl::StrCat("readlink ", path)); 294 } 295 296 return std::string(buf, ret); 297 } 298 299 PosixError WalkTree( 300 absl::string_view path, bool recursive, 301 const std::function<void(absl::string_view, const struct stat&)>& cb) { 302 DIR* dir = opendir(std::string(path).c_str()); 303 if (dir == nullptr) { 304 return PosixError(errno, absl::StrCat("opendir ", path)); 305 } 306 auto dir_closer = Cleanup([&dir]() { closedir(dir); }); 307 while (true) { 308 // Readdir(3): If the end of the directory stream is reached, NULL is 309 // returned and errno is not changed. If an error occurs, NULL is returned 310 // and errno is set appropriately. To distinguish end of stream and from an 311 // error, set errno to zero before calling readdir() and then check the 312 // value of errno if NULL is returned. 313 errno = 0; 314 struct dirent* dp = readdir(dir); 315 if (dp == nullptr) { 316 if (errno != 0) { 317 return PosixError(errno, absl::StrCat("readdir ", path)); 318 } 319 break; // We're done. 320 } 321 322 if (strcmp(dp->d_name, ".") == 0 || strcmp(dp->d_name, "..") == 0) { 323 // Skip dots. 324 continue; 325 } 326 327 auto full_path = JoinPath(path, dp->d_name); 328 ASSIGN_OR_RETURN_ERRNO(struct stat s, Stat(full_path)); 329 if (S_ISDIR(s.st_mode) && recursive) { 330 RETURN_IF_ERRNO(WalkTree(full_path, recursive, cb)); 331 } else { 332 cb(full_path, s); 333 } 334 } 335 // We're done walking so let's invoke our cleanup callback now. 336 dir_closer.Release()(); 337 338 // And we have to dispatch the callback on the base directory. 339 ASSIGN_OR_RETURN_ERRNO(struct stat s, Stat(path)); 340 cb(path, s); 341 342 return NoError(); 343 } 344 345 PosixErrorOr<std::vector<std::string>> ListDir(absl::string_view abspath, 346 bool skipdots) { 347 std::vector<std::string> files; 348 349 DIR* dir = opendir(std::string(abspath).c_str()); 350 if (dir == nullptr) { 351 return PosixError(errno, absl::StrCat("opendir ", abspath)); 352 } 353 auto dir_closer = Cleanup([&dir]() { closedir(dir); }); 354 while (true) { 355 // Readdir(3): If the end of the directory stream is reached, NULL is 356 // returned and errno is not changed. If an error occurs, NULL is returned 357 // and errno is set appropriately. To distinguish end of stream and from an 358 // error, set errno to zero before calling readdir() and then check the 359 // value of errno if NULL is returned. 360 errno = 0; 361 struct dirent* dp = readdir(dir); 362 if (dp == nullptr) { 363 if (errno != 0) { 364 return PosixError(errno, absl::StrCat("readdir ", abspath)); 365 } 366 break; // We're done. 367 } 368 369 if (strcmp(dp->d_name, ".") == 0 || strcmp(dp->d_name, "..") == 0) { 370 if (skipdots) { 371 continue; 372 } 373 } 374 files.push_back(std::string(dp->d_name)); 375 } 376 377 return files; 378 } 379 380 PosixError DirContains(absl::string_view path, 381 const std::vector<std::string>& expect, 382 const std::vector<std::string>& exclude) { 383 ASSIGN_OR_RETURN_ERRNO(auto listing, ListDir(path, false)); 384 385 for (auto& expected_entry : expect) { 386 auto cursor = std::find(listing.begin(), listing.end(), expected_entry); 387 if (cursor == listing.end()) { 388 return PosixError(ENOENT, absl::StrFormat("Failed to find '%s' in '%s'", 389 expected_entry, path)); 390 } 391 } 392 for (auto& excluded_entry : exclude) { 393 auto cursor = std::find(listing.begin(), listing.end(), excluded_entry); 394 if (cursor != listing.end()) { 395 return PosixError(ENOENT, absl::StrCat("File '", excluded_entry, 396 "' found in path '", path, "'")); 397 } 398 } 399 return NoError(); 400 } 401 402 PosixError EventuallyDirContains(absl::string_view path, 403 const std::vector<std::string>& expect, 404 const std::vector<std::string>& exclude) { 405 constexpr int kRetryCount = 100; 406 const absl::Duration kRetryDelay = absl::Milliseconds(100); 407 408 for (int i = 0; i < kRetryCount; ++i) { 409 auto res = DirContains(path, expect, exclude); 410 if (res.ok()) { 411 return res; 412 } 413 if (i < kRetryCount - 1) { 414 // Sleep if this isn't the final iteration. 415 absl::SleepFor(kRetryDelay); 416 } 417 } 418 return PosixError(ETIMEDOUT, 419 "Timed out while waiting for directory to contain files "); 420 } 421 422 PosixError RecursivelyDelete(absl::string_view path, int* undeleted_dirs, 423 int* undeleted_files) { 424 ASSIGN_OR_RETURN_ERRNO(bool exists, Exists(path)); 425 if (!exists) { 426 return PosixError(ENOENT, absl::StrCat(path, " does not exist")); 427 } 428 429 ASSIGN_OR_RETURN_ERRNO(bool dir, IsDirectory(path)); 430 if (!dir) { 431 // Nothing recursive needs to happen we can just call Delete. 432 auto status = Delete(path); 433 if (!status.ok() && undeleted_files) { 434 (*undeleted_files)++; 435 } 436 return status; 437 } 438 439 return WalkTree(path, /*recursive=*/true, 440 [&](absl::string_view absolute_path, const struct stat& s) { 441 if (S_ISDIR(s.st_mode)) { 442 auto rm_status = Rmdir(absolute_path); 443 if (!rm_status.ok() && undeleted_dirs) { 444 (*undeleted_dirs)++; 445 } 446 } else { 447 auto delete_status = Delete(absolute_path); 448 if (!delete_status.ok() && undeleted_files) { 449 (*undeleted_files)++; 450 } 451 } 452 }); 453 } 454 455 PosixError RecursivelyCreateDir(absl::string_view path) { 456 if (path.empty() || path == "/") { 457 return PosixError(EINVAL, "Cannot create root!"); 458 } 459 460 // Does it already exist, if so we're done. 461 ASSIGN_OR_RETURN_ERRNO(bool exists, Exists(path)); 462 if (exists) { 463 return NoError(); 464 } 465 466 // Do we need to create directories under us? 467 auto dirname = Dirname(path); 468 ASSIGN_OR_RETURN_ERRNO(exists, Exists(dirname)); 469 if (!exists) { 470 RETURN_IF_ERRNO(RecursivelyCreateDir(dirname)); 471 } 472 473 return Mkdir(path); 474 } 475 476 // Makes a path absolute with respect to an optional base. If no base is 477 // provided it will use the current working directory. 478 PosixErrorOr<std::string> MakeAbsolute(absl::string_view filename, 479 absl::string_view base) { 480 if (filename.empty()) { 481 return PosixError(EINVAL, "filename cannot be empty."); 482 } 483 484 if (filename[0] == '/') { 485 // This path is already absolute. 486 return std::string(filename); 487 } 488 489 std::string actual_base; 490 if (!base.empty()) { 491 actual_base = std::string(base); 492 } else { 493 auto cwd_or = GetCWD(); 494 RETURN_IF_ERRNO(cwd_or.error()); 495 actual_base = cwd_or.ValueOrDie(); 496 } 497 498 // Reverse iterate removing trailing slashes, effectively right trim '/'. 499 for (int i = actual_base.size() - 1; i >= 0 && actual_base[i] == '/'; --i) { 500 actual_base.erase(i, 1); 501 } 502 503 if (filename == ".") { 504 return actual_base.empty() ? "/" : actual_base; 505 } 506 507 return absl::StrCat(actual_base, "/", filename); 508 } 509 510 std::string CleanPath(const absl::string_view unclean_path) { 511 std::string path = std::string(unclean_path); 512 const char* src = path.c_str(); 513 std::string::iterator dst = path.begin(); 514 515 // Check for absolute path and determine initial backtrack limit. 516 const bool is_absolute_path = *src == '/'; 517 if (is_absolute_path) { 518 *dst++ = *src++; 519 while (*src == '/') ++src; 520 } 521 std::string::const_iterator backtrack_limit = dst; 522 523 // Process all parts 524 while (*src) { 525 bool parsed = false; 526 527 if (src[0] == '.') { 528 // 1dot ".<whateverisnext>", check for END or SEP. 529 if (src[1] == '/' || !src[1]) { 530 if (*++src) { 531 ++src; 532 } 533 parsed = true; 534 } else if (src[1] == '.' && (src[2] == '/' || !src[2])) { 535 // 2dot END or SEP (".." | "../<whateverisnext>"). 536 src += 2; 537 if (dst != backtrack_limit) { 538 // We can backtrack the previous part 539 for (--dst; dst != backtrack_limit && dst[-1] != '/'; --dst) { 540 // Empty. 541 } 542 } else if (!is_absolute_path) { 543 // Failed to backtrack and we can't skip it either. Rewind and copy. 544 src -= 2; 545 *dst++ = *src++; 546 *dst++ = *src++; 547 if (*src) { 548 *dst++ = *src; 549 } 550 // We can never backtrack over a copied "../" part so set new limit. 551 backtrack_limit = dst; 552 } 553 if (*src) { 554 ++src; 555 } 556 parsed = true; 557 } 558 } 559 560 // If not parsed, copy entire part until the next SEP or EOS. 561 if (!parsed) { 562 while (*src && *src != '/') { 563 *dst++ = *src++; 564 } 565 if (*src) { 566 *dst++ = *src++; 567 } 568 } 569 570 // Skip consecutive SEP occurrences 571 while (*src == '/') { 572 ++src; 573 } 574 } 575 576 // Calculate and check the length of the cleaned path. 577 int path_length = dst - path.begin(); 578 if (path_length != 0) { 579 // Remove trailing '/' except if it is root path ("/" ==> path_length := 1) 580 if (path_length > 1 && path[path_length - 1] == '/') { 581 --path_length; 582 } 583 path.resize(path_length); 584 } else { 585 // The cleaned path is empty; assign "." as per the spec. 586 path.assign(1, '.'); 587 } 588 return path; 589 } 590 591 PosixErrorOr<std::string> GetRelativePath(absl::string_view source, 592 absl::string_view dest) { 593 if (!absl::StartsWith(source, "/") || !absl::StartsWith(dest, "/")) { 594 // At least one of the inputs is not an absolute path. 595 return PosixError( 596 EINVAL, 597 "GetRelativePath: At least one of the inputs is not an absolute path."); 598 } 599 const std::string clean_source = CleanPath(source); 600 const std::string clean_dest = CleanPath(dest); 601 auto source_parts = absl::StrSplit(clean_source, '/', absl::SkipEmpty()); 602 auto dest_parts = absl::StrSplit(clean_dest, '/', absl::SkipEmpty()); 603 auto source_iter = source_parts.begin(); 604 auto dest_iter = dest_parts.begin(); 605 606 // Advance past common prefix. 607 while (source_iter != source_parts.end() && dest_iter != dest_parts.end() && 608 *source_iter == *dest_iter) { 609 ++source_iter; 610 ++dest_iter; 611 } 612 613 // Build result backtracking. 614 std::string result = ""; 615 while (source_iter != source_parts.end()) { 616 absl::StrAppend(&result, "../"); 617 ++source_iter; 618 } 619 620 // Add remaining path to dest. 621 while (dest_iter != dest_parts.end()) { 622 absl::StrAppend(&result, *dest_iter, "/"); 623 ++dest_iter; 624 } 625 626 if (result.empty()) { 627 return std::string("."); 628 } 629 630 // Remove trailing slash. 631 result.erase(result.size() - 1); 632 return result; 633 } 634 635 absl::string_view Dirname(absl::string_view path) { 636 return SplitPath(path).first; 637 } 638 639 absl::string_view Basename(absl::string_view path) { 640 return SplitPath(path).second; 641 } 642 643 std::pair<absl::string_view, absl::string_view> SplitPath( 644 absl::string_view path) { 645 std::string::size_type pos = path.find_last_of('/'); 646 647 // Handle the case with no '/' in 'path'. 648 if (pos == absl::string_view::npos) { 649 return std::make_pair(path.substr(0, 0), path); 650 } 651 652 // Handle the case with a single leading '/' in 'path'. 653 if (pos == 0) { 654 return std::make_pair(path.substr(0, 1), absl::ClippedSubstr(path, 1)); 655 } 656 657 return std::make_pair(path.substr(0, pos), 658 absl::ClippedSubstr(path, pos + 1)); 659 } 660 661 std::string JoinPath(absl::string_view path1, absl::string_view path2) { 662 if (path1.empty()) { 663 return std::string(path2); 664 } 665 if (path2.empty()) { 666 return std::string(path1); 667 } 668 669 if (path1.back() == '/') { 670 if (path2.front() == '/') { 671 return absl::StrCat(path1, absl::ClippedSubstr(path2, 1)); 672 } 673 } else { 674 if (path2.front() != '/') { 675 return absl::StrCat(path1, "/", path2); 676 } 677 } 678 return absl::StrCat(path1, path2); 679 } 680 681 PosixErrorOr<std::string> ProcessExePath(int pid) { 682 if (pid <= 0) { 683 return PosixError(EINVAL, "Invalid pid specified"); 684 } 685 686 return ReadLink(absl::StrCat("/proc/", pid, "/exe")); 687 } 688 689 #ifdef __linux__ 690 PosixErrorOr<bool> IsTmpfs(const std::string& path) { 691 struct statfs stat; 692 if (statfs(path.c_str(), &stat)) { 693 if (errno == ENOENT) { 694 // Nothing at path, don't raise this as an error. Instead, just report no 695 // tmpfs at path. 696 return false; 697 } 698 return PosixError(errno, 699 absl::StrFormat("statfs(\"%s\", %#p)", path, &stat)); 700 } 701 return stat.f_type == TMPFS_MAGIC; 702 } 703 #endif // __linux__ 704 705 PosixErrorOr<bool> IsOverlayfs(const std::string& path) { 706 struct statfs stat; 707 if (statfs(path.c_str(), &stat)) { 708 if (errno == ENOENT) { 709 // Nothing at path, don't raise this as an error. Instead, just report no 710 // overlayfs at path. 711 return false; 712 } 713 return PosixError(errno, 714 absl::StrFormat("statfs(\"%s\", %#p)", path, &stat)); 715 } 716 return stat.f_type == OVERLAYFS_SUPER_MAGIC; 717 } 718 719 PosixError CheckSameFile(const FileDescriptor& fd1, const FileDescriptor& fd2) { 720 struct stat stat_result1, stat_result2; 721 int res = fstat(fd1.get(), &stat_result1); 722 if (res < 0) { 723 return PosixError(errno, absl::StrCat("fstat ", fd1.get())); 724 } 725 726 res = fstat(fd2.get(), &stat_result2); 727 if (res < 0) { 728 return PosixError(errno, absl::StrCat("fstat ", fd2.get())); 729 } 730 EXPECT_EQ(stat_result1.st_dev, stat_result2.st_dev); 731 EXPECT_EQ(stat_result1.st_ino, stat_result2.st_ino); 732 733 return NoError(); 734 } 735 736 ::testing::Matcher<mode_t> PermissionIs(mode_t want) { 737 return MakeMatcher(new ModePermissionMatcher(want)); 738 } 739 740 } // namespace testing 741 } // namespace gvisor