fso_is_changeable.c   [plain text]


/*
 * Copyright (c) 2008 BBN Technologies Corp.  All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 3. Neither the name of BBN Technologies nor the names of its contributors
 *    may be used to endorse or promote products derived from this software
 *    without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY BBN TECHNOLOGIES AND CONTRIBUTORS ``AS IS''
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL BBN TECHNOLOGIES OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

#ifdef HAVE_CONFIG_H
#  include <config.h>
#endif

#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <stdlib.h>
#include <assert.h>
#include <unistd.h>
#include <sys/stat.h>

#include <svnstsw/fso_is_changeable.h>

typedef struct {
    const char* search;
    _Bool match_end;
    _Bool match_previous_slash;
    const char* replace;
} search_replace_t;

static int resolve_symlink(char* buf, size_t bufsize, const char* path);
static int read_symlink(char* buf, size_t bufsize, const char* path);
static _Bool is_symlink(const char* path);
static int parent_dir(char* buf, size_t bufsize, const char* path);
static int clean_path(char* buf, size_t bufsize, const char* path);
static int get_cwd(char* buf, size_t bufsize);

_Bool
svnstsw_fso_is_changeable(const char* filename)
{
    // BASE CASE

    struct stat st;

    // get the file/directory details
    if (stat(filename, &st) == -1)
        return 1;

    // if it's owned by the user, that's a problem (since they
    // can turn on the write bit)
    if (st.st_uid == getuid())
        return 1;

    // do not check writeability if it's a directory and the sticky
    // bit is set
    if (!(S_ISDIR(st.st_mode) && (st.st_mode & S_ISVTX)))
    {
        // if it's writable, that's a problem
        const int errno_backup = errno;
        if (access(filename, W_OK) == 0)
            return 1;
        if (errno != EACCES)
            return 1;
        errno = errno_backup;
    }

    // RECURSIVE CASES

    // is filename the root directory?
    if (strncmp("/", filename, 2) != 0)
    {
        // no, so get the parent directory
        char parent[parent_dir(NULL, 0, filename) + 1];
        {
            int tmp = parent_dir(parent, sizeof(parent), filename);
            if (tmp < 0)
                return 1;
            assert(tmp == (sizeof(parent) - 1));
        }

        // test the parent directory
        if (svnstsw_fso_is_changeable(parent))
            return 1;
    }

    // does the filename refer to a symbolic link?
    _Bool is_sym;
    {
        const int errno_backup = errno;
        errno = 0;
        is_sym = is_symlink(filename);
        if (errno)
            return 1;
        errno = errno_backup;
    }

    if (is_sym)
    {
        // resolve the symlink
        char resolved[resolve_symlink(NULL, 0, filename) + 1];
        {
            int tmp = resolve_symlink(resolved, sizeof(resolved), filename);
            if (tmp < 0)
                return 1;
            assert(tmp == (sizeof(resolved) - 1));
        }

        // check if the target is changeable
        if (svnstsw_fso_is_changeable(resolved))
            return 1;
    }

    return 0;
}

/**
 * @defgroup libsvnstswprvchange fso_is_changeable
 * @ingroup libsvnstswprv
 *
 * Helper functions for the implementation of
 * svnstsw_fso_is_changeable().
 *
 * @{
 */

/**
 * @brief Resolves a symlink to a absolute path name.
 *
 * The resolved path is cleaned as if passed through clean_path().
 *
 * This function is thread safe.
 *
 * @param buf Buffer of length @a bufsize that will contain the
 * symlink contents.  This may be the null pointer if @a bufsize is 0.
 * If this buffer is not big enough to hold the full symlink contents,
 * the cleaned path will be truncated (but still null-terminated) to
 * fit.
 *
 * @param bufsize Size of the buffer starting at @a buf.  This
 * function will not write more than this number of bytes beyond @a
 * buf.
 *
 * @param path Null-terminated string containing the path to the
 * symlink to read.
 *
 * @return On success, returns the length of the symlink contents.  On
 * error, returns a negative value, sets @p errno, and the contents of
 * @a buf are undefined.
 */
