ddn.net.scp

Restricted legacy SCP compatibility (M4).

SCP is an rcp-style protocol spoken over an accepted exec channel whose command runs a compatible remote scp executable; a Dropbear SSH daemon alone is not sufficient. The remote side runs scp -t <target> to receive a file from us (ScpDirection.SEND) or scp -f to send a file to us (ScpDirection.RECEIVE). Modern OpenSSH scp clients default to SFTP; interop tooling must force legacy mode (scp -O) when the OpenSSH CLI is used against this module's servers. This module speaks only the legacy wire protocol.

Protocol contract

The protocol is a sequence of control records and bulk data with a one-byte acknowledgment after every record:

  • Acknowledgment byte: 0 success, 1 warning/error,

    2 fatal error. Nonzero acks are followed by a message terminated by \n, bounded by ScpLimits.errorTextMax.

  • Timestamp record (T mode, optional): T<mtime> 0

    <atime> 0\n precedes the header when preserved times are requested; it is acknowledged like any other record.

  • Header record: C<mode> <size> <name>\n with octal mode,

    decimal byte count, and a single base-name component.

  • Data phase: exactly size raw bytes followed by one

    terminating success byte from the sender, then one acknowledgment byte from the receiver.

  • End of transfer: the sender half-closes its write direction

    after the last per-file acknowledgment; the receiver observes EOF. A bare status byte is not an end-of-list marker and is a protocol error for OpenSSH-derived sinks.

Byte counts, progress, and completion

Operations report streamed data bytes separately from acknowledged bytes. Channel acceptance of a write is never completion: a send completes only after the remote sink acknowledges the data terminator and the remote command then completes cleanly once our write direction closed; a receive completes only after the exact declared size and its terminator arrived. A source that delivers fewer or more bytes than its declared size is a protocol failure, never a silent truncation.

Cancellation, stderr, and EOF/close ownership

ScpSend.cancel / ScpReceive.cancel stop scheduling and fail the operation; already-accepted channel bytes may still drain, and the caller then closes the channel. The remote command writes diagnostics to the channel's extended data (stderr) stream; the transport drains it, bounded, and never parses it as protocol. Premature channel EOF is an error during an active transfer and is interpreted against the transfer state. The caller owns the channel and the exec/session setup; this module never closes the channel and never runs commands.

Command construction

Remote command arguments are built by buildScpCommand with POSIX shell quoting (shellQuote) and leading-option handling: relative pathnames starting with - are rewritten with a ./ prefix so remote scp never treats them as options. Pathnames containing NUL bytes, empty pathnames, and oversize inputs are rejected rather than quoted.

Recursion

