| File: | blib/lib/App/FileSync/Delta.pm |
| Coverage: | 88.9% |
| line | stmt | bran | cond | sub | time | code |
|---|---|---|---|---|---|---|
| 1 | package App::FileSync::Delta; | |||||
| 2 | ||||||
| 3 | # rsync-style block matching. The receiver describes the file it already has as | |||||
| 4 | # one strong hash per fixed-size block; the sender scans the new file with a | |||||
| 5 | # rolling checksum and emits either a copy of a block the receiver already has | |||||
| 6 | # or the literal bytes that changed. | |||||
| 7 | # | |||||
| 8 | # The scanner is pure Perl, so it is deliberately bounded: only files of at | |||||
| 9 | # least $MIN_SIZE are worth a signature, and at most $SCAN_LIMIT bytes per file | |||||
| 10 | # are examined for matches. Whatever lies beyond that is sent literally, which | |||||
| 11 | # is still correct, just not smaller. A small edit in a large file is cheap | |||||
| 12 | # because the unchanged leading blocks are matched without scanning. | |||||
| 13 | ||||||
| 14 | 313 313 313 | 780 252 4065 | use App::FileSync::Platform; | |||
| 15 | ||||||
| 16 | 313 313 313 | 488 257 2562 | use strict; | |||
| 17 | 313 313 313 | 710 283 6177 | use warnings; | |||
| 18 | 313 313 313 | 501 214 691 | use autodie qw(:all); | |||
| 19 | ||||||
| 20 | 313 313 313 | 654517 331 2592 | use Digest::SHA (); | |||
| 21 | 313 313 313 | 487 264 7185 | use Readonly; | |||
| 22 | 313 313 313 | 644 292 895 | use Sub::Private { mode => 'enforce' }; | |||
| 23 | ||||||
| 24 | 313 313 313 | 29860 298 6400 | use App::FileSync::I18N qw(throw os_error); | |||
| 25 | 313 313 313 | 580 260 4610 | use App::FileSync::IO qw(open_file); | |||
| 26 | ||||||
| 27 | 313 313 313 | 504 232 894 | use version; our $VERSION = version->declare('0.001.1'); | |||
| 28 | ||||||
| 29 | # Tunables (package variables so tests can shrink them with "local"). | |||||
| 30 | our $MIN_SIZE = 65_536; | |||||
| 31 | our $BLOCK = 16_384; | |||||
| 32 | our $MAX_BLOCKS = 4_096; | |||||
| 33 | our $SCAN_LIMIT = 8 * 1024 * 1024; | |||||
| 34 | our $LITERAL = 262_144; | |||||
| 35 | ||||||
| 36 | # Wire format of a signature record: a 32-bit weak sum then a SHA-256. | |||||
| 37 | Readonly my $WEAK_BYTES => 4; | |||||
| 38 | Readonly my $STRONG_BYTES => 32; | |||||
| 39 | Readonly my $RECORD_BYTES => $WEAK_BYTES + $STRONG_BYTES; | |||||
| 40 | ||||||
| 41 | # Delta records: "C" + offset + length, or "L" + length + bytes. | |||||
| 42 | Readonly my $COPY => 'C'; | |||||
| 43 | Readonly my $LIT => 'L'; | |||||
| 44 | Readonly my $COPY_HEADER => 8; | |||||
| 45 | Readonly my $LIT_HEADER => 4; | |||||
| 46 | ||||||
| 47 | # Block sizes are rounded to whole kibibytes. | |||||
| 48 | Readonly my $KIB => 1024; | |||||
| 49 | ||||||
| 50 | # Each half of the weak checksum is a 16-bit sum. | |||||
| 51 | Readonly my $HALF => 0xffff; | |||||
| 52 | ||||||
| 53 | # rsync's weak checksum: two 16-bit sums, cheap to roll by one byte. Lexical: | |||||
| 54 | # it runs once per block. | |||||
| 55 | my $weak_parts = sub { | |||||
| 56 | my ($window) = @_; | |||||
| 57 | ||||||
| 58 | my $length = length $window; | |||||
| 59 | my ($a, $b) = (0, 0); | |||||
| 60 | for my $index (0 .. $length - 1) { | |||||
| 61 | my $value = vec($window, $index, 8); | |||||
| 62 | $a += $value; | |||||
| 63 | $b += ($length - $index) * $value; | |||||
| 64 | } | |||||
| 65 | ||||||
| 66 | return ($a & $HALF, $b & $HALF); | |||||
| 67 | }; | |||||
| 68 | ||||||
| 69 | my $weak = sub { | |||||
| 70 | my ($a, $b) = $weak_parts->($_[0]); | |||||
| 71 | return (($b & $HALF) << 16) | ($a & $HALF); | |||||
| 72 | }; | |||||
| 73 | ||||||
| 74 | =encoding utf8 | |||||
| 75 | ||||||
| 76 - 128 | =head1 NAME
App::FileSync::Delta - rsync-style block deltas in pure Perl
=head1 SYNOPSIS
my $signature = App::FileSync::Delta::signature($old_path);
App::FileSync::Delta::diff($new_path, $signature, $spool);
seek $spool, 0, 0;
App::FileSync::Delta::apply($old_path, $spool, $out);
=head1 FUNCTIONS
=head2 block_size
Purpose: the block length for a file of a given size.
Args: C<$size> in bytes.
Returns: C<$BLOCK> (16 KiB) for files up to C<$BLOCK * $MAX_BLOCKS>; above that,
a larger whole-KiB block so the signature stays at most about C<$MAX_BLOCKS>
records.
Side Effects: none.
Usage:
my $block = App::FileSync::Delta::block_size(-s $path);
=head3 EXAMPLE
print App::FileSync::Delta::block_size(1_000_000), "\n"; # 16384
=head3 API SPECIFICATION
=head4 INPUT
[ { name => 'size', type => 'integer', min => 0, optional => 1 } ]
=head4 OUTPUT
{ type => 'integer', min => 1024 }
=head3 MESSAGES
None.
=head3 FORMAL SPECIFICATION
block_size(s) = BLOCK if s ⤠BLOCK à MAX_BLOCKS
= (ââs / MAX_BLOCKSâ / 1024â + 1) Ã 1024 otherwise
=cut | |||||
| 129 | ||||||
| 130 | sub block_size { | |||||
| 131 | 9 | 355 | my ($size) = @_; | |||
| 132 | ||||||
| 133 | 9 | 35 | my $per_block = (defined $size && $size > $BLOCK * $MAX_BLOCKS) | |||
| 134 | ? int(($size + $MAX_BLOCKS - 1) / $MAX_BLOCKS) | |||||
| 135 | : undef; | |||||
| 136 | ||||||
| 137 | 9 | 18 | return defined $per_block ? (int(($per_block + $KIB - 1) / $KIB) + 1) * $KIB : $BLOCK; | |||
| 138 | } | |||||
| 139 | ||||||
| 140 - 186 | =head2 signature
Purpose: describe a file the receiver already holds.
Args: C<$path>, optional C<$block> length (default L</block_size>).
Returns: C<{ size, blocklen, blocks =E<gt> [ [ weak, strong ], ... ] }>, one pair
per full block (a short final block is not included; its bytes are simply never
reused), or C<undef> for a missing file or one smaller than C<$MIN_SIZE>.
Side Effects: reads the file.
Usage:
my $signature = App::FileSync::Delta::signature($abs) or return 0;
=head3 EXAMPLE
my $sig = App::FileSync::Delta::signature('/tmp/big.iso');
printf "%d blocks of %d bytes\n", scalar @{ $sig->{blocks} }, $sig->{blocklen} if $sig;
=head3 API SPECIFICATION
=head4 INPUT
[
{ name => 'path', type => 'string', optional => 1 },
{ name => 'block', type => 'integer', min => 1, optional => 1 },
]
=head4 OUTPUT
{ type => 'hashref', optional => 1 }
=head3 MESSAGES
Key Meaning Resolution
io.read the file cannot be read check permissions
=head3 FORMAL SPECIFICATION
signature(p, b) = ⥠if p â dom FS ⨠#FS(p) < MIN_SIZE
= â¨size ⦠#F, blocklen ⦠b,
blocks ⦠â¨(weak(F[i]), sha256(F[i])) | i â 0 .. â#F / bâ â 1â©â©
where F = FS(p), F[i] = bytes iÃb .. (i+1)Ãb â 1
=cut | |||||
| 187 | ||||||
| 188 | sub signature { | |||||
| 189 | 7 | 423203 | my ($path, $block) = @_; | |||
| 190 | ||||||
| 191 | 7 | 62 | return undef unless defined $path && length $path && -f $path; ## no critic (Subroutines::ProhibitExplicitReturnUndef) | |||
| 192 | 7 | 10 | my $size = -s _; | |||
| 193 | 7 | 16 | return undef unless $size >= $MIN_SIZE; ## no critic (Subroutines::ProhibitExplicitReturnUndef) | |||
| 194 | ||||||
| 195 | 7 | 24 | $block = block_size($size) unless defined $block && $block > 0; | |||
| 196 | ||||||
| 197 | 7 | 22 | my $fh = open_file($path, '<'); | |||
| 198 | 7 | 9 | my @blocks; | |||
| 199 | 7 | 20 | for (1 .. int($size / $block)) { | |||
| 200 | 169 | 11689 | my $data = read_exact($fh, $block, $path); | |||
| 201 | 169 | 239 | push @blocks, [ $weak->($data), Digest::SHA::sha256($data) ]; | |||
| 202 | } | |||||
| 203 | 7 | 521 | close $fh; | |||
| 204 | ||||||
| 205 | 7 | 1334 | return { size => $size, blocklen => $block, blocks => \@blocks }; | |||
| 206 | } | |||||
| 207 | ||||||
| 208 - 258 | =head2 encode_signature
Documented with L</decode_signature> below.
=head2 decode_signature
Purpose: put a signature's blocks on the wire and take them off again.
Args: C<encode_signature($signature)>; C<decode_signature($bytes, $count)>.
Returns: the encoded bytes (36 per block: a big-endian weak sum then the
32-byte SHA-256); the decoded arrayref of C<[ weak, strong ]> pairs.
Side Effects: none.
Usage:
my $bytes = App::FileSync::Delta::encode_signature($signature);
my $blocks = App::FileSync::Delta::decode_signature($bytes, $count);
=head3 EXAMPLE
my $blocks = App::FileSync::Delta::decode_signature(
App::FileSync::Delta::encode_signature($sig), scalar @{ $sig->{blocks} });
=head3 API SPECIFICATION
=head4 INPUT
[
{ name => 'bytes', type => 'string', optional => 1 },
{ name => 'count', type => 'integer', min => 0, optional => 1 },
]
=head4 OUTPUT
{ type => 'arrayref' }
=head3 MESSAGES
Key Meaning Resolution
delta.bad_count the block count is not a number a peer bug; resync with --no-delta
delta.bad_length the bytes do not match the count a peer bug; resync with --no-delta
=head3 FORMAL SPECIFICATION
encode(â¨(wâ,sâ), â¦, (wâ,sâ)â©) = be32(wâ) â sâ â ⦠â be32(wâ) â sâ
decode(encode(bs), #bs) = bs
pre decode: #bytes = 36 Ã count
=cut | |||||
| 259 | ||||||
| 260 | sub encode_signature { | |||||
| 261 | 3 | 969 | my ($signature) = @_; | |||
| 262 | ||||||
| 263 | 3 72 3 | 5 105 10 | return join '', map { pack('N', $_->[0]) . $_->[1] } @{ $signature->{blocks} || [] }; | |||
| 264 | } | |||||
| 265 | ||||||
| 266 | sub decode_signature { | |||||
| 267 | 4 | 2259 | my ($bytes, $count) = @_; | |||
| 268 | ||||||
| 269 | 4 | 7 | $count = 0 unless defined $count; | |||
| 270 | 4 | 16 | throw('delta.bad_count', { count => $count }) unless $count =~ /\A\d+\z/; | |||
| 271 | ||||||
| 272 | 4 | 9 | my $length = defined $bytes ? length $bytes : 0; | |||
| 273 | 4 | 13 | throw('delta.bad_length', { length => $length, expected => $count * $RECORD_BYTES }) | |||
| 274 | unless $length == $count * $RECORD_BYTES; | |||||
| 275 | ||||||
| 276 | my @blocks = map { | |||||
| 277 | 3 72 | 23 302 | my $record = substr($bytes, $_ * $RECORD_BYTES, $RECORD_BYTES); | |||
| 278 | 72 | 203 | [ unpack('N', substr($record, 0, $WEAK_BYTES)), substr($record, $WEAK_BYTES, $STRONG_BYTES) ]; | |||
| 279 | } 0 .. $count - 1; | |||||
| 280 | ||||||
| 281 | 3 | 21 | return \@blocks; | |||
| 282 | } | |||||
| 283 | ||||||
| 284 - 354 | =head2 diff
Purpose: write the delta that turns the receiver's file into C<$path>.
Args: C<$path> (the new content), C<$signature> (of what the receiver has;
C<blocklen> and C<blocks> are used), C<$out> (a writable handle).
Returns: C<{ literal, copy, ops }>: bytes sent literally, bytes copied, and
records written, so the caller can decide whether the delta beats the file.
Side Effects: reads C<$path>; writes copy and literal records to C<$out>.
Usage:
my $stats = App::FileSync::Delta::diff($abs, $signature, $spool);
=head3 EXAMPLE
open my $spool, '+>', undef;
my $stats = App::FileSync::Delta::diff('/tmp/new.bin', $sig, $spool);
printf "%d literal bytes in %d records\n", $stats->{literal}, $stats->{ops};
=head3 API SPECIFICATION
=head4 INPUT
[
{ name => 'path', type => 'string', min => 1 },
{ name => 'signature', type => 'hashref' },
{ name => 'out', type => 'handle' },
]
=head4 OUTPUT
{
type => 'hashref',
schema => {
literal => { type => 'integer', min => 0 },
copy => { type => 'integer', min => 0 },
ops => { type => 'integer', min => 0 },
},
}
=head3 MESSAGES
Key Meaning Resolution
delta.no_output no output handle a caller bug
delta.no_block the signature has no block length a peer bug
delta.write the delta cannot be written check space for the spool
io.read the new file cannot be read check permissions
=head3 FORMAL SPECIFICATION
let R = records written to out
apply(old, R) = FS(path) (correctness)
literal = Σ { #l | L(l) â R } ; copy = Σ { n | C(o, n) â R } ; ops = #R
literal + copy = #FS(path)
=head3 PSEUDOCODE
index the receiver's blocks by weak checksum
while the next full block equals the receiver's block at the same index:
emit a copy of it (no rolling needed for an unchanged prefix)
read up to SCAN_LIMIT more bytes into a window buffer
slide a one-block window through the buffer:
if its weak sum is indexed and the strong hash matches a block:
emit a copy, jump a whole block
else: add one byte to the literal run, roll the sums forward one byte
flush the rest of the buffer and the unscanned tail as literals
=cut | |||||
| 355 | ||||||
| 356 | sub diff { | |||||
| 357 | 6 | 16 | my ($path, $signature, $out) = @_; | |||
| 358 | ||||||
| 359 | 6 | 16 | throw('delta.no_output') unless $out; | |||
| 360 | 6 | 25 | my $block = $signature->{blocklen} or throw('delta.no_block'); | |||
| 361 | 6 | 13 | my $blocks = $signature->{blocks} || []; | |||
| 362 | ||||||
| 363 | 6 | 22 | my $fh = open_file($path, '<'); | |||
| 364 | 6 | 45 | my $state = { | |||
| 365 | out => $out, | |||||
| 366 | path => $path, | |||||
| 367 | block => $block, | |||||
| 368 | literal => '', | |||||
| 369 | stats => { literal => 0, copy => 0, ops => 0 }, | |||||
| 370 | }; | |||||
| 371 | ||||||
| 372 | 6 | 38 | my $pending = _match_prefix($state, $fh, $blocks, -s $path); | |||
| 373 | 6 | 16 | my $buffer = _fill_window($fh, $pending, $SCAN_LIMIT + $block, $path); | |||
| 374 | 6 | 18 | _rolling_scan($state, $buffer, $blocks); | |||
| 375 | ||||||
| 376 | # Everything past the scanned window goes literally. | |||||
| 377 | # length(), not truth: a final chunk of "0" is still data. | |||||
| 378 | 6 | 18 | while (length(my $chunk = _read_some($fh, $LITERAL, $path))) { | |||
| 379 | 0 | 0 | _add_literal($state, $chunk); | |||
| 380 | } | |||||
| 381 | 6 | 15 | _flush($state); | |||
| 382 | 6 | 15 | close $fh; | |||
| 383 | ||||||
| 384 | 6 | 690 | return $state->{stats}; | |||
| 385 | } | |||||
| 386 | ||||||
| 387 - 435 | =head2 write_copy
Documented with L</write_literal> below.
=head2 write_literal
Purpose: write one delta record.
Args: C<write_copy($out, $offset, $length)>; C<write_literal($out, $bytes)>
(an empty literal writes nothing).
Returns: 1.
Side Effects: writes to C<$out>.
Usage:
App::FileSync::Delta::write_copy($spool, $offset, $length);
App::FileSync::Delta::write_literal($spool, $bytes);
=head3 EXAMPLE
App::FileSync::Delta::write_literal($out, "new text\n");
=head3 API SPECIFICATION
=head4 INPUT
[
{ name => 'out', type => 'handle' },
{ name => 'offset', type => 'integer', min => 0 },
{ name => 'length', type => 'integer', min => 0 },
]
=head4 OUTPUT
{ type => 'integer', min => 1, max => 1 }
=head3 MESSAGES
Key Meaning Resolution
delta.write the record cannot be written check space for the spool
=head3 FORMAL SPECIFICATION
write_copy(o, n): out' = out â "C" â be32(o) â be32(n)
write_literal(b): out' = out â "L" â be32(#b) â b if b â â¨â©
=cut | |||||
| 436 | ||||||
| 437 | sub write_copy { | |||||
| 438 | 98 | 96 | my ($out, $offset, $length) = @_; | |||
| 439 | ||||||
| 440 | 98 98 | 58 243 | print {$out} $COPY . pack('NN', $offset, $length) or throw('delta.write', { error => "$!" }); | |||
| 441 | 98 | 318 | return 1; | |||
| 442 | } | |||||
| 443 | ||||||
| 444 | sub write_literal { | |||||
| 445 | 11 | 89 | my ($out, $bytes) = @_; | |||
| 446 | ||||||
| 447 | 11 | 48 | if (defined $bytes && length $bytes) { | |||
| 448 | 11 11 | 11 54 | print {$out} $LIT . pack('N', length $bytes) . $bytes or throw('delta.write', { error => "$!" }); | |||
| 449 | } | |||||
| 450 | 11 | 669 | return 1; | |||
| 451 | } | |||||
| 452 | ||||||
| 453 - 496 | =head2 read_op
Purpose: read the next delta record.
Args: C<$in>, a handle positioned at a record.
Returns: C<('copy', $offset, $length)>, C<('literal', $bytes)>, or the empty
list at the end. A list because callers loop with
C<while (my ($type, @rest) = read_op($in))>.
Side Effects: advances C<$in>.
Usage:
while (my ($type, @rest) = App::FileSync::Delta::read_op($spool)) { ... }
=head3 EXAMPLE
my ($type, $offset, $length) = App::FileSync::Delta::read_op($spool);
=head3 API SPECIFICATION
=head4 INPUT
[ { name => 'in', type => 'handle' } ]
=head4 OUTPUT
{ type => 'arrayref' }
=head3 MESSAGES
Key Meaning Resolution
delta.read the delta cannot be read check the spool
delta.bad_record an unknown record type a peer bug; resync with --no-delta
io.short the delta ends mid-record a peer bug; resync with --no-delta
=head3 FORMAL SPECIFICATION
read_op("C" â be32(o) â be32(n) â rest) = (copy, o, n)
read_op("L" â be32(k) â b â rest) = (literal, b) where #b = k
read_op(â¨â©) = â¨â©
=cut | |||||
| 497 | ||||||
| 498 | sub read_op { | |||||
| 499 | 112 | 1211 | my ($in) = @_; | |||
| 500 | ||||||
| 501 | 112 | 128 | my $type = _read_some($in, 1, 'delta', 'delta.read'); | |||
| 502 | 112 | 110 | return () unless length $type; | |||
| 503 | 108 | 127 | throw('delta.bad_record', { type => $type }) unless $type eq $COPY || $type eq $LIT; | |||
| 504 | ||||||
| 505 | 108 | 289 | return ('copy', unpack('NN', read_exact($in, $COPY_HEADER, 'delta'))) if $type eq $COPY; | |||
| 506 | ||||||
| 507 | 10 | 31 | my $length = unpack('N', read_exact($in, $LIT_HEADER, 'delta')); | |||
| 508 | 10 | 19 | return ('literal', $length ? read_exact($in, $length, 'delta') : ''); | |||
| 509 | } | |||||
| 510 | ||||||
| 511 - 556 | =head2 apply
Purpose: rebuild a file from the old copy and a spooled delta.
Args: C<$old_path>, C<$in> (the delta), C<$out> (where the new content goes).
Returns: C<{ size }>, the bytes written.
Side Effects: reads C<$old_path> and C<$in>; writes C<$out>.
Usage:
my $result = App::FileSync::Delta::apply($old, $spool, $out);
=head3 EXAMPLE
open my $out, '>', '/tmp/rebuilt.bin';
App::FileSync::Delta::apply('/tmp/old.bin', $spool, $out);
=head3 API SPECIFICATION
=head4 INPUT
[
{ name => 'old_path', type => 'string', min => 1 },
{ name => 'in', type => 'handle' },
{ name => 'out', type => 'handle' },
]
=head4 OUTPUT
{ type => 'hashref', schema => { size => { type => 'integer', min => 0 } } }
=head3 MESSAGES
Key Meaning Resolution
io.read the old file cannot be read check permissions
io.seek a copy points outside the file the delta does not fit this file
delta.write_restored the output cannot be written check space
=head3 FORMAL SPECIFICATION
apply(old, R) = â/ ⨠bytes(r) | r â R â©
where bytes(C(o, n)) = FS(old)[o .. o + n â 1], bytes(L(b)) = b
=cut | |||||
| 557 | ||||||
| 558 | sub apply { | |||||
| 559 | 2 | 1532 | my ($old_path, $in, $out) = @_; | |||
| 560 | ||||||
| 561 | 2 | 6 | my $old = open_file($old_path, '<'); | |||
| 562 | 2 | 2 | my $written = 0; | |||
| 563 | ||||||
| 564 | 2 | 6 | while (my ($type, @rest) = read_op($in)) { | |||
| 565 | 57 | 63 | my $bytes = $type eq 'copy' ? _copy_from($old, @rest, $old_path) : $rest[0]; | |||
| 566 | 57 57 | 37 720 | print {$out} $bytes or throw('delta.write_restored', { error => "$!" }); | |||
| 567 | 57 | 65 | $written += length $bytes; | |||
| 568 | } | |||||
| 569 | ||||||
| 570 | 2 | 3 | close $old; | |||
| 571 | 2 | 45 | return { size => $written }; | |||
| 572 | } | |||||
| 573 | ||||||
| 574 - 617 | =head2 read_exact
Purpose: read exactly C<$length> bytes.
Args: C<$fh>, C<$length>, C<$what> (named in errors).
Returns: the bytes.
Side Effects: advances C<$fh>.
Usage:
my $data = App::FileSync::Delta::read_exact($fh, $block, $path);
=head3 EXAMPLE
my $header = App::FileSync::Delta::read_exact($spool, 8, 'delta');
=head3 API SPECIFICATION
=head4 INPUT
[
{ name => 'fh', type => 'handle' },
{ name => 'length', type => 'integer', min => 0 },
{ name => 'what', type => 'string' },
]
=head4 OUTPUT
{ type => 'string' }
=head3 MESSAGES
Key Meaning Resolution
io.read the read failed check the device
io.short fewer bytes than required the file was truncated
=head3 FORMAL SPECIFICATION
read_exact(fh, n, w) = take(n, rest(fh)) if #rest(fh) ⥠n
â io.short otherwise
=cut | |||||
| 618 | ||||||
| 619 | sub read_exact { | |||||
| 620 | 523 | 813 | my ($fh, $length, $what) = @_; | |||
| 621 | ||||||
| 622 | 523 | 543 | my $bytes = ''; | |||
| 623 | 523 | 657 | while (length($bytes) < $length) { | |||
| 624 | 523 | 869 | my $chunk = _read_some($fh, $length - length($bytes), $what); | |||
| 625 | 523 | 528 | throw('io.short', { path => $what }) unless length $chunk; | |||
| 626 | 522 | 1712 | $bytes .= $chunk; | |||
| 627 | } | |||||
| 628 | ||||||
| 629 | 522 | 704 | return $bytes; | |||
| 630 | } | |||||
| 631 | ||||||
| 632 | # _read_some | |||||
| 633 | # Purpose: one read() that reports failure with a catalogued message. | |||||
| 634 | # Entry: $fh is readable; $want > 0. | |||||
| 635 | # Exit: returns the bytes read ('' at end of file). | |||||
| 636 | # Side effects: advances $fh; raises $key (default io.read) on error. | |||||
| 637 | sub _read_some :Private { | |||||
| 638 | my ($fh, $want, $what, $key) = @_; | |||||
| 639 | ||||||
| 640 | my $chunk; | |||||
| 641 | eval { read($fh, $chunk, $want); 1 } | |||||
| 642 | or throw($key || 'io.read', { path => $what, error => os_error($@) }); | |||||
| 643 | ||||||
| 644 | return defined $chunk ? $chunk : ''; | |||||
| 645 | 313 313 313 | 258423 373 5853 | } | |||
| 646 | ||||||
| 647 | # _copy_from | |||||
| 648 | # Purpose: the bytes a copy record refers to in the old file. | |||||
| 649 | # Entry: $old is the old file's handle. | |||||
| 650 | # Exit: returns $length bytes from $offset. | |||||
| 651 | # Side effects: moves $old's position; raises io.seek or io.short. | |||||
| 652 | sub _copy_from :Private { | |||||
| 653 | my ($old, $offset, $length, $old_path) = @_; | |||||
| 654 | ||||||
| 655 | eval { seek($old, $offset, 0); 1 } | |||||
| 656 | or throw('io.seek', { path => $old_path, error => os_error($@) }); | |||||
| 657 | ||||||
| 658 | return $length ? read_exact($old, $length, $old_path) : ''; | |||||
| 659 | 313 313 313 | 95273 249 2613 | } | |||
| 660 | ||||||
| 661 | # _match_prefix | |||||
| 662 | # Purpose: copy the unchanged leading blocks without rolling a checksum. | |||||
| 663 | # Entry: $fh is at offset 0 of the new file; $size is its length. | |||||
| 664 | # Exit: returns the first block that did not match ('' if none), which | |||||
| 665 | # the rolling scan must start from. | |||||
| 666 | # Side effects: writes copy records; advances $fh. | |||||
| 667 | sub _match_prefix :Private { | |||||
| 668 | my ($state, $fh, $blocks, $size) = @_; | |||||
| 669 | ||||||
| 670 | my $block = $state->{block}; | |||||
| 671 | my $offset = 0; | |||||
| 672 | ||||||
| 673 | # The leading blocks are usually untouched: compare them directly, which | |||||
| 674 | # avoids scanning terabyte-sized prefixes a byte at a time. | |||||
| 675 | for my $index (0 .. $#{$blocks}) { | |||||
| 676 | last if $offset + $block > $size; | |||||
| 677 | my $data = read_exact($fh, $block, $state->{path}); | |||||
| 678 | return $data unless Digest::SHA::sha256($data) eq $blocks->[$index][1]; | |||||
| 679 | ||||||
| 680 | _add_copy($state, $offset, $block); | |||||
| 681 | $offset += $block; | |||||
| 682 | } | |||||
| 683 | ||||||
| 684 | return ''; | |||||
| 685 | 313 313 313 | 95422 263 2494 | } | |||
| 686 | ||||||
| 687 | # _fill_window | |||||
| 688 | # Purpose: read the region the rolling scan examines into one string. | |||||
| 689 | # Entry: $pending is data already read; $want the window size. | |||||
| 690 | # Exit: returns up to $want bytes (less at end of file). | |||||
| 691 | # Side effects: advances $fh. One string lets the loop use vec() without | |||||
| 692 | # copying. | |||||
| 693 | sub _fill_window :Private { | |||||
| 694 | my ($fh, $pending, $want, $path) = @_; | |||||
| 695 | ||||||
| 696 | my $buffer = $pending; | |||||
| 697 | while (length($buffer) < $want) { | |||||
| 698 | my $chunk = _read_some($fh, $want - length($buffer), $path); | |||||
| 699 | last unless length $chunk; | |||||
| 700 | $buffer .= $chunk; | |||||
| 701 | } | |||||
| 702 | ||||||
| 703 | return $buffer; | |||||
| 704 | 313 313 313 | 90589 295 2366 | } | |||
| 705 | ||||||
| 706 | # _rolling_scan | |||||
| 707 | # Purpose: find the receiver's blocks anywhere in the window. | |||||
| 708 | # Entry: $buffer is the window; $blocks the receiver's signature. | |||||
| 709 | # Exit: returns 1. | |||||
| 710 | # Side effects: writes copy and literal records for the whole buffer. | |||||
| 711 | sub _rolling_scan :Private { | |||||
| 712 | my ($state, $buffer, $blocks) = @_; | |||||
| 713 | ||||||
| 714 | my $block = $state->{block}; | |||||
| 715 | my $limit = length($buffer) - $block; | |||||
| 716 | ||||||
| 717 | # Shorter than one block: nothing can match. | |||||
| 718 | if ($limit < 0) { | |||||
| 719 | _add_literal($state, $buffer); | |||||
| 720 | return 1; | |||||
| 721 | } | |||||
| 722 | ||||||
| 723 | my %by_weak; | |||||
| 724 | push @{ $by_weak{ $blocks->[$_][0] } }, [ $_, $blocks->[$_][1] ] for 0 .. $#{$blocks}; | |||||
| 725 | ||||||
| 726 | my $last = $limit > $SCAN_LIMIT ? $SCAN_LIMIT : $limit; | |||||
| 727 | my ($a, $b) = $weak_parts->(substr($buffer, 0, $block)); | |||||
| 728 | my $position = 0; | |||||
| 729 | ||||||
| 730 | while ($position <= $last) { | |||||
| 731 | my $winner = _strong_match(\%by_weak, (($b & $HALF) << 16) | ($a & $HALF), $buffer, $position, $block); | |||||
| 732 | ||||||
| 733 | if (defined $winner) { | |||||
| 734 | _add_copy($state, $winner * $block, $block); | |||||
| 735 | $position += $block; | |||||
| 736 | ($a, $b) = $weak_parts->(substr($buffer, $position, $block)) if $position <= $last; | |||||
| 737 | next; | |||||
| 738 | } | |||||
| 739 | ||||||
| 740 | _add_literal($state, substr($buffer, $position, 1)); | |||||
| 741 | ||||||
| 742 | # Roll the window forward one byte, unless this was the last full | |||||
| 743 | # window in the buffer. The byte entering the window is the one just | |||||
| 744 | # past its end. | |||||
| 745 | if ($position < $last) { | |||||
| 746 | my $out_byte = vec($buffer, $position, 8); | |||||
| 747 | my $in_byte = vec($buffer, $position + $block, 8); | |||||
| 748 | $a = ($a - $out_byte + $in_byte) & $HALF; | |||||
| 749 | $b = ($b - $block * $out_byte + $a) & $HALF; | |||||
| 750 | } | |||||
| 751 | $position++; | |||||
| 752 | } | |||||
| 753 | ||||||
| 754 | _add_literal($state, substr($buffer, $position)) if $position < length($buffer); | |||||
| 755 | return 1; | |||||
| 756 | 313 313 313 | 130277 291 2524 | } | |||
| 757 | ||||||
| 758 | # _strong_match | |||||
| 759 | # Purpose: confirm a weak-checksum hit with the strong hash. | |||||
| 760 | # Entry: $by_weak maps weak sums to [ index, sha256 ] candidates. | |||||
| 761 | # Exit: returns the matching block index, or undef. | |||||
| 762 | # Side effects: none. | |||||
| 763 | sub _strong_match :Private { | |||||
| 764 | my ($by_weak, $weak_sum, $buffer, $position, $block) = @_; | |||||
| 765 | ||||||
| 766 | my $candidates = $by_weak->{$weak_sum} or return undef; ## no critic (Subroutines::ProhibitExplicitReturnUndef) | |||||
| 767 | my $strong = Digest::SHA::sha256(substr($buffer, $position, $block)); | |||||
| 768 | my ($hit) = grep { $_->[1] eq $strong } @{$candidates}; | |||||
| 769 | ||||||
| 770 | return $hit ? $hit->[0] : undef; | |||||
| 771 | 313 313 313 | 91598 270 2400 | } | |||
| 772 | ||||||
| 773 | # _add_copy | |||||
| 774 | # Purpose: emit a copy record, flushing any pending literal first. | |||||
| 775 | # Entry: $offset/$length name bytes in the receiver's file. | |||||
| 776 | # Exit: returns 1. | |||||
| 777 | # Side effects: writes to the output; updates the stats. | |||||
| 778 | sub _add_copy :Private { | |||||
| 779 | my ($state, $offset, $length) = @_; | |||||
| 780 | ||||||
| 781 | _flush($state); | |||||
| 782 | write_copy($state->{out}, $offset, $length); | |||||
| 783 | $state->{stats}{copy} += $length; | |||||
| 784 | $state->{stats}{ops}++; | |||||
| 785 | ||||||
| 786 | return 1; | |||||
| 787 | 313 313 313 | 84150 309 2262 | } | |||
| 788 | ||||||
| 789 | # _add_literal | |||||
| 790 | # Purpose: queue literal bytes, writing a record once enough accumulate. | |||||
| 791 | # Entry: $bytes may be empty. | |||||
| 792 | # Exit: returns 1. | |||||
| 793 | # Side effects: may write to the output. | |||||
| 794 | sub _add_literal :Private { | |||||
| 795 | my ($state, $bytes) = @_; | |||||
| 796 | ||||||
| 797 | $state->{literal} .= $bytes; | |||||
| 798 | _flush($state) if length($state->{literal}) >= $LITERAL; | |||||
| 799 | ||||||
| 800 | return 1; | |||||
| 801 | 313 313 313 | 82336 303 2188 | } | |||
| 802 | ||||||
| 803 | # _flush | |||||
| 804 | # Purpose: write the queued literal bytes as one record. | |||||
| 805 | # Entry: $state->{literal} may be empty. | |||||
| 806 | # Exit: returns 1. | |||||
| 807 | # Side effects: writes to the output; updates the stats. | |||||
| 808 | sub _flush :Private { | |||||
| 809 | my ($state) = @_; | |||||
| 810 | ||||||
| 811 | my $length = length $state->{literal}; | |||||
| 812 | if ($length) { | |||||
| 813 | write_literal($state->{out}, $state->{literal}); | |||||
| 814 | $state->{stats}{literal} += $length; | |||||
| 815 | $state->{stats}{ops}++; | |||||
| 816 | $state->{literal} = ''; | |||||
| 817 | } | |||||
| 818 | ||||||
| 819 | return 1; | |||||
| 820 | 313 313 313 | 93809 326 2388 | } | |||
| 821 | ||||||
| 822 | 1; | |||||
| 823 | ||||||