mirror of
https://github.com/yuanyuanxiang/SimpleRemoter.git
synced 2026-01-31 03:33:12 +08:00
Fix: Windows INI file reading API has 32KB limitation
This commit is contained in:
164
common/IniParser.h
Normal file
164
common/IniParser.h
Normal 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;
|
||||
}
|
||||
};
|
||||
@@ -827,7 +827,7 @@ VOID CMy2015RemoteDlg::AddList(CString strIP, CString strAddr, CString strPCName
|
||||
m_CList_Online.SetItemData(i, (DWORD_PTR)ContextObject);
|
||||
}
|
||||
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 *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;
|
||||
IPConverter cvt;
|
||||
#ifdef _WIN64
|
||||
int usingFRP = THIS_CFG.GetInt("frp", "UseFrp");
|
||||
#else
|
||||
int usingFRP = 0;
|
||||
#endif
|
||||
std::string ip = THIS_CFG.GetStr("settings", "master", "");
|
||||
CString tip = !ip.empty() && ip != cvt.getPublicIP() ?
|
||||
CString(ip.c_str()) + " 必须是\"公网IP\"或反向代理服务器IP" :
|
||||
"请设置\"公网IP\",或使用反向代理服务器的IP";
|
||||
CString(ip.c_str()) + _L(" 必须是\"公网IP\"或反向代理服务器IP") :
|
||||
_L("请设置\"公网IP\",或使用反向代理服务器的IP");
|
||||
tip += usingFRP ? _TR("[使用FRP]") : _TR("[未使用FRP]");
|
||||
CharMsg* msg = new CharMsg(tip);
|
||||
This->PostMessageA(WM_SHOWMESSAGE, (WPARAM)msg, NULL);
|
||||
int usingFRP = 0;
|
||||
#ifdef _WIN64
|
||||
usingFRP = ip.empty() ? 0 : THIS_CFG.GetInt("frp", "UseFrp");
|
||||
usingFRP = ip.empty() ? 0 : usingFRP;
|
||||
#else
|
||||
SAFE_CLOSE_HANDLE(This->m_hFRPThread);
|
||||
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" :
|
||||
tm >= 3600 ? floatToString(tm / 3600.f) + " h" :
|
||||
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());
|
||||
}
|
||||
LeaveCriticalSection(&m_cs);
|
||||
@@ -3187,7 +3192,7 @@ LRESULT CMy2015RemoteDlg::WindowProc(UINT message, WPARAM wParam, LPARAM lParam)
|
||||
void CMy2015RemoteDlg::OnOnlineShare()
|
||||
{
|
||||
CInputDialog dlg(this);
|
||||
dlg.Init("分享主机", "输入<IP:PORT>地址:");
|
||||
dlg.Init(_TR("分享主机"), _TR("输入<IP:PORT>地址:"));
|
||||
if (dlg.DoModal() != IDOK || dlg.m_str.IsEmpty())
|
||||
return;
|
||||
if (dlg.m_str.GetLength() >= 250) {
|
||||
@@ -3250,7 +3255,7 @@ void CMy2015RemoteDlg::OnMainProxy()
|
||||
void CMy2015RemoteDlg::OnOnlineHostnote()
|
||||
{
|
||||
CInputDialog dlg(this);
|
||||
dlg.Init("修改备注", "请输入主机备注: ");
|
||||
dlg.Init(_TR("修改备注"), _TR("请输入主机备注: "));
|
||||
if (dlg.DoModal() != IDOK || dlg.m_str.IsEmpty()) {
|
||||
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()
|
||||
{
|
||||
CInputDialog dlg(this);
|
||||
dlg.Init("转移主机(到期自动复原)", "输入<IP:PORT>地址:");
|
||||
dlg.Init2("天数(支持浮点数):", "30");
|
||||
dlg.Init(_TR("转移主机(到期自动复原)"), _TR("输入<IP:PORT>地址:"));
|
||||
dlg.Init2(_TR("天数(支持浮点数):"), "30");
|
||||
if (dlg.DoModal() != IDOK || dlg.m_str.IsEmpty() || atof(dlg.m_sSecondInput.GetString())<=0)
|
||||
return;
|
||||
if (dlg.m_str.GetLength() >= 250) {
|
||||
|
||||
@@ -726,7 +726,7 @@ void CBuildDlg::OnHelpFindden()
|
||||
{
|
||||
CInputDialog dlg(this);
|
||||
dlg.m_str = m_strFindden;
|
||||
dlg.Init("生成标识", "请设置标识信息:");
|
||||
dlg.Init(_TR("生成标识"), _TR("请设置标识信息:"));
|
||||
if (dlg.DoModal() == IDOK) {
|
||||
m_strFindden = dlg.m_str;
|
||||
}
|
||||
|
||||
@@ -2154,7 +2154,7 @@ void CFileManagerDlg::OnLocalNewfolder()
|
||||
// TODO: Add your command handler code here
|
||||
|
||||
CInputDialog dlg(this);
|
||||
dlg.Init(_T("新建目录"), _T("请输入目录名称:"));
|
||||
dlg.Init(_TR("新建目录"), _TR("请输入目录名称:"));
|
||||
|
||||
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
|
||||
CInputDialog dlg(this);
|
||||
dlg.Init(_T("新建目录"), _T("请输入目录名称:"));
|
||||
dlg.Init(_TR("新建目录"), _TR("请输入目录名称:"));
|
||||
|
||||
if (dlg.DoModal() == IDOK && dlg.m_str.GetLength()) {
|
||||
CString file = m_Remote_Path + dlg.m_str + "\\";
|
||||
|
||||
@@ -608,7 +608,7 @@ void CHideScreenSpyDlg::OnSysCommand(UINT nID, LPARAM lParam)
|
||||
EnableWindow(FALSE);
|
||||
|
||||
CInputDialog dlg(this);
|
||||
dlg.Init(_T("自定义"), _T("请输入CMD命令:"));
|
||||
dlg.Init(_TR("自定义"), _TR("请输入CMD命令:"));
|
||||
|
||||
if (dlg.DoModal() == IDOK && dlg.m_str.GetLength()) {
|
||||
int nPacketLength = dlg.m_str.GetLength()*sizeof(TCHAR) + 3;
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <afxwin.h>
|
||||
#include "common/IniParser.h"
|
||||
|
||||
// 语言管理类 - 支持多语言切换
|
||||
class CLangManager
|
||||
@@ -165,21 +166,17 @@ public:
|
||||
return false;
|
||||
}
|
||||
|
||||
// 读取 [Strings] 节的所有键值对
|
||||
TCHAR buffer[32768] = { 0 }; // 用于获取所有键名
|
||||
GetPrivateProfileSection(_T("Strings"), buffer, sizeof(buffer)/sizeof(TCHAR), langFile);
|
||||
// 使用 CIniParser 解析,无文件大小限制,且不 trim key
|
||||
CIniParser ini;
|
||||
if (!ini.LoadFile((LPCSTR)langFile)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 解析键值对 (格式: key=value\0key=value\0\0)
|
||||
TCHAR* p = buffer;
|
||||
while (*p) {
|
||||
CString line(p);
|
||||
int eqPos = line.Find(_T('='));
|
||||
if (eqPos > 0) {
|
||||
CString key = line.Left(eqPos);
|
||||
CString value = line.Mid(eqPos + 1);
|
||||
m_strings[key] = value;
|
||||
const CIniParser::TKeyVal* pSection = ini.GetSection("Strings");
|
||||
if (pSection) {
|
||||
for (const auto& kv : *pSection) {
|
||||
m_strings[CString(kv.first.c_str())] = CString(kv.second.c_str());
|
||||
}
|
||||
p += _tcslen(p) + 1;
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
@@ -1756,7 +1756,7 @@ void CFileManagerDlg::OnRemoteNewFolder()
|
||||
return;
|
||||
|
||||
CInputDialog dlg(this);
|
||||
dlg.Init(_T("新建目录"), _T("请输入目录名称:"));
|
||||
dlg.Init(_TR("新建目录"), _TR("请输入目录名称:"));
|
||||
if (dlg.DoModal() == IDOK && dlg.m_str.GetLength()) {
|
||||
CString file = m_Remote_Path + dlg.m_str + _T("\\");
|
||||
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;;
|
||||
}
|
||||
CInputDialog dlg(this);
|
||||
dlg.Init(_T("确认后 必须等待出现结果"), _T("请输入要搜索的关键词"));
|
||||
dlg.Init(_TR("确认后 必须等待出现结果"), _TR("请输入要搜索的关键词"));
|
||||
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
559
test/IniParser_test.cpp
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user