int
resolve_symlink(char* buf, size_t bufsize, const char* path)
{
    // clean up path if it's too ugly
    if ((!path)
        || (path[0] == '\0')
        || (path[0] != '/'))
    {
        char cp[clean_path(NULL, 0, path)];
        {
            int tmp = clean_path(cp, sizeof(cp), path);
            if (tmp < 0)
                return -1;
            assert(tmp == (sizeof(cp) - 1));
        }

        // recursive call
        return resolve_symlink(buf, bufsize, cp);
    }

    // make sure path refers to a symlink
    {
        const int errno_backup = errno;
        errno = 0;
        if (!is_symlink(path))
        {
            if (!errno)
                errno = EINVAL;
            return -1;
        }
        errno = errno_backup;
    }

    // read the symlink
    char symlink_contents[read_symlink(NULL, 0, path) + 1];
    {
        int tmp = read_symlink(symlink_contents, sizeof(symlink_contents),
                               path);
        if (read_symlink < 0)
            return -1;
        assert(tmp == (sizeof(symlink_contents) - 1));
    }

    // symlinks should never point to an empty string
    if (symlink_contents[0] == '\0')
    {
        errno = EIO;
        return -1;
    }

    // if the target is an absolute path, just clean it up and return
    // it
    if (symlink_contents[0] == '/')
        return clean_path(buf, bufsize, symlink_contents);

    // the target is a relative path.  it's relative to the parent
    // directory of the symlink, so get the symlink's parent
    char sym_parent[parent_dir(NULL, 0, path) + 1];
    {
        int tmp = parent_dir(sym_parent, sizeof(sym_parent), path);
        if (tmp < 0)
            return -1;
        assert(tmp == (sizeof(sym_parent) - 1));
    }

    // concatenate the symlink's parent directory with the relative
    // target
    char abs_contents[sizeof(sym_parent) + sizeof(symlink_contents)];
    {
        int tmp = snprintf(abs_contents, sizeof(abs_contents), "%s/%s",
                           sym_parent, symlink_contents);
        if (tmp < 0)
            return -1;
        assert(tmp == (sizeof(abs_contents) - 1));
    }

    // clean up the concatenated path and return it
    return clean_path(buf, bufsize, abs_contents);
}

/**
 * @brief Determines if @a path refers to a symlink.
 *
 * This function is thread safe.
 *
 * @param path Null-terminated string containing the path to test.
 *
 * @return Returns 1 if there is no error and @a path refers to a
 * symlink, returns 0 otherwise.  On error, @p errno is set.
 */
_Bool
is_symlink(const char* path)
{
    struct stat st;
    if (lstat(path, &st) == -1)
        return 0;

    return S_ISLNK(st.st_mode);
}

/**
 * @brief Reads the contents of a symlink.
 *
 * This function is thread safe.
 *
 * @param buf Buffer of length @a bufsize that will contain the
 * symlink contents.  This may be the null pointer if @a bufsize is 0.
 * If this buffer is not big enough to hold the full symlink contents,
 * the cleaned path will be truncated (but still null-terminated) to
 * fit.
 *
 * @param bufsize Size of the buffer starting at @a buf.  This
 * function will not write more than this number of bytes beyond @a
 * buf.
 *
 * @param path Null-terminated string containing the path to the
 * symlink to read.
 *
 * @return On success, returns the length of the symlink contents.  On
 * error, returns a negative value, sets @p errno, and the contents of
 * @a buf are undefined.
 */
int
read_symlink(char* buf, size_t bufsize, const char* path)
{
    // make sure path really is a symlink
    const int errno_backup = errno;
    errno = 0;
    if (!is_symlink(path))
    {
        if (!errno)
            errno = EINVAL;
        return -1;
    }
    errno = errno_backup;

    // size for the temporary buffer that will hold the contents of
    // the symlink
    size_t len = (bufsize > 1024) ? bufsize : 1024;

    // keep trying bigger and bigger buffers until everything can fit
    while (1)
    {
        // allocate a temporary buffer
        char tmp[len];

        // fill the buffer with the contents of the symlink
        ssize_t written = readlink(path, tmp, len);

        // was the buffer big enough?
        if (written < len)
        {
            // there was an error reading the link
            if (written == -1)
                return -1;

            // buffer was big enough.  readlink() doesn't
            // null-terminate, so we must do that.
            tmp[written] = '\0';

            // copy the results to the user's buffer
            return snprintf(buf, bufsize, "%s", tmp);
        }

        // buffer wasn't big enough -- try again with a bigger buffer
        len *= 2;
    }
    // shouldn't be possible to get here
    abort();
}

