blob: eaf82c57c651da0fdd7c50dd1aeb15c82c061a88 [file] [log] [blame]
/*
* 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 search_replace_t {
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();
}
/**
* @}
*/