#4999

Fix generic path format when formatting to wide strings

OpenNgoQuocViet2001wants to merge intofrom

On Windows fmt::format(L"{:g}", std::filesystem::path(L"C:\\foo")) returned L"C:\\foo", while fmt::format("{:g}", ...) and fmt::format(L"{:?g}", ...) give C:/foo. get_path_string used the native or generic string computed by the formatter only when converting wchar_t to UTF-8 and otherwise called p.string<Char>(), so g was ignored whenever Char is path::value_type.

This uses the passed string in that case, as write_escaped_path already does. On POSIX it also makes narrow {:g} use generic_string(), which can differ from the native string only where the standard library normalizes separators (libstdc++ collapses repeated ones), matching {:?g}.

  • Ran: std-test with MSVC 2022 in C++17 and C++20 (27 and 29 tests pass, the new assertion fails without the fix) and C++14 (23 pass), plus xchar-test (38 pass) and compile-test (22 pass) in C++17.
  • Checked: clang-format 21 on include/fmt/std.h reports no changes.

Activity

5
skyon0522-ai4d ago

[AI-assisted review] POSIX wide g still uses the native path

There seems to be one remaining cross-character case in this fix. On Linux, a path with repeated separators has different native and generic representations: std::filesystem::path("a//b").generic_string() is a/b.

This standalone example uses the public formatter API (C++17, header-only):

#define FMT_HEADER_ONLY
#include "fmt/std.h"
#include "fmt/xchar.h"
#include <filesystem>

int main() {
  const std::filesystem::path p("a//b");
  fmt::print("std generic: {}\nnarrow g: {}\nwide g == a/b: {}\n"
             "wide g == a//b: {}\nwide ?g == quoted a//b: {}\n",
             p.generic_string(), fmt::format("{:g}", p),
             fmt::format(L"{:g}", p) == L"a/b",
             fmt::format(L"{:g}", p) == L"a//b",
             fmt::format(L"{:?g}", p) == L"\"a//b\"");
}

At PR head adb4f149c0655c3d12e033cff11f5830b5234bf9, compiled with GCC 13.3.0 on Linux:

std generic: a/b
narrow g: a/b
wide g == a/b: false
wide g == a//b: true
wide ?g == quoted a//b: true

At base 0fc31633e7d53ac895ae0a688b83b7d44f0b0c1f, only narrow g differs (a//b); the wide results are the same. Thus the PR fixes the narrow case while the wide case remains.

The formatter selects the generic path string, but for POSIX Char=wchar_t / path::value_type=char, get_path_string falls through to p.string<Char>(), reading the native path again. The escaped-path helper has the analogous cross-character branch.

I infer from the shared g parser and generic/native selection that wide output should preserve the generic representation too. If that is intended, could the cross-character conversion retain that selection, with a POSIX wchar_t test for this case?

Prepared with Codex assistance; a separate Codex reviewer rebuilt the pinned base/head comparison. This is focused Linux/GCC reproduction with ordinary ASCII input. The full fmt suite and Windows/other-OS behavior were not run locally.

NgoQuocViet20012d ago

@skyon0522-ai Confirmed with GCC 13.3 on Linux: for path("a//b"), L"{:g}" gave a//b and L"{:?g}" gave "a//b", while generic_wstring() is a/b.

Fixed in 0fe44d5. The cross-character branches of get_path_string and write_escaped_path now convert the string the formatter already selected (path(native).string<Char>()) instead of calling p.string<Char>(). std_test.path has new a//b checks against generic_string() and generic_wstring(). The two wide ones fail without the change, and std-test passes with it (GCC 13.3, C++17).

vitaut2d ago

@skyon0522-ai please don't post AI slop or you will be banned.