ProtoCore v0.0.2
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
ftp.cpp
Go to the documentation of this file.
1// Copyright (C) 2026 Douglas Quigg (dstroy0) <dquigg123@gmail.com>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4/**
5 * @file ftp.cpp
6 * @brief FTP client wire codec implementation (see ftp.h).
7 */
8
9#include "ftp.h"
10
11#if PC_ENABLE_FTP
12
13#include <string.h>
14
15static const size_t FTP_SENT = (size_t)-1; // "overflowed" sentinel threaded through the emitters
16
17// Append raw bytes; propagates the overflow sentinel.
18static size_t pc_ftp_emit(char *buf, size_t cap, size_t n, const char *s, size_t slen)
19{
20 // Overflow-safe bound: n <= cap is invariant (every non-sentinel return is <= cap), but guard
21 // n > cap explicitly so cap - n provably cannot underflow; written as subtraction so a huge
22 // slen cannot wrap n + slen.
23 if (n == FTP_SENT || n > cap || slen > cap - n) // GCOVR_EXCL_BR_LINE n > cap can't fire: every
24 {
25 return FTP_SENT; // caller passes n from 0 or a prior emit's return,
26 // which the invariant above keeps <= cap
27 }
28 // The guard above proves n + slen <= cap, so this write stays inside buf[0, cap). S3519 can't link
29 // buf's size to the separate cap parameter and follows an infeasible path (same FP as mms.cpp).
30 memcpy(buf + n, s, slen); // NOSONAR - bound proven above; analyzer follows an infeasible path
31 return n + slen;
32}
33
34// Append an unsigned decimal; propagates the overflow sentinel.
35static size_t pc_ftp_emit_uint(char *buf, size_t cap, size_t n, unsigned v)
36{
37 if (n == FTP_SENT)
38 {
39 return FTP_SENT;
40 }
41 char rev[10];
42 int ri = 0;
43 if (v == 0)
44 {
45 rev[ri++] = '0';
46 }
47 else
48 {
49 while (v)
50 {
51 rev[ri++] = (char)('0' + (v % 10));
52 v /= 10;
53 }
54 }
55 if ((size_t)ri > cap - n) // n <= cap invariant (checked above); subtraction form can't overflow
56 {
57 return FTP_SENT;
58 }
59 for (int k = 0; k < ri; k++)
60 {
61 buf[n + k] = rev[ri - 1 - k];
62 }
63 return n + (size_t)ri;
64}
65
66// Finish: on no overflow and room for the NUL, terminate and return the length; else 0.
67static size_t pc_ftp_finish(char *buf, size_t cap, size_t n)
68{
69 if (n == FTP_SENT || n >= cap) // no room for the NUL (n == cap); n <= cap invariant avoids n + 1 overflow
70 {
71 return 0;
72 }
73 buf[n] = 0;
74 return n;
75}
76
77size_t pc_ftp_build_command(char *buf, size_t cap, const char *verb, const char *arg)
78{
79 if (!buf || !verb || !verb[0])
80 {
81 return 0;
82 }
83 size_t n = 0;
84 n = pc_ftp_emit(buf, cap, n, verb, strnlen(verb, cap));
85 if (arg && arg[0])
86 {
87 n = pc_ftp_emit(buf, cap, n, " ", 1);
88 n = pc_ftp_emit(buf, cap, n, arg, strnlen(arg, cap));
89 }
90 n = pc_ftp_emit(buf, cap, n, "\r\n", 2);
91 return pc_ftp_finish(buf, cap, n);
92}
93
94size_t pc_ftp_build_port(char *buf, size_t cap, const uint8_t ip[4], uint16_t port)
95{
96 if (!buf || !ip)
97 {
98 return 0;
99 }
100 size_t n = 0;
101 n = pc_ftp_emit(buf, cap, n, "PORT ", 5);
102 for (int i = 0; i < 4; i++)
103 {
104 n = pc_ftp_emit_uint(buf, cap, n, ip[i]);
105 n = pc_ftp_emit(buf, cap, n, ",", 1);
106 }
107 n = pc_ftp_emit_uint(buf, cap, n, (unsigned)(port >> 8));
108 n = pc_ftp_emit(buf, cap, n, ",", 1);
109 n = pc_ftp_emit_uint(buf, cap, n, (unsigned)(port & 0xFF));
110 n = pc_ftp_emit(buf, cap, n, "\r\n", 2);
111 return pc_ftp_finish(buf, cap, n);
112}
113
114size_t pc_ftp_build_eprt(char *buf, size_t cap, const char *ip_str, bool ipv6, uint16_t port)
115{
116 if (!buf || !ip_str || !ip_str[0])
117 {
118 return 0;
119 }
120 size_t n = 0;
121 n = pc_ftp_emit(buf, cap, n, "EPRT |", 6);
122 n = pc_ftp_emit(buf, cap, n, ipv6 ? "2" : "1", 1);
123 n = pc_ftp_emit(buf, cap, n, "|", 1);
124 n = pc_ftp_emit(buf, cap, n, ip_str, strnlen(ip_str, cap));
125 n = pc_ftp_emit(buf, cap, n, "|", 1);
126 n = pc_ftp_emit_uint(buf, cap, n, port);
127 n = pc_ftp_emit(buf, cap, n, "|\r\n", 3);
128 return pc_ftp_finish(buf, cap, n);
129}
130
131static bool pc_ftp_is_3digit(const char *p)
132{
133 return p[0] >= '0' && p[0] <= '9' && p[1] >= '0' && p[1] <= '9' && p[2] >= '0' && p[2] <= '9';
134}
135
136static int pc_ftp_code3(const char *p)
137{
138 return (p[0] - '0') * 100 + (p[1] - '0') * 10 + (p[2] - '0');
139}
140
141// Index just past the LF of the line starting at @p start, or 0 if the line is not yet complete.
142static size_t pc_ftp_line_end(const char *buf, size_t len, size_t start)
143{
144 for (size_t i = start; i < len; i++)
145 {
146 if (buf[i] == '\n')
147 {
148 return i + 1;
149 }
150 }
151 return 0;
152}
153
154bool pc_ftp_parse_reply(const char *buf, size_t len, int *code, size_t *consumed)
155{
156 if (!buf || len < 4 || !pc_ftp_is_3digit(buf))
157 {
158 return false;
159 }
160 int first = pc_ftp_code3(buf);
161 char sep = buf[3];
162
163 if (sep == ' ')
164 {
165 size_t eol = pc_ftp_line_end(buf, len, 0);
166 if (!eol)
167 {
168 return false; // line not fully received
169 }
170 *code = first;
171 *consumed = eol;
172 return true;
173 }
174 if (sep != '-')
175 {
176 return false; // malformed: the separator must be SP or '-'
177 }
178
179 // Multiline: end at the first line that begins with the same code followed by a space.
180 size_t pos = pc_ftp_line_end(buf, len, 0);
181 if (!pos)
182 {
183 return false;
184 }
185 while (pos < len)
186 {
187 if (len - pos >= 4 && pc_ftp_is_3digit(buf + pos) && pc_ftp_code3(buf + pos) == first && buf[pos + 3] == ' ')
188 {
189 size_t eol = pc_ftp_line_end(buf, len, pos);
190 if (!eol)
191 {
192 return false; // terminator line not fully received
193 }
194 *code = first;
195 *consumed = eol;
196 return true;
197 }
198 size_t eol = pc_ftp_line_end(buf, len, pos);
199 if (!eol)
200 {
201 return false; // partial continuation line; need more
202 }
203 pos = eol;
204 }
205 return false; // no terminator yet
206}
207
208bool pc_ftp_parse_pasv(const char *buf, size_t len, uint8_t ip[4], uint16_t *port)
209{
210 if (!buf || !ip || !port)
211 {
212 return false;
213 }
214 size_t i = 0;
215 while (i < len && buf[i] != '(')
216 {
217 i++;
218 }
219 if (i >= len)
220 {
221 return false;
222 }
223 i++; // past '('
224
225 unsigned nums[6];
226 for (int ni = 0; ni < 6; ni++)
227 {
228 if (i >= len || buf[i] < '0' || buf[i] > '9')
229 {
230 return false; // the guard above guarantees at least one digit in this field
231 }
232 unsigned v = 0;
233 while (i < len && buf[i] >= '0' && buf[i] <= '9')
234 {
235 v = v * 10 + (unsigned)(buf[i] - '0');
236 if (v > 255)
237 {
238 return false;
239 }
240 i++;
241 }
242 nums[ni] = v;
243 if (ni < 5)
244 {
245 if (i >= len || buf[i] != ',')
246 {
247 return false;
248 }
249 i++;
250 }
251 }
252 ip[0] = (uint8_t)nums[0];
253 ip[1] = (uint8_t)nums[1];
254 ip[2] = (uint8_t)nums[2];
255 ip[3] = (uint8_t)nums[3];
256 *port = (uint16_t)(nums[4] * 256 + nums[5]);
257 return true;
258}
259
260bool pc_ftp_parse_epsv(const char *buf, size_t len, uint16_t *port)
261{
262 if (!buf || !port)
263 {
264 return false;
265 }
266 size_t i = 0;
267 while (i < len && buf[i] != '(')
268 {
269 i++;
270 }
271 if (i >= len)
272 {
273 return false;
274 }
275 i++; // past '('
276 if (i >= len)
277 {
278 return false;
279 }
280 char d = buf[i]; // the delimiter (RFC 2428 recommends '|')
281
282 // Skip the 3 leading delimiters (empty net-prt + net-addr fields) to reach the port field.
283 int seen = 0;
284 while (i < len && seen < 3)
285 {
286 if (buf[i] == d)
287 {
288 seen++;
289 }
290 i++;
291 }
292 if (seen < 3)
293 {
294 return false;
295 }
296
297 if (i >= len || buf[i] < '0' || buf[i] > '9')
298 {
299 return false; // the guard above guarantees at least one port digit follows
300 }
301 unsigned v = 0;
302 while (i < len && buf[i] >= '0' && buf[i] <= '9')
303 {
304 v = v * 10 + (unsigned)(buf[i] - '0');
305 if (v > 65535)
306 {
307 return false;
308 }
309 i++;
310 }
311 *port = (uint16_t)v;
312 return true;
313}
314
315#endif // PC_ENABLE_FTP
FTP client wire codec (RFC 959 + RFC 2428 + RFC 3659), PC_ENABLE_FTP.