fossilize 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ coverage
6
+ InstalledFiles
7
+ lib/bundler/man
8
+ pkg
9
+ rdoc
10
+ spec/reports
11
+ test/tmp
12
+ test/version_tmp
13
+ tmp
14
+
15
+ # YARD artifacts
16
+ .yardoc
17
+ _yardoc
18
+ doc/
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in fossilize.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Mark Anthony Gibbins
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,105 @@
1
+ # Fossilize
2
+
3
+ Fossilize is an FFI-powered C-extension for Ruby that interfaces with the delta encoding algorithm
4
+ created by D. Richard Hipp for the [FOSSIL SCM project][fossil]. It enables a Ruby program to quickly (and I mean quickly) generate a delta between files and strings, as well as apply those deltas.
5
+
6
+ Deltas can be created between a Ruby File object and a String and vice-versa, so you can read in some JSON from a remote server as a String, create a delta from your local File copy and then apply that delta to your local copy to merge the differences.
7
+
8
+ **The project is currently considered a work-in-progress.**
9
+
10
+ [fossil]: http://www.fossil-scm.org
11
+
12
+ ## Why use Fossilize?
13
+
14
+ The algorithm itself is based on rsync and is a form of [Delta Encoding][de] (sometimes called Delta Compression). A delta encoding algorithm is designed to analyse two pieces of data and produce a delta (the differences between them) as an encoded string.
15
+
16
+ Here, I'll give an example. If I give the following two strings to the algorithm:
17
+
18
+ xiy needs to get a job!
19
+ maybe xiy needs to get a real job!
20
+
21
+ It spits out the following *delta string* (I sense sarcasm in its tone):
22
+
23
+ _
24
+ 6:maybe J@0,B:*real* job!1rx1Az;
25
+
26
+ Although explaining the format of the delta string is out of the scope of this internets page, you can see why this algorithm is so damn cool.
27
+
28
+ For more info on the algorithm, see the excellent documentation over [here][delta-format].
29
+
30
+ Git uses a similar algorithm to only store the changes to tracked files between revisions. However, the deltas created by Git can sometimes be huge.
31
+
32
+ ### Real World Examples
33
+
34
+ As a real world example, here are the differences between `ruby/ruby/array.c@e3efce` and it's previous commit (you can see the diff [here](https://github.com/ruby/ruby/commit/e3efce6df1aa691e17c59f442b35b4fd129d3a13#array.c)).
35
+
36
+ WBD
37
+ N86@0,g:rb_random_ulong_limited((randgen), (max)-1)U@OvG,8:shuffle!H@OGW,_B@N9S,4:long49@Nii,F:i = RAND_UPTO(lb@NnL,4:<= iK@Nn~,N@Ntk,H: }
38
+ return ptr[i]b@3Rx,33@Npg,H:RAND_UPTO(len - iG@L00,8:k = len;~@NtF,C:len < k) {
39
+ R@O1i,K@DZ~,_:n; ++i) {
40
+ if (rnds[i] >= len) {
41
+ I@F9W,C:new2(0);
42
+ }H@QGF,U@NrC,16@Nu_,7:rnds[0]r@Nv~,L:rnds[0];
43
+ j = rnds[1]1F@Nxf,Z:rnds[0];
44
+ j = rnds[1];
45
+ k = rnds[2]40@N~E,I:rnds[0];
46
+ for (i=1J@OBW,B:k = rnds[i]86X@O4S,32IfMm;
47
+
48
+ In terms of diffing, it wouldn't be hard to parse this delta and determine where in a file modifications took place at a *per-character level* as opposed to the traditional per-line approach.
49
+
50
+ ### Things Fossilize is good at:
51
+
52
+ 1. Patching - the only data to transmit is the delta string.
53
+ 2. Diffing - although the format isn't human readable, it wouldn't be hard to make it so. Unlike *diff*, the algorithm compares by character, not by line.
54
+ 3. Syncing - my project `wormhole` uses this algorithm to ensure it only syncs the updated portions of my files, not the entire thing.
55
+
56
+ ### Things Fossilize is *not* good at:
57
+
58
+ 1. Comparing differences between two almost completely different pieces of data (see [Wikipedia][de]).
59
+ 2. Binary diffing - while it can successfully create and apply binary patches, there are algorithms and tools better designed for these types of files. `bsdiff` will create a 2kb diff whereas Fossilize will create a 4kb diff. This is probably due to the fact that the algorithm uses base64 encoding of plain-text which ends up with binary artefacts popping up in the diff. Although they don't make it into the output, it's obviously better to use something like bsdiff. It would however be possible to modify the algorithm to use a different "mode" for binary files that uses binary encoding instead.
60
+
61
+
62
+ [de]: http://en.wikipedia.org/wiki/Delta_encoding
63
+ [delta-format]: http://www.fossil-scm.org/xfer/doc/trunk/www/delta_format.wiki
64
+
65
+ ## Installation
66
+
67
+ Add this line to your application's Gemfile:
68
+
69
+ gem 'fossilize'
70
+
71
+ And then execute:
72
+
73
+ $ bundle install
74
+
75
+ Or install it yourself as:
76
+
77
+ $ gem install fossilize
78
+
79
+ ## Licensing
80
+ Fossilized is distributed under the MIT License.
81
+
82
+ Fossil (and the Fossil delta encoding algorithm included within) are distributed under the Simplified BSD License/FreeBSD License:
83
+
84
+
85
+ Copyright (c) 2006 D. Richard Hipp
86
+
87
+ This program is free software; you can redistribute it and/or
88
+ modify it under the terms of the Simplified BSD License (also
89
+ known as the "2-Clause License" or "FreeBSD License".)
90
+
91
+ This program is distributed in the hope that it will be useful,
92
+ but without any warranty; without even the implied warranty of
93
+ merchantability or fitness for a particular purpose.
94
+
95
+ Author contact information:
96
+ drh@hwaci.com
97
+ http://www.hwaci.com/drh/
98
+
99
+ ## Contributing
100
+
101
+ 1. Fork it
102
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
103
+ 3. Commit your changes (`git commit -am 'Added some feature'`)
104
+ 4. Push to the branch (`git push origin my-new-feature`)
105
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
data/bin/fossilize ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "fossilize"
4
+
5
+ delta = Fossilize.create(ARGV[0], ARGV[1])
6
+ output = Fossilize.apply(ARGV[0], delta)
7
+
8
+ File.write("fossilized_#{ARGV[0]}", output)
9
+
@@ -0,0 +1,7 @@
1
+ require 'mkmf'
2
+
3
+ dir_config 'fossilize'
4
+ RbConfig::MAKEFILE_CONFIG['CC'] = 'gcc'
5
+ RbConfig::MAKEFILE_CONFIG['CXX'] = 'g++'
6
+
7
+ create_makefile 'fossilize/fossilize'
@@ -0,0 +1,626 @@
1
+ /*
2
+ ** Copyright (c) 2006 D. Richard Hipp
3
+ **
4
+ ** This program is free software; you can redistribute it and/or
5
+ ** modify it under the terms of the Simplified BSD License (also
6
+ ** known as the "2-Clause License" or "FreeBSD License".)
7
+
8
+ ** This program is distributed in the hope that it will be useful,
9
+ ** but without any warranty; without even the implied warranty of
10
+ ** merchantability or fitness for a particular purpose.
11
+ **
12
+ ** Author contact information:
13
+ ** drh@hwaci.com
14
+ ** http://www.hwaci.com/drh/
15
+ **
16
+ *******************************************************************************
17
+ **
18
+ ** This module implements the delta compress algorithm.
19
+ **
20
+ ** Though developed specifically for fossil, the code in this file
21
+ ** is generally appliable and is thus easily separated from the
22
+ ** fossil source code base. Nothing in this file depends on anything
23
+ ** else in fossil.
24
+ */
25
+ #include <stdio.h>
26
+ #include <assert.h>
27
+ #include <stdlib.h>
28
+ #include <string.h>
29
+
30
+ /*
31
+ ** Macros for turning debugging printfs on and off
32
+ */
33
+ #if 0
34
+ # define DEBUG1(X) X
35
+ #else
36
+ # define DEBUG1(X)
37
+ #endif
38
+ #if 0
39
+ #define DEBUG2(X) X
40
+ /*
41
+ ** For debugging:
42
+ ** Print 16 characters of text from zBuf
43
+ */
44
+ static const char *print16(const char *z){
45
+ int i;
46
+ static char zBuf[20];
47
+ for(i=0; i<16; i++){
48
+ if( z[i]>=0x20 && z[i]<=0x7e ){
49
+ zBuf[i] = z[i];
50
+ }else{
51
+ zBuf[i] = '.';
52
+ }
53
+ }
54
+ zBuf[i] = 0;
55
+ return zBuf;
56
+ }
57
+ #else
58
+ # define DEBUG2(X)
59
+ #endif
60
+
61
+ // #if INTERFACE
62
+ /*
63
+ ** The "u32" type must be an unsigned 32-bit integer. Adjust this
64
+ */
65
+ typedef unsigned int u32;
66
+
67
+ /*
68
+ ** Must be a 16-bit value
69
+ */
70
+ typedef short int s16;
71
+ typedef unsigned short int u16;
72
+
73
+ // #endif /* INTERFACE */
74
+
75
+ /*
76
+ ** The width of a hash window in bytes. The algorithm only works if this
77
+ ** is a power of 2.
78
+ */
79
+ #define NHASH 16
80
+
81
+ /*
82
+ ** The current state of the rolling hash.
83
+ **
84
+ ** z[] holds the values that have been hashed. z[] is a circular buffer.
85
+ ** z[i] is the first entry and z[(i+NHASH-1)%NHASH] is the last entry of
86
+ ** the window.
87
+ **
88
+ ** Hash.a is the sum of all elements of hash.z[]. Hash.b is a weighted
89
+ ** sum. Hash.b is z[i]*NHASH + z[i+1]*(NHASH-1) + ... + z[i+NHASH-1]*1.
90
+ ** (Each index for z[] should be module NHASH, of course. The %NHASH operator
91
+ ** is omitted in the prior expression for brevity.)
92
+ */
93
+ typedef struct hash hash;
94
+ struct hash {
95
+ u16 a, b; /* Hash values */
96
+ u16 i; /* Start of the hash window */
97
+ char z[NHASH]; /* The values that have been hashed */
98
+ };
99
+
100
+ /*
101
+ ** Malloc and free routines that cannot fail
102
+ */
103
+ void *fossil_malloc(size_t n){
104
+ void *p = malloc(n==0 ? 1 : n);
105
+ if( p==0 ) printf("out of memory");
106
+ return p;
107
+ }
108
+ void fossil_free(void *p){
109
+ free(p);
110
+ }
111
+ void *fossil_realloc(void *p, size_t n){
112
+ p = realloc(p, n);
113
+ if( p==0 ) printf("out of memory");
114
+ return p;
115
+ }
116
+
117
+ /*
118
+ ** Initialize the rolling hash using the first NHASH characters of z[]
119
+ */
120
+ static void hash_init(hash *pHash, const char *z){
121
+ u16 a, b, i;
122
+ a = b = 0;
123
+ for(i=0; i<NHASH; i++){
124
+ a += z[i];
125
+ b += (NHASH-i)*z[i];
126
+ pHash->z[i] = z[i];
127
+ }
128
+ pHash->a = a & 0xffff;
129
+ pHash->b = b & 0xffff;
130
+ pHash->i = 0;
131
+ }
132
+
133
+ /*
134
+ ** Advance the rolling hash by a single character "c"
135
+ */
136
+ static void hash_next(hash *pHash, int c){
137
+ u16 old = pHash->z[pHash->i];
138
+ pHash->z[pHash->i] = c;
139
+ pHash->i = (pHash->i+1)&(NHASH-1);
140
+ pHash->a = pHash->a - old + c;
141
+ pHash->b = pHash->b - NHASH*old + pHash->a;
142
+ }
143
+
144
+ /*
145
+ ** Return a 32-bit hash value
146
+ */
147
+ static u32 hash_32bit(hash *pHash){
148
+ return (pHash->a & 0xffff) | (((u32)(pHash->b & 0xffff))<<16);
149
+ }
150
+
151
+ /*
152
+ ** Write an base-64 integer into the given buffer.
153
+ */
154
+ static void putInt(unsigned int v, char **pz){
155
+ static const char zDigits[] =
156
+ "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz~";
157
+ /* 123456789 123456789 123456789 123456789 123456789 123456789 123 */
158
+ int i, j;
159
+ char zBuf[20];
160
+ if( v==0 ){
161
+ *(*pz)++ = '0';
162
+ return;
163
+ }
164
+ for(i=0; v>0; i++, v>>=6){
165
+ zBuf[i] = zDigits[v&0x3f];
166
+ }
167
+ for(j=i-1; j>=0; j--){
168
+ *(*pz)++ = zBuf[j];
169
+ }
170
+ }
171
+
172
+ /*
173
+ ** Read bytes from *pz and convert them into a positive integer. When
174
+ ** finished, leave *pz pointing to the first character past the end of
175
+ ** the integer. The *pLen parameter holds the length of the string
176
+ ** in *pz and is decremented once for each character in the integer.
177
+ */
178
+ static unsigned int getInt(const char **pz, int *pLen){
179
+ static const signed char zValue[] = {
180
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
181
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
182
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
183
+ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, -1, -1, -1, -1, -1, -1,
184
+ -1, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24,
185
+ 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, -1, -1, -1, -1, 36,
186
+ -1, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51,
187
+ 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, -1, -1, -1, 63, -1,
188
+ };
189
+ unsigned int v = 0;
190
+ int c;
191
+ unsigned char *z = (unsigned char*)*pz;
192
+ unsigned char *zStart = z;
193
+ while( (c = zValue[0x7f&*(z++)])>=0 ){
194
+ v = (v<<6) + c;
195
+ }
196
+ z--;
197
+ *pLen -= z - zStart;
198
+ *pz = (char*)z;
199
+ return v;
200
+ }
201
+
202
+ /*
203
+ ** Return the number digits in the base-64 representation of a positive integer
204
+ */
205
+ static int digit_count(int v){
206
+ int i, x;
207
+ for(i=1, x=64; v>=x; i++, x <<= 6){}
208
+ return i;
209
+ }
210
+
211
+ /*
212
+ ** Compute a 32-bit checksum on the N-byte buffer. Return the result.
213
+ */
214
+ static unsigned int checksum(const char *zIn, size_t N){
215
+ const unsigned char *z = (const unsigned char *)zIn;
216
+ unsigned sum0 = 0;
217
+ unsigned sum1 = 0;
218
+ unsigned sum2 = 0;
219
+ unsigned sum3 = 0;
220
+ while(N >= 16){
221
+ sum0 += ((unsigned)z[0] + z[4] + z[8] + z[12]);
222
+ sum1 += ((unsigned)z[1] + z[5] + z[9] + z[13]);
223
+ sum2 += ((unsigned)z[2] + z[6] + z[10]+ z[14]);
224
+ sum3 += ((unsigned)z[3] + z[7] + z[11]+ z[15]);
225
+ z += 16;
226
+ N -= 16;
227
+ }
228
+ while(N >= 4){
229
+ sum0 += z[0];
230
+ sum1 += z[1];
231
+ sum2 += z[2];
232
+ sum3 += z[3];
233
+ z += 4;
234
+ N -= 4;
235
+ }
236
+ sum3 += (sum2 << 8) + (sum1 << 16) + (sum0 << 24);
237
+ switch(N){
238
+ case 3: sum3 += (z[2] << 8);
239
+ case 2: sum3 += (z[1] << 16);
240
+ case 1: sum3 += (z[0] << 24);
241
+ default: ;
242
+ }
243
+ return sum3;
244
+ }
245
+
246
+ /*
247
+ ** Create a new delta.
248
+ **
249
+ ** The delta is written into a preallocated buffer, zDelta, which
250
+ ** should be at least 60 bytes longer than the target file, zOut.
251
+ ** The delta string will be NUL-terminated, but it might also contain
252
+ ** embedded NUL characters if either the zSrc or zOut files are
253
+ ** binary. This function returns the length of the delta string
254
+ ** in bytes, excluding the final NUL terminator character.
255
+ **
256
+ ** Output Format:
257
+ **
258
+ ** The delta begins with a base64 number followed by a newline. This
259
+ ** number is the number of bytes in the TARGET file. Thus, given a
260
+ ** delta file z, a program can compute the size of the output file
261
+ ** simply by reading the first line and decoding the base-64 number
262
+ ** found there. The delta_output_size() routine does exactly this.
263
+ **
264
+ ** After the initial size number, the delta consists of a series of
265
+ ** literal text segments and commands to copy from the SOURCE file.
266
+ ** A copy command looks like this:
267
+ **
268
+ ** NNN@MMM,
269
+ **
270
+ ** where NNN is the number of bytes to be copied and MMM is the offset
271
+ ** into the source file of the first byte (both base-64). If NNN is 0
272
+ ** it means copy the rest of the input file. Literal text is like this:
273
+ **
274
+ ** NNN:TTTTT
275
+ **
276
+ ** where NNN is the number of bytes of text (base-64) and TTTTT is the text.
277
+ **
278
+ ** The last term is of the form
279
+ **
280
+ ** NNN;
281
+ **
282
+ ** In this case, NNN is a 32-bit bigendian checksum of the output file
283
+ ** that can be used to verify that the delta applied correctly. All
284
+ ** numbers are in base-64.
285
+ **
286
+ ** Pure text files generate a pure text delta. Binary files generate a
287
+ ** delta that may contain some binary data.
288
+ **
289
+ ** Algorithm:
290
+ **
291
+ ** The encoder first builds a hash table to help it find matching
292
+ ** patterns in the source file. 16-byte chunks of the source file
293
+ ** sampled at evenly spaced intervals are used to populate the hash
294
+ ** table.
295
+ **
296
+ ** Next we begin scanning the target file using a sliding 16-byte
297
+ ** window. The hash of the 16-byte window in the target is used to
298
+ ** search for a matching section in the source file. When a match
299
+ ** is found, a copy command is added to the delta. An effort is
300
+ ** made to extend the matching section to regions that come before
301
+ ** and after the 16-byte hash window. A copy command is only issued
302
+ ** if the result would use less space that just quoting the text
303
+ ** literally. Literal text is added to the delta for sections that
304
+ ** do not match or which can not be encoded efficiently using copy
305
+ ** commands.
306
+ */
307
+ int delta_create(
308
+ const char *zSrc, /* The source or pattern file */
309
+ unsigned int lenSrc, /* Length of the source file */
310
+ const char *zOut, /* The target file */
311
+ unsigned int lenOut, /* Length of the target file */
312
+ char *zDelta /* Write the delta into this buffer */
313
+ ){
314
+ int i, base;
315
+ char *zOrigDelta = zDelta;
316
+ hash h;
317
+ int nHash; /* Number of hash table entries */
318
+ int *landmark; /* Primary hash table */
319
+ int *collide; /* Collision chain */
320
+ int lastRead = -1; /* Last byte of zSrc read by a COPY command */
321
+
322
+ /* Add the target file size to the beginning of the delta
323
+ */
324
+ putInt(lenOut, &zDelta);
325
+ *(zDelta++) = '\n';
326
+
327
+ /* If the source file is very small, it means that we have no
328
+ ** chance of ever doing a copy command. Just output a single
329
+ ** literal segment for the entire target and exit.
330
+ */
331
+ if( lenSrc<=NHASH ){
332
+ putInt(lenOut, &zDelta);
333
+ *(zDelta++) = ':';
334
+ memcpy(zDelta, zOut, lenOut);
335
+ zDelta += lenOut;
336
+ putInt(checksum(zOut, lenOut), &zDelta);
337
+ *(zDelta++) = ';';
338
+ return zDelta - zOrigDelta;
339
+ }
340
+
341
+ /* Compute the hash table used to locate matching sections in the
342
+ ** source file.
343
+ */
344
+ nHash = lenSrc/NHASH;
345
+ collide = (int*)fossil_malloc( nHash*2*sizeof(int) );
346
+ landmark = &collide[nHash];
347
+ memset(landmark, -1, nHash*sizeof(int));
348
+ memset(collide, -1, nHash*sizeof(int));
349
+ for(i=0; i<lenSrc-NHASH; i+=NHASH){
350
+ int hv;
351
+ hash_init(&h, &zSrc[i]);
352
+ hv = hash_32bit(&h) % nHash;
353
+ collide[i/NHASH] = landmark[hv];
354
+ landmark[hv] = i/NHASH;
355
+ }
356
+
357
+ /* Begin scanning the target file and generating copy commands and
358
+ ** literal sections of the delta.
359
+ */
360
+ base = 0; /* We have already generated everything before zOut[base] */
361
+ while( base+NHASH<lenOut ){
362
+ int iSrc, iBlock;
363
+ unsigned int bestCnt, bestOfst=0, bestLitsz=0;
364
+ hash_init(&h, &zOut[base]);
365
+ i = 0; /* Trying to match a landmark against zOut[base+i] */
366
+ bestCnt = 0;
367
+ while( 1 ){
368
+ int hv;
369
+ int limit = 250;
370
+
371
+ hv = hash_32bit(&h) % nHash;
372
+ DEBUG2( printf("LOOKING: %4d [%s]\n", base+i, print16(&zOut[base+i])); )
373
+ iBlock = landmark[hv];
374
+ while( iBlock>=0 && (limit--)>0 ){
375
+ /*
376
+ ** The hash window has identified a potential match against
377
+ ** landmark block iBlock. But we need to investigate further.
378
+ **
379
+ ** Look for a region in zOut that matches zSrc. Anchor the search
380
+ ** at zSrc[iSrc] and zOut[base+i]. Do not include anything prior to
381
+ ** zOut[base] or after zOut[outLen] nor anything after zSrc[srcLen].
382
+ **
383
+ ** Set cnt equal to the length of the match and set ofst so that
384
+ ** zSrc[ofst] is the first element of the match. litsz is the number
385
+ ** of characters between zOut[base] and the beginning of the match.
386
+ ** sz will be the overhead (in bytes) needed to encode the copy
387
+ ** command. Only generate copy command if the overhead of the
388
+ ** copy command is less than the amount of literal text to be copied.
389
+ */
390
+ int cnt, ofst, litsz;
391
+ int j, k, x, y;
392
+ int sz;
393
+
394
+ /* Beginning at iSrc, match forwards as far as we can. j counts
395
+ ** the number of characters that match */
396
+ iSrc = iBlock*NHASH;
397
+ for(j=0, x=iSrc, y=base+i; x<lenSrc && y<lenOut; j++, x++, y++){
398
+ if( zSrc[x]!=zOut[y] ) break;
399
+ }
400
+ j--;
401
+
402
+ /* Beginning at iSrc-1, match backwards as far as we can. k counts
403
+ ** the number of characters that match */
404
+ for(k=1; k<iSrc && k<=i; k++){
405
+ if( zSrc[iSrc-k]!=zOut[base+i-k] ) break;
406
+ }
407
+ k--;
408
+
409
+ /* Compute the offset and size of the matching region */
410
+ ofst = iSrc-k;
411
+ cnt = j+k+1;
412
+ litsz = i-k; /* Number of bytes of literal text before the copy */
413
+ DEBUG2( printf("MATCH %d bytes at %d: [%s] litsz=%d\n",
414
+ cnt, ofst, print16(&zSrc[ofst]), litsz); )
415
+ /* sz will hold the number of bytes needed to encode the "insert"
416
+ ** command and the copy command, not counting the "insert" text */
417
+ sz = digit_count(i-k)+digit_count(cnt)+digit_count(ofst)+3;
418
+ if( cnt>=sz && cnt>bestCnt ){
419
+ /* Remember this match only if it is the best so far and it
420
+ ** does not increase the file size */
421
+ bestCnt = cnt;
422
+ bestOfst = iSrc-k;
423
+ bestLitsz = litsz;
424
+ DEBUG2( printf("... BEST SO FAR\n"); )
425
+ }
426
+
427
+ /* Check the next matching block */
428
+ iBlock = collide[iBlock];
429
+ }
430
+
431
+ /* We have a copy command that does not cause the delta to be larger
432
+ ** than a literal insert. So add the copy command to the delta.
433
+ */
434
+ if( bestCnt>0 ){
435
+ if( bestLitsz>0 ){
436
+ /* Add an insert command before the copy */
437
+ putInt(bestLitsz,&zDelta);
438
+ *(zDelta++) = ':';
439
+ memcpy(zDelta, &zOut[base], bestLitsz);
440
+ zDelta += bestLitsz;
441
+ base += bestLitsz;
442
+ DEBUG2( printf("insert %d\n", bestLitsz); )
443
+ }
444
+ base += bestCnt;
445
+ putInt(bestCnt, &zDelta);
446
+ *(zDelta++) = '@';
447
+ putInt(bestOfst, &zDelta);
448
+ DEBUG2( printf("copy %d bytes from %d\n", bestCnt, bestOfst); )
449
+ *(zDelta++) = ',';
450
+ if( bestOfst + bestCnt -1 > lastRead ){
451
+ lastRead = bestOfst + bestCnt - 1;
452
+ DEBUG2( printf("lastRead becomes %d\n", lastRead); )
453
+ }
454
+ bestCnt = 0;
455
+ break;
456
+ }
457
+
458
+ /* If we reach this point, it means no match is found so far */
459
+ if( base+i+NHASH>=lenOut ){
460
+ /* We have reached the end of the file and have not found any
461
+ ** matches. Do an "insert" for everything that does not match */
462
+ putInt(lenOut-base, &zDelta);
463
+ *(zDelta++) = ':';
464
+ memcpy(zDelta, &zOut[base], lenOut-base);
465
+ zDelta += lenOut-base;
466
+ base = lenOut;
467
+ break;
468
+ }
469
+
470
+ /* Advance the hash by one character. Keep looking for a match */
471
+ hash_next(&h, zOut[base+i+NHASH]);
472
+ i++;
473
+ }
474
+ }
475
+ /* Output a final "insert" record to get all the text at the end of
476
+ ** the file that does not match anything in the source file.
477
+ */
478
+ if( base<lenOut ){
479
+ putInt(lenOut-base, &zDelta);
480
+ *(zDelta++) = ':';
481
+ memcpy(zDelta, &zOut[base], lenOut-base);
482
+ zDelta += lenOut-base;
483
+ }
484
+ /* Output the final checksum record. */
485
+ putInt(checksum(zOut, lenOut), &zDelta);
486
+ *(zDelta++) = ';';
487
+ free(collide);
488
+ return zDelta - zOrigDelta;
489
+ }
490
+
491
+ /*
492
+ ** Return the size (in bytes) of the output from applying
493
+ ** a delta.
494
+ **
495
+ ** This routine is provided so that an procedure that is able
496
+ ** to call delta_apply() can learn how much space is required
497
+ ** for the output and hence allocate nor more space that is really
498
+ ** needed.
499
+ */
500
+ int delta_output_size(const char *zDelta, int lenDelta){
501
+ int size;
502
+ size = getInt(&zDelta, &lenDelta);
503
+ if( *zDelta!='\n' ){
504
+ /* ERROR: size integer not terminated by "\n" */
505
+ return -1;
506
+ }
507
+ return size;
508
+ }
509
+
510
+
511
+ /*
512
+ ** Apply a delta.
513
+ **
514
+ ** The output buffer should be big enough to hold the whole output
515
+ ** file and a NUL terminator at the end. The delta_output_size()
516
+ ** routine will determine this size for you.
517
+ **
518
+ ** The delta string should be null-terminated. But the delta string
519
+ ** may contain embedded NUL characters (if the input and output are
520
+ ** binary files) so we also have to pass in the length of the delta in
521
+ ** the lenDelta parameter.
522
+ **
523
+ ** This function returns the size of the output file in bytes (excluding
524
+ ** the final NUL terminator character). Except, if the delta string is
525
+ ** malformed or intended for use with a source file other than zSrc,
526
+ ** then this routine returns -1.
527
+ **
528
+ ** Refer to the delta_create() documentation above for a description
529
+ ** of the delta file format.
530
+ */
531
+ int delta_apply(
532
+ const char *zSrc, /* The source or pattern file */
533
+ int lenSrc, /* Length of the source file */
534
+ const char *zDelta, /* Delta to apply to the pattern */
535
+ int lenDelta, /* Length of the delta */
536
+ char *zOut /* Write the output into this preallocated buffer */
537
+ ){
538
+ unsigned int limit;
539
+ unsigned int total = 0;
540
+ #ifndef FOSSIL_OMIT_DELTA_CKSUM_TEST
541
+ char *zOrigOut = zOut;
542
+ #endif
543
+
544
+ limit = getInt(&zDelta, &lenDelta);
545
+ if( *zDelta!='\n' ){
546
+ /* ERROR: size integer not terminated by "\n" */
547
+ printf("ERROR: size integer not terminated by ""\/n""\n");
548
+ return -1;
549
+ }
550
+ zDelta++; lenDelta--;
551
+ while( *zDelta && lenDelta>0 ){
552
+ unsigned int cnt, ofst;
553
+ cnt = getInt(&zDelta, &lenDelta);
554
+ switch( zDelta[0] ){
555
+ case '@': {
556
+ zDelta++; lenDelta--;
557
+ ofst = getInt(&zDelta, &lenDelta);
558
+ if( lenDelta>0 && zDelta[0]!=',' ){
559
+ /* ERROR: copy command not terminated by ',' */
560
+ printf("ERROR: copy command not terminated by ','\n");
561
+ return -1;
562
+ }
563
+ zDelta++; lenDelta--;
564
+ DEBUG1( printf("COPY %d from %d\n", cnt, ofst); )
565
+ total += cnt;
566
+ if( total>limit ){
567
+ /* ERROR: copy exceeds output file size */
568
+ printf("ERROR: copy exceeds output file size\n");
569
+ return -1;
570
+ }
571
+ if( ofst+cnt > lenSrc ){
572
+ /* ERROR: copy extends past end of input */
573
+ printf("ERROR: copy extends past end of input: ofst: %d, cnt: %d, lenSrc: %d \n", ofst, cnt, lenSrc);
574
+ return -1;
575
+ }
576
+ memcpy(zOut, &zSrc[ofst], cnt);
577
+ zOut += cnt;
578
+ break;
579
+ }
580
+ case ':': {
581
+ zDelta++; lenDelta--;
582
+ total += cnt;
583
+ if( total>limit ){
584
+ /* ERROR: insert command gives an output larger than predicted */
585
+ printf("ERROR: insert command gives an output larger than predicted\n");
586
+ return -1;
587
+ }
588
+ DEBUG1( printf("INSERT %d\n", cnt); )
589
+ if( cnt>lenDelta ){
590
+ /* ERROR: insert count exceeds size of delta */
591
+ printf("ERROR: insert count exceeds size of delta\n");
592
+ return -1;
593
+ }
594
+ memcpy(zOut, zDelta, cnt);
595
+ zOut += cnt;
596
+ zDelta += cnt;
597
+ lenDelta -= cnt;
598
+ break;
599
+ }
600
+ case ';': {
601
+ zDelta++; lenDelta--;
602
+ zOut[0] = 0;
603
+ #ifndef FOSSIL_OMIT_DELTA_CKSUM_TEST
604
+ if( cnt!=checksum(zOrigOut, total) ){
605
+ /* ERROR: bad checksum */
606
+ return -1;
607
+ }
608
+ #endif
609
+ if( total!=limit ){
610
+ /* ERROR: generated size does not match predicted size */
611
+ printf("ERROR: generated size does not match predicted size\n");
612
+ return -1;
613
+ }
614
+ return total;
615
+ }
616
+ default: {
617
+ /* ERROR: unknown delta operator */
618
+ printf("ERROR: unknown delta operator\n");
619
+ return -1;
620
+ }
621
+ }
622
+ }
623
+ /* ERROR: unterminated delta */
624
+ printf("ERROR: unterminated delta\n");
625
+ return -1;
626
+ }
data/fossilize.gemspec ADDED
@@ -0,0 +1,27 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/fossilize/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Mark Anthony Gibbins"]
6
+ gem.email = ["xiy3x0@gmail.com"]
7
+ gem.description = %q{A ruby extension to the Fossil delta compression algorithm written
8
+ by D. Richard Hipp for the Fossil SCM project.}
9
+ gem.summary = %q{Delta compression for Ruby using the Fossil delta
10
+ compression algorithm.}
11
+ gem.homepage = "http://github.com/xiy/fossilize"
12
+
13
+ gem.files = `git ls-files`.split($\)
14
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
15
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
16
+ gem.name = "fossilize"
17
+ gem.require_paths = ["lib"]
18
+ gem.version = Fossilize::VERSION
19
+
20
+ gem.extensions = ['ext/fossilize/extconf.rb']
21
+
22
+ gem.add_runtime_dependency('ffi')
23
+ gem.add_runtime_dependency('digest-crc')
24
+
25
+ gem.add_development_dependency('rspec', '2.8.0')
26
+ gem.add_development_dependency('tomdoc')
27
+ end
@@ -0,0 +1,13 @@
1
+ require 'digest-crc/crc32'
2
+
3
+ module Fossilize
4
+ module Delta
5
+ attr_accessor :origin, :target, :chunks
6
+
7
+ NHASH = 16
8
+
9
+ def self.process_origin(origin)
10
+
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,27 @@
1
+ module Fossilize
2
+ class RingBuffer < Array
3
+
4
+ alias_method :array_push, :push
5
+ alias_method :array_element, :[]
6
+
7
+ def initialize(size)
8
+ @ring_size = size
9
+ super(size)
10
+ end
11
+
12
+ def push(element)
13
+ if length == @ring_size
14
+ shift # loose element
15
+ end
16
+ array_push element
17
+ end
18
+
19
+ # Access elements in the RingBuffer
20
+ #
21
+ # offset will be typically negative!
22
+ #
23
+ def [](offset = 0)
24
+ return self.array_element(- 1 + offset)
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,3 @@
1
+ module Fossilize
2
+ VERSION = "1.0.0"
3
+ end
data/lib/fossilize.rb ADDED
@@ -0,0 +1,161 @@
1
+ require "fossilize/version"
2
+ require "ffi"
3
+
4
+ # Accesss
5
+ # Public Module
6
+ #
7
+ # Summary
8
+ # Provides an interface through Ruby-FFI to the delta encoding algorithm
9
+ # written for the Fossil SCM project by D. Richard Hipp. All methods are module methods.
10
+ #
11
+ # Examples
12
+ #
13
+ # delta = Fossilize.create(file1, file2)
14
+ # output = Fossilize.apply(file1, delta)
15
+ #
16
+ module Fossilize
17
+ extend FFI::Library
18
+
19
+ ffi_lib File.expand_path("../../ext/fossilize/fossilize.#{RbConfig::CONFIG['DLEXT']}", __FILE__)
20
+
21
+ attach_function :delta_create, [:pointer, :int, :pointer, :int, :pointer], :int
22
+ attach_function :delta_output_size, [:pointer, :int], :int
23
+ attach_function :delta_apply, [:pointer, :int, :pointer, :int, :pointer], :int
24
+
25
+
26
+ # Access
27
+ # Public Module Method
28
+ #
29
+ # Summary
30
+ # Creates a delta of two strings using the Fossil delta encoding algorithm.
31
+ #
32
+ # Parameters
33
+ # old - The old string.
34
+ # new - The new string to create the delta from.
35
+ #
36
+ # Returns a String that represents the deltaed differences between the two Strings.
37
+ #
38
+ # Examples
39
+ #
40
+ # # Create the delta between two strings
41
+ # Fossilize.create("Hello World!", "Hello Everyone!")
42
+ #
43
+ # # Create the delta between two files (note the passing of a File object)
44
+ # source = File.new("README.md", "r")
45
+ # target = File.new("README_new.md", "r")
46
+ # Fossilize.create(source, target)
47
+ #
48
+ # # You can also create a delta between a file and a string (the arguments are interchangeable)
49
+ # Fossilize(source, "This is the new README for Fossilize!")
50
+ #
51
+ def self.create(source, target)
52
+ # Because this method can accept three different types of parameter (path, String or File)
53
+ # we need to do a sanity check on the input parameters.
54
+ source_string = check_input(source)
55
+ target_string = check_input(target)
56
+
57
+ # Create native memory buffers for the input Strings.
58
+ source_ptr = FFI::MemoryPointer.new(:char, source_string.size)
59
+ source_ptr.put_bytes(0, source_string)
60
+ target_ptr = FFI::MemoryPointer.new(:char, target_string.size)
61
+ target_ptr.put_bytes(0, target_string)
62
+
63
+ # Create a bare string to hold to returning delta from the C function that's the
64
+ # size of the target + 60 (according to the Fossil source docs).
65
+ delta = (' ' * (target_string.size + 60))
66
+
67
+ # create the delta, retaining the size of the delta output and return the delta,
68
+ # stripping out any excess left over (needs refinement...).
69
+ delta_size = delta_create(source_ptr, source_ptr.size, target_ptr, target_ptr.size, delta)
70
+ return delta.strip!
71
+ end
72
+
73
+ # Access
74
+ # Public Module Method
75
+ #
76
+ # Summary
77
+ # Applies a delta string to another string.
78
+ #
79
+ # Parameters
80
+ # source - The old string to apply the delta string to.
81
+ # delta - The delta string created using *create*.
82
+ #
83
+ # Returns a new unified string created by applying the delta to the source
84
+ # if successful. The algorithm returns -1 as the output_size if the delta was
85
+ # not created from the given source or is malformed. In this case, this method returns nil.
86
+ #
87
+ # Examples
88
+ #
89
+ # # original is a String or File object
90
+ # result = Fossilize.apply(original, delta)
91
+ #
92
+ def self.apply(source, delta)
93
+ # Check the input types first
94
+ source_string = check_input(source)
95
+ delta_string = check_input(delta)
96
+
97
+ # Get the eventual size of the deltaed file and create a string to hold it
98
+ expected_output_size = delta_output_size(delta_string, delta_string.size)
99
+ puts "expected = #{expected_output_size}"
100
+
101
+ # The algorithm will return -1 as the output size if there was an error
102
+ if expected_output_size == -1
103
+ raise MalformedDeltaError, "Was this delta intended for this string/file?"
104
+ return nil
105
+ end
106
+
107
+ # Create an empty string that is at-least the output_size given by *delta_output_size*
108
+ output = "\0" * expected_output_size
109
+
110
+ # Apply the delta to the old file to produce the merged result
111
+ output_size = delta_apply(source_string, source_string.size, delta_string, delta_string.size, output)
112
+
113
+ if output_size != expected_output_size
114
+ raise DeltaApplicationError,
115
+ "Output was #{output_size}, but I expected #{expected_output_size}!"
116
+ return nil
117
+ end
118
+
119
+ return output.strip!
120
+ end
121
+
122
+ private
123
+
124
+ # Access
125
+ # Private Module Method
126
+ #
127
+ # Summary
128
+ # checks that the input given to the delta methods is sane, i.e. can it be passed
129
+ # to the C-extension without any problems? The algorithm itself expects a String no matter what.
130
+ #
131
+ # Parameters
132
+ # input - The input to perform the sanity check on.
133
+ #
134
+ # Returns a String as read from a File object (either through a path or a File object) or a
135
+ # direct String as passed to the calling method.
136
+ def self.check_input(input)
137
+ raise ArgumentError, "source or target input was nil!" if input.nil?
138
+ raise ArgumentError,
139
+ "Only Strings (including file paths) and File objects can be used to create deltas." if
140
+ (!input.instance_of? String and !input.instance_of? File)
141
+
142
+ # Now we know the input is a valid type, we need to check exactly what type it is.
143
+ # We know straight away if the input is a File object, just read from it.
144
+ if input.instance_of? File
145
+ input_string = input.read
146
+ elsif input.instance_of? String
147
+ # We can determine quickly if the input is a path to a file or merely a String object
148
+ # by checking if it actually exists. Sneaky.
149
+ if File.exists?(input)
150
+ input_string = File.read(input)
151
+ else
152
+ return input
153
+ end
154
+ end
155
+
156
+ return input_string
157
+ end
158
+ end
159
+
160
+ class MalformedDeltaError < StandardError; end
161
+ class DeltaApplicationError < StandardError; end
@@ -0,0 +1,35 @@
1
+ require "spec_helper"
2
+ require "fossilize"
3
+
4
+ describe Fossilize do
5
+ describe "#create" do
6
+ it "should return a valid delta string from two String arguements" do
7
+ Fossilize.create("Test", "Test String").should == "B\nB:Test String3U9pwb;"
8
+ end
9
+
10
+ it "should return a valid delta string from two File arguments" do
11
+ f1 = File.open('Gemfile')
12
+ f2 = File.open('Rakefile')
13
+ Fossilize.create(f1, f2).should ==
14
+ "l\nl:#!/usr/bin/env rake\nrequire \"bundler/gem_tasks\"\nhrAtN;"
15
+ end
16
+
17
+ it "should return a valid delta when the arguments contain a file path" do
18
+ puts Fossilize.create('README.md', 'ext/fossil_delta/extconf.rb')
19
+ end
20
+
21
+ it "should return a valid delta when the arguments are mixed" do
22
+ puts Fossilize.create('/Users/xiy/.zshrc', 'Is this in the README?')
23
+ end
24
+ end
25
+
26
+ describe "#input_is_sane?" do
27
+ it "should raise an ArgumentError if the input is an invalid type" do
28
+ expect { Fossilize.check_input(1) }.to raise_error ArgumentError
29
+ end
30
+
31
+ it "should raise an ArgumentError if the input is nil" do
32
+ expect { Fossilize.check_input(nil) }.to raise_error ArgumentError
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,7 @@
1
+ require 'rspec/expectations'
2
+
3
+ RSpec.configure do |config|
4
+ config.treat_symbols_as_metadata_keys_with_true_values = true
5
+ config.run_all_when_everything_filtered = true
6
+ config.filter_run :focus
7
+ end
metadata ADDED
@@ -0,0 +1,129 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: fossilize
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Mark Anthony Gibbins
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-10-15 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: ffi
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ - !ruby/object:Gem::Dependency
31
+ name: digest-crc
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: rspec
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - '='
52
+ - !ruby/object:Gem::Version
53
+ version: 2.8.0
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - '='
60
+ - !ruby/object:Gem::Version
61
+ version: 2.8.0
62
+ - !ruby/object:Gem::Dependency
63
+ name: tomdoc
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ type: :development
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ description: ! "A ruby extension to the Fossil delta compression algorithm written\n
79
+ \ by D. Richard Hipp for the Fossil SCM project."
80
+ email:
81
+ - xiy3x0@gmail.com
82
+ executables:
83
+ - fossilize
84
+ extensions:
85
+ - ext/fossilize/extconf.rb
86
+ extra_rdoc_files: []
87
+ files:
88
+ - .gitignore
89
+ - Gemfile
90
+ - LICENSE
91
+ - README.md
92
+ - Rakefile
93
+ - bin/fossilize
94
+ - ext/fossilize/extconf.rb
95
+ - ext/fossilize/fossilize.c
96
+ - fossilize.gemspec
97
+ - lib/fossilize.rb
98
+ - lib/fossilize/delta.rb
99
+ - lib/fossilize/ring_buffer.rb
100
+ - lib/fossilize/version.rb
101
+ - spec/fossilize_spec.rb
102
+ - spec/spec_helper.rb
103
+ homepage: http://github.com/xiy/fossilize
104
+ licenses: []
105
+ post_install_message:
106
+ rdoc_options: []
107
+ require_paths:
108
+ - lib
109
+ required_ruby_version: !ruby/object:Gem::Requirement
110
+ none: false
111
+ requirements:
112
+ - - ! '>='
113
+ - !ruby/object:Gem::Version
114
+ version: '0'
115
+ required_rubygems_version: !ruby/object:Gem::Requirement
116
+ none: false
117
+ requirements:
118
+ - - ! '>='
119
+ - !ruby/object:Gem::Version
120
+ version: '0'
121
+ requirements: []
122
+ rubyforge_project:
123
+ rubygems_version: 1.8.24
124
+ signing_key:
125
+ specification_version: 3
126
+ summary: Delta compression for Ruby using the Fossil delta compression algorithm.
127
+ test_files:
128
+ - spec/fossilize_spec.rb
129
+ - spec/spec_helper.rb