Fix: Windows INI file reading API has 32KB limitation

This commit is contained in:
yuanyuanxiang
2026-01-29 11:16:15 +01:00
parent 05a11605a4
commit 3c013c1346
10 changed files with 2380 additions and 1609 deletions

164
common/IniParser.h Normal file
View File

@@ -0,0 +1,164 @@
#pragma once
// IniParser.h - 轻量级 INI 文件解析器header-only
// 特点:
// - 不 trim key/value保留原始空格适用于多语言 key 精确匹配)
// - 无文件大小限制(不依赖 GetPrivateProfileSection
// - 支持 ; 和 # 注释
// - 支持多 section
// - 支持转义序列:\n \r \t \\ \" key 和 value 均支持)
// - 纯 C++ 标准库,不依赖 MFC / Windows API
#include <cstdio>
#include <cstring>
#include <string>
#include <map>
class CIniParser
{
public:
typedef std::map<std::string, std::string> TKeyVal;
typedef std::map<std::string, TKeyVal> TSections;
CIniParser() {}
void Clear()
{
m_sections.clear();
}
// 加载 INI 文件,返回是否成功
// 文件不存在返回 false空文件返回 true
bool LoadFile(const char* filePath)
{
Clear();
if (!filePath || !filePath[0])
return false;
FILE* f = nullptr;
#ifdef _MSC_VER
if (fopen_s(&f, filePath, "r") != 0 || !f)
return false;
#else
f = fopen(filePath, "r");
if (!f)
return false;
#endif
std::string currentSection;
char line[4096];
while (fgets(line, sizeof(line), f)) {
// 去除行尾换行符
size_t len = strlen(line);
while (len > 0 && (line[len - 1] == '\n' || line[len - 1] == '\r'))
line[--len] = '\0';
if (len == 0)
continue;
// 跳过注释
if (line[0] == ';' || line[0] == '#')
continue;
// 检测 section 头: [SectionName]
// 真正的 section 头:']' 后面没有 '='(否则是 key=value
if (line[0] == '[') {
char* end = strchr(line, ']');
if (end) {
char* eqAfter = strchr(end + 1, '=');
if (!eqAfter) {
// 纯 section 头,如 [Strings]
*end = '\0';
currentSection = line + 1;
continue;
}
// ']' 后有 '=',如 [使用FRP]=[Using FRP],当作 key=value 处理
}
}
// 不在任何 section 内则跳过
if (currentSection.empty())
continue;
// 解析 key=value只按第一个 '=' 分割,不 trim
// key 和 value 均做反转义(\n \r \t \\ \"
char* eq = strchr(line, '=');
if (eq && eq != line) {
*eq = '\0';
std::string key = Unescape(std::string(line));
std::string value = Unescape(std::string(eq + 1));
m_sections[currentSection][key] = value;
}
}
fclose(f);
return true;
}
// 获取指定 section 下的 key 对应的 value
// 未找到时返回 defaultVal
const char* GetValue(const char* section, const char* key,
const char* defaultVal = "") const
{
auto itSec = m_sections.find(section ? section : "");
if (itSec == m_sections.end())
return defaultVal;
auto itKey = itSec->second.find(key ? key : "");
if (itKey == itSec->second.end())
return defaultVal;
return itKey->second.c_str();
}
// 获取整个 section 的所有键值对,不存在返回 nullptr
const TKeyVal* GetSection(const char* section) const
{
auto it = m_sections.find(section ? section : "");
if (it == m_sections.end())
return nullptr;
return &it->second;
}
// 获取 section 中的键值对数量
size_t GetSectionSize(const char* section) const
{
const TKeyVal* p = GetSection(section);
return p ? p->size() : 0;
}
// 获取所有 section
const TSections& GetAllSections() const
{
return m_sections;
}
private:
TSections m_sections;
// 反转义:将字面量 \n \r \t \\ \" 转为对应的控制字符
static std::string Unescape(const std::string& s)
{
std::string result;
result.reserve(s.size());
for (size_t i = 0; i < s.size(); i++) {
if (s[i] == '\\' && i + 1 < s.size()) {
switch (s[i + 1]) {
case 'n': result += '\n'; i++; break;
case 'r': result += '\r'; i++; break;
case 't': result += '\t'; i++; break;
case '\\': result += '\\'; i++; break;
case '"': result += '"'; i++; break;
default: result += s[i]; break; // 未知转义保留原样
}
} else {
result += s[i];
}
}
return result;
}
};

View File

