NAME
App::FileSync::Delta - rsync-style block deltas in pure Perl
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);
FUNCTIONS
block_size
Purpose: the block length for a file of a given size.
Args: $size in bytes.
Returns: $BLOCK (16 KiB) for files up to $BLOCK * $MAX_BLOCKS; above that, a larger whole-KiB block so the signature stays at most about $MAX_BLOCKS records.
Side Effects: none.
Usage:
my $block = App::FileSync::Delta::block_size(-s $path);
EXAMPLE
print App::FileSync::Delta::block_size(1_000_000), "\n"; # 16384
API SPECIFICATION
INPUT
[ { name => 'size', type => 'integer', min => 0, optional => 1 } ]
OUTPUT
{ type => 'integer', min => 1024 }
MESSAGES
None.
FORMAL SPECIFICATION
block_size(s) = BLOCK if s ≤ BLOCK × MAX_BLOCKS
= (⌈⌈s / MAX_BLOCKS⌉ / 1024⌉ + 1) × 1024 otherwise
signature
Purpose: describe a file the receiver already holds.
Args: $path, optional $block length (default "block_size").
Returns: { size, blocklen, blocks => [ [ weak, strong ], ... ] }, one pair per full block (a short final block is not included; its bytes are simply never reused), or undef for a missing file or one smaller than $MIN_SIZE.
Side Effects: reads the file.
Usage:
my $signature = App::FileSync::Delta::signature($abs) or return 0;
EXAMPLE
my $sig = App::FileSync::Delta::signature('/tmp/big.iso');
printf "%d blocks of %d bytes\n", scalar @{ $sig->{blocks} }, $sig->{blocklen} if $sig;
API SPECIFICATION
INPUT
[
{ name => 'path', type => 'string', optional => 1 },
{ name => 'block', type => 'integer', min => 1, optional => 1 },
]
OUTPUT
{ type => 'hashref', optional => 1 }
MESSAGES
Key Meaning Resolution
io.read the file cannot be read check permissions
FORMAL SPECIFICATION
signature(p, b) = ⊥ if p ¬in; 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
encode_signature
Documented with "decode_signature" below.
decode_signature
Purpose: put a signature's blocks on the wire and take them off again.
Args: encode_signature($signature); 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 [ weak, strong ] pairs.
Side Effects: none.
Usage:
my $bytes = App::FileSync::Delta::encode_signature($signature);
my $blocks = App::FileSync::Delta::decode_signature($bytes, $count);
EXAMPLE
my $blocks = App::FileSync::Delta::decode_signature(
App::FileSync::Delta::encode_signature($sig), scalar @{ $sig->{blocks} });
API SPECIFICATION
INPUT
[
{ name => 'bytes', type => 'string', optional => 1 },
{ name => 'count', type => 'integer', min => 0, optional => 1 },
]
OUTPUT
{ type => 'arrayref' }
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
FORMAL SPECIFICATION
encode(⟨(w₁,s₁), …, (wₙ,sₙ)⟩) = be32(w₁) ⁀ s₁ ⁀ … ⁀ be32(wₙ) ⁀ sₙ
decode(encode(bs), #bs) = bs
pre decode: #bytes = 36 × count
diff
Purpose: write the delta that turns the receiver's file into $path.
Args: $path (the new content), $signature (of what the receiver has; blocklen and blocks are used), $out (a writable handle).
Returns: { 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 $path; writes copy and literal records to $out.
Usage:
my $stats = App::FileSync::Delta::diff($abs, $signature, $spool);
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};
API SPECIFICATION
INPUT
[
{ name => 'path', type => 'string', min => 1 },
{ name => 'signature', type => 'hashref' },
{ name => 'out', type => 'handle' },
]
OUTPUT
{
type => 'hashref',
schema => {
literal => { type => 'integer', min => 0 },
copy => { type => 'integer', min => 0 },
ops => { type => 'integer', min => 0 },
},
}
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
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)
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
write_copy
Documented with "write_literal" below.
write_literal
Purpose: write one delta record.
Args: write_copy($out, $offset, $length); write_literal($out, $bytes) (an empty literal writes nothing).
Returns: 1.
Side Effects: writes to $out.
Usage:
App::FileSync::Delta::write_copy($spool, $offset, $length);
App::FileSync::Delta::write_literal($spool, $bytes);
EXAMPLE
App::FileSync::Delta::write_literal($out, "new text\n");
API SPECIFICATION
INPUT
[
{ name => 'out', type => 'handle' },
{ name => 'offset', type => 'integer', min => 0 },
{ name => 'length', type => 'integer', min => 0 },
]
OUTPUT
{ type => 'integer', min => 1, max => 1 }
MESSAGES
Key Meaning Resolution
delta.write the record cannot be written check space for the spool
FORMAL SPECIFICATION
write_copy(o, n): out' = out ⁀ "C" ⁀ be32(o) ⁀ be32(n)
write_literal(b): out' = out ⁀ "L" ⁀ be32(#b) ⁀ b if b ≠ ⟨⟩
read_op
Purpose: read the next delta record.
Args: $in, a handle positioned at a record.
Returns: ('copy', $offset, $length), ('literal', $bytes), or the empty list at the end. A list because callers loop with while (my ($type, @rest) = read_op($in)).
Side Effects: advances $in.
Usage:
while (my ($type, @rest) = App::FileSync::Delta::read_op($spool)) { ... }
EXAMPLE
my ($type, $offset, $length) = App::FileSync::Delta::read_op($spool);
API SPECIFICATION
INPUT
[ { name => 'in', type => 'handle' } ]
OUTPUT
{ type => 'arrayref' }
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
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(⟨⟩) = ⟨⟩
apply
Purpose: rebuild a file from the old copy and a spooled delta.
Args: $old_path, $in (the delta), $out (where the new content goes).
Returns: { size }, the bytes written.
Side Effects: reads $old_path and $in; writes $out.
Usage:
my $result = App::FileSync::Delta::apply($old, $spool, $out);
EXAMPLE
open my $out, '>', '/tmp/rebuilt.bin';
App::FileSync::Delta::apply('/tmp/old.bin', $spool, $out);
API SPECIFICATION
INPUT
[
{ name => 'old_path', type => 'string', min => 1 },
{ name => 'in', type => 'handle' },
{ name => 'out', type => 'handle' },
]
OUTPUT
{ type => 'hashref', schema => { size => { type => 'integer', min => 0 } } }
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
FORMAL SPECIFICATION
apply(old, R) = ⁀/ ⟨ bytes(r) | r ← R ⟩
where bytes(C(o, n)) = FS(old)[o .. o + n − 1], bytes(L(b)) = b
read_exact
Purpose: read exactly $length bytes.
Args: $fh, $length, $what (named in errors).
Returns: the bytes.
Side Effects: advances $fh.
Usage:
my $data = App::FileSync::Delta::read_exact($fh, $block, $path);
EXAMPLE
my $header = App::FileSync::Delta::read_exact($spool, 8, 'delta');
API SPECIFICATION
INPUT
[
{ name => 'fh', type => 'handle' },
{ name => 'length', type => 'integer', min => 0 },
{ name => 'what', type => 'string' },
]
OUTPUT
{ type => 'string' }
MESSAGES
Key Meaning Resolution
io.read the read failed check the device
io.short fewer bytes than required the file was truncated
FORMAL SPECIFICATION
read_exact(fh, n, w) = take(n, rest(fh)) if #rest(fh) ≥ n
↑ io.short otherwise
LIMITATIONS
Pure Perl: the rolling loop runs at a few MB/s, which is why only
$SCAN_LIMITbytes after the matched prefix are scanned. An edit near the end of a large file whose earlier blocks also shifted is sent mostly literally.A block that moved by a whole number of blocks is found; a block that occurs twice in the receiver's file is matched to its first occurrence.
Offsets and lengths are 32-bit on the wire, so a delta cannot refer beyond 4 GiB into the receiver's file; larger files fall back to whole-file transfer of the remainder as literals.
AUTHOR
Nigel Horne, <njh at nigelhorne.com>
LICENCE AND COPYRIGHT
Copyright 2026 Nigel Horne.
Usage is subject to the GPL2 licence terms. If you use it, please let me know.