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:
0success,1warning/error,2fatal error. Nonzero acks are followed by a message terminated by\n, bounded by ScpLimits.errorTextMax. - Timestamp record (
Tmode, optional):T<mtime> 0precedes the header when preserved times are requested; it is acknowledged like any other record.<atime> 0\n
- Header record:
C<mode> <size> <name>\nwith octal mode,decimal byte count, and a single base-name component.
- Data phase: exactly
sizeraw bytes followed by oneterminating 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
Raised for SCP contract violations: unsafe pathnames, malformed construction inputs, or protocol framing violations.
this(string msg, string file = __FILE__, size_t line = __LINE__)Constructs an SCP exception.SCP limits from the support matrix.
Direction of a single-file SCP transfer.
Options for remote command construction.
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.
bool applyModeApply the C-header permission bits to the local filebool applyTimesPreserve timestamps (T records; requests remote -p)Parameters of a single-file send (local to remote).
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 streameduint 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 flagsProduces 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).
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 failureScpMetadataPolicy metadataMetadata policy applied to the local fileTyped SCP operation error (local or remote).
string messageHuman-readable description; empty when no error occurredTerminal and non-terminal states of an SCP operation.
Acknowledgment and error statuses carried by single status bytes.
A T timestamp record (seconds resolution).
ulong mtimeSecondsModification time in seconds since the epochulong atimeSecondsAccess time in seconds since the epochA C file header record.
uint modePermission bits as transmitted (low 07777 meaningful)ulong sizeDeclared data byte countimmutable(ubyte)[] nameBase name; safe per isSafeLocalBaseNameOne parsed stream event from ScpStreamParser.
ScpEventKind kindEvent kindubyte 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.
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.
private ubyte[] pending_private ulong dataRemaining_private bool inData_private ubyte recordLead_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.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.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).
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_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 complete()bool pumpInput()void handleEvent(ScpEvent event)void handlePeerEof()void evaluateCompletion()bool pumpEmit()bool flushOutbound()void queueBytes(scope const(ubyte)[] bytes)this(SshChannel channel, ScpSendOptions options, ScpDataSource source)Constructs a send operation on an accepted exec channel running the remote sink.SendStepStaged, 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.
private int fd_private int parentFd_private string path_private string baseName_private string stagingName_private bool created_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.bool startsWithSlash() const @safe pure nothrowWhether the path starts at the filesystem root.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 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).
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_bool cleanupPending() @property constReturns: whether a private staging name still requires cleanup.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 complete()bool pumpInput()void handleEvent(ScpEvent event)void handleData(scope const(ubyte)[] data)void acceptHeader(ScpHeaderRecord header)void handleTerminator(ScpEvent event)void handlePeerEof()bool pumpEmit()bool flushOutbound()void queueBytes(scope const(ubyte)[] bytes)this(SshChannel channel, ScpReceiveOptions options)Constructs a receive operation on an accepted exec channel running the remote source.RecvStepFunctions 8
string shellQuote(scope const(ubyte)[] argument)Quotes one remote command argument for a POSIX shell.void validateScpRemotePathname(scope const(ubyte)[] path)Validates a remote pathname for command construction.bool 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.string buildScpCommand(ScpDirection direction,
scope const(ubyte)[] remotePath,
ScpCommandOptions options = ScpCommandOptions.init)Builds the remote scp command for a single-file transfer.ubyte[] encodeStatusRecord(ubyte status, string message = "")Encodes one status record (acknowledgment, end marker, or error).bool remoteCompletionError(const SshExitStatus exit,
ref ScpError problem)Classifies the remote command's exit information.