diff options
Diffstat (limited to 'rredtool.c')
-rw-r--r-- | rredtool.c | 1459 |
1 files changed, 1459 insertions, 0 deletions
diff --git a/rredtool.c b/rredtool.c new file mode 100644 index 0000000..5e2a60b --- /dev/null +++ b/rredtool.c @@ -0,0 +1,1459 @@ +/* This file is part of "reprepro" + * Copyright (C) 2009 Bernhard R. Link + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + * + * 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02111-1301 USA + */ +#include <config.h> + +#include <stdint.h> +#include <errno.h> +#include <stdio.h> +#include <stdlib.h> +#include <unistd.h> +#include <getopt.h> +#include <string.h> +#include <sys/stat.h> +#include <sys/types.h> +#include <sys/wait.h> +#include <fcntl.h> +#include <signal.h> +#include <dirent.h> +#include <assert.h> +#include "globals.h" +#include "error.h" +#include "mprintf.h" +#include "sha1.h" +#include "filecntl.h" +#include "rredpatch.h" +#include "time.h" + +/* apt had a bug, http://bugs.debian.org/545694 + * to fail if a patch file only prepends text. + * This if fixed in apt version 0.7.24, + * so this workaround can be disabled when older apt + * versions are no longer expected (i.e. sqeeze is oldstable) */ +#define APT_545694_WORKAROUND + +/* apt always wants to apply the last patch + * (see http://bugs.debian.org/545699), so + * always create an fake-empty patch last */ +#define APT_545699_WORKAROUND + +static int max_patch_count = 20; + +static const struct option options[] = { + {"version", no_argument, NULL, 'V'}, + {"help", no_argument, NULL, 'h'}, + {"debug", no_argument, NULL, 'D'}, + {"merge", no_argument, NULL, 'm'}, + {"max-patch-count", required_argument, NULL, 'N'}, + {"reprepro-hook", no_argument, NULL, 'R'}, + {"patch", no_argument, NULL, 'p'}, + {NULL, 0, NULL, 0} +}; + +static void usage(FILE *f) { + fputs( +"rredtool: handle the restricted subset of ed patches\n" +" as used by Debian {Packages,Sources}.diff files.\n" +"Syntax:\n" +" rredtool <directory> <newfile> <oldfile> <mode>\n" +" update .diff directory (to be called from reprepro)\n" +" rredtool --merge <patches..>\n" +" merge patches into one patch\n" +" rredtool --patch <file> <patches..>\n" +" apply patches to file\n", f); +} + +static const char tab[16] = {'0', '1', '2', '3', '4', '5', '6', '7', + '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}; + +struct hash { + char sha1[2*SHA1_DIGEST_SIZE+1]; + off_t len; +}; +/* we only need sha1 sum and we need it a lot, so implement a "only sha1" */ +static void finalize_sha1(struct SHA1_Context *context, off_t len, /*@out@*/struct hash *hash){ + char *sha1; + unsigned char sha1buffer[SHA1_DIGEST_SIZE]; + int i; + + SHA1Final(context, sha1buffer); + sha1 = hash->sha1; + for (i = 0 ; i < SHA1_DIGEST_SIZE ; i++) { + *(sha1++) = tab[sha1buffer[i] >> 4]; + *(sha1++) = tab[sha1buffer[i] & 0xF]; + } + *sha1 = '\0'; + hash->len = len; +} + +static retvalue gen_sha1sum(const char *fullfilename, /*@out@*/struct hash *hash) { + struct SHA1_Context context; + static const size_t bufsize = 16384; + unsigned char *buffer = malloc(bufsize); + ssize_t sizeread; + int e, i; + int infd; + struct stat s; + + if (FAILEDTOALLOC(buffer)) + return RET_ERROR_OOM; + + SHA1Init(&context); + + infd = open(fullfilename, O_RDONLY); + if (infd < 0) { + e = errno; + if ((e == EACCES || e == ENOENT) && + !isregularfile(fullfilename)) { + free(buffer); + return RET_NOTHING; + } + fprintf(stderr, "Error %d opening '%s': %s\n", + e, fullfilename, strerror(e)); + free(buffer); + return RET_ERRNO(e); + } + i = fstat(infd, &s); + if (i != 0) { + e = errno; + fprintf(stderr, "Error %d getting information about '%s': %s\n", + e, fullfilename, strerror(e)); + (void)close(infd); + free(buffer); + return RET_ERRNO(e); + } + do { + sizeread = read(infd, buffer, bufsize); + if (sizeread < 0) { + e = errno; + fprintf(stderr, "Error %d while reading %s: %s\n", + e, fullfilename, strerror(e)); + free(buffer); + (void)close(infd); + return RET_ERRNO(e); + } + SHA1Update(&context, buffer, (size_t)sizeread); + } while (sizeread > 0); + free(buffer); + i = close(infd); + if (i != 0) { + e = errno; + fprintf(stderr, "Error %d reading %s: %s\n", + e, fullfilename, strerror(e)); + return RET_ERRNO(e); + } + finalize_sha1(&context, s.st_size, hash); + return RET_OK; +} + +struct fileandhash { + FILE *f; + off_t len; + struct SHA1_Context context; +}; + +static void hash_and_write(const void *data, size_t len, void *p) { + struct fileandhash *fh = p; + + fwrite(data, len, 1, fh->f); + SHA1Update(&fh->context, data, len); + fh->len += len; +} + +#define DATEFMT "%Y-%m-%d-%H%M.%S" +#define DATELEN (4 + 1 + 2 + 1 + 2 + 1 + 2 + 2 + 1 + 2) + +static retvalue get_date_string(char *date, size_t max) { + struct tm *tm; + time_t current_time; + size_t len; + + assert (max == DATELEN + 1); + + current_time = time(NULL); + if (current_time == ((time_t) -1)) { + int e = errno; + fprintf(stderr, "rredtool: Error %d from time: %s\n", + e, strerror(e)); + return RET_ERROR; + } + tm = gmtime(¤t_time); + if (tm == NULL) { + int e = errno; + fprintf(stderr, "rredtool: Error %d from gmtime: %s\n", + e, strerror(e)); + return RET_ERROR; + } + errno = 0; + len = strftime(date, max, DATEFMT, tm); + if (len == 0 || len != DATELEN) { + fprintf(stderr, +"rredtool: internal problem calling strftime!\n"); + return RET_ERROR; + } + return RET_OK; +} + +static int create_temporary_file(void) { + const char *tempdir; + char *filename; + int fd; + + tempdir = getenv("TMPDIR"); + if (tempdir == NULL) + tempdir = getenv("TEMPDIR"); + if (tempdir == NULL) + tempdir = "/tmp"; + filename = mprintf("%s/XXXXXX", tempdir); + if (FAILEDTOALLOC(filename)) { + errno = ENOMEM; + return -1; + } +#ifdef HAVE_MKOSTEMP + fd = mkostemp(filename, 0600); +#else +#ifdef HAVE_MKSTEMP + fd = mkstemp(filename); +#else +#error Need mkostemp or mkstemp +#endif +#endif + if (fd >= 0) + unlink(filename); + free(filename); + return fd; +} + +static retvalue execute_into_file(const char * const argv[], /*@out@*/int *fd_p, int expected_exit_code) { + pid_t child, pid; + int fd, status; + + fd = create_temporary_file(); + if (fd < 0) { + int e = errno; + fprintf(stderr, "Error %d creating temporary file: %s\n", + e, strerror(e)); + return RET_ERRNO(e); + } + + child = fork(); + if (child == (pid_t)-1) { + int e = errno; + fprintf(stderr, "rredtool: Error %d forking: %s\n", + e, strerror(e)); + return RET_ERRNO(e); + } + if (child == 0) { + int e, i; + + do { + i = dup2(fd, 1); + e = errno; + } while (i < 0 && (e == EINTR || e == EBUSY)); + if (i < 0) { + fprintf(stderr, +"rredtool: Error %d in dup2(%d, 0): %s\n", + e, fd, strerror(e)); + raise(SIGUSR1); + exit(EXIT_FAILURE); + } + close(fd); + closefrom(3); + execvp(argv[0], (char * const *)argv); + fprintf(stderr, "rredtool: Error %d executing %s: %s\n", + e, argv[0], strerror(e)); + raise(SIGUSR1); + exit(EXIT_FAILURE); + } + do { + pid = waitpid(child, &status, 0); + } while (pid == (pid_t)-1 && errno == EINTR); + if (pid == (pid_t)-1) { + int e = errno; + fprintf(stderr, +"rredtool: Error %d waiting for %s child %lu: %s!\n", + e, argv[0], (unsigned long)child, strerror(e)); + (void)close(fd); + return RET_ERROR; + } + if (WIFEXITED(status) && WEXITSTATUS(status) == expected_exit_code) { + if (lseek(fd, 0, SEEK_SET) == (off_t)-1) { + int e = errno; + fprintf(stderr, +"rredtool: Error %d rewinding temporary file to start: %s!\n", + e, strerror(e)); + (void)close(fd); + return RET_ERROR; + } + *fd_p = fd; + return RET_OK; + } + close(fd); + if (WIFEXITED(status)) { + fprintf(stderr, +"rredtool: %s returned with unexpected exit code %d\n", + argv[0], (int)(WEXITSTATUS(status))); + return RET_ERROR; + } + if (WIFSIGNALED(status)) { + if (WTERMSIG(status) != SIGUSR1) + fprintf(stderr, "rredtool: %s killed by signal %d\n", + argv[0], (int)(WTERMSIG(status))); + return RET_ERROR; + } + fprintf(stderr, "rredtool: %s child dies mysteriously (status=%d)\n", + argv[0], status); + return RET_ERROR; +} + +struct old_index_file { + struct old_patch { + struct old_patch *next, *prev; + char *basefilename; + /* part until the + in the name */ + char *nameprefix; + struct hash hash; + } *first, *last; + struct hash hash; +}; + +static void old_index_done(/*@only@*/struct old_index_file *o) { + while (o->first != NULL) { + struct old_patch *p = o->first; + + o->first = p->next; + free(p->basefilename); + free(p->nameprefix); + free(p); + } + o->last = NULL; +} + +static retvalue make_prefix_uniq(struct old_patch *o) { + struct old_patch *p, *last = NULL; + const char *lookfor = o->nameprefix; + + /* make the prefix uniq by extending all previous occurrences + * of this prefix with an additional +. As this might already + * have happened, this has to be possibly repeated */ + + while (true) { + for (p = o->prev ; p != NULL ; p = p->prev) { + if (p == last) + continue; + if (strcmp(p->nameprefix, lookfor) == 0) { + char *h; + size_t l = strlen(p->nameprefix); + + h = realloc(p->nameprefix, l+2); + if (FAILEDTOALLOC(h)) + return RET_ERROR_OOM; + h[l] = '+' ; + h[l+1] = '\0'; + p->nameprefix = h; + lookfor = h; + last = p; + break; + } + } + if (p == NULL) + return RET_OK; + } +} + +static inline retvalue parse_old_index(char *p, size_t len, struct old_index_file *oldindex) { + char *q, *e = p + len; + off_t filesize; + struct old_patch *o; + retvalue r; + + /* This is only supposed to parse files it wrote itself + * (otherwise not having merged patches would most likely break + * things in ugly ways), so parsing it can be very strict and easy: */ + +#define checkorfail(val) if (e - p < (intptr_t)strlen(val) || memcmp(p, val, strlen(val)) != 0) return RET_NOTHING; else { p += strlen(val); } + + checkorfail("SHA1-Current: "); + q = strchr(p, '\n'); + if (q != NULL && q - p > 2 * SHA1_DIGEST_SIZE) + q = memchr(p, ' ', q - p); + if (q == NULL || q - p != 2 * SHA1_DIGEST_SIZE) + return RET_NOTHING; + memcpy(oldindex->hash.sha1, p, 2 * SHA1_DIGEST_SIZE); + oldindex->hash.sha1[2 * SHA1_DIGEST_SIZE] = '\0'; + p = q; + if (*p == ' ') { + p++; + filesize = 0; + while (*p >= '0' && *p <= '9') { + filesize = 10 * filesize + (*p - '0'); + p++; + } + oldindex->hash.len = filesize; + } else + oldindex->hash.len = (off_t)-1; + checkorfail("\nSHA1-History:\n"); + while (*p == ' ') { + p++; + + q = p; + while ((*p >= '0' && *p <= '9') || (*p >= 'a' && *p <= 'f')) { + p++; + } + if (p - q != 2 * SHA1_DIGEST_SIZE) + return RET_NOTHING; + + o = zNEW(struct old_patch); + if (FAILEDTOALLOC(o)) + return RET_ERROR_OOM; + o->prev = oldindex->last; + oldindex->last = o; + if (o->prev == NULL) + oldindex->first = o; + else + o->prev->next = o; + + memcpy(o->hash.sha1, q, 2 * SHA1_DIGEST_SIZE); + + while (*p == ' ') + p++; + if (*p < '0' || *p > '9') + return RET_NOTHING; + filesize = 0; + while (*p >= '0' && *p <= '9') { + filesize = 10 * filesize + (*p - '0'); + p++; + } + o->hash.len = filesize; + if (*p != ' ') + return RET_NOTHING; + p++; + q = strchr(p, '\n'); + if (q == NULL) + return RET_NOTHING; + o->basefilename = strndup(p, (size_t)(q-p)); + if (FAILEDTOALLOC(o->basefilename)) + return RET_ERROR_OOM; + p = q + 1; + q = strchr(o->basefilename, '+'); + if (q == NULL) + o->nameprefix = mprintf("%s+", o->basefilename); + else + o->nameprefix = strndup(o->basefilename, + 1 + (size_t)(q - o->basefilename)); + if (FAILEDTOALLOC(o->nameprefix)) + return RET_ERROR_OOM; + r = make_prefix_uniq(o); + if (RET_WAS_ERROR(r)) + return r; + + /* allow pseudo-empty fake patches */ + if (memcmp(o->hash.sha1, oldindex->hash.sha1, + 2 * SHA1_DIGEST_SIZE) == 0) + continue; + // TODO: verify filename and create prefix... + } + checkorfail("SHA1-Patches:\n"); + o = oldindex->first; + while (*p == ' ') { + p++; + + if (o == NULL) + return RET_NOTHING; + + q = p; + while ((*p >= '0' && *p <= '9') || (*p >= 'a' && *p <= 'f')) { + p++; + } + if (p - q != 2 * SHA1_DIGEST_SIZE) + return RET_NOTHING; + + while (*p == ' ') + p++; + if (*p < '0' || *p > '9') + return RET_NOTHING; + while (*p >= '0' && *p <= '9') { + p++; + } + if (*p != ' ') + return RET_NOTHING; + p++; + q = strchr(p, '\n'); + if (q == NULL) + return RET_NOTHING; + if (strncmp(o->basefilename, p, (size_t)(q-p)) != 0 + || o->basefilename[q-p] != '\0') + return RET_NOTHING; + p = q + 1; + o = o->next; + } + checkorfail("X-Patch-Precedence: merged\n"); + if (*p != '\0' || p != e) + return RET_NOTHING; + // TODO: check for dangerous stuff (like ../ in basename) + // TODO: ignore patches where the filename is missing? + return RET_OK; +#undef checkorfail +} + +static retvalue read_old_index(const char *fullfilename, /*@out@*/struct old_index_file *oldindex) { + int fd, i; + char *buffer; + size_t buffersize = 102400, available = 0; + ssize_t bytes_read; + retvalue r; + + setzero(struct old_index_file, oldindex); + + if (!isregularfile(fullfilename)) + return RET_NOTHING; + + fd = open(fullfilename, O_RDONLY); + if (fd < 0) { + int e = errno; + + fprintf(stderr, "rredtool: Error %d opening '%s': %s\n", + e, fullfilename, strerror(e)); + return RET_ERRNO(e); + } + + /* index file should not be that big, so read into memory as a whole */ + buffer = malloc(buffersize); + if (FAILEDTOALLOC(buffer)) { + close(fd); + return RET_ERROR_OOM; + } + do { + bytes_read = read(fd, buffer + available, + buffersize - available - 1); + if (bytes_read < 0) { + int e = errno; + + fprintf(stderr, "rredtool: Error %d reading '%s': %s\n", + e, fullfilename, strerror(e)); + (void)close(fd); + free(buffer); + return RET_ERRNO(e); + } + assert ((size_t)bytes_read < buffersize - available); + available += bytes_read; + if (available + 1 >= buffersize) { + fprintf(stderr, +"rredtool: Ridicilous long '%s' file!\n", + fullfilename); + (void)close(fd); + free(buffer); + return RET_ERROR; + } + } while (bytes_read > 0); + i = close(fd); + if (i != 0) { + int e = errno; + + fprintf(stderr, "rredtool: Error %d reading '%s': %s\n", + e, fullfilename, strerror(e)); + free(buffer); + return RET_ERRNO(e); + } + buffer[available] = '\0'; + + r = parse_old_index(buffer, available, oldindex); + free(buffer); + if (r == RET_NOTHING) { + /* wrong format, most likely a left over file */ + fprintf(stderr, +"rredtool: File '%s' does not look like created by rredtool, ignoring!\n", + fullfilename); + old_index_done(oldindex); + setzero(struct old_index_file, oldindex); + return RET_NOTHING; + } + if (RET_WAS_ERROR(r)) { + old_index_done(oldindex); + setzero(struct old_index_file, oldindex); + return r; + } + return RET_OK; +} + +struct patch { + struct patch *next; + char *basefilename; + size_t basefilename_len; + char *fullfilename; + struct hash hash, from; +}; + +static void patches_free(struct patch *r) { + while (r != NULL) { + struct patch *n = r->next; + + free(r->basefilename); + if (r->fullfilename != NULL) { + (void)unlink(r->fullfilename); + free(r->fullfilename); + } + free(r); + r = n; + } +} + +static retvalue new_diff_file(struct patch **root_p, const char *directory, const char *relfilename, const char *since, const char date[DATELEN+1], struct modification *r) { + struct patch *p; + int i, status, fd, pipefds[2], tries = 3; + pid_t child, pid; + retvalue result; + struct fileandhash fh; + + p = zNEW(struct patch); + if (FAILEDTOALLOC(p)) + return RET_ERROR_OOM; + + if (since == NULL) + since = ""; + p->basefilename = mprintf("%s%s", since, date); + if (FAILEDTOALLOC(p->basefilename)) { + patches_free(p); + return RET_ERROR_OOM; + } + p->basefilename_len = strlen(p->basefilename); + p->fullfilename = mprintf("%s/%s.diff/%s.gz.new", + directory, relfilename, p->basefilename); + if (FAILEDTOALLOC(p->fullfilename)) { + patches_free(p); + return RET_ERROR_OOM; + } + /* create the file */ + while (tries-- > 0) { + int e; + + fd = open(p->fullfilename, O_CREAT|O_EXCL|O_NOCTTY|O_WRONLY, 0666); + if (fd >= 0) + break; + e = errno; + if (e == EEXIST && tries > 0) + unlink(p->fullfilename); + else { + fprintf(stderr, +"rredtool: Error %d creating '%s': %s\n", + e, p->fullfilename, strerror(e)); + return RET_ERROR; + } + } + assert (fd > 0); + /* start an child to compress connected via a pipe */ + i = pipe(pipefds); + assert (pipefds[0] > 0); + if (i != 0) { + int e = errno; + fprintf(stderr, "rredtool: Error %d creating pipe: %s\n", + e, strerror(e)); + unlink(p->fullfilename); + return RET_ERROR; + } + child = fork(); + if (child == (pid_t)-1) { + int e = errno; + fprintf(stderr, "rredtool: Error %d forking: %s\n", + e, strerror(e)); + unlink(p->fullfilename); + return RET_ERROR; + } + if (child == 0) { + int e; + + close(pipefds[1]); + do { + i = dup2(pipefds[0], 0); + e = errno; + } while (i < 0 && (e == EINTR || e == EBUSY)); + if (i < 0) { + fprintf(stderr, +"rredtool: Error %d in dup2(%d, 0): %s\n", + e, pipefds[0], strerror(e)); + raise(SIGUSR1); + exit(EXIT_FAILURE); + } + do { + i = dup2(fd, 1); + e = errno; + } while (i < 0 && (e == EINTR || e == EBUSY)); + if (i < 0) { + fprintf(stderr, +"rredtool: Error %d in dup2(%d, 0): %s\n", + e, fd, strerror(e)); + raise(SIGUSR1); + exit(EXIT_FAILURE); + } + close(pipefds[0]); + close(fd); + closefrom(3); + execlp("gzip", "gzip", "-9", (char*)NULL); + fprintf(stderr, "rredtool: Error %d executing gzip: %s\n", + e, strerror(e)); + raise(SIGUSR1); + exit(EXIT_FAILURE); + } + close(pipefds[0]); + close(fd); + /* send the data to the child */ + fh.f = fdopen(pipefds[1], "w"); + if (fh.f == NULL) { + int e = errno; + fprintf(stderr, +"rredtool: Error %d fdopen'ing write end of pipe to compressor: %s\n", + e, strerror(e)); + close(pipefds[1]); + unlink(p->fullfilename); + patches_free(p); + kill(child, SIGTERM); + waitpid(child, NULL, 0); + return RET_ERROR; + } + SHA1Init(&fh.context); + fh.len = 0; + modification_printaspatch(&fh, r, hash_and_write); + result = RET_OK; + i = ferror(fh.f); + if (i != 0) { + fprintf(stderr, "rredtool: Error sending data to gzip!\n"); + (void)fclose(fh.f); + result = RET_ERROR; + } else { + i = fclose(fh.f); + if (i != 0) { + int e = errno; + fprintf(stderr, +"rredtool: Error %d sending data to gzip: %s!\n", + e, strerror(e)); + result = RET_ERROR; + } + } + do { + pid = waitpid(child, &status, 0); + } while (pid == (pid_t)-1 && errno == EINTR); + if (pid == (pid_t)-1) { + int e = errno; + fprintf(stderr, +"rredtool: Error %d waiting for gzip child %lu: %s!\n", + e, (unsigned long)child, strerror(e)); + return RET_ERROR; + } + if (WIFEXITED(status) && WEXITSTATUS(status) == 0) { + if (RET_IS_OK(result)) { + finalize_sha1(&fh.context, fh.len, &p->hash); + p->next = *root_p; + *root_p = p; + } + return result; + } + unlink(p->fullfilename); + patches_free(p); + if (WIFEXITED(status)) { + fprintf(stderr, +"rredtool: gzip returned with non-zero exit code %d\n", + (int)(WEXITSTATUS(status))); + return RET_ERROR; + } + if (WIFSIGNALED(status)) { + fprintf(stderr, "rredtool: gzip killed by signal %d\n", + (int)(WTERMSIG(status))); + return RET_ERROR; + } + fprintf(stderr, "rredtool: gzip child dies mysteriously (status=%d)\n", + status); + return RET_ERROR; +} + +static retvalue write_new_index(const char *newindexfilename, const struct hash *newhash, const struct patch *root) { + int tries, fd, i; + const struct patch *p; + + tries = 2; + while (tries > 0) { + errno = 0; + fd = open(newindexfilename, + O_WRONLY|O_CREAT|O_EXCL|O_NOCTTY, 0666); + if (fd >= 0) + break; + if (errno == EINTR) + continue; + tries--; + if (errno != EEXIST) + break; + unlink(newindexfilename); + } + if (fd < 0) { + int e = errno; + fprintf(stderr, "Error %d creating '%s': %s\n", + e, newindexfilename, strerror(e)); + return RET_ERROR; + } + i = dprintf(fd, "SHA1-Current: %s %lld\n" "SHA1-History:\n", + newhash->sha1, (long long)newhash->len); + for (p = root ; i >= 0 && p != NULL ; p = p->next) { + i = dprintf(fd, " %s %7ld %s\n", + p->from.sha1, (long int)p->from.len, + p->basefilename); + } + if (i >= 0) + i = dprintf(fd, "SHA1-Patches:\n"); + for (p = root ; i >= 0 && p != NULL ; p = p->next) { + i = dprintf(fd, " %s %7ld %s\n", + p->hash.sha1, (long int)p->hash.len, + p->basefilename); + } + if (i >= 0) + i = dprintf(fd, "X-Patch-Precedence: merged\n"); + if (i >= 0) { + i = close(fd); + fd = -1; + } + if (i < 0) { + int e = errno; + fprintf(stderr, "Error %d writing to '%s': %s\n", + e, newindexfilename, strerror(e)); + if (fd >= 0) + (void)close(fd); + unlink(newindexfilename); + return RET_ERRNO(e); + } + return RET_OK; +} + +static void remove_old_diffs(const char *relfilename, const char *diffdirectory, const char *indexfilename, const struct patch *keep) { + struct dirent *de; + DIR *dir; + const struct patch *p; + + if (!isdirectory(diffdirectory)) + return; + + dir = opendir(diffdirectory); + if (dir == NULL) + return; + + while ((de = readdir(dir)) != NULL) { + size_t len = strlen(de->d_name); + + /* special rule for that */ + if (len == 5 && strcmp(de->d_name, "Index") == 0) + continue; + + /* if it does not end with .gz or .gz.new, ignore */ + if (len >= 4 && memcmp(de->d_name + len - 4, ".new", 4) == 0) + len -= 4; + if (len < 3) + continue; + if (memcmp(de->d_name + len - 3, ".gz", 3) != 0) + continue; + len -= 3; + + /* do not mark files to be deleted we still need: */ + for (p = keep ; p != NULL ; p = p->next) { + if (p->basefilename_len != len) + continue; + if (memcmp(p->basefilename, de->d_name, len) == 0) + break; + } + if (p != NULL) + continue; + /* otherwise, tell reprepro this file is no longer needed: */ + dprintf(3, "%s.diff/%s.tobedeleted\n", + relfilename, + de->d_name); + } + closedir(dir); + if (isregularfile(indexfilename) && keep == NULL) + dprintf(3, "%s.diff/Index.tobedeleted\n", + relfilename); +} + +static retvalue ed_diff(const char *oldfullfilename, const char *newfullfilename, /*@out@*/struct rred_patch **rred_p) { + const char *argv[6]; + int fd; + retvalue r; + + argv[0] = "diff"; + argv[1] = "--ed"; + argv[2] = "--minimal"; + argv[3] = oldfullfilename; + argv[4] = newfullfilename; + argv[5] = NULL; + + r = execute_into_file(argv, &fd, 1); + if (RET_WAS_ERROR(r)) + return r; + + return patch_loadfd("<temporary file>", fd, -1, rred_p); +} + +static retvalue read_old_patch(const char *directory, const char *relfilename, const struct old_patch *o, /*@out@*/struct rred_patch **rred_p) { + retvalue r; + const char *args[4]; + char *filename; + int fd; + + filename = mprintf("%s/%s.diff/%s.gz", + directory, relfilename, o->basefilename); + + if (!isregularfile(filename)) + return RET_NOTHING; + args[0] = "gunzip"; + args[1] = "-c"; + args[2] = filename; + args[3] = NULL; + + r = execute_into_file(args, &fd, 0); + free(filename); + if (RET_WAS_ERROR(r)) + return r; + + return patch_loadfd("<temporary file>", fd, -1, rred_p); +} + +static retvalue handle_diff(const char *directory, const char *mode, const char *relfilename, const char *fullfilename, const char *fullnewfilename, const char *diffdirectory, const char *indexfilename, const char *newindexfilename) { + retvalue r; + int patch_count; + struct hash oldhash, newhash; + char date[DATELEN + 1]; + struct patch *p, *root = NULL; + enum {mode_OLD, mode_NEW, mode_CHANGE} m; + struct rred_patch *new_rred_patch; + struct modification *new_modifications; + struct old_index_file old_index; + struct old_patch *o; +#if defined(APT_545694_WORKAROUND) || defined(APT_545699_WORKAROUND) + char *line; + struct modification *newdup; +#endif + + if (strcmp(mode, "new") == 0) + m = mode_NEW; + else if (strcmp(mode, "old") == 0) + m = mode_OLD; + else if (strcmp(mode, "change") == 0) + m = mode_CHANGE; + else { + usage(stderr); + fprintf(stderr, +"Error: 4th argument to rredtool in .diff maintenance mode must be 'new', 'old' or 'change'!\n"); + return RET_ERROR; + } + + if (m == mode_NEW) { + /* There is no old file, nothing to do. + * except checking for old diff files + * and marking them to be deleted */ + remove_old_diffs(relfilename, diffdirectory, + indexfilename, NULL); + return RET_OK; + } + + r = get_date_string(date, sizeof(date)); + if (RET_WAS_ERROR(r)) + return r; + + assert (m == mode_OLD || m == mode_CHANGE); + + /* calculate sha1 checksum of old file */ + r = gen_sha1sum(fullfilename, &oldhash); + if (r == RET_NOTHING) { + fprintf(stderr, "rredtool: expected file '%s' is missing!\n", + fullfilename); + r = RET_ERROR; + } + if (RET_WAS_ERROR(r)) + return r; + + if (m == mode_CHANGE) { + /* calculate sha1 checksum of the new file */ + r = gen_sha1sum(fullnewfilename, &newhash); + if (r == RET_NOTHING) { + fprintf(stderr, +"rredtool: expected file '%s' is missing!\n", + fullnewfilename); + r = RET_ERROR; + } + if (RET_WAS_ERROR(r)) + return r; + + /* if new == old, nothing to do */ + if (newhash.len == oldhash.len && + strcmp(newhash.sha1, oldhash.sha1) == 0) { + m = mode_OLD; + } + } + + if (oldhash.len == 0 || (m == mode_CHANGE && newhash.len == 0)) { + /* Old or new file empty. treat as mode_NEW. + * (checked here instead of letting later + * more general optimisations catch this as + * this garantees there are enough lines to + * make patches longer to work around apt bugs, + * and because no need to parse Index if we want to delete + * it anyway) */ + remove_old_diffs(relfilename, diffdirectory, + indexfilename, NULL); + return RET_OK; + } + + r = read_old_index(indexfilename, &old_index); + if (RET_WAS_ERROR(r)) + return r; + + /* ignore old Index file if it does not match the old file */ + if (old_index.hash.len != (off_t)-1 && old_index.hash.len != oldhash.len) { + old_index_done(&old_index); + memset(&old_index, 0, sizeof(old_index)); + } + if (memcmp(old_index.hash.sha1, oldhash.sha1, 2*SHA1_DIGEST_SIZE) != 0) { + old_index_done(&old_index); + memset(&old_index, 0, sizeof(old_index)); + } + + if (m == mode_OLD) { + /* this index file did not change. + * keep old or delete if not current */ + if (old_index.hash.sha1[0] != '\0') { + for (o = old_index.first ; o != NULL ; o = o->next) + dprintf(3, "%s.diff/%s.gz.keep\n", + relfilename, o->basefilename); + dprintf(3, "%s.diff/Index\n", relfilename); + } else { + remove_old_diffs(relfilename, diffdirectory, + indexfilename, NULL); + } + old_index_done(&old_index); + return RET_OK; + } + assert (m == mode_CHANGE); + + mkdir(diffdirectory, 0777); + +#ifdef APT_545699_WORKAROUND + /* create a fake diff to work around http://bugs.debian.org/545699 */ + newdup = NULL; + r = modification_addstuff(fullnewfilename, &newdup, &line); + if (RET_WAS_ERROR(r)) { + modification_freelist(newdup); + old_index_done(&old_index); + return r; + } + /* save this compressed and store it's sha1sum */ + r = new_diff_file(&root, directory, relfilename, "aptbug545699+", date, + newdup); + modification_freelist(newdup); + free(line); + if (RET_WAS_ERROR(r)) { + old_index_done(&old_index); + return r; + } + root->from = newhash; +#endif + + /* create new diff calling diff --ed */ + r = ed_diff(fullfilename, fullnewfilename, &new_rred_patch); + if (RET_WAS_ERROR(r)) { + old_index_done(&old_index); + patches_free(root); + return r; + } + + new_modifications = patch_getmodifications(new_rred_patch); + assert (new_modifications != NULL); + +#ifdef APT_545694_WORKAROUND + newdup = modification_dup(new_modifications); + if (RET_WAS_ERROR(r)) { + modification_freelist(new_modifications); + patch_free(new_rred_patch); + old_index_done(&old_index); + patches_free(root); + return r; + } + r = modification_addstuff(fullnewfilename, &newdup, &line); + if (RET_WAS_ERROR(r)) { + modification_freelist(newdup); + modification_freelist(new_modifications); + patch_free(new_rred_patch); + old_index_done(&old_index); + patches_free(root); + return r; + } +#endif + + /* save this compressed and store it's sha1sum */ + r = new_diff_file(&root, directory, relfilename, NULL, date, +#ifdef APT_545694_WORKAROUND + newdup); + modification_freelist(newdup); + free(line); +#else + new_modifications); +#endif + // TODO: special handling of misparsing to cope with that better? + if (RET_WAS_ERROR(r)) { + modification_freelist(new_modifications); + patch_free(new_rred_patch); + old_index_done(&old_index); + patches_free(root); + return r; + } + root->from = oldhash; + + /* if the diff is bigger than the new file, + * there is no point in not getting the full file. + * And as in all but extremely strange situations this + * also means all older patches will get bigger when merged, + * do not even bother to calculate them but remove all. */ + if (root->hash.len > newhash.len) { + modification_freelist(new_modifications); + patch_free(new_rred_patch); + old_index_done(&old_index); + patches_free(root); + remove_old_diffs(relfilename, diffdirectory, + indexfilename, NULL); + return RET_OK; + } + + patch_count = 1; + /* merge this into the old patches */ + for (o = old_index.last ; o != NULL ; o = o->prev) { + struct rred_patch *old_rred_patch; + struct modification *d, *merged; + + /* ignore old and new hash, to filter out old + * pseudo-empty patches and to reduce the number + * of patches in case the file is reverted to an + * earlier state */ + if (memcmp(o->hash.sha1, old_index.hash.sha1, + sizeof(old_index.hash.sha1)) == 0) + continue; + if (memcmp(o->hash.sha1, newhash.sha1, + sizeof(newhash.sha1)) == 0) + continue; + /* limit number of patches + * (Index needs to be downloaded, too) */ + + if (patch_count >= max_patch_count) + continue; + + /* empty files only make problems. + * If you have a non-empty file with the sha1sum of an empty + * one: Congratulations */ + if (strcmp(o->hash.sha1, + "da39a3ee5e6b4b0d3255bfef95601890afd80709") == 0) + continue; + + r = read_old_patch(directory, relfilename, o, &old_rred_patch); + if (r == RET_NOTHING) + continue; + // TODO: special handling of misparsing to cope with that better? + if (RET_WAS_ERROR(r)) { + modification_freelist(new_modifications); + patch_free(new_rred_patch); + old_index_done(&old_index); + patches_free(root); + return r; + } + + d = modification_dup(new_modifications); + if (RET_WAS_ERROR(r)) { + patch_free(old_rred_patch); + patch_free(new_rred_patch); + old_index_done(&old_index); + patches_free(root); + return r; + } + r = combine_patches(&merged, + patch_getmodifications(old_rred_patch), d); + if (RET_WAS_ERROR(r)) { + modification_freelist(new_modifications); + patch_free(old_rred_patch); + patch_free(new_rred_patch); + old_index_done(&old_index); + patches_free(root); + return r; + } + if (merged == NULL) { + /* this should never happen as the sha1sum should + * already be the same, but better safe than sorry */ + patch_free(old_rred_patch); + continue; + } +#ifdef APT_545694_WORKAROUND + r = modification_addstuff(fullnewfilename, &merged, &line); + if (RET_WAS_ERROR(r)) { + modification_freelist(merged); + patch_free(old_rred_patch); + modification_freelist(new_modifications); + patch_free(new_rred_patch); + old_index_done(&old_index); + patches_free(root); + return r; + } +#endif + r = new_diff_file(&root, directory, relfilename, + o->nameprefix, date, merged); + modification_freelist(merged); +#ifdef APT_545694_WORKAROUND + free(line); +#endif + patch_free(old_rred_patch); + if (RET_WAS_ERROR(r)) { + modification_freelist(new_modifications); + patch_free(new_rred_patch); + old_index_done(&old_index); + patches_free(root); + return r; + } + root->from = o->hash; + + /* remove patches that are bigger than the new file */ + if (root->hash.len >= newhash.len) { + struct patch *n; + + n = root; + root = n->next; + n->next = NULL; + patches_free(n); + } + patch_count++; + } + + modification_freelist(new_modifications); + patch_free(new_rred_patch); + old_index_done(&old_index); + + assert (root != NULL); +#ifdef APT_545699_WORKAROUND + assert (root->next != NULL); +#endif + + /* write new Index file */ + r = write_new_index(newindexfilename, &newhash, root); + if (RET_WAS_ERROR(r)) { + patches_free(root); + return r; + } + + /* tell reprepro to remove all no longer needed files */ + remove_old_diffs(relfilename, diffdirectory, indexfilename, root); + + /* tell reprepro to move those files to their final place + * and include the Index in the Release file */ + + for (p = root ; p != NULL ; p = p->next) { + /* the trailing . means add but do not put in Release */ + dprintf(3, "%s.diff/%s.gz.new.\n", + relfilename, p->basefilename); + /* no longer delete: */ + free(p->fullfilename); + p->fullfilename = NULL; + } + dprintf(3, "%s.diff/Index.new\n", relfilename); + patches_free(root); + return RET_OK; +} + +static retvalue handle_diff_dir(const char *args[4]) { + const char *directory = args[0]; + const char *mode = args[3]; + const char *relfilename = args[2]; + const char *relnewfilename = args[1]; + char *fullfilename, *fullnewfilename; + char *diffdirectory; + char *indexfilename; + char *newindexfilename; + retvalue r; + + fullfilename = mprintf("%s/%s", directory, relfilename); + fullnewfilename = mprintf("%s/%s", directory, relnewfilename); + if (FAILEDTOALLOC(fullfilename) || FAILEDTOALLOC(fullnewfilename)) { + free(fullfilename); + free(fullnewfilename); + return RET_ERROR_OOM; + } + diffdirectory = mprintf("%s.diff", fullfilename); + indexfilename = mprintf("%s.diff/Index", fullfilename); + newindexfilename = mprintf("%s.diff/Index.new", fullfilename); + if (FAILEDTOALLOC(diffdirectory) || FAILEDTOALLOC(indexfilename) + || FAILEDTOALLOC(newindexfilename)) { + free(diffdirectory); + free(indexfilename); + free(newindexfilename); + free(fullfilename); + free(fullnewfilename); + return RET_ERROR_OOM; + } + r = handle_diff(directory, mode, relfilename, + fullfilename, fullnewfilename, diffdirectory, + indexfilename, newindexfilename); + free(diffdirectory); + free(indexfilename); + free(newindexfilename); + free(fullfilename); + free(fullnewfilename); + return r; +} + +static void write_to_file(const void *data, size_t len, void *to) { + FILE *f = to; + fwrite(data, len, 1, f); +} + +int main(int argc, const char *argv[]) { + struct rred_patch *patches[argc]; + struct modification *m; + retvalue r; + bool mergemode = false; + bool patchmode = false; + bool repreprohook = false; + int i, count; + const char *sourcename; + int debug = 0; + + while ((i = getopt_long(argc, (char**)argv, "+hVDmpR", options, NULL)) != -1) { + switch (i) { + case 'h': + usage(stdout); + return EXIT_SUCCESS; + case 'V': + printf( +"rred-tool from " PACKAGE_NAME " version " PACKAGE_VERSION); + return EXIT_SUCCESS; + case 'D': + debug++; + break; + case 'm': + mergemode = 1; + break; + case 'p': + patchmode = 1; + break; + case 'N': + max_patch_count = atoi(optarg); + break; + case 'R': + repreprohook = 1; + break; + case '?': + default: + return EXIT_FAILURE; + + } + } + + if (repreprohook && mergemode) { + fprintf(stderr, +"Cannot do --reprepro-hook and --merge at the same time!\n"); + return EXIT_FAILURE; + } + if (repreprohook && patchmode) { + fprintf(stderr, +"Cannot do --reprepro-hook and --patch at the same time!\n"); + return EXIT_FAILURE; + } + + if (repreprohook || (!mergemode && !patchmode)) { + if (optind + 4 != argc) { + usage(stderr); + return EXIT_FAILURE; + } + r = handle_diff_dir(argv + optind); + if (r == RET_ERROR_OOM) { + fputs("Out of memory!\n", stderr); + } + if (RET_WAS_ERROR(r)) + return EXIT_FAILURE; + return EXIT_SUCCESS; + } + + i = optind; + if (!mergemode) { + if (i >= argc) { + fprintf(stderr, "Not enough arguments!\n"); + return EXIT_FAILURE; + } + sourcename = argv[i++]; + } else { + SETBUTNOTUSED( sourcename = NULL; ) + } + if (mergemode && patchmode) { + fprintf(stderr, +"Cannot do --merge and --patch at the same time!\n"); + return EXIT_FAILURE; + } + + count = 0; + while (i < argc) { + r = patch_load(argv[i], -1, &patches[count]); + if (RET_IS_OK(r)) + count++; + if (RET_WAS_ERROR(r)) { + if (r == RET_ERROR_OOM) + fputs("Out of memory!\n", stderr); + else + fputs("Aborting...\n", stderr); + return EXIT_FAILURE; + } + i++; + } + if (count <= 0) { + fprintf(stderr, "Not enough patches for operation...\n"); + return EXIT_FAILURE; + } + m = patch_getmodifications(patches[0]); + for (i = 1; i < count ; i++) { + struct modification *a = patch_getmodifications(patches[i]); + if (debug) { + fputs("--------RESULT SO FAR--------\n", stderr); + modification_printaspatch(stderr, m, write_to_file); + fputs("--------TO BE MERGED WITH-----\n", stderr); + modification_printaspatch(stderr, a, write_to_file); + fputs("-------------END--------------\n", stderr); + } + r = combine_patches(&m, m, a); + if (RET_WAS_ERROR(r)) { + for (i = 0 ; i < count ; i++) { + patch_free(patches[i]); + } + if (r == RET_ERROR_OOM) + fputs("Out of memory!\n", stderr); + else + fputs("Aborting...\n", stderr); + return EXIT_FAILURE; + } + } + r = RET_OK; + if (mergemode) { + modification_printaspatch(stdout, m, write_to_file); + } else { + r = patch_file(stdout, sourcename, m); + } + if (ferror(stdout)) { + fputs("Error writing to stdout!\n", stderr); + r = RET_ERROR; + } + modification_freelist(m); + for (i = 0 ; i < count ; i++) + patch_free(patches[i]); + if (r == RET_ERROR_OOM) + fputs("Out of memory!\n", stderr); + if (RET_WAS_ERROR(r)) + return EXIT_FAILURE; + return EXIT_SUCCESS; +} |