DeterministicESPAsyncWebServer v6.27.1
Zero-allocation, bounded-execution async HTTP server for ESP32
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 DETWS_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 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)
24 return FTP_SENT;
25 // The guard above proves n + slen <= cap, so this write stays inside buf[0, cap). S3519 can't link
26 // buf's size to the separate cap parameter and follows an infeasible path (same FP as mms.cpp).
27 memcpy(buf + n, s, slen); // NOSONAR - bound proven above; analyzer follows an infeasible path
28 return n + slen;
29}
30
31// Append an unsigned decimal; propagates the overflow sentinel.
32static size_t ftp_emit_uint(char *buf, size_t cap, size_t n, unsigned v)
33{
34 if (n == FTP_SENT)
35 return FTP_SENT;
36 char rev[10];
37 int ri = 0;
38 if (v == 0)
39 rev[ri++] = '0';
40 else
41 while (v)
42 {
43 rev[ri++] = (char)('0' + (v % 10));
44 v /= 10;
45 }
46 if ((size_t)ri > cap - n) // n <= cap invariant (checked above); subtraction form can't overflow
47 return FTP_SENT;
48 for (int k = 0; k < ri; k++)
49 buf[n + k] = rev[ri - 1 - k];
50 return n + (size_t)ri;
51}
52
53// Finish: on no overflow and room for the NUL, terminate and return the length; else 0.
54static size_t ftp_finish(char *buf, size_t cap, size_t n)
55{
56 if (n == FTP_SENT || n >= cap) // no room for the NUL (n == cap); n <= cap invariant avoids n + 1 overflow
57 return 0;
58 buf[n] = 0;
59 return n;
60}
61
62size_t ftp_build_command(char *buf, size_t cap, const char *verb, const char *arg)
63{
64 if (!buf || !verb || !verb[0])
65 return 0;
66 size_t n = 0;
67 n = ftp_emit(buf, cap, n, verb, strnlen(verb, cap));
68 if (arg && arg[0])
69 {
70 n = ftp_emit(buf, cap, n, " ", 1);
71 n = ftp_emit(buf, cap, n, arg, strnlen(arg, cap));
72 }
73 n = ftp_emit(buf, cap, n, "\r\n", 2);
74 return ftp_finish(buf, cap, n);
75}
76
77size_t ftp_build_port(char *buf, size_t cap, const uint8_t ip[4], uint16_t port)
78{
79 if (!buf || !ip)
80 return 0;
81 size_t n = 0;
82 n = ftp_emit(buf, cap, n, "PORT ", 5);
83 for (int i = 0; i < 4; i++)
84 {
85 n = ftp_emit_uint(buf, cap, n, ip[i]);
86 n = ftp_emit(buf, cap, n, ",", 1);
87 }
88 n = ftp_emit_uint(buf, cap, n, (unsigned)(port >> 8));
89 n = ftp_emit(buf, cap, n, ",", 1);
90 n = ftp_emit_uint(buf, cap, n, (unsigned)(port & 0xFF));
91 n = ftp_emit(buf, cap, n, "\r\n", 2);
92 return ftp_finish(buf, cap, n);
93}
94
95size_t ftp_build_eprt(char *buf, size_t cap, const char *ip_str, bool ipv6, uint16_t port)
96{
97 if (!buf || !ip_str || !ip_str[0])
98 return 0;
99 size_t n = 0;
100 n = ftp_emit(buf, cap, n, "EPRT |", 6);
101 n = ftp_emit(buf, cap, n, ipv6 ? "2" : "1", 1);
102 n = ftp_emit(buf, cap, n, "|", 1);
103 n = ftp_emit(buf, cap, n, ip_str, strnlen(ip_str, cap));
104 n = ftp_emit(buf, cap, n, "|", 1);
105 n = ftp_emit_uint(buf, cap, n, port);
106 n = ftp_emit(buf, cap, n, "|\r\n", 3);
107 return ftp_finish(buf, cap, n);
108}
109
110static bool ftp_is_3digit(const char *p)
111{
112 return p[0] >= '0' && p[0] <= '9' && p[1] >= '0' && p[1] <= '9' && p[2] >= '0' && p[2] <= '9';
113}
114
115static int ftp_code3(const char *p)
116{
117 return (p[0] - '0') * 100 + (p[1] - '0') * 10 + (p[2] - '0');
118}
119
120// Index just past the LF of the line starting at @p start, or 0 if the line is not yet complete.
121static size_t ftp_line_end(const char *buf, size_t len, size_t start)
122{
123 for (size_t i = start; i < len; i++)
124 if (buf[i] == '\n')
125 return i + 1;
126 return 0;
127}
128
129bool ftp_parse_reply(const char *buf, size_t len, int *code, size_t *consumed)
130{
131 if (!buf || len < 4 || !ftp_is_3digit(buf))
132 return false;
133 int first = ftp_code3(buf);
134 char sep = buf[3];
135
136 if (sep == ' ')
137 {
138 size_t eol = ftp_line_end(buf, len, 0);
139 if (!eol)
140 return false; // line not fully received
141 *code = first;
142 *consumed = eol;
143 return true;
144 }
145 if (sep != '-')
146 return false; // malformed: the separator must be SP or '-'
147
148 // Multiline: end at the first line that begins with the same code followed by a space.
149 size_t pos = ftp_line_end(buf, len, 0);
150 if (!pos)
151 return false;
152 while (pos < len)
153 {
154 if (len - pos >= 4 && ftp_is_3digit(buf + pos) && ftp_code3(buf + pos) == first && buf[pos + 3] == ' ')
155 {
156 size_t eol = ftp_line_end(buf, len, pos);
157 if (!eol)
158 return false; // terminator line not fully received
159 *code = first;
160 *consumed = eol;
161 return true;
162 }
163 size_t eol = ftp_line_end(buf, len, pos);
164 if (!eol)
165 return false; // partial continuation line; need more
166 pos = eol;
167 }
168 return false; // no terminator yet
169}
170
171bool ftp_parse_pasv(const char *buf, size_t len, uint8_t ip[4], uint16_t *port)
172{
173 if (!buf || !ip || !port)
174 return false;
175 size_t i = 0;
176 while (i < len && buf[i] != '(')
177 i++;
178 if (i >= len)
179 return false;
180 i++; // past '('
181
182 unsigned nums[6];
183 for (int ni = 0; ni < 6; ni++)
184 {
185 if (i >= len || buf[i] < '0' || buf[i] > '9')
186 return false; // the guard above guarantees at least one digit in this field
187 unsigned v = 0;
188 while (i < len && buf[i] >= '0' && buf[i] <= '9')
189 {
190 v = v * 10 + (unsigned)(buf[i] - '0');
191 if (v > 255)
192 return false;
193 i++;
194 }
195 nums[ni] = v;
196 if (ni < 5)
197 {
198 if (i >= len || buf[i] != ',')
199 return false;
200 i++;
201 }
202 }
203 ip[0] = (uint8_t)nums[0];
204 ip[1] = (uint8_t)nums[1];
205 ip[2] = (uint8_t)nums[2];
206 ip[3] = (uint8_t)nums[3];
207 *port = (uint16_t)(nums[4] * 256 + nums[5]);
208 return true;
209}
210
211bool ftp_parse_epsv(const char *buf, size_t len, uint16_t *port)
212{
213 if (!buf || !port)
214 return false;
215 size_t i = 0;
216 while (i < len && buf[i] != '(')
217 i++;
218 if (i >= len)
219 return false;
220 i++; // past '('
221 if (i >= len)
222 return false;
223 char d = buf[i]; // the delimiter (RFC 2428 recommends '|')
224
225 // Skip the 3 leading delimiters (empty net-prt + net-addr fields) to reach the port field.
226 int seen = 0;
227 while (i < len && seen < 3)
228 {
229 if (buf[i] == d)
230 seen++;
231 i++;
232 }
233 if (seen < 3)
234 return false;
235
236 if (i >= len || buf[i] < '0' || buf[i] > '9')
237 return false; // the guard above guarantees at least one port digit follows
238 unsigned v = 0;
239 while (i < len && buf[i] >= '0' && buf[i] <= '9')
240 {
241 v = v * 10 + (unsigned)(buf[i] - '0');
242 if (v > 65535)
243 return false;
244 i++;
245 }
246 *port = (uint16_t)v;
247 return true;
248}
249
250#endif // DETWS_ENABLE_FTP
FTP client wire codec (RFC 959 + RFC 2428 + RFC 3659), DETWS_ENABLE_FTP.