ProtoCore v0.0.1
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
fs_path.h
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 fs_path.h
6 * @brief Shared filesystem path helpers for the file-transfer servers (SFTP / SCP, and the pattern the
7 * static file server + WebDAV also use): join a mount root with a request subpath and reject `..`
8 * traversal - the single choke point that keeps a request from escaping its mount.
9 *
10 * Pure string logic (no fs::FS, no heap, no stdlib) so it is host-testable and identical on device + host.
11 * The guard is a substring reject (not realpath/symlink resolution) - the on-flash filesystems (FAT /
12 * LittleFS) have no symlinks, so a `..`-free joined path cannot escape the root.
13 *
14 * @author Douglas Quigg (dstroy0)
15 * @date 2026
16 */
17
18#ifndef PROTOCORE_FS_PATH_H
19#define PROTOCORE_FS_PATH_H
20
21#include "protocore_config.h"
22#include "shared_primitives/strbuf.h" // pc_sb frame builder
23#include <stddef.h>
24#include <stdio.h>
25#include <string.h>
26
27/** @brief Join a filesystem @p root and a @p sub path into @p out, normalizing the separator. @return false on
28 * overflow. */
29inline bool fs_path_join(const char *root, const char *sub, char *out, size_t cap)
30{
31 size_t rlen = strnlen(root, cap);
32 bool root_slash = (rlen > 0 && root[rlen - 1] == '/');
33 if (root_slash && sub[0] == '/')
34 {
35 sub++;
36 }
37 bool sub_slash = (sub[0] == '/');
38 const char *sep = (root_slash || sub_slash) ? "" : "/";
39 pc_sb sb_out = {out, cap, 0, true};
40 pc_sb_put(&sb_out, root);
41 pc_sb_put(&sb_out, sep);
42 pc_sb_put(&sb_out, sub);
43 int wn = (int)pc_sb_finish(&sb_out);
44 // wn <= 0 is unreachable, for both halves of "<= 0": wn < 0 only comes from an encoding error,
45 // which "%s" of plain non-null C strings cannot raise (see file_serving.cpp's identical guard);
46 // wn == 0 would need root, sep, AND sub all empty simultaneously, but sep is "" only when
47 // root_slash or sub_slash is true, and both of those require a nonempty contributing string
48 // (a nonempty root, or a sub that still starts with '/' after the sub++ skip) - so the formatted
49 // result is never shorter than 1 byte.
50 return wn > 0 && wn < (int)cap; // GCOVR_EXCL_BR_LINE wn <= 0 unreachable (see above)
51}
52
53/**
54 * @brief Resolve a mount @p root + a request @p sub path to an on-disk path in @p out: reject any `..`
55 * traversal, join onto the root, and drop a trailing '/'.
56 * @return 0 on success, -1 on a traversal attempt (`..` present), -2 if the joined path would overflow @p out.
57 */
58inline int fs_path_resolve(const char *root, const char *sub, char *out, size_t cap)
59{
60 if (strstr(sub, ".."))
61 {
62 return -1; // path traversal - refuse before touching the filesystem
63 }
64 if (!fs_path_join(root, sub, out, cap))
65 {
66 return -2;
67 }
68 size_t fpl = strnlen(out, cap);
69 if (fpl > 1 && out[fpl - 1] == '/')
70 {
71 out[fpl - 1] = '\0';
72 }
73 return 0;
74}
75
76#endif // PROTOCORE_FS_PATH_H
bool fs_path_join(const char *root, const char *sub, char *out, size_t cap)
Join a filesystem root and a sub path into out, normalizing the separator.
Definition fs_path.h:29
int fs_path_resolve(const char *root, const char *sub, char *out, size_t cap)
Resolve a mount root + a request sub path to an on-disk path in out: reject any .....
Definition fs_path.h:58
User-facing configuration for ProtoCore.
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_put(pc_sb *b, const char *s)
Append NUL-terminated s; leaves the buffer untouched and clears ok if it would not fit.
Definition strbuf.h:60
Bump-append target; ok latches false once an append would overflow cap.
Definition strbuf.h:30