ProtoCore v0.0.2
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
spa_router.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 spa_router.cpp
6 * @brief Single-page-app micro-routing decision (see spa_router.h).
7 */
8
10
11#if PC_ENABLE_SPA_ROUTER
12
13#include <string.h>
14
15bool pc_spa_has_extension(const char *path)
16{
17 if (!path)
18 {
19 return false;
20 }
21 // Look at the last path segment; an extension is a '.' after the last '/', not at the segment start.
22 const char *slash = strrchr(path, '/');
23 const char *seg = slash ? slash + 1 : path;
24 const char *dot = strrchr(seg, '.');
25 return dot && dot != seg && dot[1] != '\0';
26}
27
28pc_spa_action pc_spa_route(const char *path, const char *api_prefix)
29{
30 if (!path || path[0] == '\0' || (path[0] == '/' && path[1] == '\0'))
31 {
32 return pc_spa_action::PC_SPA_SERVE_SHELL; // "" or "/" -> the app shell
33 }
34
35 if (api_prefix && api_prefix[0])
36 {
37 size_t pl = strnlen(api_prefix, MAX_PATH_LEN + 1);
38 if (strncmp(path, api_prefix, pl) == 0)
39 {
40 return pc_spa_action::PC_SPA_PASSTHROUGH; // "/api/..." -> handlers
41 }
42 }
43
44 return pc_spa_has_extension(path) ? pc_spa_action::PC_SPA_SERVE_FILE : pc_spa_action::PC_SPA_SERVE_SHELL;
45}
46
47pc_spa_action pc_spa_route_ex(const char *path, const pc_spa_ctx *ctx)
48{
49 if (!ctx)
50 {
51 return pc_spa_route(path, nullptr);
52 }
53
54 pc_spa_action a = pc_spa_route(path, ctx->api_prefix);
55 // Only the shell decision can degrade. An asset request still resolves to the file (the caller
56 // reports a real 404 if it is missing), and the API must keep passing through - the fallback
57 // page posts to those same endpoints, so cutting them off would make it useless.
58 if (a != pc_spa_action::PC_SPA_SERVE_SHELL)
59 {
60 return a;
61 }
62 if (!ctx->shell_available || !ctx->client_scripting || ctx->degraded)
63 {
64 return pc_spa_action::PC_SPA_SERVE_FALLBACK;
65 }
66 return a;
67}
68
69// ---------------------------------------------------------------------------
70// Conditional UI streaming
71// ---------------------------------------------------------------------------
72
73void pc_ui_stream_begin(pc_ui_stream *s, const pc_ui_fragment *frags, size_t count, void *ctx)
74{
75 if (!s)
76 {
77 return;
78 }
79 s->frags = frags;
80 s->count = frags ? count : 0;
81 s->ctx = ctx;
82 s->idx = 0;
83 s->off = 0;
84 s->done = (s->count == 0);
85}
86
87size_t pc_ui_stream_next(pc_ui_stream *s, char *out, size_t cap)
88{
89 if (!s || !out || cap == 0 || s->done)
90 {
91 return 0;
92 }
93
94 size_t written = 0;
95 while (written < cap && s->idx < s->count)
96 {
97 const pc_ui_fragment *f = &s->frags[s->idx];
98 // Evaluated here rather than at begin(), so a fragment reflects the state that holds when
99 // the stream reaches it. Skipping costs nothing - no bytes are emitted for it at all.
100 if (!f->html || (f->when && !f->when(s->ctx)))
101 {
102 s->idx++;
103 s->off = 0;
104 continue;
105 }
106 const char *src = f->html + s->off;
107 size_t room = cap - written;
108 size_t n = 0;
109 while (n < room && src[n] != '\0')
110 {
111 n++;
112 }
113 memcpy(out + written, src, n);
114 written += n;
115 s->off += n;
116 if (src[n] == '\0') // this fragment is fully emitted
117 {
118 s->idx++;
119 s->off = 0;
120 }
121 }
122 if (s->idx >= s->count)
123 {
124 s->done = true;
125 }
126 return written;
127}
128
129bool pc_ui_stream_done(const pc_ui_stream *s)
130{
131 return !s || s->done;
132}
133
134#endif // PC_ENABLE_SPA_ROUTER
#define MAX_PATH_LEN
Maximum URL path length (including leading /).
Single-page-app micro-routing + conditional UI streaming (PC_ENABLE_SPA_ROUTER).