diff options
Diffstat (limited to 'third_party/nix/src/libutil/archive.cc')
-rw-r--r-- | third_party/nix/src/libutil/archive.cc | 398 |
1 files changed, 398 insertions, 0 deletions
diff --git a/third_party/nix/src/libutil/archive.cc b/third_party/nix/src/libutil/archive.cc new file mode 100644 index 000000000000..e470ad7be6ce --- /dev/null +++ b/third_party/nix/src/libutil/archive.cc @@ -0,0 +1,398 @@ +#include "libutil/archive.hh" + +#include <algorithm> +#include <cerrno> +#include <map> +#include <vector> + +#include <dirent.h> +#include <fcntl.h> +#include <glog/logging.h> +#include <strings.h> // for strcasecmp +#include <sys/stat.h> +#include <sys/types.h> +#include <unistd.h> + +#include "libutil/config.hh" +#include "libutil/util.hh" + +namespace nix { + +struct ArchiveSettings : Config { + Setting<bool> useCaseHack { + this, +#if __APPLE__ + true, +#else + false, +#endif + "use-case-hack", + "Whether to enable a Darwin-specific hack for dealing with file name " + "collisions." + }; +}; + +static ArchiveSettings archiveSettings; + +static GlobalConfig::Register r1(&archiveSettings); + +constexpr std::string_view kCaseHackSuffix = "~nix~case~hack~"; + +PathFilter defaultPathFilter = [](const Path& /*unused*/) { return true; }; + +static void dumpContents(const Path& path, size_t size, Sink& sink) { + sink << "contents" << size; + + AutoCloseFD fd(open(path.c_str(), O_RDONLY | O_CLOEXEC)); + if (!fd) { + throw SysError(format("opening file '%1%'") % path); + } + + std::vector<unsigned char> buf(65536); + size_t left = size; + + while (left > 0) { + auto n = std::min(left, buf.size()); + readFull(fd.get(), buf.data(), n); + left -= n; + sink(buf.data(), n); + } + + writePadding(size, sink); +} + +static void dump(const Path& path, Sink& sink, PathFilter& filter) { + checkInterrupt(); + + struct stat st; + if (lstat(path.c_str(), &st) != 0) { + throw SysError(format("getting attributes of path '%1%'") % path); + } + + sink << "("; + + if (S_ISREG(st.st_mode)) { + sink << "type" + << "regular"; + if ((st.st_mode & S_IXUSR) != 0u) { + sink << "executable" + << ""; + } + dumpContents(path, static_cast<size_t>(st.st_size), sink); + } + + else if (S_ISDIR(st.st_mode)) { + sink << "type" + << "directory"; + + /* If we're on a case-insensitive system like macOS, undo + the case hack applied by restorePath(). */ + std::map<std::string, std::string> unhacked; + for (auto& i : readDirectory(path)) { + if (archiveSettings.useCaseHack) { + std::string name(i.name); + size_t pos = i.name.find(kCaseHackSuffix); + if (pos != std::string::npos) { + DLOG(INFO) << "removing case hack suffix from " << path << "/" + << i.name; + + name.erase(pos); + } + if (unhacked.find(name) != unhacked.end()) { + throw Error(format("file name collision in between '%1%' and '%2%'") % + (path + "/" + unhacked[name]) % (path + "/" + i.name)); + } + unhacked[name] = i.name; + } else { + unhacked[i.name] = i.name; + } + } + + for (auto& i : unhacked) { + if (filter(path + "/" + i.first)) { + sink << "entry" + << "(" + << "name" << i.first << "node"; + dump(path + "/" + i.second, sink, filter); + sink << ")"; + } + } + } + + else if (S_ISLNK(st.st_mode)) { + sink << "type" + << "symlink" + << "target" << readLink(path); + + } else { + throw Error(format("file '%1%' has an unsupported type") % path); + } + + sink << ")"; +} + +void dumpPath(const Path& path, Sink& sink, PathFilter& filter) { + sink << std::string(kNarVersionMagic1); + dump(path, sink, filter); +} + +void dumpString(const std::string& s, Sink& sink) { + sink << std::string(kNarVersionMagic1) << "(" + << "type" + << "regular" + << "contents" << s << ")"; +} + +static SerialisationError badArchive(const std::string& s) { + return SerialisationError("bad archive: " + s); +} + +#if 0 +static void skipGeneric(Source & source) +{ + if (readString(source) == "(") { + while (readString(source) != ")") + skipGeneric(source); + } +} +#endif + +static void parseContents(ParseSink& sink, Source& source, const Path& path) { + unsigned long long size = readLongLong(source); + + sink.preallocateContents(size); + + unsigned long long left = size; + std::vector<unsigned char> buf(65536); + + while (left != 0u) { + checkInterrupt(); + auto n = buf.size(); + if (static_cast<unsigned long long>(n) > left) { + n = left; + } + source(buf.data(), n); + sink.receiveContents(buf.data(), n); + left -= n; + } + + readPadding(size, source); +} + +struct CaseInsensitiveCompare { + bool operator()(const std::string& a, const std::string& b) const { + return strcasecmp(a.c_str(), b.c_str()) < 0; + } +}; + +static void parse(ParseSink& sink, Source& source, const Path& path) { + std::string s; + + s = readString(source); + if (s != "(") { + throw badArchive("expected open tag"); + } + + enum { tpUnknown, tpRegular, tpDirectory, tpSymlink } type = tpUnknown; + + std::map<Path, int, CaseInsensitiveCompare> names; + + while (true) { + checkInterrupt(); + + s = readString(source); + + if (s == ")") { + break; + } + + if (s == "type") { + if (type != tpUnknown) { + throw badArchive("multiple type fields"); + } + std::string t = readString(source); + + if (t == "regular") { + type = tpRegular; + sink.createRegularFile(path); + } + + else if (t == "directory") { + sink.createDirectory(path); + type = tpDirectory; + } + + else if (t == "symlink") { + type = tpSymlink; + } + + else { + throw badArchive("unknown file type " + t); + } + + } + + else if (s == "contents" && type == tpRegular) { + parseContents(sink, source, path); + } + + else if (s == "executable" && type == tpRegular) { + auto s = readString(source); + if (!s.empty()) { + throw badArchive("executable marker has non-empty value"); + } + sink.isExecutable(); + } + + else if (s == "entry" && type == tpDirectory) { + std::string name; + std::string prevName; + + s = readString(source); + if (s != "(") { + throw badArchive("expected open tag"); + } + + while (true) { + checkInterrupt(); + + s = readString(source); + + if (s == ")") { + break; + } + if (s == "name") { + name = readString(source); + if (name.empty() || name == "." || name == ".." || + name.find('/') != std::string::npos || + name.find(static_cast<char>(0)) != std::string::npos) { + throw Error(format("NAR contains invalid file name '%1%'") % name); + } + if (name <= prevName) { + throw Error("NAR directory is not sorted"); + } + prevName = name; + if (archiveSettings.useCaseHack) { + auto i = names.find(name); + if (i != names.end()) { + DLOG(INFO) << "case collision between '" << i->first << "' and '" + << name << "'"; + name += kCaseHackSuffix; + name += std::to_string(++i->second); + } else { + names[name] = 0; + } + } + } else if (s == "node") { + if (s.empty()) { + throw badArchive("entry name missing"); + } + parse(sink, source, path + "/" + name); + } else { + throw badArchive("unknown field " + s); + } + } + } + + else if (s == "target" && type == tpSymlink) { + std::string target = readString(source); + sink.createSymlink(path, target); + } + + else { + throw badArchive("unknown field " + s); + } + } +} + +void parseDump(ParseSink& sink, Source& source) { + std::string version; + try { + version = readString(source, kNarVersionMagic1.size()); + } catch (SerialisationError& e) { + /* This generally means the integer at the start couldn't be + decoded. Ignore and throw the exception below. */ + } + if (version != kNarVersionMagic1) { + throw badArchive("input doesn't look like a Nix archive"); + } + parse(sink, source, ""); +} + +struct RestoreSink : ParseSink { + Path dstPath; + AutoCloseFD fd; + + void createDirectory(const Path& path) override { + Path p = dstPath + path; + if (mkdir(p.c_str(), 0777) == -1) { + throw SysError(format("creating directory '%1%'") % p); + } + }; + + void createRegularFile(const Path& path) override { + Path p = dstPath + path; + fd = AutoCloseFD( + open(p.c_str(), O_CREAT | O_EXCL | O_WRONLY | O_CLOEXEC, 0666)); + if (!fd) { + throw SysError(format("creating file '%1%'") % p); + } + } + + void isExecutable() override { + struct stat st; + if (fstat(fd.get(), &st) == -1) { + throw SysError("fstat"); + } + if (fchmod(fd.get(), st.st_mode | (S_IXUSR | S_IXGRP | S_IXOTH)) == -1) { + throw SysError("fchmod"); + } + } + + void preallocateContents(unsigned long long len) override { +#if HAVE_POSIX_FALLOCATE + if (len != 0u) { + errno = posix_fallocate(fd.get(), 0, len); + /* Note that EINVAL may indicate that the underlying + filesystem doesn't support preallocation (e.g. on + OpenSolaris). Since preallocation is just an + optimisation, ignore it. */ + if (errno && errno != EINVAL && errno != EOPNOTSUPP && errno != ENOSYS) { + throw SysError(format("preallocating file of %1% bytes") % len); + } + } +#endif + } + + void receiveContents(unsigned char* data, unsigned int len) override { + writeFull(fd.get(), data, len); + } + + void createSymlink(const Path& path, const std::string& target) override { + Path p = dstPath + path; + nix::createSymlink(target, p); + } +}; + +void restorePath(const Path& path, Source& source) { + RestoreSink sink; + sink.dstPath = path; + parseDump(sink, source); +} + +void copyNAR(Source& source, Sink& sink) { + // FIXME: if 'source' is the output of dumpPath() followed by EOF, + // we should just forward all data directly without parsing. + + ParseSink parseSink; /* null sink; just parse the NAR */ + + LambdaSource wrapper([&](unsigned char* data, size_t len) { + auto n = source.read(data, len); + sink(data, n); + return n; + }); + + parseDump(parseSink, wrapper); +} + +} // namespace nix |