blob: ab8d7439e770cb0f6c1aae58b932ab98d79bf280 [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 <optional>
#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);
uint8_t buf[100];
for (size_t i = 0; i < sizeof(buf); i++) {
buf[i] = (uint8_t)rand();
}
ASSERT_EQ(write(fd, buf, sizeof(buf)), static_cast<ssize_t>(sizeof(buf)));
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);
uint8_t buf[100];
for (size_t i = 0; i < sizeof(buf); i++) {
buf[i] = (uint8_t)rand();
}
ASSERT_EQ(write(fd, buf, sizeof(buf)), static_cast<ssize_t>(sizeof(buf)));
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 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);
}
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());
} // namespace
} // namespace fs_test