about summary refs log tree commit diff
path: root/third_party/nix/src/libutil/archive.cc
diff options
context:
space:
mode:
Diffstat (limited to 'third_party/nix/src/libutil/archive.cc')
-rw-r--r--third_party/nix/src/libutil/archive.cc398
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