gzip: support the --rsyncable option
authorRusty Russell <rusty@rustcorp.com.au>
Sat, 5 Sep 2015 19:08:28 +0000 (12:08 -0700)
committerJim Meyering <meyering@fb.com>
Wed, 2 Mar 2016 16:58:14 +0000 (08:58 -0800)
* deflate.c: Include verify.h.
(RSYNC_WIN, RSYNC_SUM_MATCH): Define.
(rsync_sum, rsync_chunk_end): Declare file-scoped globals.
(lm_init): Initialize globals.
(fill_window): Update rsync_chunk_end.
(rsync_roll): New function.
(RSYNC_ROLL): New macro.
(FLUSH_BLOCK): Update for new "pad" parameter.
(deflate_fast): Use RSYNC_ROLL and flush/pad.
(deflate): Likewise.
* trees.c (flush_block): Add "pad" parameter.
* gzip.c (rsync): New global.
(RSYNCABLE_OPTION, longopts, help): Add the option.
(main): Set the new global.
* gzip.h (rsync): Declare new global.
(flush_block): Update prototype.
* doc/gzip.texi: Document it.
* gzip.1: Likewise.
* bootstrap.conf: Use verify module.
* NEWS (New feature): Mention it.
* Makefile.am (check-local): Add tests and use AM_V__* command-
hiding opions. Reported against Debian here:
https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=118118

Makefile.am
NEWS
bootstrap.conf
deflate.c
doc/gzip.texi
gzip.1
gzip.c
gzip.h
trees.c

index 9d06d98baaa36b8f543dff5d2afe29f37275c53a..b01ddee020afd808535e7bcd6f8ed9843b96d055 100644 (file)
@@ -108,16 +108,19 @@ gen-ChangeLog:
 FILES_TO_CHECK = $(bin_SCRIPTS) \
   $(top_srcdir)/ChangeLog $(top_srcdir)/configure $(top_srcdir)/gzip.c
 check-local: $(FILES_TO_CHECK) $(bin_PROGRAMS) gzip.doc.gz
-       { test '$(srcdir)' != . || ./zdiff --__bindir . -c gzip.doc.gz; }
-       ./zdiff --__bindir . -c $(srcdir)/gzip.doc $(srcdir)/gzip.doc
-       ./zdiff --__bindir . $(srcdir)/gzip.doc gzip.doc.gz
-       ./zdiff --__bindir . -c - $(srcdir)/gzip.doc <gzip.doc.gz
-       ./zdiff --__bindir . -c gzip.doc.gz gzip.doc.gz
-       ./zgrep --__bindir . -iV >/dev/null
-       for file in $(FILES_TO_CHECK); do \
-         ./gzip -cv -- "$$file" | ./gzip -d | cmp - "$$file" || exit 1; \
+       $(AM_V_GEN){ test '$(srcdir)' != . \
+                      || ./zdiff --__bindir . -c gzip.doc.gz; }
+       $(AM_V_at)./zdiff --__bindir . -c $(srcdir)/gzip.doc $(srcdir)/gzip.doc
+       $(AM_V_at)./zdiff --__bindir . $(srcdir)/gzip.doc gzip.doc.gz
+       $(AM_V_at)./zdiff --__bindir . -c - $(srcdir)/gzip.doc <gzip.doc.gz
+       $(AM_V_at)./zdiff --__bindir . -c gzip.doc.gz gzip.doc.gz
+       $(AM_V_at)./zgrep --__bindir . -iV >/dev/null
+       $(AM_V_at)for opt in --rsyncable '' -1 -9; do   \
+          for file in $(FILES_TO_CHECK); do            \
+            ./gzip $$opt -c -- "$$file"                        \
+              | ./gzip -d | cmp - "$$file" || exit 1;  \
+          done;                                                \
        done
-       @echo 'Test succeeded.'
 
 install-exec-hook: remove-installed-links
 install-exec-hook remove-installed-links:
diff --git a/NEWS b/NEWS
index a3989afa6e81ad8e83571169e99556751a6010f4..e3815d1c9b02afdb9454c9d071745dbba73b69c9 100644 (file)
--- a/NEWS
+++ b/NEWS
@@ -16,6 +16,14 @@ GNU gzip NEWS                                    -*- outline -*-
   this option makes gzip safer in the presence of system crashes, it
   can make gzip considerably slower.
 
+  gzip now accepts the --rsyncable option. This option is accepted in
+  all modes, but has effect only when compressing: it makes the resulting
+  output more amenable to efficient use of rsync.  For example, when a
+  large input file gets a small change, a gzip --rsyncable image of
+  that file will remain largely unchanged, too.  Without --rsyncable,
+  even a tiny change in the input could result in a totally different
+  gzip-compressed output file.
+
 ** Bug fixes
 
   gzip -k -v no longer reports that files are replaced.
