From c0b6b31a9a35ae70557753f89a14a1390d2f442e Mon Sep 17 00:00:00 2001 From: gaozheng Date: Wed, 1 Jul 2026 08:07:40 +0800 Subject: [PATCH] =?UTF-8?q?fix(io/gpr):=20=E9=9B=B7=E8=BE=BE=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E6=8C=89=E5=AE=BD=E5=AD=97=E7=AC=A6=E6=89=93=E5=BC=80?= =?UTF-8?q?,=E6=94=AF=E6=8C=81=E4=B8=AD=E6=96=87=E7=9B=AE=E5=BD=95?= =?UTF-8?q?=E8=B7=AF=E5=BE=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 规范化/Impulse 雷达 reader 的 std::ifstream 用 toLocal8Bit 产出的窄字节(GBK)路径 打开文件。GUI app 链接 QWebEngine(Chromium)/VTK,启动时 setlocale(LC_ALL,"") 把 LC_CTYPE 提升为系统 UTF-8 locale,此后 narrow ifstream 把 GBK 路径字节当 UTF-8 解析 → 打不开 → "打开 .head 失败"。纯 "C" locale 的无头/单测环境用 CP_ACP=GBK 解窄路径, 不触发,故此前未暴露。 新增 io/gpr/LocalPath: Windows 用 MultiByteToWideChar(CP_ACP) 把本地 8 位字节解成 宽字符 std::filesystem::path,使 ifstream/ofstream/file_size 走宽字符打开,与 locale 无关;非 Windows 直接 UTF-8。改到所有外部导入路径 open 点(.head/.data/.iprb/.iprh/ .ord/.gps)。 回归测试 OpensCjkDirectoryPathUnderUtf8Locale: 显式置 .UTF-8 locale 复现 app 运行期 条件,走真实 buildLineVolumeFromNormalized 断言中文目录建体成功(退回 narrow 则抛错, 红/绿已验)。 --- src/data/StreamingVolumeBuilder.cpp | 5 +- src/io/gpr/CMakeLists.txt | 2 +- src/io/gpr/GprSurveyAssembler.cpp | 3 +- src/io/gpr/GpsTrack.cpp | 4 +- src/io/gpr/IprbReader.cpp | 6 +- src/io/gpr/LocalPath.cpp | 24 +++++++ src/io/gpr/LocalPath.hpp | 16 +++++ src/io/gpr/NormalizedRadarReader.cpp | 5 +- src/io/gpr/NormalizedRadarVolumeBridge.cpp | 3 +- tests/io/gpr/test_normalized_radar_bridge.cpp | 70 +++++++++++++++++++ 10 files changed, 128 insertions(+), 10 deletions(-) create mode 100644 src/io/gpr/LocalPath.cpp create mode 100644 src/io/gpr/LocalPath.hpp diff --git a/src/data/StreamingVolumeBuilder.cpp b/src/data/StreamingVolumeBuilder.cpp index 20c6a6d..473f5cc 100644 --- a/src/data/StreamingVolumeBuilder.cpp +++ b/src/data/StreamingVolumeBuilder.cpp @@ -16,6 +16,7 @@ #include "data/store/ChunkedVolumeStore.hpp" #include "io/gpr/GprSurveyAssembler.hpp" #include "io/gpr/IprHeader.hpp" +#include "io/gpr/LocalPath.hpp" namespace geopro::data { @@ -49,7 +50,7 @@ std::string toHeaderPath(const std::string& iprbPath) { } std::string readFileText(const std::string& path) { - std::ifstream f(path, std::ios::binary); + std::ifstream f(geopro::io::gpr::localPath(path), std::ios::binary); if (!f) throw std::runtime_error("StreamingVolumeBuilder: 无法打开 " + path); std::ostringstream ss; ss << f.rdbuf(); @@ -68,7 +69,7 @@ std::int64_t totalTraces(const std::vector& iprb, double& surveyDx) throw std::runtime_error("StreamingVolumeBuilder: samples<=0"); if (c == 0) surveyDx = h.distanceInterval; const std::int64_t bytes = - static_cast(fs::file_size(fs::path(iprb[c]))); + static_cast(fs::file_size(geopro::io::gpr::localPath(iprb[c]))); const std::int64_t per = static_cast(h.samples) * 2; if (per <= 0 || bytes % per != 0) throw std::runtime_error("StreamingVolumeBuilder: .iprb 字节非整道"); diff --git a/src/io/gpr/CMakeLists.txt b/src/io/gpr/CMakeLists.txt index 0470637..4a811d4 100644 --- a/src/io/gpr/CMakeLists.txt +++ b/src/io/gpr/CMakeLists.txt @@ -1,6 +1,6 @@ add_library(geopro_io_gpr STATIC IprHeader.cpp IprbReader.cpp GprGeometry.cpp GprSurveyAssembler.cpp - GpsTrack.cpp NormalizedRadarReader.cpp + GpsTrack.cpp NormalizedRadarReader.cpp LocalPath.cpp RadarVolumeAssembler.cpp NormalizedRadarVolumeBridge.cpp) target_include_directories(geopro_io_gpr PUBLIC ${CMAKE_SOURCE_DIR}/src) target_compile_features(geopro_io_gpr PUBLIC cxx_std_17) diff --git a/src/io/gpr/GprSurveyAssembler.cpp b/src/io/gpr/GprSurveyAssembler.cpp index e9e3c2e..4371f3d 100644 --- a/src/io/gpr/GprSurveyAssembler.cpp +++ b/src/io/gpr/GprSurveyAssembler.cpp @@ -10,6 +10,7 @@ #include "io/gpr/GprGeometry.hpp" #include "io/gpr/IprHeader.hpp" #include "io/gpr/IprbReader.hpp" +#include "io/gpr/LocalPath.hpp" namespace geopro::io::gpr { namespace { @@ -27,7 +28,7 @@ std::string toHeaderPath(const std::string& iprbPath) { } std::string readFileText(const std::string& path) { - std::ifstream f(path, std::ios::binary); + std::ifstream f(localPath(path), std::ios::binary); if (!f) { throw std::runtime_error("无法打开文件: " + path); } diff --git a/src/io/gpr/GpsTrack.cpp b/src/io/gpr/GpsTrack.cpp index 5004b9a..04e8e84 100644 --- a/src/io/gpr/GpsTrack.cpp +++ b/src/io/gpr/GpsTrack.cpp @@ -7,6 +7,8 @@ #include #include +#include "io/gpr/LocalPath.hpp" + namespace geopro::io::gpr { namespace { @@ -26,7 +28,7 @@ bool parseDouble(const std::string& s, double& out) { } // namespace GpsTrack parseGps(const std::string& path) { - std::ifstream f(path); + std::ifstream f(localPath(path)); if (!f) throw std::runtime_error("parseGps: 打不开 " + path); GpsTrack track; diff --git a/src/io/gpr/IprbReader.cpp b/src/io/gpr/IprbReader.cpp index 2f6c76d..c55122b 100644 --- a/src/io/gpr/IprbReader.cpp +++ b/src/io/gpr/IprbReader.cpp @@ -4,10 +4,12 @@ #include #include +#include "io/gpr/LocalPath.hpp" + namespace geopro::io::gpr { BScan readIprb(const std::string& path, const IprHeader& h) { - std::ifstream f(path, std::ios::binary | std::ios::ate); + std::ifstream f(localPath(path), std::ios::binary | std::ios::ate); if (!f) { throw std::runtime_error("readIprb: 无法打开文件: " + path); } @@ -43,7 +45,7 @@ BScan readIprb(const std::string& path, const IprHeader& h) { BScan readIprbRange(const std::string& path, const IprHeader& h, std::int64_t t0, std::int64_t t1) { - std::ifstream f(path, std::ios::binary | std::ios::ate); + std::ifstream f(localPath(path), std::ios::binary | std::ios::ate); if (!f) { throw std::runtime_error("readIprbRange: 无法打开文件: " + path); } diff --git a/src/io/gpr/LocalPath.cpp b/src/io/gpr/LocalPath.cpp new file mode 100644 index 0000000..03f67de --- /dev/null +++ b/src/io/gpr/LocalPath.cpp @@ -0,0 +1,24 @@ +#include "io/gpr/LocalPath.hpp" + +#ifdef _WIN32 +#include +#endif + +namespace geopro::io::gpr { + +std::filesystem::path localPath(const std::string& p) { +#ifdef _WIN32 + if (p.empty()) return std::filesystem::path{}; + const int wlen = ::MultiByteToWideChar( + CP_ACP, 0, p.data(), static_cast(p.size()), nullptr, 0); + if (wlen <= 0) return std::filesystem::path(p); // 退化:原样(ASCII 安全) + std::wstring w(static_cast(wlen), L'\0'); + ::MultiByteToWideChar(CP_ACP, 0, p.data(), static_cast(p.size()), + w.data(), wlen); + return std::filesystem::path(w); +#else + return std::filesystem::path(p); // POSIX:窄字节即 UTF-8 原生路径 +#endif +} + +} // namespace geopro::io::gpr diff --git a/src/io/gpr/LocalPath.hpp b/src/io/gpr/LocalPath.hpp new file mode 100644 index 0000000..4adf024 --- /dev/null +++ b/src/io/gpr/LocalPath.hpp @@ -0,0 +1,16 @@ +#pragma once + +#include +#include + +namespace geopro::io::gpr { + +// 把"本地 8 位编码"的窄字节路径转成 std::filesystem::path。 +// Windows:源串按当前 ANSI 代码页(简中=GBK/936,即 QString::toLocal8Bit 产物) +// 解码为宽字符 path,使 std::ifstream/ofstream 走宽字符打开 —— 否则 +// 窄字符 ifstream 在默认 "C" locale 下无法解析含中文的路径,open 失败。 +// 其它平台:窄字节即原生 UTF-8 路径,直接构造。 +// 退化保护:转换失败时原样返回(ASCII 路径不受影响)。 +std::filesystem::path localPath(const std::string& p); + +} // namespace geopro::io::gpr diff --git a/src/io/gpr/NormalizedRadarReader.cpp b/src/io/gpr/NormalizedRadarReader.cpp index a3d360f..2f28b1d 100644 --- a/src/io/gpr/NormalizedRadarReader.cpp +++ b/src/io/gpr/NormalizedRadarReader.cpp @@ -1,4 +1,5 @@ #include "io/gpr/NormalizedRadarReader.hpp" +#include "io/gpr/LocalPath.hpp" #include #include #include @@ -76,11 +77,11 @@ std::vector readRadarDataCube(const std::string& dataPath, const std::size_t n = static_cast(h.lastTrace) * h.samples; const std::uintmax_t expect = static_cast(n) * 2; std::error_code ec; - const auto fsize = std::filesystem::file_size(dataPath, ec); + const auto fsize = std::filesystem::file_size(localPath(dataPath), ec); if (ec || fsize != expect) throw std::runtime_error("规范化 .data 大小不符: " + dataPath); std::vector cube(n); - std::ifstream f(dataPath, std::ios::binary); + std::ifstream f(localPath(dataPath), std::ios::binary); if (!f) throw std::runtime_error("打开 .data 失败: " + dataPath); f.read(reinterpret_cast(cube.data()), static_cast(expect)); if (!f) throw std::runtime_error("读 .data 失败: " + dataPath); diff --git a/src/io/gpr/NormalizedRadarVolumeBridge.cpp b/src/io/gpr/NormalizedRadarVolumeBridge.cpp index ca021d6..9a21015 100644 --- a/src/io/gpr/NormalizedRadarVolumeBridge.cpp +++ b/src/io/gpr/NormalizedRadarVolumeBridge.cpp @@ -7,6 +7,7 @@ #include #include +#include "io/gpr/LocalPath.hpp" #include "io/gpr/NormalizedRadarReader.hpp" #include "io/gpr/RadarVolumeAssembler.hpp" @@ -20,7 +21,7 @@ geopro::core::BuiltI16 buildLineVolumeFromNormalized(const std::string& lineDir, std::string headText; { - std::ifstream f(head); + std::ifstream f(localPath(head)); if (!f) throw std::runtime_error("打开 .head 失败: " + head); std::stringstream ss; ss << f.rdbuf(); diff --git a/tests/io/gpr/test_normalized_radar_bridge.cpp b/tests/io/gpr/test_normalized_radar_bridge.cpp index 8e06d1f..1adc0de 100644 --- a/tests/io/gpr/test_normalized_radar_bridge.cpp +++ b/tests/io/gpr/test_normalized_radar_bridge.cpp @@ -1,11 +1,29 @@ #include +#include #include #include #include +#include #include "core/algo/GprVolumeBuilder.hpp" #include "io/gpr/NormalizedRadarVolumeBridge.hpp" +#ifdef _WIN32 +#include +#endif namespace fs = std::filesystem; +namespace { +// 在指定目录写出一组最小可解析的规范化 .head/.data(K=4 道 M=2 通道 N=3 采样)。 +void writeMinimalLine(const fs::path& head, const fs::path& data) { + { std::ofstream f(head); + f << "SAMPLES:3\nNUMBER_OF_CH:2\nLAST_TRACE:8\nBITS:16\nENDIAN_TYPE:1\n" + "DISTANCE_INTERVAL:0.1\nTIMEWINDOW:30\nDIELECTRIC:9\n"; } + { std::ofstream f(data, std::ios::binary); + for (int t = 0; t < 4; ++t) for (int c = 0; c < 2; ++c) for (int s = 0; s < 3; ++s) { + std::int16_t v = static_cast(t * 10 + c * 100 + s); + f.write(reinterpret_cast(&v), 2); } } +} +} // namespace + TEST(NormalizedRadarBridge, BuildsVolumeWithExpectedAxes) { // K=4 道, M=2 通道, N=3 采样, 无通道偏移(不插值), coarse=1。 fs::path dir = fs::temp_directory_path() / "radar_bridge_test"; @@ -26,3 +44,55 @@ TEST(NormalizedRadarBridge, BuildsVolumeWithExpectedAxes) { EXPECT_GT(b.spacing[2], 0.0); // dz 由 timewindow/dielectric 求得 >0 EXPECT_NEAR(b.quant.toPhys(b.vol.at(3, 1, 2)), 132.0, b.quant.scale); // t3c1s2=30+100+2 } + +// 回归:中文目录路径必须能打开渲染。app 传入的是 QString::toLocal8Bit(),即当前 +// ANSI 代码页(简中=GBK)窄字节。关键复现条件——GUI app 链接 QWebEngine(Chromium)/VTK, +// 它们在启动时 setlocale(LC_ALL,"") 把 LC_CTYPE 提升为系统 UTF-8 locale;此后窄字符 +// ifstream 会把 GBK 路径字节当 UTF-8 解析 → open 失败(即"打开 .head 失败")。 +// 故本测试显式置 UTF-8 locale 复现该失败面,走宽字符打开(见 io/gpr/LocalPath)守护回归。 +// (纯 "C" locale 下 UCRT 用 CP_ACP=GBK 解窄路径,反而不失败,无法复现——须置 UTF-8。) +TEST(NormalizedRadarBridge, OpensCjkDirectoryPathUnderUtf8Locale) { +#ifdef _WIN32 + const std::wstring wname = L"radar_cjk_南同大道"; + const fs::path dir = fs::temp_directory_path() / wname; + fs::remove_all(dir); + fs::create_directories(dir); + writeMinimalLine(dir / L"南同大道_000.head", dir / L"南同大道_000.data"); + + // 模拟 app:宽字符 → 当前 ANSI 代码页(GBK)窄字节,等价 QString::toLocal8Bit()。 + auto toAcp = [](const std::wstring& w) { + const int n = ::WideCharToMultiByte(CP_ACP, 0, w.data(), + static_cast(w.size()), nullptr, 0, + nullptr, nullptr); + std::string s(static_cast(n), '\0'); + ::WideCharToMultiByte(CP_ACP, 0, w.data(), static_cast(w.size()), + s.data(), n, nullptr, nullptr); + return s; + }; + const std::string dirAcp = toAcp(dir.wstring()); + const std::string prefixAcp = toAcp(L"南同大道_000"); + + // 复现 app 运行期的 UTF-8 C locale(QWebEngine/VTK 所置)——不修复则 narrow open 失败。 + const char* prevC = std::setlocale(LC_ALL, nullptr); + const std::string savedC = prevC ? prevC : "C"; + std::setlocale(LC_ALL, ".UTF-8"); + + geopro::core::BuiltI16 b; + try { + b = geopro::io::gpr::buildLineVolumeFromNormalized( + dirAcp, prefixAcp, /*coarse=*/1, /*targetDy=*/0.0); + } catch (...) { + std::setlocale(LC_ALL, savedC.c_str()); + fs::remove_all(dir); + throw; // 未修复时会在此抛"打开 .head 失败"→ 测试红,正是回归守护 + } + std::setlocale(LC_ALL, savedC.c_str()); + + EXPECT_EQ(b.vol.nx(), 4); + EXPECT_EQ(b.vol.ny(), 2); + EXPECT_EQ(b.vol.nz(), 3); + fs::remove_all(dir); +#else + GTEST_SKIP() << "中文窄路径打开问题仅 Windows 相关"; +#endif +}