Pharos 0.7.23
Modern Web Framework & Web App Hosting Service.
Loading...
Searching...
No Matches
native_dynamic_media.c
Go to the documentation of this file.
1/**
2 * @file
3 * @brief Serves static assets from the artifact directory for in-process
4 * dynamic-app request handling, and provides media mutation helpers
5 * for upload, delete, and path-resolution operations.
6 */
7
9
11#include "native_fs.h"
12#include "native_http_log.h"
14#include "native_support.h"
16
17#include <ctype.h>
18#include <stdio.h>
19#include <stdlib.h>
20#include <string.h>
21
22/**
23 * @brief Well-known JSON endpoints served from the artifact directory.
24 */
25static const char *WELL_KNOWN_JSON_PATHS[] = {
26 "/manifest.json",
27 "/route-manifest.json",
28 "/app-graph-schema.json",
29 "/app-model.json",
30 "/component-graph.json",
31 "/server-actions.json",
32 "/dispatch-table.json",
33 "/permission-matrix.json",
34 "/cache-matrix.json",
35 "/tau-state-catalog.json",
36 "/tau-profile.json",
37 "/native-graph.json",
38 "/radius-graph.json",
39 "/route-explain-project-detail.json",
40 "/impact-project-card.json",
41 "/dead-surface-report.json",
42 "/consistency-report.json",
43 "/policy-cache-report.json",
44 "/performance-budget.json",
45 "/generated-docs.json",
46 "/generated-tests.json",
47 "/scoreboard.json",
48 "/release-evidence.json",
49 "/api/manifest",
50 "/api/graph/export/native",
51 "/api/graph/export/radius",
52 "/api/graph/explain",
53 "/api/graph/impact",
54 "/api/tau/profile",
55 "/api/projects",
56 "/api/projects/sample",
57 "/api/scoreboard",
58 NULL
59};
60
61/**
62 * @brief Well-known HTML fallback routes that resolve to index.html.
63 */
64static const char *WELL_KNOWN_HTML_FALLBACKS[] = {
65 "/",
66 NULL
67};
68
70 size_t i;
71
72 if (path == NULL || path[0] == '\0') {
73 return 0;
74 }
75
76 /* Static asset directories */
77 if (starts_with(path, "/static/") || starts_with(path, "/media/")) {
78 return 1;
79 }
80
81 /* Well-known JSON endpoints */
82 for (i = 0; WELL_KNOWN_JSON_PATHS[i] != NULL; i++) {
83 if (streq(path, WELL_KNOWN_JSON_PATHS[i])) {
84 return 1;
85 }
86 }
87
88 return 0;
89}
90
91char *native_dynamic_media_resolve_path(const char *artifact_dir, const char *path) {
92 const char *relative;
93 size_t path_len;
94
95 if (artifact_dir == NULL || path == NULL) {
96 return NULL;
97 }
98
99 path_len = strlen(path);
100
101 /* Well-known JSON endpoints map to their artifact-local source file. */
102 if (streq(path, "/manifest.json")) {
103 return path_join(artifact_dir, "pharos.app.json");
104 }
105 if (streq(path, "/dispatch-table.json")) {
106 return path_join(artifact_dir, "dispatch-table.json");
107 }
108 if (streq(path, "/api/manifest")) {
109 return path_join(artifact_dir, "pharos.app.json");
110 }
111
112 /* Root and well-known HTML fallbacks -> index.html */
113 if (path_len == 0 || (path_len == 1 && path[0] == '/')) {
114 return path_join(artifact_dir, "index.html");
115 }
116
117 /* Static and media directories */
118 if (starts_with(path, "/static/") || starts_with(path, "/media/")) {
119 relative = path + 1; /* skip the leading / */
120 return path_join(artifact_dir, relative);
121 }
122
123 /* Trailing slash -> try index.html in that directory */
124 if (path[path_len - 1] == '/') {
125 relative = path + 1;
126 {
127 char *dir_path = path_join(artifact_dir, relative);
128 char *result = NULL;
129 if (dir_path != NULL) {
130 result = path_join(dir_path, "index.html");
131 free(dir_path);
132 }
133 return result;
134 }
135 }
136
137 /* Everything else: strip the leading / and look in artifact dir */
138 relative = path + 1;
139 return path_join(artifact_dir, relative);
140}
141
143 NativeConnection *connection,
144 const char *artifact_dir,
145 const char *path,
146 const HttpRequest *request
147) {
148 char *file_path = NULL;
149 char *content_type = NULL;
150 FILE *file = NULL;
151 int status = 500;
152 long long content_length = 0;
153
154 (void)request;
155
156 if (connection == NULL || artifact_dir == NULL || path == NULL) {
157 native_send_text_response(connection, 500, "Internal Server Error", "invalid parameters\n");
158 return 500;
159 }
160
161 /* Block path traversal */
162 if (strstr(path, "..") != NULL) {
163 native_send_text_response(connection, 400, "Bad Request", "blocked path traversal\n");
164 return 400;
165 }
166
167 file_path = native_dynamic_media_resolve_path(artifact_dir, path);
168 if (file_path == NULL) {
169 native_send_text_response(connection, 500, "Internal Server Error", "memory allocation failed\n");
170 return 500;
171 }
172
173 if (!path_exists(file_path)) {
174 native_send_text_response(connection, 404, "Not Found", "Not Found");
175 free(file_path);
176 return 404;
177 }
178
179 if (path_is_directory(file_path)) {
180 char *dir_index = path_join(file_path, "index.html");
181 free(file_path);
182 file_path = dir_index;
183 if (file_path == NULL || !path_exists(file_path)) {
184 native_send_text_response(connection, 404, "Not Found", "Not Found");
185 free(file_path);
186 return 404;
187 }
188 }
189
190 file = fopen(file_path, "rb");
191 if (file == NULL) {
192 native_send_text_response(connection, 404, "Not Found", "Not Found");
193 free(file_path);
194 return 404;
195 }
196
197 if (fseek(file, 0, SEEK_END) != 0) {
198 native_send_text_response(connection, 500, "Internal Server Error", "failed to read asset\n");
199 status = 500;
200 goto cleanup;
201 }
202 content_length = (long long)ftell(file);
203 if (content_length < 0 || fseek(file, 0, SEEK_SET) != 0) {
204 native_send_text_response(connection, 500, "Internal Server Error", "failed to read asset\n");
205 status = 500;
206 goto cleanup;
207 }
208
209 content_type = mime_type_for_path(file_path);
210 if (content_type == NULL) {
211 native_send_text_response(connection, 500, "Internal Server Error", "mime type failure\n");
212 status = 500;
213 goto cleanup;
214 }
215
216 /* API paths that resolve to octet-stream should be treated as JSON */
217 if (starts_with(path, "/api/") && streq(content_type, "application/octet-stream")) {
218 free(content_type);
219 content_type = strdup("application/json");
220 if (content_type == NULL) {
221 native_send_text_response(connection, 500, "Internal Server Error", "mime type failure\n");
222 status = 500;
223 goto cleanup;
224 }
225 }
226
228 connection,
229 "HTTP/1.1 200 OK\r\n"
230 "Content-Type: %s\r\n"
231 "Content-Length: %lld\r\n"
232 "Connection: close\r\n"
233 "\r\n",
234 content_type,
235 content_length
236 ) != 0) {
237 status = -1;
238 goto cleanup;
239 }
240
241 {
242 char chunk[4096];
243 size_t bytes_read;
244 while ((bytes_read = fread(chunk, 1, sizeof(chunk), file)) > 0) {
245 if (native_connection_write(connection, chunk, bytes_read) != 0) {
246 status = -1;
247 goto cleanup;
248 }
249 }
250 }
251 status = 200;
252
253cleanup:
254 free(file_path);
255 free(content_type);
256 if (file != NULL) {
257 fclose(file);
258 }
259 return status;
260}
261
262/* ------------------------------------------------------------------ */
263/* Media mutation helpers */
264/* ------------------------------------------------------------------ */
265
266char *safe_upload_filename_dup(const char *raw_name) {
267 const char *base_name = raw_name;
268 char *safe_name = NULL;
269 size_t needed = 0;
270 size_t write_index = 0;
271 size_t index = 0;
272 if (base_name == NULL) {
273 return strdup("");
274 }
275 {
276 const char *slash = strrchr(base_name, '/');
277 const char *backslash = strrchr(base_name, '\\');
278 if (slash != NULL && (backslash == NULL || slash > backslash)) {
279 base_name = slash + 1;
280 } else if (backslash != NULL) {
281 base_name = backslash + 1;
282 }
283 }
284 needed = strlen(base_name) + 1;
285 safe_name = (char *)malloc(needed);
286 if (safe_name == NULL) {
287 return NULL;
288 }
289 for (index = 0; base_name[index] != '\0'; index++) {
290 unsigned char ch = (unsigned char)base_name[index];
291 char out = '_';
292 if (isalnum(ch) || ch == '.' || ch == '_' || ch == '-') {
293 out = (char)ch;
294 } else if (ch == ' ') {
295 out = '_';
296 }
297 safe_name[write_index++] = out;
298 }
299 safe_name[write_index] = '\0';
300 while (safe_name[0] == '_' || safe_name[0] == '.' || safe_name[0] == '-') {
301 memmove(safe_name, safe_name + 1, strlen(safe_name));
302 }
303 while (write_index > 0) {
304 char tail = safe_name[write_index - 1];
305 if (tail != '_' && tail != '.' && tail != '-') {
306 break;
307 }
308 safe_name[--write_index] = '\0';
309 }
310 return safe_name;
311}
312
313char *media_disk_path_dup(const char *artifact_dir, const char *media_url) {
314 if (artifact_dir == NULL || artifact_dir[0] == '\0' || media_url == NULL || !starts_with(media_url, "/media/")) {
315 return NULL;
316 }
317 return path_join(artifact_dir, media_url + 1);
318}
319
320int ensure_media_subtree(const char *artifact_dir, const char *entity_type, const char *entity_slug) {
321 char *fixture_path = NULL;
322 yyjson_doc *doc = NULL;
323 yyjson_val *root = NULL;
324 yyjson_val *subtrees = NULL;
325 yyjson_val *entry = NULL;
326 size_t idx = 0;
327 size_t count = 0;
328 yyjson_val *type_val = NULL;
329 const char *entry_type = NULL;
330 char *base_template = NULL;
331 yyjson_val *subdirs = NULL;
332 char *base_path = NULL;
333 int code = 69;
334
335 if (artifact_dir == NULL || entity_type == NULL || entity_slug == NULL ||
336 artifact_dir[0] == '\0' || entity_type[0] == '\0' || entity_slug[0] == '\0') {
337 return 64;
338 }
339
340 /* Build fixture path */
341 fixture_path = path_join(artifact_dir, "data/framework/media-policy-v1.json");
342 if (fixture_path == NULL) {
343 return 69;
344 }
345
346 /* Load the fixture */
347 doc = native_json_doc_load_file(fixture_path);
348 free(fixture_path);
349 if (doc == NULL) {
350 return 69;
351 }
352
353 root = yyjson_doc_get_root(doc);
354 if (root == NULL || !yyjson_is_obj(root)) {
356 return 69;
357 }
358
359 /* Get the media_subtrees array */
360 subtrees = native_json_obj_get_array(root, "media_subtrees");
361 if (subtrees == NULL) {
363 return 69;
364 }
365
366 /* Look for the matching entity type */
367 count = yyjson_arr_size(subtrees);
368 entry = NULL;
369 for (idx = 0; idx < count; idx++) {
370 yyjson_val *candidate = yyjson_arr_get(subtrees, idx);
371 if (candidate == NULL || !yyjson_is_obj(candidate)) {
372 continue;
373 }
374 type_val = native_json_obj_get(candidate, "entity_type");
375 if (type_val == NULL || !yyjson_is_str(type_val)) {
376 continue;
377 }
378 entry_type = yyjson_get_str(type_val);
379 if (streq(entry_type, entity_type)) {
380 entry = candidate;
381 break;
382 }
383 }
384
385 if (entry == NULL) {
387 return 0; /* No matching policy — not an error */
388 }
389
390 /* Get the base path template and replace {slug} */
391 base_template = native_json_obj_get_string_dup(entry, "base_path_template");
392 if (base_template == NULL || base_template[0] == '\0') {
393 free(base_template);
395 return 64;
396 }
397
398 {
399 /* Replace {slug} with the entity slug */
400 const char *placeholder = "{slug}";
401 char *slug_pos = strstr(base_template, placeholder);
402 if (slug_pos != NULL) {
403 size_t prefix_len = (size_t)(slug_pos - base_template);
404 size_t suffix_len = strlen(slug_pos + strlen(placeholder));
405 size_t slug_len = strlen(entity_slug);
406 base_path = (char *)malloc(prefix_len + slug_len + suffix_len + 1);
407 if (base_path == NULL) {
408 free(base_template);
410 return 69;
411 }
412 memcpy(base_path, base_template, prefix_len);
413 memcpy(base_path + prefix_len, entity_slug, slug_len);
414 memmove(base_path + prefix_len + slug_len, slug_pos + strlen(placeholder), suffix_len + 1);
415 } else {
416 base_path = strdup(base_template);
417 if (base_path == NULL) {
418 free(base_template);
420 return 69;
421 }
422 }
423 free(base_template);
424 }
425
426 /* Create the base directory */
427 {
428 char *full_dir = path_join(artifact_dir, base_path);
429 if (full_dir == NULL) {
430 free(base_path);
432 return 69;
433 }
434 code = ensure_directory(full_dir);
435 free(full_dir);
436 if (code != 0) {
437 free(base_path);
439 return code;
440 }
441 }
442
443 /* Create each subdirectory declared in the fixture */
444 subdirs = native_json_obj_get_array(entry, "subdirectories");
445 if (subdirs != NULL) {
446 size_t sub_count = yyjson_arr_size(subdirs);
447 for (idx = 0; idx < sub_count; idx++) {
448 yyjson_val *sub_val = yyjson_arr_get(subdirs, idx);
449 if (sub_val == NULL || !yyjson_is_str(sub_val)) {
450 continue;
451 }
452 {
453 const char *sub_name = yyjson_get_str(sub_val);
454 char *sub_path = path_join(base_path, sub_name);
455 char *full_sub_dir = NULL;
456 if (sub_path == NULL) {
457 free(base_path);
459 return 69;
460 }
461 full_sub_dir = path_join(artifact_dir, sub_path);
462 free(sub_path);
463 if (full_sub_dir == NULL) {
464 free(base_path);
466 return 69;
467 }
468 code = ensure_directory(full_sub_dir);
469 free(full_sub_dir);
470 if (code != 0) {
471 free(base_path);
473 return code;
474 }
475 }
476 }
477 }
478
479 free(base_path);
481 return 0;
482}
483
484int ensure_business_media_tree(const char *artifact_dir, const char *business_slug) {
485 return ensure_media_subtree(artifact_dir, "business", business_slug);
486}
487
488int remove_media_url_file(const char *artifact_dir, const char *media_url) {
489 char *disk_path = NULL;
490 if (artifact_dir == NULL || artifact_dir[0] == '\0' || media_url == NULL || !starts_with(media_url, "/media/businesses/")) {
491 return 0;
492 }
493 disk_path = media_disk_path_dup(artifact_dir, media_url);
494 if (disk_path != NULL && path_exists(disk_path)) {
495 remove(disk_path);
496 }
497 free(disk_path);
498 return 0;
499}
500
502 const char *artifact_dir,
503 const char *content_type,
504 const char *body_path,
505 const char *field_name,
506 const char *target_url_dir,
507 const char *fallback_filename
508) {
509 unsigned char *body_bytes = NULL;
510 size_t body_len = 0;
511 const unsigned char *content_start = NULL;
512 size_t content_len = 0;
513 char *raw_filename = NULL;
514 char *safe_filename = NULL;
515 char *target_dir = NULL;
516 char *target_path = NULL;
517 char *media_url = NULL;
518 size_t media_url_len = 0;
519 int code = 69;
520 if (artifact_dir == NULL || artifact_dir[0] == '\0' || content_type == NULL || body_path == NULL || body_path[0] == '\0' || field_name == NULL || field_name[0] == '\0' || target_url_dir == NULL || target_url_dir[0] == '\0') {
521 return NULL;
522 }
523 body_bytes = read_file_bytes_dup_runtime(body_path, &body_len);
524 if (body_bytes == NULL) {
525 return NULL;
526 }
527 if (!native_dynamic_multipart_find_part(body_bytes, body_len, content_type, field_name, NULL, &raw_filename, &content_start, &content_len)) {
528 code = 0;
529 goto cleanup;
530 }
531 safe_filename = safe_upload_filename_dup(raw_filename != NULL && raw_filename[0] != '\0' ? raw_filename : fallback_filename);
532 if ((safe_filename == NULL || safe_filename[0] == '\0') && fallback_filename != NULL && fallback_filename[0] != '\0') {
533 free(safe_filename);
534 safe_filename = safe_upload_filename_dup(fallback_filename);
535 }
536 if (safe_filename == NULL || safe_filename[0] == '\0') {
537 code = 0;
538 goto cleanup;
539 }
540 target_dir = path_join(artifact_dir, target_url_dir[0] == '/' ? target_url_dir + 1 : target_url_dir);
541 target_path = target_dir != NULL ? path_join(target_dir, safe_filename) : NULL;
542 if (target_dir == NULL || target_path == NULL || ensure_directory(target_dir) != 0) {
543 code = 69;
544 goto cleanup;
545 }
546 code = write_binary_file_runtime(target_path, content_start, content_len);
547 if (code != 0) {
548 goto cleanup;
549 }
550 media_url_len = strlen(target_url_dir) + 1 + strlen(safe_filename) + 1;
551 media_url = (char *)malloc(media_url_len);
552 if (media_url == NULL) {
553 code = 69;
554 goto cleanup;
555 }
556 snprintf(media_url, media_url_len, "%s/%s", target_url_dir, safe_filename);
557 code = 0;
558cleanup:
559 free(body_bytes);
560 free(raw_filename);
561 free(safe_filename);
562 free(target_dir);
563 free(target_path);
564 if (code != 0) {
565 free(media_url);
566 media_url = NULL;
567 }
568 return media_url;
569}
int native_connection_write(NativeConnection *connection, const void *buffer, size_t len)
Writes bytes to a native connection.
int remove_media_url_file(const char *artifact_dir, const char *media_url)
Removes a managed business media file from an app artifact when it exists.
char * store_uploaded_media(const char *artifact_dir, const char *content_type, const char *body_path, const char *field_name, const char *target_url_dir, const char *fallback_filename)
Extracts a multipart upload into the app artifact media tree and returns the resulting media URL.
char * native_dynamic_media_resolve_path(const char *artifact_dir, const char *path)
Resolves a request path to a file-system path inside the artifact directory.
char * safe_upload_filename_dup(const char *raw_name)
Sanitises a raw upload filename into a safe filesystem name.
int native_dynamic_media_serve_asset(NativeConnection *connection, const char *artifact_dir, const char *path, const HttpRequest *request)
Serves a static asset from the artifact directory.
static const char * WELL_KNOWN_JSON_PATHS[]
Well-known JSON endpoints served from the artifact directory.
static const char * WELL_KNOWN_HTML_FALLBACKS[]
Well-known HTML fallback routes that resolve to index.html.
int ensure_media_subtree(const char *artifact_dir, const char *entity_type, const char *entity_slug)
Ensures a managed media subtree exists for an entity type by reading the media policy fixture at {art...
int ensure_business_media_tree(const char *artifact_dir, const char *business_slug)
Ensures the managed business media subtree exists under an app artifact.
char * media_disk_path_dup(const char *artifact_dir, const char *media_url)
Resolves a media URL into an on-disk path inside an app artifact directory.
int native_dynamic_media_is_static_path(const char *path)
Determines whether a request path targets a known static asset.
Media and static asset serving for in-process dynamic handling.
int native_dynamic_multipart_find_part(const unsigned char *body, size_t body_len, const char *content_type, const char *field_name, char **out_text, char **out_filename, const unsigned char **out_content_start, size_t *out_content_len)
Finds a named part in a multipart request body.
Pharos-owned multipart form-data parsing for in-process dynamic handling.
int ensure_directory(const char *path)
Creates a directory path and any missing parent directories.
Definition native_fs.c:31
Declares native filesystem helper routines.
unsigned char * read_file_bytes_dup_runtime(const char *path, size_t *out_size)
Reads the full content of a binary file into a heap-allocated buffer.
int write_binary_file_runtime(const char *path, const unsigned char *data, size_t length)
Writes a binary byte range to a file, creating parent directories as needed.
char * mime_type_for_path(const char *path)
Resolves the MIME type that should be used for a file path.
Declares HTTP logging and response formatting helpers.
void native_send_text_response(NativeConnection *connection, int status, const char *status_text, const char *body)
Sends a plain-text HTTP response with explicit content length.
int native_connection_printf(NativeConnection *connection, const char *format,...)
Writes formatted bytes to a native connection.
Declares native HTTP response writing helpers.
char * path_join(const char *left, const char *right)
Joins two path segments using the native path separator.
int streq(const char *a, const char *b)
Tests whether two strings are exactly equal.
int path_is_directory(const char *path)
Tests whether a filesystem path refers to a directory.
int starts_with(const char *value, const char *prefix)
Tests whether one string starts with another string.
int path_exists(const char *path)
Tests whether a filesystem path currently exists.
Declares shared low-level support helpers used across the Pharos native runtime.
yyjson_doc * native_json_doc_load_file(const char *path)
Parses a JSON file into an owned yyjson document.
void native_json_doc_free(yyjson_doc *doc)
Releases a document returned by the native yyjson helpers.
char * native_json_obj_get_string_dup(yyjson_val *obj, const char *key)
Duplicates a string-valued key from a JSON object.
yyjson_val * native_json_obj_get_array(yyjson_val *obj, const char *key)
Looks up an array-valued key from a JSON object value.
yyjson_val * native_json_obj_get(yyjson_val *obj, const char *key)
Looks up a key from a JSON object value.
Declares shared helper wrappers around vendored yyjson parsing and serialization APIs.
Parsed HTTP request fields extracted from a native connection.
Accepted client connection and its resolved transport metadata.
yyjson_api_inline bool yyjson_is_str(const yyjson_val *val)
Definition yyjson.h:5390
yyjson_api_inline bool yyjson_is_obj(const yyjson_val *val)
Definition yyjson.h:5398
yyjson_api_inline yyjson_val * yyjson_arr_get(const yyjson_val *arr, size_t idx)
Definition yyjson.h:5603
yyjson_api_inline yyjson_val * yyjson_doc_get_root(const yyjson_doc *doc)
Definition yyjson.h:5323
yyjson_api_inline size_t yyjson_arr_size(const yyjson_val *arr)
Definition yyjson.h:5599
yyjson_api_inline const char * yyjson_get_str(const yyjson_val *val)
Definition yyjson.h:5469