blob: bf249c2e2600b795e025e47640f7fbeb70a3530d [file] [log] [blame]
// Copyright 2016 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.
#include <assert.h>
#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
#include <string>
#include <fbl/string.h>
#include <fbl/string_printf.h>
#include <fbl/unique_fd.h>
#include "src/storage/fs_test/fs_test_fixture.h"
#include "src/storage/fs_test/misc.h"
namespace fs_test {
namespace {
using DirectoryTest = FilesystemTest;
TEST_P(DirectoryTest, DirectoryFilenameMax) {
// TODO(smklein): This value may be filesystem-specific. Plumb it through
// from the test driver.
constexpr int max_file_len = 255;
// Unless the max_file_len is approaching half of PATH_MAX,
// this shouldn't be an issue.
static_assert((2 /* '::' */ + (max_file_len + 1) + 1 /* slash */ + max_file_len) < PATH_MAX);
// Large components should not crash vfs
std::string path =
GetPath(fbl::StringPrintf("%0*d/%0*d", max_file_len + 1, 0xBEEF, max_file_len, 0xBEEF));
ASSERT_EQ(open(path.c_str(), O_RDWR | O_CREAT | O_EXCL, 0644), -1);
ASSERT_EQ(errno, ENAMETOOLONG);
// Largest possible file length
path = GetPath(fbl::StringPrintf("%0*d", max_file_len, 0x1337));
fbl::unique_fd fd(open(path.c_str(), O_RDWR | O_CREAT | O_EXCL, 0644));
ASSERT_TRUE(fd);
ASSERT_EQ(close(fd.release()), 0);
ASSERT_EQ(unlink(path.c_str()), 0);
// Slightly too large file length
path = GetPath(fbl::StringPrintf("%0*d", max_file_len + 1, 0xBEEF));
ASSERT_EQ(open(path.c_str(), O_RDWR | O_CREAT | O_EXCL, 0644), -1);
ASSERT_EQ(errno, ENAMETOOLONG);
}
// Hopefully not pushing against any 'max file length' boundaries, but large enough to fill a
// directory quickly.
constexpr int kLargePathLength = 128;
TEST_P(DirectoryTest, Large) {
// Write a bunch of files to a directory.
// FAT is very slow (see fxbug.dev/56389), so limit the number of files for that filesystem.
const int num_files = fs().GetTraits().is_fat ? 100 : 1024;
for (int i = 0; i < num_files; i++) {
char path[kLargePathLength + 1];
snprintf(path, sizeof(path), "%0*d", kLargePathLength - 2, i);
fbl::unique_fd fd(open(GetPath(path).c_str(), O_RDWR | O_CREAT | O_EXCL, 0644));
ASSERT_TRUE(fd) << path << ": " << strerror(errno);
}
// Unlink all those files
for (int i = 0; i < num_files; i++) {
char path[kLargePathLength + 1];
snprintf(path, sizeof(path), "%0*d", kLargePathLength - 2, i);
ASSERT_EQ(unlink(GetPath(path).c_str()), 0);
}
// TODO(smklein): Verify contents
}
void TestDirectoryCoalesceHelper(const std::string& base_path, const int* unlink_order) {
const std::string files[] = {
base_path + "/aaaaaaaa", base_path + "/bbbbbbbb", base_path + "/cccccccc",
base_path + "/dddddddd", base_path + "/eeeeeeee",
};
int num_files = std::size(files);
// Allocate a bunch of files in a directory
ASSERT_EQ(mkdir(base_path.c_str(), 0755), 0);
for (int i = 0; i < num_files; i++) {
fbl::unique_fd fd(open(files[i].c_str(), O_RDWR | O_CREAT | O_EXCL, 0644));
ASSERT_TRUE(fd);
}
// Unlink all those files in the order specified
for (int i = 0; i < num_files; i++) {
assert(0 <= unlink_order[i] && unlink_order[i] < num_files);
ASSERT_EQ(unlink(files[unlink_order[i]].c_str()), 0);
}
ASSERT_EQ(rmdir(base_path.c_str()), 0);
}
TEST_P(DirectoryTest, DirectoryCoalesce) {
// Test some cases of coalescing, assuming the directory was filled
// according to allocation order. If it wasn't, this test should still pass,
// but there is no mechanism to check the "location of a direntry in a
// directory", so this is our best shot at "poking" the filesystem to try to
// coalesce.
// Case 1: Test merge-with-left
const int merge_with_left[] = {0, 1, 2, 3, 4};
ASSERT_NO_FATAL_FAILURE(TestDirectoryCoalesceHelper(GetPath("coalesce"), merge_with_left));
// Case 2: Test merge-with-right
const int merge_with_right[] = {4, 3, 2, 1, 0};
ASSERT_NO_FATAL_FAILURE(TestDirectoryCoalesceHelper(GetPath("coalesce"), merge_with_right));
// Case 3: Test merge-with-both
const int merge_with_both[] = {1, 3, 2, 0, 4};
ASSERT_NO_FATAL_FAILURE(TestDirectoryCoalesceHelper(GetPath("coalesce"), merge_with_both));
}
// This test prevents the regression of an fsck bug, which could also
// occur in a filesystem which does similar checks at runtime.
//
// This test ensures that if multiple large direntries are created
// and coalesced, the 'last remaining entry' still has a valid size,
// even though it may be quite large.
TEST_P(DirectoryTest, TestDirectoryCoalesceLargeRecord) {
char buf[NAME_MAX + 1];
ASSERT_EQ(mkdir(GetPath("coalesce_lr").c_str(), 0666), 0);
fbl::unique_fd dirfd(open(GetPath("coalesce_lr").c_str(), O_RDONLY | O_DIRECTORY));
ASSERT_GT(dirfd.get(), 0);
const int kNumEntries = 20;
// Make the entries
for (int i = 0; i < kNumEntries; i++) {
memset(buf, 'a' + i, 50);
buf[50] = '\0';
ASSERT_EQ(mkdirat(dirfd.get(), buf, 0666), 0);
}
// Unlink all the entries except the last one
for (int i = 0; i < kNumEntries - 1; i++) {
memset(buf, 'a' + i, 50);
buf[50] = '\0';
ASSERT_EQ(unlinkat(dirfd.get(), buf, AT_REMOVEDIR), 0);
}
// Check that the 'large remaining entry', which may
// have a fairly large size, isn't marked as 'invalid' by
// fsck.
if (fs().GetTraits().can_unmount) {
ASSERT_EQ(close(dirfd.release()), 0);
EXPECT_EQ(fs().Unmount().status_value(), ZX_OK);
EXPECT_EQ(fs().Mount().status_value(), ZX_OK);
dirfd.reset(open(GetPath("coalesce_lr").c_str(), O_RDONLY | O_DIRECTORY));
ASSERT_GT(dirfd.get(), 0);
}
// Unlink the final entry
memset(buf, 'a' + kNumEntries - 1, 50);
buf[50] = '\0';
ASSERT_EQ(unlinkat(dirfd.get(), buf, AT_REMOVEDIR), 0);
ASSERT_EQ(close(dirfd.release()), 0);
ASSERT_EQ(rmdir(GetPath("coalesce_lr").c_str()), 0);
}
TEST_P(DirectoryTest, TestDirectoryTrailingSlash) {
// We should be able to refer to directories with any number of trailing
// slashes, and still refer to the same entity.
ASSERT_EQ(mkdir(GetPath("a").c_str(), 0755), 0);
ASSERT_EQ(mkdir(GetPath("b/").c_str(), 0755), 0);
ASSERT_EQ(mkdir(GetPath("c//").c_str(), 0755), 0);
ASSERT_EQ(mkdir(GetPath("d///").c_str(), 0755), 0);
ASSERT_EQ(rmdir(GetPath("a///").c_str()), 0);
ASSERT_EQ(rmdir(GetPath("b//").c_str()), 0);
ASSERT_EQ(rmdir(GetPath("c/").c_str()), 0);
// Before we unlink 'd', try renaming it using some trailing '/' characters.
ASSERT_EQ(rename(GetPath("d").c_str(), GetPath("e").c_str()), 0);
ASSERT_EQ(rename(GetPath("e").c_str(), GetPath("d/").c_str()), 0);
ASSERT_EQ(rename(GetPath("d/").c_str(), GetPath("e").c_str()), 0);
ASSERT_EQ(rename(GetPath("e/").c_str(), GetPath("d/").c_str()), 0);
ASSERT_EQ(rmdir(GetPath("d").c_str()), 0);
// We can make / unlink a file...
fbl::unique_fd fd(open(GetPath("a").c_str(), O_RDWR | O_CREAT | O_EXCL, 0644));
ASSERT_TRUE(fd);
ASSERT_EQ(close(fd.release()), 0);
ASSERT_EQ(unlink(GetPath("a").c_str()), 0);
// ... But we cannot refer to that file using a trailing '/'.
fd.reset(open(GetPath("a").c_str(), O_RDWR | O_CREAT | O_EXCL, 0644));
ASSERT_TRUE(fd);
ASSERT_EQ(close(fd.release()), 0);
ASSERT_EQ(open(GetPath("a/").c_str(), O_RDWR, 0644), -1);
// We can rename the file...
ASSERT_EQ(rename(GetPath("a").c_str(), GetPath("b").c_str()), 0);
// ... But neither the source (nor the destination) can have trailing slashes.
ASSERT_EQ(rename(GetPath("b").c_str(), GetPath("a/").c_str()), -1);
ASSERT_EQ(rename(GetPath("b/").c_str(), GetPath("a").c_str()), -1);
ASSERT_EQ(rename(GetPath("b/").c_str(), GetPath("a/").c_str()), -1);
ASSERT_EQ(unlink(GetPath("b/").c_str()), -1);
ASSERT_EQ(unlink(GetPath("b").c_str()), 0);
}
TEST_P(DirectoryTest, TestDirectoryReaddir) {
ASSERT_EQ(mkdir(GetPath("a").c_str(), 0755), 0);
ASSERT_EQ(mkdir(GetPath("a").c_str(), 0755), -1);
ExpectedDirectoryEntry empty_dir[] = {
{".", DT_DIR},
};
ASSERT_NO_FATAL_FAILURE(CheckDirectoryContents(GetPath("a").c_str(), empty_dir));
ASSERT_EQ(mkdir(GetPath("a/dir1").c_str(), 0755), 0);
fbl::unique_fd fd(open(GetPath("a/file1").c_str(), O_RDWR | O_CREAT | O_EXCL, 0644));
ASSERT_TRUE(fd);
ASSERT_EQ(close(fd.release()), 0);
fd.reset(open(GetPath("a/file2").c_str(), O_RDWR | O_CREAT | O_EXCL, 0644));
ASSERT_TRUE(fd);
ASSERT_EQ(close(fd.release()), 0);
ASSERT_EQ(mkdir(GetPath("a/dir2").c_str(), 0755), 0);
ExpectedDirectoryEntry filled_dir[] = {
{".", DT_DIR}, {"dir1", DT_DIR}, {"dir2", DT_DIR}, {"file1", DT_REG}, {"file2", DT_REG},
};
ASSERT_NO_FATAL_FAILURE(CheckDirectoryContents(GetPath("a").c_str(), filled_dir));
ASSERT_EQ(rmdir(GetPath("a/dir2").c_str()), 0);
ASSERT_EQ(unlink(GetPath("a/file2").c_str()), 0);
ExpectedDirectoryEntry partial_dir[] = {
{".", DT_DIR},
{"dir1", DT_DIR},
{"file1", DT_REG},
};
ASSERT_NO_FATAL_FAILURE(CheckDirectoryContents(GetPath("a").c_str(), partial_dir));
ASSERT_EQ(rmdir(GetPath("a/dir1").c_str()), 0);
ASSERT_EQ(unlink(GetPath("a/file1").c_str()), 0);
ASSERT_NO_FATAL_FAILURE(CheckDirectoryContents(GetPath("a").c_str(), empty_dir));
ASSERT_EQ(unlink(GetPath("a").c_str()), 0);
}
TEST_P(DirectoryTest, TestDirectoryReaddirRmAll) {
// FAT is very slow (see fxbug.dev/56389), so limit the number of directories for that filesystem.
const size_t kNumEntries = fs().GetTraits().is_fat ? 100 : 1000;
// Create a directory named GetPath("dir").c_str() with entries "00000", "00001" ... up to
// kNumEntries.
ASSERT_EQ(mkdir(GetPath("dir").c_str(), 0755), 0);
// Create a large directory (ideally, large enough that our libc implementation can't cache the
// entire contents of the directory with one 'getdirents' call).
for (size_t i = 0; i < kNumEntries; i++) {
char dirname[100];
snprintf(dirname, 100, GetPath("dir/%05lu").c_str(), i);
ASSERT_EQ(mkdir(dirname, 0755), 0);
}
DIR* dir = opendir(GetPath("dir").c_str());
ASSERT_NE(dir, nullptr);
// Unlink all the entries as we read them.
struct dirent* de;
size_t num_seen = 0;
size_t i = 0;
while ((de = readdir(dir)) != NULL) {
if (!strcmp(de->d_name, ".") || !strcmp(de->d_name, "..")) {
// Ignore these entries
continue;
}
char dirname[100];
snprintf(dirname, 100, "%05lu", i++);
ASSERT_EQ(strcmp(de->d_name, dirname), 0) << "Unexpected dirent";
ASSERT_EQ(unlinkat(dirfd(dir), dirname, AT_REMOVEDIR), 0);
num_seen++;
}
ASSERT_EQ(num_seen, kNumEntries) << "Did not see all expected entries";
ASSERT_EQ(closedir(dir), 0);
ASSERT_EQ(rmdir(GetPath("dir").c_str()), 0);
}
TEST_P(DirectoryTest, TestDirectoryCreateAlternatingThenDeleteSucceeds) {
ASSERT_EQ(mkdir(GetPath("a").c_str(), 0755), 0);
ASSERT_EQ(mkdir(GetPath("b").c_str(), 0755), 0);
constexpr ssize_t kNumEntries = 200;
std::vector<fbl::unique_fd> fds;
for (ssize_t i = 0; i < kNumEntries; i++) {
// Create the files, leaving them open.
fbl::unique_fd fd(open(GetPath(fbl::StringPrintf("a/%zd", i)).c_str(), O_CREAT | O_RDWR));
ASSERT_TRUE(fd) << "a/" << i << ": " << strerror(errno);
fds.push_back(std::move(fd));
fd = fbl::unique_fd(open(GetPath(fbl::StringPrintf("b/%zd", i)).c_str(), O_CREAT | O_RDWR));
ASSERT_TRUE(fd) << "b/" << i << ": " << strerror(errno);
fds.push_back(std::move(fd));
}
for (ssize_t i = kNumEntries - 1; i >= 0; i--) {
// Delete the files from one directory.
ASSERT_EQ(unlink(GetPath(fbl::StringPrintf("a/%zd", i)).c_str()), 0)
<< "unlink a/" << i << ": " << strerror(errno);
}
}
TEST_P(DirectoryTest, TestDirectoryRewind) {
ASSERT_EQ(mkdir(GetPath("a").c_str(), 0755), 0);
ExpectedDirectoryEntry empty_dir[] = {
{".", DT_DIR},
};
DIR* dir = opendir(GetPath("a").c_str());
ASSERT_NE(dir, nullptr);
// We should be able to repeatedly access the directory without
// re-opening it.
ASSERT_NO_FATAL_FAILURE(CheckDirectoryContents(dir, empty_dir));
ASSERT_NO_FATAL_FAILURE(CheckDirectoryContents(dir, empty_dir));
ASSERT_EQ(mkdirat(dirfd(dir), "b", 0755), 0);
ASSERT_EQ(mkdirat(dirfd(dir), "c", 0755), 0);
// We should be able to modify the directory and re-process it without
// re-opening it.
ExpectedDirectoryEntry dir_contents[] = {
{".", DT_DIR},
{"b", DT_DIR},
{"c", DT_DIR},
};
ASSERT_NO_FATAL_FAILURE(CheckDirectoryContents(dir, dir_contents));
ASSERT_NO_FATAL_FAILURE(CheckDirectoryContents(dir, dir_contents));
ASSERT_EQ(rmdir(GetPath("a/b").c_str()), 0);
ASSERT_EQ(rmdir(GetPath("a/c").c_str()), 0);
ASSERT_NO_FATAL_FAILURE(CheckDirectoryContents(dir, empty_dir));
ASSERT_NO_FATAL_FAILURE(CheckDirectoryContents(dir, empty_dir));
ASSERT_EQ(closedir(dir), 0);
ASSERT_EQ(rmdir(GetPath("a").c_str()), 0);
}
TEST_P(DirectoryTest, DirectoryAfterRmdir) {
ExpectedDirectoryEntry empty_dir[] = {
{".", DT_DIR},
};
// Make a directory...
ASSERT_EQ(mkdir(GetPath("dir").c_str(), 0755), 0);
DIR* dir = opendir(GetPath("dir").c_str());
ASSERT_NE(dir, nullptr);
// We can make and delete subdirectories, since GetPath("dir").c_str() exists...
ASSERT_EQ(mkdir(GetPath("dir/subdir").c_str(), 0755), 0);
ASSERT_EQ(rmdir(GetPath("dir/subdir").c_str()), 0);
ASSERT_NO_FATAL_FAILURE(CheckDirectoryContents(dir, empty_dir));
// Remove the directory. It's still open, so it should appear empty.
ASSERT_EQ(rmdir(GetPath("dir").c_str()), 0);
ASSERT_NO_FATAL_FAILURE(CheckDirectoryContents(dir, {}));
// But we can't make new files / directories, by path...
ASSERT_EQ(mkdir(GetPath("dir/subdir").c_str(), 0755), -1);
// ... Or with the open fd.
fbl::unique_fd fd(dirfd(dir));
ASSERT_TRUE(fd);
ASSERT_EQ(openat(fd.get(), "file", O_CREAT | O_RDWR), -1)
<< "Can't make new files in deleted dirs";
ASSERT_EQ(mkdirat(fd.get(), "dir", 0755), -1) << "Can't make new files in deleted dirs";
// In fact, the "dir" path should still be usable, even as a file!
fbl::unique_fd fd2(open(GetPath("dir").c_str(), O_CREAT | O_EXCL | O_RDWR));
ASSERT_TRUE(fd2);
ASSERT_NO_FATAL_FAILURE(CheckDirectoryContents(dir, {}));
ASSERT_EQ(close(fd2.release()), 0);
ASSERT_EQ(unlink(GetPath("dir").c_str()), 0);
// After all that, dir still looks like an empty directory...
ASSERT_NO_FATAL_FAILURE(CheckDirectoryContents(dir, {}));
ASSERT_EQ(closedir(dir), 0);
}
TEST_P(DirectoryTest, RenameIntoUnlinkedDirectoryFails) {
ASSERT_EQ(mkdir(GetPath("foo").c_str(), 0755), 0);
fbl::unique_fd foo_fd(open(GetPath("foo").c_str(), O_RDONLY | O_DIRECTORY, 0644));
ASSERT_TRUE(foo_fd);
fbl::unique_fd baz_fd(open(GetPath("baz").c_str(), O_CREAT | O_RDWR));
ASSERT_TRUE(baz_fd);
fbl::unique_fd root_fd(open(GetPath("").c_str(), O_RDONLY | O_DIRECTORY, 0644));
ASSERT_TRUE(root_fd);
ASSERT_EQ(renameat(root_fd.get(), "baz", foo_fd.get(), "baz"), 0);
ASSERT_EQ(renameat(foo_fd.get(), "baz", root_fd.get(), "baz"), 0);
ASSERT_EQ(unlink(GetPath("foo").c_str()), 0);
ASSERT_EQ(renameat(root_fd.get(), "baz", foo_fd.get(), "baz"), -1);
ASSERT_EQ(errno, ENOENT);
}
INSTANTIATE_TEST_SUITE_P(/*no prefix*/, DirectoryTest, testing::ValuesIn(AllTestFilesystems()),
testing::PrintToStringParamName());
} // namespace
} // namespace fs_test