std::filesystem Implementation Corner Cases
[Update 2020-02-08: A post on stackoverflow.com showed an additional corner case, read more about it in part 2.]
In summer 2018 I started on changing my private C++ filesystem abstraction into
a more std::filesystem
conforming structure. Main motivation was the low chance of
an official implementation on my primary platform, macOS, anytime soon.
The first feature-complete public release supporting C++11, C++14 and C++17
on macOS, Windows and Linux was in September 2018.
Early on, I started writing lots of tests and wrote my test-suite in a way, that I could run
my tests against an existing std::filesystem
implementation, where one is available.
The test code is based on Catch2 macros.
Currently, using the valuable services of Travis-CI and Appveyor, ghc::filesystem
is
tested on GCC 5 to GCC 9, Clang 5 to Clang 9, Xcode 9/10/11 and Visual Studio 2015/2017/2019.
The tests are also run against std::filesystem
in GCC 8.3 and GCC 9.3 with libstdc++
and
Clang 7.1/8.0/9.0 with libc++
and MSVC 19.16 and MSVC 19.23.
Important note:
With this post I want to share some corner cases I found, where my tests
showed different behaviour for the tested std::filesystem
implementation in C++17 mode.
All tests pass on ghc::filesystem
because they are from its test suite.
I have by no means the authority nor knowledge of all mails, forums, chats and other channels outside of the standard that have lead the developers to their implementation, and I might have missed an LWG-issue that is relevant here or simply missed something in the standard, so I’m not saying mine is the way to do things, and I already changed some behaviour to follow other implementations, as I’m interested in a common sense of what should happen. So I don’t want to blame anyone for anything, but some results are still irritating for me, so I keep my implementation the way it is, until I have better arguments or understanding to do otherwise.
Any opinions on any of the following “issues” is very welcome! Also new/missing corner cases are welcome to get a more complete picture!
You can reach me via twitter by @gulrak or drop me a ✉ mail.
List of Implementation Corner Cases
path native format observers [fs.path.native.obs]
GCC 8.1, 8.2, 8.3:
FAILED:
CHECK( fs::u8path("\xc3\xa4/\xe2\x82\xac\xf0\x9d\x84\x9e").u16string() == std::u16string(u"\u00E4/\u20AC\U0001D11E") )
due to unexpected exception with messages:
filesystem error: Cannot convert character sequence: Illegal byte sequence
I had observed this with GCC 8.1/8.2 on Ubuntu 18.04 and macOS, and GCC 8.3.0 on Wandbox. The compiler doesn’t recognize the valid Unicode codepoint U+1D11E. This was fixed with GCC issue 90281).
GCC 9.2:
FAILED:
CHECK( fs::u8path("\xc3\xa4/\xe2\x82\xac").wstring() == std::wstring(L"ä/€") )
due to unexpected exception with message:
filesystem error: Cannot convert character sequence: Invalid or incomplete
multibyte or wide character
This is observed on an Homebrew install of GCC-9 on macOS and on
Travis-CI Linux GCC 9.2, and with this Godbolt example.
The std::filesystem::wstring()
is the problem here, and std::filesystem::u32string()
works as expected and the returned std::u32string
is the expected 3 char32_t
long.
path generic format observers [fs.path.generic.obs]
GCC 9.2:
FAILED:
CHECK( fs::u8path("\xc3\xa4/\xe2\x82\xac").generic_wstring() == std::wstring(L"ä/€") )
due to unexpected exception with message:
filesystem error: Cannot convert character sequence: Invalid or incomplete
multibyte or wide character
This is observed on an Homebrew install of GCC-9 on macOS and on
Travis-CI Linux GCC 9.2, and with this Godbolt example.
The std::filesystem::generic_wstring()
is the problem here, and std::filesystem::generic_u32string()
works as expected and the returned std::u32string
is the expected 3 char32_t
long.
path iterators [fs.path.itr]
MSVC 19.16, 19.23:
FAILED:
CHECK( "/,foo,bar" == iterateResult(fs::path("///foo/bar")) )
with expansion:
"/,foo,bar" == "///,foo,bar"
The used helper iterateResult
is this:
static std::string iterateResult(const fs::path& path)
{
std::ostringstream result;
for (fs::path::const_iterator i = path.begin(); i != path.end(); ++i) {
if (i != path.begin()) {
result << ",";
}
result << i->generic_string();
}
return result.str();
}
GCC, Clang and my implementation all return /
for the ///
part, only
MSVC decides to return ///
. As Windows is supporting /
as seperator too,
even if it’s not the preferred seperator, I’m not sure about this and I didn’t mimic
this behaviour on Windows.
path inserter and extractor [fs.path.io]
MSVC 19.16, 19.23:
With an std::ostringstream
filled by:
std::ostringstream os;
os << fs::path("/root/foo bar");
we get this failure:
FAILED:
CHECK( os.str() == "\"\\\\root\\\\foo bar\"" )
with expansion:
""/root/foo bar"" == ""\\root\\foo bar""
The standard describes the effect of the path inserter as: “Equivalent to: os « quoted(p.string<charT, traits>());” and fs::path::string()
is one of the native format observers that MSVC returns a backslashed path for. Still the stream inserter outputs the generic format (that Windows API will accept) and this seems inconsistent.
directory entry [fs.dir.entry]
fs::directory_entry none;
none.refresh();
This code currently throws an exception on MSVC 19.16, 19.23 and ghc::filesystem
while not
throwing on all versions of GCC/Clang.
fs::directory_entry de;
de.assign("");
This code currently only throws on MSVC 19.16 and ghc::filesystem
.
fs::directory_entry de{"existing_dir/existing_file"};
de.replace_filename("bar");
This code currently only throws on MSVC 19.16 and ghc::filesystem
.
MSVC 19.23:
auto none = fs::directory_entry();
CHECK( none.status().type() == fs::file_type::not_found );
FAILED:
CHECK( none.status().type() == fs::file_type::not_found )
with expansion:
2 == 1
This is surprizing. I have no idea why this happens, als 2 would be
fs::file_type::regular
for a directory_entry
with an empty path.
absolute [fs.op.absolute]
GCC 8.3, 9.2:
FAILED:
CHECK( fs::absolute("") == fs::current_path() / "" )
due to unexpected exception with message:
filesystem error: cannot make absolute path: Invalid argument
GCC is the only one throwing in this case, or sets the error_core
in case of
fs::absolute(error_code&)
, besides the standard note: “Implementations are
strongly encouraged to not query secondary storage, and not consider !exists(p)
an error.” and the example implementation from the standard:
“For POSIX-based operating systems, absolute(p) is simply current_path()/p.”.
MSVC 19.16, 19.23:
FAILED:
CHECK( fs::absolute("") == fs::current_path() / "" )
with expansion:
"" == "C:\projects\filesystem\build\"
MSVC is returning an empty result.
Others
Clang and my implementation return the suggested result of current_path()/p
.
canonical [fs.op.canonical]
Clang 7, 8, 9, MSVC 19.16, 19.23:
FAILED:
CHECK_THROWS_AS( fs::canonical(""), fs::filesystem_error )
because no exception was thrown where one was expected:
This seems to be a violation of the standard, as 30.10.15.2 (4) specifies
"!exists(p)
is an error." and the compilers report false
for exists("")
.
My guess is, Clang first calls absolute(p)
and after that, p
is not empty
and exists(absolute(""))
is normally true
as it is the current directory path.
The GCC implementations treat this as an error, as well as my own.
create_directory, create_directories
[fs.op.create_directory], [fs.op.create_directories]
Given a p
with:
auto p = fs::path{"some_existing_regular_file"};
We get:
GCC 7, MSVC 19.16:
FAILED:
CHECK_THROWS_AS( fs::create_directories(p), fs::filesystem_error )
because no exception was thrown where one was expected
Clang 7, 8, 9 and MSVC 19.16:
FAILED:
CHECK_THROWS_AS( fs::create_directory(p), fs::filesystem_error )
because no exception was thrown where one was expected
This was not well defined with the original standard and the LWG#2935 suggested this behaviour. The more intuitive approach to directory creation of treating a file with that name as an error is also advocated by the newer paper WG21 P1164R0, the revison P1161R1 was agreed upon on Kona 2019 meeting see merge and MSVC and GCC by now switched to following its proposal (GCC #86910).
equivalent [fs.op.equivalent]
GCC 8.x, 9.2:
CHECK_THROWS_AS( fs::equivalent("foo", "foo3"), fs::filesystem_error )
because no exception was thrown where one was expected:
This test expects LWG #2937 result conformance.
CHECK( ec )
with expansion:
system:0
with message:
This test expects LWG #2937 result conformance.
CHECK_THROWS_AS( fs::equivalent("foo3", "foo"), fs::filesystem_error )
because no exception was thrown where one was expected:
This test expects LWG #2937 result conformance.
CHECK( ec )
with expansion:
system:0
with message:
This test expects LWG #2937 result conformance.
With LWG #2937 was specified, that it is an error if either of the given path objects doesn’t resolve to an existing file and Clang and MSVC follow #2937. My implementation follows this rule too (but there is a define at the top, where this can be disabled).
weakly_canonical [fs.op.weakly_canonical]
Clang 7, 8, 9 and GCC 8.3, 9.2:
FAILED:
CHECK( fs::weakly_canonical("") == "." )
with expansion:
"/home/travis/build/gulrak/filesystem/build"
==
"."
Clang and GCC are returning fs::current_path()
for fs::canonical("")
so it is to be expected
that they return the same for fs::weakly_canonical("")
. MSVC and my implementation are
returning .
as the normalized empty path ([fs.def.normal.form], 8).
Raw Test Issue Table
I wrote a script generating an html table with the issues ordered by category and compiler and I try to reupload a new one, if something relevant changes. I’ll link to it only for completeness:
Thanks!
I would like to thank Jonathan Wakely, whom I had contact with through my last GCCs issues, for taking me serious and for pointing me to the right directions a few times. He was always helpful and quick in fixing actual issues I found and reported.
I also want to thank Nico Josuttis for his
WG21 P1164R0
to clear up the create_directory
/create_directories
issue, his efforts to
make the standard clearer for normal developers and his
C++17 - The Complete Guide.
And thanks to all those I talked to on Meeting C++ but can not list here, I hope to meet many of you next year.