/**
 * @brief Determines the parent directory of @a path.
 *
 * The results are clean (as if passed through clean_path()).
 *
 * This function is thread safe.
 *
 * @param buf Buffer of length @a bufsize that will contain the parent
 * directory.  This may be the null pointer if @a bufsize is 0.  If
 * this buffer is not big enough to hold the full parent directory,
 * the cleaned path will be truncated (but still null-terminated) to
 * fit.
 *
 * @param bufsize Size of the buffer starting at @a buf.  This
 * function will not write more than this number of bytes beyond @a
 * buf.
 *
 * @param path Null-terminated string containing the path whose parent
 * should be placed in @a buf.
 *
 * @return On success, returns the length of the parent directory.  On
 * error, returns a negative value, sets @p errno, and the contents of
 * @a buf are undefined.
 */
int
parent_dir(char* buf, size_t bufsize, const char* path)
{
    // clean path
    char cp[clean_path(NULL, 0, path) + 1];
    {
        int tmp = clean_path(cp, sizeof(cp), path);
        if (tmp < 0)
            return -1;
        assert(tmp == (sizeof(cp) - 1));
    }

    // find the last slash
    char* found = strrchr(cp, '/');
    assert(found);

    // terminate the string at or just after the slash
    if (found == cp)
        found[1] = '\0';
    else
        found[0] = '\0';

    // copy the string to the user's buffer
    return snprintf(buf, bufsize, cp);
}

/**
 * @brief Takes @a path and converts it to an absolute path with no "."
 * or ".." components.
 *
 * This function is like realpath() except this doesn't resolve
 * symbolic links.
 *
 * If @a path is a relative path, it is converted to an absolute path
 * by prepending the results of calling get_cwd().
 *
 * This function is thread safe.
 *
 * @param buf Buffer of length @a bufsize that will contain the
 * cleaned-up path.  This may be the null pointer if @a bufsize is 0.
 * If this buffer is not big enough to hold the full cleaned path, the
 * cleaned path will be truncated (but still null-terminated) to fit.
 *
 * @param bufsize Size of the buffer starting at @a buf.  This
 * function will not write more than this number of bytes beyond @a
 * buf.
 *
 * @param path Null-terminated string containing the path to clean up.
 *
 * @return On success, returns the length of the cleaned path.  On
 * error, returns a negative value, sets @p errno, and the contents of
 * @a buf are undefined.
 */
