| // Copyright 2022 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 "src/storage/f2fs/test/compatibility/compatibility.h" |
| |
| namespace f2fs { |
| std::string ConvertModeString(mode_t mode) { |
| std::stringstream ss; |
| ss << std::oct << mode; |
| return ss.str(); |
| } |
| |
| std::string EscapedFilename(std::string_view filename) { |
| std::set<char> special_character = {'`', '\"', '\\', '$'}; |
| |
| std::string ret = "\""; |
| |
| for (size_t i = 0; i < filename.length(); ++i) { |
| if (special_character.find(filename[i]) != special_character.end()) { |
| ret.push_back('\\'); |
| } |
| ret.push_back(filename[i]); |
| } |
| |
| ret.push_back('\"'); |
| |
| return ret; |
| } |
| |
| bool LinuxTestFile::IsValid() { |
| std::string result; |
| linux_operator_->ExecuteWithAssert( |
| {"bash -c 'set +H;[ -e", EscapedFilename(filename_), "]; echo $?'"}, &result); |
| return result == "0" || result == "0\n"; |
| } |
| |
| ssize_t LinuxTestFile::Write(const void* buf, size_t count) { |
| const uint8_t* buf_c = static_cast<const uint8_t*>(buf); |
| |
| std::string hex_string; |
| for (uint64_t i = 0; i < count; ++i) { |
| char substring[10]; |
| sprintf(substring, "\\x%02x", buf_c[i]); |
| hex_string.append(substring); |
| |
| if (i > 0 && i % 500 == 0) { |
| linux_operator_->ExecuteWithAssert( |
| {"echo", "-en", std::string("\"").append(hex_string).append("\""), ">>", filename_}); |
| hex_string.clear(); |
| } |
| } |
| |
| linux_operator_->ExecuteWithAssert( |
| {"echo", "-en", std::string("\"").append(hex_string).append("\""), ">>", filename_}); |
| linux_operator_->ExecuteWithAssert({"ls -al", filename_}); |
| |
| return count; |
| } |
| |
| int LinuxTestFile::Fchmod(mode_t mode) { |
| std::string result; |
| linux_operator_->ExecuteWithAssert({"chmod", ConvertModeString(mode), filename_}, &result); |
| |
| if (result.length() > 0) { |
| return -1; |
| } |
| |
| return 0; |
| } |
| |
| int LinuxTestFile::Fstat(struct stat& file_stat) { |
| std::string result; |
| linux_operator_->ExecuteWithAssert({"stat", "-c", "\"%i %f %h %s %Z %Y %b\"", filename_}, |
| &result); |
| |
| std::vector<std::string> tokens; |
| std::stringstream ss(result); |
| std::string token; |
| while (getline(ss, token, ' ')) { |
| tokens.push_back(token); |
| } |
| |
| file_stat.st_ino = std::stoul(tokens[0]); |
| { |
| std::stringstream ss; |
| ss << std::hex << tokens[1]; |
| ss >> file_stat.st_mode; |
| } |
| file_stat.st_nlink = std::stoul(tokens[2]); |
| file_stat.st_size = std::stol(tokens[3]); |
| file_stat.st_ctime = std::stoul(tokens[4]); |
| file_stat.st_mtime = std::stoul(tokens[5]); |
| file_stat.st_blocks = std::stol(tokens[6]); |
| |
| return 0; |
| } |
| |
| int LinuxTestFile::Ftruncate(off_t len) { |
| std::string result; |
| linux_operator_->ExecuteWithAssert({"truncate", "-s", std::to_string(len), filename_}, &result); |
| return result.length() == 0 ? 0 : -1; |
| } |
| |
| int LinuxTestFile::Fallocate(int mode, off_t offset, off_t len) { |
| auto converted_filename = filename_; |
| std::string command = "fallocate "; |
| |
| if (mode & FALLOC_FL_PUNCH_HOLE) { |
| command.append("-p "); |
| } |
| if (mode & FALLOC_FL_KEEP_SIZE) { |
| command.append("-n "); |
| } |
| |
| command.append("-o ") |
| .append(std::to_string(offset)) |
| .append(" -l ") |
| .append(std::to_string(len)) |
| .append(" ") |
| .append(converted_filename); |
| linux_operator_->ExecuteWithAssert({command}); |
| |
| return 0; |
| } |
| |
| void LinuxTestFile::WritePattern(size_t block_count, size_t interval) { |
| for (uint32_t i = 0; i < block_count; i += interval) { |
| std::string pattern = std::to_string(i); |
| |
| ASSERT_EQ(Write(pattern.c_str(), pattern.length()), static_cast<ssize_t>(pattern.length())); |
| |
| off_t next_size = std::min(i + interval, block_count) * kBlockSize; |
| ASSERT_EQ(Ftruncate(next_size), 0); |
| } |
| } |
| |
| void LinuxTestFile::VerifyPattern(size_t block_count, size_t interval) { |
| for (uint32_t i = 0; i < block_count; i += interval) { |
| std::string result; |
| linux_operator_->ExecuteWithAssert( |
| {"od -An -j", std::to_string(i * kBlockSize), "-N", |
| std::to_string(std::to_string(i).length()), "-c", filename_, "| tr -d ' \\n'"}, |
| &result); |
| ASSERT_EQ(result, std::to_string(i)); |
| } |
| } |
| |
| ssize_t FuchsiaTestFile::Read(void* buf, size_t count) { |
| if (!vnode_->IsReg()) { |
| return 0; |
| } |
| |
| File* file = static_cast<File*>(vnode_.get()); |
| size_t ret = 0; |
| |
| zx::stream stream; |
| if (auto ret = file->CreateStream(ZX_STREAM_MODE_READ, &stream); ret != ZX_OK) { |
| return 0; |
| } |
| |
| zx_iovec_t iov = { |
| .buffer = buf, |
| .capacity = count, |
| }; |
| |
| if (stream.readv_at(0, offset_, &iov, 1, &ret) != ZX_OK) { |
| return 0; |
| } |
| |
| offset_ += ret; |
| |
| return ret; |
| } |
| |
| ssize_t FuchsiaTestFile::Write(const void* buf, size_t count) { |
| if (!vnode_->IsReg()) { |
| return 0; |
| } |
| |
| File* file = static_cast<File*>(vnode_.get()); |
| size_t ret = 0; |
| |
| zx::stream stream; |
| if (auto ret = file->CreateStream(ZX_STREAM_MODE_WRITE, &stream); ret != ZX_OK) { |
| return 0; |
| } |
| |
| // Since zx_iovec_t::buffer is not a const type, we make a copied buffer and use it. |
| auto copied = std::make_unique<uint8_t[]>(count); |
| std::memcpy(copied.get(), buf, count); |
| zx_iovec_t iov = { |
| .buffer = copied.get(), |
| .capacity = count, |
| }; |
| |
| if (stream.writev_at(0, offset_, &iov, 1, &ret) != ZX_OK) { |
| return 0; |
| } |
| |
| offset_ += ret; |
| |
| return ret; |
| } |
| |
| int FuchsiaTestFile::Fstat(struct stat& file_stat) { |
| fs::VnodeAttributes attr; |
| if (zx_status_t status = vnode_->GetAttributes(&attr); status != ZX_OK) { |
| return -EIO; |
| } |
| |
| file_stat.st_ino = attr.inode; |
| file_stat.st_mode = static_cast<mode_t>(attr.mode); |
| file_stat.st_nlink = attr.link_count; |
| file_stat.st_size = attr.content_size; |
| file_stat.st_ctim.tv_sec = attr.creation_time / ZX_SEC(1); |
| file_stat.st_ctim.tv_nsec = attr.creation_time % ZX_SEC(1); |
| file_stat.st_mtim.tv_sec = attr.modification_time / ZX_SEC(1); |
| file_stat.st_mtim.tv_nsec = attr.modification_time % ZX_SEC(1); |
| file_stat.st_blocks = (vnode_->GetBlocks()) |
| << vnode_->fs()->GetSuperblockInfo().GetLogSectorsPerBlock(); |
| |
| return 0; |
| } |
| |
| int FuchsiaTestFile::Ftruncate(off_t len) { |
| if (!vnode_->IsReg()) { |
| return -ENOTSUP; |
| } |
| |
| File* file = static_cast<File*>(vnode_.get()); |
| if (zx_status_t status = file->Truncate(len); status != ZX_OK) { |
| return -EIO; |
| } |
| |
| return 0; |
| } |
| |
| void FuchsiaTestFile::WritePattern(size_t block_count, size_t interval) { |
| char buffer[kBlockSize]; |
| for (uint32_t i = 0; i < block_count; ++i) { |
| std::memset(buffer, 0, kBlockSize); |
| if (i % interval == 0) { |
| strcpy(buffer, std::to_string(i).c_str()); |
| } |
| ASSERT_EQ(Write(buffer, sizeof(buffer)), static_cast<ssize_t>(sizeof(buffer))); |
| } |
| } |
| |
| void FuchsiaTestFile::VerifyPattern(size_t block_count, size_t interval) { |
| char buffer[kBlockSize]; |
| char zero_buffer[kBlockSize]; |
| std::memset(zero_buffer, 0, kBlockSize); |
| for (uint32_t i = 0; i < block_count; ++i) { |
| ASSERT_EQ(Read(buffer, sizeof(buffer)), static_cast<ssize_t>(sizeof(buffer))); |
| if (i % interval == 0) { |
| ASSERT_EQ(std::string(buffer), std::to_string(i)); |
| } else { |
| ASSERT_EQ(memcmp(buffer, zero_buffer, kBlockSize), 0); |
| } |
| } |
| } |
| |
| zx_status_t LinuxOperator::Execute(const std::vector<std::string>& argv, std::string* result) { |
| return debian_guest_->Execute(argv, {}, zx::time::infinite(), result, nullptr); |
| } |
| |
| void LinuxOperator::ExecuteWithAssert(const std::vector<std::string>& argv, std::string* result) { |
| ASSERT_EQ(Execute(argv, result), ZX_OK); |
| } |
| |
| std::string LinuxOperator::ConvertPath(std::string_view path) { |
| // Convert only if |path| starts with |kLinuxPathPrefix| |
| if (path.substr(0, kLinuxPathPrefix.length()) == kLinuxPathPrefix) { |
| return std::string(mount_path_).append("/").append(path.substr(kLinuxPathPrefix.length())); |
| } |
| |
| return std::string(path); |
| } |
| |
| void LinuxOperator::CheckLinuxVersion(const int major, const int minor) { |
| std::string result; |
| ExecuteWithAssert({"uname -r | cut -d'.' -f1"}, &result); |
| int actual_major = std::stoi(result); |
| |
| result.clear(); |
| ExecuteWithAssert({"uname -r | cut -d'.' -f2"}, &result); |
| int actual_minor = std::stoi(result); |
| |
| if (actual_major < major || (actual_major == major && actual_minor < minor)) { |
| GTEST_SKIP() << "need Linux version > " << major << "." << minor; |
| } |
| } |
| |
| void LinuxOperator::CheckF2fsToolsVersion(const int major, const int minor) { |
| std::string result; |
| ExecuteWithAssert({"mkfs.f2fs -V | cut -d' ' -f2 | cut -d'.' -f1"}, &result); |
| int actual_major = std::stoi(result); |
| |
| result.clear(); |
| ExecuteWithAssert({"mkfs.f2fs -V | cut -d' ' -f2 | cut -d'.' -f2"}, &result); |
| int actual_minor = std::stoi(result); |
| |
| if (actual_major < major || (actual_major == major && actual_minor < minor)) { |
| GTEST_SKIP() << "need f2fs-tools version > " << major << "." << minor; |
| } |
| } |
| |
| void LinuxOperator::Mkfs(std::string_view opt) { |
| ExecuteWithAssert({"mkfs.f2fs", test_device_, "-f", opt.data()}); |
| } |
| |
| void LinuxOperator::Fsck() { ExecuteWithAssert({"fsck.f2fs", test_device_, "--dry-run"}); } |
| |
| void LinuxOperator::Mount(std::string_view opt) { |
| ExecuteWithAssert({"mkdir", "-p", mount_path_}); |
| ExecuteWithAssert({"mount", test_device_, mount_path_, opt.data()}); |
| } |
| |
| void LinuxOperator::Umount() { ExecuteWithAssert({"umount", mount_path_}); } |
| |
| void LinuxOperator::Mkdir(std::string_view path, mode_t mode) { |
| ExecuteWithAssert({"mkdir", "-m", ConvertModeString(mode), ConvertPath(path)}); |
| } |
| |
| int LinuxOperator::Rmdir(std::string_view path) { |
| std::string result; |
| ExecuteWithAssert({"rmdir", ConvertPath(path)}, &result); |
| return (result.length() == 0) ? 0 : -1; |
| } |
| |
| std::unique_ptr<TestFile> LinuxOperator::Open(std::string_view path, int flags, mode_t mode) { |
| if (flags & O_CREAT) { |
| if (flags & O_DIRECTORY) { |
| Mkdir(path, mode); |
| } else { |
| ExecuteWithAssert({"bash -c 'set +H;touch", EscapedFilename(ConvertPath(path)), ";chmod ", |
| ConvertModeString(mode), EscapedFilename(ConvertPath(path)), "'"}); |
| } |
| } |
| |
| return std::unique_ptr<TestFile>(new LinuxTestFile(ConvertPath(path), this)); |
| } |
| |
| void LinuxOperator::Rename(std::string_view oldpath, std::string_view newpath) { |
| ExecuteWithAssert({"mv", ConvertPath(oldpath), ConvertPath(newpath)}); |
| } |
| |
| void FuchsiaOperator::Mkfs(MkfsOptions opt) { |
| MkfsWorker mkfs(std::move(bc_), opt); |
| auto ret = mkfs.DoMkfs(); |
| ASSERT_TRUE(ret.is_ok()); |
| bc_ = std::move(*ret); |
| } |
| |
| void FuchsiaOperator::Fsck() { |
| FsckWorker fsck(std::move(bc_), FsckOptions{.repair = false}); |
| ASSERT_EQ(fsck.Run(), ZX_OK); |
| bc_ = fsck.Destroy(); |
| } |
| |
| void FuchsiaOperator::Mount(MountOptions opt) { |
| auto vfs_or = Runner::CreateRunner(loop_.dispatcher()); |
| ASSERT_TRUE(vfs_or.is_ok()); |
| |
| auto fs_or = F2fs::Create(nullptr, std::move(bc_), opt, (*vfs_or).get()); |
| ASSERT_TRUE(fs_or.is_ok()); |
| (*fs_or)->SetVfsForTests(std::move(*vfs_or)); |
| fs_ = std::move(*fs_or); |
| |
| ASSERT_EQ(VnodeF2fs::Vget(fs_.get(), fs_->GetSuperblockInfo().GetRootIno(), &root_), ZX_OK); |
| ASSERT_EQ(root_->Open(root_->ValidateOptions(fs::VnodeConnectionOptions()).value(), nullptr), |
| ZX_OK); |
| } |
| |
| void FuchsiaOperator::Umount() { |
| ASSERT_EQ(root_->Close(), ZX_OK); |
| root_.reset(); |
| |
| fs_->Sync(); |
| fs_->PutSuper(); |
| |
| auto vfs_or = fs_->TakeVfsForTests(); |
| ASSERT_TRUE(vfs_or.is_ok()); |
| |
| auto bc_or = fs_->TakeBc(); |
| ASSERT_TRUE(bc_or.is_ok()); |
| bc_ = std::move(*bc_or); |
| |
| (*vfs_or).reset(); |
| } |
| |
| void FuchsiaOperator::Mkdir(std::string_view path, mode_t mode) { |
| auto new_dir = Open(path, O_CREAT | O_EXCL, S_IFDIR | mode); |
| ASSERT_TRUE(new_dir->IsValid()); |
| } |
| |
| int FuchsiaOperator::Rmdir(std::string_view path) { |
| auto ret = GetLastDirVnodeAndFileName(path); |
| if (ret.is_error()) { |
| return -1; |
| } |
| auto [parent_vnode, child_name] = ret.value(); |
| |
| if (zx_status_t status = fs_->vfs()->Unlink(parent_vnode, child_name, true); status != ZX_OK) { |
| return -1; |
| } |
| return 0; |
| } |
| |
| std::unique_ptr<TestFile> FuchsiaOperator::Open(std::string_view path, int flags, mode_t mode) { |
| auto result = fs_->vfs()->Open(root_, path, ConvertFlag(flags), fs::Rights::ReadWrite(), mode); |
| if (result.is_error()) { |
| return std::unique_ptr<TestFile>(new FuchsiaTestFile(nullptr)); |
| } |
| |
| fbl::RefPtr<VnodeF2fs> vnode = fbl::RefPtr<VnodeF2fs>::Downcast(result.ok().vnode); |
| |
| std::unique_ptr<TestFile> ret = std::make_unique<FuchsiaTestFile>(std::move(vnode)); |
| |
| return ret; |
| } |
| |
| void FuchsiaOperator::Rename(std::string_view oldpath, std::string_view newpath) { |
| auto result = GetLastDirVnodeAndFileName(oldpath); |
| ASSERT_TRUE(result.is_ok()); |
| auto [oldparent_vn, oldchild_name] = result.value(); |
| |
| result = GetLastDirVnodeAndFileName(newpath); |
| ASSERT_TRUE(result.is_ok()); |
| auto [newparent_vn, newchild_name] = result.value(); |
| |
| ASSERT_EQ(oldparent_vn->Rename(newparent_vn, oldchild_name, newchild_name, false, false), ZX_OK); |
| } |
| |
| uint32_t FuchsiaOperator::MaxInlineDentrySlots() { |
| Mkfs(); |
| MountOptions options; |
| options.SetValue(MountOption::kInlineDentry, 1); |
| Mount(options); |
| |
| auto umount = fit::defer([&] { Umount(); }); |
| |
| // Create inline directory |
| constexpr std::string_view inline_dir_path = "/inline"; |
| Mkdir(inline_dir_path, 0755); |
| auto inline_dir = Open(inline_dir_path, O_RDWR, 0644); |
| Dir* raw_ptr = |
| static_cast<Dir*>(static_cast<FuchsiaTestFile*>(inline_dir.get())->GetRawVnodePtr()); |
| |
| return raw_ptr->MaxInlineDentry(); |
| } |
| |
| uint32_t FuchsiaOperator::MaxInlineDataLength() { |
| Mkfs(); |
| MountOptions options; |
| Mount(options); |
| |
| auto umount = fit::defer([&] { Umount(); }); |
| |
| auto inline_file = Open("/inline", O_RDWR | O_CREAT, 0644); |
| File* raw_ptr = |
| static_cast<File*>(static_cast<FuchsiaTestFile*>(inline_file.get())->GetRawVnodePtr()); |
| |
| return raw_ptr->MaxInlineData(); |
| } |
| |
| zx::result<std::pair<fbl::RefPtr<fs::Vnode>, std::string>> |
| FuchsiaOperator::GetLastDirVnodeAndFileName(std::string_view absolute_path) { |
| std::filesystem::path path(absolute_path); |
| if (!path.has_root_directory() || !path.has_filename()) { |
| return zx::error(ZX_ERR_INVALID_ARGS); |
| } |
| fbl::RefPtr<fs::Vnode> vnode = root_; |
| |
| for (auto token : path.parent_path().relative_path()) { |
| if (auto ret = vnode->Lookup(token.string(), &vnode); ret != ZX_OK) { |
| return zx::error(ret); |
| } |
| } |
| return zx::ok(make_pair(std::move(vnode), path.filename().string())); |
| } |
| |
| fs::VnodeConnectionOptions ConvertFlag(int flags) { |
| fs::VnodeConnectionOptions options; |
| |
| // TODO: O_PATH, O_DIRECT, O_TRUNC, O_APPEND |
| switch (flags & O_ACCMODE) { |
| case O_RDONLY: |
| options.rights.read = true; |
| break; |
| case O_WRONLY: |
| options.rights.write = true; |
| break; |
| case O_RDWR: |
| options.rights.read = true; |
| options.rights.write = true; |
| break; |
| default: |
| break; |
| } |
| |
| if (flags & O_CREAT) { |
| options.flags.create = true; |
| } |
| if (flags & O_EXCL) { |
| options.flags.fail_if_exists = true; |
| } |
| |
| return options; |
| } |
| |
| void CompareStat(const struct stat& a, const struct stat& b) { |
| EXPECT_EQ(a.st_ino, b.st_ino); |
| EXPECT_EQ(a.st_mode, b.st_mode); |
| EXPECT_EQ(a.st_nlink, b.st_nlink); |
| EXPECT_EQ(a.st_size, b.st_size); |
| EXPECT_EQ(a.st_ctime, b.st_ctime); |
| EXPECT_EQ(a.st_mtime, b.st_mtime); |
| ASSERT_EQ(a.st_blocks, b.st_blocks); |
| } |
| |
| } // namespace f2fs |