index 308dc5edaebb2cb303a3173f966a2c45148803e1..25acaacc87ff7c87458cc1758ab15e82a03af4a8 100644 (file)
@@ -62,6 +62,7 @@ unistd-safer
 unlinkat
 update-copyright
 utimens
+verify
 xalloc
 yesno
 '
index 020810d6e9edf70e7da57297460955ecd6cf9d34..6e235f3d182b01360e3eceb1b83823a0b53ef492 100644 (file)
--- a/deflate.c
+++ b/deflate.c
@@ -80,6 +80,7 @@
 #include "tailor.h"
 #include "gzip.h"
 #include "lzw.h" /* just for consistency checking */
+#include "verify.h"
 
 /* ===========================================================================
  * Configuration parameters
 #endif
 /* Matches of length 3 are discarded if their distance exceeds TOO_FAR */
 
+#ifndef RSYNC_WIN
+#  define RSYNC_WIN 4096
+#endif
+verify(RSYNC_WIN < MAX_DIST);
+
+#define RSYNC_SUM_MATCH(sum) ((sum) % RSYNC_WIN == 0)
+/* Whether window sum matches magic value */
+
 /* ===========================================================================
  * Local data used by the "longest match" routines.
  */
@@ -212,6 +221,8 @@ local int compr_level;
 unsigned good_match;
 /* Use a faster search when the previous match is longer than this */
 
+local ulg rsync_sum;  /* rolling sum of rsync window */
+local ulg rsync_chunk_end; /* next rsync sequence point */
 
 /* Values for max_lazy_match, good_match and max_chain_length, depending on
  * the desired pack level (0..9). The values given below have been tuned to
@@ -314,6 +325,10 @@ void lm_init (pack_level, flags)
 #endif
     /* prev will be initialized on the fly */
 
+    /* rsync params */
+    rsync_chunk_end = 0xFFFFFFFFUL;
+    rsync_sum = 0;
+
     /* Set the default configuration parameters:
      */
     max_lazy_match   = configuration_table[pack_level].max_lazy;
@@ -550,6 +565,8 @@ local void fill_window()
         memcpy((char*)window, (char*)window+WSIZE, (unsigned)WSIZE);
         match_start -= WSIZE;
         strstart    -= WSIZE; /* we now have strstart >= MAX_DIST: */
+        if (rsync_chunk_end != 0xFFFFFFFFUL)
+            rsync_chunk_end -= WSIZE;
 
         block_start -= (long) WSIZE;
 
@@ -579,13 +596,47 @@ local void fill_window()
     }
 }
 
+/* With an initial offset of START, advance rsync's rolling checksum
+   by NUM bytes.  */
+local void rsync_roll(unsigned int start, unsigned int num)
+{
+    unsigned i;
+
+    if (start < RSYNC_WIN) {
+        /* before window fills. */
+        for (i = start; i < RSYNC_WIN; i++) {
+            if (i == start + num)
+                return;
+            rsync_sum += (ulg)window[i];
+        }
+        num -= (RSYNC_WIN - start);
+        start = RSYNC_WIN;
+    }
+
+    /* buffer after window full */
+    for (i = start; i < start+num; i++) {
+        /* New character in */
+        rsync_sum += (ulg)window[i];
+        /* Old character out */
+        rsync_sum -= (ulg)window[i - RSYNC_WIN];
+        if (rsync_chunk_end == 0xFFFFFFFFUL && RSYNC_SUM_MATCH(rsync_sum))
+            rsync_chunk_end = i;
+    }
+}
+
+/* ===========================================================================
+ * Set rsync_chunk_end if window sum matches magic value.
+ */
+#define RSYNC_ROLL(s, n) \
+   do { if (rsync) rsync_roll((s), (n)); } while(0)
+
 /* ===========================================================================
  * Flush the current block, with given end-of-file flag.
  * IN assertion: strstart is set to the end of the current match.
  */
 #define FLUSH_BLOCK(eof) \
    flush_block(block_start >= 0L ? (char*)&window[(unsigned)block_start] : \
-                (char*)NULL, (long)strstart - block_start, (eof))
+                (char*)NULL, (long)strstart - block_start, flush-1, (eof))
 
 /* ===========================================================================
  * Processes a new input file and return its compressed length. This
@@ -596,7 +647,7 @@ local void fill_window()
 local off_t deflate_fast()
 {
     IPos hash_head; /* head of the hash chain */
-    int flush;      /* set if current block must be flushed */
+    int flush = 0;  /* set if current block must be flushed, 2=>and padded  */
     unsigned match_length = 0;  /* length of best match */
 
     prev_length = MIN_MATCH-1;
@@ -626,6 +677,7 @@ local off_t deflate_fast()
 
             lookahead -= match_length;
 
