blob: a1a93d49500413f6e1395cf7f2f994278354ef4a [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
#include <fcntl.h>
#include <gtest/gtest.h>
#include <sys/stat.h>
#include <cstring>
#include <string>
#include "common/tsfile_common.h"
#include "file/read_file.h"
#include "file/restorable_tsfile_io_writer.h"
#include "file/utf8_file_open.h"
#include "file/write_file.h"
#include "writer/tsfile_writer.h"
#ifdef _WIN32
#include <io.h>
#include <windows.h>
#else
#include <unistd.h>
#endif
extern "C" {
typedef void* CWriteFile;
CWriteFile write_file_new(const char* pathname, int32_t* err_code);
void free_write_file(CWriteFile* write_file);
}
// Note: storage::WriteFile / storage::ReadFile are named after Win32 API
// functions that <windows.h> declares at global scope, so the class names are
// always written qualified here.
using namespace common;
namespace {
// The path bytes are spelled with explicit escapes so the test does not depend
// on how the compiler re-encodes non-ASCII literals from the source charset.
// UTF-8 for U+6D4B U+8BD5 ("test" in Chinese) followed by U+00E9 ("e-acute").
const char* kUtf8Stem = "\xE6\xB5\x8B\xE8\xAF\x95_\xC3\xA9_";
// Every gtest case runs as its own process (gtest_discover_tests) in a
// shared working directory, and ctest starts several of them at once
// (--parallel N). A fixed file name would let one case's SetUp/TearDown
// unlink the file another case is mid-test with, so each case gets its
// own name, suffixed with the gtest case name (which is plain ASCII).
std::string Utf8Name() {
const ::testing::TestInfo* info =
::testing::UnitTest::GetInstance()->current_test_info();
return std::string(kUtf8Stem) +
(info != nullptr ? info->name() : "unknown") + ".tsfile";
}
#ifdef _WIN32
// Same name as Utf8Name(), spelled as UTF-16 code units. Test names are
// ASCII, so widening them is a plain char-by-char copy.
std::wstring WideName() {
const ::testing::TestInfo* info =
::testing::UnitTest::GetInstance()->current_test_info();
std::wstring wide(L"\x6D4B\x8BD5_\x00E9_");
const char* tag = info != nullptr ? info->name() : "unknown";
while (*tag != '\0') {
wide += static_cast<wchar_t>(*tag++);
}
wide += L".tsfile";
return wide;
}
#endif
// Smallest byte sequence that counts as a complete TsFile: head magic, the
// current version byte, then the tail magic. This is what ReadFile::open()
// requires (>= MIN_FILE_SIZE bytes, magic at both ends) and also what
// RestorableTsFileIOWriter's self check treats as complete.
//
// Built on first use rather than at file scope: VERSION_NUM_BYTE is defined in
// another translation unit, so a file-scope initializer would depend on static
// initialization order.
const std::string& MinimalTsFile() {
static const std::string bytes = std::string(storage::MAGIC_STRING_TSFILE) +
storage::VERSION_NUM_BYTE +
storage::MAGIC_STRING_TSFILE;
return bytes;
}
// Remove both spellings: the intended wide name, and the mojibake name the
// narrow CRT produces from the UTF-8 bytes under a non-UTF-8 code page. Leaving
// the latter behind would let one test's stray file satisfy the next test.
void RemoveTestFiles() {
#ifdef _WIN32
::_wunlink(WideName().c_str());
::_unlink(Utf8Name().c_str());
#else
::unlink(Utf8Name().c_str());
#endif
}
// Existence check that never routes the name through the narrow CRT, so it
// cannot be fooled by the same encoding bug the test is about.
bool ExistsByWidePath() {
#ifdef _WIN32
return ::GetFileAttributesW(WideName().c_str()) != INVALID_FILE_ATTRIBUTES;
#else
struct stat st;
return ::stat(Utf8Name().c_str(), &st) == 0;
#endif
}
// Create the fixture through the wide API on Windows, so the file on disk
// really carries the non-ASCII name regardless of the active code page.
bool CreateFixtureByWidePath() {
#ifdef _WIN32
const int fd = ::_wopen(WideName().c_str(),
_O_WRONLY | _O_CREAT | _O_TRUNC | _O_BINARY,
_S_IREAD | _S_IWRITE);
#else
const int fd =
::open(Utf8Name().c_str(), O_WRONLY | O_CREAT | O_TRUNC, 0666);
#endif
if (fd < 0) {
return false;
}
const std::string& content = MinimalTsFile();
const int written = static_cast<int>(
::write(fd, content.data(), static_cast<unsigned>(content.size())));
#ifdef _WIN32
::_close(fd);
#else
::close(fd);
#endif
return written == static_cast<int>(content.size());
}
bool MinimalTsFileIsUnchanged() {
storage::ReadFile read_file;
std::string buf(MinimalTsFile().size(), '\0');
int32_t read_len = 0;
const bool opened = read_file.open(Utf8Name()) == E_OK;
const bool read_ok =
opened &&
read_file.file_size() == static_cast<int64_t>(MinimalTsFile().size()) &&
read_file.read(0, &buf[0], static_cast<int32_t>(buf.size()),
read_len) == E_OK &&
read_len == static_cast<int32_t>(buf.size()) && buf == MinimalTsFile();
read_file.close();
return read_ok;
}
class Utf8PathTest : public ::testing::Test {
protected:
void SetUp() override { RemoveTestFiles(); }
void TearDown() override { RemoveTestFiles(); }
};
} // namespace
// WriteFile must create the file under the UTF-8 name it was given. On Windows
// the narrow ::open() interprets the bytes in the active code page, so the file
// lands under a mojibake name (or not at all) and the wide-path lookup fails.
TEST_F(Utf8PathTest, WriteFileCreatesUtf8Path) {
storage::WriteFile write_file;
ASSERT_EQ(write_file.create(Utf8Name(), O_WRONLY | O_CREAT | O_TRUNC, 0666),
E_OK);
ASSERT_TRUE(write_file.file_opened());
const std::string& content = MinimalTsFile();
ASSERT_EQ(
write_file.write(content.data(), static_cast<uint32_t>(content.size())),
E_OK);
ASSERT_EQ(write_file.close(), E_OK);
EXPECT_TRUE(ExistsByWidePath())
<< "the file was not created under the UTF-8 path that was requested";
}
TEST_F(Utf8PathTest, ExistingUtf8PathIsRejectedByTsFileWriter) {
ASSERT_TRUE(CreateFixtureByWidePath());
storage::TsFileWriter writer;
int flags = O_WRONLY | O_CREAT | O_TRUNC;
#ifdef _WIN32
flags |= O_BINARY;
#endif
EXPECT_EQ(writer.open(Utf8Name(), flags, 0666), E_ALREADY_EXIST);
EXPECT_TRUE(MinimalTsFileIsUnchanged());
}
TEST_F(Utf8PathTest, ExistingUtf8PathIsRejectedByCWrapper) {
ASSERT_TRUE(CreateFixtureByWidePath());
int32_t error_code = E_OK;
CWriteFile file = write_file_new(Utf8Name().c_str(), &error_code);
EXPECT_EQ(file, nullptr);
EXPECT_EQ(error_code, E_ALREADY_EXIST);
if (file != nullptr) {
free_write_file(&file);
}
EXPECT_TRUE(MinimalTsFileIsUnchanged());
}
// ReadFile must find a file that exists on disk under a non-ASCII name.
TEST_F(Utf8PathTest, ReadFileOpensUtf8Path) {
ASSERT_TRUE(CreateFixtureByWidePath());
ASSERT_TRUE(ExistsByWidePath());
storage::ReadFile read_file;
EXPECT_EQ(read_file.open(Utf8Name()), E_OK)
<< "an existing file with a non-ASCII name could not be opened";
EXPECT_TRUE(read_file.is_opened());
read_file.close();
}
// Round trip through both classes: what WriteFile wrote, ReadFile must read.
// The wide-path check matters even though the round trip alone would succeed
// while both sides are equally broken -- a consistently mangled name still
// round trips, so only the on-disk name proves the bytes were honoured.
TEST_F(Utf8PathTest, Utf8PathRoundTripsBetweenWriteAndRead) {
storage::WriteFile write_file;
ASSERT_EQ(write_file.create(Utf8Name(), O_WRONLY | O_CREAT | O_TRUNC, 0666),
E_OK);
const std::string& content = MinimalTsFile();
ASSERT_EQ(
write_file.write(content.data(), static_cast<uint32_t>(content.size())),
E_OK);
ASSERT_EQ(write_file.close(), E_OK);
ASSERT_TRUE(ExistsByWidePath())
<< "the round trip used a name that is not the requested UTF-8 path";
storage::ReadFile read_file;
ASSERT_EQ(read_file.open(Utf8Name()), E_OK);
EXPECT_EQ(read_file.file_size(), static_cast<int64_t>(content.size()));
std::string buf(content.size(), '\0');
int32_t read_len = 0;
ASSERT_EQ(
read_file.read(0, &buf[0], static_cast<int32_t>(buf.size()), read_len),
E_OK);
EXPECT_EQ(read_len, static_cast<int32_t>(content.size()));
EXPECT_EQ(buf, content);
read_file.close();
}
// The third caller of open_utf8() is the self-check reader inside
// RestorableTsFileIOWriter. Given a complete file under a non-ASCII name, the
// self check must recognise it as complete. When the name is mangled instead,
// the writer creates an empty file under the wrong name and reports a crashed
// file that may be written from scratch -- which would silently discard data.
TEST_F(Utf8PathTest, RestorableWriterSelfChecksUtf8Path) {
ASSERT_TRUE(CreateFixtureByWidePath());
storage::RestorableTsFileIOWriter writer;
ASSERT_EQ(writer.open(Utf8Name(), true), E_OK);
EXPECT_EQ(writer.get_truncated_size(), storage::TSFILE_CHECK_COMPLETE)
<< "the self check did not see the existing complete file";
EXPECT_FALSE(writer.has_crashed());
EXPECT_FALSE(writer.can_write());
writer.close();
}
#ifdef _WIN32
// Rejection paths of the conversion helper. These are Windows-only: the POSIX
// branch hands the bytes to ::open() unchanged, where a path holding a NUL
// would silently be truncated at it rather than rejected.
TEST_F(Utf8PathTest, OpenUtf8RejectsInvalidPaths) {
errno = 0;
EXPECT_EQ(storage::file_internal::open_utf8("", O_RDONLY), -1);
EXPECT_EQ(errno, ENOENT);
std::string embedded_nul("a\0b.tsfile", 10);
errno = 0;
EXPECT_EQ(storage::file_internal::open_utf8(embedded_nul, O_RDONLY), -1);
EXPECT_EQ(errno, EINVAL);
// 0xFF 0xFE is not a valid UTF-8 sequence, so the conversion must fail
// rather than fall back to a lossy interpretation.
errno = 0;
EXPECT_EQ(
storage::file_internal::open_utf8("\xFF\xFE_bad.tsfile", O_RDONLY), -1);
EXPECT_EQ(errno, EINVAL);
}
#endif