File Coverage

File:blib/lib/App/FileSync/Delta.pm
Coverage:88.9%

linestmtbrancondsubtimecode
1package 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").
30our $MIN_SIZE   = 65_536;
31our $BLOCK      = 16_384;
32our $MAX_BLOCKS = 4_096;
33our $SCAN_LIMIT = 8 * 1024 * 1024;
34our $LITERAL    = 262_144;
35
36# Wire format of a signature record: a 32-bit weak sum then a SHA-256.
37Readonly my $WEAK_BYTES   => 4;
38Readonly my $STRONG_BYTES => 32;
39Readonly my $RECORD_BYTES => $WEAK_BYTES + $STRONG_BYTES;
40
41# Delta records: "C" + offset + length, or "L" + length + bytes.
42Readonly my $COPY        => 'C';
43Readonly my $LIT         => 'L';
44Readonly my $COPY_HEADER => 8;
45Readonly my $LIT_HEADER  => 4;
46
47# Block sizes are rounded to whole kibibytes.
48Readonly my $KIB => 1024;
49
50# Each half of the weak checksum is a 16-bit sum.
51Readonly 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.
55my $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
69my $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
130sub 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
188sub 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
260sub 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
266sub 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
356sub 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
437sub 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
444sub 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
498sub 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
558sub 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
619sub 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.
637sub _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.
652sub _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.
667sub _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.
693sub _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.
711sub _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.
763sub _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.
778sub _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.
794sub _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.
808sub _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
8221;
823