NAH 3.0.0
Native Application Host - Library API Reference
Loading...
Searching...
No Matches
nah_json.h
Go to the documentation of this file.
1/*
2 * NAH JSON - JSON Parsing for NAH Types
3 *
4 * This file parses NAH's JSON boundary formats and exposes contract serializers.
5 * Requires nlohmann/json.
6 *
7 * SPDX-License-Identifier: MIT
8 */
9
10#ifndef NAH_JSON_H
11#define NAH_JSON_H
12
13#ifdef __cplusplus
14
15#include "nah_core.h"
16#include <initializer_list>
17#include <nlohmann/json.hpp>
18
19namespace nah {
20namespace json {
21
22using json = nlohmann::json;
23
24// ============================================================================
25// PARSE RESULTS
26// ============================================================================
27
28template<typename T>
29struct ParseResult {
30 bool ok = false;
31 std::string error;
32 T value;
33 std::vector<std::string> warnings;
34};
35
36// ============================================================================
37// HELPER FUNCTIONS
38// ============================================================================
39
40namespace detail {
41
42inline std::string validate_keys(const json& value,
43 std::initializer_list<const char*> allowed,
44 const std::string& context) {
45 if (!value.is_object()) return context + " must be an object";
46 for (auto item = value.begin(); item != value.end(); ++item) {
47 const std::string key = item.key();
48 const bool known = std::any_of(allowed.begin(), allowed.end(),
49 [&key](const char* candidate) { return key == candidate; });
50 if (!known) return "unknown field in " + context + ": " + key;
51 }
52 return {};
53}
54
55inline std::string validate_string_array(const json& value, const std::string& context) {
56 if (!value.is_array()) return context + " must be an array";
57 for (const auto& item : value) {
58 if (!item.is_string()) return context + " must contain only strings";
59 }
60 return {};
61}
62
63inline std::string get_string(const json& j, const std::string& key, const std::string& default_val = "") {
64 if (j.contains(key) && j[key].is_string()) {
65 return j[key].get<std::string>();
66 }
67 return default_val;
68}
69
70inline std::vector<std::string> get_string_array(const json& j, const std::string& key) {
71 std::vector<std::string> result;
72 if (j.contains(key) && j[key].is_array()) {
73 for (const auto& item : j[key]) {
74 if (item.is_string()) {
75 result.push_back(item.get<std::string>());
76 }
77 }
78 }
79 return result;
80}
81
82inline bool get_bool(const json& j, const std::string& key, bool default_val = false) {
83 if (j.contains(key) && j[key].is_boolean()) {
84 return j[key].get<bool>();
85 }
86 return default_val;
87}
88
89} // namespace detail
90
91// ============================================================================
92// ENV VALUE PARSING
93// ============================================================================
94
95inline core::EnvValue parse_env_value(const json& j) {
96 core::EnvValue ev;
97
98 if (j.is_string()) {
99 ev.op = core::EnvOp::Set;
100 ev.value = j.get<std::string>();
101 return ev;
102 }
103
104 if (j.is_object()) {
105 std::string op_str = detail::get_string(j, "op", "set");
106 auto op = core::parse_env_op(op_str);
107 ev.op = op.value_or(core::EnvOp::Set);
108 ev.value = detail::get_string(j, "value");
109 ev.separator = detail::get_string(j, "separator", ":");
110 }
111
112 return ev;
113}
114
115inline core::EnvMap parse_env_map(const json& j) {
116 core::EnvMap result;
117 if (j.is_object()) {
118 for (auto& [key, val] : j.items()) {
119 result[key] = parse_env_value(val);
120 }
121 }
122 return result;
123}
124
125inline std::string validate_env_map(const json& values) {
126 if (!values.is_object()) return "environment must be an object";
127 for (const auto& [key, value] : values.items()) {
128 if (key.empty()) return "environment keys must not be empty";
129 if (key.find('=') != std::string::npos || key.find('\0') != std::string::npos) {
130 return "invalid environment key: '" + key + "'";
131 }
132 if (value.is_string()) continue;
133 if (!value.is_object()) return "environment value for '" + key + "' must be a string or object";
134 if (const auto error = detail::validate_keys(value, {"op", "value", "separator"},
135 "environment operation for '" + key + "'"); !error.empty()) return error;
136 if (!value.contains("op") || !value["op"].is_string()) {
137 return "environment operation for '" + key + "' requires a string op";
138 }
139 const auto op = core::parse_env_op(value["op"].get<std::string>());
140 if (!op) return "unknown environment operation for '" + key + "'";
141 if (*op != core::EnvOp::Unset && (!value.contains("value") || !value["value"].is_string())) {
142 return "environment operation for '" + key + "' requires a string value";
143 }
144 if (value.contains("separator") && !value["separator"].is_string()) {
145 return "environment separator for '" + key + "' must be a string";
146 }
147 }
148 return {};
149}
150
151// ============================================================================
152// TRUST INFO PARSING
153// ============================================================================
154
155inline core::TrustInfo parse_trust_info(const json& j) {
156 core::TrustInfo ti;
157
158 std::string state_str = detail::get_string(j, "state", "unknown");
159 auto state = core::parse_trust_state(state_str);
160 ti.state = state.value_or(core::TrustState::Unknown);
161
162 ti.source = detail::get_string(j, "source");
163 ti.evaluated_at = detail::get_string(j, "evaluated_at");
164 ti.expires_at = detail::get_string(j, "expires_at");
165 ti.inputs_hash = detail::get_string(j, "inputs_hash");
166
167 if (j.contains("details") && j["details"].is_object()) {
168 for (auto& [key, val] : j["details"].items()) {
169 if (val.is_string()) {
170 ti.details[key] = val.get<std::string>();
171 }
172 }
173 }
174
175 return ti;
176}
177
178// ============================================================================
179// LOADER CONFIG PARSING
180// ============================================================================
181
182inline core::LoaderConfig parse_loader_config(const json& j) {
183 core::LoaderConfig lc;
184 lc.exec_path = detail::get_string(j, "exec_path");
185 lc.args_template = detail::get_string_array(j, "args_template");
186 return lc;
187}
188
189// ============================================================================
190// APP DECLARATION PARSING
191// ============================================================================
192
193inline ParseResult<core::AppDeclaration> parse_app_declaration(const std::string& json_str) {
194 ParseResult<core::AppDeclaration> result;
195
196 try {
197 json j = json::parse(json_str);
198
199 if (!j.is_object()) {
200 result.error = "app manifest must be an object";
201 return result;
202 }
203 if (const auto error = detail::validate_keys(j, {"$schema", "app"}, "app manifest"); !error.empty()) {
204 result.error = error;
205 return result;
206 }
207 if (j.contains("$schema") && (!j["$schema"].is_string() ||
208 j["$schema"].get<std::string>() != "https://nah.rtorr.com/schemas/nap.v2.json")) {
209 result.error = "unsupported app manifest schema";
210 return result;
211 }
212 if (!j.contains("app") || !j["app"].is_object()) {
213 result.error = "missing required object: app";
214 return result;
215 }
216 j = j["app"];
217 if (const auto error = detail::validate_keys(j,
218 {"identity", "execution", "layout", "environment", "permissions", "exports", "metadata"},
219 "app"); !error.empty()) {
220 result.error = error;
221 return result;
222 }
223
224 auto& app = result.value;
225 if (!j.contains("identity") || !j["identity"].is_object()) {
226 result.error = "missing required object: app.identity";
227 return result;
228 }
229 const auto& identity = j["identity"];
230 if (const auto error = detail::validate_keys(identity,
231 {"id", "version", "nak_id", "nak_version_req"}, "app.identity"); !error.empty()) {
232 result.error = error;
233 return result;
234 }
235 app.id = detail::get_string(identity, "id");
236 app.version = detail::get_string(identity, "version");
237 app.nak_id = detail::get_string(identity, "nak_id");
238 app.nak_version_req = detail::get_string(identity, "nak_version_req");
239
240 if (app.id.empty()) {
241 result.error = "missing required field: id";
242 return result;
243 }
244 if (app.version.empty()) {
245 result.error = "missing required field: version";
246 return result;
247 }
248
249 if (!j.contains("execution") || !j["execution"].is_object()) {
250 result.error = "missing required object: app.execution";
251 return result;
252 }
253 const auto& execution = j["execution"];
254 if (const auto error = detail::validate_keys(execution, {"entrypoint", "loader", "args"},
255 "app.execution"); !error.empty()) {
256 result.error = error;
257 return result;
258 }
259 if (execution.contains("args")) {
260 if (const auto error = detail::validate_string_array(execution["args"], "app.execution.args");
261 !error.empty()) { result.error = error; return result; }
262 }
263 app.entrypoint_path = detail::get_string(execution, "entrypoint");
264 app.entrypoint_args = detail::get_string_array(execution, "args");
265 app.nak_loader = detail::get_string(execution, "loader");
266
267 if (app.entrypoint_path.empty()) {
268 result.error = "missing required field: entrypoint path";
269 return result;
270 }
271
272 if (j.contains("layout")) {
273 const auto& layout = j["layout"];
274 if (const auto error = detail::validate_keys(layout, {"lib_dirs", "asset_dirs"}, "app.layout");
275 !error.empty()) { result.error = error; return result; }
276 for (const char* key : {"lib_dirs", "asset_dirs"}) {
277 if (layout.contains(key)) {
278 if (const auto error = detail::validate_string_array(layout[key], std::string("app.layout.") + key);
279 !error.empty()) { result.error = error; return result; }
280 }
281 }
282 app.lib_dirs = detail::get_string_array(layout, "lib_dirs");
283 app.asset_dirs = detail::get_string_array(layout, "asset_dirs");
284 }
285
286 if (j.contains("environment") && j["environment"].is_object()) {
287 const auto error = validate_env_map(j["environment"]);
288 if (!error.empty()) {
289 result.error = error;
290 return result;
291 }
292 app.environment = parse_env_map(j["environment"]);
293 } else if (j.contains("environment")) {
294 result.error = "environment must be an object";
295 return result;
296 }
297
298 if (j.contains("exports")) {
299 if (!j["exports"].is_array()) { result.error = "app.exports must be an array"; return result; }
300 for (const auto& exp : j["exports"]) {
301 if (const auto error = detail::validate_keys(exp, {"id", "path", "type"}, "app.exports item");
302 !error.empty()) { result.error = error; return result; }
303 core::AssetExportDecl aed;
304 aed.id = detail::get_string(exp, "id");
305 aed.path = detail::get_string(exp, "path");
306 aed.type = detail::get_string(exp, "type");
307 if (aed.id.empty() || aed.path.empty()) {
308 result.error = "app.exports items require string id and path";
309 return result;
310 }
311 app.asset_exports.push_back(aed);
312 }
313 }
314
315 if (j.contains("permissions")) {
316 const auto& permissions = j["permissions"];
317 if (const auto error = detail::validate_keys(permissions, {"filesystem", "network"}, "app.permissions");
318 !error.empty()) { result.error = error; return result; }
319 for (const char* key : {"filesystem", "network"}) {
320 if (permissions.contains(key)) {
321 if (const auto error = detail::validate_string_array(permissions[key], std::string("app.permissions.") + key);
322 !error.empty()) { result.error = error; return result; }
323 }
324 }
325 app.permissions_filesystem = detail::get_string_array(permissions, "filesystem");
326 app.permissions_network = detail::get_string_array(permissions, "network");
327 }
328
329 if (j.contains("metadata")) {
330 if (!j["metadata"].is_object()) { result.error = "app.metadata must be an object"; return result; }
331 for (const char* key : {"description", "author", "license", "homepage"}) {
332 if (j["metadata"].contains(key) && !j["metadata"][key].is_string()) {
333 result.error = std::string("app.metadata.") + key + " must be a string";
334 return result;
335 }
336 }
337 app.description = detail::get_string(j["metadata"], "description");
338 app.author = detail::get_string(j["metadata"], "author");
339 app.license = detail::get_string(j["metadata"], "license");
340 app.homepage = detail::get_string(j["metadata"], "homepage");
341 }
342
343 result.ok = true;
344
345 } catch (const json::exception& e) {
346 result.error = std::string("JSON parse error: ") + e.what();
347 }
348
349 return result;
350}
351
352// ============================================================================
353// HOST ENVIRONMENT PARSING
354// ============================================================================
355
356inline ParseResult<core::HostEnvironment> parse_host_environment(const json& j,
357 const std::string& source_path = "") {
358 ParseResult<core::HostEnvironment> result;
359
360 try {
361 if (j.contains("$schema") &&
362 (!j["$schema"].is_string() ||
363 j["$schema"].get<std::string>() != "https://nah.rtorr.com/schemas/nah.v2.json")) {
364 result.error = "unsupported host schema";
365 return result;
366 }
367 if (const auto error = detail::validate_keys(j, {"$schema", "environment", "paths"}, "host configuration");
368 !error.empty()) { result.error = error; return result; }
369 auto& host_env = result.value;
370 const json& config = j;
371
372 host_env.source_path = source_path;
373
374 // Environment section
375 if (config.contains("environment") && config["environment"].is_object()) {
376 const auto error = validate_env_map(config["environment"]);
377 if (!error.empty()) {
378 result.error = error;
379 return result;
380 }
381 host_env.vars = parse_env_map(config["environment"]);
382 } else if (config.contains("environment")) {
383 result.error = "environment must be an object";
384 return result;
385 }
386
387 // Paths section
388 if (config.contains("paths")) {
389 if (const auto error = detail::validate_keys(config["paths"], {"library_prepend", "library_append"},
390 "host paths"); !error.empty()) {
391 result.error = error; return result;
392 }
393 for (const char* key : {"library_prepend", "library_append"}) {
394 if (config["paths"].contains(key)) {
395 if (const auto error = detail::validate_string_array(config["paths"][key],
396 std::string("host paths.") + key); !error.empty()) {
397 result.error = error; return result;
398 }
399 }
400 }
401 host_env.paths.library_prepend = detail::get_string_array(config["paths"], "library_prepend");
402 host_env.paths.library_append = detail::get_string_array(config["paths"], "library_append");
403 }
404
405 result.ok = true;
406
407 } catch (const json::exception& e) {
408 result.error = std::string("JSON parse error: ") + e.what();
409 }
410
411 return result;
412}
413
414inline ParseResult<core::HostEnvironment> parse_host_environment(const std::string& json_str,
415 const std::string& source_path = "") {
416 ParseResult<core::HostEnvironment> result;
417
418 try {
419 json j = json::parse(json_str);
420 return parse_host_environment(j, source_path);
421 } catch (const json::exception& e) {
422 result.error = std::string("JSON parse error: ") + e.what();
423 }
424
425 return result;
426}
427
428// ============================================================================
429// INSTALL RECORD PARSING
430// ============================================================================
431
432inline ParseResult<core::InstallRecord> parse_install_record(const std::string& json_str,
433 const std::string& source_path = "") {
434 ParseResult<core::InstallRecord> result;
435
436 try {
437 json j = json::parse(json_str);
438 auto& ir = result.value;
439
440 if (j.contains("$schema") &&
441 (!j["$schema"].is_string() ||
442 j["$schema"].get<std::string>() != "https://nah.rtorr.com/schemas/app-record.v2.json")) {
443 result.error = "unsupported app record schema";
444 return result;
445 }
446 if (const auto error = detail::validate_keys(j,
447 {"$schema", "install", "app", "nak", "paths", "provenance", "trust", "overrides"},
448 "app install record"); !error.empty()) { result.error = error; return result; }
449 for (const char* section : {"install", "app", "paths"}) {
450 if (!j.contains(section) || !j[section].is_object()) {
451 result.error = std::string("missing required object: ") + section;
452 return result;
453 }
454 }
455
456 ir.source_path = source_path;
457
458 // Install section
459 if (j.contains("install") && j["install"].is_object()) {
460 ir.install.instance_id = detail::get_string(j["install"], "instance_id");
461 }
462
463 if (ir.install.instance_id.empty()) {
464 result.error = "missing required field: install.instance_id";
465 return result;
466 }
467
468 // App section (audit only)
469 if (j.contains("app") && j["app"].is_object()) {
470 ir.app.id = detail::get_string(j["app"], "id");
471 ir.app.version = detail::get_string(j["app"], "version");
472 ir.app.nak_id = detail::get_string(j["app"], "nak_id");
473 ir.app.nak_version_req = detail::get_string(j["app"], "nak_version_req");
474 }
475 if (ir.app.id.empty() || ir.app.version.empty()) {
476 result.error = "app record requires app.id and app.version";
477 return result;
478 }
479
480 // NAK section
481 if (j.contains("nak") && j["nak"].is_object()) {
482 ir.nak.id = detail::get_string(j["nak"], "id");
483 ir.nak.version = detail::get_string(j["nak"], "version");
484 ir.nak.record_ref = detail::get_string(j["nak"], "record_ref");
485 ir.nak.loader = detail::get_string(j["nak"], "loader");
486 ir.nak.selection_reason = detail::get_string(j["nak"], "selection_reason");
487 }
488
489 // Paths section
490 if (j.contains("paths") && j["paths"].is_object()) {
491 ir.paths.install_root = detail::get_string(j["paths"], "install_root");
492 }
493
494 if (ir.paths.install_root.empty()) {
495 result.error = "missing required field: paths.install_root";
496 return result;
497 }
498
499 // Provenance section
500 if (j.contains("provenance") && j["provenance"].is_object()) {
501 ir.provenance.package_hash = detail::get_string(j["provenance"], "package_hash");
502 ir.provenance.installed_at = detail::get_string(j["provenance"], "installed_at");
503 ir.provenance.installed_by = detail::get_string(j["provenance"], "installed_by");
504 ir.provenance.source = detail::get_string(j["provenance"], "source");
505 }
506
507 // Trust section
508 if (j.contains("trust") && j["trust"].is_object()) {
509 ir.trust = parse_trust_info(j["trust"]);
510 }
511
512 // Overrides section
513 if (j.contains("overrides") && j["overrides"].is_object()) {
514 const auto& ovr = j["overrides"];
515
516 if (ovr.contains("environment") && ovr["environment"].is_object()) {
517 const auto error = validate_env_map(ovr["environment"]);
518 if (!error.empty()) {
519 result.error = error;
520 return result;
521 }
522 ir.overrides.environment = parse_env_map(ovr["environment"]);
523 } else if (ovr.contains("environment")) {
524 result.error = "environment must be an object";
525 return result;
526 }
527
528 if (ovr.contains("arguments") && ovr["arguments"].is_object()) {
529 ir.overrides.arguments.prepend = detail::get_string_array(ovr["arguments"], "prepend");
530 ir.overrides.arguments.append = detail::get_string_array(ovr["arguments"], "append");
531 }
532
533 if (ovr.contains("paths") && ovr["paths"].is_object()) {
534 ir.overrides.paths.library_prepend = detail::get_string_array(ovr["paths"], "library_prepend");
535 ir.overrides.paths.library_append = detail::get_string_array(ovr["paths"], "library_append");
536 }
537 }
538
539 result.ok = true;
540
541 } catch (const json::exception& e) {
542 result.error = std::string("JSON parse error: ") + e.what();
543 }
544
545 return result;
546}
547
548// ============================================================================
549// RUNTIME DESCRIPTOR PARSING
550// ============================================================================
551
552inline ParseResult<core::RuntimeDescriptor> parse_runtime_descriptor(const std::string& json_str,
553 const std::string& source_path = "") {
554 ParseResult<core::RuntimeDescriptor> result;
555
556 try {
557 json j = json::parse(json_str);
558 auto& rd = result.value;
559
560 if (j.contains("$schema") &&
561 (!j["$schema"].is_string() ||
562 j["$schema"].get<std::string>() != "https://nah.rtorr.com/schemas/nak-record.v1.json")) {
563 result.error = "unsupported NAK record schema";
564 return result;
565 }
566 if (const auto error = detail::validate_keys(j,
567 {"$schema", "nak", "paths", "environment", "loaders", "execution", "provenance", "trust"},
568 "NAK install record"); !error.empty()) { result.error = error; return result; }
569 for (const char* section : {"nak", "paths"}) {
570 if (!j.contains(section) || !j[section].is_object()) {
571 result.error = std::string("missing required object: ") + section;
572 return result;
573 }
574 }
575
576 rd.source_path = source_path;
577
578 // NAK section
579 if (j.contains("nak") && j["nak"].is_object()) {
580 rd.nak.id = detail::get_string(j["nak"], "id");
581 rd.nak.version = detail::get_string(j["nak"], "version");
582 }
583
584 if (rd.nak.id.empty()) {
585 result.error = "missing required field: nak.id";
586 return result;
587 }
588 if (rd.nak.version.empty()) {
589 result.error = "missing required field: nak.version";
590 return result;
591 }
592
593 // Paths section
594 if (j.contains("paths") && j["paths"].is_object()) {
595 rd.paths.root = detail::get_string(j["paths"], "root");
596 rd.paths.resource_root = detail::get_string(j["paths"], "resource_root");
597 rd.paths.lib_dirs = detail::get_string_array(j["paths"], "lib_dirs");
598 }
599
600 if (rd.paths.root.empty()) {
601 result.error = "missing required field: paths.root";
602 return result;
603 }
604
605 // Environment section
606 if (j.contains("environment") && j["environment"].is_object()) {
607 const auto error = validate_env_map(j["environment"]);
608 if (!error.empty()) {
609 result.error = error;
610 return result;
611 }
612 rd.environment = parse_env_map(j["environment"]);
613 } else if (j.contains("environment")) {
614 result.error = "environment must be an object";
615 return result;
616 }
617
618 // Loaders section
619 if (j.contains("loaders") && j["loaders"].is_object()) {
620 for (auto& [name, config] : j["loaders"].items()) {
621 if (const auto error = detail::validate_keys(config, {"exec_path", "args_template"},
622 "NAK loader " + name); !error.empty()) { result.error = error; return result; }
623 if (!config.contains("exec_path") || !config["exec_path"].is_string() ||
624 config["exec_path"].get<std::string>().empty()) {
625 result.error = "NAK loader requires a non-empty exec_path: " + name;
626 return result;
627 }
628 if (config.contains("args_template")) {
629 if (const auto error = detail::validate_string_array(config["args_template"],
630 "NAK loader args_template"); !error.empty()) { result.error = error; return result; }
631 }
632 rd.loaders[name] = parse_loader_config(config);
633 }
634 } else if (j.contains("loaders")) {
635 result.error = "loaders must be an object";
636 return result;
637 }
638
639 // Execution section
640 if (j.contains("execution") && j["execution"].is_object()) {
641 rd.execution.present = true;
642 rd.execution.cwd = detail::get_string(j["execution"], "cwd");
643 }
644
645 // Provenance section
646 if (j.contains("provenance") && j["provenance"].is_object()) {
647 rd.provenance.package_hash = detail::get_string(j["provenance"], "package_hash");
648 rd.provenance.installed_at = detail::get_string(j["provenance"], "installed_at");
649 rd.provenance.installed_by = detail::get_string(j["provenance"], "installed_by");
650 rd.provenance.source = detail::get_string(j["provenance"], "source");
651 }
652
653 if (j.contains("trust") && j["trust"].is_object()) {
654 rd.trust = parse_trust_info(j["trust"]);
655 }
656
657 result.ok = true;
658
659 } catch (const json::exception& e) {
660 result.error = std::string("JSON parse error: ") + e.what();
661 }
662
663 return result;
664}
665
666// ============================================================================
667// LAUNCH CONTRACT SERIALIZATION (already in nah_core.h, re-export here)
668// ============================================================================
669
670using core::serialize_contract;
671using core::serialize_result;
672
673// ============================================================================
674// LAUNCH CONTRACT PARSING (for cached contracts)
675// ============================================================================
676
677inline ParseResult<core::LaunchContract> parse_launch_contract(const std::string& json_str) {
678 ParseResult<core::LaunchContract> result;
679
680 try {
681 json j = json::parse(json_str);
682 auto& c = result.value;
683
684 if (const auto error = detail::validate_keys(j,
685 {"schema", "app", "nak", "execution", "environment", "permissions", "trust"},
686 "launch contract"); !error.empty()) { result.error = error; return result; }
687 for (const char* section : {"app", "nak", "execution", "environment", "permissions", "trust"}) {
688 if (!j.contains(section) || !j[section].is_object()) {
689 result.error = std::string("missing required object: ") + section;
690 return result;
691 }
692 }
693 if (!j.contains("schema") || !j["schema"].is_string() ||
694 j["schema"].get<std::string>() != core::NAH_CONTRACT_SCHEMA) {
695 result.error = "unsupported launch contract schema";
696 return result;
697 }
698
699 if (const auto error = detail::validate_keys(j["app"],
700 {"id", "version", "root", "entrypoint", "package_hash"}, "launch app");
701 !error.empty()) { result.error = error; return result; }
702 for (const char* key : {"id", "version", "root", "entrypoint", "package_hash"}) {
703 if (!j["app"].contains(key) || !j["app"][key].is_string()) {
704 result.error = std::string("launch app requires string field: ") + key;
705 return result;
706 }
707 }
708 if (const auto error = detail::validate_keys(j["nak"],
709 {"id", "version", "root", "resource_root", "record_ref", "package_hash"}, "launch NAK");
710 !error.empty()) { result.error = error; return result; }
711 for (const char* key : {"id", "version", "root", "resource_root", "record_ref", "package_hash"}) {
712 if (!j["nak"].contains(key) || !j["nak"][key].is_string()) {
713 result.error = std::string("launch NAK requires string field: ") + key;
714 return result;
715 }
716 }
717 if (const auto error = detail::validate_keys(j["execution"],
718 {"binary", "arguments", "cwd", "library_path_env_key", "library_paths"}, "launch execution");
719 !error.empty()) { result.error = error; return result; }
720 for (const char* key : {"binary", "cwd", "library_path_env_key"}) {
721 if (!j["execution"].contains(key) || !j["execution"][key].is_string()) {
722 result.error = std::string("launch execution requires string field: ") + key;
723 return result;
724 }
725 }
726 for (const char* key : {"arguments", "library_paths"}) {
727 if (!j["execution"].contains(key)) {
728 result.error = std::string("launch execution requires field: ") + key;
729 return result;
730 }
731 if (const auto error = detail::validate_string_array(j["execution"][key],
732 std::string("launch execution.") + key); !error.empty()) {
733 result.error = error; return result;
734 }
735 }
736 if (const auto error = detail::validate_keys(j["permissions"], {"filesystem", "network"},
737 "launch permissions"); !error.empty()) {
738 result.error = error; return result;
739 }
740 for (const char* key : {"filesystem", "network"}) {
741 if (!j["permissions"].contains(key)) {
742 result.error = std::string("launch permissions requires field: ") + key;
743 return result;
744 }
745 if (const auto error = detail::validate_string_array(j["permissions"][key],
746 std::string("launch permissions.") + key); !error.empty()) {
747 result.error = error; return result;
748 }
749 }
750 for (const auto& [key, value] : j["environment"].items()) {
751 if (!value.is_string()) { result.error = "launch environment value must be a string: " + key; return result; }
752 }
753 if (const auto error = detail::validate_keys(j["trust"],
754 {"state", "source", "evaluated_at", "expires_at"}, "launch trust");
755 !error.empty()) { result.error = error; return result; }
756 for (const char* key : {"state", "source", "evaluated_at", "expires_at"}) {
757 if (!j["trust"].contains(key) || !j["trust"][key].is_string()) {
758 result.error = std::string("launch trust requires string field: ") + key;
759 return result;
760 }
761 }
762 if (!core::parse_trust_state(j["trust"]["state"].get<std::string>())) {
763 result.error = "launch trust state is invalid";
764 return result;
765 }
766
767 // App section
768 if (j.contains("app") && j["app"].is_object()) {
769 c.app.id = detail::get_string(j["app"], "id");
770 c.app.version = detail::get_string(j["app"], "version");
771 c.app.root = detail::get_string(j["app"], "root");
772 c.app.entrypoint = detail::get_string(j["app"], "entrypoint");
773 c.app.package_hash = detail::get_string(j["app"], "package_hash");
774 }
775
776 // NAK section
777 if (j.contains("nak") && j["nak"].is_object()) {
778 c.nak.id = detail::get_string(j["nak"], "id");
779 c.nak.version = detail::get_string(j["nak"], "version");
780 c.nak.root = detail::get_string(j["nak"], "root");
781 c.nak.resource_root = detail::get_string(j["nak"], "resource_root");
782 c.nak.record_ref = detail::get_string(j["nak"], "record_ref");
783 c.nak.package_hash = detail::get_string(j["nak"], "package_hash");
784 }
785
786 // Execution section
787 if (j.contains("execution") && j["execution"].is_object()) {
788 c.execution.binary = detail::get_string(j["execution"], "binary");
789 c.execution.arguments = detail::get_string_array(j["execution"], "arguments");
790 c.execution.cwd = detail::get_string(j["execution"], "cwd");
791 c.execution.library_path_env_key = detail::get_string(j["execution"], "library_path_env_key");
792 c.execution.library_paths = detail::get_string_array(j["execution"], "library_paths");
793 }
794
795 // Environment section
796 if (j.contains("environment") && j["environment"].is_object()) {
797 for (auto& [key, val] : j["environment"].items()) {
798 if (val.is_string()) {
799 c.environment[key] = val.get<std::string>();
800 }
801 }
802 }
803
804 // Permission requests
805 if (j.contains("permissions") && j["permissions"].is_object()) {
806 c.permissions.filesystem = detail::get_string_array(j["permissions"], "filesystem");
807 c.permissions.network = detail::get_string_array(j["permissions"], "network");
808 }
809
810 // Trust section
811 if (j.contains("trust") && j["trust"].is_object()) {
812 c.trust = parse_trust_info(j["trust"]);
813 }
814
815 result.ok = true;
816
817 } catch (const json::exception& e) {
818 result.error = std::string("JSON parse error: ") + e.what();
819 }
820
821 return result;
822}
823
824} // namespace json
825} // namespace nah
826
827#endif // __cplusplus
828
829#endif // NAH_JSON_H