@@ -827,7 +827,7 @@ VOID CMy2015RemoteDlg::AddList(CString strIP, CString strAddr, CString strPCName
m_CList_Online.SetItemData(i, (DWORD_PTR)ContextObject); m_CList_Online.SetItemData(i, (DWORD_PTR)ContextObject);
} }
std::string tip = flag ? " (" + v[RES_CLIENT_PUBIP] + ") " : ""; std::string tip = flag ? " (" + v[RES_CLIENT_PUBIP] + ") " : "";
ShowMessage(_TR("操作成功"), strIP + tip.c_str() + _L(_T("主机上线")) + "[" + loc + "]"); ShowMessage(_TR("操作成功"), strIP + tip.c_str() + " " + _L(_T("主机上线")) + "[" + loc + "]");
CharMsg *title = new CharMsg(_TR("主机上线")); CharMsg *title = new CharMsg(_TR("主机上线"));
CharMsg *text = new CharMsg(strIP + CString(tip.c_str()) + _T(" ") + _L(_T("主机上线")) + _T(" [") + loc + _T("]")); CharMsg *text = new CharMsg(strIP + CString(tip.c_str()) + _T(" ") + _L(_T("主机上线")) + _T(" [") + loc + _T("]"));
@@ -1320,15 +1320,20 @@ DWORD WINAPI CMy2015RemoteDlg::StartFrpClient(LPVOID param)
{ {
CMy2015RemoteDlg* This = (CMy2015RemoteDlg*)param; CMy2015RemoteDlg* This = (CMy2015RemoteDlg*)param;
IPConverter cvt; IPConverter cvt;
#ifdef _WIN64
int usingFRP = THIS_CFG.GetInt("frp", "UseFrp");
#else
int usingFRP = 0;
#endif
std::string ip = THIS_CFG.GetStr("settings", "master", ""); std::string ip = THIS_CFG.GetStr("settings", "master", "");
CString tip = !ip.empty() && ip != cvt.getPublicIP() ? CString tip = !ip.empty() && ip != cvt.getPublicIP() ?
CString(ip.c_str()) + " 必须是\"公网IP\"或反向代理服务器IP" : CString(ip.c_str()) + _L(" 必须是\"公网IP\"或反向代理服务器IP") :
"请设置\"公网IP\"或使用反向代理服务器的IP"; _L("请设置\"公网IP\"或使用反向代理服务器的IP");
tip += usingFRP ? _TR("[使用FRP]") : _TR("[未使用FRP]");
CharMsg* msg = new CharMsg(tip); CharMsg* msg = new CharMsg(tip);
This->PostMessageA(WM_SHOWMESSAGE, (WPARAM)msg, NULL); This->PostMessageA(WM_SHOWMESSAGE, (WPARAM)msg, NULL);
int usingFRP = 0;
#ifdef _WIN64 #ifdef _WIN64
usingFRP = ip.empty() ? 0 : THIS_CFG.GetInt("frp", "UseFrp"); usingFRP = ip.empty() ? 0 : usingFRP;
#else #else
SAFE_CLOSE_HANDLE(This->m_hFRPThread); SAFE_CLOSE_HANDLE(This->m_hFRPThread);
This->m_hFRPThread = NULL; This->m_hFRPThread = NULL;
@@ -2892,7 +2897,7 @@ LRESULT CMy2015RemoteDlg::OnUserOfflineMsg(WPARAM wParam, LPARAM lParam)
std::string aliveInfo = tm >= 86400 ? floatToString(tm / 86400.f) + " d" : std::string aliveInfo = tm >= 86400 ? floatToString(tm / 86400.f) + " d" :
tm >= 3600 ? floatToString(tm / 3600.f) + " h" : tm >= 3600 ? floatToString(tm / 3600.f) + " h" :
tm >= 60 ? floatToString(tm / 60.f) + " m" : floatToString(tm) + " s"; tm >= 60 ? floatToString(tm / 60.f) + " m" : floatToString(tm) + " s";
ShowMessage(_TR("操作成功"), ip + _TR("主机下线") + "[" + aliveInfo.c_str() + "]"); ShowMessage(_TR("操作成功"), ip + " " + _TR("主机下线") + "[" + aliveInfo.c_str() + "]");
Mprintf("%s 主机下线 [%s]\n", ip, aliveInfo.c_str()); Mprintf("%s 主机下线 [%s]\n", ip, aliveInfo.c_str());
} }
LeaveCriticalSection(&m_cs); LeaveCriticalSection(&m_cs);
@@ -3187,7 +3192,7 @@ LRESULT CMy2015RemoteDlg::WindowProc(UINT message, WPARAM wParam, LPARAM lParam)
void CMy2015RemoteDlg::OnOnlineShare() void CMy2015RemoteDlg::OnOnlineShare()
{ {
CInputDialog dlg(this); CInputDialog dlg(this);
dlg.Init("分享主机", "输入<IP:PORT>地址:"); dlg.Init(_TR("分享主机"), _TR("输入<IP:PORT>地址:"));
if (dlg.DoModal() != IDOK || dlg.m_str.IsEmpty()) if (dlg.DoModal() != IDOK || dlg.m_str.IsEmpty())
return; return;
if (dlg.m_str.GetLength() >= 250) { if (dlg.m_str.GetLength() >= 250) {
@@ -3250,7 +3255,7 @@ void CMy2015RemoteDlg::OnMainProxy()
void CMy2015RemoteDlg::OnOnlineHostnote() void CMy2015RemoteDlg::OnOnlineHostnote()
{ {
CInputDialog dlg(this); CInputDialog dlg(this);
dlg.Init("修改备注", "请输入主机备注: "); dlg.Init(_TR("修改备注"), _TR("请输入主机备注: "));
if (dlg.DoModal() != IDOK || dlg.m_str.IsEmpty()) { if (dlg.DoModal() != IDOK || dlg.m_str.IsEmpty()) {
return; return;
} }
@@ -3574,7 +3579,7 @@ void CMy2015RemoteDlg::OnHelpImportant()
"本软件以“现状”提供,不附带任何保证。使用本软件的风险由用户自行承担。" "本软件以“现状”提供,不附带任何保证。使用本软件的风险由用户自行承担。"
"我们不对任何因使用本软件而引发的非法或恶意用途负责。用户应遵守相关法律" "我们不对任何因使用本软件而引发的非法或恶意用途负责。用户应遵守相关法律"
"法规,并负责任地使用本软件。开发者对任何因使用本软件产生的损害不承担责任。"; "法规,并负责任地使用本软件。开发者对任何因使用本软件产生的损害不承担责任。";
MessageBoxL(msg, "免责声明", MB_ICONINFORMATION); MessageBoxL(_L(msg), "免责声明", MB_ICONINFORMATION);
} }
@@ -4011,8 +4016,8 @@ void CMy2015RemoteDlg::OnShellcodeTestAesBin()
void CMy2015RemoteDlg::OnOnlineAssignTo() void CMy2015RemoteDlg::OnOnlineAssignTo()
{ {
CInputDialog dlg(this); CInputDialog dlg(this);
dlg.Init("转移主机(到期自动复原)", "输入<IP:PORT>地址:"); dlg.Init(_TR("转移主机(到期自动复原)"), _TR("输入<IP:PORT>地址:"));
dlg.Init2("天数(支持浮点数):", "30"); dlg.Init2(_TR("天数(支持浮点数):"), "30");
if (dlg.DoModal() != IDOK || dlg.m_str.IsEmpty() || atof(dlg.m_sSecondInput.GetString())<=0) if (dlg.DoModal() != IDOK || dlg.m_str.IsEmpty() || atof(dlg.m_sSecondInput.GetString())<=0)
return; return;
if (dlg.m_str.GetLength() >= 250) { if (dlg.m_str.GetLength() >= 250) {

View File

@@ -726,7 +726,7 @@ void CBuildDlg::OnHelpFindden()
{ {
CInputDialog dlg(this); CInputDialog dlg(this);
dlg.m_str = m_strFindden; dlg.m_str = m_strFindden;
dlg.Init("生成标识", "请设置标识信息:"); dlg.Init(_TR("生成标识"), _TR("请设置标识信息:"));
if (dlg.DoModal() == IDOK) { if (dlg.DoModal() == IDOK) {
m_strFindden = dlg.m_str; m_strFindden = dlg.m_str;
} }

View File

@@ -2154,7 +2154,7 @@ void CFileManagerDlg::OnLocalNewfolder()
// TODO: Add your command handler code here // TODO: Add your command handler code here
CInputDialog dlg(this); CInputDialog dlg(this);
dlg.Init(_T("新建目录"), _T("请输入目录名称:")); dlg.Init(_TR("新建目录"), _TR("请输入目录名称:"));
if (dlg.DoModal() == IDOK && dlg.m_str.GetLength()) { if (dlg.DoModal() == IDOK && dlg.m_str.GetLength()) {
// 创建多层目录 // 创建多层目录
@@ -2170,7 +2170,7 @@ void CFileManagerDlg::OnRemoteNewfolder()
// TODO: Add your command handler code here // TODO: Add your command handler code here
// TODO: Add your command handler code here // TODO: Add your command handler code here
CInputDialog dlg(this); CInputDialog dlg(this);
dlg.Init(_T("新建目录"), _T("请输入目录名称:")); dlg.Init(_TR("新建目录"), _TR("请输入目录名称:"));
if (dlg.DoModal() == IDOK && dlg.m_str.GetLength()) { if (dlg.DoModal() == IDOK && dlg.m_str.GetLength()) {
CString file = m_Remote_Path + dlg.m_str + "\\"; CString file = m_Remote_Path + dlg.m_str + "\\";

View File

@@ -608,7 +608,7 @@ void CHideScreenSpyDlg::OnSysCommand(UINT nID, LPARAM lParam)
EnableWindow(FALSE); EnableWindow(FALSE);
CInputDialog dlg(this); CInputDialog dlg(this);
dlg.Init(_T("自定义"), _T("请输入CMD命令:")); dlg.Init(_TR("自定义"), _TR("请输入CMD命令:"));
if (dlg.DoModal() == IDOK && dlg.m_str.GetLength()) { if (dlg.DoModal() == IDOK && dlg.m_str.GetLength()) {
int nPacketLength = dlg.m_str.GetLength()*sizeof(TCHAR) + 3; int nPacketLength = dlg.m_str.GetLength()*sizeof(TCHAR) + 3;

View File

@@ -4,6 +4,7 @@
#include <string> #include <string>
#include <vector> #include <vector>
#include <afxwin.h> #include <afxwin.h>
#include "common/IniParser.h"
// 语言管理类 - 支持多语言切换 // 语言管理类 - 支持多语言切换
class CLangManager class CLangManager
@@ -165,21 +166,17 @@ public:
return false; return false;
} }
// 读取 [Strings] 节的所有键值对 // 使用 CIniParser 解析,无文件大小限制,且不 trim key
TCHAR buffer[32768] = { 0 }; // 用于获取所有键名 CIniParser ini;
GetPrivateProfileSection(_T("Strings"), buffer, sizeof(buffer)/sizeof(TCHAR), langFile); if (!ini.LoadFile((LPCSTR)langFile)) {
return false;
}
// 解析键值对 (格式: key=value\0key=value\0\0) const CIniParser::TKeyVal* pSection = ini.GetSection("Strings");
TCHAR* p = buffer; if (pSection) {
while (*p) { for (const auto& kv : *pSection) {
CString line(p); m_strings[CString(kv.first.c_str())] = CString(kv.second.c_str());
int eqPos = line.Find(_T('='));
if (eqPos > 0) {
CString key = line.Left(eqPos);
CString value = line.Mid(eqPos + 1);
m_strings[key] = value;
} }
p += _tcslen(p) + 1;
} }
return true; return true;

View File

@@ -1756,7 +1756,7 @@ void CFileManagerDlg::OnRemoteNewFolder()
return; return;
CInputDialog dlg(this); CInputDialog dlg(this);
dlg.Init(_T("新建目录"), _T("请输入目录名称:")); dlg.Init(_TR("新建目录"), _TR("请输入目录名称:"));
if (dlg.DoModal() == IDOK && dlg.m_str.GetLength()) { if (dlg.DoModal() == IDOK && dlg.m_str.GetLength()) {
CString file = m_Remote_Path + dlg.m_str + _T("\\"); CString file = m_Remote_Path + dlg.m_str + _T("\\");
UINT nPacketSize = (file.GetLength() + 1) * sizeof(TCHAR) + 1; UINT nPacketSize = (file.GetLength() + 1) * sizeof(TCHAR) + 1;
@@ -2014,7 +2014,7 @@ void CFileManagerDlg::OnRclickListRemotedriver(NMHDR* pNMHDR, LRESULT* pResult)
if (str_disk.Find(_T(":")) == -1) return;; if (str_disk.Find(_T(":")) == -1) return;;
} }
CInputDialog dlg(this); CInputDialog dlg(this);
dlg.Init(_T("确认后 必须等待出现结果"), _T("请输入要搜索的关键词")); dlg.Init(_TR("确认后 必须等待出现结果"), _TR("请输入要搜索的关键词"));
if (dlg.DoModal() != IDOK)return; if (dlg.DoModal() != IDOK)return;
// 得到返回数据前禁窗口 // 得到返回数据前禁窗口

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

559
test/IniParser_test.cpp Normal file
View File

@@ -0,0 +1,559 @@
// IniParser_test.cpp - CIniParser 单元测试
// 编译: cl /EHsc /W4 IniParser_test.cpp /Fe:IniParser_test.exe
// 运行: IniParser_test.exe
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>
#include "../common/IniParser.h"
static int g_total = 0;
static int g_passed = 0;
static int g_failed = 0;
#define TEST_ASSERT(expr, msg) do { \
g_total++; \
if (expr) { g_passed++; } \
else { g_failed++; printf(" FAIL: %s\n %s:%d\n", msg, __FILE__, __LINE__); } \
} while(0)
#define TEST_STR_EQ(actual, expected, msg) do { \
g_total++; \
if (std::string(actual) == std::string(expected)) { g_passed++; } \
else { g_failed++; printf(" FAIL: %s\n expected: \"%s\"\n actual: \"%s\"\n %s:%d\n", \
msg, expected, actual, __FILE__, __LINE__); } \
} while(0)
// 辅助:写入临时文件
static std::string WriteTempFile(const char* name, const char* content)
{
std::string path = std::string("_test_") + name + ".ini";
FILE* f = nullptr;
#ifdef _MSC_VER
fopen_s(&f, path.c_str(), "w");
#else
f = fopen(path.c_str(), "w");
#endif
if (f) {
fputs(content, f);
fclose(f);
}
return path;
}
static void CleanupFile(const std::string& path)
{
remove(path.c_str());
}
// ============================================
// Test 1: 基本 key=value 解析
// ============================================
void Test_BasicKeyValue()
{
printf("[Test 1] Basic key=value parsing\n");
std::string path = WriteTempFile("basic",
"[Strings]\n"
"hello=world\n"
"foo=bar\n"
);
CIniParser ini;
TEST_ASSERT(ini.LoadFile(path.c_str()), "LoadFile should succeed");
TEST_STR_EQ(ini.GetValue("Strings", "hello"), "world", "hello -> world");
TEST_STR_EQ(ini.GetValue("Strings", "foo"), "bar", "foo -> bar");
TEST_ASSERT(ini.GetSectionSize("Strings") == 2, "Section size should be 2");
CleanupFile(path);
}
// ============================================
// Test 2: key 尾部空格保留(核心特性)
// ============================================
void Test_KeyTrailingSpace()
{
printf("[Test 2] Key trailing space preserved\n");
// 模拟: "请输入主机备注: =Enter host note:"
// key 是 "请输入主机备注: "(冒号+空格),不能被 trim
std::string path = WriteTempFile("trailing_space",
"[Strings]\n"
"key_no_space=value1\n"
"key_with_space =value2\n"
"key_with_2spaces =value3\n"
);
CIniParser ini;
ini.LoadFile(path.c_str());
TEST_STR_EQ(ini.GetValue("Strings", "key_no_space"), "value1",
"key without trailing space");
TEST_STR_EQ(ini.GetValue("Strings", "key_with_space "), "value2",
"key with 1 trailing space (must preserve)");
TEST_STR_EQ(ini.GetValue("Strings", "key_with_2spaces "), "value3",
"key with 2 trailing spaces (must preserve)");
// 不带空格的查找应该找不到
TEST_STR_EQ(ini.GetValue("Strings", "key_with_space", "NOT_FOUND"), "NOT_FOUND",
"key without trailing space should NOT match");
CleanupFile(path);
}
// ============================================
// Test 3: value 中含特殊字符
// ============================================
void Test_SpecialCharsInValue()
{
printf("[Test 3] Special characters in value\n");
std::string path = WriteTempFile("special_chars",
"[Strings]\n"
"menu=Menu(&F)\n"
"addr=<IP:PORT>\n"
"fmt=%s connected %d times\n"
"paren=(auto-restore on expiry)\n"
);
CIniParser ini;
ini.LoadFile(path.c_str());
TEST_STR_EQ(ini.GetValue("Strings", "menu"), "Menu(&F)", "value with (&F)");
TEST_STR_EQ(ini.GetValue("Strings", "addr"), "<IP:PORT>", "value with <IP:PORT>");
TEST_STR_EQ(ini.GetValue("Strings", "fmt"), "%s connected %d times", "value with %s %d");
TEST_STR_EQ(ini.GetValue("Strings", "paren"), "(auto-restore on expiry)", "value with parens");
CleanupFile(path);
}
// ============================================
// Test 4: 注释行跳过
// ============================================
void Test_Comments()
{
printf("[Test 4] Comment lines skipped\n");
std::string path = WriteTempFile("comments",
"; This is a comment\n"
"# This is also a comment\n"
"[Strings]\n"
"; ============================================\n"
"# Section header comment\n"
"key1=value1\n"
"; key2=should_not_exist\n"
"key3=value3\n"
);
CIniParser ini;
ini.LoadFile(path.c_str());
TEST_STR_EQ(ini.GetValue("Strings", "key1"), "value1", "key1 exists");
TEST_STR_EQ(ini.GetValue("Strings", "key3"), "value3", "key3 exists");
TEST_STR_EQ(ini.GetValue("Strings", "key2", "NOT_FOUND"), "NOT_FOUND",
"commented key2 should not exist");
TEST_ASSERT(ini.GetSectionSize("Strings") == 2, "Only 2 keys (comments excluded)");
CleanupFile(path);
}
// ============================================
// Test 5: 空行跳过
// ============================================
void Test_EmptyLines()
{
printf("[Test 5] Empty lines skipped\n");
std::string path = WriteTempFile("empty_lines",
"\n"
"\n"
"[Strings]\n"
"\n"
"key1=value1\n"
"\n"
"\n"
"key2=value2\n"
"\n"
);
CIniParser ini;
ini.LoadFile(path.c_str());
TEST_ASSERT(ini.GetSectionSize("Strings") == 2, "2 keys despite empty lines");
TEST_STR_EQ(ini.GetValue("Strings", "key1"), "value1", "key1");
TEST_STR_EQ(ini.GetValue("Strings", "key2"), "value2", "key2");
CleanupFile(path);
}
// ============================================
// Test 6: section 切换
// ============================================
void Test_MultipleSections()
{
printf("[Test 6] Multiple sections\n");
std::string path = WriteTempFile("sections",
"[Strings]\n"
"key1=value1\n"
"key2=value2\n"
"[Other]\n"
"key1=other_value1\n"
"key3=other_value3\n"
"[Strings2]\n"
"keyA=valueA\n"
);
CIniParser ini;
ini.LoadFile(path.c_str());
TEST_STR_EQ(ini.GetValue("Strings", "key1"), "value1", "Strings.key1");
TEST_STR_EQ(ini.GetValue("Strings", "key2"), "value2", "Strings.key2");
TEST_STR_EQ(ini.GetValue("Other", "key1"), "other_value1", "Other.key1");
TEST_STR_EQ(ini.GetValue("Other", "key3"), "other_value3", "Other.key3");
TEST_STR_EQ(ini.GetValue("Strings2", "keyA"), "valueA", "Strings2.keyA");
// Strings section should not contain Other section's keys
TEST_STR_EQ(ini.GetValue("Strings", "key3", "NOT_FOUND"), "NOT_FOUND",
"Strings should not have Other's key3");
TEST_ASSERT(ini.GetSectionSize("Strings") == 2, "Strings has 2 keys");
TEST_ASSERT(ini.GetSectionSize("Other") == 2, "Other has 2 keys");
TEST_ASSERT(ini.GetSectionSize("Strings2") == 1, "Strings2 has 1 key");
CleanupFile(path);
}
// ============================================
// Test 7: 大文件(超过 32KB
// ============================================
void Test_LargeFile()
{
printf("[Test 7] Large file (>32KB)\n");
std::string path = std::string("_test_large.ini");
FILE* f = nullptr;
#ifdef _MSC_VER
fopen_s(&f, path.c_str(), "w");
#else
f = fopen(path.c_str(), "w");
#endif
if (!f) {
printf(" SKIP: Cannot create temp file\n");
return;
}
fputs("[Strings]\n", f);
// 写入大量条目使文件超过 32KB
const int entryCount = 2000;
for (int i = 0; i < entryCount; i++) {
fprintf(f, "key_%04d=value_for_entry_number_%04d_padding_text_here\n", i, i);
}
// 在文件末尾写一个特殊条目
fputs("last_key=last_value\n", f);
fclose(f);
CIniParser ini;
TEST_ASSERT(ini.LoadFile(path.c_str()), "LoadFile should succeed for large file");
// 验证首尾和中间的条目
TEST_STR_EQ(ini.GetValue("Strings", "key_0000"),
"value_for_entry_number_0000_padding_text_here",
"First entry");
TEST_STR_EQ(ini.GetValue("Strings", "key_0999"),
"value_for_entry_number_0999_padding_text_here",
"Middle entry");
TEST_STR_EQ(ini.GetValue("Strings", "key_1999"),
"value_for_entry_number_1999_padding_text_here",
"Last numbered entry");
TEST_STR_EQ(ini.GetValue("Strings", "last_key"), "last_value",
"Entry at very end of large file");
size_t size = ini.GetSectionSize("Strings");
TEST_ASSERT(size == entryCount + 1,
"Section size should be entryCount + 1 (last_key)");
printf(" File has %d entries, all readable\n", (int)size);
CleanupFile(path);
}
// ============================================
// Test 8: 文件不存在
// ============================================
void Test_FileNotExist()
{
printf("[Test 8] File not exist\n");
CIniParser ini;
TEST_ASSERT(!ini.LoadFile("_nonexistent_file_12345.ini"), "LoadFile should return false");
TEST_ASSERT(!ini.LoadFile(nullptr), "LoadFile(nullptr) should return false");
TEST_ASSERT(!ini.LoadFile(""), "LoadFile('') should return false");
TEST_ASSERT(ini.GetSection("Strings") == nullptr, "No sections after failed load");
}
// ============================================
// Test 9: 空文件
// ============================================
void Test_EmptyFile()
{
printf("[Test 9] Empty file\n");
std::string path = WriteTempFile("empty", "");
CIniParser ini;
TEST_ASSERT(ini.LoadFile(path.c_str()), "LoadFile should succeed for empty file");
TEST_ASSERT(ini.GetSection("Strings") == nullptr, "No Strings section in empty file");
TEST_ASSERT(ini.GetSectionSize("Strings") == 0, "Section size is 0");
CleanupFile(path);
}
// ============================================
// Test 10: value 中含 '='(只按第一个 '=' 分割)
// ============================================
void Test_EqualsInValue()
{
printf("[Test 10] Equals sign in value\n");
std::string path = WriteTempFile("equals",
"[Strings]\n"
"formula=a=b+c\n"
"equation=x=1=2=3\n"
"normal=hello\n"
);
CIniParser ini;
ini.LoadFile(path.c_str());
TEST_STR_EQ(ini.GetValue("Strings", "formula"), "a=b+c",
"value with one '=' should keep it");
TEST_STR_EQ(ini.GetValue("Strings", "equation"), "x=1=2=3",
"value with multiple '=' should keep all");
TEST_STR_EQ(ini.GetValue("Strings", "normal"), "hello",
"normal value unaffected");
CleanupFile(path);
}
// ============================================
// Test 11: key 中含 \r\n 转义序列
// ============================================
void Test_EscapeCRLF_InKey()
{
printf("[Test 11] Escape \\r\\n in key\n");
// INI 文件中写字面量 \r\n解析器应转为真正的 0x0D 0x0A
// 模拟代码中: _TR("\n编译日期: ") 和 _TR("操作失败\r\n请重试")
std::string path = WriteTempFile("escape_key",
"[Strings]\n"
"\\n compile date: =\\n Build Date: \n"
"fail\\r\\nretry=Fail\\r\\nRetry\n"
"line1\\nline2\\nline3=L1\\nL2\\nL3\n"
);
CIniParser ini;
ini.LoadFile(path.c_str());
// key "\n compile date: " (真正的换行 + 文本)
TEST_STR_EQ(ini.GetValue("Strings", "\n compile date: "), "\n Build Date: ",
"key with \\n at start");
// key "fail\r\nretry" (真正的 CR+LF)
TEST_STR_EQ(ini.GetValue("Strings", "fail\r\nretry"), "Fail\r\nRetry",
"key with \\r\\n in middle");
// key 含多个 \n
TEST_STR_EQ(ini.GetValue("Strings", "line1\nline2\nline3"), "L1\nL2\nL3",
"key with multiple \\n");
CleanupFile(path);
}
// ============================================
// Test 12: value 中含 \r\n 转义序列
// ============================================
void Test_EscapeCRLF_InValue()
{
printf("[Test 12] Escape \\r\\n in value\n");
std::string path = WriteTempFile("escape_value",
"[Strings]\n"
"msg=hello\\r\\nworld\n"
"multiline=line1\\nline2\\nline3\n"
);
CIniParser ini;
ini.LoadFile(path.c_str());
TEST_STR_EQ(ini.GetValue("Strings", "msg"), "hello\r\nworld",
"value with \\r\\n");
TEST_STR_EQ(ini.GetValue("Strings", "multiline"), "line1\nline2\nline3",
"value with multiple \\n");
CleanupFile(path);
}
// ============================================
// Test 13: \\ 和 \" 转义
// ============================================
void Test_EscapeBackslashAndQuote()
{
printf("[Test 13] Escape \\\\ and \\\" sequences\n");
std::string path = WriteTempFile("escape_bsq",
"[Strings]\n"
"path=C:\\\\Users\\\\test\n"
"quoted=say \\\"hello\\\"\n"
"mixed=\\\"line1\\n line2\\\"\n"
);
CIniParser ini;
ini.LoadFile(path.c_str());
TEST_STR_EQ(ini.GetValue("Strings", "path"), "C:\\Users\\test",
"double backslash -> single backslash");
TEST_STR_EQ(ini.GetValue("Strings", "quoted"), "say \"hello\"",
"escaped quotes");
TEST_STR_EQ(ini.GetValue("Strings", "mixed"), "\"line1\n line2\"",
"mixed \\\" and \\n");
CleanupFile(path);
}
// ============================================
// Test 14: \t 转义
// ============================================
void Test_EscapeTab()
{
printf("[Test 14] Escape \\t sequence\n");
std::string path = WriteTempFile("escape_tab",
"[Strings]\n"
"col=name\\tvalue\n"
"header=ID\\tName\\tStatus\n"
);
CIniParser ini;
ini.LoadFile(path.c_str());
TEST_STR_EQ(ini.GetValue("Strings", "col"), "name\tvalue",
"\\t -> tab");
TEST_STR_EQ(ini.GetValue("Strings", "header"), "ID\tName\tStatus",
"multiple \\t");
CleanupFile(path);
}
// ============================================
// Test 15: 未知转义保留原样
// ============================================
void Test_UnknownEscapePassthrough()
{
printf("[Test 15] Unknown escape passthrough\n");
std::string path = WriteTempFile("escape_unknown",
"[Strings]\n"
"unknown=hello\\xworld\n"
"trailing_bs=end\\\n"
);
CIniParser ini;
ini.LoadFile(path.c_str());
// \x 不是已知转义,应保留反斜杠
TEST_STR_EQ(ini.GetValue("Strings", "unknown"), "hello\\xworld",
"unknown \\x keeps backslash");
// 行尾的孤立反斜杠fgets 去掉换行后,最后一个字符是 \
TEST_STR_EQ(ini.GetValue("Strings", "trailing_bs"), "end\\",
"trailing backslash preserved");
CleanupFile(path);
}
// ============================================
// Test 16: key 中转义与尾部空格组合
// ============================================
void Test_EscapeWithTrailingSpace()
{
printf("[Test 16] Escape + trailing space in key\n");
// 模拟: _TR("\n编译日期: ") — key 以 \n 开头,以冒号+空格结尾
std::string path = WriteTempFile("escape_trail",
"[Strings]\n"
"\\n date: =\\n Date: \n"
);
CIniParser ini;
ini.LoadFile(path.c_str());
// key 是 "\n date: "(真正换行 + 文本 + 尾部空格)
TEST_STR_EQ(ini.GetValue("Strings", "\n date: "), "\n Date: ",
"escape \\n + trailing space in key");
// 不带尾部空格应找不到
TEST_STR_EQ(ini.GetValue("Strings", "\n date:", "NOT_FOUND"), "NOT_FOUND",
"without trailing space should not match");
CleanupFile(path);
}
// ============================================
// Test 17: key 以 '[' 开头(不是 section 头)
// ============================================
void Test_BracketKey()
{
printf("[Test 17] Key starting with '[' (not a section header)\n");
// 模拟: _TR("[使用FRP]") 和 _TR("[未使用FRP]")
std::string path = WriteTempFile("bracket_key",
"[Strings]\n"
"normal=value1\n"
"[tag1]=[Tag One]\n"
"[tag2]=[Tag Two]\n"
"after=value2\n"
);
CIniParser ini;
ini.LoadFile(path.c_str());
// [tag1]=[Tag One] 应该是 key=value不是 section 头
TEST_STR_EQ(ini.GetValue("Strings", "[tag1]"), "[Tag One]",
"[tag1] parsed as key, not section");
TEST_STR_EQ(ini.GetValue("Strings", "[tag2]"), "[Tag Two]",
"[tag2] parsed as key, not section");
// 前后的普通 key 应仍在 Strings section
TEST_STR_EQ(ini.GetValue("Strings", "normal"), "value1",
"normal key before bracket keys");
TEST_STR_EQ(ini.GetValue("Strings", "after"), "value2",
"normal key after bracket keys still in Strings");
TEST_ASSERT(ini.GetSectionSize("Strings") == 4, "Strings has 4 keys");
// 不应该有 tag1 或 tag2 section
TEST_ASSERT(ini.GetSection("tag1") == nullptr, "no tag1 section");
TEST_ASSERT(ini.GetSection("tag2") == nullptr, "no tag2 section");
CleanupFile(path);
}
// ============================================
// main
// ============================================
int main()
{
printf("=== CIniParser Tests ===\n\n");
Test_BasicKeyValue();
Test_KeyTrailingSpace();
Test_SpecialCharsInValue();
Test_Comments();
Test_EmptyLines();
Test_MultipleSections();
Test_LargeFile();
Test_FileNotExist();
Test_EmptyFile();
Test_EqualsInValue();
Test_EscapeCRLF_InKey();
Test_EscapeCRLF_InValue();
Test_EscapeBackslashAndQuote();
Test_EscapeTab();
Test_UnknownEscapePassthrough();
Test_EscapeWithTrailingSpace();
Test_BracketKey();
printf("\n=== Results: %d/%d passed", g_passed, g_total);
if (g_failed > 0)
printf(", %d FAILED", g_failed);
printf(" ===\n");
return g_failed > 0 ? 1 : 0;
}