blob: 1d247db8db65a5b831979fcbf51f4939251c1e7d [file] [log] [blame]
// Copyright 2017 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 <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <unistd.h>
#include <algorithm>
#include <array>
#include <optional>
#include <random>
#include <vector>
#include "src/storage/fs_test/fs_test_fixture.h"
#include "src/storage/fs_test/misc.h"
namespace fs_test {
namespace {
using HardLinkTest = FilesystemTest;
void CheckLinkCount(const std::string& path, unsigned count) {
struct stat s;
ASSERT_EQ(stat(path.c_str(), &s), 0);
ASSERT_EQ(s.st_nlink, count);
}
TEST_P(HardLinkTest, Basic) {
const std::string old_path = GetPath("a");
const std::string new_path = GetPath("b");
// Make a file, fill it with content
int fd = open(old_path.c_str(), O_RDWR | O_CREAT | O_EXCL, 0644);
ASSERT_GT(fd, 0);
std::array<uint8_t, 100> buf;
std::generate(buf.begin(), buf.end(), rand);
ASSERT_EQ(write(fd, buf.data(), buf.size()), static_cast<ssize_t>(buf.size()));
ASSERT_NO_FATAL_FAILURE(CheckFileContents(fd, buf));
ASSERT_NO_FATAL_FAILURE(CheckLinkCount(old_path, 1));
ASSERT_EQ(link(old_path.c_str(), new_path.c_str()), 0);
ASSERT_NO_FATAL_FAILURE(CheckLinkCount(old_path, 2));
ASSERT_NO_FATAL_FAILURE(CheckLinkCount(new_path, 2));
// Confirm that both the old link and the new links exist
int fd2 = open(new_path.c_str(), O_RDONLY, 0644);
ASSERT_GT(fd2, 0);
ASSERT_NO_FATAL_FAILURE(CheckFileContents(fd2, buf));
ASSERT_NO_FATAL_FAILURE(CheckFileContents(fd, buf));
// Remove the old link
ASSERT_EQ(close(fd), 0);
ASSERT_EQ(close(fd2), 0);
ASSERT_EQ(unlink(old_path.c_str()), 0);
ASSERT_NO_FATAL_FAILURE(CheckLinkCount(new_path, 1));
// Open the link by its new name, and verify that the contents have
// not been altered by the removal of the old link.
fd = open(new_path.c_str(), O_RDONLY, 0644);
ASSERT_GT(fd, 0);
ASSERT_NO_FATAL_FAILURE(CheckFileContents(fd, buf));
ASSERT_EQ(close(fd), 0);
ASSERT_EQ(unlink(new_path.c_str()), 0);
}
TEST_P(HardLinkTest, test_link_count_dirs) {
ASSERT_EQ(mkdir(GetPath("dira").c_str(), 0755), 0);
// New directories should have two links:
// Parent --> newdir
// newdir ('.') --> newdir
ASSERT_NO_FATAL_FAILURE(CheckLinkCount(GetPath("dira"), 2));
// Adding a file won't change the parent link count...
int fd = open(GetPath("dira/file").c_str(), O_RDWR | O_CREAT | O_EXCL, 0644);
ASSERT_GT(fd, 0);
ASSERT_EQ(close(fd), 0);
ASSERT_NO_FATAL_FAILURE(CheckLinkCount(GetPath("dira"), 2));
ASSERT_NO_FATAL_FAILURE(CheckLinkCount(GetPath("dira/file"), 1));
// But adding a directory WILL change the parent link count.
ASSERT_EQ(mkdir(GetPath("dira/dirb").c_str(), 0755), 0);
ASSERT_NO_FATAL_FAILURE(CheckLinkCount(GetPath("dira"), 3));
ASSERT_NO_FATAL_FAILURE(CheckLinkCount(GetPath("dira/dirb"), 2));
// Test that adding "depth" increases the dir count as we expect.
ASSERT_EQ(mkdir(GetPath("dira/dirb/dirc").c_str(), 0755), 0);
ASSERT_NO_FATAL_FAILURE(CheckLinkCount(GetPath("dira"), 3));
ASSERT_NO_FATAL_FAILURE(CheckLinkCount(GetPath("dira/dirb"), 3));
ASSERT_NO_FATAL_FAILURE(CheckLinkCount(GetPath("dira/dirb/dirc"), 2));
// Demonstrate that unwinding also reduces the link count.
ASSERT_EQ(unlink(GetPath("dira/dirb/dirc").c_str()), 0);
ASSERT_NO_FATAL_FAILURE(CheckLinkCount(GetPath("dira"), 3));
ASSERT_NO_FATAL_FAILURE(CheckLinkCount(GetPath("dira/dirb"), 2));
ASSERT_EQ(unlink(GetPath("dira/dirb").c_str()), 0);
ASSERT_NO_FATAL_FAILURE(CheckLinkCount(GetPath("dira"), 2));
// Test that adding "width" increases the dir count too.
ASSERT_EQ(mkdir(GetPath("dira/dirb").c_str(), 0755), 0);
ASSERT_NO_FATAL_FAILURE(CheckLinkCount(GetPath("dira"), 3));
ASSERT_NO_FATAL_FAILURE(CheckLinkCount(GetPath("dira/dirb"), 2));
ASSERT_EQ(mkdir(GetPath("dira/dirc").c_str(), 0755), 0);
ASSERT_NO_FATAL_FAILURE(CheckLinkCount(GetPath("dira"), 4));
ASSERT_NO_FATAL_FAILURE(CheckLinkCount(GetPath("dira/dirb"), 2));
ASSERT_NO_FATAL_FAILURE(CheckLinkCount(GetPath("dira/dirc"), 2));
// Demonstrate that unwinding also reduces the link count.
ASSERT_EQ(unlink(GetPath("dira/dirc").c_str()), 0);
ASSERT_NO_FATAL_FAILURE(CheckLinkCount(GetPath("dira"), 3));
ASSERT_NO_FATAL_FAILURE(CheckLinkCount(GetPath("dira/dirb"), 2));
ASSERT_EQ(unlink(GetPath("dira/dirb").c_str()), 0);
ASSERT_NO_FATAL_FAILURE(CheckLinkCount(GetPath("dira"), 2));
ASSERT_EQ(unlink(GetPath("dira/file").c_str()), 0);
ASSERT_EQ(unlink(GetPath("dira").c_str()), 0);
}
TEST_P(HardLinkTest, CorrectLinkCountAfterRename) {
// Check that link count does not change with simple rename
ASSERT_EQ(mkdir(GetPath("dir").c_str(), 0755), 0);
ASSERT_NO_FATAL_FAILURE(CheckLinkCount(GetPath("dir"), 2));
ASSERT_EQ(rename(GetPath("dir").c_str(), GetPath("dir_parent").c_str()), 0);
ASSERT_NO_FATAL_FAILURE(CheckLinkCount(GetPath("dir_parent"), 2));
// Set up parent directory with child directories
ASSERT_EQ(mkdir(GetPath("dir_parent/dir_child_a").c_str(), 0755), 0);
ASSERT_EQ(mkdir(GetPath("dir_parent/dir_child_b").c_str(), 0755), 0);
ASSERT_NO_FATAL_FAILURE(CheckLinkCount(GetPath("dir_parent"), 4));
ASSERT_NO_FATAL_FAILURE(CheckLinkCount(GetPath("dir_parent/dir_child_a"), 2));
ASSERT_NO_FATAL_FAILURE(CheckLinkCount(GetPath("dir_parent/dir_child_b"), 2));
// Rename a child directory out of its parent directory
ASSERT_EQ(rename(GetPath("dir_parent/dir_child_b").c_str(), GetPath("dir_parent_alt").c_str()),
0);
ASSERT_NO_FATAL_FAILURE(CheckLinkCount(GetPath("dir_parent"), 3));
ASSERT_NO_FATAL_FAILURE(CheckLinkCount(GetPath("dir_parent/dir_child_a"), 2));
ASSERT_NO_FATAL_FAILURE(CheckLinkCount(GetPath("dir_parent_alt"), 2));
// Rename a parent directory into another directory
ASSERT_EQ(
rename(GetPath("dir_parent").c_str(), GetPath("dir_parent_alt/dir_semi_parent").c_str()), 0);
ASSERT_NO_FATAL_FAILURE(CheckLinkCount(GetPath("dir_parent_alt"), 3));
ASSERT_NO_FATAL_FAILURE(CheckLinkCount(GetPath("dir_parent_alt/dir_semi_parent"), 3));
ASSERT_NO_FATAL_FAILURE(CheckLinkCount(GetPath("dir_parent_alt/dir_semi_parent/dir_child_a"), 2));
// Rename a directory on top of an empty directory
ASSERT_EQ(mkdir(GetPath("dir_child").c_str(), 0755), 0);
ASSERT_EQ(rename(GetPath("dir_child").c_str(),
GetPath("dir_parent_alt/dir_semi_parent/dir_child_a").c_str()),
0);
ASSERT_NO_FATAL_FAILURE(CheckLinkCount(GetPath("dir_parent_alt"), 3));
ASSERT_NO_FATAL_FAILURE(CheckLinkCount(GetPath("dir_parent_alt/dir_semi_parent"), 3));
ASSERT_NO_FATAL_FAILURE(CheckLinkCount(GetPath("dir_parent_alt/dir_semi_parent/dir_child_a"), 2));
// Rename a directory on top of an empty directory from a non-root directory
ASSERT_EQ(mkdir(GetPath("dir").c_str(), 0755), 0);
ASSERT_EQ(mkdir(GetPath("dir/dir_child").c_str(), 0755), 0);
ASSERT_NO_FATAL_FAILURE(CheckLinkCount(GetPath("dir"), 3));
ASSERT_NO_FATAL_FAILURE(CheckLinkCount(GetPath("dir/dir_child"), 2));
ASSERT_EQ(rename(GetPath("dir/dir_child").c_str(),
GetPath("dir_parent_alt/dir_semi_parent/dir_child_a").c_str()),
0);
ASSERT_NO_FATAL_FAILURE(CheckLinkCount(GetPath("dir"), 2));
ASSERT_NO_FATAL_FAILURE(CheckLinkCount(GetPath("dir_parent_alt"), 3));
ASSERT_NO_FATAL_FAILURE(CheckLinkCount(GetPath("dir_parent_alt/dir_semi_parent"), 3));
ASSERT_NO_FATAL_FAILURE(CheckLinkCount(GetPath("dir_parent_alt/dir_semi_parent/dir_child_a"), 2));
// Rename a file on top of a file from a non-root directory
ASSERT_EQ(unlink(GetPath("dir_parent_alt/dir_semi_parent/dir_child_a").c_str()), 0);
int fd = open(GetPath("dir/dir_child").c_str(), O_RDWR | O_CREAT | O_EXCL, 0644);
ASSERT_GT(fd, 0);
ASSERT_NO_FATAL_FAILURE(CheckLinkCount(GetPath("dir"), 2));
ASSERT_NO_FATAL_FAILURE(CheckLinkCount(GetPath("dir/dir_child"), 1));
int fd2 = open(GetPath("dir_parent_alt/dir_semi_parent/dir_child_a").c_str(),
O_RDWR | O_CREAT | O_EXCL, 0644);
ASSERT_GT(fd2, 0);
ASSERT_EQ(rename(GetPath("dir/dir_child").c_str(),
GetPath("dir_parent_alt/dir_semi_parent/dir_child_a").c_str()),
0);
ASSERT_NO_FATAL_FAILURE(CheckLinkCount(GetPath("dir"), 2));
ASSERT_NO_FATAL_FAILURE(CheckLinkCount(GetPath("dir_parent_alt"), 3));
ASSERT_NO_FATAL_FAILURE(CheckLinkCount(GetPath("dir_parent_alt/dir_semi_parent"), 2));
ASSERT_NO_FATAL_FAILURE(CheckLinkCount(GetPath("dir_parent_alt/dir_semi_parent/dir_child_a"), 1));
ASSERT_EQ(close(fd), 0);
ASSERT_EQ(close(fd2), 0);
// Clean up
ASSERT_EQ(unlink(GetPath("dir_parent_alt/dir_semi_parent/dir_child_a").c_str()), 0);
ASSERT_NO_FATAL_FAILURE(CheckLinkCount(GetPath("dir_parent_alt"), 3));
ASSERT_NO_FATAL_FAILURE(CheckLinkCount(GetPath("dir_parent_alt/dir_semi_parent"), 2));
ASSERT_EQ(unlink(GetPath("dir_parent_alt/dir_semi_parent").c_str()), 0);
ASSERT_NO_FATAL_FAILURE(CheckLinkCount(GetPath("dir_parent_alt"), 2));
ASSERT_EQ(unlink(GetPath("dir_parent_alt").c_str()), 0);
ASSERT_EQ(unlink(GetPath("dir").c_str()), 0);
}
TEST_P(HardLinkTest, AcrossDirectories) {
ASSERT_EQ(mkdir(GetPath("dira").c_str(), 0755), 0);
// New directories should have two links:
// Parent --> newdir
// newdir ('.') --> newdir
ASSERT_NO_FATAL_FAILURE(CheckLinkCount(GetPath("dira"), 2));
ASSERT_EQ(mkdir(GetPath("dirb").c_str(), 0755), 0);
ASSERT_NO_FATAL_FAILURE(CheckLinkCount(GetPath("dirb"), 2));
const std::string old_path = GetPath("dira/a");
const std::string new_path = GetPath("dirb/b");
// Make a file, fill it with content
int fd = open(old_path.c_str(), O_RDWR | O_CREAT | O_EXCL, 0644);
ASSERT_GT(fd, 0);
std::array<uint8_t, 100> buf;
std::generate(buf.begin(), buf.end(), rand);
ASSERT_EQ(write(fd, buf.data(), buf.size()), static_cast<ssize_t>(buf.size()));
ASSERT_NO_FATAL_FAILURE(CheckFileContents(fd, buf));
ASSERT_EQ(link(old_path.c_str(), new_path.c_str()), 0);
// Confirm that both the old link and the new links exist
int fd2 = open(new_path.c_str(), O_RDWR, 0644);
ASSERT_GT(fd2, 0);
ASSERT_NO_FATAL_FAILURE(CheckFileContents(fd2, buf));
ASSERT_NO_FATAL_FAILURE(CheckFileContents(fd, buf));
// Remove the old link
ASSERT_EQ(close(fd), 0);
ASSERT_EQ(close(fd2), 0);
ASSERT_EQ(unlink(old_path.c_str()), 0);
// Open the link by its new name
fd = open(new_path.c_str(), O_RDWR, 0644);
ASSERT_GT(fd, 0);
ASSERT_NO_FATAL_FAILURE(CheckFileContents(fd, buf));
ASSERT_EQ(close(fd), 0);
ASSERT_EQ(unlink(new_path.c_str()), 0);
ASSERT_EQ(unlink(GetPath("dira").c_str()), 0);
ASSERT_EQ(unlink(GetPath("dirb").c_str()), 0);
}
TEST_P(HardLinkTest, Errors) {
const std::string dir_path = GetPath("dir");
const std::string old_path = GetPath("a");
const std::string new_path = GetPath("b");
const std::string new_path_dir = GetPath("b/");
// We should not be able to create hard links to directories
ASSERT_EQ(mkdir(dir_path.c_str(), 0755), 0);
ASSERT_EQ(link(dir_path.c_str(), new_path.c_str()), -1);
ASSERT_EQ(unlink(dir_path.c_str()), 0);
// We should not be able to create hard links to non-existent files
ASSERT_EQ(link(old_path.c_str(), new_path.c_str()), -1);
ASSERT_EQ(errno, ENOENT);
int fd = open(old_path.c_str(), O_RDWR | O_CREAT | O_EXCL, 0644);
ASSERT_GT(fd, 0);
ASSERT_EQ(close(fd), 0);
// We should not be able to link to or from . or ..
ASSERT_EQ(link(old_path.c_str(), GetPath(".").c_str()), -1);
ASSERT_EQ(link(old_path.c_str(), GetPath("..").c_str()), -1);
ASSERT_EQ(link(GetPath(".").c_str(), new_path.c_str()), -1);
ASSERT_EQ(link(GetPath("..").c_str(), new_path.c_str()), -1);
// We should not be able to link a file to itself
ASSERT_EQ(link(old_path.c_str(), old_path.c_str()), -1);
ASSERT_EQ(errno, EEXIST);
// We should not be able to link a file over an existing file
fd = open(new_path.c_str(), O_RDWR | O_CREAT | O_EXCL, 0644);
ASSERT_GT(fd, 0);
ASSERT_EQ(close(fd), 0);
ASSERT_EQ(link(old_path.c_str(), new_path.c_str()), -1);
ASSERT_EQ(errno, EEXIST);
ASSERT_EQ(unlink(new_path.c_str()), 0);
// We should not be able to link a file over an existing directory
ASSERT_EQ(mkdir(new_path.c_str(), 0755), 0);
ASSERT_EQ(link(old_path.c_str(), new_path.c_str()), -1);
ASSERT_EQ(errno, EEXIST);
ASSERT_EQ(unlink(new_path.c_str()), 0);
// We should not be able to link a file to a path that implies it must be a directory
ASSERT_EQ(link(old_path.c_str(), new_path_dir.c_str()), -1);
// After linking, we shouldn't be able to link again
ASSERT_EQ(link(old_path.c_str(), new_path.c_str()), 0);
ASSERT_EQ(link(old_path.c_str(), new_path.c_str()), -1);
ASSERT_EQ(errno, EEXIST);
// In either order
ASSERT_EQ(link(new_path.c_str(), old_path.c_str()), -1);
ASSERT_EQ(errno, EEXIST);
ASSERT_EQ(unlink(new_path.c_str()), 0);
ASSERT_EQ(unlink(old_path.c_str()), 0);
}
TEST_P(HardLinkTest, UnlinkRace) {
const std::string file = GetPath("a");
const char* filename = file.c_str();
std::random_device random;
std::uniform_int_distribution distribution(0, 1000);
for (int i = 0; i < 100; ++i) {
{
fbl::unique_fd fd(open(filename, O_RDWR | O_CREAT | O_EXCL, 0644));
ASSERT_TRUE(fd) << strerror(errno);
EXPECT_EQ(write(fd.get(), "hello", 5), 5);
}
std::thread thread([&] {
const std::string file2 = GetPath("b");
const char* filename2 = file2.c_str();
if (link(filename, filename2) == 0) {
// The link succeeded
fbl::unique_fd fd(open(filename2, O_RDONLY));
ASSERT_TRUE(fd);
char buf[5];
EXPECT_EQ(read(fd.get(), buf, 5), 5);
EXPECT_EQ(memcmp(buf, "hello", 5), 0);
EXPECT_EQ(unlink(filename2), 0);
} else {
ASSERT_EQ(errno, ENOENT);
}
});
int time = distribution(random);
usleep(time);
EXPECT_EQ(unlink(filename), 0) << "errno: " << errno;
thread.join();
}
}
using HardLinkUnsupportedTest = FilesystemTest;
TEST_P(HardLinkUnsupportedTest, LinkReturnsError) {
const std::string path = GetPath("a");
const std::string newpath = GetPath("b");
fbl::unique_fd fd(open(path.c_str(), O_RDWR | O_CREAT, 0644));
ASSERT_TRUE(fd) << strerror(errno);
ASSERT_NE(link(path.c_str(), newpath.c_str()), 0);
ASSERT_EQ(errno, ENOTSUP) << strerror(errno);
}
INSTANTIATE_TEST_SUITE_P(
/*no prefix*/, HardLinkTest,
testing::ValuesIn(MapAndFilterAllTestFilesystems(
[](const TestFilesystemOptions& options) -> std::optional<TestFilesystemOptions> {
if (options.filesystem->GetTraits().supports_hard_links) {
return options;
} else {
return std::nullopt;
}
})),
testing::PrintToStringParamName());
GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(HardLinkTest);
INSTANTIATE_TEST_SUITE_P(
/*no prefix*/, HardLinkUnsupportedTest,
testing::ValuesIn(MapAndFilterAllTestFilesystems(
[](const TestFilesystemOptions& options) -> std::optional<TestFilesystemOptions> {
if (options.filesystem->GetTraits().supports_hard_links) {
return std::nullopt;
} else {
return options;
}
})),
testing::PrintToStringParamName());
GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(HardLinkUnsupportedTest);
} // namespace
} // namespace fs_test