mirror of
https://github.com/Lime3DS/Lime3DS.git
synced 2026-05-12 15:49:39 -06:00
macOS: normalize SDMC directory filenames (#2080)
* macOS: normalize SDMC directory filenames * Guard macOS filename normalization behind __APPLE__ * Guard macOS filename normalization test * Apply clang-format * Update license headers
This commit is contained in:
parent
ec6a0dd1c8
commit
83eef0012e
@ -1,3 +1,7 @@
|
||||
// Copyright Citra Emulator Project / Azahar Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
// Copyright 2013 Dolphin Emulator Project / 2014 Citra Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
@ -20,6 +24,10 @@
|
||||
#include <windows.h>
|
||||
#endif
|
||||
|
||||
#if defined(__APPLE__)
|
||||
#include <CoreFoundation/CFString.h>
|
||||
#endif
|
||||
|
||||
namespace Common {
|
||||
|
||||
/// Make a char lowercase
|
||||
@ -164,6 +172,52 @@ std::u16string UTF8ToUTF16(std::string_view input) {
|
||||
return boost::locale::conv::utf_to_utf<char16_t>(input.data(), input.data() + input.size());
|
||||
}
|
||||
|
||||
#if defined(__APPLE__)
|
||||
// macOS filesystems may expose decomposed Unicode names through directory listings.
|
||||
// Normalize to NFC before passing names to guest APIs that expect stable text.
|
||||
std::string NormalizeNFDToNFC(std::string_view input) {
|
||||
const std::string fallback(input);
|
||||
|
||||
// Core Foundation string
|
||||
CFStringRef source =
|
||||
CFStringCreateWithBytes(kCFAllocatorDefault, reinterpret_cast<const UInt8*>(input.data()),
|
||||
static_cast<CFIndex>(input.size()), kCFStringEncodingUTF8, false);
|
||||
|
||||
if (source == nullptr) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
// Mutable copy of the source string
|
||||
CFMutableStringRef normalized = CFStringCreateMutableCopy(kCFAllocatorDefault, 0, source);
|
||||
CFRelease(source);
|
||||
|
||||
if (normalized == nullptr) {
|
||||
return fallback;
|
||||
}
|
||||
// Normalize the string to NFC form
|
||||
CFStringNormalize(normalized, kCFStringNormalizationFormC);
|
||||
|
||||
const CFIndex max_size = CFStringGetMaximumSizeForEncoding(CFStringGetLength(normalized),
|
||||
kCFStringEncodingUTF8) +
|
||||
1; // +1 for null terminator
|
||||
|
||||
std::string output(static_cast<std::size_t>(max_size), '\0');
|
||||
|
||||
// Convert the normalized string back to UTF-8
|
||||
const bool converted =
|
||||
CFStringGetCString(normalized, &output[0], max_size, kCFStringEncodingUTF8);
|
||||
|
||||
CFRelease(normalized);
|
||||
|
||||
if (!converted) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
output.resize(std::strlen(output.c_str()));
|
||||
return output;
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef _WIN32
|
||||
static std::wstring CPToUTF16(u32 code_page, const std::string& input) {
|
||||
const auto size =
|
||||
|
||||
@ -49,6 +49,10 @@ void BuildCompleteFilename(std::string& _CompleteFilename, const std::string& _P
|
||||
|
||||
[[nodiscard]] std::string UTF16ToUTF8(std::u16string_view input);
|
||||
[[nodiscard]] std::u16string UTF8ToUTF16(std::string_view input);
|
||||
// Returns UTF-8 normalized to NFC on platforms that need explicit Unicode normalization.
|
||||
#if defined(__APPLE__)
|
||||
[[nodiscard]] std::string NormalizeNFDToNFC(std::string_view input);
|
||||
#endif
|
||||
|
||||
#ifdef _WIN32
|
||||
[[nodiscard]] std::string UTF16ToUTF8(const std::wstring& input);
|
||||
|
||||
@ -1,13 +1,15 @@
|
||||
// Copyright 2014 Citra Emulator Project
|
||||
// Copyright Citra Emulator Project / Azahar Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#include <algorithm>
|
||||
#include <iterator>
|
||||
#include <memory>
|
||||
#include "common/archives.h"
|
||||
#include "common/common_types.h"
|
||||
#include "common/file_util.h"
|
||||
#include "common/logging/log.h"
|
||||
#include "common/string_util.h"
|
||||
#include "core/file_sys/disk_archive.h"
|
||||
#include "core/file_sys/errors.h"
|
||||
|
||||
@ -62,22 +64,29 @@ u32 DiskDirectory::Read(const u32 count, Entry* entries) {
|
||||
|
||||
while (entries_read < count && children_iterator != directory.children.cend()) {
|
||||
const FileUtil::FSTEntry& file = *children_iterator;
|
||||
// Directory entries are exposed to the guest as UTF-16. Normalize host UTF-8 names first
|
||||
// so host Unicode normalization differences do not leak into guest-visible SDMC paths.
|
||||
#if defined(__APPLE__)
|
||||
const std::string filename = Common::NormalizeNFDToNFC(file.virtualName);
|
||||
#else
|
||||
const std::string& filename = file.virtualName;
|
||||
#endif
|
||||
const std::u16string filename_utf16 = Common::UTF8ToUTF16(filename);
|
||||
Entry& entry = entries[entries_read];
|
||||
|
||||
LOG_TRACE(Service_FS, "File {}: size={} dir={}", filename, file.size, file.isDirectory);
|
||||
|
||||
// TODO(Link Mauve): use a proper conversion to UTF-16.
|
||||
for (std::size_t j = 0; j < FILENAME_LENGTH; ++j) {
|
||||
entry.filename[j] = filename[j];
|
||||
if (!filename[j])
|
||||
break;
|
||||
std::fill(std::begin(entry.filename), std::end(entry.filename), u'\0');
|
||||
|
||||
const std::size_t copy_length = std::min(filename_utf16.size(), FILENAME_LENGTH - 1);
|
||||
for (std::size_t j = 0; j < copy_length; ++j) {
|
||||
entry.filename[j] = filename_utf16[j];
|
||||
}
|
||||
|
||||
FileUtil::SplitFilename83(filename, entry.short_name, entry.extension);
|
||||
|
||||
entry.is_directory = file.isDirectory;
|
||||
entry.is_hidden = (filename[0] == '.');
|
||||
entry.is_hidden = (!filename.empty() && filename[0] == '.');
|
||||
entry.is_read_only = 0;
|
||||
entry.file_size = file.size;
|
||||
|
||||
@ -92,5 +101,4 @@ u32 DiskDirectory::Read(const u32 count, Entry* entries) {
|
||||
}
|
||||
return entries_read;
|
||||
}
|
||||
|
||||
} // namespace FileSys
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright 2023 Citra Emulator Project
|
||||
// Copyright Citra Emulator Project / Azahar Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
@ -24,3 +24,13 @@ TEST_CASE("SplitFilename83 Sanity", "[common]") {
|
||||
REQUIRE(std::memcmp(short_name.data(), expected_short_name.data(), short_name.size()) == 0);
|
||||
REQUIRE(std::memcmp(extension.data(), expected_extension.data(), extension.size()) == 0);
|
||||
}
|
||||
|
||||
#if defined(__APPLE__)
|
||||
|
||||
TEST_CASE("NormalizeNFDToNFC Sanity", "[common]") {
|
||||
const std::string decomposed = "i\xCC\x81";
|
||||
const std::string composed = "\xC3\xAD";
|
||||
|
||||
REQUIRE(Common::NormalizeNFDToNFC(decomposed) == composed);
|
||||
}
|
||||
#endif
|
||||
Loading…
Reference in New Issue
Block a user