NAME
App::FileSync::Protocol - the line-delimited JSON wire protocol
SYNOPSIS
my $connection = App::FileSync::Protocol->new(in => $reader, out => $writer);
$connection->send({ cmd => 'hello', proto => 2 });
my $reply = $connection->recv();
$connection->send_data({ cmd => 'chunk', p => $path }, $bytes);
DESCRIPTION
Each control message is one JSON object on one line. A message announcing a payload carries len (the bytes on the wire) and, when the payload was deflated, z; the payload follows the line directly. The connection counts payload bytes in each direction for the transfer report.
When reading or writing fails because the pipe itself failed (the peer exited, ssh was refused), "transport_failed" becomes true, so a caller can show the transport's own diagnostics instead of a protocol error.
METHODS
new
Purpose: wrap a pair of handles as a protocol connection.
Args: in and out (handles), optional language.
Returns: the connection.
Side Effects: puts both handles in binary mode and makes out autoflush.
Usage:
my $connection = App::FileSync::Protocol->new(in => \*STDIN, out => \*STDOUT);
EXAMPLE
my $connection = App::FileSync::Protocol->new(in => $transport->reader, out => $transport->writer);
API SPECIFICATION
INPUT
{
in => { type => 'handle' },
out => { type => 'handle' },
language => { type => 'string', optional => 1 },
}
OUTPUT
{ type => 'object', isa => 'App::FileSync::Protocol' }
MESSAGES
Key Meaning Resolution
args.invalid a handle is missing pass in and out
FORMAL SPECIFICATION
new(in, out) = ⟨in, out, buffer ↦ ⟨⟩, closed ↦ false, sent ↦ 0, received ↦ 0⟩
send
Purpose: send one control message.
Args: \%message.
Returns: 1.
Side Effects: writes one line.
Usage:
$connection->send({ cmd => 'plan' });
EXAMPLE
$connection->send({ ok => 1, off => $offset });
API SPECIFICATION
INPUT
[
{ name => 'self', type => 'object' },
{ name => 'message', type => 'hashref' },
]
OUTPUT
{ type => 'integer', min => 1, max => 1 }
MESSAGES
Key Meaning Resolution
protocol.closed the connection was closed a caller bug
protocol.write_failed the pipe would not take it see the transport's stderr
FORMAL SPECIFICATION
pre ¬closed
out' = out ⁀ json(message) ⁀ "\n"
recv
Purpose: read one control message.
Args: none.
Returns: the decoded message hashref.
Side Effects: reads from in; blocks until a whole line arrives.
Usage:
my $reply = $connection->recv();
EXAMPLE
my $hello = $connection->recv();
die unless $hello->{ok};
API SPECIFICATION
INPUT
[ { name => 'self', type => 'object' } ]
OUTPUT
{ type => 'hashref' }
MESSAGES
Key Meaning Resolution
protocol.malformed the line is not JSON version mismatch or corrupt stream
protocol.not_object the JSON is not an object version mismatch
protocol.eof the peer closed the connection see the peer's log
protocol.read_failed the pipe failed see the transport's stderr
FORMAL SPECIFICATION
in = line ⁀ "\n" ⁀ rest ⇒ recv = json⁻¹(line) ∧ in' = rest
pre json⁻¹(line) ∈ OBJECT
send_bytes
Purpose: write raw bytes (the payload after a header).
Args: $bytes; empty or undef writes nothing.
Returns: 1.
Side Effects: writes to out. Not counted in "bytes_sent"; use "send_data" for counted payloads.
Usage:
$connection->send_bytes($payload);
EXAMPLE
$connection->send({ len => length $bytes });
$connection->send_bytes($bytes);
API SPECIFICATION
INPUT
[
{ name => 'self', type => 'object' },
{ name => 'bytes', type => 'string', optional => 1 },
]
OUTPUT
{ type => 'integer', min => 1, max => 1 }
MESSAGES
Key Meaning Resolution
protocol.closed the connection was closed a caller bug
protocol.write_failed the pipe would not take it see the transport's stderr
FORMAL SPECIFICATION
out' = out ⁀ bytes
send_data
Purpose: send a header and its payload, compressing when worthwhile.
Args: \%header, $bytes.
Returns: 1.
Side Effects: writes the header (with len and, if compressed, z) and the payload; adds the wire length to "bytes_sent".
Usage:
$connection->send_data({ cmd => 'chunk', p => $path }, $bytes);
EXAMPLE
$connection->send_data({ ok => 1, eof => 1 }, $last_chunk);
API SPECIFICATION
INPUT
[
{ name => 'self', type => 'object' },
{ name => 'message', type => 'hashref', optional => 1 },
{ name => 'bytes', type => 'string', optional => 1 },
]
OUTPUT
{ type => 'integer', min => 1, max => 1 }
MESSAGES
As for "send".
FORMAL SPECIFICATION
let (w, z) = deflate(bytes)
out' = out ⁀ json(header ⊕ {len ↦ #w} ⊕ (z ⇒ {z ↦ 1})) ⁀ "\n" ⁀ w
sent' = sent + #w
read_data
Purpose: read the payload a header announced.
Args: \%header as returned by "recv".
Returns: the payload, decompressed if it was sent compressed.
Side Effects: reads len bytes; adds them to "bytes_received".
Usage:
my $bytes = $connection->read_data($header);
EXAMPLE
my $reply = $connection->recv();
my $bytes = $connection->read_data($reply);
API SPECIFICATION
INPUT
[
{ name => 'self', type => 'object' },
{ name => 'message', type => 'hashref' },
]
OUTPUT
{ type => 'string' }
MESSAGES
Key Meaning Resolution
protocol.bad_length len is not a number or exceeds 64 MiB a peer bug
protocol.inflate the payload cannot be decompressed a corrupt stream
protocol.corrupt the compressed payload is corrupt a corrupt stream
protocol.too_large it would inflate beyond 64 MiB a hostile or buggy peer
FORMAL SPECIFICATION
pre header.len ∈ 0 .. MAX_WIRE
read_data(h) = inflate(take(h.len, in)) if h.z
= take(h.len, in) otherwise
received' = received + h.len
read_line
Documented with "read_bytes" below.
read_bytes
Purpose: read up to the next newline, or exactly $length bytes.
Args: read_bytes($length).
Returns: the line (without its newline) or the bytes.
Side Effects: consumes them from the buffer, reading from in as needed.
Usage:
my $line = $connection->read_line();
my $bytes = $connection->read_bytes(1024);
EXAMPLE
my $payload = $connection->read_bytes($header->{len});
API SPECIFICATION
INPUT
[
{ name => 'self', type => 'object' },
{ name => 'length', type => 'integer', min => 0 },
]
OUTPUT
{ type => 'string' }
MESSAGES
Key Meaning Resolution
protocol.negative a negative length was asked for a caller bug
protocol.eof the peer closed the connection see the peer's log
protocol.read_failed the pipe failed see the transport's stderr
FORMAL SPECIFICATION
read_line: in = l ⁀ "\n" ⁀ r ∧ "\n" ¬in; ran l ⇒ result = l ∧ in' = r
read_bytes(n): in = b ⁀ r ∧ #b = n ⇒ result = b ∧ in' = r
bytes_sent
Documented with "bytes_received" below.
bytes_received
Purpose: payload bytes that crossed the wire in each direction.
Args: none.
Returns: a byte count (headers and control messages excluded).
Side Effects: none.
Usage:
printf "sent %d bytes\n", $connection->bytes_sent;
EXAMPLE
my %summary = (bytes_sent => $connection->bytes_sent, bytes_received => $connection->bytes_received);
API SPECIFICATION
INPUT
[ { name => 'self', type => 'object' } ]
OUTPUT
{ type => 'integer', min => 0 }
MESSAGES
None.
FORMAL SPECIFICATION
bytes_sent = sent ; bytes_received = received
transport_failed
Purpose: whether the last failure was the pipe rather than the protocol.
Args: none.
Returns: 1 after a read or write on the underlying handles failed, or the peer closed the connection; otherwise 0.
Side Effects: none.
Usage:
my $detail = $connection->transport_failed ? $transport->error_text : undef;
EXAMPLE
eval { $connection->recv() } or print "pipe failed\n" if $connection->transport_failed;
API SPECIFICATION
INPUT
[ { name => 'self', type => 'object' } ]
OUTPUT
{ type => 'integer', min => 0, max => 1 }
MESSAGES
None.
FORMAL SPECIFICATION
transport_failed ⇔ ∃ an I/O failure or end of file on in or out
close
Purpose: close both handles.
Args: none.
Returns: 1, or nothing if already closed.
Side Effects: closes in and out; later sends raise protocol.closed.
Usage:
$connection->close;
EXAMPLE
$connection->send({ cmd => 'done' });
$connection->close;
API SPECIFICATION
INPUT
[ { name => 'self', type => 'object' } ]
OUTPUT
{ type => 'integer', optional => 1 }
MESSAGES
None; close errors are ignored, as the session is over.
FORMAL SPECIFICATION
closed' = true
LIMITATIONS
There is no cap on the length of a control line, because the plan for a large tree is one line; see "LIMITATIONS" in App::FileSync.
The size check on a compressed payload happens after inflating it, so a hostile peer can make the receiver allocate a large buffer once. The peer is authenticated by ssh before it can send anything.
The method names
send,recvandcloseshadow Perl builtins; call them as methods only.
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.