diff options
Diffstat (limited to 'src/lzma/io.c')
-rw-r--r-- | src/lzma/io.c | 664 |
1 files changed, 664 insertions, 0 deletions
diff --git a/src/lzma/io.c b/src/lzma/io.c new file mode 100644 index 00000000..a7683fcc --- /dev/null +++ b/src/lzma/io.c @@ -0,0 +1,664 @@ +/////////////////////////////////////////////////////////////////////////////// +// +/// \file io.c +/// \brief File opening, unlinking, and closing +// +// Copyright (C) 2007 Lasse Collin +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; either +// version 2.1 of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +/////////////////////////////////////////////////////////////////////////////// + +#include "private.h" + +#if defined(HAVE_FUTIMES) || defined(HAVE_FUTIMESAT) +# include <sys/time.h> +#endif + +#ifndef O_SEARCH +# define O_SEARCH O_RDONLY +#endif + + +/// \brief Number of open file_pairs +/// +/// Once the main() function has requested processing of all files, +/// we wait that open_pairs drops back to zero. Then it is safe to +/// exit from the program. +static size_t open_pairs = 0; + + +/// \brief mutex for file system operations +/// +/// All file system operations are done via the functions in this file. +/// They use fchdir() to avoid some race conditions (more portable than +/// openat() & co.). +/// +/// Synchronizing all file system operations shouldn't affect speed notably, +/// since the actual reading from and writing to files is done in parallel. +static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; + + +/// This condition is invoked when a file is closed and the value of +/// the open_files variable has dropped to zero. The only listener for +/// this condition is io_finish() which is called from main(). +static pthread_cond_t io_cond = PTHREAD_COND_INITIALIZER; + + +/// True when stdout is being used by some thread +static bool stdout_in_use = false; + + +/// This condition is signalled when a thread releases stdout (no longer +/// writes data to it). +static pthread_cond_t stdout_cond = PTHREAD_COND_INITIALIZER; + + +/// \brief Directory where we were started +/// +/// This is needed when a new file, whose name was given on command line, +/// is opened. +static int start_dir; + + +static uid_t uid; +static gid_t gid; + + +extern void +io_init(void) +{ + start_dir = open(".", O_SEARCH | O_NOCTTY); + if (start_dir == -1) { + errmsg(V_ERROR, _("Cannot get file descriptor of the current " + "directory: %s"), strerror(errno)); + my_exit(ERROR); + } + + uid = getuid(); + gid = getgid(); + + return; +} + + +/// Waits until the number of open file_pairs has dropped to zero. +extern void +io_finish(void) +{ + pthread_mutex_lock(&mutex); + + while (open_pairs != 0) + pthread_cond_wait(&io_cond, &mutex); + + (void)close(start_dir); + + pthread_mutex_unlock(&mutex); + + return; +} + + +/// \brief Unlinks a file +/// +/// \param dir_fd File descriptor of the directory containing the file +/// \param name Name of the file with or without path +/// +/// \return Zero on success. On error, -1 is returned and errno set. +/// +static void +io_unlink(int dir_fd, const char *name, ino_t ino) +{ + const char *base = str_filename(name); + if (base == NULL) { + // This shouldn't happen. + errmsg(V_ERROR, _("%s: Invalid filename"), name); + return; + } + + pthread_mutex_lock(&mutex); + + if (fchdir(dir_fd)) { + errmsg(V_ERROR, _("Cannot change directory: %s"), + strerror(errno)); + } else { + struct stat st; + if (lstat(base, &st) || st.st_ino != ino) + errmsg(V_ERROR, _("%s: File seems to be moved, " + "not removing"), name); + + // There's a race condition between lstat() and unlink() + // but at least we have tried to avoid removing wrong file. + else if (unlink(base)) + errmsg(V_ERROR, _("%s: Cannot remove: %s"), + name, strerror(errno)); + } + + pthread_mutex_unlock(&mutex); + + return; +} + + +/// \brief Copies owner/group and permissions +/// +/// \todo ACL and EA support +/// +static void +io_copy_attrs(const file_pair *pair) +{ + // This function is more tricky than you may think at first. + // Blindly copying permissions may permit users to access the + // destination file who didn't have permission to access the + // source file. + + if (uid == 0 && fchown(pair->dest_fd, pair->src_st.st_uid, -1)) + errmsg(V_WARNING, _("%s: Cannot set the file owner: %s"), + pair->dest_name, strerror(errno)); + + mode_t mode; + + if (fchown(pair->dest_fd, -1, pair->src_st.st_gid)) { + errmsg(V_WARNING, _("%s: Cannot set the file group: %s"), + pair->dest_name, strerror(errno)); + // We can still safely copy some additional permissions: + // `group' must be at least as strict as `other' and + // also vice versa. + // + // NOTE: After this, the owner of the source file may + // get additional permissions. This shouldn't be too bad, + // because the owner would have had permission to chmod + // the original file anyway. + mode = ((pair->src_st.st_mode & 0070) >> 3) + & (pair->src_st.st_mode & 0007); + mode = (pair->src_st.st_mode & 0700) | (mode << 3) | mode; + } else { + // Drop the setuid, setgid, and sticky bits. + mode = pair->src_st.st_mode & 0777; + } + + if (fchmod(pair->dest_fd, mode)) + errmsg(V_WARNING, _("%s: Cannot set the file permissions: %s"), + pair->dest_name, strerror(errno)); + + // Copy the timestamps only if we have a secure function to do it. +#if defined(HAVE_FUTIMES) || defined(HAVE_FUTIMESAT) + struct timeval tv[2]; + tv[0].tv_sec = pair->src_st.st_atime; + tv[1].tv_sec = pair->src_st.st_mtime; + +# if defined(HAVE_STRUCT_STAT_ST_ATIM_TV_NSEC) + tv[0].tv_usec = pair->src_st.st_atim.tv_nsec / 1000; +# elif defined(HAVE_STRUCT_STAT_ST_ATIMESPEC_TV_NSEC) + tv[0].tv_usec = pair->src_st.st_atimespec.tv_nsec / 1000; +# else + tv[0].tv_usec = 0; +# endif + +# if defined(HAVE_STRUCT_STAT_ST_MTIM_TV_NSEC) + tv[1].tv_usec = pair->src_st.st_mtim.tv_nsec / 1000; +# elif defined(HAVE_STRUCT_STAT_ST_MTIMESPEC_TV_NSEC) + tv[1].tv_usec = pair->src_st.st_mtimespec.tv_nsec / 1000; +# else + tv[1].tv_usec = 0; +# endif + +# ifdef HAVE_FUTIMES + (void)futimes(pair->dest_fd, tv); +# else + (void)futimesat(pair->dest_fd, NULL, tv); +# endif +#endif + + return; +} + + +/// Opens and changes into the directory containing the source file. +static int +io_open_dir(file_pair *pair) +{ + if (pair->src_name == stdin_filename) + return 0; + + if (fchdir(start_dir)) { + errmsg(V_ERROR, _("Cannot change directory: %s"), + strerror(errno)); + return -1; + } + + const char *split = strrchr(pair->src_name, '/'); + if (split == NULL) { + pair->dir_fd = start_dir; + } else { + // Copy also the slash. It's needed to support filenames + // like "/foo" (dirname being "/"), and it never hurts anyway. + const size_t dirname_len = split - pair->src_name + 1; + char dirname[dirname_len + 1]; + memcpy(dirname, pair->src_name, dirname_len); + dirname[dirname_len] = '\0'; + + // Open the directory and change into it. + pair->dir_fd = open(dirname, O_SEARCH | O_NOCTTY); + if (pair->dir_fd == -1 || fchdir(pair->dir_fd)) { + errmsg(V_ERROR, _("%s: Cannot open the directory " + "containing the file: %s"), + pair->src_name, strerror(errno)); + (void)close(pair->dir_fd); + return -1; + } + } + + return 0; +} + + +static void +io_close_dir(file_pair *pair) +{ + if (pair->dir_fd != start_dir) + (void)close(pair->dir_fd); + + return; +} + + +/// Opens the source file. The file is opened using the plain filename without +/// path, thus the file must be in the current working directory. This is +/// ensured because io_open_dir() is always called before this function. +static int +io_open_src(file_pair *pair) +{ + if (pair->src_name == stdin_filename) { + pair->src_fd = STDIN_FILENO; + } else { + // Strip the pathname. Thanks to io_open_dir(), the file + // is now in the current working directory. + const char *filename = str_filename(pair->src_name); + if (filename == NULL) + return -1; + + // Symlinks are followed if --stdout or --force has been + // specified. + const bool follow_symlinks = opt_stdout || opt_force; + pair->src_fd = open(filename, O_RDONLY | O_NOCTTY + | (follow_symlinks ? 0 : O_NOFOLLOW)); + if (pair->src_fd == -1) { + // Give an understandable error message in if reason + // for failing was that the file was a symbolic link. + // - Linux, OpenBSD, Solaris: ELOOP + // - FreeBSD: EMLINK + // - Tru64: ENOTSUP + // It seems to be safe to check for all these, since + // those errno values aren't used for other purporses + // on any of the listed operating system *when* the + // above flags are used with open(). + if (!follow_symlinks + && (errno == ELOOP +#ifdef EMLINK + || errno == EMLINK +#endif +#ifdef ENOTSUP + || errno == ENOTSUP +#endif + )) { + errmsg(V_WARNING, _("%s: Is a symbolic link, " + "skipping"), pair->src_name); + } else { + errmsg(V_ERROR, "%s: %s", pair->src_name, + strerror(errno)); + } + + return -1; + } + + if (fstat(pair->src_fd, &pair->src_st)) { + errmsg(V_ERROR, "%s: %s", pair->src_name, + strerror(errno)); + goto error; + } + + if (S_ISDIR(pair->src_st.st_mode)) { + errmsg(V_WARNING, _("%s: Is a directory, skipping"), + pair->src_name); + goto error; + } + + if (!opt_stdout) { + if (!opt_force && !S_ISREG(pair->src_st.st_mode)) { + errmsg(V_WARNING, _("%s: Not a regular file, " + "skipping"), pair->src_name); + goto error; + } + + if (pair->src_st.st_mode & (S_ISUID | S_ISGID)) { + // Setuid and setgid files are rejected even + // with --force. This is good for security + // (hopefully) but it's a bit weird to reject + // file when --force was given. At least this + // matches gzip's behavior. + errmsg(V_WARNING, _("%s: File has setuid or " + "setgid bit set, skipping"), + pair->src_name); + goto error; + } + + if (!opt_force && (pair->src_st.st_mode & S_ISVTX)) { + errmsg(V_WARNING, _("%s: File has sticky bit " + "set, skipping"), + pair->src_name); + goto error; + } + + if (pair->src_st.st_nlink > 1) { + errmsg(V_WARNING, _("%s: Input file has more " + "than one hard link, " + "skipping"), pair->src_name); + goto error; + } + } + } + + return 0; + +error: + (void)close(pair->src_fd); + return -1; +} + + +/// \brief Closes source file of the file_pair structure +/// +/// \param pair File whose src_fd should be closed +/// \param success If true, the file will be removed from the disk if +/// closing succeeds and --keep hasn't been used. +static void +io_close_src(file_pair *pair, bool success) +{ + if (pair->src_fd == STDIN_FILENO || pair->src_fd == -1) + return; + + if (close(pair->src_fd)) { + errmsg(V_ERROR, _("%s: Closing the file failed: %s"), + pair->src_name, strerror(errno)); + } else if (success && !opt_keep_original) { + io_unlink(pair->dir_fd, pair->src_name, pair->src_st.st_ino); + } + + return; +} + + +static int +io_open_dest(file_pair *pair) +{ + if (opt_stdout || pair->src_fd == STDIN_FILENO) { + // We don't modify or free() this. + pair->dest_name = (char *)"(stdout)"; + pair->dest_fd = STDOUT_FILENO; + + // Synchronize the order in which files get written to stdout. + // Unlocking the mutex is safe, because opening the file_pair + // can no longer fail. + while (stdout_in_use) + pthread_cond_wait(&stdout_cond, &mutex); + + stdout_in_use = true; + + } else { + pair->dest_name = get_dest_name(pair->src_name); + if (pair->dest_name == NULL) + return -1; + + // This cannot fail, because get_dest_name() doesn't return + // invalid names. + const char *filename = str_filename(pair->dest_name); + assert(filename != NULL); + + pair->dest_fd = open(filename, O_WRONLY | O_NOCTTY | O_CREAT + | (opt_force ? O_TRUNC : O_EXCL), + S_IRUSR | S_IWUSR); + if (pair->dest_fd == -1) { + errmsg(V_ERROR, "%s: %s", pair->dest_name, + strerror(errno)); + free(pair->dest_name); + return -1; + } + + // If this really fails... well, we have a safe fallback. + struct stat st; + if (fstat(pair->dest_fd, &st)) + pair->dest_ino = 0; + else + pair->dest_ino = st.st_ino; + } + + return 0; +} + + +/// \brief Closes destination file of the file_pair structure +/// +/// \param pair File whose dest_fd should be closed +/// \param success If false, the file will be removed from the disk. +/// +/// \return Zero if closing succeeds. On error, -1 is returned and +/// error message printed. +static int +io_close_dest(file_pair *pair, bool success) +{ + if (pair->dest_fd == -1) + return 0; + + if (pair->dest_fd == STDOUT_FILENO) { + stdout_in_use = false; + pthread_cond_signal(&stdout_cond); + return 0; + } + + if (close(pair->dest_fd)) { + errmsg(V_ERROR, _("%s: Closing the file failed: %s"), + pair->dest_name, strerror(errno)); + + // Closing destination file failed, so we cannot trust its + // contents. Get rid of junk: + io_unlink(pair->dir_fd, pair->dest_name, pair->dest_ino); + free(pair->dest_name); + return -1; + } + + // If the operation using this file wasn't successful, we git rid + // of the junk file. + if (!success) + io_unlink(pair->dir_fd, pair->dest_name, pair->dest_ino); + + free(pair->dest_name); + + return 0; +} + + +extern file_pair * +io_open(const char *src_name) +{ + if (is_empty_filename(src_name)) + return NULL; + + file_pair *pair = malloc(sizeof(file_pair)); + if (pair == NULL) { + out_of_memory(); + return NULL; + } + + *pair = (file_pair){ + .src_name = src_name, + .dest_name = NULL, + .dir_fd = -1, + .src_fd = -1, + .dest_fd = -1, + .src_eof = false, + }; + + pthread_mutex_lock(&mutex); + + ++open_pairs; + + if (io_open_dir(pair)) + goto error_dir; + + if (io_open_src(pair)) + goto error_src; + + if (user_abort || io_open_dest(pair)) + goto error_dest; + + pthread_mutex_unlock(&mutex); + + return pair; + +error_dest: + io_close_src(pair, false); +error_src: + io_close_dir(pair); +error_dir: + --open_pairs; + pthread_mutex_unlock(&mutex); + free(pair); + return NULL; +} + + +/// \brief Closes the file descriptors and frees the structure +extern void +io_close(file_pair *pair, bool success) +{ + if (success && pair->dest_fd != STDOUT_FILENO) + io_copy_attrs(pair); + + // Close the destination first. If it fails, we must not remove + // the source file! + if (!io_close_dest(pair, success)) { + // Closing destination file succeeded. Remove the source file + // if the operation using this file pair was successful + // and we haven't been requested to keep the source file. + io_close_src(pair, success); + } else { + // We don't care if operation using this file pair was + // successful or not, since closing the destination file + // failed. Don't remove the original file. + io_close_src(pair, false); + } + + io_close_dir(pair); + + free(pair); + + pthread_mutex_lock(&mutex); + + if (--open_pairs == 0) + pthread_cond_signal(&io_cond); + + pthread_mutex_unlock(&mutex); + + return; +} + + +/// \brief Reads from a file to a buffer +/// +/// \param pair File pair having the sourcefile open for reading +/// \param buf Destination buffer to hold the read data +/// \param size Size of the buffer; assumed be smaller than SSIZE_MAX +/// +/// \return On success, number of bytes read is returned. On end of +/// file zero is returned and pair->src_eof set to true. +/// On error, SIZE_MAX is returned and error message printed. +/// +/// \note This does no locking, thus two threads must not read from +/// the same file. This no problem in this program. +extern size_t +io_read(file_pair *pair, uint8_t *buf, size_t size) +{ + // We use small buffers here. + assert(size < SSIZE_MAX); + + size_t left = size; + + while (left > 0) { + const ssize_t amount = read(pair->src_fd, buf, left); + + if (amount == 0) { + pair->src_eof = true; + break; + } + + if (amount == -1) { + if (errno == EINTR) { + if (user_abort) + return SIZE_MAX; + + continue; + } + + errmsg(V_ERROR, _("%s: Read error: %s"), + pair->src_name, strerror(errno)); + + // FIXME Is this needed? + pair->src_eof = true; + + return SIZE_MAX; + } + + buf += (size_t)(amount); + left -= (size_t)(amount); + } + + return size - left; +} + + +/// \brief Writes a buffer to a file +/// +/// \param pair File pair having the destination file open for writing +/// \param buf Buffer containing the data to be written +/// \param size Size of the buffer; assumed be smaller than SSIZE_MAX +/// +/// \return On success, zero is returned. On error, -1 is returned +/// and error message printed. +/// +/// \note This does no locking, thus two threads must not write to +/// the same file. This no problem in this program. +extern int +io_write(const file_pair *pair, const uint8_t *buf, size_t size) +{ + assert(size < SSIZE_MAX); + + while (size > 0) { + const ssize_t amount = write(pair->dest_fd, buf, size); + if (amount == -1) { + if (errno == EINTR) { + if (user_abort) + return -1; + + continue; + } + + errmsg(V_ERROR, _("%s: Write error: %s"), + pair->dest_name, strerror(errno)); + return -1; + } + + buf += (size_t)(amount); + size -= (size_t)(amount); + } + + return 0; +} |