Skip to content

Commit a655630

Browse files
committed
fix(Xml): XmlReader::Create could not open a file under a non-ASCII path on Windows
Found by running CNA's content suite with TEMP pointed at a directory whose name is not representable in the process code page: every XML source under it failed with XML_ERROR_FILE_NOT_FOUND while the file was plainly there. tinyxml2 opens with fopen/fopen_s (vendor/tinyxml2/tinyxml2.cpp:2316-2327), which is the ANSI code page on Windows and has no wide branch. This is the same shape as stb_image and cgltf, and it gets the same treatment: do not hand the library a filename. tinyxml2's FILE* overload of LoadFile takes the file already open, so the OPEN happens through a wide path while every one of tinyxml2's own error codes and its BOM and encoding handling stay exactly as they were. On a failure to widen or open, it falls through to the original call so tinyxml2 still reports its own XML_ERROR_FILE_NOT_FOUND rather than leaving a silently empty document. The narrow std::string this API takes means UTF-8, as it does throughout. Linux: 562/562 Xml tests pass. The added code is inside `#if defined(_WIN32)`.
1 parent 491b937 commit a655630

1 file changed

Lines changed: 40 additions & 1 deletion

File tree

modules/xml/src/System/Xml/XmlReader.cpp

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,16 @@
22
// Copyright (c) Robert Vokac and contributors
33
// Portions based on .NET runtime API (MIT License, Copyright .NET Foundation and Contributors)
44
#include "System/Xml/XmlReader.hpp"
5+
6+
#include <cstdio>
7+
8+
#if defined(_WIN32)
9+
// Contained in this translation unit: <windows.h> in a header is what produced the ERROR and
10+
// min/max macro collisions recorded in CNA as WINNATIVE-F5 and F11.
11+
# define WIN32_LEAN_AND_MEAN
12+
# define NOMINMAX
13+
# include <windows.h>
14+
#endif
515
#include <algorithm>
616
#include "System/ArgumentOutOfRangeException.hpp"
717
#include "System/Convert.hpp"
@@ -572,6 +582,35 @@ static void applySettings(XmlReaderState& st, const XmlReaderSettings& settings)
572582
st.events = std::move(kept);
573583
}
574584

585+
namespace {
586+
// tinyxml2 opens with fopen/fopen_s, which on Windows is the process ANSI code page -- so an
587+
// .xml under a directory that code page cannot spell simply reported
588+
// XML_ERROR_FILE_NOT_FOUND. Its FILE* overload takes the file already open, which keeps every
589+
// one of tinyxml2's own error codes and BOM/encoding rules intact while letting the OPEN
590+
// happen through a wide path.
591+
void loadXmlFile(tinyxml2::XMLDocument& doc, const std::string& utf8Path) {
592+
#if defined(_WIN32)
593+
const int needed = ::MultiByteToWideChar(CP_UTF8, 0, utf8Path.c_str(),
594+
static_cast<int>(utf8Path.size()), nullptr, 0);
595+
if (needed > 0) {
596+
std::wstring wide(static_cast<std::size_t>(needed), L'\0');
597+
if (::MultiByteToWideChar(CP_UTF8, 0, utf8Path.c_str(),
598+
static_cast<int>(utf8Path.size()), wide.data(), needed) > 0) {
599+
FILE* file = nullptr;
600+
if (::_wfopen_s(&file, wide.c_str(), L"rb") == 0 && file != nullptr) {
601+
doc.LoadFile(file);
602+
std::fclose(file);
603+
return;
604+
}
605+
}
606+
}
607+
// Fall through so tinyxml2 reports its own XML_ERROR_FILE_NOT_FOUND rather than a
608+
// silently empty document.
609+
#endif
610+
doc.LoadFile(utf8Path.c_str());
611+
}
612+
}
613+
575614
XmlReader* XmlReader::Create(const std::string& inputUri) {
576615
auto st = std::make_unique<XmlReaderState>();
577616
// Heuristic: treat as file path if it ends with .xml or contains a path separator --
@@ -590,7 +629,7 @@ XmlReader* XmlReader::Create(const std::string& inputUri) {
590629
(inputUri.size() >= 4 &&
591630
inputUri.substr(inputUri.size() - 4) == ".xml"));
592631
if (isFile)
593-
st->doc.LoadFile(inputUri.c_str());
632+
loadXmlFile(st->doc, inputUri);
594633
else
595634
st->doc.Parse(inputUri.c_str());
596635
return createFromDoc(std::move(st));

0 commit comments

Comments
 (0)