ProtoCore v1.0.16
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
ftp.h
Go to the documentation of this file.
1// ProtoCore v1.0.16 - Copyright (C) 2026 Douglas Quigg (dstroy0) <dquigg123@gmail.com>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4/**
5 * @file ftp.h
6 * @brief FTP client wire codec (RFC 959 + RFC 2428 + RFC 3659), PROTOCORE_ENABLE_FTP.
7 *
8 * The pure protocol layer of an FTP client: build control-channel commands, parse the
9 * (possibly multiline) 3-digit reply, and decode the PASV / EPSV data-channel address the
10 * server hands back. A device can then push/pull files - e.g. drip a `.nc` program to a CNC
11 * controller's FTP program store (Fanuc / Haas / Mazak / Heidenhain all expose one), fetch a
12 * config, or archive a log. No heap, no stdlib; the two sockets (control + data) are the
13 * application's - this is only the bytes on the wire, so it is fully host-testable.
14 *
15 * FTP replies (RFC 959 sec 4.2): a single line is `NNN<SP>text<CRLF>`; a multiline reply is
16 * `NNN-text<CRLF>` continuation lines `... <CRLF>` and a final `NNN<SP>text<CRLF>` (the same
17 * code followed by a space marks the end). Passive mode: `227 ...(h1,h2,h3,h4,p1,p2)` gives the
18 * data address (ip = h1.h2.h3.h4, port = p1*256+p2); extended passive `229 ...(|||port|)`
19 * (RFC 2428) gives just the port on the control host.
20 *
21 * @author Douglas Quigg (dstroy0)
22 * @date 2026
23 */
24
25#ifndef PROTOCORE_FTP_H
26#define PROTOCORE_FTP_H
27
28#include "protocore_config.h" // the entry point: protocore_types.h for the widths
29
30#if PROTOCORE_ENABLE_FTP
31
33
34// This module holds nothing between calls, so it carves no borrow and states none. An entry
35// takes one all the same, and never reads it, so every namespace in the tree is invoked the
36// same way.
37
38/** @brief First digit of a reply code (1 preliminary, 2 complete, 3 intermediate, 4/5 error), or 0. */
39static inline int protocore_ftp_reply_class(int code)
40{
41 return (code >= 100 && code <= 599) ? code / 100 : 0;
42}
43
44/** @brief A 2xx positive-completion reply. */
45static inline proto_bool protocore_ftp_reply_ok(int code)
46{
47 return protocore_ftp_reply_class(code) == 2;
48}
49
50/** @brief What build_command takes: buf, cap, verb, arg. */
51typedef struct
52{
53 char *buf;
54 size_t cap;
55 const char *verb;
56 const char *arg; ///< the argument, or nullptr / "" for a bare verb (no trailing space)
57} FtpBuildCommandArgs;
58
59/** @brief What build_port takes: buf, cap, ip, port. */
60typedef struct
61{
62 char *buf;
63 size_t cap;
64 const uint8_t *ip; ///< 4 bytes.
65 uint16_t port;
66} FtpBuildPortArgs;
67
68/** @brief What build_eprt takes: buf, cap, ip_str, ipv6, port. */
69typedef struct
70{
71 char *buf;
72 size_t cap;
73 const char *ip_str; ///< dotted-decimal IPv4 or RFC 4291 IPv6 text (copied verbatim)
74 proto_bool ipv6; ///< false => net-prt 1 (IPv4), true => net-prt 2 (IPv6)
75 uint16_t port;
76} FtpBuildEprtArgs;
77
78/** @brief What parse_reply takes: buf, len, code, consumed. */
79typedef struct
80{
81 const char *buf;
82 size_t len;
83 int *code;
84 size_t *consumed;
85} FtpParseReplyArgs;
86
87/** @brief What parse_pasv takes: buf, len, ip, port. */
88typedef struct
89{
90 const char *buf;
91 size_t len;
92 uint8_t *ip; ///< 4 bytes.
93 uint16_t *port;
94} FtpParsePasvArgs;
95
96/** @brief What parse_epsv takes: buf, len, port. */
97typedef struct
98{
99 const char *buf;
100 size_t len;
101 uint16_t *port;
102} FtpParseEpsvArgs;
103
104/**
105 * @brief FTP client wire codec (RFC 959 + RFC 2428 + RFC 3659), PROTOCORE_ENABLE_FTP.
106 *
107 * A caller sets the members a call takes, invokes it through ::Ftp with the bytes it runs
108 * out of, and reads the outcome off the same handle.
109 *
110 * Ftp.build_command_args.buf = ...;
111 * Ftp.build_command_args.cap = ...;
112 * Ftp.build_command_args.verb = ...;
113 * Ftp.build_command_args.arg = ...;
114 * Ftp.build_command(work);
115 * // Ftp.n is what the call reports
116 *
117 * @var FtpNs::build_command_args what build_command takes: buf, cap, verb, arg
118 * @var FtpNs::build_port_args what build_port takes: buf, cap, ip, port
119 * @var FtpNs::build_eprt_args what build_eprt takes: buf, cap, ip_str, ipv6, port
120 * @var FtpNs::parse_reply_args what parse_reply takes: buf, len, code, consumed
121 * @var FtpNs::parse_pasv_args what parse_pasv takes: buf, len, ip, port
122 * @var FtpNs::parse_epsv_args what parse_epsv takes: buf, len, port
123 * @var FtpNs::ok true if a complete reply is present; false if the buffer holds only ...
124 * @var FtpNs::n bytes written (excluding the NUL terminator), or 0 on overflow / ...
125 * @var FtpNs::build_command build a control command line: `VERB<CRLF>` or `VERB<SP>ARG<CRLF>`. ...
126 * @var FtpNs::build_port build an active-mode `PORT h1,h2,h3,h4,p1,p2<CRLF>` from an IPv4 ...
127 * @var FtpNs::build_eprt build an extended active-mode ...
128 * @var FtpNs::parse_reply detect and measure a complete control-channel reply at the head of ...
129 * @var FtpNs::parse_pasv decode the data address from a `227` passive-mode reply. Reads the ...
130 * @var FtpNs::parse_epsv decode the port from a `229` extended-passive reply ...
131 *
132 * @c work is bytes the CALLER holds. This module reads none of them: it carries nothing
133 * between calls, so there is no state to keep and nothing to wipe. The parameter is there so
134 * a caller drives every namespace the same way.
135 */
136typedef struct
137{
138 FtpBuildCommandArgs build_command_args;
139 FtpBuildPortArgs build_port_args;
140 FtpBuildEprtArgs build_eprt_args;
141 FtpParseReplyArgs parse_reply_args;
142 FtpParsePasvArgs parse_pasv_args;
143 FtpParseEpsvArgs parse_epsv_args;
144 proto_bool ok;
145 size_t n;
146} FtpVars;
147
148/** @brief The operands and the outcome. */
149extern FtpVars FtpV;
150
151/** @brief The entries. */
152typedef struct
153{
154 void (*const build_command)(uint8_t *work);
155 void (*const build_port)(uint8_t *work);
156 void (*const build_eprt)(uint8_t *work);
157 void (*const parse_reply)(uint8_t *work);
158 void (*const parse_pasv)(uint8_t *work);
159 void (*const parse_epsv)(uint8_t *work);
160} FtpNs;
161
162// What the table binds, defined once in the .c and taking one parameter each: everything
163// else an entry needs is an operand in FtpV or a region of the borrow at a fixed offset.
164void protocore_ftp_build_command(uint8_t *work);
165void protocore_ftp_build_port(uint8_t *work);
166void protocore_ftp_build_eprt(uint8_t *work);
167void protocore_ftp_parse_reply(uint8_t *work);
168void protocore_ftp_parse_pasv(uint8_t *work);
169void protocore_ftp_parse_epsv(uint8_t *work);
170
171// `static const`, initialised HERE rather than `extern` against a definition in the .c: a
172// const object whose initializer every translation unit can see is a COMPILE-TIME FACT, so
173// `Ftp.build_command(work)` resolves to a named function and becomes a DIRECT call. An extern table
174// leaves the call indirect and the symbol live at every level, -O2 -flto included.
175static const FtpNs Ftp __attribute__((unused)) = {
176 .build_command = protocore_ftp_build_command,
177 .build_port = protocore_ftp_build_port,
178 .build_eprt = protocore_ftp_build_eprt,
179 .parse_reply = protocore_ftp_parse_reply,
180 .parse_pasv = protocore_ftp_parse_pasv,
181 .parse_epsv = protocore_ftp_parse_epsv,
182};
183
185
186#endif // PROTOCORE_ENABLE_FTP
187
188#endif // PROTOCORE_FTP_H
#define PROTOCORE_BEGIN_DECLS
Give a header's declarations C linkage, so their symbol names carry no parameter types.
Definition types.h:96
_Bool proto_bool
The truth value.
Definition types.h:64
#define PROTOCORE_END_DECLS
Definition types.h:97