libutil: catch std::filesystem errors

catch and rewrap std::fs errors in some meaningful way, otherwise lix
can crash with foreign exception aborts if filesystem failures happen
that aren't already being caught. renames in particular can fail with
incomprehensible errors in even pretty normal circumstances elsewise.

fixes #1219, fixes #995

Change-Id: I0b1e5696d300aac3422e43cffc17a2426803d64e
This commit is contained in:
eldritch horrors
2026-06-05 17:17:18 +00:00
parent c13ca9adcf
commit e108432cb7
2 changed files with 67 additions and 38 deletions
+46 -25
View File
@@ -9,6 +9,7 @@
#include "lix/libutil/logging.hh"
#include <filesystem>
#include <glob.h>
namespace nix {
@@ -395,8 +396,19 @@ static bool isAcceptableLixSubcommandExe(const std::filesystem::path & exe_path)
{
namespace fs = std::filesystem;
return fs::is_regular_file(exe_path) &&
(fs::status(exe_path).permissions() & (fs::perms::owner_exec | fs::perms::group_exec | fs::perms::others_exec)) != fs::perms::none;
try {
return fs::is_regular_file(exe_path)
&& (fs::status(exe_path).permissions()
& (fs::perms::owner_exec | fs::perms::group_exec | fs::perms::others_exec))
!= fs::perms::none;
} catch (fs::filesystem_error & fs_exc) { // NOLINT(lix-foreign-exceptions)
if (fs_exc.code() != std::errc::no_such_file_or_directory
&& fs_exc.code() != std::errc::not_a_directory && fs_exc.code() != std::errc::permission_denied)
{
throw Error("%s", Uncolored(fs_exc.what()));
}
return false;
}
}
std::optional<ref<Command>> searchForCustomSubcommand(AsyncIoRoot & aio, const std::string_view & command, const std::string_view & prefix, const Strings & searchPaths)
@@ -412,10 +424,10 @@ std::optional<ref<Command>> searchForCustomSubcommand(AsyncIoRoot & aio, const s
debug("Found requested external subcommand '%s' in '%s'", command, path);
return make_ref<ExternalCommand>(aio, path);
}
} catch (fs::filesystem_error & fs_exc) { // NOLINT(lix-foreign-exceptions)
if (fs_exc.code() != std::errc::no_such_file_or_directory && fs_exc.code() != std::errc::not_a_directory && fs_exc.code() != std::errc::permission_denied) {
throw SysError("while searching for the subcommand '%1%' in search path '%2%': '%3%'", command, searchPath, fs_exc.what());
}
} catch (Error & exc) {
exc.addTrace(
{}, "while searching for the subcommand '%1%' in search path '%2%'", command, searchPath
);
}
}
@@ -430,33 +442,42 @@ Strings searchForAllAvailableCustomSubcommands(const std::string_view & prefix,
for (auto searchPath : searchPaths) {
if (searchPath.empty()) continue;
if (!fs::exists(searchPath) || !fs::is_directory(searchPath)) {
// TODO(Raito): this will break all the time our functional tests
// for people with garbage in their $PATH which is my personal case.
try {
if (!fs::exists(searchPath) || !fs::is_directory(searchPath)) {
// TODO(Raito): this will break all the time our functional tests
// for people with garbage in their $PATH which is my personal case.
// warn("The search path '%s' for custom subcommands does not exist or is not a directory, ignoring...", searchPath);
continue;
}
// warn("The search path '%s' for custom subcommands does not exist or is not a directory,
// ignoring...", searchPath);
continue;
}
// Browse per prefix.
for (const auto& entry : fs::directory_iterator(searchPath)) {
try {
if (isAcceptableLixSubcommandExe(entry.path())) {
auto filename = entry.path().filename().string();
// Browse per prefix.
for (const auto & entry : fs::directory_iterator(searchPath)) {
try {
if (isAcceptableLixSubcommandExe(entry.path())) {
auto filename = entry.path().filename().string();
if (filename.starts_with(prefix)) {
auto suffix = filename.substr(prefix.size());
if (filename.starts_with(prefix)) {
auto suffix = filename.substr(prefix.size());
debug("Found custom subcommand ('%s') '%s'", filename, suffix);
debug("Found custom subcommand ('%s') '%s'", filename, suffix);
commandNames.push_back(suffix);
commandNames.push_back(suffix);
}
}
}
} catch (fs::filesystem_error & fs_exc) { // NOLINT(lix-foreign-exceptions)
if (fs_exc.code() != std::errc::no_such_file_or_directory && fs_exc.code() != std::errc::not_a_directory && fs_exc.code() != std::errc::permission_denied) {
throw SysError("while searching for all available commands in search path '%1%', while analyzing '%2%': %3%'", searchPath, entry.path(), fs_exc.what());
} catch (Error & exc) {
exc.addTrace(
{},
"while searching for all available commands in search path '%1%', while analyzing "
"'%2%'",
searchPath,
entry.path()
);
}
}
} catch (fs::filesystem_error & exc) { // NOLINT(lix-foreign-exceptions)
throw Error("failed to look up available subcommands in %s: %s", searchPath, exc.what());
}
}
+21 -13
View File
@@ -767,7 +767,7 @@ void setWriteTime(const fs::path & p, const struct stat & st)
}
void copy(const fs::directory_entry & from, const fs::path & to, CopyFileFlags flags)
{
try {
// TODO: Rewrite the `is_*` to use `symlink_status()`
auto statOfFrom = lstat(from.path());
auto fromStatus = from.symlink_status();
@@ -801,9 +801,10 @@ void copy(const fs::directory_entry & from, const fs::path & to, CopyFileFlags f
fs::permissions(from.path(), fs::perms::owner_write, fs::perm_options::add | fs::perm_options::nofollow);
fs::remove(from.path());
}
} catch (fs::filesystem_error & e) { // NOLINT(lix-foreign-exceptions)
throw Error("failed to copy %s to %s: %s", from.path(), to, e.what());
}
void copyFile(const Path & oldPath, const Path & newPath, CopyFileFlags flags)
{
return copy(fs::directory_entry(fs::path(oldPath)), fs::path(newPath), flags);
@@ -811,28 +812,35 @@ void copyFile(const Path & oldPath, const Path & newPath, CopyFileFlags flags)
void renameFile(const Path & oldName, const Path & newName)
{
fs::rename(oldName, newName);
try {
fs::rename(oldName, newName);
} catch (fs::filesystem_error & e) { // NOLINT(lix-foreign-exceptions)
throw Error("failed to rename %s to %s: %s", oldName, newName, e.what());
}
}
void moveFile(const Path & oldName, const Path & newName)
{
try {
renameFile(oldName, newName);
fs::rename(oldName, newName);
} catch (fs::filesystem_error & e) { // NOLINT(lix-foreign-exceptions)
auto oldPath = fs::path(oldName);
auto newPath = fs::path(newName);
// For the move to be as atomic as possible, copy to a temporary
// directory
fs::path temp = createTempSubdir(newPath.parent_path(), "rename-tmp");
Finally removeTemp = [&]() { fs::remove(temp); };
auto tempCopyTarget = temp / "copy-target";
if (e.code().value() == EXDEV) {
fs::remove(newPath);
printTaggedWarning("Cant rename %s as %s, copying instead", oldName, newName);
copy(fs::directory_entry(oldPath), tempCopyTarget, { .deleteAfter = true });
renameFile(tempCopyTarget, newPath);
try {
fs::path temp = createTempSubdir(newPath.parent_path(), "rename-tmp");
Finally removeTemp = [&]() { fs::remove(temp); };
auto tempCopyTarget = temp / "copy-target";
if (e.code().value() == EXDEV) {
fs::remove(newPath);
printTaggedWarning("Cant rename %s as %s, copying instead", oldName, newName);
copy(fs::directory_entry(oldPath), tempCopyTarget, {.deleteAfter = true});
fs::rename(tempCopyTarget, newPath);
}
} catch (fs::filesystem_error & e) { // NOLINT(lix-foreign-exceptions)
throw Error("failed to move %s to %s: %s", oldName, newName, e.what());
}
}
}
}