Files
2026-07-13 12:47:42 +08:00

288 lines
10 KiB
C++

// Copyright 2025-present the zvec project
//
// Licensed 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.
#if defined(_WIN32) || defined(_WIN64)
#define WIN32_LEAN_AND_MEAN
#define NOMINMAX
#include <windows.h>
#undef DeleteFile
#undef RemoveDirectory
#undef CreateFile
#endif
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <fstream>
#include <iostream>
#include <string>
#include <gtest/gtest.h>
#include <zvec/ailego/io/file.h>
#include <zvec/ailego/utility/file_helper.h>
#include <zvec/ailego/utility/string_helper.h>
using namespace zvec::ailego;
// UTF-8 encoded Chinese / Japanese test strings.
// These contain characters outside the Windows ANSI codepage, so any path
// handling that falls back to narrow Win32 APIs will produce the mojibake.
static const std::string kChinese =
"\xe4\xb8\xad\xe6\x96\x87\xe8\xb7\xaf\xe5\xbe\x84"; // 中文路径
static const std::string kJapanese =
"\xe3\x83\x86\xe3\x82\xb9\xe3\x83\x88"; // テスト
static const std::string kMixed = "data_测试_2025"; // data_测试_2025
class Utf8PathTest : public ::testing::Test {
protected:
void TearDown() override {
for (auto &p : cleanup_) {
FileHelper::RemovePath(p.c_str());
}
}
void ScheduleCleanup(const std::string &path) {
cleanup_.push_back(path);
}
private:
std::vector<std::string> cleanup_;
};
// ---------------------------------------------------------------------------
// 1. Directory create / query / remove with Chinese path
// ---------------------------------------------------------------------------
TEST_F(Utf8PathTest, MakePath_Chinese) {
std::string dir = "utf8_test_" + kChinese;
ScheduleCleanup(dir);
ASSERT_TRUE(FileHelper::MakePath(dir.c_str()));
EXPECT_TRUE(FileHelper::IsExist(dir.c_str()));
EXPECT_TRUE(FileHelper::IsDirectory(dir.c_str()));
EXPECT_FALSE(FileHelper::IsRegular(dir.c_str()));
ASSERT_TRUE(FileHelper::RemoveDirectory(dir.c_str()));
EXPECT_FALSE(FileHelper::IsExist(dir.c_str()));
}
// ---------------------------------------------------------------------------
// 2. Nested directories with mixed UTF-8 segments
// ---------------------------------------------------------------------------
TEST_F(Utf8PathTest, MakePath_Nested) {
std::string root = "utf8_test_nested_" + kChinese;
std::string nested = FileHelper::PathJoin(root, kJapanese, kMixed);
ScheduleCleanup(root);
ASSERT_TRUE(FileHelper::MakePath(nested.c_str()));
EXPECT_TRUE(FileHelper::IsDirectory(nested.c_str()));
ASSERT_TRUE(FileHelper::RemoveDirectory(root.c_str()));
EXPECT_FALSE(FileHelper::IsExist(root.c_str()));
}
// ---------------------------------------------------------------------------
// 3. File create / read / write through ailego::File with UTF-8 path
// ---------------------------------------------------------------------------
TEST_F(Utf8PathTest, FileCreateAndReadWrite) {
std::string dir = "utf8_test_file_" + kChinese;
std::string file_path = FileHelper::PathJoin(dir, kJapanese + ".dat");
ScheduleCleanup(dir);
ASSERT_TRUE(FileHelper::MakePath(dir.c_str()));
const std::string payload =
"hello_utf8_\xe4\xbd\xa0\xe5\xa5\xbd"; // hello_utf8_你好
{
File f;
ASSERT_TRUE(f.create(file_path.c_str(), 0));
EXPECT_EQ(payload.size(), f.write(payload.data(), payload.size()));
EXPECT_TRUE(f.flush());
}
EXPECT_TRUE(FileHelper::IsRegular(file_path.c_str()));
size_t sz = 0;
ASSERT_TRUE(FileHelper::GetFileSize(file_path.c_str(), &sz));
EXPECT_EQ(payload.size(), sz);
{
File f;
ASSERT_TRUE(f.open(file_path.c_str(), true));
std::string buf(sz, '\0');
EXPECT_EQ(sz, f.read(buf.data(), sz));
EXPECT_EQ(payload, buf);
}
ASSERT_TRUE(FileHelper::DeleteFile(file_path.c_str()));
EXPECT_FALSE(FileHelper::IsExist(file_path.c_str()));
}
// ---------------------------------------------------------------------------
// 4. FileHelper::RenameFile across UTF-8 paths
// ---------------------------------------------------------------------------
TEST_F(Utf8PathTest, RenameFile) {
std::string dir = "utf8_test_rename_" + kChinese;
std::string src = FileHelper::PathJoin(dir, "src_" + kJapanese);
std::string dst = FileHelper::PathJoin(dir, "dst_" + kMixed);
ScheduleCleanup(dir);
ASSERT_TRUE(FileHelper::MakePath(dir.c_str()));
{
File f;
ASSERT_TRUE(f.create(src.c_str(), 0));
}
EXPECT_TRUE(FileHelper::IsExist(src.c_str()));
ASSERT_TRUE(FileHelper::RenameFile(src.c_str(), dst.c_str()));
EXPECT_FALSE(FileHelper::IsExist(src.c_str()));
EXPECT_TRUE(FileHelper::IsExist(dst.c_str()));
}
// ---------------------------------------------------------------------------
// 5. OpenIfstream / OpenOfstream with UTF-8 path
// ---------------------------------------------------------------------------
TEST_F(Utf8PathTest, FstreamUtf8) {
std::string dir = "utf8_test_fstream_" + kChinese;
std::string file_path = FileHelper::PathJoin(dir, kMixed + ".txt");
ScheduleCleanup(dir);
ASSERT_TRUE(FileHelper::MakePath(dir.c_str()));
const std::string content =
"line1\nline2_\xe6\xb5\x8b\xe8\xaf\x95\n"; // line2_测试
{
std::ofstream ofs;
FileHelper::OpenOfstream(ofs, file_path, std::ios::binary);
ASSERT_TRUE(ofs.is_open());
ofs.write(content.data(), content.size());
}
{
std::ifstream ifs;
FileHelper::OpenIfstream(ifs, file_path, std::ios::binary);
ASSERT_TRUE(ifs.is_open());
std::string buf((std::istreambuf_iterator<char>(ifs)),
std::istreambuf_iterator<char>());
EXPECT_EQ(content, buf);
}
}
// ---------------------------------------------------------------------------
// 6. PathJoin produces correct separators
// ---------------------------------------------------------------------------
TEST_F(Utf8PathTest, PathJoinSeparator) {
std::string result = FileHelper::PathJoin("root", "sub", "file.txt");
#if defined(_WIN32) || defined(_WIN64)
EXPECT_EQ("root\\sub\\file.txt", result);
#else
EXPECT_EQ("root/sub/file.txt", result);
#endif
}
// ---------------------------------------------------------------------------
// 7. Verify NO mojibake on actual filesystem via "dir" (Windows) / "ls" (POSIX)
// We create a directory + file with Chinese names, then list the parent
// using the OS shell and check the expected name appears verbatim.
// ---------------------------------------------------------------------------
#if defined(_WIN32) || defined(_WIN64)
TEST_F(Utf8PathTest, NoGarbledNames_FindFirstFile) {
std::string parent = "utf8_test_dir_verify";
std::string child_dir = kChinese;
std::string child_file = kJapanese + ".dat";
std::string full_dir = FileHelper::PathJoin(parent, child_dir);
std::string full_file = FileHelper::PathJoin(parent, child_file);
ScheduleCleanup(parent);
ASSERT_TRUE(FileHelper::MakePath(full_dir.c_str()));
{
File f;
ASSERT_TRUE(f.create(full_file.c_str(), 0));
}
// Use FindFirstFileW / FindNextFileW to enumerate the parent directory.
// This is the most reliable way to verify the OS actually stored correct
// Unicode names (no mojibake), bypassing any console codepage issues.
std::wstring pattern = FileHelper::Utf8ToWide(parent) + L"\\*";
WIN32_FIND_DATAW fd;
HANDLE hFind = FindFirstFileW(pattern.c_str(), &fd);
ASSERT_NE(INVALID_HANDLE_VALUE, hFind) << "FindFirstFileW failed";
std::vector<std::wstring> entries;
do {
std::wstring name(fd.cFileName);
if (name != L"." && name != L"..") {
entries.push_back(name);
}
} while (FindNextFileW(hFind, &fd));
FindClose(hFind);
std::wstring expected_dir = FileHelper::Utf8ToWide(child_dir);
std::wstring expected_file = FileHelper::Utf8ToWide(child_file);
bool found_dir = false, found_file = false;
std::cout << "[NoGarbledNames] Entries in " << parent << ":" << std::endl;
for (const auto &e : entries) {
std::string utf8_name = FileHelper::WideToUtf8(e);
std::cout << " " << utf8_name << std::endl;
if (e == expected_dir) found_dir = true;
if (e == expected_file) found_file = true;
}
EXPECT_TRUE(found_dir) << "Directory '" << child_dir
<< "' not found (garbled Unicode?)";
EXPECT_TRUE(found_file) << "File '" << (kJapanese + ".dat")
<< "' not found (garbled Unicode?)";
}
TEST_F(Utf8PathTest, NoGarbledNames_DirCmd) {
std::string parent = "utf8_test_dir_cmd";
std::string child = kChinese;
std::string full = FileHelper::PathJoin(parent, child);
std::string tmpfile = "utf8_test_dir_cmd_output.txt";
ScheduleCleanup(parent);
ASSERT_TRUE(FileHelper::MakePath(full.c_str()));
// "cmd /u" makes built-in commands output UTF-16LE when redirected to file.
std::wstring wparent = FileHelper::Utf8ToWide(parent);
std::wstring wtmp = FileHelper::Utf8ToWide(tmpfile);
std::wstring cmd =
L"cmd /u /c dir /b \"" + wparent + L"\" >\"" + wtmp + L"\"";
_wsystem(cmd.c_str());
// Read back the file as raw bytes (UTF-16LE)
std::ifstream ifs;
FileHelper::OpenIfstream(ifs, tmpfile, std::ios::binary);
ASSERT_TRUE(ifs.is_open()) << "Could not read dir output file";
std::string raw((std::istreambuf_iterator<char>(ifs)),
std::istreambuf_iterator<char>());
ifs.close();
FileHelper::DeleteFile(tmpfile.c_str());
// Interpret as UTF-16LE: skip BOM if present, then convert to UTF-8
const wchar_t *wdata = reinterpret_cast<const wchar_t *>(raw.data());
size_t wlen = raw.size() / sizeof(wchar_t);
if (wlen > 0 && wdata[0] == L'\xFEFF') {
wdata++;
wlen--;
}
std::wstring woutput(wdata, wlen);
std::string output = FileHelper::WideToUtf8(woutput);
std::cout << "[NoGarbledNames_DirCmd] dir output: [" << output << "]";
EXPECT_NE(std::string::npos, output.find(child))
<< "Chinese directory name not found in 'dir' output (garbled?).\n"
<< "Got: [" << output << "]";
}
#endif