+            RSYNC_ROLL(strstart, match_length);
             /* Insert new strings in the hash table only if the match length
              * is not too large. This saves time but degrades compression.
              */
@@ -654,9 +706,14 @@ local off_t deflate_fast()
             /* No match, output a literal byte */
             Tracevv((stderr,"%c",window[strstart]));
             flush = ct_tally (0, window[strstart]);
+            RSYNC_ROLL(strstart, 1);
             lookahead--;
             strstart++;
         }
+        if (rsync && strstart > rsync_chunk_end) {
+            rsync_chunk_end = 0xFFFFFFFFUL;
+            flush = 2;
+        }
         if (flush) FLUSH_BLOCK(0), block_start = strstart;
 
         /* Make sure that we always have enough lookahead, except
@@ -679,7 +736,7 @@ off_t deflate()
 {
     IPos hash_head;          /* head of hash chain */
     IPos prev_match;         /* previous match */
-    int flush;               /* set if current block must be flushed */
+    int flush = 0;           /* set if current block must be flushed */
     int match_available = 0; /* set if previous match exists */
     register unsigned match_length = MIN_MATCH-1; /* length of best match */
 
@@ -730,6 +787,7 @@ off_t deflate()
              */
             lookahead -= prev_length-1;
             prev_length -= 2;
