From fe476dff697a9f116c115bdc76d13c2fc078a2cb Mon Sep 17 00:00:00 2001 From: asypost Date: Sat, 27 Jun 2026 18:37:37 +0800 Subject: [PATCH] =?UTF-8?q?1=E3=80=81=E5=AE=9E=E7=8E=B0=E7=9B=AE=E5=BD=95?= =?UTF-8?q?=E8=AF=BB=E5=8F=96=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CMakeLists.txt | 4 + include/seafile/fs/file_context.hpp | 11 ++ include/seafile/fs/seafile_file_system.hpp | 9 +- include/seafile/repo_entry.hpp | 1 + include/seafile/seafile_api.hpp | 16 ++ include/util/icu_raii.hpp | 22 +++ include/util/string_util.hpp | 27 +++ src/main.cpp | 6 +- src/seafile/fs/seafile_file_system.cpp | 205 ++++++++++----------- src/seafile/fs/winfsp_adapter.cpp | 14 +- src/util/string_util.cpp | 121 ++++++++++++ 11 files changed, 320 insertions(+), 116 deletions(-) create mode 100644 include/util/icu_raii.hpp create mode 100644 include/util/string_util.hpp create mode 100644 src/util/string_util.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 3f2a502..eb76b26 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -10,6 +10,7 @@ find_package(cpr CONFIG REQUIRED) find_package(argparse CONFIG REQUIRED) find_package(ada CONFIG REQUIRED) find_package(spdlog CONFIG REQUIRED) +find_package(ICU REQUIRED COMPONENTS uc i18n) cmake_host_system_information( RESULT WINFSP_DIR @@ -52,6 +53,7 @@ add_executable(seadisk src/main.cpp src/seafile/fs/seafile_file_system.cpp src/seafile/fs/winfsp_adapter.cpp include/seafile/fs/winfsp_adapter.hpp + src/util/string_util.cpp ) target_compile_definitions(seadisk PRIVATE @@ -80,6 +82,8 @@ target_link_libraries(seadisk PRIVATE argparse::argparse ada::ada spdlog::spdlog + ICU::uc + ICU::i18n ) target_link_options(seadisk PRIVATE diff --git a/include/seafile/fs/file_context.hpp b/include/seafile/fs/file_context.hpp index 4b5cd0f..b091ade 100644 --- a/include/seafile/fs/file_context.hpp +++ b/include/seafile/fs/file_context.hpp @@ -9,12 +9,23 @@ namespace seafile::fs { struct SeaFileContext final { + enum class Type + { + FILE, + DIRECTORY, + }; + + std::string repo_id; std::string path; + Type type; + uint64_t size{}; std::string download_url; + + }; } diff --git a/include/seafile/fs/seafile_file_system.hpp b/include/seafile/fs/seafile_file_system.hpp index 9f3d229..e0f7c67 100644 --- a/include/seafile/fs/seafile_file_system.hpp +++ b/include/seafile/fs/seafile_file_system.hpp @@ -9,7 +9,6 @@ #include "seafile/seafile_api.hpp" #include "seafile/token_type.hpp" -#include "seafile/fs/file_context.hpp" namespace seafile::fs { @@ -19,15 +18,15 @@ namespace seafile::fs std::string token; TokenType token_type = TokenType::TOKEN; SeaFileApi api; - std::wstring mount_point; + std::wstring volume_label; public: explicit SeafileFileSystem(const std::string& server, - const std::wstring& mount_point) : api(server), mount_point(mount_point) + const std::wstring& volume_label) : api(server), volume_label(volume_label) { } - void auth(const std::string& username, const std::string& password); + tl::expected initialize(const std::string& username, const std::string& password); SeafileFileSystem(const SeafileFileSystem&) = delete; @@ -37,7 +36,7 @@ namespace seafile::fs SeafileFileSystem& operator=(SeafileFileSystem&&) = delete; - NTSTATUS GetVolumeInfo(FSP_FSCTL_VOLUME_INFO* VolumeInfo); + NTSTATUS GetVolumeInfo(FSP_FSCTL_VOLUME_INFO* VolumeInfo) const; NTSTATUS SetVolumeLabel_(PWSTR VolumeLabel, FSP_FSCTL_VOLUME_INFO* VolumeInfo); diff --git a/include/seafile/repo_entry.hpp b/include/seafile/repo_entry.hpp index 0d4e605..33f3223 100644 --- a/include/seafile/repo_entry.hpp +++ b/include/seafile/repo_entry.hpp @@ -7,6 +7,7 @@ #include #include #include +#include namespace seafile { diff --git a/include/seafile/seafile_api.hpp b/include/seafile/seafile_api.hpp index cfd7364..7c9d7e1 100644 --- a/include/seafile/seafile_api.hpp +++ b/include/seafile/seafile_api.hpp @@ -16,6 +16,7 @@ #include "account_info.hpp" #include "auth_token.hpp" #include "default_repo.hpp" +#include "dir_info.hpp" #include "file_info.hpp" #include "repo_entry.hpp" #include "repo_info.hpp" @@ -272,6 +273,21 @@ namespace seafile return download_url; } } + + tl::expected dirDetail(const std::string& repo_id, const std::string& path, + const std::string& token, const TokenType token_type) const + { + const auto auth = makeAuthToken(token, token_type); + const auto url = cpr::Url{makeUrl(std::format("/api/v2.1/repos/{}/dir/detail/", repo_id))}; + const auto response = cpr::Get(url, cpr::Header{ + {"accept", "application/json"}, + {"authorization", auth} + }, cpr::Parameters{ + {"path", path} + }); + + return parseResponse(response); + } }; } diff --git a/include/util/icu_raii.hpp b/include/util/icu_raii.hpp new file mode 100644 index 0000000..a1e0c2f --- /dev/null +++ b/include/util/icu_raii.hpp @@ -0,0 +1,22 @@ +// +// Created by Shell on 2026-04-13. +// + +#ifndef FORCAST_API_ICU_RAII_H +#define FORCAST_API_ICU_RAII_H +#include + +struct UCharsetDetectorDeleter final +{ + void operator ()(UCharsetDetector* ptr) const + { + if (ptr) + { + ucsdet_close(ptr); + } + } +}; + +using UCharsetDetectorPtr = std::unique_ptr; + +#endif //FORCAST_API_ICU_RAII_H diff --git a/include/util/string_util.hpp b/include/util/string_util.hpp new file mode 100644 index 0000000..2653fae --- /dev/null +++ b/include/util/string_util.hpp @@ -0,0 +1,27 @@ +// +// Created by Shell on 2025-12-17. +// + +#ifndef FORCAST_API_CLIENT_STRING_UTIL_HPP +#define FORCAST_API_CLIENT_STRING_UTIL_HPP +#include +#include + +namespace shell::util { + class StringUtil final { + public: + static std::vector split(const std::string &s, const std::string &delim); + static std::string convert_encoding(const std::string& input,const std::string& from,const std::string& to); + static std::string detect_encoding(const std::string& data); + static std::string convert_encoding(const std::string& input,const std::string& to); +#ifdef _WIN32 + static std::wstring utf8_to_utf16le(const std::string& input); + + static std::string utf16le_to_utf8(const std::wstring& input); + + static std::string utf16le_to_utf8(const wchar_t* input); + }; +#endif +} + +#endif //FORCAST_API_CLIENT_STRING_UTIL_HPP \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index 944b7ee..981cef0 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,11 +1,11 @@ -#include "seafile/seafile_api.hpp" #include - #include "seafile/fs/winfsp_adapter.hpp" +#include "seafile/seafile_api.hpp" using namespace seafile::fs; -int wmain(int argc, wchar_t **argv) { +auto wmain(int argc, wchar_t** argv) -> int +{ if (!NT_SUCCESS(FspLoad(nullptr))) return ERROR_DELAY_LOAD_FAILED; diff --git a/src/seafile/fs/seafile_file_system.cpp b/src/seafile/fs/seafile_file_system.cpp index 24f38a7..af501a1 100644 --- a/src/seafile/fs/seafile_file_system.cpp +++ b/src/seafile/fs/seafile_file_system.cpp @@ -1,5 +1,8 @@ #include "seafile/fs/seafile_file_system.hpp" +#include "seafile/fs/file_context.hpp" +#include "util/string_util.hpp" + namespace seafile::fs { namespace @@ -31,38 +34,24 @@ namespace seafile::fs FileInfo->ChangeTime = ft; FileInfo->IndexNumber = 0; } - - void SplitPath(const std::wstring& path, std::string& repo_id, std::string& sub_path) - { - repo_id.clear(); - sub_path = "/"; - auto p = path; - while (!p.empty() && p[0] == L'\\') - p = p.substr(1); - if (p.empty()) - return; - - auto sep = p.find(L'\\'); - auto repo_id_w = (sep == std::wstring::npos) ? p : p.substr(0, sep); - repo_id.assign(repo_id_w.begin(), repo_id_w.end()); - - if (sep != std::wstring::npos) - { - auto rest = p.substr(sep); - sub_path.clear(); - for (auto c : rest) - sub_path += static_cast(c); - while (!sub_path.empty() && sub_path.front() == '\\') - sub_path = sub_path.substr(1); - sub_path = "/" + sub_path; - } - } } - void SeafileFileSystem::auth(const std::string& username, const std::string& password) + + tl::expected SeafileFileSystem::initialize(const std::string& username, + const std::string& password) { - if (const auto authToken = api.authToken(username, password); authToken.has_value()) + const auto authToken = api.authToken(username, password); + if (authToken.has_value()) + { this->token = authToken.value().token; + if (const auto repo = api.defaultRepo(this->token, this->token_type); repo.has_value()) + { + this->repo_id = repo.value().repo_id.value_or(""); + return {}; + } + return tl::make_unexpected(SeafileException("No default repo found")); + } + return tl::make_unexpected(authToken.error()); } void SeafileFileSystem::Close(PVOID FileContext) @@ -74,13 +63,13 @@ namespace seafile::fs PSECURITY_DESCRIPTOR SecurityDescriptor, SIZE_T* PSecurityDescriptorSize) { - if (PFileAttributes) { if (wcscmp(FileName, L"\\") == 0) { *PFileAttributes = FILE_ATTRIBUTE_DIRECTORY; - }else + } + else { *PFileAttributes = FILE_ATTRIBUTE_NORMAL; } @@ -91,22 +80,25 @@ namespace seafile::fs return STATUS_SUCCESS; } - NTSTATUS SeafileFileSystem::Overwrite(PVOID FileContext, UINT32 FileAttributes, BOOLEAN ReplaceFileAttributes, UINT64 AllocationSize, FSP_FSCTL_FILE_INFO* FileInfo) + NTSTATUS SeafileFileSystem::Overwrite(PVOID FileContext, UINT32 FileAttributes, BOOLEAN ReplaceFileAttributes, + UINT64 AllocationSize, FSP_FSCTL_FILE_INFO* FileInfo) { - return STATUS_NOT_IMPLEMENTED; + return STATUS_MEDIA_WRITE_PROTECTED; } - NTSTATUS SeafileFileSystem::GetVolumeInfo(FSP_FSCTL_VOLUME_INFO* VolumeInfo) + NTSTATUS SeafileFileSystem::GetVolumeInfo(FSP_FSCTL_VOLUME_INFO* VolumeInfo) const { - const auto userInfo = api.accountInfo(this->token, this->token_type); - if (userInfo.has_value()) + if (const auto userInfo = api.accountInfo(this->token, this->token_type); userInfo.has_value()) { const auto& info = userInfo.value(); VolumeInfo->FreeSize = info.total - info.usage; VolumeInfo->TotalSize = info.total; - wcscpy(VolumeInfo->VolumeLabel, L"Seafile"); - VolumeInfo->VolumeLabelLength = static_cast((wcslen(VolumeInfo->VolumeLabel) * sizeof(WCHAR))); + if (!this->volume_label.empty()) + { + wcscpy_s(VolumeInfo->VolumeLabel, this->volume_label.data()); + VolumeInfo->VolumeLabelLength = static_cast((wcslen(VolumeInfo->VolumeLabel) * sizeof(WCHAR))); + } return STATUS_SUCCESS; } return STATUS_END_OF_MEDIA; @@ -114,31 +106,25 @@ namespace seafile::fs void SeafileFileSystem::Cleanup(PVOID FileContext, PWSTR FileName, ULONG Flags) { - } NTSTATUS SeafileFileSystem::Open(PWSTR FileName, UINT32 CreateOptions, UINT32 GrantedAccess, PVOID* PFileContext, FSP_FSCTL_FILE_INFO* FileInfo) { - std::string repo_id, sub_path; - SplitPath(FileName, repo_id, sub_path); - const bool expect_dir = (CreateOptions & FILE_DIRECTORY_FILE) != 0; if (repo_id.empty()) { - auto ctx = new SeaFileContext{"", "/", 0, ""}; - *PFileContext = ctx; - SetFileInfo(FileInfo, FILE_ATTRIBUTE_DIRECTORY, 0, 0); - return STATUS_SUCCESS; + return STATUS_OBJECT_NAME_NOT_FOUND; } - if (sub_path == "/") + const auto path = shell::util::StringUtil::utf16le_to_utf8(FileName); + if (wcscmp(FileName, L"\\") == 0) { auto repo = api.repoInfo(repo_id, token, token_type); if (!repo.has_value()) return STATUS_OBJECT_NAME_NOT_FOUND; - auto ctx = new SeaFileContext{repo->id, "/", 0, ""}; + const auto ctx = new SeaFileContext{repo->id, path, SeaFileContext::Type::DIRECTORY, 0, ""}; *PFileContext = ctx; SetFileInfo(FileInfo, FILE_ATTRIBUTE_DIRECTORY, repo->size, repo->mtime); return STATUS_SUCCESS; @@ -146,31 +132,26 @@ namespace seafile::fs if (expect_dir) { - auto entries = api.repoDir(repo_id, sub_path, token, token_type); - if (entries.has_value()) + if (const auto dirInfo = api.dirDetail(repo_id, path, token, token_type); dirInfo.has_value()) { - auto ctx = new SeaFileContext{repo_id, sub_path, 0, ""}; + const auto& info = dirInfo.value(); + const auto ctx = new SeaFileContext{info.repo_id, info.path, SeaFileContext::Type::DIRECTORY, 0, ""}; *PFileContext = ctx; - SetFileInfo(FileInfo, FILE_ATTRIBUTE_DIRECTORY, 0, 0); + SetFileInfo(FileInfo,FILE_ATTRIBUTE_DIRECTORY, 0, 0); return STATUS_SUCCESS; } - auto fdetail = api.fileDetail(repo_id, sub_path, token, token_type); - if (fdetail.has_value()) - return STATUS_NOT_A_DIRECTORY; - return STATUS_OBJECT_NAME_NOT_FOUND; } - - auto fdetail = api.fileDetail(repo_id, sub_path, token, token_type); - if (fdetail.has_value()) + else { - auto ctx = new SeaFileContext{repo_id, sub_path, fdetail->size, ""}; - *PFileContext = ctx; - SetFileInfo(FileInfo, FILE_ATTRIBUTE_NORMAL, fdetail->size, fdetail->mtime); - return STATUS_SUCCESS; + if (const auto fileInfo = api.fileDetail(repo_id, path, token, token_type); fileInfo.has_value()) + { + const auto& info = fileInfo.value(); + const auto ctx = new SeaFileContext(repo_id, path, SeaFileContext::Type::FILE, info.size, ""); + *PFileContext = ctx; + SetFileInfo(FileInfo,FILE_ATTRIBUTE_ARCHIVE, 0, 0); + return STATUS_SUCCESS; + } } - auto entries = api.repoDir(repo_id, sub_path, token, token_type); - if (entries.has_value()) - return STATUS_FILE_IS_A_DIRECTORY; return STATUS_OBJECT_NAME_NOT_FOUND; } @@ -198,46 +179,62 @@ namespace seafile::fs { const auto fc = static_cast(FileContext); *PBytesTransferred = 0; + const auto path = std::ranges::replace(fc->path,'\\','/'); + const auto entries_result = api.repoDir(fc->repo_id, fc->path, token, token_type); - auto add_entry = [&](const std::wstring& name, UINT32 attrs, uint64_t size, int64_t mtime) -> bool + if (entries_result.has_value()) { - if (Marker && wcscmp(name.c_str(), Marker) <= 0) - return true; + const auto& entries = entries_result.value(); + size_t start = 0; + if (Marker) + { + const auto marker = shell::util::StringUtil::utf16le_to_utf8(Marker); + for (auto i = 0; i < entries.size(); ++i) + { + if (entries[i].name == marker) + { + start = i + 1; + break; + } + } + } + for (auto i = start; i < entries.size(); ++i) + { + const auto entry = entries[i]; + const auto entry_name = shell::util::StringUtil::utf8_to_utf16le(entry.name); + const auto size = sizeof(FSP_FSCTL_DIR_INFO) + ((entry_name.size() + 1) * sizeof(WCHAR)); - const auto name_bytes = (wcslen(name.c_str()) + 1) * sizeof(WCHAR); - const auto entry_size = static_cast(offsetof(FSP_FSCTL_DIR_INFO, FileNameBuf) + name_bytes); - if (*PBytesTransferred + entry_size > BufferLength) - return false; + const auto dir_info = static_cast(std::malloc(size)); + std::memset(dir_info, 0, size); + dir_info->Size = size; + dir_info->FileInfo.FileSize = entry.size.value_or(0); + if (entry.type == "file") + { + dir_info->FileInfo.FileAttributes = FILE_ATTRIBUTE_ARCHIVE; + } + else + { + dir_info->FileInfo.FileAttributes = FILE_ATTRIBUTE_DIRECTORY; + } + wcscpy_s(dir_info->FileNameBuf, entry_name.size() + 1, entry_name.data()); + if (*PBytesTransferred + size < BufferLength) + { + if (!FspFileSystemAddDirInfo(dir_info, Buffer, BufferLength, PBytesTransferred)) + { + break; + } + } + else + { + break; + } + } - auto DirInfo = reinterpret_cast( - static_cast(Buffer) + *PBytesTransferred); - memset(DirInfo, 0, sizeof(FSP_FSCTL_DIR_INFO)); - DirInfo->Size = entry_size; - SetFileInfo(&DirInfo->FileInfo, attrs, size, mtime); - memcpy(DirInfo->FileNameBuf, name.c_str(), name_bytes); - *PBytesTransferred += entry_size; - return true; - }; - - if (fc->repo_id.empty()) - { - return STATUS_OBJECT_NAME_NOT_FOUND; - } - bool ok = true; - auto entries = api.repoDir(fc->repo_id, fc->path, token, token_type); - if (!entries.has_value()) - return STATUS_UNSUCCESSFUL; - - for (const auto& entry : entries.value()) - { - auto wname = std::wstring(entry.name.begin(), entry.name.end()); - const auto attrs = (entry.type == "dir") ? FILE_ATTRIBUTE_DIRECTORY : FILE_ATTRIBUTE_NORMAL; - const auto sz = entry.size.value_or(0); - ok = add_entry(wname, attrs, sz, entry.mtime); - if (!ok) break; + return STATUS_SUCCESS; } - return STATUS_SUCCESS; + std::cout<Size = static_cast(offsetof(FSP_FSCTL_DIR_INFO, FileNameBuf) + name_bytes); + DirInfo->Size = offsetof(FSP_FSCTL_DIR_INFO, FileNameBuf) + name_bytes; const auto attrs = (entry.type == "dir") ? FILE_ATTRIBUTE_DIRECTORY : FILE_ATTRIBUTE_NORMAL; SetFileInfo(&DirInfo->FileInfo, attrs, entry.size.value_or(0), entry.mtime); wcscpy_s(DirInfo->FileNameBuf, name.size() + 1, FileName); @@ -314,6 +313,6 @@ namespace seafile::fs NTSTATUS SeafileFileSystem::SetVolumeLabel_(PWSTR VolumeLabel, FSP_FSCTL_VOLUME_INFO* VolumeInfo) { - return STATUS_SUCCESS; + return STATUS_ACCESS_DENIED; } } diff --git a/src/seafile/fs/winfsp_adapter.cpp b/src/seafile/fs/winfsp_adapter.cpp index 1eafced..a408f4d 100644 --- a/src/seafile/fs/winfsp_adapter.cpp +++ b/src/seafile/fs/winfsp_adapter.cpp @@ -65,15 +65,19 @@ namespace seafile::fs } auto mount_point = std::wstring(L"X:"); auto seafile_file_system = new SeafileFileSystem("https://pan.hanwangsoft.com", mount_point); - seafile_file_system->auth("asypost@yeah.net", "Shell0805@china"); + if (const auto ret= seafile_file_system->initialize("asypost@yeah.net", "Shell0805@china"); !ret.has_value()) + { + Result = STATUS_UNSUCCESSFUL; + FspFileSystemDelete(FileSystem); + delete seafile_file_system; + + return Result; + } FileSystem->UserContext = seafile_file_system; fstab.insert({FileSystem, seafile_file_system}); Result = FspFileSystemSetMountPoint(FileSystem, static_cast(mount_point.data())); - DWORD LastError = GetLastError(); - std::cout << LastError << std::endl; - if (!NT_SUCCESS(Result)) { FspFileSystemDelete(FileSystem); @@ -208,7 +212,7 @@ namespace seafile::fs SECURITY_INFORMATION SecurityInformation, PSECURITY_DESCRIPTOR ModificationDescriptor) { - return STATUS_NOT_IMPLEMENTED; + return STATUS_ACCESS_DENIED; } NTSTATUS WinFSPAdapter::GetSecurity(FSP_FILE_SYSTEM* FileSystem, PVOID FileContext, diff --git a/src/util/string_util.cpp b/src/util/string_util.cpp new file mode 100644 index 0000000..4b6cbf2 --- /dev/null +++ b/src/util/string_util.cpp @@ -0,0 +1,121 @@ +// +// Created by Shell on 2025-12-17. +// + +#include "util/string_util.hpp" + +#include +#include +#include +#include +#include + +#include "util/icu_raii.hpp" + +namespace shell::util +{ + std::vector StringUtil::split(const std::string& s, const std::string& delim) + { + if (delim.empty()) + { + return {s}; + } + std::vector result; + size_t start = 0, pos; + + while ((pos = s.find(delim, start)) != std::string::npos) + { + result.emplace_back(s.substr(start, pos - start)); + start = pos + delim.length(); + } + + result.emplace_back(s.substr(start)); + return result; + } + + std::string StringUtil::convert_encoding(const std::string& input, const std::string& to) + { + return convert_encoding(input, detect_encoding(input), to); + } + + std::string StringUtil::detect_encoding(const std::string& data) + { + UErrorCode status = U_ZERO_ERROR; + + const auto detector = UCharsetDetectorPtr(ucsdet_open(&status)); + if (U_FAILURE(status)) + { + throw std::runtime_error(std::format("ucsdet_open failed:{}", u_errorName(status))); + } + + // 设置待检测数据 + ucsdet_setText(detector.get(), data.data(), data.size(), &status); + if (U_FAILURE(status)) + { + throw std::runtime_error(std::format("ucsdet_open failed,can't set text:{}", u_errorName(status))); + } + + // 检测 + const UCharsetMatch* match = ucsdet_detect(detector.get(), &status); + + std::string encoding; + if (match && U_SUCCESS(status)) + { + encoding = ucsdet_getName(match, &status); + } + + return encoding; + } + + std::string StringUtil::convert_encoding(const std::string& input, const std::string& from, const std::string& to) + { + UErrorCode status = U_ZERO_ERROR; + + // 先获取输出长度 + const int32_t len = ucnv_convert( + to.c_str(), from.c_str(), + nullptr, 0, + input.data(), input.size(), + &status + ); + + status = U_ZERO_ERROR; + + std::string output(len, 0); + + ucnv_convert( + to.c_str(), from.c_str(), + output.data(), len, + input.data(), input.size(), + &status + ); + + if (U_FAILURE(status)) + { + throw std::runtime_error(std::format("convert from {} to {} failed:{}", from, to, u_errorName(status))); + } + + return output; + } + + +#ifdef _WIN32 + std::wstring StringUtil::utf8_to_utf16le(const std::string& input) + { + const icu::UnicodeString unicode = icu::UnicodeString::fromUTF8(input); + return std::wstring{unicode.getBuffer(), unicode.getBuffer() + unicode.length()}; + } + + std::string StringUtil::utf16le_to_utf8(const std::wstring& input) + { + return utf16le_to_utf8(input.data()); + } + + std::string StringUtil::utf16le_to_utf8(const wchar_t* input) + { + const auto unicode = icu::UnicodeString(input); + std::string utf8; + return unicode.toUTF8String(utf8); + } +#endif +}