int
clean_path(char* buf, size_t bufsize, const char* path)
{
    // we don't like null or empty strings
    if ((!path) || (path[0] == '\0'))
    {
        errno = EINVAL;
        return -1;
    }

    // make sure it's an absolute path
    if (path[0] != '/')
    {
        // get the current working directory
        char cwd[get_cwd(NULL, 0) + 1];
        {
            int tmp = get_cwd(cwd, sizeof(cwd));
            if (tmp < 0)
                return -1;
            assert(tmp == (sizeof(cwd) - 1));
        }

        // append path to the working directory
        char abs_path[strlen(cwd) + 1 + strlen(path) + 1];
        {
            int tmp = snprintf(abs_path, sizeof(abs_path), "%s/%s", cwd, path);
            if (tmp < 0)
                return -1;
            assert(tmp == (sizeof(abs_path) - 1));
        }

        // recurse
        return clean_path(buf, bufsize, abs_path);
    }

    // size of the buffer to hold the cleaned-up path.  Since cleaning
    // never causes the length of the path to increase, strlen(path)
    // should always be big enough.
    const size_t pathbuflen = strlen(path) + 1;

    // buffer that will hold the final cleaned result.  it starts off
    // as path and will get progressively cleaner until we're done.
    char fixed[pathbuflen];
    if (snprintf(fixed, pathbuflen, "%s", path) < 0)
        return -1;

    // buffer to hold a working copy of the cleaned result.  this will
    // be hacked until an incremental cleaning stage is complete.
    char working[pathbuflen];
    if (snprintf(working, pathbuflen, "%s", path) < 0)
        return -1;

    // search and replace directives to clean the path.
    const search_replace_t sr[] = {
        {"/./",  0, 0, "/"}, // any "/./" can be compressed to "/"
        {"/.",   1, 0, ""},  // remove "/." at end of path
        {"//",   0, 0, "/"}, // compress any consecutive slashes to a single
        {"/",    1, 0, ""},  // remove trailing slashes
        {"/../", 0, 1, "/"}, // compress "/foo/../" to "/"
        {"/..",  1, 1, ""},  // compress trailing "/foo/.." to ""
        {NULL,   0, 0, NULL}
    };

    // perform all of the above search and replaces
    for (size_t i = 0; sr[i].search; ++i)
    {
        // save the number of characters in the search string
        const size_t searchlen = strlen(sr[i].search);

        // the search and replace algorithm only works if we're not
        // searching for the empty string and the replace text is
        // shorter than the search text (otherwise we'd have to deal
        // with buffers not being big enough)
        assert(searchlen && (searchlen >= strlen(sr[i].replace)));

        // where to start looking for the search string (defaults to
        // the beginning of the path)
        char* searchstart = working;

        // where the search string was found
        char* found = NULL;

        // repeatedly search the string, replacing all occurrences of
        // the search string with the replace string
        while ((found = strstr(searchstart, sr[i].search)) != NULL)
        {
            // if we must match the end of the path and we're not yet
            // at the end, continue the search
            if (sr[i].match_end)
            {
                // are we at the end yet?
                if (found[searchlen] != '\0')
                {
                    // nope.  find the next match.
                    searchstart = found + 1;
                    continue;
                }
                else
                {
                    // we're at the end.  by setting searchstart to
                    // working, we'll repeat the search from the
                    // beginning after the replacement happens.  this
                    // is in case the replacement would result in
                    // another ending match
                    searchstart = working;
                }
            }

            // size of the block of matching text that will be
            // replaced.  by default, this equals the length of the
            // search string
            size_t matchlen = searchlen;

            // do we need to include everything after the previous
            // slash?
            if (sr[i].match_previous_slash)
            {
                // terminate the string to make it easy to find the
                // previous slash
                found[0] = '\0';

                // locate the previous slash
                char* found_sl = strrchr(working, '/');

                // was there a previous slash?
                if (found_sl)
                {
                    // update the length of the match to include the
                    // number of characters at and after the previous
                    // slash
                    matchlen += (found - found_sl);

                    // the block of text to remove begins at the
                    // previous slash
                    found = found_sl;
                }
            }

            // the number of bytes into the path where the match begins
            int offset = found - working;
            assert((offset >= 0) && (offset < (pathbuflen - matchlen)));

            // perform the replace
            if (snprintf(found, pathbuflen - offset, "%s%s", sr[i].replace,
                         fixed + offset + matchlen) < 0)
                return -1;

            // update the fixed buffer
            if (snprintf(fixed, pathbuflen, "%s", working) < 0)
                return -1;
        }
    }
    // path is completely cleaned.  put the results in the caller's
    // buffer.
    return snprintf(buf, bufsize, "%s", fixed);
}

/**
 * @brief Gets the current working directory.
 *
 * This function is thread safe.
 *
 * @param buf Buffer of length @a bufsize that will contain the
 * current working directory.  This may be the null pointer if @a
 * bufsize is 0.  If this buffer is not big enough to hold the full
 * working directory, the working directory will be truncated (but
 * still null-terminated) to fit.
 *
 * @param bufsize Size of the buffer starting at @a buf.  This
 * function will not write more than this number of bytes beyond @a
 * buf.
 *
 * @return On success, returns the length of the current working
 * directory.  On error, returns a negative value, sets @p errno, and
 * the contents of @a buf are undefined.
 */
int
get_cwd(char* buf, size_t bufsize)
{
    const int errno_backup = errno;

    // size of the temporary buffer to allocate
    size_t cwdlen = (bufsize > 1024) ? bufsize : 1024;

    // keep trying to get the current working directory until we have
    // allocated a big enough buffer to hold the whole thing.
    while (1)
    {
        // allocate the buffer
        char tmp[cwdlen];

        // fill the buffer with the cwd
        const char* d = getcwd(tmp, sizeof(tmp));

        // was getcwd() successful?
        if (d)
            return snprintf(buf, bufsize, "%s", tmp);

        // not successful -- was the problem something other than the
        // buffer being too small?
        if (errno != ERANGE)
            return -1;

        // buffer was too small.  restore errno since we haven't
        // failed yet.
        errno = errno_backup;

        // double the size of the buffer
        cwdlen *= 2;
    }
    // shouldn't be possible to get here.
    abort();
}

/**
 * @}
 */