blob: 3e37327904591016118d68c84694ac8a73ff2de8 [file] [log] [blame]
// Copyright 2021 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.
// inotify is a Linux extension that is also supported on Fuchsia.
#if defined(__Fuchsia__) || defined(__linux__)
#include <fcntl.h>
#include <limits.h>
#include <sys/inotify.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <optional>
#include <string>
#include <vector>
#if defined(__Fuchsia__)
#include <fidl/fuchsia.io/cpp/wire_test_base.h>
#include <lib/async-loop/cpp/loop.h>
#include <lib/async-loop/default.h>
#include <lib/async/cpp/task.h>
#include <lib/fdio/fd.h>
#include <lib/fdio/namespace.h>
#include <lib/fidl-async/cpp/bind.h>
#endif // __Fuchsia__
#include <fbl/unique_fd.h>
#include <zxtest/zxtest.h>
#include "predicates.h"
namespace {
// Reading from an inotify object produces a buffer with a number of
// variable-length records containing an optional field. This format
// is annoying. InotifyEventIterator provides a C++ friendly
class InotifyEventIterator {
public:
// |buf| should contain at least |buf_len| bytes of data and must outlive this object.
InotifyEventIterator(char* buf, size_t buf_len) : buf_(buf), offset_(0u), buf_len_(buf_len) {}
struct Entry {
int wd;
uint32_t mask;
uint32_t cookie;
std::string_view name;
};
// Returns the next entry in the buffer, or std::nullopt if there are no more entries.
std::optional<Entry> Next();
bool Done() { return offset_ == buf_len_; }
size_t offset() const { return offset_; }
private:
char* buf_;
size_t offset_;
size_t buf_len_;
};
std::optional<InotifyEventIterator::Entry> InotifyEventIterator::Next() {
if (offset_ + sizeof(struct inotify_event) > buf_len_) {
return std::nullopt;
}
struct inotify_event* event = reinterpret_cast<struct inotify_event*>(buf_ + offset_);
offset_ += sizeof(struct inotify_event);
if (offset_ + static_cast<size_t>(event->len) > buf_len_) {
return std::nullopt;
}
offset_ += event->len;
std::string_view name;
if (event->len > 0) {
name = std::string_view(event->name, event->len);
}
return Entry{
.wd = event->wd,
.mask = event->mask,
.cookie = event->cookie,
.name = name,
};
}
constexpr struct inotify_event kEvent { .wd = 1, .mask = IN_OPEN, .cookie = 0, };
// The inotify read interface can return a variable amount of data since it encodes
// a path name. This type allocates enough space to read at least one inotify event.
using EventBuf = std::array<char, sizeof(inotify_event) + NAME_MAX + 1>;
#if defined(__Fuchsia__)
namespace fio = fuchsia_io;
constexpr char kTmpfsPath[] = "/tmp-inotify";
class Server final : public fidl::testing::WireTestBase<fuchsia_io::Directory> {
public:
explicit Server(async_dispatcher_t* dispatcher) : dispatcher_(dispatcher) {}
void NotImplemented_(const std::string& name, fidl::CompleterBase& completer) override {
ADD_FAILURE("%s should not be called", name.c_str());
completer.Close(ZX_ERR_NOT_SUPPORTED);
}
void Open(OpenRequestView request, OpenCompleter::Sync& completer) override {
uint32_t incoming_filter = static_cast<uint32_t>(filter_) & IN_OPEN;
ASSERT_EQ(incoming_filter, IN_OPEN, "Inotify filter %u does not match open event",
static_cast<uint32_t>(filter_));
ASSERT_TRUE(inotify_socket_.has_value());
// Write the Open event to the socket.
// First, figure out how large the event will be including the path.
const fidl::StringView& path = request->path;
size_t name_len = path.size();
size_t event_len = sizeof(kEvent) + name_len +
1; // The serialized event record must include a null terminator.
std::vector<char> event_buf(event_len);
struct inotify_event* event = reinterpret_cast<struct inotify_event*>(event_buf.data());
// Copy in the fixed fields to the event buffer.
memcpy(event, &kEvent, sizeof(kEvent));
event->len =
static_cast<uint32_t>(name_len + 1); // The |len| field includes the null terminator.
// Copy in the name.
memcpy(event->name, path.data(), name_len);
event->name[name_len] = '\0';
// Send over the inotify socket.
size_t actual = 0u;
ASSERT_OK(inotify_socket_.value().write(0, event_buf.data(), event_buf.size(), &actual));
ASSERT_EQ(actual, event_buf.size());
}
void AddInotifyFilter(AddInotifyFilterRequestView request,
AddInotifyFilterCompleter::Sync& completer) override {
filter_ = request->filter;
if (add_inotify_filter_async_) {
async::PostDelayedTask(
dispatcher_,
[&, socket = std::move(request->socket), completer = completer.ToAsync()]() mutable {
ASSERT_FALSE(inotify_socket_.has_value());
inotify_socket_ = std::move(socket);
completer.Reply();
},
zx::msec(50));
} else {
ASSERT_FALSE(inotify_socket_.has_value());
inotify_socket_ = std::move(request->socket);
completer.Reply();
}
}
void SetAddInotifyFilterAsync() { add_inotify_filter_async_ = true; }
private:
async_dispatcher_t* dispatcher_;
bool add_inotify_filter_async_ = false;
std::optional<zx::socket> inotify_socket_;
fio::wire::InotifyWatchMask filter_;
};
#endif // defined(__Fuchsia__)
// InotifyTestDirectory creates and tears down a directory suitable for inotify tests.
class InotifyTestDirectory {
public:
virtual ~InotifyTestDirectory() = default;
virtual void SetUp() = 0;
virtual void TearDown() = 0;
virtual const char* Path() = 0;
#if defined(__Fuchsia__)
virtual Server& mutable_server() = 0;
#endif // defined(__Fuchsia__)
};
class InotifyTestDirectoryHost : public InotifyTestDirectory {
public:
void SetUp() final {
const char* temp_env_var = getenv("TMPDIR");
temp_dir_name_ = temp_env_var ? temp_env_var : "/tmp";
temp_dir_name_ += "/inotify-test-XXXXXX";
mkdtemp(temp_dir_name_.data());
}
void TearDown() final { ASSERT_SUCCESS(rmdir(temp_dir_name_.c_str())); }
const char* Path() final { return temp_dir_name_.c_str(); }
private:
std::string temp_dir_name_;
};
#if defined(__Fuchsia__)
class InotifyTestDirectoryFuchsia : public InotifyTestDirectory {
public:
InotifyTestDirectoryFuchsia()
: loop_(&kAsyncLoopConfigNoAttachToCurrentThread), server_(Server(loop_.dispatcher())) {}
void SetUp() final {
ASSERT_OK(loop_.StartThread("fake-filesystem"));
// client-server channel logic
zx::status endpoints = fidl::CreateEndpoints<fuchsia_io::Directory>();
ASSERT_OK(endpoints.status_value());
fidl::BindServer(loop_.dispatcher(), std::move(endpoints->server), &server_);
// install namespace for local-filesystem.
ASSERT_OK(fdio_ns_get_installed(&namespace_));
ASSERT_OK(fdio_ns_bind(namespace_, kTmpfsPath, endpoints->client.channel().release()));
}
void TearDown() final { ASSERT_OK(fdio_ns_unbind(namespace_, kTmpfsPath)); }
const char* Path() final { return "/tmp-inotify"; }
Server& mutable_server() { return server_; }
private:
async::Loop loop_;
Server server_;
fdio_ns_t* namespace_;
};
#endif // defined(__Fuchsia__)
class InotifyAddFilter : public zxtest::Test {
protected:
InotifyAddFilter() {
#if defined(__Fuchsia__)
test_dir_ = std::make_unique<InotifyTestDirectoryFuchsia>();
#else
test_dir_ = std::make_unique<InotifyTestDirectoryHost>();
#endif // defined(__Fuchsia__)
}
void SetUp() override {
test_dir_->SetUp();
ASSERT_TRUE(fd_ = fbl::unique_fd(inotify_init1(0)), "%s", strerror(errno));
}
void TearDown() override { test_dir_->TearDown(); }
const char* TempFsPath() { return test_dir_->Path(); }
const fbl::unique_fd& fd() { return fd_; }
#if defined(__Fuchsia__)
Server& mutable_server() { return test_dir_->mutable_server(); }
#endif // defined(__Fuchsia__)
private:
std::unique_ptr<InotifyTestDirectory> test_dir_;
fbl::unique_fd fd_;
};
TEST(InotifyTest, InitBadFlags) {
ASSERT_EQ(inotify_init1(5), -1);
ASSERT_ERRNO(EINVAL);
}
TEST_F(InotifyAddFilter, AddWatchWithNullFilePath) {
ASSERT_EQ(inotify_add_watch(fd().get(), nullptr, IN_OPEN), -1);
ASSERT_ERRNO(EFAULT);
}
TEST_F(InotifyAddFilter, AddWatchWithZeroLengthFilePath) {
ASSERT_EQ(inotify_add_watch(fd().get(), "", IN_OPEN), -1);
ASSERT_ERRNO(ENOENT);
}
TEST_F(InotifyAddFilter, AddWatch) {
ASSERT_GE(inotify_add_watch(fd().get(), TempFsPath(), IN_OPEN), 0);
}
TEST_F(InotifyAddFilter, AddWatchOpenRace) {
#if defined(__Fuchsia__)
mutable_server().SetAddInotifyFilterAsync();
#endif // __Fuchsia__
ASSERT_GE(inotify_add_watch(fd().get(), TempFsPath(), IN_OPEN), 0);
// Expected to fail on Fuchsia since Server::Open drops the request on the
// floor. That's OK, we don't really need to open, just to trigger an inotify
// event.
std::string temp_file_path = std::string(TempFsPath()) + "/" + __FUNCTION__;
int temp_fd = open(temp_file_path.c_str(), O_CREAT, 0644);
#if defined(__Fuchsia__)
EXPECT_EQ(temp_fd, -1);
#else
ASSERT_GT(temp_fd, 0);
fbl::unique_fd temp_file(temp_fd);
ASSERT_SUCCESS(unlink(temp_file_path.c_str()));
#endif // defined(__Fuchsia__)
// Mark the inotify fd nonblocking to avoid deadlock in this test's failure case.
int flags;
ASSERT_GE(flags = fcntl(fd().get(), F_GETFL), 0, "%s", strerror(errno));
ASSERT_SUCCESS(fcntl(fd().get(), F_SETFL, flags | O_NONBLOCK));
EventBuf event_buf;
ASSERT_GE(read(fd().get(), event_buf.data(), event_buf.size()), sizeof(struct inotify_event),
"%s", strerror(errno));
struct inotify_event* event = reinterpret_cast<struct inotify_event*>(event_buf.data());
ASSERT_EQ(event->mask, IN_OPEN);
ASSERT_EQ(event->wd, kEvent.wd);
ASSERT_EQ(event->cookie, kEvent.cookie);
}
TEST_F(InotifyAddFilter, AddWatchWithTooLongFilePath) {
std::string long_filepath(PATH_MAX + 1, 'x');
ASSERT_EQ(inotify_add_watch(fd().get(), long_filepath.c_str(), IN_OPEN), -1);
ASSERT_ERRNO(ENAMETOOLONG);
}
TEST_F(InotifyAddFilter, AddMultipleFilters) {
#if defined(__Fuchsia__)
mutable_server().SetAddInotifyFilterAsync();
#endif // defined(__Fuchsia__)
// Use multiple filters in the same add_watch.
ASSERT_GE(inotify_add_watch(fd().get(), TempFsPath(), IN_OPEN | IN_CREATE), 0);
constexpr char kTestFileName[] = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
std::string filepath = std::string(TempFsPath()) + "/" + std::string(kTestFileName);
int temp_fd = open(filepath.c_str(), O_CREAT, 0644);
#if defined(__Fuchsia__)
EXPECT_EQ(temp_fd, -1);
#else
ASSERT_GT(temp_fd, 0);
fbl::unique_fd temp_file(temp_fd);
ASSERT_SUCCESS(unlink(filepath.c_str()));
#endif // defined(__Fuchsia__)
constexpr size_t kBufSize = 2 * sizeof(struct inotify_event) + 2 * NAME_MAX + 2;
std::array<char, kBufSize> event_buf;
ssize_t bytes_read = read(fd().get(), event_buf.data(), event_buf.size());
// This should read two events, one for IN_CREATE and one for IN_OPEN.
InotifyEventIterator iter(event_buf.data(), static_cast<size_t>(bytes_read));
#if !defined(__Fuchsia__)
// TODO(https://fxbug.dev/60109): On Fuchsia, currently we do not get an IN_CREATE event in this
// case.
std::optional create_event = iter.Next();
ASSERT_TRUE(create_event.has_value());
EXPECT_EQ(create_event->mask, IN_CREATE);
EXPECT_EQ(create_event->wd, kEvent.wd);
EXPECT_EQ(create_event->cookie, kEvent.cookie);
EXPECT_STREQ(std::string(create_event->name), kTestFileName);
#endif // !defined(__Fuchsia__)
std::optional open_event = iter.Next();
ASSERT_TRUE(open_event.has_value());
EXPECT_EQ(open_event->mask, IN_OPEN);
EXPECT_EQ(open_event->wd, kEvent.wd);
EXPECT_EQ(open_event->cookie, kEvent.cookie);
EXPECT_STREQ(std::string(open_event->name), kTestFileName);
EXPECT_TRUE(iter.Done(), "consumed %ld of %ld bytes", iter.offset(), bytes_read);
}
TEST_F(InotifyAddFilter, DatagramPayloadNoShortReads) {
#if defined(__Fuchsia__)
mutable_server().SetAddInotifyFilterAsync();
#endif // defined(__Fuchsia__)
ASSERT_GE(inotify_add_watch(fd().get(), TempFsPath(), IN_OPEN), 0);
// Call open multiple times and see if we receive the inotify_event event in the form of
// of a single unit, without short reads/writes.
for (int i = 0; i < 3; i++) {
std::string temp_file_path = std::string(TempFsPath()) + "/" + __FUNCTION__;
int temp_fd = open(temp_file_path.c_str(), O_CREAT, 0644);
#if defined(__Fuchsia__)
EXPECT_EQ(temp_fd, -1);
#else
ASSERT_GT(temp_fd, 0);
fbl::unique_fd temp_file(temp_fd);
ASSERT_SUCCESS(unlink(temp_file_path.c_str()));
#endif // defined(__Fuchsia__)
// Try to read events. Make sure we always receive inotify_event as a whole structure, i.e
// no short reads.
EventBuf event_buf;
ASSERT_GE(read(fd().get(), event_buf.data(), event_buf.size()), sizeof(inotify_event), "%s",
strerror(errno));
struct inotify_event* event = reinterpret_cast<struct inotify_event*>(event_buf.data());
EXPECT_EQ(event->mask, kEvent.mask, "iteration %d", i);
EXPECT_EQ(event->wd, kEvent.wd, "iteration %d", i);
EXPECT_EQ(event->cookie, kEvent.cookie, "iteration %d", i);
}
}
class InotifyRemove : public InotifyAddFilter {
protected:
void SetUp() override {
InotifyAddFilter::SetUp();
ASSERT_GE(wd_ = inotify_add_watch(fd().get(), TempFsPath(), IN_OPEN), 0, "%s", strerror(errno));
}
int wd() const { return wd_; }
private:
int wd_;
};
TEST_F(InotifyRemove, Remove) { ASSERT_SUCCESS(inotify_rm_watch(fd().get(), wd())); }
TEST_F(InotifyRemove, RemoveWithInvalidInotifyDescriptor) {
ASSERT_EQ(inotify_rm_watch(fd().get() + 1, wd()), -1);
ASSERT_ERRNO(EBADF);
}
TEST_F(InotifyRemove, RemoveWithInvalidWatchDescriptor) {
ASSERT_EQ(inotify_rm_watch(fd().get(), wd() + 1), -1);
ASSERT_ERRNO(EINVAL);
}
} // namespace
#endif // defined(__Fuchsia__) || defined(__linux__)