Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 41 additions & 41 deletions src/libutil/archive.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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);

expectTag("(");

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;
}
if (tag != "entry")
throw badArchive("expected tag 'entry' or ')', got '%s'", tag);

expectTag("node");
expectTag("(");

parse(sink, source, path / name);
expectTag("name");

expectTag(")");
}
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(")");
}
});
}

else if (type == "symlink") {
Expand Down
76 changes: 70 additions & 6 deletions src/libutil/fs-sink.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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());
}

Expand Down
33 changes: 33 additions & 0 deletions src/libutil/include/nix/util/fs-sink.hh
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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)
Expand All @@ -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;
Expand Down
6 changes: 6 additions & 0 deletions src/libutil/include/nix/util/strings.hh
Original file line number Diff line number Diff line change
Expand Up @@ -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
11 changes: 11 additions & 0 deletions src/libutil/strings.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand Down Expand Up @@ -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
7 changes: 5 additions & 2 deletions src/libutil/url.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion tests/functional/nars.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading