/* * 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(); } /** * @} */