Single-file transfers only. Recursive directory mode (scp -r) is deliberately not implemented and is explicitly deferred: directory-enter (D... and directory-leave (E records are rejected as protocol violations by the framing layer, so a hostile tree can never expand into unbounded local creation or nested traversal state. Recipients must not pass -r to the remote program and senders must only announce regular-file headers.

Types 19

classScpException : Exception

Raised for SCP contract violations: unsafe pathnames, malformed construction inputs, or protocol framing violations.

Constructors
this(string msg, string file = __FILE__, size_t line = __LINE__)Constructs an SCP exception.
enumScpLimits : size_t

SCP limits from the support matrix.

pathMax = 4096Maximum remote pathname bytes accepted by command construction
commandMax = 8192Maximum constructed remote command length
headerMax = 8192Maximum control header line length, including the trailing newline
errorTextMax = 8192Maximum remote error message length
nameMax = 255Maximum local destination base name length
dataChunkMax = 32768Maximum bytes of file data delivered per parsed data event
bufferMax = 65536Maximum bytes held in the bounded send buffer

Direction of a single-file SCP transfer.

SENDLocal to remote: the remote runs scp -t as a sink
RECEIVERemote to local: the remote runs scp -f as a source

Options for remote command construction.

Fields
bool preserveTimesRequest preserved modification times (-p)
string scpProgramRemote SCP program name or path (shell-quoted on use)

Explicit metadata policy for the transferred file. Nothing is applied unless the corresponding flag is set: permission bits and timestamps are never implied.

Fields
bool applyModeApply the C-header permission bits to the local file
bool applyTimesPreserve timestamps (T records; requests remote -p)

Parameters of a single-file send (local to remote).

Fields
const(ubyte)[] sourceNameBase name announced in the C header; must be a safe single component (see isSafeLocalBaseName)
ulong sourceSizeExact number of data bytes that will be streamed
uint fileModePermission bits announced in the C header (default 0644)
ulong mtimeSecondsModification time in seconds announced in the T record (sent when metadata.applyTimes is set)
ulong atimeSecondsAccess time in seconds announced in the T record (sent when metadata.applyTimes is set)
ScpMetadataPolicy metadataMetadata policy applied on the remote side by request flags
aliasScpDataSource = size_t delegate(scope ubyte[] destination)

Produces send-side file data into the provided buffer. Returning zero marks the source exhausted; partial production is legal and the operation asks again on later pumps. Throwing from the delegate fails the operation with the exception's message.

Parameters of a single-file receive (remote to local).

Fields
string destinationPathCaller-selected local destination file path. Its parent must be owned by the effective user and not group/world writable. Data is staged there and atomically published without replacing a name.
const(ubyte)[] expectedNameExpected remote base name; when non-empty, a C header announcing any other name is a protocol failure
ScpMetadataPolicy metadataMetadata policy applied to the local file
structScpError

Typed SCP operation error (local or remote).

Fields
string messageHuman-readable description; empty when no error occurred
Methods
bool isNone() const pure nothrow @safe @nogcReturns: true when no error occurred.

Terminal and non-terminal states of an SCP operation.

PENDINGConstructed but not yet pumped
RUNNINGTransfer is being pumped
DONEFinished successfully
FAILEDTerminated by protocol failure, channel failure, or cancellation
enumScpAck : ubyte

Acknowledgment and error statuses carried by single status bytes.

OK = 0Success: acknowledgment or end of the file list
ERROR = 1Nonfatal error: status byte followed by a message line
FATAL = 2Fatal error: status byte followed by a message line

A T timestamp record (seconds resolution).

Fields
ulong mtimeSecondsModification time in seconds since the epoch
ulong atimeSecondsAccess time in seconds since the epoch

A C file header record.

Fields
uint modePermission bits as transmitted (low 07777 meaningful)
ulong sizeDeclared data byte count
immutable(ubyte)[] nameBase name; safe per isSafeLocalBaseName
structScpEvent

One parsed stream event from ScpStreamParser.

Fields
ScpEventKind kindEvent kind
ubyte statusStatus byte (ScpEventKind.STATUS)
string messageDiagnostic message (ScpEventKind.STATUS with a nonzero status; empty otherwise)
ScpTimestampRecord timestampTimestamps (ScpEventKind.TIMESTAMP)
ScpHeaderRecord headerFile header (ScpEventKind.HEADER)
const(ubyte)[] dataData bytes (ScpEventKind.DATA); a slice of the bytes passed to ScpStreamParser.feed, valid only until that call returns.

Event kinds produced by ScpStreamParser.

STATUSStatus byte with optional diagnostic message
TIMESTAMPT timestamp record
HEADERC file header record
DATARaw file data bytes counted against the declared size

Incremental bounded parser for an SCP stream.

The parser consumes arbitrary byte fragments and delivers self-describing events through a sink: status records (acknowledg- ments, end markers, errors), T timestamp records, C headers, and raw data bytes while a data phase is active. Record lengths, numeric fields, and names are validated before use; malformed, truncated, or overflowing input raises ScpException exactly once and the parser must then be discarded.

File data phases are explicit: after a C header is accepted, the state machine calls ScpStreamParser.beginDataPhase with the declared size. Exactly that many bytes are then delivered as ScpEventKind.DATA events before the stream returns to record framing, where the sender's terminator status record arrives. The distinction between an acknowledgment status record and an end-of-list status record is positional and belongs to the transfer state machine; the parser reports both as ScpEventKind.STATUS.

Fields
private ubyte[] pending_
private ulong dataRemaining_
private bool inData_
private ubyte recordLead_
Methods
void feed(scope const(ubyte)[] bytes, scope ScpEventSink sink)Feeds raw stream bytes, delivering every event they complete.
void beginDataPhase(ulong declaredSize)Declares that exactly declaredSize data bytes follow an accepted header. A zero size skips the data phase entirely; the sender's terminator status record follows directly.
ulong dataRemaining() @property const pure nothrow @safe @nogcReturns: unread data bytes still expected in the active data phase.
bool inDataPhase() @property const pure nothrow @safe @nogcReturns: true while file data bytes are expected.
private size_t contentLimit(ubyte lead) static pure nothrow @safe @nogc
private bool tryCompleteRecord(ScpEventSink sink)Attempts to complete one record from pending_. The pending buffer is cleared before the sink runs so state transitions such as beginDataPhase are legal from inside the sink.
private string parseMessage(scope const(ubyte)[] line) static
private ScpTimestampRecord parseTimestampLine( scope const(ubyte)[] line) static
private ScpHeaderRecord parseHeaderLine( scope const(ubyte)[] line) static
private void parseSpace(scope const(ubyte)[] line, ref size_t index) static
private ulong parseDecimal(scope const(ubyte)[] line, ref size_t index) static
private uint parseOctal(scope const(ubyte)[] line, ref size_t index) static
classScpSend

Single-file SCP send (local to remote) over an accepted exec channel.

The caller owns the session, the exec request (buildScpCommand with ScpDirection.SEND), and the channel; the operation never closes the channel. The operation sequences the optional timestamp record, the file header, exactly ScpSendOptions.sourceSize data bytes pulled from the caller's source within a bounded buffer, the data terminator, and a half-close of the channel's write direction as the end of transfer, honoring the remote sink's acknowledgment after every record. Completion requires protocol success and the remote command's clean completion: source exhaustion before the declared size, channel EOF before the end byte, remote error or fatal records, and nonzero or signal exits all fail the operation with distinct messages. Cancellation is cooperative and terminal; already-accepted channel bytes may still drain and the caller then closes the channel. Single-file only: recursive directory mode is not implemented (see the module documentation).

Fields
private SshChannel channel_
private ScpSendOptions options_
private ScpDataSource source_
private ScpProgressState state_
private ScpError error_
private ulong acceptedBytes_
private ulong acknowledgedBytes_
private SendStep step_
private ScpStreamParser parser_
private ubyte[] outbound_
private size_t outboundOffset_
private ulong streamed_
private bool stderrConfigured_
Methods
ScpProgressState state() @property constReturns: current operation state.
bool done() @property constReturns: true when the operation reached a terminal state.
bool succeeded() @property constReturns: true when the operation succeeded.
ScpError error() @property constReturns: the error when failed; empty otherwise.
ulong acceptedBytes() @property constReturns: file data bytes taken from the source so far.
ulong acknowledgedBytes() @property constReturns: file data bytes confirmed by the remote acknowledgment.
void cancel()Cancels caller interest in this transfer. Terminal: the operation fails with a cancellation error and the caller closes the channel.
bool pump()Advances the transfer as far as channel windows and protocol state allow; never blocks and never reenters the caller. Malformed remote records fail the operation through error and the cleanup path;...
void fail(ScpError error)
void complete()
private bool pumpInput()
private void handleEvent(ScpEvent event)
private void handlePeerEof()
private void evaluateCompletion()
private bool pumpEmit()
private bool flushOutbound()
private void queueBytes(scope const(ubyte)[] bytes)
Constructors
this(SshChannel channel, ScpSendOptions options, ScpDataSource source)Constructs a send operation on an accepted exec channel running the remote sink.
Nested Templates
SendStep
private structScpLocalFile

Staged, race-safe local destination file for SCP receives.

A private staging name is created in a pinned, trusted parent. On success linkat publishes it at the destination without replacing an existing name. Once that link succeeds publication is committed; failure to unlink the private staging name is deferred to later cleanup and cannot turn the published transfer into failure. Cleanup only unlinks the private staging name, never the public destination name. Local file support is POSIX-only in this version.

Fields
private int fd_
private int parentFd_
private string path_
private string baseName_
private string stagingName_
private bool created_
Methods
bool createExclusive(string path, ref string errorMessage)Creates a private staging file in the destination parent.
bool openParentDirectory(string path, ref string errorMessage)Resolves the parent directory of the destination safely.
private bool startsWithSlash() const @safe pure nothrowWhether the path starts at the filesystem root.
private void closeParent() @trusted nothrowCloses the pinned parent directory.
bool writeAll(scope const(ubyte)[] data, ref string errorMessage)Writes the whole buffer to the destination.
bool finish(ScpMetadataPolicy policy, uint mode, ulong mtimeSeconds, ulong atimeSeconds, ref string errorMessage)Applies caller-authorized metadata and closes the file.
bool cleanupPending() @property constReturns: whether a private staging name still needs removal.
bool cleanup(ref string errorMessage)Closes the data descriptor and removes only the private staging name. A failed unlink keeps the pinned parent and name for a bounded retry.

Single-file SCP receive (remote to local) over an accepted exec channel.

The caller owns the session, the exec request (buildScpCommand with ScpDirection.RECEIVE), and the channel; the operation never closes the channel. The operation accepts exactly one file whose C header name matches ScpReceiveOptions.expectedName when set, streams exactly the declared size to the caller-selected destination, and applies only caller-authorized metadata. Content is written under a private staging name in a trusted parent and published without replacement (see ScpLocalFile); a pre-existing destination is refused.

Completion requires the data terminator, the source's end of transfer (channel EOF), and the remote command's clean completion. Local create, write, metadata, or close failures, unsafe or unexpected names, malformed records, and premature EOF all fail the operation with the staging file removed, so incomplete content is never published or reported successful; a file whose content completed exactly as declared is kept even when the later remote completion check fails. Local failures are reported to the remote source with an error record in place of the per-file acknowledgment, preserving the protocol's error semantics. Cancellation is cooperative and terminal with the same cleanup. Single-file only: directory-enter and directory-leave records are protocol failures (see the module documentation).

Fields
private SshChannel channel_
private ScpReceiveOptions options_
private ScpProgressState state_
private ScpError error_
private ulong acceptedBytes_
private ulong acknowledgedBytes_
private RecvStep step_
private ScpStreamParser parser_
private ScpLocalFile file_
private ubyte[] outbound_
private size_t outboundOffset_
private bool stderrConfigured_
private bool headerSeen_
private bool haveTimes_
private ScpTimestampRecord pendingTimes_
private ulong declaredSize_
private uint declaredMode_
private string localError_
private string cleanupError_
Methods
ScpProgressState state() @property constReturns: current operation state.
bool done() @property constReturns: true when the operation reached a terminal state.
bool succeeded() @property constReturns: true when the operation succeeded.
ScpError error() @property constReturns: the error when failed; empty otherwise.
bool cleanupPending() @property constReturns: whether a private staging name still requires cleanup.
string cleanupError() @property constReturns: the latest staging cleanup failure, if any.
bool retryCleanup()Retries removal of a private staging name without changing the transfer's terminal result.
ulong acceptedBytes() @property constReturns: file data bytes accepted from the remote source so far.
ulong acknowledgedBytes() @property constReturns: file data bytes acknowledged to the remote source so far.
void cancel()Cancels caller interest in this transfer. Terminal: the operation fails with a cancellation error and removes the destination file it created.
bool pump()Advances the transfer as far as channel data and protocol state allow; never blocks and never reenters the caller. Malformed remote records raise ScpException; every other failure is reported throu...
void fail(ScpError error)
void complete()
private bool pumpInput()
private void handleEvent(ScpEvent event)
private void handleData(scope const(ubyte)[] data)
private void acceptHeader(ScpHeaderRecord header)
private void handleTerminator(ScpEvent event)
private void handlePeerEof()
private bool pumpEmit()
private bool flushOutbound()
private void queueBytes(scope const(ubyte)[] bytes)
Constructors
this(SshChannel channel, ScpReceiveOptions options)Constructs a receive operation on an accepted exec channel running the remote source.
Nested Templates
RecvStep

Functions 8

fnstring shellQuote(scope const(ubyte)[] argument)Quotes one remote command argument for a POSIX shell.
fnvoid validateScpRemotePathname(scope const(ubyte)[] path)Validates a remote pathname for command construction.
fnbool isSafeLocalBaseName(scope const(ubyte)[] name) pure nothrow @safe @nogcReports whether a protocol-supplied name is safe to use as a local destination base name and as a C-header name.
fnstring buildScpCommand(ScpDirection direction, scope const(ubyte)[] remotePath, ScpCommandOptions options = ScpCommandOptions.init)Builds the remote scp command for a single-file transfer.
fnubyte[] encodeStatusRecord(ubyte status, string message = "")Encodes one status record (acknowledgment, end marker, or error).
fnubyte[] encodeTimestampRecord(ScpTimestampRecord record)Encodes one T timestamp record.
fnubyte[] encodeHeaderRecord(ScpHeaderRecord record)Encodes one C file header record.
private fnbool remoteCompletionError(const SshExitStatus exit, ref ScpError problem)Classifies the remote command's exit information.