mirror of
https://github.com/NixOS/nix.git
synced 2025-11-22 10:19:36 +01:00
Merge pull request #14597 from NixOS/restore-sink-openat
libutil: Make RestoreSink use *at system calls on UNIX
This commit is contained in:
commit
70b9fbd76c
7 changed files with 165 additions and 48 deletions
|
|
@ -200,54 +200,54 @@ static void parse(FileSystemObjectSink & sink, Source & source, const CanonPath
|
|||
}
|
||||
|
||||
else if (type == "directory") {
|
||||
sink.createDirectory(path);
|
||||
sink.createDirectory(path, [&](FileSystemObjectSink & dirSink, const CanonPath & relDirPath) {
|
||||
std::map<Path, int, CaseInsensitiveCompare> names;
|
||||
|
||||
std::map<Path, int, CaseInsensitiveCompare> names;
|
||||
std::string prevName;
|
||||
|
||||
std::string prevName;
|
||||
while (1) {
|
||||
auto tag = getString();
|
||||
|
||||
while (1) {
|
||||
auto tag = getString();
|
||||
if (tag == ")")
|
||||
break;
|
||||
|
||||
if (tag == ")")
|
||||
break;
|
||||
if (tag != "entry")
|
||||
throw badArchive("expected tag 'entry' or ')', got '%s'", tag);
|
||||
|
||||
if (tag != "entry")
|
||||
throw badArchive("expected tag 'entry' or ')', got '%s'", tag);
|
||||
expectTag("(");
|
||||
|
||||
expectTag("(");
|
||||
expectTag("name");
|
||||
|
||||
expectTag("name");
|
||||
auto name = getString();
|
||||
if (name.empty() || name == "." || name == ".." || name.find('/') != std::string::npos
|
||||
|| name.find((char) 0) != std::string::npos)
|
||||
throw badArchive("NAR contains invalid file name '%1%'", name);
|
||||
if (name <= prevName)
|
||||
throw badArchive("NAR directory is not sorted");
|
||||
prevName = name;
|
||||
if (archiveSettings.useCaseHack) {
|
||||
auto i = names.find(name);
|
||||
if (i != names.end()) {
|
||||
debug("case collision between '%1%' and '%2%'", i->first, name);
|
||||
name += caseHackSuffix;
|
||||
name += std::to_string(++i->second);
|
||||
auto j = names.find(name);
|
||||
if (j != names.end())
|
||||
throw badArchive(
|
||||
"NAR contains file name '%s' that collides with case-hacked file name '%s'",
|
||||
prevName,
|
||||
j->first);
|
||||
} else
|
||||
names[name] = 0;
|
||||
}
|
||||
|
||||
auto name = getString();
|
||||
if (name.empty() || name == "." || name == ".." || name.find('/') != std::string::npos
|
||||
|| name.find((char) 0) != std::string::npos)
|
||||
throw badArchive("NAR contains invalid file name '%1%'", name);
|
||||
if (name <= prevName)
|
||||
throw badArchive("NAR directory is not sorted");
|
||||
prevName = name;
|
||||
if (archiveSettings.useCaseHack) {
|
||||
auto i = names.find(name);
|
||||
if (i != names.end()) {
|
||||
debug("case collision between '%1%' and '%2%'", i->first, name);
|
||||
name += caseHackSuffix;
|
||||
name += std::to_string(++i->second);
|
||||
auto j = names.find(name);
|
||||
if (j != names.end())
|
||||
throw badArchive(
|
||||
"NAR contains file name '%s' that collides with case-hacked file name '%s'",
|
||||
prevName,
|
||||
j->first);
|
||||
} else
|
||||
names[name] = 0;
|
||||
expectTag("node");
|
||||
|
||||
parse(dirSink, source, relDirPath / name);
|
||||
|
||||
expectTag(")");
|
||||
}
|
||||
|
||||
expectTag("node");
|
||||
|
||||
parse(sink, source, path / name);
|
||||
|
||||
expectTag(")");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
else if (type == "symlink") {
|
||||
|
|
|
|||
|
|
@ -34,11 +34,12 @@ void copyRecursive(SourceAccessor & accessor, const CanonPath & from, FileSystem
|
|||
}
|
||||
|
||||
case SourceAccessor::tDirectory: {
|
||||
sink.createDirectory(to);
|
||||
for (auto & [name, _] : accessor.readDirectory(from)) {
|
||||
copyRecursive(accessor, from / name, sink, to / name);
|
||||
break;
|
||||
}
|
||||
sink.createDirectory(to, [&](FileSystemObjectSink & dirSink, const CanonPath & relDirPath) {
|
||||
for (auto & [name, _] : accessor.readDirectory(from)) {
|
||||
copyRecursive(accessor, from / name, dirSink, relDirPath / name);
|
||||
break;
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
@ -70,11 +71,60 @@ static std::filesystem::path append(const std::filesystem::path & src, const Can
|
|||
return dst;
|
||||
}
|
||||
|
||||
#ifndef _WIN32
|
||||
void RestoreSink::createDirectory(const CanonPath & path, DirectoryCreatedCallback callback)
|
||||
{
|
||||
if (path.isRoot()) {
|
||||
createDirectory(path);
|
||||
callback(*this, path);
|
||||
return;
|
||||
}
|
||||
|
||||
createDirectory(path);
|
||||
assert(dirFd); // If that's not true the above call must have thrown an exception.
|
||||
|
||||
RestoreSink dirSink{startFsync};
|
||||
dirSink.dstPath = append(dstPath, path);
|
||||
dirSink.dirFd = ::openat(dirFd.get(), path.rel_c_str(), O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
|
||||
|
||||
if (!dirSink.dirFd)
|
||||
throw SysError("opening directory '%s'", dirSink.dstPath.string());
|
||||
|
||||
callback(dirSink, CanonPath::root);
|
||||
}
|
||||
#endif
|
||||
|
||||
void RestoreSink::createDirectory(const CanonPath & path)
|
||||
{
|
||||
auto p = append(dstPath, path);
|
||||
|
||||
#ifndef _WIN32
|
||||
if (dirFd) {
|
||||
if (path.isRoot())
|
||||
/* Trying to create a directory that we already have a file descriptor for. */
|
||||
throw Error("path '%s' already exists", p.string());
|
||||
|
||||
if (::mkdirat(dirFd.get(), path.rel_c_str(), 0777) == -1)
|
||||
throw SysError("creating directory '%s'", p.string());
|
||||
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (!std::filesystem::create_directory(p))
|
||||
throw Error("path '%s' already exists", p.string());
|
||||
|
||||
#ifndef _WIN32
|
||||
if (path.isRoot()) {
|
||||
assert(!dirFd); // Handled above
|
||||
|
||||
/* Open directory for further *at operations relative to the sink root
|
||||
directory. */
|
||||
dirFd = open(p.c_str(), O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
|
||||
if (!dirFd)
|
||||
throw SysError("creating directory '%1%'", p.string());
|
||||
}
|
||||
#endif
|
||||
};
|
||||
|
||||
struct RestoreRegularFile : CreateRegularFileSink
|
||||
|
|
@ -114,7 +164,14 @@ void RestoreSink::createRegularFile(const CanonPath & path, std::function<void(C
|
|||
FILE_ATTRIBUTE_NORMAL,
|
||||
NULL)
|
||||
#else
|
||||
open(p.c_str(), O_CREAT | O_EXCL | O_WRONLY | O_CLOEXEC, 0666)
|
||||
[&]() {
|
||||
/* O_EXCL together with O_CREAT ensures symbolic links in the last
|
||||
component are not followed. */
|
||||
constexpr int flags = O_CREAT | O_EXCL | O_WRONLY | O_CLOEXEC;
|
||||
if (!dirFd)
|
||||
return ::open(p.c_str(), flags, 0666);
|
||||
return ::openat(dirFd.get(), path.rel_c_str(), flags, 0666);
|
||||
}();
|
||||
#endif
|
||||
;
|
||||
if (!crf.fd)
|
||||
|
|
@ -161,6 +218,13 @@ void RestoreRegularFile::operator()(std::string_view data)
|
|||
void RestoreSink::createSymlink(const CanonPath & path, const std::string & target)
|
||||
{
|
||||
auto p = append(dstPath, path);
|
||||
#ifndef _WIN32
|
||||
if (dirFd) {
|
||||
if (::symlinkat(requireCString(target), dirFd.get(), path.rel_c_str()) == -1)
|
||||
throw SysError("creating symlink from '%1%' -> '%2%'", p.string(), target);
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
nix::createSymlink(target, p.string());
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -36,6 +36,23 @@ struct FileSystemObjectSink
|
|||
|
||||
virtual void createDirectory(const CanonPath & path) = 0;
|
||||
|
||||
using DirectoryCreatedCallback = std::function<void(FileSystemObjectSink & dirSink, const CanonPath & dirRelPath)>;
|
||||
|
||||
/**
|
||||
* Create a directory and invoke a callback with a pair of sink + CanonPath
|
||||
* of the created subdirectory relative to dirSink.
|
||||
*
|
||||
* @note This allows for UNIX RestoreSink implementations to implement
|
||||
* *at-style accessors that always keep an open file descriptor for the
|
||||
* freshly created directory. Use this when it's important to disallow any
|
||||
* intermediate path components from being symlinks.
|
||||
*/
|
||||
virtual void createDirectory(const CanonPath & path, DirectoryCreatedCallback callback)
|
||||
{
|
||||
createDirectory(path);
|
||||
callback(*this, path);
|
||||
}
|
||||
|
||||
/**
|
||||
* This function in general is no re-entrant. Only one file can be
|
||||
* written at a time.
|
||||
|
|
@ -82,6 +99,18 @@ struct NullFileSystemObjectSink : FileSystemObjectSink
|
|||
struct RestoreSink : FileSystemObjectSink
|
||||
{
|
||||
std::filesystem::path dstPath;
|
||||
#ifndef _WIN32
|
||||
/**
|
||||
* File descriptor for the directory located at dstPath. Used for *at
|
||||
* operations relative to this file descriptor. This sink must *never*
|
||||
* follow intermediate symlinks (starting from dstPath) in case a file
|
||||
* collision is encountered for various reasons like case-insensitivity or
|
||||
* other types on normalization. using appropriate *at system calls and traversing
|
||||
* only one path component at a time ensures that writing is race-free and is
|
||||
* is not susceptible to symlink replacement.
|
||||
*/
|
||||
AutoCloseFD dirFd;
|
||||
#endif
|
||||
bool startFsync = false;
|
||||
|
||||
explicit RestoreSink(bool startFsync)
|
||||
|
|
@ -91,6 +120,10 @@ struct RestoreSink : FileSystemObjectSink
|
|||
|
||||
void createDirectory(const CanonPath & path) override;
|
||||
|
||||
#ifndef _WIN32
|
||||
void createDirectory(const CanonPath & path, DirectoryCreatedCallback callback) override;
|
||||
#endif
|
||||
|
||||
void createRegularFile(const CanonPath & path, std::function<void(CreateRegularFileSink &)>) override;
|
||||
|
||||
void createSymlink(const CanonPath & path, const std::string & target) override;
|
||||
|
|
|
|||
|
|
@ -166,4 +166,10 @@ public:
|
|||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Check that the string does not contain any NUL bytes and return c_str().
|
||||
* @throws Error if str contains '\0' bytes.
|
||||
*/
|
||||
const char * requireCString(const std::string & str);
|
||||
|
||||
} // namespace nix
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
#include "nix/util/strings-inline.hh"
|
||||
#include "nix/util/os-string.hh"
|
||||
#include "nix/util/error.hh"
|
||||
#include "nix/util/util.hh"
|
||||
|
||||
namespace nix {
|
||||
|
||||
|
|
@ -152,4 +153,14 @@ std::string optionalBracket(std::string_view prefix, std::string_view content, s
|
|||
return result;
|
||||
}
|
||||
|
||||
const char * requireCString(const std::string & s)
|
||||
{
|
||||
if (std::memchr(s.data(), '\0', s.size())) [[unlikely]] {
|
||||
using namespace std::string_view_literals;
|
||||
auto str = replaceStrings(s, "\0"sv, "␀"sv);
|
||||
throw Error("string '%s' with null (\\0) bytes used where it's not allowed", str);
|
||||
}
|
||||
return s.c_str();
|
||||
}
|
||||
|
||||
} // namespace nix
|
||||
|
|
|
|||
|
|
@ -327,8 +327,11 @@ Path renderUrlPathEnsureLegal(const std::vector<std::string> & urlPath)
|
|||
/* This is only really valid for UNIX. Windows has more restrictions. */
|
||||
if (comp.contains('/'))
|
||||
throw BadURL("URL path component '%s' contains '/', which is not allowed in file names", comp);
|
||||
if (comp.contains(char(0)))
|
||||
throw BadURL("URL path component '%s' contains NUL byte which is not allowed", comp);
|
||||
if (comp.contains(char(0))) {
|
||||
using namespace std::string_view_literals;
|
||||
auto str = replaceStrings(comp, "\0"sv, "␀"sv);
|
||||
throw BadURL("URL path component '%s' contains NUL byte which is not allowed", str);
|
||||
}
|
||||
}
|
||||
|
||||
return concatStringsSep("/", urlPath);
|
||||
|
|
|
|||
|
|
@ -114,7 +114,7 @@ if (( unicodeTestCode == 1 )); then
|
|||
# If the command failed (MacOS or ZFS + normalization), checks that it failed
|
||||
# with the expected "already exists" error, and that this is the same
|
||||
# behavior as `touch`
|
||||
echo "$unicodeTestOut" | grepQuiet "path '.*/out/â' already exists"
|
||||
echo "$unicodeTestOut" | grepQuiet "creating directory '.*/out/â': File exists"
|
||||
|
||||
(( touchFilesCount == 1 ))
|
||||
elif (( unicodeTestCode == 0 )); then
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue