dnl GNU Guix --- Functional package management for GNU dnl Copyright © 2012, 2013, 2014, 2015, 2016, 2018, 2019, 2020, 2021 Ludovic Courtès dnl Copyright © 2014 Mark H Weaver dnl Copyright © 2017, 2020, 2021 Efraim Flashner dnl Copyright © 2021 Chris Marusich dnl dnl This file is part of GNU Guix. dnl dnl GNU Guix is free software; you can redistribute it and/or modify it dnl under the terms of the GNU General Public License as published by dnl the Free Software Foundation; either version 3 of the License, or (at dnl your option) any later version. dnl dnl GNU Guix is distributed in the hope that it will be useful, but dnl WITHOUT ANY WARRANTY; without even the implied warranty of dnl MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the dnl GNU General Public License for more details. dnl dnl You should have received a copy of the GNU General Public License dnl along with GNU Guix. If not, see
aboutsummaryrefslogtreecommitdiff
#include "config.h"

#include "util.hh"
#include "affinity.hh"

#include <iostream>
#include <cerrno>
#include <cstdio>
#include <cstdlib>
#include <sstream>
#include <cstring>

#include <sys/wait.h>
#include <unistd.h>
#include <fcntl.h>
#include <limits.h>

#ifdef __APPLE__
#include <sys/syscall.h>
#endif

#ifdef __linux__
#include <sys/prctl.h>
#endif


extern char * * environ;


namespace nix {


BaseError::BaseError(const FormatOrString & fs, unsigned int status)
    : status(status)
{
    err = fs.s;
}


BaseError & BaseError::addPrefix(const FormatOrString & fs)
{
    prefix_ = fs.s + prefix_;
    return *this;
}


SysError::SysError(const FormatOrString & fs)
    : Error(format("%1%: %2%") % fs.s % strerror(errno))
    , errNo(errno)
{
}


string getEnv(const string & key, const string & def)
{
    char * value = getenv(key.c_str());
    return value ? string(value) : def;
}


Path absPath(Path path, Path dir)
{
    if (path[0] != '/') {
        if (dir == "") {
#ifdef __GNU__
            /* GNU (aka. GNU/Hurd) doesn't have any limitation on path
               lengths and doesn't define `PATH_MAX'.  */
            char *buf = getcwd(NULL, 0);
            if (buf == NULL)
#else
            char buf[PATH_MAX];
            if (!getcwd(buf, sizeof(buf)))
#endif
                throw SysError("cannot get cwd");
            dir = buf;
#ifdef __GNU__
            free(buf);
#endif
        }
        path = dir + "/" + path;
    }
    return canonPath(path);
}


Path canonPath(const Path & path, bool resolveSymlinks)
{
    string s;

    if (path[0] != '/')
        throw Error(format("not an absolute path: `%1%'") % path);

    string::const_iterator i = path.begin(), end = path.end();
    string temp;

    /* Count the number of times we follow a symlink and stop at some
       arbitrary (but high) limit to prevent infinite loops. */
    unsigned int followCount = 0, maxFollow = 1024;

    while (1) {

        /* Skip slashes. */
        while (i != end && *i == '/') i++;
        if (i == end) break;

        /* Ignore `.'. */
        if (*i == '.' && (i + 1 == end || i[1] == '/'))
            i++;

        /* If `..', delete the last component. */
        else if (*i == '.' && i + 1 < end && i[1] == '.' &&
            (i + 2 == end || i[2] == '/'))
        {
            if (!s.empty()) s.erase(s.rfind('/'));
            i += 2;
        }

        /* Normal component; copy it. */
        else {
            s += '/';
            while (i != end && *i != '/') s += *i++;

            /* If s points to a symlink, resolve it and restart (since
               the symlink target might contain new symlinks). */
            if (resolveSymlinks && isLink(s)) {
                if (++followCount >= maxFollow)
                    throw Error(format("infinite symlink recursion in path `%1%'") % path);
                temp = absPath(readLink(s), dirOf(s))
                    + string(i, end);
                i = temp.begin(); /* restart */
                end = temp.end();
                s = "";
            }
        }
    }

    return s.empty() ? "/" : s;
}


Path dirOf(const Path & path)
{
    Path::size_type pos = path.rfind('/');
    if (pos == string::npos)
        throw Error(format("invalid file name `%1%'") % path);
    return pos == 0 ? "/" : Path(path, 0, pos);
}


string baseNameOf(const Path & path)
{
    Path::size_type pos = path.rfind('/');
    if (pos == string::npos)
        throw Error(format("invalid file name `%1%'") % path);
    return string(path, pos + 1);
}


bool isInDir(const Path & path, const Path & dir)
{
    return path[0] == '/'
        && string(path, 0, dir.size()) == dir
        && path.size() >= dir.size() + 2
        && path[dir.size()] == '/';
}


struct stat lstat(const Path & path)
{
    struct stat st;
    if (lstat(path.c_str(), &st))
        throw SysError(format("getting status of `%1%'") % path);
    return st;
}


bool pathExists(const Path & path)
{
    int res;
#ifdef HAVE_STATX
    struct statx st;
    res = statx(AT_FDCWD, path.c_str(), AT_SYMLINK_NOFOLLOW, 0, &st);
#else
    struct stat st;
    res = lstat(path.c_str(), &st);
#endif
    if (!res) return true;
    if (errno != ENOENT && errno != ENOTDIR)
        throw SysError(format("getting status of %1%") % path);
    return false;
}


Path readLink(const Path & path)
{
    checkInterrupt();
    struct stat st = lstat(path);
    if (!S_ISLNK(st.st_mode))
        throw Error(format("`%1%' is not a symlink") % path);
    char buf[st.st_size];
    ssize_t rlsize = readlink(path.c_str(), buf, st.st_size);
    if (rlsize == -1)
        throw SysError(format("reading symbolic link '%1%'") % path);
    else if (rlsize > st.st_size)
        throw Error(format("symbolic link ‘%1%’ size overflow %2% > %3%")
            % path % rlsize % st.st_size);
    return string(buf, st.st_size);
}


bool isLink(const Path & path)
{
    struct stat st = lstat(path);
    return S_ISLNK(st.st_mode);
}


static DirEntries readDirectory(DIR *dir)
{
    DirEntries entries;
    entries.reserve(64);

    struct dirent * dirent;
    while (errno = 0, dirent = readdir(dir)) { /* sic */
        checkInterrupt();
        string name = dirent->d_name;
        if (name == "." || name == "..") continue;
        entries.emplace_back(name, dirent->d_ino, dirent->d_type);
    }
    if (errno) throw SysError(format("reading directory"));

    return entries;
}

DirEntries readDirectory(const Path & path)
{
    AutoCloseDir dir = opendir(path.c_str());
    if (!dir) throw SysError(format("opening directory `%1%'") % path);
    return readDirectory(dir);
}

static DirEntries readDirectory(int fd)
{
    /* Since 'closedir' closes the underlying file descriptor, duplicate FD
       beforehand.  */
    int fdcopy = dup(fd);
    if (fdcopy < 0) throw SysError("dup");

    AutoCloseDir dir = fdopendir(fdcopy);
    if (!dir) throw SysError(format("opening directory from file descriptor `%1%'") % fd);
    return readDirectory(dir);
}

unsigned char getFileType(const Path & path)
{
    struct stat st = lstat(path);
    if (S_ISDIR(st.st_mode)) return DT_DIR;
    if (S_ISLNK(st.st_mode)) return DT_LNK;
    if (S_ISREG(st.st_mode)) return DT_REG;
    return DT_UNKNOWN;
}


string readFile(int fd)
{
    struct stat st;
    if (fstat(fd, &st) == -1)
        throw SysError("statting file");

    unsigned char * buf = new unsigned char[st.st_size];
    AutoDeleteArray<unsigned char> d(buf);
    readFull(fd, buf, st.st_size);

    return string((char *) buf, st.st_size);
}


string readFile(const Path & path, bool drain)
{
    AutoCloseFD fd = open(path.c_str(), O_RDONLY);
    if (fd == -1)
        throw SysError(format("reading file `%1%'") % path);
    return drain ? drainFD(fd) : readFile(fd);
}


void writeFile(const Path & path, const string & s)
{
    AutoCloseFD fd = open(path.c_str(), O_WRONLY | O_TRUNC | O_CREAT, 0666);
    if (fd == -1)
        throw SysError(format("writing file '%1%'") % path);
    writeFull(fd, s);
}


string readLine(int fd)
{
    string s;
    while (1) {
        checkInterrupt();
        char ch;
        ssize_t rd = read(fd, &ch, 1);
        if (rd == -1) {
            if (errno != EINTR)
                throw SysError("reading a line");
        } else if (rd == 0)
            throw EndOfFile("unexpected EOF reading a line");
        else {
            if (ch == '\n') return s;
            s += ch;
        }
    }
}


void writeLine(int fd, string s)
{
    s += '\n';
    writeFull(fd, s);
}


static void _deletePath(const Path & path, unsigned long long & bytesFreed, size_t linkThreshold)
{
    checkInterrupt();

    printMsg(lvlVomit, format("%1%") % path);

#ifdef HAVE_STATX
# define st_mode stx_mode
# define st_size stx_size
# define st_nlink stx_nlink
    struct statx st;
    if (statx(AT_FDCWD, path.c_str(),
	      AT_SYMLINK_NOFOLLOW,
	      STATX_SIZE | STATX_NLINK | STATX_MODE, &st) == -1)
	throw SysError(format("getting status of `%1%'") % path);
#else
    struct stat st = lstat(path);
#endif

    if (!S_ISDIR(st.st_mode) && st.st_nlink <= linkThreshold)
	bytesFreed += st.st_size;

    if (S_ISDIR(st.st_mode)) {
        /* Make the directory writable. */
        if (!(st.st_mode & S_IWUSR)) {
            if (chmod(path.c_str(), st.st_mode | S_IWUSR) == -1)
                throw SysError(format("making `%1%' writable") % path);
        }

        for (auto & i : readDirectory(path))
            _deletePath(path + "/" + i.name, bytesFreed, linkThreshold);
    }

    int ret;
    ret = S_ISDIR(st.st_mode) ? rmdir(path.c_str()) : unlink(path.c_str());
    if (ret == -1)
        throw SysError(format("cannot unlink `%1%'") % path);

#undef st_mode
#undef st_size
#undef st_nlink
}


void deletePath(const Path & path)
{
    unsigned long long dummy;
    deletePath(path, dummy);
}


void deletePath(const Path & path, unsigned long long & bytesFreed, size_t linkThreshold)
{
    startNest(nest, lvlDebug,
        format("recursively deleting path `%1%'") % path);
    bytesFreed = 0;
    _deletePath(path, bytesFreed, linkThreshold);
}

static void copyFile(int sourceFd, int destinationFd)
{
    struct stat st;
    if (fstat(sourceFd, &st) == -1) throw SysError("statting file");

    ssize_t result = copy_file_range(sourceFd, NULL, destinationFd, NULL, st.st_size, 0);
    if (result < 0 && errno == ENOSYS) {
	for (size_t remaining = st.st_size; remaining > 0; ) {
	    unsigned char buf[8192];
	    size_t count = std::min(remaining, sizeof buf);

	    readFull(sourceFd, buf, count);
	    writeFull(destinationFd, buf, count);
	    remaining -= count;
	}
    } else {
	if (result < 0)
	    throw SysError(format("copy_file_range `%1%' to `%2%'") % sourceFd % destinationFd);

	/* If 'copy_file_range' copied less than requested, try again.  */
	for (ssize_t copied = result; copied < st.st_size; copied += result) {
	    result = copy_file_range(sourceFd, NULL, destinationFd, NULL,
				     st.st_size - copied, 0);
	    if (result < 0)
		throw SysError(format("copy_file_range `%1%' to `%2%'") % sourceFd % destinationFd);
	}
    }
}

static void copyFileRecursively(int sourceroot, const Path &source,
				int destinationroot, const Path &destination,
				bool deleteSource)
{
    struct stat st;
    if (fstatat(sourceroot, source.c_str(), &st, AT_SYMLINK_NOFOLLOW) == -1)
	throw SysError(format("statting file `%1%'") % source);

    if (S_ISREG(st.st_mode)) {
	AutoCloseFD sourceFd = openat(sourceroot, source.c_str(),
				      O_CLOEXEC | O_NOFOLLOW | O_RDONLY);
	if (sourceFd == -1) throw SysError(format("opening `%1%'") % source);

	AutoCloseFD destinationFd = openat(destinationroot, destination.c_str(),
					   O_CLOEXEC | O_CREAT | O_WRONLY | O_TRUNC,
					   st.st_mode);
	if (destinationFd == -1) throw SysError(format("opening `%1%'") % source);

	copyFile(sourceFd, destinationFd);
	fchown(destinationFd, st.st_uid, st.st_gid);
    } else if (S_ISLNK(st.st_mode)) {
	char target[st.st_size + 1];
	ssize_t result = readlinkat(sourceroot, source.c_str(), target, st.st_size);
	if (result != st.st_size) throw SysError("reading symlink target");
	target[st.st_size] = '\0';
	int err = symlinkat(target, destinationroot, destination.c_str());
	if (err != 0)
	    throw SysError(format("creating symlink `%1%'") % destination);
	fchownat(destinationroot, destination.c_str(),
		 st.st_uid, st.st_gid, AT_SYMLINK_NOFOLLOW);
    } else if (S_ISDIR(st.st_mode)) {
	int err = mkdirat(destinationroot, destination.c_str(), 0755);
	if (err != 0)
	    throw SysError(format("creating directory `%1%'") % destination);

	AutoCloseFD destinationFd = openat(destinationroot, destination.c_str(),
					   O_CLOEXEC | O_RDONLY | O_DIRECTORY);
	if (err != 0)
	    throw SysError(format("opening directory `%1%'") % destination);

	AutoCloseFD sourceFd = openat(sourceroot, source.c_str(),
				      O_CLOEXEC | O_NOFOLLOW | O_RDONLY);
	if (sourceFd == -1)
	    throw SysError(format("opening `%1%'") % source);

        if (deleteSource && !(st.st_mode & S_IWUSR)) {
	    /* Ensure the directory writable so files within it can be
	       deleted.  */
            if (fchmod(sourceFd, st.st_mode | S_IWUSR) == -1)
                throw SysError(format("making `%1%' directory writable") % source);
        }

        for (auto & i : readDirectory(sourceFd))
	    copyFileRecursively((int)sourceFd, i.name, (int)destinationFd, i.name,
				deleteSource);
	fchown(destinationFd, st.st_uid, st.st_gid);
    } else throw Error(format("refusing to copy irregular file `%1%'") % source);

    if (deleteSource)
	unlinkat(sourceroot, source.c_str(),
		 S_ISDIR(st.st_mode) ? AT_REMOVEDIR : 0);
}

void copyFileRecursively(const Path &source, const Path &destination, bool deleteSource)
{
    copyFileRecursively(AT_FDCWD, source, AT_FDCWD, destination, deleteSource);
}

static Path tempName(Path tmpRoot, const Path & prefix, bool includePid,
    int & counter)
{
    tmpRoot = canonPath(tmpRoot.empty() ? getEnv("TMPDIR", "/tmp") : tmpRoot, true);
    if (includePid)
        return (format("%1%/%2%-%3%-%4%") % tmpRoot % prefix % getpid() % counter++).str();
    else
        return (format(&