+            RSYNC_ROLL(strstart, prev_length+1);
             do {
                 strstart++;
                 INSERT_STRING(strstart, hash_head);
@@ -742,24 +800,40 @@ off_t deflate()
             match_available = 0;
             match_length = MIN_MATCH-1;
             strstart++;
-            if (flush) FLUSH_BLOCK(0), block_start = strstart;
 
+            if (rsync && strstart > rsync_chunk_end) {
+                rsync_chunk_end = 0xFFFFFFFFUL;
+                flush = 2;
+            }
+            if (flush) FLUSH_BLOCK(0), block_start = strstart;
         } else if (match_available) {
             /* If there was no match at the previous position, output a
              * single literal. If there was a match but the current match
              * is longer, truncate the previous match to a single literal.
              */
             Tracevv((stderr,"%c",window[strstart-1]));
-            if (ct_tally (0, window[strstart-1])) {
-                FLUSH_BLOCK(0), block_start = strstart;
+            flush = ct_tally (0, window[strstart-1]);
+            if (rsync && strstart > rsync_chunk_end) {
+                rsync_chunk_end = 0xFFFFFFFFUL;
+                flush = 2;
             }
+            if (flush) FLUSH_BLOCK(0), block_start = strstart;
+            RSYNC_ROLL(strstart, 1);
             strstart++;
             lookahead--;
         } else {
             /* There is no previous match to compare with, wait for
              * the next step to decide.
              */
+            if (rsync && strstart > rsync_chunk_end) {
+                /* Reset huffman tree */
+                rsync_chunk_end = 0xFFFFFFFFUL;
+                flush = 2;
+                FLUSH_BLOCK(0), block_start = strstart;
+            }
+
             match_available = 1;
+            RSYNC_ROLL(strstart, 1);
             strstart++;
             lookahead--;
         }
index fa94b84ae53fd0336b85b367ca4f11f99123dadc..2721e9d3640d2414c539432726bb40011a30adf0 100644 (file)
@@ -360,6 +360,13 @@ specified on the command line are directories, @command{gzip} will descend
 into the directory and compress all the files it finds there (or
 decompress them in the case of @command{gunzip}).
 
+@item --rsyncable
+Cater better to the @command{rsync} program by periodically resetting
+the internal structure of the compressed data stream.  This lets the
+@code{rsync} program take advantage of similarities in the uncompressed
+input when synchronizing two files compressed with this flag.  The cost:
+the compressed output is usually about one percent larger.
+
 @item --suffix @var{suf}
 @itemx -S @var{suf}
 Use suffix @var{suf} instead of @samp{.gz}.  Any suffix can be
diff --git a/gzip.1 b/gzip.1
index 3262a8788507450cb484bf2be9aeff73576cb2e1..e6aa278cd2282fd8c4e22d2eee35437f45e5e833 100644 (file)
--- a/gzip.1
+++ b/gzip.1
@@ -328,6 +328,11 @@ indicates the slowest compression method (best compression).
 The default compression level is
 .BR \-6
 (that is, biased towards high compression at expense of speed).
+.TP
+.B \-\-rsyncable
+When you synchronize a compressed file between two computers, this option allows rsync to transfer only files that were changed in the archive instead of the entire archive.
+Normally, after a change is made to any file in the archive, the compression algorithm can generate a new version of the archive that does not match the previous version of the archive. In this case, rsync transfers the entire new version of the archive to the remote computer.
+With this option, rsync can transfer only the changed files as well as a small amount of metadata that is required to update the archive structure in the area that was changed.
 .SH "ADVANCED USAGE"
 Multiple compressed files can be concatenated. In this case,
 .I gunzip
diff --git a/gzip.c b/gzip.c
index d9cdfaa5ac51df572db2d3c06e261355fde66e39..e243f2edf357b57132fe9fa7bf4163f2f51be94c 100644 (file)
--- a/gzip.c
+++ b/gzip.c
@@ -220,6 +220,7 @@ static int dfd = -1;       /* output directory file descriptor */
 unsigned insize;           /* valid bytes in inbuf */
 unsigned inptr;            /* index of next byte to be processed in inbuf */
 unsigned outcnt;           /* bytes in output buffer */
+int rsync = 0;             /* make ryncable chunks */
 
 static int handled_sig[] =
   {
@@ -248,6 +249,7 @@ static int handled_sig[] =
 enum
 {
   PRESUME_INPUT_TTY_OPTION = CHAR_MAX + 1,
+  RSYNCABLE_OPTION,
   SYNCHRONOUS_OPTION,
 
   /* A value greater than all valid long options, used as a flag to
@@ -288,7 +290,7 @@ static const struct option longopts[] =
     {"best",       0, 0, '9'}, /* compress better */
     {"lzw",        0, 0, 'Z'}, /* make output compatible with old compress */
     {"bits",       1, 0, 'b'}, /* max number of bits per code (implies -Z) */
-
+    {"rsyncable",  0, 0, RSYNCABLE_OPTION}, /* make rsync-friendly archive */
     { 0, 0, 0, 0 }
 };
 
@@ -373,6 +375,7 @@ local void help()
  "  -Z, --lzw         produce output compatible with old compress",
  "  -b, --bits=BITS   max number of bits per code (implies -Z)",
 #endif
+ "      --rsyncable   Make rsync-friendly archive",
  "",
  "With no FILE, or when FILE is -, read standard input.",
  "",
@@ -555,6 +558,10 @@ int main (int argc, char **argv)
             recursive = 1;
 #endif
             break;
+
+        case RSYNCABLE_OPTION:
+            rsync = 1;
+            break;
         case 'S':
 #ifdef NO_MULTIPLE_DOTS
             if (*optarg == '.') optarg++;
diff --git a/gzip.h b/gzip.h
index bb4000abce89e0a316b67dab22f80097062562e2..f298b47345a3c22c64385107607475071079bed6 100644 (file)
--- a/gzip.h
+++ b/gzip.h
@@ -140,6 +140,7 @@ EXTERN(uch, window);         /* Sliding window and suffix table (unlzw) */
 extern unsigned insize; /* valid bytes in inbuf */
 extern unsigned inptr;  /* index of next byte to be processed in inbuf */
 extern unsigned outcnt; /* bytes in output buffer */
+extern int rsync;  /* deflate into rsyncable chunks */
 
 extern off_t bytes_in;   /* number of input bytes */
 extern off_t bytes_out;  /* number of output bytes */
@@ -287,7 +288,7 @@ extern off_t deflate (void);
         /* in trees.c */
 extern void ct_init     (ush *attr, int *method);
 extern int  ct_tally    (int dist, int lc);
-extern off_t flush_block (char *buf, ulg stored_len, int eof);
+extern off_t flush_block (char *buf, ulg stored_len, int pad, int eof);
 
         /* in bits.c */
 extern void     bi_init    (file_t zipfile);
diff --git a/trees.c b/trees.c
index f314e574639125a77b5f7f243848bfec441edc44..025d5ba2623c9ab82a6f6dea312195023416f13d 100644 (file)
--- a/trees.c
+++ b/trees.c
@@ -856,9 +856,10 @@ local void send_all_trees(lcodes, dcodes, blcodes)
  * trees or store, and output the encoded block to the zip file. This function
  * returns the total compressed length for the file so far.
  */
-off_t flush_block(buf, stored_len, eof)
+off_t flush_block(buf, stored_len, pad, eof)
     char *buf;        /* input block, or NULL if too old */
     ulg stored_len;   /* length of input block */
+    int pad;          /* pad output to byte boundary */
     int eof;          /* true if this is the last block for a file */
 {
     ulg opt_lenb, static_lenb; /* opt_len and static_len in bytes */
@@ -951,6 +952,10 @@ off_t flush_block(buf, stored_len, eof)
         Assert (input_len == bytes_in, "bad input size");
         bi_windup();
         compressed_len += 7;  /* align on byte boundary */
+    } else if (pad && (compressed_len % 8) != 0) {
+        send_bits((STORED_BLOCK<<1)+eof, 3);  /* send block type */
+        compressed_len = (compressed_len + 3 + 7) & ~7L;
+        copy_block(buf, 0, 1); /* with header */
     }
 
     return compressed_len >> 3;