ProtoCore v0.0.2
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
sftp.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 sftp.cpp
6 * @brief SFTP protocol v3 wire codec - implementation. See sftp.h.
7 */
8
10#include "shared_primitives/strbuf.h" // pc_sb frame builder
11
12#include "shared_primitives/time_compat.h" // pc_gmtime_r (portable reentrant UTC)
13
14#if PC_ENABLE_SSH_SFTP
15
16#include <stdio.h>
17#include <string.h>
18#include <time.h>
19
20// --- reader (big-endian, bounds-checked) ---------------------------------------------------------
21
22void pc_sftp_rd_init(SftpReader *r, const uint8_t *payload, size_t len)
23{
24 r->p = payload;
25 r->len = len;
26 r->off = 0;
27 r->ok = true;
28}
29
30uint8_t pc_sftp_rd_u8(SftpReader *r)
31{
32 if (!r->ok || r->off + 1 > r->len)
33 {
34 r->ok = false;
35 return 0;
36 }
37 return r->p[r->off++];
38}
39
40uint32_t pc_sftp_rd_u32(SftpReader *r)
41{
42 if (!r->ok || r->off + 4 > r->len)
43 {
44 r->ok = false;
45 return 0;
46 }
47 uint32_t v = ((uint32_t)r->p[r->off] << 24) | ((uint32_t)r->p[r->off + 1] << 16) |
48 ((uint32_t)r->p[r->off + 2] << 8) | (uint32_t)r->p[r->off + 3];
49 r->off += 4;
50 return v;
51}
52
53uint64_t pc_sftp_rd_u64(SftpReader *r)
54{
55 if (!r->ok || r->off + 8 > r->len)
56 {
57 r->ok = false;
58 return 0;
59 }
60 uint64_t v = 0;
61 for (int i = 0; i < 8; i++)
62 {
63 v = (v << 8) | r->p[r->off + i];
64 }
65 r->off += 8;
66 return v;
67}
68
69bool pc_sftp_rd_string(SftpReader *r, const uint8_t **out, uint32_t *out_len)
70{
71 uint32_t n = pc_sftp_rd_u32(r);
72 if (!r->ok || r->off + n > r->len)
73 {
74 r->ok = false;
75 return false;
76 }
77 if (out)
78 {
79 *out = r->p + r->off;
80 }
81 if (out_len)
82 {
83 *out_len = n;
84 }
85 r->off += n;
86 return true;
87}
88
89bool pc_sftp_rd_attrs(SftpReader *r, SftpAttrs *a)
90{
91 a->flags = pc_sftp_rd_u32(r);
92 a->size = 0;
93 a->permissions = 0;
94 a->atime = 0;
95 a->mtime = 0;
96 if (a->flags & PC_SSH_FILEXFER_ATTR_SIZE)
97 {
98 a->size = pc_sftp_rd_u64(r);
99 }
100 if (a->flags & PC_SSH_FILEXFER_ATTR_UIDGID)
101 {
102 pc_sftp_rd_u32(r); // uid (ignored)
103 pc_sftp_rd_u32(r); // gid (ignored)
104 }
105 if (a->flags & PC_SSH_FILEXFER_ATTR_PERMS)
106 {
107 a->permissions = pc_sftp_rd_u32(r);
108 }
109 if (a->flags & PC_SSH_FILEXFER_ATTR_ACMODTIME)
110 {
111 a->atime = pc_sftp_rd_u32(r);
112 a->mtime = pc_sftp_rd_u32(r);
113 }
114 if (a->flags & PC_SSH_FILEXFER_ATTR_EXTENDED)
115 {
116 uint32_t ec = pc_sftp_rd_u32(r);
117 for (uint32_t i = 0; i < ec && r->ok; i++)
118 {
119 pc_sftp_rd_string(r, nullptr, nullptr); // extended type
120 pc_sftp_rd_string(r, nullptr, nullptr); // extended data
121 }
122 }
123 return r->ok;
124}
125
126// --- writer --------------------------------------------------------------------------------------
127
128void pc_sftp_wr_init(SftpWriter *w, uint8_t *out, size_t cap)
129{
130 w->p = out;
131 w->cap = cap;
132 w->off = 4; // reserve the length prefix
133 w->ovf = (cap < 4);
134}
135
136void pc_sftp_wr_u8(SftpWriter *w, uint8_t v)
137{
138 if (w->ovf || w->off + 1 > w->cap)
139 {
140 w->ovf = true;
141 return;
142 }
143 w->p[w->off++] = v;
144}
145
146void pc_sftp_wr_u32(SftpWriter *w, uint32_t v)
147{
148 if (w->ovf || w->off + 4 > w->cap)
149 {
150 w->ovf = true;
151 return;
152 }
153 w->p[w->off++] = (uint8_t)(v >> 24);
154 w->p[w->off++] = (uint8_t)(v >> 16);
155 w->p[w->off++] = (uint8_t)(v >> 8);
156 w->p[w->off++] = (uint8_t)v;
157}
158
159void pc_sftp_wr_u64(SftpWriter *w, uint64_t v)
160{
161 if (w->ovf || w->off + 8 > w->cap)
162 {
163 w->ovf = true;
164 return;
165 }
166 for (int i = 7; i >= 0; i--)
167 {
168 w->p[w->off++] = (uint8_t)(v >> (8 * i));
169 }
170}
171
172void pc_sftp_wr_bytes(SftpWriter *w, const void *b, size_t n)
173{
174 if (w->ovf || w->off + n > w->cap)
175 {
176 w->ovf = true;
177 return;
178 }
179 memcpy(w->p + w->off, b, n);
180 w->off += n;
181}
182
183void pc_sftp_wr_string(SftpWriter *w, const void *s, uint32_t n)
184{
185 pc_sftp_wr_u32(w, n);
186 pc_sftp_wr_bytes(w, s, n);
187}
188
189void pc_sftp_wr_attrs(SftpWriter *w, const SftpAttrs *a)
190{
191 pc_sftp_wr_u32(w, a->flags);
192 if (a->flags & PC_SSH_FILEXFER_ATTR_SIZE)
193 {
194 pc_sftp_wr_u64(w, a->size);
195 }
196 if (a->flags & PC_SSH_FILEXFER_ATTR_UIDGID)
197 {
198 pc_sftp_wr_u32(w, 0);
199 pc_sftp_wr_u32(w, 0);
200 }
201 if (a->flags & PC_SSH_FILEXFER_ATTR_PERMS)
202 {
203 pc_sftp_wr_u32(w, a->permissions);
204 }
205 if (a->flags & PC_SSH_FILEXFER_ATTR_ACMODTIME)
206 {
207 pc_sftp_wr_u32(w, a->atime);
208 pc_sftp_wr_u32(w, a->mtime);
209 }
210}
211
212size_t pc_sftp_wr_finish(SftpWriter *w)
213{
214 if (w->ovf)
215 {
216 return 0;
217 }
218 uint32_t plen = (uint32_t)(w->off - 4);
219 w->p[0] = (uint8_t)(plen >> 24);
220 w->p[1] = (uint8_t)(plen >> 16);
221 w->p[2] = (uint8_t)(plen >> 8);
222 w->p[3] = (uint8_t)plen;
223 return w->off;
224}
225
226size_t pc_sftp_wr_pos(const SftpWriter *w)
227{
228 return w->off;
229}
230
231void pc_sftp_wr_patch_u32(SftpWriter *w, size_t at, uint32_t v)
232{
233 if (at + 4 > w->cap)
234 {
235 return;
236 }
237 w->p[at] = (uint8_t)(v >> 24);
238 w->p[at + 1] = (uint8_t)(v >> 16);
239 w->p[at + 2] = (uint8_t)(v >> 8);
240 w->p[at + 3] = (uint8_t)v;
241}
242
243// --- framing -------------------------------------------------------------------------------------
244
245size_t pc_sftp_frame_len(const uint8_t *buf, size_t have, size_t max)
246{
247 if (have < 4)
248 {
249 return 0; // need at least the length prefix
250 }
251 uint32_t plen = ((uint32_t)buf[0] << 24) | ((uint32_t)buf[1] << 16) | ((uint32_t)buf[2] << 8) | (uint32_t)buf[3];
252 size_t total = (size_t)plen + 4;
253 if (plen == 0 || total > max)
254 {
255 return (size_t)-1; // malformed (0-length) or larger than the caller can hold -> drop
256 }
257 return total;
258}
259
260// --- response builders ---------------------------------------------------------------------------
261
262size_t pc_sftp_build_version(uint8_t *out, size_t cap)
263{
264 SftpWriter w;
265 pc_sftp_wr_init(&w, out, cap);
266 pc_sftp_wr_u8(&w, PC_SSH_FXP_VERSION);
267 pc_sftp_wr_u32(&w, PC_SFTP_VERSION);
268 return pc_sftp_wr_finish(&w);
269}
270
271size_t pc_sftp_build_status(uint32_t id, uint32_t code, const char *msg, uint8_t *out, size_t cap)
272{
273 SftpWriter w;
274 pc_sftp_wr_init(&w, out, cap);
275 pc_sftp_wr_u8(&w, PC_SSH_FXP_STATUS);
276 pc_sftp_wr_u32(&w, id);
277 pc_sftp_wr_u32(&w, code);
278 size_t ml = msg ? strnlen(msg, cap) : 0;
279 pc_sftp_wr_string(&w, msg ? msg : "", (uint32_t)ml);
280 pc_sftp_wr_string(&w, "", 0); // language tag
281 return pc_sftp_wr_finish(&w);
282}
283
284size_t pc_sftp_build_handle(uint32_t id, const void *handle, uint32_t hlen, uint8_t *out, size_t cap)
285{
286 SftpWriter w;
287 pc_sftp_wr_init(&w, out, cap);
288 pc_sftp_wr_u8(&w, PC_SSH_FXP_HANDLE);
289 pc_sftp_wr_u32(&w, id);
290 pc_sftp_wr_string(&w, handle, hlen);
291 return pc_sftp_wr_finish(&w);
292}
293
294size_t pc_sftp_build_attrs(uint32_t id, const SftpAttrs *a, uint8_t *out, size_t cap)
295{
296 SftpWriter w;
297 pc_sftp_wr_init(&w, out, cap);
298 pc_sftp_wr_u8(&w, PC_SSH_FXP_ATTRS);
299 pc_sftp_wr_u32(&w, id);
300 pc_sftp_wr_attrs(&w, a);
301 return pc_sftp_wr_finish(&w);
302}
303
304size_t pc_sftp_build_data(uint32_t id, const void *data, uint32_t dlen, uint8_t *out, size_t cap)
305{
306 SftpWriter w;
307 pc_sftp_wr_init(&w, out, cap);
308 pc_sftp_wr_u8(&w, PC_SSH_FXP_DATA);
309 pc_sftp_wr_u32(&w, id);
310 pc_sftp_wr_string(&w, data, dlen);
311 return pc_sftp_wr_finish(&w);
312}
313
314size_t pc_sftp_build_name1(uint32_t id, const char *name, const char *longname, const SftpAttrs *a, uint8_t *out,
315 size_t cap)
316{
317 SftpWriter w;
318 pc_sftp_wr_init(&w, out, cap);
319 pc_sftp_wr_u8(&w, PC_SSH_FXP_NAME);
320 pc_sftp_wr_u32(&w, id);
321 pc_sftp_wr_u32(&w, 1); // one entry
322 pc_sftp_wr_string(&w, name, (uint32_t)strnlen(name, cap));
323 pc_sftp_wr_string(&w, longname, (uint32_t)strnlen(longname, cap));
324 pc_sftp_wr_attrs(&w, a);
325 return pc_sftp_wr_finish(&w);
326}
327
328size_t pc_sftp_format_longname(bool is_dir, uint32_t perms, uint64_t size, uint32_t mtime, const char *name, char *out,
329 size_t cap)
330{
331 static const char *kMonths[12] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun",
332 "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"};
333 char mode[11];
334 mode[0] = is_dir ? 'd' : '-';
335 static const char rwx[9] = {'r', 'w', 'x', 'r', 'w', 'x', 'r', 'w', 'x'};
336 for (int i = 0; i < 9; i++)
337 {
338 mode[1 + i] = (perms & (1u << (8 - i))) ? rwx[i] : '-';
339 }
340 mode[10] = '\0';
341
342 time_t t = (time_t)mtime;
343 struct tm tmv;
344 memset(&tmv, 0, sizeof(tmv));
345 pc_gmtime_r(&t, &tmv); // reentrant; mtime==0 -> epoch, a harmless placeholder date
346 // mtime is a uint32_t, so t is always inside the range every gmtime implementation accepts and the
347 // conversion yields tm_mon in [0,11] by definition; tmv is zeroed above, so even a failed conversion
348 // leaves month 0. The range test is purely a bounds guard on the kMonths[] index below.
349 int mon = (tmv.tm_mon >= 0 && tmv.tm_mon < 12) ? tmv.tm_mon : 0; // GCOVR_EXCL_LINE tm_mon is always 0..11
350
351 // Clipping appenders here, not the fail-closed ones. `longname` is the
352 // draft-ietf-secsh-filexfer display field ("an expanded format for the file name, similar to
353 // what is returned by ls -l"), so a short rendering is correct where a refused one would leave
354 // a directory listing with an empty column. The wire framing is the separate length-prefixed
355 // string written by pc_sftp_wr_string, and that is never clipped. The date is appended in
356 // place rather than staged: the two column widths are what `ls -l` alignment means, and
357 // pc_sb_u64_clip states them directly.
358 pc_sb sb_out = {out, cap, 0, true};
359 pc_sb_put_clip(&sb_out, mode);
360 pc_sb_put_clip(&sb_out, " 1 0 0 ");
361 pc_sb_u64_clip(&sb_out, (uint64_t)size, 0);
362 pc_sb_put_clip(&sb_out, " ");
363 pc_sb_put_clip(&sb_out, kMonths[mon]);
364 pc_sb_put_clip(&sb_out, " ");
365 pc_sb_u64_clip(&sb_out, (uint64_t)tmv.tm_mday, 2);
366 pc_sb_put_clip(&sb_out, " ");
367 pc_sb_u64_clip(&sb_out, (uint64_t)(tmv.tm_year + 1900), 5);
368 pc_sb_put_clip(&sb_out, " ");
369 pc_sb_put_clip(&sb_out, name);
370 return pc_sb_finish(&sb_out);
371}
372
373#endif // PC_ENABLE_SSH_SFTP
SFTP protocol v3 wire codec (SSH_FXP_*, draft-ietf-secsh-filexfer-02) - the pure, host-testable half ...
Bounded no-heap string builder that fails closed on overflow (one shared copy).
size_t pc_sb_finish(pc_sb *b)
NUL-terminate and return the built length, or 0 if the build overflowed.
Definition strbuf.h:648
void pc_sb_u64_clip(pc_sb *b, uint64_t v, uint8_t columns)
Append v as decimal, right-aligned in at least columns with leading spaces, if the whole field fits -...
Definition strbuf.h:112
void pc_sb_put_clip(pc_sb *b, const char *s)
Append as much of s as fits and stop, WITHOUT latching ok.
Definition strbuf.h:90
Bump-append target; ok latches false once an append would overflow cap.
Definition strbuf.h:30
Reentrant UTC broken-down time, portable across the host and target toolchains.
struct tm * pc_gmtime_r(const time_t *epoch, struct tm *out)
Convert epoch to broken-down UTC in caller storage (reentrant).
Definition time_compat.h:28