ProtoCore v1.0.16
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
filesystem.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 filesystem.h
6 * @brief The filesystem accessor: it owns the mount root, it owns the resolved path, and it is the
7 * only way a request reaches storage.
8 *
9 * A wire protocol (SFTP, SCP, WebDAV) knows a *request* path - the bytes a client sent. It does not
10 * know where the mount lives, and it must not be able to escape it. So it never builds a path and
11 * never holds one: it hands the request path to an operation here, and this file joins it onto the
12 * root, rejects `..`, and dispatches to the mounted backend (mnt.h, alongside this file).
13 *
14 * That is why the operations take a request path rather than returning one. An accessor that
15 * returned a resolved string would put a path buffer, its capacity, and its overflow check into
16 * every caller - which is exactly the duplication that had nine identical `char disk[]` arrays and
17 * two copies of the `..` guard spread across the SFTP and SCP servers. There is one buffer here,
18 * and callers do not size it, see it, or carry it.
19 *
20 * protocore_fs_path() is the single exception, for the one caller that genuinely needs the text: SFTP
21 * REALPATH answers with a path string. It hands back a pointer into this file's storage, or
22 * nullptr when it cannot - there is no out/capacity pair to get wrong.
23 *
24 * The `..` guard is a rejection, not realpath/symlink resolution: the on-flash filesystems
25 * (the small embedded ones) have no symlinks, so a `..`-free joined path cannot leave the root.
26 *
27 * @author Douglas Quigg (dstroy0)
28 * @date 2026
29 */
30
31#ifndef PROTOCORE_FILESYSTEM_H
32#define PROTOCORE_FILESYSTEM_H
33
34#include "numeros_scribo/numeros_scribo.h" // the one frame engine
36
37#include "protocore_config.h"
38
40
41// root, dir, name. A whole path is these three pieces, so it is ONE build: a caller that assembled
42// dir+name itself and handed the result over would frame the same bytes twice, into two buffers,
43// for the same result. A mount root ends with '/', and a dir that carries a name ends with '/', so
44// both separators are already in the strings and the spec needs no literal between the fields.
45static const mmgr_field FILESYSTEM_JOIN[] = {MMGR_STR, MMGR_STR, MMGR_STR, MMGR_END};
46
47// The mount root, copied in so its trailing '/' is owned rather than assumed (see protocore_fs_begin).
48static const mmgr_field FILESYSTEM_ROOT[] = {MMGR_STR, MMGR_END};
49
50/**
51 * @brief Roots that can be bound at once (see protocore_fs_begin).
52 *
53 * One per service that wants its own storage - "mnt/scp", "mnt/sftp", a local region held for temp
54 * files - and more can be bound by raising this. Two services naming the same root share it and
55 * cost one entry.
56 */
57#ifndef PROTOCORE_FS_MAX_ROOTS
58#define PROTOCORE_FS_MAX_ROOTS 4
59#endif
60
61/** @brief Longest root name (e.g. "mnt/sftp"), including the terminator. */
62#ifndef PROTOCORE_FS_ROOT_NAME_MAX
63#define PROTOCORE_FS_ROOT_NAME_MAX 24
64#endif
65
66/**
67 * @brief How deep protocore_fs_remove() / protocore_fs_copy() descend before refusing a tree.
68 *
69 * The bound is what turns a tree walk into a fixed cost: one level of path storage per level, all of
70 * it in this file's context, and no call recursion - the walks are loops over that array, so the
71 * depth a request can force is the array's extent rather than however many stack frames it takes to
72 * exhaust RAM.
73 */
74#ifndef PROTOCORE_FS_MAX_DEPTH
75#define PROTOCORE_FS_MAX_DEPTH 8
76#endif
77
78/**
79 * @brief The storage block this file aligns its transfers to.
80 *
81 * Block alignment is ours, not the caller's and not the store's. Flash and SD both erase and program
82 * in blocks: a transfer that is not a whole block makes the store read the block, merge, and write
83 * it back, so an unaligned copy costs a read-modify-write per block and burns an erase cycle to move
84 * bytes that were already there. 512 is the SD sector and the common flash program page, so one
85 * block is one operation on both.
86 *
87 * This is why protocore_fs_copy() moves a block at a time rather than "some convenient buffer size", and
88 * why the number lives here instead of at each call site - a caller cannot know what the mounted
89 * store erases in, and should not have to.
90 */
91#ifndef PROTOCORE_FS_BLOCK
92#define PROTOCORE_FS_BLOCK 512
93#endif
94
95/** @brief Join a mount @p root, a request @p dir, and a leaf @p name into @p out.
96 *
97 * @param name the leaf, or "" when @p dir is the whole path. When @p name is given, @p dir must end
98 * with '/' - that is what a directory destination means, and it is known at the call
99 * site rather than tested here.
100 * @return bytes written, or 0 on overflow - the engine already knows the length, so it is handed
101 * back rather than left for the caller to rediscover with a scan. */
102PROTOCORE_INLINE size_t protocore_fs_join(const char *root, const char *dir, const char *name, char *out, size_t cap)
103{
104 if (dir[0] == '/')
105 {
106 dir++; // the root carries the separator; a second one would be "//"
107 }
108 return EMBED_CALL(numer.build, NumerosCfg, .out = out, .cap = cap, .spec = FILESYSTEM_JOIN,
109 .vals = (const mmgr_fval[]){MMGR_VSTR(root), MMGR_VSTR(dir), MMGR_VSTR(name)}, .nvals = 3);
110}
111
112/**
113 * @brief Resolve a mount @p root + a request @p dir + a leaf @p name to an on-disk path in @p out:
114 * reject any `..` traversal, join onto the root, and drop a trailing '/'.
115 * @return 0 on success, -1 on a traversal attempt (`..` present), -2 if the joined path would
116 * overflow @p out.
117 */
118/** @brief True if @p s contains a `..` traversal.
119 *
120 * ".." is two bytes at one offset, not a pattern to search for: compare the pair and advance. */
122{
123 for (const char *p = s; p[0] != '\0' && p[1] != '\0'; p++)
124 {
125 if (p[0] == '.' && p[1] == '.')
126 {
127 return PROTO_TRUE;
128 }
129 }
130 return PROTO_FALSE;
131}
132
133PROTOCORE_INLINE int protocore_fs_resolve(const char *root, const char *dir, const char *name, char *out, size_t cap)
134{
135 // Both request-supplied pieces are checked; the root is ours.
137 {
138 return -1; // path traversal - refuse before touching the filesystem
139 }
140 size_t fpl = protocore_fs_join(root, dir, name, out, cap);
141 if (fpl == 0)
142 {
143 return -2;
144 }
145 if (fpl > 1 && out[fpl - 1] == '/')
146 {
147 out[fpl - 1] = '\0';
148 }
149 return 0;
150}
151
152// --- status ---------------------------------------------------------------------------------
153//
154// Every operation here fails closed: 0, false, or -1. That is the right answer, but on its own it
155// throws away WHY, and the two reasons a caller cares about are not the same thing - "the path you
156// asked for is wrong" and "there is no storage behind this filesystem" want different handling.
157//
158// A null store is a legitimate, intentional configuration, not an error. protocore_fs_begin() binds a root
159// and protocore_fs_path() resolves against it with nothing mounted, so an application can hold a filesystem
160// and do local-only path work before (or without) ever attaching a store. What it must not do is
161// look identical to a fault.
162//
163// So the reason is a sticky mask, the same way mmgr_cspan carries a sticky err: bits accumulate as
164// operations fail and a caller tests once, at whatever granularity suits it, instead of branching on
165// every call. Mask it for the bit you care about.
166//
167// protocore_fs_clear_status();
168// protocore_fs_write_file(root, "/a", "", buf, n);
169// protocore_fs_write_file(root, "/b", "", buf, n);
170// if (protocore_fs_status() & PROTOCORE_FS_STORAGE_EXHAUSTED) { ... } // no store, or it would not take more
171
172#define PROTOCORE_FS_OK 0u
173#define PROTOCORE_FS_STORAGE_EXHAUSTED (1u << 0) ///< nothing mounted, or the store could not take the write.
174#define PROTOCORE_FS_BAD_ROOT (1u << 1) ///< the root handle was never bound (see protocore_fs_begin).
175#define PROTOCORE_FS_TRAVERSAL (1u << 2) ///< the request path contained `..` and was refused.
176#define PROTOCORE_FS_TOO_LONG (1u << 3) ///< the resolved path did not fit.
177
178/** @brief The file one call names: which mount, which directory, which entry. */
179typedef struct
180{
181 int root; ///< the mount point the path is resolved against
182 const char *dir; ///< the directory within it
183 const char *name; ///< the entry within that
184} FsPathArgs;
185
186/** @brief The second path a rename or a copy needs. */
187typedef struct
188{
189 const char *dir; ///< the destination directory
190 const char *name; ///< the destination entry
191} FsDestArgs;
192
193/** @brief The open file a call acts on, and the bytes it moves. */
194typedef struct
195{
196 int handle; ///< the open file or directory a call acts on
197 protocore_mnt_mode mode; ///< how an open asks for it
198 void *buf; ///< where a read lands, or what a write sends
199 const void *wbuf; ///< the bytes a write sends, when they are const
200 size_t n; ///< how many
201 uint64_t off; ///< where a seek moves to
202 char *name_out; ///< where a directory read writes the entry name
203 size_t name_cap; ///< how much room that has
204 protocore_mnt_stat *stat; ///< where a stat or a directory read lands its entry
205} FsIoArgs;
206
207/**
208 * @brief The path-safe filesystem surface over a mount.
209 *
210 * A caller sets the members a call takes, invokes it through ::Fs, and reads the outcome off the
211 * same handle. Every path is joined and bounds-checked here before it reaches a backend.
212 *
213 * @var FilesystemNs::path the file one call names
214 * @var FilesystemNs::dest the second path a rename or a copy needs
215 * @var FilesystemNs::io the open file a call acts on, and the bytes it moves
216 * @var FilesystemNs::mount the mount a begin opens, by name
217 * @var FilesystemNs::ok a call's true/false outcome
218 * @var FilesystemNs::i32 a handle, a byte count, or < 0 on failure
219 * @var FilesystemNs::len a file length a size or a whole-file read reports
220 * @var FilesystemNs::bits the sticky fault bits
221 * @var FilesystemNs::text the joined path a resolve reports, or NULL when it would escape
222 * @var FilesystemNs::status the sticky fault bits since the last clear
223 * @var FilesystemNs::clear drop them
224 * @var FilesystemNs::present storage is mounted and answering
225 * @var FilesystemNs::begin open the named mount
226 * @var FilesystemNs::resolve join and bounds-check a path without touching storage
227 * @var FilesystemNs::open open a file
228 * @var FilesystemNs::read read from an open file
229 * @var FilesystemNs::write write to an open file
230 * @var FilesystemNs::close close an open file or directory
231 * @var FilesystemNs::seek move an open file's cursor
232 * @var FilesystemNs::size a file's length
233 * @var FilesystemNs::exists a file is there
234 * @var FilesystemNs::stat a file's metadata
235 * @var FilesystemNs::remove delete a file
236 * @var FilesystemNs::rename move one
237 * @var FilesystemNs::copy duplicate one
238 * @var FilesystemNs::mkdir create a directory
239 * @var FilesystemNs::rmdir remove one
240 * @var FilesystemNs::opendir open a directory for listing
241 * @var FilesystemNs::readdir take the next entry from it
242 * @var FilesystemNs::read_file read a whole file into one buffer
243 * @var FilesystemNs::write_file write one buffer as a whole file
244 *
245 * The joined path is `..`-free by construction, and the small embedded filesystems have no
246 * symlinks, so a resolved path cannot leave its root.
247 */
248typedef struct
249{
253 const char *mount;
254
256 int i32;
257 long len;
258 uint32_t bits;
259 const char *text;
260
261 void (*const status)(uint8_t *work);
262 void (*const clear)(uint8_t *work);
263 void (*const present)(uint8_t *work);
264 void (*const begin)(uint8_t *work);
265 void (*const resolve)(uint8_t *work);
266 void (*const open)(uint8_t *work);
267 void (*const read)(uint8_t *work);
268 void (*const write)(uint8_t *work);
269 void (*const close)(uint8_t *work);
270 void (*const seek)(uint8_t *work);
271 void (*const size)(uint8_t *work);
272 void (*const exists)(uint8_t *work);
273 void (*const stat)(uint8_t *work);
274 void (*const remove)(uint8_t *work);
275 void (*const rename)(uint8_t *work);
276 void (*const copy)(uint8_t *work);
277 void (*const mkdir)(uint8_t *work);
278 void (*const rmdir)(uint8_t *work);
279 void (*const opendir)(uint8_t *work);
280 void (*const readdir)(uint8_t *work);
281 void (*const read_file)(uint8_t *work);
282 void (*const write_file)(uint8_t *work);
284
285/** @brief The one symbol this module exports. */
286extern FilesystemNs Fs;
287
288/**
289 * @brief The PROTOCORE_FILESYSTEM_BORROW bytes this module's state lives in.
290 *
291 * Stated beside the namespace rather than on it: an entry takes a borrow, and this is where
292 * that borrow comes from. Taken once from the end of the pool, which no mark and no release
293 * walks, so the state lasts the life of the program.
294 *
295 * @return the span.
296 */
298
300
301#endif // PROTOCORE_FILESYSTEM_H
#define PROTOCORE_INLINE
Linkage for a leaf primitive whose body is cheaper than the call that reaches it.
PROTOCORE_INLINE proto_bool protocore_fs_has_dotdot(const char *s)
Resolve a mount root + a request dir + a leaf name to an on-disk path in out: reject any ....
Definition filesystem.h:121
uint8_t * protocore_filesystem_span(void)
The PROTOCORE_FILESYSTEM_BORROW bytes this module's state lives in.
PROTOCORE_INLINE int protocore_fs_resolve(const char *root, const char *dir, const char *name, char *out, size_t cap)
Definition filesystem.h:133
PROTOCORE_INLINE size_t protocore_fs_join(const char *root, const char *dir, const char *name, char *out, size_t cap)
Join a mount root, a request dir, and a leaf name into out.
Definition filesystem.h:102
FilesystemNs Fs
The one symbol this module exports.
The mount: which store is behind the filesystem, and the vtable it answers through.
PROTOCORE_BEGIN_DECLS enum PROTO_ENUM_PACKED protocore_mnt_mode
Open modes.
FsPathArgs path
Definition filesystem.h:250
const char * text
Definition filesystem.h:259
proto_bool ok
Definition filesystem.h:255
FsDestArgs dest
Definition filesystem.h:251
FsIoArgs io
Definition filesystem.h:252
uint32_t bits
Definition filesystem.h:258
const char * mount
Definition filesystem.h:253
The second path a rename or a copy needs.
Definition filesystem.h:188
const char * name
the destination entry
Definition filesystem.h:190
const char * dir
the destination directory
Definition filesystem.h:189
The open file a call acts on, and the bytes it moves.
Definition filesystem.h:195
uint64_t off
where a seek moves to
Definition filesystem.h:201
char * name_out
where a directory read writes the entry name
Definition filesystem.h:202
void * buf
where a read lands, or what a write sends
Definition filesystem.h:198
protocore_mnt_mode mode
how an open asks for it
Definition filesystem.h:197
size_t n
how many
Definition filesystem.h:200
size_t name_cap
how much room that has
Definition filesystem.h:203
int handle
the open file or directory a call acts on
Definition filesystem.h:196
const void * wbuf
the bytes a write sends, when they are const
Definition filesystem.h:199
protocore_mnt_stat * stat
where a stat or a directory read lands its entry
Definition filesystem.h:204
The file one call names: which mount, which directory, which entry.
Definition filesystem.h:180
int root
the mount point the path is resolved against
Definition filesystem.h:181
const char * dir
the directory within it
Definition filesystem.h:182
const char * name
the entry within that
Definition filesystem.h:183
What a stat and a directory entry both answer: the facts about one file, and no name.
Definition mnt.h:71
#define PROTO_FALSE
the false value
Definition types.h:68
#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 PROTO_TRUE
the true value, spelled so a caller never writes a bare 1
Definition types.h:67
#define PROTOCORE_END_DECLS
Definition types.h:97