fossilize 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +18 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +105 -0
- data/Rakefile +2 -0
- data/bin/fossilize +9 -0
- data/ext/fossilize/extconf.rb +7 -0
- data/ext/fossilize/fossilize.c +626 -0
- data/fossilize.gemspec +27 -0
- data/lib/fossilize/delta.rb +13 -0
- data/lib/fossilize/ring_buffer.rb +27 -0
- data/lib/fossilize/version.rb +3 -0
- data/lib/fossilize.rb +161 -0
- data/spec/fossilize_spec.rb +35 -0
- data/spec/spec_helper.rb +7 -0
- metadata +129 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
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
data/bin/fossilize
ADDED
@@ -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,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
|
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
|
data/spec/spec_helper.rb
ADDED
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
|