diff --git a/.github/workflows/c-sdk.yml b/.github/workflows/c-sdk.yml new file mode 100644 index 0000000..eb426ac --- /dev/null +++ b/.github/workflows/c-sdk.yml @@ -0,0 +1,58 @@ +# SPDX-License-Identifier: Apache-2.0 +name: c-sdk + +on: + push: + branches: [main] + paths: + - 'packages/device-connect-edge-c/**' + - 'packages/device-connect-agent-tools-c/**' + - '.github/workflows/c-sdk.yml' + pull_request: + paths: + - 'packages/device-connect-edge-c/**' + - 'packages/device-connect-agent-tools-c/**' + - '.github/workflows/c-sdk.yml' + workflow_dispatch: + +jobs: + build-and-test: + name: ${{ matrix.os }} + runs-on: ${{ matrix.os }} + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest] + steps: + - uses: actions/checkout@v4 + + - name: Install deps (Linux) + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install -y cmake libssl-dev libcurl4-openssl-dev + # NATS C client (cnats) from source + git clone --depth 1 https://github.com/nats-io/nats.c.git + cmake -S nats.c -B nats.c/build -DNATS_BUILD_STREAMING=OFF -DCMAKE_BUILD_TYPE=Release + sudo cmake --build nats.c/build --target install -j + sudo ldconfig + echo "NATS_PREFIX=/usr/local" >> "$GITHUB_ENV" + + - name: Install deps (macOS) + if: runner.os == 'macOS' + run: | + brew install cnats curl + echo "NATS_PREFIX=$(brew --prefix cnats)" >> "$GITHUB_ENV" + + - name: Build + test edge-c + working-directory: packages/device-connect-edge-c + run: | + make NATS_CFLAGS="-I${NATS_PREFIX}/include" NATS_LIBS="-L${NATS_PREFIX}/lib -lnats" + make test + + - name: Build + test agent-tools-c + working-directory: packages/device-connect-agent-tools-c + run: | + make + make test diff --git a/packages/device-connect-agent-tools-c/.gitignore b/packages/device-connect-agent-tools-c/.gitignore new file mode 100644 index 0000000..6cb2746 --- /dev/null +++ b/packages/device-connect-agent-tools-c/.gitignore @@ -0,0 +1,8 @@ +# C build artifacts +*.o +*.a +# example/test binaries (extensionless, built in-tree) +/temp_sensor +/dc_agent +tests/test_edge +tests/test_agent diff --git a/packages/device-connect-agent-tools-c/Makefile b/packages/device-connect-agent-tools-c/Makefile new file mode 100644 index 0000000..ad74bf9 --- /dev/null +++ b/packages/device-connect-agent-tools-c/Makefile @@ -0,0 +1,40 @@ +# SPDX-License-Identifier: Apache-2.0 +# Device Connect C agent-tools. +# +# Builds libdc_agent_tools.a and the dc_agent CLI. Requires libcurl. +# +# make +# make CURL_CFLAGS=... CURL_LIBS=-lcurl +# make test +# make clean + +CC ?= cc +CFLAGS ?= -std=c11 -D_GNU_SOURCE -Wall -Wextra -O2 +CURL_CFLAGS ?= +CURL_LIBS ?= -lcurl +INCLUDES = -Iinclude $(CURL_CFLAGS) +LDLIBS = $(CURL_LIBS) + +LIB = libdc_agent_tools.a +SRCS = src/json.c src/http.c src/agent_tools.c +OBJS = $(SRCS:.c=.o) + +all: $(LIB) dc_agent + +$(LIB): $(OBJS) + ar rcs $@ $^ + +src/%.o: src/%.c + $(CC) $(CFLAGS) $(INCLUDES) -c $< -o $@ + +dc_agent: examples/dc_agent.c $(LIB) + $(CC) $(CFLAGS) $(INCLUDES) -o $@ examples/dc_agent.c $(LIB) $(LDLIBS) + +test: $(LIB) + $(MAKE) -C tests test + +clean: + rm -f $(OBJS) $(LIB) dc_agent + $(MAKE) -C tests clean 2>/dev/null || true + +.PHONY: all test clean diff --git a/packages/device-connect-agent-tools-c/README.md b/packages/device-connect-agent-tools-c/README.md new file mode 100644 index 0000000..e7eabb8 --- /dev/null +++ b/packages/device-connect-agent-tools-c/README.md @@ -0,0 +1,49 @@ + +# device-connect-agent-tools-c + +The **Device Connect agent-tools in C** — the external-agent side. A C analogue +of `device-connect-agent-tools`: the meta-tools an agent uses to discover and +drive a fleet through the portal HTTP API. + +## Tools (`dc/agent_tools.h`) + +- `dc_describe_fleet` -> `GET /api/agent/v1/fleet` +- `dc_list_devices(device_type, location)` -> `GET /api/agent/v1/devices` +- `dc_get_device_functions(device_id)` -> `GET /api/agent/v1/devices/{id}/functions` +- `dc_invoke_device(device_id, function, params, reason)` -> `POST /api/agent/v1/devices/{id}/invoke` + +Each returns the parsed JSON response (caller `json_free`). HTTP + TLS via +libcurl (`dc/http.h`); JSON via the shared `dc/json.h`. Portal URL and token +come from `DEVICE_CONNECT_PORTAL_URL` / `DEVICE_CONNECT_PORTAL_TOKEN` (or the +`dc_agent` struct). + +## Build + +Requires libcurl. + +``` +make # builds libdc_agent_tools.a + the dc_agent CLI +make test +``` + +## CLI + +``` +export DEVICE_CONNECT_PORTAL_URL=https://portal.deviceconnect.dev +export DEVICE_CONNECT_PORTAL_TOKEN=dcp_... +./dc_agent fleet +./dc_agent list temp_sensor +./dc_agent functions alpha-temp-001 +./dc_agent invoke alpha-temp-001 set_target '{"celsius":42}' "daily setpoint" +``` + +Verified end-to-end against a live DC portal driving the C edge SDK +(`device-connect-edge-c`): discovered 3 devices and invoked them with the +device JSON-RPC responses (and `-32601`/`-32602` errors) propagating through +the portal envelope. + +## Scope / not yet + +- The four core meta-tools over the portal HTTP API. Event streaming + (`/events/.../stream`) and the Strands/LangChain/MCP adapters from the Python + package are not ported. diff --git a/packages/device-connect-agent-tools-c/examples/dc_agent.c b/packages/device-connect-agent-tools-c/examples/dc_agent.c new file mode 100644 index 0000000..efda171 --- /dev/null +++ b/packages/device-connect-agent-tools-c/examples/dc_agent.c @@ -0,0 +1,86 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) 2024-2026, Arm Limited and Contributors. All rights reserved. + * + * dc_agent.c -- a small CLI over the C agent-tools, mirroring the dc-portalctl + * discover/invoke surface. Reads DEVICE_CONNECT_PORTAL_URL / _TOKEN from the + * environment. + * + * dc_agent fleet + * dc_agent list [device_type] [location] + * dc_agent functions + * dc_agent invoke [json_params] [reason] + * + * ASCII-only source. + */ + +#include "dc/agent_tools.h" +#include "dc/http.h" +#include "dc/json.h" + +#include +#include +#include + +static int dump(json *j, long status) { + if (j == NULL) { + fprintf(stderr, "request failed (http=%ld)\n", status); + return 1; + } + char *s = json_dumps(j, 1, NULL); + printf("%s\n", s ? s : "(null)"); + free(s); + json_free(j); + return 0; +} + +int main(int argc, char **argv) { + if (argc < 2) { + fprintf(stderr, + "usage: dc_agent fleet | list [type] [loc] | functions | " + "invoke [json_params] [reason]\n"); + return 2; + } + dc_http_global_init(); + dc_agent a; + memset(&a, 0, sizeof(a)); + if (dc_agent_resolve(&a) != 0) { + fprintf(stderr, + "set DEVICE_CONNECT_PORTAL_URL and DEVICE_CONNECT_PORTAL_TOKEN\n"); + dc_http_global_cleanup(); + return 2; + } + + long status = 0; + int rc = 1; + const char *cmd = argv[1]; + if (!strcmp(cmd, "fleet")) { + rc = dump(dc_describe_fleet(&a, &status), status); + } else if (!strcmp(cmd, "list")) { + const char *type = argc > 2 ? argv[2] : NULL; + const char *loc = argc > 3 ? argv[3] : NULL; + rc = dump(dc_list_devices(&a, type, loc, &status), status); + } else if (!strcmp(cmd, "functions") && argc > 2) { + rc = dump(dc_get_device_functions(&a, argv[2], &status), status); + } else if (!strcmp(cmd, "invoke") && argc > 3) { + const char *id = argv[2]; + const char *fn = argv[3]; + const char *pj = argc > 4 ? argv[4] : "{}"; + const char *reason = argc > 5 ? argv[5] : "dc_agent CLI invoke"; + const char *err = NULL; + json *params = json_parse(pj, strlen(pj), &err); + if (params == NULL) { + fprintf(stderr, "bad json params: %s\n", err ? err : "?"); + dc_http_global_cleanup(); + return 2; + } + rc = dump(dc_invoke_device(&a, id, fn, params, reason, &status), + status); + json_free(params); + } else { + fprintf(stderr, "unknown command or missing args: %s\n", cmd); + rc = 2; + } + dc_http_global_cleanup(); + return rc; +} diff --git a/packages/device-connect-agent-tools-c/include/dc/agent_tools.h b/packages/device-connect-agent-tools-c/include/dc/agent_tools.h new file mode 100644 index 0000000..250bf14 --- /dev/null +++ b/packages/device-connect-agent-tools-c/include/dc/agent_tools.h @@ -0,0 +1,55 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) 2024-2026, Arm Limited and Contributors. All rights reserved. + * + * dc/agent_tools.h -- the Device Connect agent-tools meta-tools in C. + * + * The external-agent path (the C analogue of device-connect-agent-tools): + * describe_fleet / list_devices / get_device_functions / invoke_device over + * the portal HTTP API. Results are returned as parsed JSON (caller json_free). + * + * ASCII-only source. + */ + +#ifndef DC_AGENT_TOOLS_H +#define DC_AGENT_TOOLS_H + +#include "dc/json.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct { + const char *portal_url; /* e.g. https://portal.deviceconnect.dev; NULL -> $DEVICE_CONNECT_PORTAL_URL */ + const char *token; /* dcp_... ; NULL -> $DEVICE_CONNECT_PORTAL_TOKEN */ +} dc_agent; + +/* Resolve portal_url/token from the struct or the environment. Returns 0 if + * both are available, -1 otherwise. */ +int dc_agent_resolve(dc_agent *a); + +/* + * Each call performs one portal request and returns the parsed JSON response + * (caller json_free), or NULL on transport/parse failure. *http_status, when + * non-NULL, receives the HTTP status code. + */ +json *dc_describe_fleet(const dc_agent *a, long *http_status); +json *dc_list_devices(const dc_agent *a, const char *device_type, + const char *location, long *http_status); +json *dc_get_device_functions(const dc_agent *a, const char *device_id, + long *http_status); +/* + * Invoke a device function. `params` is a JSON object (borrowed; may be NULL). + * `reason` is the mandatory audit string. Returns the portal's response + * envelope (which embeds the device's JSON-RPC `response`). + */ +json *dc_invoke_device(const dc_agent *a, const char *device_id, + const char *function, const json *params, + const char *reason, long *http_status); + +#ifdef __cplusplus +} +#endif + +#endif /* DC_AGENT_TOOLS_H */ diff --git a/packages/device-connect-agent-tools-c/include/dc/http.h b/packages/device-connect-agent-tools-c/include/dc/http.h new file mode 100644 index 0000000..b063420 --- /dev/null +++ b/packages/device-connect-agent-tools-c/include/dc/http.h @@ -0,0 +1,49 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) 2024-2026, Arm Limited and Contributors. All rights reserved. + * + * dc/http.h -- a tiny libcurl wrapper for the Device Connect portal HTTP API. + * + * Bearer-authenticated GET/POST returning the response body. Used by the + * agent-tools layer (dc/agent_tools.h). TLS is handled by libcurl. + * + * ASCII-only source. + */ + +#ifndef DC_HTTP_H +#define DC_HTTP_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct { + long status; /* HTTP status code, or 0 on transport failure */ + char *body; /* malloc'd response body (caller frees), or NULL */ + size_t len; +} dc_http_response; + +/* Call once at startup (wraps curl_global_init). */ +int dc_http_global_init(void); +void dc_http_global_cleanup(void); + +/* + * GET url with "Authorization: Bearer ". Returns 0 on a completed + * request (check resp.status), -1 on a transport-level failure. Caller frees + * resp.body. + */ +int dc_http_get(const char *url, const char *token, dc_http_response *resp); + +/* POST url with a JSON body (Content-Type: application/json) + Bearer token. */ +int dc_http_post(const char *url, const char *token, const char *json_body, + dc_http_response *resp); + +void dc_http_response_free(dc_http_response *resp); + +#ifdef __cplusplus +} +#endif + +#endif /* DC_HTTP_H */ diff --git a/packages/device-connect-agent-tools-c/include/dc/json.h b/packages/device-connect-agent-tools-c/include/dc/json.h new file mode 100644 index 0000000..e26127d --- /dev/null +++ b/packages/device-connect-agent-tools-c/include/dc/json.h @@ -0,0 +1,112 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) 2024-2026, Arm Limited and Contributors. All rights reserved. + * + * dc/json.h -- a tiny, dependency-free JSON reader/writer for the MHP + * reference wire node. + * + * Design goals (per the MHP wire contract, specification/draft/wire_contract.md): + * - Serialize with optional lexicographically sorted object keys, so the + * manifest hash (section 7.6) is deterministic. + * - Never emit NaN / Infinity tokens: non-finite reals serialize as null + * (section 4.4, conformance clause C20). + * - Tolerate unknown members on parse so higher layers can ignore extra + * fields (clause C19). + * + * Numbers are stored as either a 64-bit integer or a double, chosen at parse + * time by whether the token carried a '.', 'e' or 'E'. Constructed numbers + * pick the form the caller asked for. + * + * Ownership: container mutators (json_array_append, json_object_set) take + * ownership of the value passed in; json_free frees a value and everything it + * transitively owns. Strings returned by accessors are owned by the node and + * are valid until it is freed. + * + * ASCII-only source (per CLAUDE.md). + */ + +#ifndef DC_JSON_H +#define DC_JSON_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef enum { + JSON_NULL = 0, + JSON_BOOL, + JSON_INT, + JSON_REAL, + JSON_STRING, + JSON_ARRAY, + JSON_OBJECT +} json_type; + +typedef struct json json; + +/* ---- constructors (all return a heap node, or NULL on OOM) ---- */ +json *json_null(void); +json *json_bool(int b); +json *json_int(int64_t v); +json *json_real(double v); +json *json_string(const char *s); /* NUL-terminated, copied */ +json *json_string_n(const char *s, size_t n); /* n bytes, copied */ +json *json_array(void); +json *json_object(void); + +/* ---- container mutators (take ownership of val; key is copied) ---- */ +int json_array_append(json *arr, json *val); /* 0 ok, -1 fail */ +int json_object_set(json *obj, const char *key, json *val); /* replaces dup key */ + +/* ---- type tests / accessors ---- */ +json_type json_typeof(const json *j); +int json_is_null(const json *j); + +/* number value as double regardless of int/real storage; 0 if not a number */ +double json_number(const json *j); +/* integer value; for a real, the truncated value; 0 if not a number */ +int64_t json_integer(const json *j); +int json_truthy(const json *j); /* JSON_BOOL value, or 0 */ + +/* string bytes (NUL-terminated) and length; NULL/0 if not a string */ +const char *json_str(const json *j); +size_t json_strlen(const json *j); + +size_t json_array_size(const json *j); +json *json_array_get(const json *j, size_t i); /* borrowed */ + +json *json_object_get(const json *j, const char *key); /* borrowed, or NULL */ +size_t json_object_size(const json *j); +/* iterate object members by index; key/val borrowed */ +const char *json_object_key_at(const json *j, size_t i); +json *json_object_val_at(const json *j, size_t i); + +/* Deep-copy a value (and everything it owns). NULL on OOM or NULL input. */ +json *json_clone(const json *j); + +/* ---- parse / serialize / free ---- */ +/* + * Parse exactly one JSON value from buf[0..len). Trailing whitespace is + * allowed; trailing non-whitespace is an error. Returns NULL on any syntax + * error. If err is non-NULL it receives a short static description (do not + * free). + */ +json *json_parse(const char *buf, size_t len, const char **err); + +/* + * Serialize to a freshly malloc'd NUL-terminated ASCII string (caller frees). + * If sorted is non-zero, object members are emitted in ascending key order. + * Returns NULL on OOM. *out_len, when non-NULL, receives strlen of the result. + */ +char *json_dumps(const json *j, int sorted, size_t *out_len); + +void json_free(json *j); + +#ifdef __cplusplus +} +#endif + +#endif /* DC_JSON_H */ diff --git a/packages/device-connect-agent-tools-c/src/agent_tools.c b/packages/device-connect-agent-tools-c/src/agent_tools.c new file mode 100644 index 0000000..372a171 --- /dev/null +++ b/packages/device-connect-agent-tools-c/src/agent_tools.c @@ -0,0 +1,154 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) 2024-2026, Arm Limited and Contributors. All rights reserved. + * + * dc/agent_tools.c -- the agent-tools meta-tools over the portal HTTP API. + * + * ASCII-only source. + */ + +#include "dc/agent_tools.h" +#include "dc/http.h" + +#include +#include +#include + +int dc_agent_resolve(dc_agent *a) { + if (a == NULL) { + return -1; + } + if (a->portal_url == NULL) { + a->portal_url = getenv("DEVICE_CONNECT_PORTAL_URL"); + } + if (a->token == NULL) { + a->token = getenv("DEVICE_CONNECT_PORTAL_TOKEN"); + } + return (a->portal_url != NULL && a->token != NULL) ? 0 : -1; +} + +/* percent-encode a query-parameter value into out (best-effort, alnum + -_.~) */ +static void urlencode(const char *s, char *out, size_t cap) { + static const char hexd[] = "0123456789ABCDEF"; + size_t o = 0; + for (; s != NULL && *s != '\0' && o + 4 < cap; s++) { + unsigned char c = (unsigned char)*s; + if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || + (c >= '0' && c <= '9') || c == '-' || c == '_' || c == '.' || + c == '~') { + out[o++] = (char)c; + } else { + out[o++] = '%'; + out[o++] = hexd[c >> 4]; + out[o++] = hexd[c & 0xf]; + } + } + out[o] = '\0'; +} + +static json *parse_or_null(dc_http_response *resp, long *http_status) { + if (http_status != NULL) { + *http_status = resp->status; + } + json *j = NULL; + if (resp->body != NULL) { + const char *err = NULL; + j = json_parse(resp->body, resp->len, &err); + } + dc_http_response_free(resp); + return j; +} + +json *dc_describe_fleet(const dc_agent *a, long *http_status) { + if (a == NULL || a->portal_url == NULL) { + return NULL; + } + char url[1024]; + snprintf(url, sizeof(url), "%s/api/agent/v1/fleet", a->portal_url); + dc_http_response resp; + if (dc_http_get(url, a->token, &resp) != 0) { + return NULL; + } + return parse_or_null(&resp, http_status); +} + +json *dc_list_devices(const dc_agent *a, const char *device_type, + const char *location, long *http_status) { + if (a == NULL || a->portal_url == NULL) { + return NULL; + } + char url[1536]; + int n = snprintf(url, sizeof(url), "%s/api/agent/v1/devices", a->portal_url); + const char *sep = "?"; + char enc[512]; + if (device_type != NULL) { + urlencode(device_type, enc, sizeof(enc)); + n += snprintf(url + n, sizeof(url) - (size_t)n, "%sdevice_type=%s", sep, + enc); + sep = "&"; + } + if (location != NULL) { + urlencode(location, enc, sizeof(enc)); + n += snprintf(url + n, sizeof(url) - (size_t)n, "%slocation=%s", sep, + enc); + } + dc_http_response resp; + if (dc_http_get(url, a->token, &resp) != 0) { + return NULL; + } + return parse_or_null(&resp, http_status); +} + +json *dc_get_device_functions(const dc_agent *a, const char *device_id, + long *http_status) { + if (a == NULL || a->portal_url == NULL || device_id == NULL) { + return NULL; + } + char enc[512]; + urlencode(device_id, enc, sizeof(enc)); + char url[1280]; + snprintf(url, sizeof(url), "%s/api/agent/v1/devices/%s/functions", + a->portal_url, enc); + dc_http_response resp; + if (dc_http_get(url, a->token, &resp) != 0) { + return NULL; + } + return parse_or_null(&resp, http_status); +} + +json *dc_invoke_device(const dc_agent *a, const char *device_id, + const char *function, const json *params, + const char *reason, long *http_status) { + if (a == NULL || a->portal_url == NULL || device_id == NULL || + function == NULL) { + return NULL; + } + /* body: {"function":..., "params":{...}, "reason":..., "timeout":10} */ + json *body = json_object(); + if (body == NULL) { + return NULL; + } + json_object_set(body, "function", json_string(function)); + json_object_set(body, "params", + params ? json_clone(params) : json_object()); + json_object_set(body, "reason", json_string(reason ? reason : "")); + json_object_set(body, "timeout", json_int(10)); + size_t blen = 0; + char *json_body = json_dumps(body, 0, &blen); + json_free(body); + if (json_body == NULL) { + return NULL; + } + char enc[512]; + urlencode(device_id, enc, sizeof(enc)); + char url[1280]; + snprintf(url, sizeof(url), "%s/api/agent/v1/devices/%s/invoke", + a->portal_url, enc); + dc_http_response resp; + int rc = dc_http_post(url, a->token, json_body, &resp); + free(json_body); + if (rc != 0) { + return NULL; + } + return parse_or_null(&resp, http_status); +} diff --git a/packages/device-connect-agent-tools-c/src/http.c b/packages/device-connect-agent-tools-c/src/http.c new file mode 100644 index 0000000..d1639c8 --- /dev/null +++ b/packages/device-connect-agent-tools-c/src/http.c @@ -0,0 +1,116 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) 2024-2026, Arm Limited and Contributors. All rights reserved. + * + * dc/http.c -- libcurl wrapper for the portal HTTP API. + * + * ASCII-only source. + */ + +#include "dc/http.h" + +#include +#include +#include +#include + +typedef struct { + char *buf; + size_t len; + size_t cap; +} growbuf; + +static size_t write_cb(char *ptr, size_t size, size_t nmemb, void *userdata) { + size_t n = size * nmemb; + growbuf *g = (growbuf *)userdata; + if (g->len + n + 1 > g->cap) { + size_t nc = g->cap ? g->cap : 256; + while (g->len + n + 1 > nc) { + nc *= 2; + } + char *nb = (char *)realloc(g->buf, nc); + if (nb == NULL) { + return 0; + } + g->buf = nb; + g->cap = nc; + } + memcpy(g->buf + g->len, ptr, n); + g->len += n; + g->buf[g->len] = '\0'; + return n; +} + +int dc_http_global_init(void) { + return curl_global_init(CURL_GLOBAL_DEFAULT) == CURLE_OK ? 0 : -1; +} + +void dc_http_global_cleanup(void) { curl_global_cleanup(); } + +static int do_request(const char *url, const char *token, const char *body, + dc_http_response *resp) { + if (resp == NULL) { + return -1; + } + resp->status = 0; + resp->body = NULL; + resp->len = 0; + + CURL *curl = curl_easy_init(); + if (curl == NULL) { + return -1; + } + growbuf g = {NULL, 0, 0}; + struct curl_slist *hdrs = NULL; + char auth[1024]; + if (token != NULL) { + snprintf(auth, sizeof(auth), "Authorization: Bearer %s", token); + hdrs = curl_slist_append(hdrs, auth); + } + if (body != NULL) { + hdrs = curl_slist_append(hdrs, "Content-Type: application/json"); + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body); + } + curl_easy_setopt(curl, CURLOPT_URL, url); + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, hdrs); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_cb); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &g); + curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); + curl_easy_setopt(curl, CURLOPT_TIMEOUT, 30L); + curl_easy_setopt(curl, CURLOPT_USERAGENT, "dc-agent-tools-c/0.1"); + + CURLcode rc = curl_easy_perform(curl); + int ret = -1; + if (rc == CURLE_OK) { + long code = 0; + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &code); + resp->status = code; + resp->body = g.buf; + resp->len = g.len; + g.buf = NULL; + ret = 0; + } else { + fprintf(stderr, "[dc-agent] http: %s\n", curl_easy_strerror(rc)); + } + free(g.buf); + curl_slist_free_all(hdrs); + curl_easy_cleanup(curl); + return ret; +} + +int dc_http_get(const char *url, const char *token, dc_http_response *resp) { + return do_request(url, token, NULL, resp); +} + +int dc_http_post(const char *url, const char *token, const char *json_body, + dc_http_response *resp) { + return do_request(url, token, json_body, resp); +} + +void dc_http_response_free(dc_http_response *resp) { + if (resp != NULL) { + free(resp->body); + resp->body = NULL; + resp->len = 0; + } +} diff --git a/packages/device-connect-agent-tools-c/src/json.c b/packages/device-connect-agent-tools-c/src/json.c new file mode 100644 index 0000000..89b2307 --- /dev/null +++ b/packages/device-connect-agent-tools-c/src/json.c @@ -0,0 +1,1001 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) 2024-2026, Arm Limited and Contributors. All rights reserved. + * + * dc/json.c -- implementation of the tiny JSON reader/writer. + * + * ASCII-only source (per CLAUDE.md). + */ + +#include "dc/json.h" + +#include +#include +#include +#include + +struct json { + json_type type; + union { + int boolean; + int64_t inum; + double rnum; + struct { + char *bytes; + size_t len; + } str; + struct { + json **items; + size_t count; + size_t cap; + } arr; + struct { + char **keys; + json **vals; + size_t count; + size_t cap; + } obj; + } u; +}; + +/* ------------------------------------------------------------------ */ +/* constructors */ +/* ------------------------------------------------------------------ */ + +static json *node_new(json_type t) { + json *j = (json *)calloc(1, sizeof(*j)); + if (j != NULL) { + j->type = t; + } + return j; +} + +json *json_null(void) { return node_new(JSON_NULL); } + +json *json_bool(int b) { + json *j = node_new(JSON_BOOL); + if (j != NULL) { + j->u.boolean = b ? 1 : 0; + } + return j; +} + +json *json_int(int64_t v) { + json *j = node_new(JSON_INT); + if (j != NULL) { + j->u.inum = v; + } + return j; +} + +json *json_real(double v) { + json *j = node_new(JSON_REAL); + if (j != NULL) { + j->u.rnum = v; + } + return j; +} + +json *json_string_n(const char *s, size_t n) { + json *j = node_new(JSON_STRING); + if (j == NULL) { + return NULL; + } + j->u.str.bytes = (char *)malloc(n + 1); + if (j->u.str.bytes == NULL) { + free(j); + return NULL; + } + if (n > 0 && s != NULL) { + memcpy(j->u.str.bytes, s, n); + } + j->u.str.bytes[n] = '\0'; + j->u.str.len = n; + return j; +} + +json *json_string(const char *s) { + return json_string_n(s, s != NULL ? strlen(s) : 0); +} + +json *json_array(void) { return node_new(JSON_ARRAY); } +json *json_object(void) { return node_new(JSON_OBJECT); } + +/* ------------------------------------------------------------------ */ +/* container mutators */ +/* ------------------------------------------------------------------ */ + +static int grow_ptrs(void ***p, size_t *cap, size_t need) { + if (*cap >= need) { + return 0; + } + size_t ncap = (*cap == 0) ? 4 : (*cap * 2); + while (ncap < need) { + ncap *= 2; + } + void **np = (void **)realloc(*p, ncap * sizeof(void *)); + if (np == NULL) { + return -1; + } + *p = np; + *cap = ncap; + return 0; +} + +int json_array_append(json *arr, json *val) { + if (arr == NULL || arr->type != JSON_ARRAY || val == NULL) { + json_free(val); + return -1; + } + if (grow_ptrs((void ***)&arr->u.arr.items, &arr->u.arr.cap, + arr->u.arr.count + 1) != 0) { + json_free(val); + return -1; + } + arr->u.arr.items[arr->u.arr.count++] = val; + return 0; +} + +int json_object_set(json *obj, const char *key, json *val) { + if (obj == NULL || obj->type != JSON_OBJECT || key == NULL || val == NULL) { + json_free(val); + return -1; + } + /* replace existing key, preserving slot */ + for (size_t i = 0; i < obj->u.obj.count; i++) { + if (strcmp(obj->u.obj.keys[i], key) == 0) { + json_free(obj->u.obj.vals[i]); + obj->u.obj.vals[i] = val; + return 0; + } + } + if (grow_ptrs((void ***)&obj->u.obj.keys, &obj->u.obj.cap, + obj->u.obj.count + 1) != 0) { + json_free(val); + return -1; + } + /* keys and vals grow in lockstep; vals cap tracked via keys cap */ + json **nv = (json **)realloc(obj->u.obj.vals, + obj->u.obj.cap * sizeof(json *)); + if (nv == NULL) { + json_free(val); + return -1; + } + obj->u.obj.vals = nv; + char *kc = (char *)malloc(strlen(key) + 1); + if (kc == NULL) { + json_free(val); + return -1; + } + strcpy(kc, key); + obj->u.obj.keys[obj->u.obj.count] = kc; + obj->u.obj.vals[obj->u.obj.count] = val; + obj->u.obj.count++; + return 0; +} + +/* ------------------------------------------------------------------ */ +/* accessors */ +/* ------------------------------------------------------------------ */ + +json_type json_typeof(const json *j) { return j != NULL ? j->type : JSON_NULL; } + +int json_is_null(const json *j) { return j == NULL || j->type == JSON_NULL; } + +double json_number(const json *j) { + if (j == NULL) { + return 0.0; + } + if (j->type == JSON_INT) { + return (double)j->u.inum; + } + if (j->type == JSON_REAL) { + return j->u.rnum; + } + return 0.0; +} + +int64_t json_integer(const json *j) { + if (j == NULL) { + return 0; + } + if (j->type == JSON_INT) { + return j->u.inum; + } + if (j->type == JSON_REAL) { + return (int64_t)j->u.rnum; + } + return 0; +} + +int json_truthy(const json *j) { + return (j != NULL && j->type == JSON_BOOL) ? j->u.boolean : 0; +} + +const char *json_str(const json *j) { + return (j != NULL && j->type == JSON_STRING) ? j->u.str.bytes : NULL; +} + +size_t json_strlen(const json *j) { + return (j != NULL && j->type == JSON_STRING) ? j->u.str.len : 0; +} + +size_t json_array_size(const json *j) { + return (j != NULL && j->type == JSON_ARRAY) ? j->u.arr.count : 0; +} + +json *json_array_get(const json *j, size_t i) { + if (j == NULL || j->type != JSON_ARRAY || i >= j->u.arr.count) { + return NULL; + } + return j->u.arr.items[i]; +} + +json *json_object_get(const json *j, const char *key) { + if (j == NULL || j->type != JSON_OBJECT || key == NULL) { + return NULL; + } + for (size_t i = 0; i < j->u.obj.count; i++) { + if (strcmp(j->u.obj.keys[i], key) == 0) { + return j->u.obj.vals[i]; + } + } + return NULL; +} + +size_t json_object_size(const json *j) { + return (j != NULL && j->type == JSON_OBJECT) ? j->u.obj.count : 0; +} + +const char *json_object_key_at(const json *j, size_t i) { + if (j == NULL || j->type != JSON_OBJECT || i >= j->u.obj.count) { + return NULL; + } + return j->u.obj.keys[i]; +} + +json *json_object_val_at(const json *j, size_t i) { + if (j == NULL || j->type != JSON_OBJECT || i >= j->u.obj.count) { + return NULL; + } + return j->u.obj.vals[i]; +} + +json *json_clone(const json *j) { + if (j == NULL) { + return NULL; + } + switch (j->type) { + case JSON_NULL: + return json_null(); + case JSON_BOOL: + return json_bool(j->u.boolean); + case JSON_INT: + return json_int(j->u.inum); + case JSON_REAL: + return json_real(j->u.rnum); + case JSON_STRING: + return json_string_n(j->u.str.bytes, j->u.str.len); + case JSON_ARRAY: { + json *a = json_array(); + if (a == NULL) { + return NULL; + } + for (size_t i = 0; i < j->u.arr.count; i++) { + json *c = json_clone(j->u.arr.items[i]); + if (c == NULL || json_array_append(a, c) != 0) { + json_free(a); + return NULL; + } + } + return a; + } + case JSON_OBJECT: { + json *o = json_object(); + if (o == NULL) { + return NULL; + } + for (size_t i = 0; i < j->u.obj.count; i++) { + json *c = json_clone(j->u.obj.vals[i]); + if (c == NULL || json_object_set(o, j->u.obj.keys[i], c) != 0) { + json_free(o); + return NULL; + } + } + return o; + } + default: + return NULL; + } +} + +/* ------------------------------------------------------------------ */ +/* free */ +/* ------------------------------------------------------------------ */ + +void json_free(json *j) { + if (j == NULL) { + return; + } + switch (j->type) { + case JSON_STRING: + free(j->u.str.bytes); + break; + case JSON_ARRAY: + for (size_t i = 0; i < j->u.arr.count; i++) { + json_free(j->u.arr.items[i]); + } + free(j->u.arr.items); + break; + case JSON_OBJECT: + for (size_t i = 0; i < j->u.obj.count; i++) { + free(j->u.obj.keys[i]); + json_free(j->u.obj.vals[i]); + } + free(j->u.obj.keys); + free(j->u.obj.vals); + break; + default: + break; + } + free(j); +} + +/* ------------------------------------------------------------------ */ +/* growable output buffer */ +/* ------------------------------------------------------------------ */ + +typedef struct { + char *buf; + size_t len; + size_t cap; + int err; +} sbuf; + +static void sbuf_reserve(sbuf *s, size_t extra) { + if (s->err) { + return; + } + if (s->len + extra + 1 <= s->cap) { + return; + } + size_t ncap = (s->cap == 0) ? 64 : s->cap; + while (s->len + extra + 1 > ncap) { + ncap *= 2; + } + char *nb = (char *)realloc(s->buf, ncap); + if (nb == NULL) { + s->err = 1; + return; + } + s->buf = nb; + s->cap = ncap; +} + +static void sbuf_putc(sbuf *s, char c) { + sbuf_reserve(s, 1); + if (s->err) { + return; + } + s->buf[s->len++] = c; +} + +static void sbuf_put(sbuf *s, const char *p, size_t n) { + sbuf_reserve(s, n); + if (s->err) { + return; + } + memcpy(s->buf + s->len, p, n); + s->len += n; +} + +static void sbuf_puts(sbuf *s, const char *p) { sbuf_put(s, p, strlen(p)); } + +/* ------------------------------------------------------------------ */ +/* serialization */ +/* ------------------------------------------------------------------ */ + +static void dump_string(sbuf *s, const char *p, size_t n) { + static const char hexd[] = "0123456789abcdef"; + sbuf_putc(s, '"'); + for (size_t i = 0; i < n; i++) { + unsigned char c = (unsigned char)p[i]; + switch (c) { + case '"': + sbuf_put(s, "\\\"", 2); + break; + case '\\': + sbuf_put(s, "\\\\", 2); + break; + case '\b': + sbuf_put(s, "\\b", 2); + break; + case '\f': + sbuf_put(s, "\\f", 2); + break; + case '\n': + sbuf_put(s, "\\n", 2); + break; + case '\r': + sbuf_put(s, "\\r", 2); + break; + case '\t': + sbuf_put(s, "\\t", 2); + break; + default: + if (c < 0x20) { + char esc[6]; + esc[0] = '\\'; + esc[1] = 'u'; + esc[2] = '0'; + esc[3] = '0'; + esc[4] = hexd[(c >> 4) & 0xf]; + esc[5] = hexd[c & 0xf]; + sbuf_put(s, esc, 6); + } else { + /* bytes >= 0x20, including UTF-8 continuation bytes, pass + * through verbatim (already valid JSON). */ + sbuf_putc(s, (char)c); + } + break; + } + } + sbuf_putc(s, '"'); +} + +static void dump_real(sbuf *s, double v) { + if (!isfinite(v)) { + /* C20 / section 4.4: never emit NaN or Infinity. */ + sbuf_puts(s, "null"); + return; + } + char tmp[40]; + /* %.17g round-trips an IEEE-754 double exactly. */ + int n = snprintf(tmp, sizeof(tmp), "%.17g", v); + if (n < 0) { + s->err = 1; + return; + } + sbuf_put(s, tmp, (size_t)n); +} + +/* index permutation for sorted object emission */ +static int cmp_keys(const void *a, const void *b, const char **keys) { + size_t ia = *(const size_t *)a; + size_t ib = *(const size_t *)b; + return strcmp(keys[ia], keys[ib]); +} + +/* qsort_r is non-portable; do a simple insertion sort on the index array + * (object sizes here are tiny -- a handful of members). */ +static void sort_indices(size_t *idx, size_t n, char **keys) { + for (size_t i = 1; i < n; i++) { + size_t cur = idx[i]; + size_t j = i; + while (j > 0 && strcmp(keys[idx[j - 1]], keys[cur]) > 0) { + idx[j] = idx[j - 1]; + j--; + } + idx[j] = cur; + } + (void)cmp_keys; +} + +static void dump_value(sbuf *s, const json *j, int sorted) { + if (j == NULL) { + sbuf_puts(s, "null"); + return; + } + switch (j->type) { + case JSON_NULL: + sbuf_puts(s, "null"); + break; + case JSON_BOOL: + sbuf_puts(s, j->u.boolean ? "true" : "false"); + break; + case JSON_INT: { + char tmp[32]; + int n = snprintf(tmp, sizeof(tmp), "%lld", (long long)j->u.inum); + if (n < 0) { + s->err = 1; + } else { + sbuf_put(s, tmp, (size_t)n); + } + break; + } + case JSON_REAL: + dump_real(s, j->u.rnum); + break; + case JSON_STRING: + dump_string(s, j->u.str.bytes, j->u.str.len); + break; + case JSON_ARRAY: + sbuf_putc(s, '['); + for (size_t i = 0; i < j->u.arr.count; i++) { + if (i > 0) { + sbuf_putc(s, ','); + } + dump_value(s, j->u.arr.items[i], sorted); + } + sbuf_putc(s, ']'); + break; + case JSON_OBJECT: { + sbuf_putc(s, '{'); + size_t n = j->u.obj.count; + if (sorted && n > 1) { + size_t stackidx[16]; + size_t *idx = stackidx; + if (n > 16) { + idx = (size_t *)malloc(n * sizeof(size_t)); + if (idx == NULL) { + s->err = 1; + break; + } + } + for (size_t i = 0; i < n; i++) { + idx[i] = i; + } + sort_indices(idx, n, j->u.obj.keys); + for (size_t i = 0; i < n; i++) { + if (i > 0) { + sbuf_putc(s, ','); + } + size_t k = idx[i]; + dump_string(s, j->u.obj.keys[k], strlen(j->u.obj.keys[k])); + sbuf_putc(s, ':'); + dump_value(s, j->u.obj.vals[k], sorted); + } + if (idx != stackidx) { + free(idx); + } + } else { + for (size_t i = 0; i < n; i++) { + if (i > 0) { + sbuf_putc(s, ','); + } + dump_string(s, j->u.obj.keys[i], strlen(j->u.obj.keys[i])); + sbuf_putc(s, ':'); + dump_value(s, j->u.obj.vals[i], sorted); + } + } + sbuf_putc(s, '}'); + break; + } + default: + sbuf_puts(s, "null"); + break; + } +} + +char *json_dumps(const json *j, int sorted, size_t *out_len) { + sbuf s; + s.buf = NULL; + s.len = 0; + s.cap = 0; + s.err = 0; + dump_value(&s, j, sorted); + if (s.err) { + free(s.buf); + return NULL; + } + sbuf_reserve(&s, 1); + if (s.err) { + free(s.buf); + return NULL; + } + s.buf[s.len] = '\0'; + if (out_len != NULL) { + *out_len = s.len; + } + return s.buf; +} + +/* ------------------------------------------------------------------ */ +/* parser */ +/* ------------------------------------------------------------------ */ + +typedef struct { + const char *p; + const char *end; + const char *err; +} pstate; + +static void skip_ws(pstate *st) { + while (st->p < st->end) { + char c = *st->p; + if (c == ' ' || c == '\t' || c == '\n' || c == '\r') { + st->p++; + } else { + break; + } + } +} + +static json *parse_value(pstate *st); + +static int parse_hex4(pstate *st, unsigned *out) { + if (st->end - st->p < 4) { + return -1; + } + unsigned v = 0; + for (int i = 0; i < 4; i++) { + char c = st->p[i]; + v <<= 4; + if (c >= '0' && c <= '9') { + v |= (unsigned)(c - '0'); + } else if (c >= 'a' && c <= 'f') { + v |= (unsigned)(c - 'a' + 10); + } else if (c >= 'A' && c <= 'F') { + v |= (unsigned)(c - 'A' + 10); + } else { + return -1; + } + } + st->p += 4; + *out = v; + return 0; +} + +static void utf8_encode(sbuf *s, unsigned cp) { + if (cp < 0x80) { + sbuf_putc(s, (char)cp); + } else if (cp < 0x800) { + sbuf_putc(s, (char)(0xC0 | (cp >> 6))); + sbuf_putc(s, (char)(0x80 | (cp & 0x3F))); + } else if (cp < 0x10000) { + sbuf_putc(s, (char)(0xE0 | (cp >> 12))); + sbuf_putc(s, (char)(0x80 | ((cp >> 6) & 0x3F))); + sbuf_putc(s, (char)(0x80 | (cp & 0x3F))); + } else { + sbuf_putc(s, (char)(0xF0 | (cp >> 18))); + sbuf_putc(s, (char)(0x80 | ((cp >> 12) & 0x3F))); + sbuf_putc(s, (char)(0x80 | ((cp >> 6) & 0x3F))); + sbuf_putc(s, (char)(0x80 | (cp & 0x3F))); + } +} + +/* parse a string token (leading '"' already consumed) into a sbuf */ +static int parse_string_raw(pstate *st, sbuf *out) { + while (st->p < st->end) { + unsigned char c = (unsigned char)*st->p++; + if (c == '"') { + return 0; + } + if (c == '\\') { + if (st->p >= st->end) { + break; + } + char e = *st->p++; + switch (e) { + case '"': + sbuf_putc(out, '"'); + break; + case '\\': + sbuf_putc(out, '\\'); + break; + case '/': + sbuf_putc(out, '/'); + break; + case 'b': + sbuf_putc(out, '\b'); + break; + case 'f': + sbuf_putc(out, '\f'); + break; + case 'n': + sbuf_putc(out, '\n'); + break; + case 'r': + sbuf_putc(out, '\r'); + break; + case 't': + sbuf_putc(out, '\t'); + break; + case 'u': { + unsigned cp = 0; + if (parse_hex4(st, &cp) != 0) { + return -1; + } + if (cp >= 0xD800 && cp <= 0xDBFF) { + /* high surrogate; expect \uXXXX low surrogate */ + if (st->end - st->p >= 2 && st->p[0] == '\\' && + st->p[1] == 'u') { + st->p += 2; + unsigned lo = 0; + if (parse_hex4(st, &lo) != 0) { + return -1; + } + if (lo >= 0xDC00 && lo <= 0xDFFF) { + cp = 0x10000 + ((cp - 0xD800) << 10) + + (lo - 0xDC00); + } else { + return -1; + } + } else { + return -1; + } + } + utf8_encode(out, cp); + break; + } + default: + return -1; + } + } else if (c < 0x20) { + return -1; /* unescaped control char */ + } else { + sbuf_putc(out, (char)c); + } + if (out->err) { + return -1; + } + } + return -1; /* unterminated */ +} + +static json *parse_string(pstate *st) { + sbuf out; + out.buf = NULL; + out.len = 0; + out.cap = 0; + out.err = 0; + if (parse_string_raw(st, &out) != 0) { + free(out.buf); + st->err = "bad string"; + return NULL; + } + json *j = json_string_n(out.buf != NULL ? out.buf : "", out.len); + free(out.buf); + if (j == NULL) { + st->err = "oom"; + } + return j; +} + +static json *parse_number(pstate *st) { + const char *start = st->p; + int is_real = 0; + if (st->p < st->end && *st->p == '-') { + st->p++; + } + while (st->p < st->end) { + char c = *st->p; + if (c >= '0' && c <= '9') { + st->p++; + } else if (c == '.' || c == 'e' || c == 'E' || c == '+' || c == '-') { + is_real = 1; + st->p++; + } else { + break; + } + } + size_t n = (size_t)(st->p - start); + if (n == 0) { + st->err = "bad number"; + return NULL; + } + char tmp[64]; + if (n >= sizeof(tmp)) { + is_real = 1; /* very long; treat as real */ + } + char *buf = tmp; + char *heap = NULL; + if (n >= sizeof(tmp)) { + heap = (char *)malloc(n + 1); + if (heap == NULL) { + st->err = "oom"; + return NULL; + } + buf = heap; + } + memcpy(buf, start, n); + buf[n] = '\0'; + json *j; + if (is_real) { + j = json_real(strtod(buf, NULL)); + } else { + long long v = strtoll(buf, NULL, 10); + j = json_int((int64_t)v); + } + if (heap != NULL) { + free(heap); + } + if (j == NULL) { + st->err = "oom"; + } + return j; +} + +static int match_lit(pstate *st, const char *lit) { + size_t n = strlen(lit); + if ((size_t)(st->end - st->p) < n) { + return -1; + } + if (memcmp(st->p, lit, n) != 0) { + return -1; + } + st->p += n; + return 0; +} + +static json *parse_array(pstate *st) { + json *arr = json_array(); + if (arr == NULL) { + st->err = "oom"; + return NULL; + } + skip_ws(st); + if (st->p < st->end && *st->p == ']') { + st->p++; + return arr; + } + for (;;) { + json *v = parse_value(st); + if (v == NULL) { + json_free(arr); + return NULL; + } + if (json_array_append(arr, v) != 0) { + st->err = "oom"; + json_free(arr); + return NULL; + } + skip_ws(st); + if (st->p >= st->end) { + st->err = "unterminated array"; + json_free(arr); + return NULL; + } + char c = *st->p++; + if (c == ',') { + skip_ws(st); + continue; + } + if (c == ']') { + return arr; + } + st->err = "expected , or ]"; + json_free(arr); + return NULL; + } +} + +static json *parse_object(pstate *st) { + json *obj = json_object(); + if (obj == NULL) { + st->err = "oom"; + return NULL; + } + skip_ws(st); + if (st->p < st->end && *st->p == '}') { + st->p++; + return obj; + } + for (;;) { + skip_ws(st); + if (st->p >= st->end || *st->p != '"') { + st->err = "expected object key"; + json_free(obj); + return NULL; + } + st->p++; /* consume opening quote */ + sbuf key; + key.buf = NULL; + key.len = 0; + key.cap = 0; + key.err = 0; + if (parse_string_raw(st, &key) != 0) { + free(key.buf); + st->err = "bad key"; + json_free(obj); + return NULL; + } + skip_ws(st); + if (st->p >= st->end || *st->p != ':') { + free(key.buf); + st->err = "expected :"; + json_free(obj); + return NULL; + } + st->p++; + json *v = parse_value(st); + if (v == NULL) { + free(key.buf); + json_free(obj); + return NULL; + } + if (json_object_set(obj, key.buf != NULL ? key.buf : "", v) != 0) { + free(key.buf); + st->err = "oom"; + json_free(obj); + return NULL; + } + free(key.buf); + skip_ws(st); + if (st->p >= st->end) { + st->err = "unterminated object"; + json_free(obj); + return NULL; + } + char c = *st->p++; + if (c == ',') { + continue; + } + if (c == '}') { + return obj; + } + st->err = "expected , or }"; + json_free(obj); + return NULL; + } +} + +static json *parse_value(pstate *st) { + skip_ws(st); + if (st->p >= st->end) { + st->err = "unexpected end"; + return NULL; + } + char c = *st->p; + switch (c) { + case '{': + st->p++; + return parse_object(st); + case '[': + st->p++; + return parse_array(st); + case '"': + st->p++; + return parse_string(st); + case 't': + if (match_lit(st, "true") == 0) { + return json_bool(1); + } + st->err = "bad literal"; + return NULL; + case 'f': + if (match_lit(st, "false") == 0) { + return json_bool(0); + } + st->err = "bad literal"; + return NULL; + case 'n': + if (match_lit(st, "null") == 0) { + return json_null(); + } + st->err = "bad literal"; + return NULL; + default: + if (c == '-' || (c >= '0' && c <= '9')) { + return parse_number(st); + } + st->err = "unexpected token"; + return NULL; + } +} + +json *json_parse(const char *buf, size_t len, const char **err) { + pstate st; + st.p = buf; + st.end = buf + len; + st.err = NULL; + json *j = parse_value(&st); + if (j == NULL) { + if (err != NULL) { + *err = st.err != NULL ? st.err : "parse error"; + } + return NULL; + } + skip_ws(&st); + if (st.p != st.end) { + if (err != NULL) { + *err = "trailing data"; + } + json_free(j); + return NULL; + } + if (err != NULL) { + *err = NULL; + } + return j; +} diff --git a/packages/device-connect-agent-tools-c/tests/Makefile b/packages/device-connect-agent-tools-c/tests/Makefile new file mode 100644 index 0000000..ee11486 --- /dev/null +++ b/packages/device-connect-agent-tools-c/tests/Makefile @@ -0,0 +1,18 @@ +# SPDX-License-Identifier: Apache-2.0 +CC ?= cc +CFLAGS ?= -std=c11 -D_GNU_SOURCE -Wall -Wextra -O2 +ROOT = .. +CURL_CFLAGS ?= +CURL_LIBS ?= -lcurl +INCLUDES = -I$(ROOT)/include -I. $(CURL_CFLAGS) +SRCS = $(ROOT)/src/json.c $(ROOT)/src/http.c $(ROOT)/src/agent_tools.c +TESTS = test_agent + +all: $(TESTS) +test_agent: test_agent.c $(SRCS) + $(CC) $(CFLAGS) $(INCLUDES) -o $@ $^ -lm $(CURL_LIBS) +test: all + @fail=0; for t in $(TESTS); do echo "== $$t =="; ./$$t || fail=1; done; exit $$fail +clean: + rm -f $(TESTS) +.PHONY: all test clean diff --git a/packages/device-connect-agent-tools-c/tests/dc_test.h b/packages/device-connect-agent-tools-c/tests/dc_test.h new file mode 100644 index 0000000..7e74221 --- /dev/null +++ b/packages/device-connect-agent-tools-c/tests/dc_test.h @@ -0,0 +1,13 @@ +/* SPDX-License-Identifier: Apache-2.0 + * Copyright (c) 2024-2026, Arm Limited and Contributors. All rights reserved. + * dc_test.h -- minimal self-contained test harness. ASCII-only. */ +#ifndef DC_TEST_H +#define DC_TEST_H +#include +#include +static int g_run, g_failed; static const char *g_t; +#define CHECK(e) do{ if(!(e)){ fprintf(stderr," FAIL %s:%d: %s\n",__FILE__,__LINE__,#e); g_failed++; } }while(0) +#define CHECK_STR(a,b) do{ const char*_a=(a),*_b=(b); if(!_a||!_b||strcmp(_a,_b)){ fprintf(stderr," FAIL %s:%d: \"%s\"!=\"%s\"\n",__FILE__,__LINE__,_a?_a:"(null)",_b?_b:"(null)"); g_failed++; } }while(0) +#define RUN(fn) do{ g_t=#fn; int b=g_failed; g_run++; fn(); fprintf(stderr," %s %s\n",(g_failed==b)?"ok ":"FAIL",#fn);}while(0) +#define REPORT() do{ fprintf(stderr,"%d tests, %d failures\n",g_run,g_failed); return g_failed?1:0;}while(0) +#endif diff --git a/packages/device-connect-agent-tools-c/tests/test_agent.c b/packages/device-connect-agent-tools-c/tests/test_agent.c new file mode 100644 index 0000000..e6ca850 --- /dev/null +++ b/packages/device-connect-agent-tools-c/tests/test_agent.c @@ -0,0 +1,66 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) 2024-2026, Arm Limited and Contributors. All rights reserved. + * + * test_agent.c -- unit tests for agent-tools JSON handling and env resolution. + * (HTTP itself is exercised by the live e2e, which needs a portal + token.) + * + * ASCII-only source. + */ + +#include "dc/agent_tools.h" +#include "dc/json.h" +#include "dc_test.h" + +#include + +static void test_resolve_env(void) { + setenv("DEVICE_CONNECT_PORTAL_URL", "https://portal.example", 1); + setenv("DEVICE_CONNECT_PORTAL_TOKEN", "dcp_x_y", 1); + dc_agent a; + memset(&a, 0, sizeof(a)); + CHECK(dc_agent_resolve(&a) == 0); + CHECK_STR(a.portal_url, "https://portal.example"); + CHECK_STR(a.token, "dcp_x_y"); + /* explicit fields win over env */ + dc_agent b = {"https://other", "dcp_a_b"}; + CHECK(dc_agent_resolve(&b) == 0); + CHECK_STR(b.portal_url, "https://other"); +} + +static void test_parse_fleet_response(void) { + /* shape the portal returns for /fleet */ + const char *body = + "{\"tenant\":\"alpha\",\"devices_registered\":3,\"devices_online\":3," + "\"by_device_type\":{\"temp_sensor\":3}}"; + const char *err = NULL; + json *j = json_parse(body, strlen(body), &err); + CHECK(j != NULL); + CHECK(json_integer(json_object_get(j, "devices_online")) == 3); + json *bt = json_object_get(j, "by_device_type"); + CHECK(json_integer(json_object_get(bt, "temp_sensor")) == 3); + json_free(j); +} + +static void test_parse_invoke_response(void) { + /* portal invoke envelope embedding the device JSON-RPC response */ + const char *body = + "{\"success\":true,\"result\":{\"device_id\":\"alpha-temp-001\"," + "\"function\":\"get_reading\",\"elapsed_ms\":42," + "\"response\":{\"jsonrpc\":\"2.0\",\"id\":\"1\"," + "\"result\":{\"temp_c\":21.5}}}}"; + const char *err = NULL; + json *j = json_parse(body, strlen(body), &err); + CHECK(j != NULL); + json *resp = json_object_get(json_object_get(j, "result"), "response"); + json *res = json_object_get(resp, "result"); + CHECK(json_number(json_object_get(res, "temp_c")) == 21.5); + json_free(j); +} + +int main(void) { + RUN(test_resolve_env); + RUN(test_parse_fleet_response); + RUN(test_parse_invoke_response); + REPORT(); +} diff --git a/packages/device-connect-edge-c/.gitignore b/packages/device-connect-edge-c/.gitignore new file mode 100644 index 0000000..6cb2746 --- /dev/null +++ b/packages/device-connect-edge-c/.gitignore @@ -0,0 +1,8 @@ +# C build artifacts +*.o +*.a +# example/test binaries (extensionless, built in-tree) +/temp_sensor +/dc_agent +tests/test_edge +tests/test_agent diff --git a/packages/device-connect-edge-c/Makefile b/packages/device-connect-edge-c/Makefile new file mode 100644 index 0000000..406254e --- /dev/null +++ b/packages/device-connect-edge-c/Makefile @@ -0,0 +1,43 @@ +# SPDX-License-Identifier: Apache-2.0 +# Device Connect C edge SDK. +# +# Builds libdc_edge.a (the device-side library) and the temp_sensor example. +# Requires the NATS C client (cnats); point NATS_CFLAGS/NATS_LIBS at it. +# +# make # build libdc_edge.a + temp_sensor +# make test # build and run unit tests +# make NATS_CFLAGS=-I/opt/homebrew/include NATS_LIBS="-L/opt/homebrew/lib -lnats" +# make clean + +CC ?= cc +CFLAGS ?= -std=c11 -D_GNU_SOURCE -Wall -Wextra -O2 +NATS_CFLAGS ?= +NATS_LIBS ?= -lnats +DEFS = -DDC_WITH_NATS +INCLUDES = -Iinclude $(NATS_CFLAGS) +LDLIBS = $(NATS_LIBS) -lm + +LIB = libdc_edge.a +SRCS = src/json.c src/hash.c src/jsonrpc.c src/security.c \ + src/transport_nats.c src/driver.c src/runtime.c +OBJS = $(SRCS:.c=.o) + +all: $(LIB) temp_sensor + +$(LIB): $(OBJS) + ar rcs $@ $^ + +src/%.o: src/%.c + $(CC) $(CFLAGS) $(DEFS) $(INCLUDES) -c $< -o $@ + +temp_sensor: examples/temp_sensor.c $(LIB) + $(CC) $(CFLAGS) $(DEFS) $(INCLUDES) -o $@ examples/temp_sensor.c $(LIB) $(LDLIBS) + +test: $(LIB) + $(MAKE) -C tests test + +clean: + rm -f $(OBJS) $(LIB) temp_sensor + $(MAKE) -C tests clean 2>/dev/null || true + +.PHONY: all test clean diff --git a/packages/device-connect-edge-c/README.md b/packages/device-connect-edge-c/README.md new file mode 100644 index 0000000..90e11fe --- /dev/null +++ b/packages/device-connect-edge-c/README.md @@ -0,0 +1,57 @@ + +# device-connect-edge-c + +The **Device Connect edge SDK in C** — the device side. A C analogue of +`device-connect-edge`: write a driver (RPC functions + events + identity), run +it under the runtime, and it connects to NATS, registers with the portal, +serves commands, and keeps its lease alive with a heartbeat. + +This is the C counterpart of the Python edge SDK, built in the same spirit as +the MHP C SDK's wire node (it reuses that node's proven JSON / JSON-RPC / +NATS-transport / security modules). + +## What it does + +- **Driver** (`dc/driver.h`): register `@rpc`-style functions by name, advertise + events, set identity/status. DC RPC semantics: a command on + `device-connect.{tenant}.{id}.cmd` is JSON-RPC where `method` *is* the + function name and `params` are the arguments; the reply carries the raw + return value as `result`. +- **Runtime** (`dc/runtime.h`): connect (NATS, via cnats), serve the cmd + subject, `registerDevice` to `device-connect.{tenant}.registry`, publish a + heartbeat every `device_ttl/3` to keep the lease (so the portal shows the + device **online**), answer `requestRegistration` pulls, re-register on + reconnect, plus `invoke_remote` (D2D) and `emit` (events). +- **Credentials**: consumes the portal's native `*.creds.json` directly + (auto-converts to an nsc-chained creds for cnats) and auto-detects + `device_id` / `tenant` from it. An nsc `*.creds` is also accepted. + +## Build + +Requires the NATS C client (cnats). + +``` +make NATS_CFLAGS="-I/opt/homebrew/include" NATS_LIBS="-L/opt/homebrew/lib -lnats" +make test +``` + +Produces `libdc_edge.a` + the `temp_sensor` example. + +## Run + +``` +NATS_CREDENTIALS_FILE=./alpha-temp-001.creds.json \ + ./temp_sensor --server nats://portal.deviceconnect.dev:4222 --device-ttl 30 +``` + +Verified end-to-end against a live DC portal (tenant `alpha`): 3 C devices +provisioned, registered, **online (sustained by heartbeat)**, discovered and +invoked via the C agent-tools (`get_reading`/`set_target`), with `-32601` +/`-32602` error semantics propagating. + +## Scope / not yet + +- Transport: NATS (cnats). MQTT/Zenoh slot into the `dc/transport.h` vtable. +- D2D presence collector, `@periodic`, and `@on` subscriptions are not yet + wired (the runtime serves portal-mode register+heartbeat+cmd+invoke_remote). +- Security: TLS + JWT/NKey via cnats; mandate verification is out of scope. diff --git a/packages/device-connect-edge-c/examples/temp_sensor.c b/packages/device-connect-edge-c/examples/temp_sensor.c new file mode 100644 index 0000000..1a7e067 --- /dev/null +++ b/packages/device-connect-edge-c/examples/temp_sensor.c @@ -0,0 +1,111 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) 2024-2026, Arm Limited and Contributors. All rights reserved. + * + * temp_sensor.c -- a minimal Device Connect device in C, the analogue of the + * Python edge SDK's number_generator/dht22 examples. Exposes get_reading and + * set_target, advertises a reading_changed event, and runs under the runtime. + * + * Usage: + * NATS_CREDENTIALS_FILE=./alpha-temp-001.creds.json \ + * temp_sensor --server nats://portal:4222 [--device-id alpha-temp-001] \ + * [--tenant alpha] [--device-ttl 30] + * + * device_id and tenant are auto-detected from a *.creds.json when omitted. + * + * ASCII-only source. + */ + +#include "dc/driver.h" +#include "dc/jsonrpc.h" /* DC_ERR_* codes */ +#include "dc/runtime.h" + +#include +#include +#include + +typedef struct { + double target_c; + double measured_c; +} sensor_state; + +static void rpc_get_reading(void *user, const json *params, dc_rpc_result *out) { + (void)params; + sensor_state *s = (sensor_state *)user; + s->measured_c += (s->target_c - s->measured_c) * 0.25; + json *r = json_object(); + json_object_set(r, "temp_c", json_real(s->measured_c)); + out->result = r; +} + +static void rpc_set_target(void *user, const json *params, dc_rpc_result *out) { + sensor_state *s = (sensor_state *)user; + json *c = json_object_get((json *)params, "celsius"); + if (c == NULL || + (json_typeof(c) != JSON_INT && json_typeof(c) != JSON_REAL)) { + out->err_code = DC_ERR_INVALID_PARAMS; + snprintf(out->err_msg, sizeof(out->err_msg), "celsius must be a number"); + return; + } + s->target_c = json_number(c); + json *r = json_object(); + json_object_set(r, "ok", json_bool(1)); + out->result = r; +} + +int main(int argc, char **argv) { + dc_runtime_config cfg; + memset(&cfg, 0, sizeof(cfg)); + cfg.device_ttl = 30; + for (int i = 1; i < argc; i++) { + if (!strcmp(argv[i], "--server") && i + 1 < argc) { + cfg.server = argv[++i]; + } else if (!strcmp(argv[i], "--device-id") && i + 1 < argc) { + cfg.device_id = argv[++i]; + } else if (!strcmp(argv[i], "--tenant") && i + 1 < argc) { + cfg.tenant = argv[++i]; + } else if (!strcmp(argv[i], "--device-ttl") && i + 1 < argc) { + cfg.device_ttl = atoi(argv[++i]); + } else { + fprintf(stderr, "unknown arg: %s\n", argv[i]); + return 2; + } + } + + sensor_state state = {0.0, 20.0}; + + dc_driver *d = dc_driver_new("temp_sensor"); + dc_driver_set_identity(d, "ACME", "T1", "0.1.0", + "Demo temperature sensor (C edge SDK)"); + dc_driver_set_location(d, "lab/bench-3"); + + json *sch = json_object(); + json *props = json_object(); + json *cel = json_object(); + json_object_set(cel, "type", json_string("number")); + json_object_set(props, "celsius", cel); + json *req = json_array(); + json_array_append(req, json_string("celsius")); + json_object_set(sch, "type", json_string("object")); + json_object_set(sch, "properties", props); + json_object_set(sch, "required", req); + + dc_driver_add_function(d, "get_reading", "Read current temperature.", NULL, + rpc_get_reading, &state); + dc_driver_add_function(d, "set_target", "Set target temperature.", sch, + rpc_set_target, &state); + dc_driver_add_event(d, "reading_changed", "Emitted when the reading moves."); + + dc_runtime *rt = dc_runtime_new(d, &cfg); + if (rt == NULL || dc_runtime_start(rt) != 0) { + fprintf(stderr, "[temp_sensor] failed to start\n"); + dc_runtime_free(rt); + dc_driver_free(d); + return 1; + } + fprintf(stderr, "[temp_sensor] serving as %s\n", dc_runtime_device_id(rt)); + dc_runtime_run(rt); /* blocks until SIGINT/SIGTERM */ + dc_runtime_free(rt); + dc_driver_free(d); + return 0; +} diff --git a/packages/device-connect-edge-c/include/dc/driver.h b/packages/device-connect-edge-c/include/dc/driver.h new file mode 100644 index 0000000..d2255ab --- /dev/null +++ b/packages/device-connect-edge-c/include/dc/driver.h @@ -0,0 +1,96 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) 2024-2026, Arm Limited and Contributors. All rights reserved. + * + * dc/driver.h -- the Device Connect C edge SDK driver surface. + * + * A driver is the device's logic: a set of named RPC functions (the @rpc + * equivalent), advertised events (@emit), and identity/status metadata. The + * runtime (dc/runtime.h) serves the driver over NATS, registers it with the + * portal registry, and dispatches inbound commands to its functions. + * + * Device Connect RPC shape: a command on device-connect.{tenant}.{id}.cmd is + * JSON-RPC where `method` IS the function name and `params` are the arguments + * directly; the reply carries the function's raw return value as `result`. + * + * ASCII-only source. + */ + +#ifndef DC_DRIVER_H +#define DC_DRIVER_H + +#include "dc/json.h" + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* Result a function fills in. On success set `result` (a heap json, ownership + * transferred to the runtime) or leave it NULL for a {}-returning call. On + * failure set err_code (a JSON-RPC code) and optionally err_msg. */ +typedef struct { + json *result; + int err_code; + char err_msg[192]; +} dc_rpc_result; + +/* An RPC handler. `params` is the command params object (the arguments), or + * NULL. */ +typedef void (*dc_rpc_fn)(void *user, const json *params, dc_rpc_result *out); + +typedef struct dc_driver dc_driver; + +dc_driver *dc_driver_new(const char *device_type); +void dc_driver_free(dc_driver *d); + +/* + * Register an RPC function. `name` is the bare function name (the JSON-RPC + * method clients call). `params_schema` is an optional JSON Schema object + * (ownership transferred; NULL for no args). Returns 0 / -1. + */ +int dc_driver_add_function(dc_driver *d, const char *name, + const char *description, json *params_schema, + dc_rpc_fn fn, void *user); + +/* Advertise an event the device emits (appears in capabilities.events). */ +int dc_driver_add_event(dc_driver *d, const char *name, + const char *description); + +/* Identity / status metadata (all optional; copied). */ +void dc_driver_set_identity(dc_driver *d, const char *manufacturer, + const char *model, const char *firmware_version, + const char *description); +void dc_driver_set_location(dc_driver *d, const char *location); +void dc_driver_set_availability(dc_driver *d, const char *availability); + +/* ---- used by the runtime ---- */ + +const char *dc_driver_device_type(const dc_driver *d); + +/* Build the DC capabilities object {description, functions[], events[]} + * (owned). */ +json *dc_driver_capabilities(const dc_driver *d); + +/* Build the DC identity object {device_type, manufacturer, model, ...} + * (owned). */ +json *dc_driver_identity(const dc_driver *d); + +/* Build the DC status object {availability, location, ...} without ts + * (owned). */ +json *dc_driver_status(const dc_driver *d); + +/* + * Dispatch a command by function name. On success returns 0 and sets + * *result_out (owned json, or NULL for void). On failure returns a JSON-RPC + * error code and fills errmsg. + */ +int dc_driver_call(const dc_driver *d, const char *function, const json *params, + json **result_out, char *errmsg, size_t errcap); + +#ifdef __cplusplus +} +#endif + +#endif /* DC_DRIVER_H */ diff --git a/packages/device-connect-edge-c/include/dc/hash.h b/packages/device-connect-edge-c/include/dc/hash.h new file mode 100644 index 0000000..f392a9e --- /dev/null +++ b/packages/device-connect-edge-c/include/dc/hash.h @@ -0,0 +1,46 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) 2024-2026, Arm Limited and Contributors. All rights reserved. + * + * dc/hash.h -- a small, dependency-free SHA-256 for the manifest hash. + * + * wire_contract.md sec 7.6 requires the manifest_hash to be a deterministic, + * stable, cryptographic-strength change-detection token; it explicitly does + * NOT require cross-implementation byte-identical hashing (only the + * originating announcer's hashes are ever compared). We therefore use plain + * SHA-256 over the canonical JSON rather than the Python SDK's BLAKE2b -- no + * external crypto dependency, full collision resistance. + * + * ASCII-only source (per CLAUDE.md). + */ + +#ifndef DC_HASH_H +#define DC_HASH_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define DC_SHA256_DIGEST 32 +#define DC_SHA256_HEXLEN 64 /* not counting the NUL */ + +void dc_sha256(const void *data, size_t len, + uint8_t digest[DC_SHA256_DIGEST]); + +/* lowercase-hex encode n bytes into out (out must hold 2*n + 1 bytes). */ +void dc_hex_encode(const uint8_t *bytes, size_t n, char *out); + +/* + * Convenience: SHA-256 of data, lowercase-hex, freshly malloc'd + * (DC_SHA256_HEXLEN + 1 bytes). Caller frees. NULL on OOM. + */ +char *dc_sha256_hex(const void *data, size_t len); + +#ifdef __cplusplus +} +#endif + +#endif /* DC_HASH_H */ diff --git a/packages/device-connect-edge-c/include/dc/json.h b/packages/device-connect-edge-c/include/dc/json.h new file mode 100644 index 0000000..e26127d --- /dev/null +++ b/packages/device-connect-edge-c/include/dc/json.h @@ -0,0 +1,112 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) 2024-2026, Arm Limited and Contributors. All rights reserved. + * + * dc/json.h -- a tiny, dependency-free JSON reader/writer for the MHP + * reference wire node. + * + * Design goals (per the MHP wire contract, specification/draft/wire_contract.md): + * - Serialize with optional lexicographically sorted object keys, so the + * manifest hash (section 7.6) is deterministic. + * - Never emit NaN / Infinity tokens: non-finite reals serialize as null + * (section 4.4, conformance clause C20). + * - Tolerate unknown members on parse so higher layers can ignore extra + * fields (clause C19). + * + * Numbers are stored as either a 64-bit integer or a double, chosen at parse + * time by whether the token carried a '.', 'e' or 'E'. Constructed numbers + * pick the form the caller asked for. + * + * Ownership: container mutators (json_array_append, json_object_set) take + * ownership of the value passed in; json_free frees a value and everything it + * transitively owns. Strings returned by accessors are owned by the node and + * are valid until it is freed. + * + * ASCII-only source (per CLAUDE.md). + */ + +#ifndef DC_JSON_H +#define DC_JSON_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef enum { + JSON_NULL = 0, + JSON_BOOL, + JSON_INT, + JSON_REAL, + JSON_STRING, + JSON_ARRAY, + JSON_OBJECT +} json_type; + +typedef struct json json; + +/* ---- constructors (all return a heap node, or NULL on OOM) ---- */ +json *json_null(void); +json *json_bool(int b); +json *json_int(int64_t v); +json *json_real(double v); +json *json_string(const char *s); /* NUL-terminated, copied */ +json *json_string_n(const char *s, size_t n); /* n bytes, copied */ +json *json_array(void); +json *json_object(void); + +/* ---- container mutators (take ownership of val; key is copied) ---- */ +int json_array_append(json *arr, json *val); /* 0 ok, -1 fail */ +int json_object_set(json *obj, const char *key, json *val); /* replaces dup key */ + +/* ---- type tests / accessors ---- */ +json_type json_typeof(const json *j); +int json_is_null(const json *j); + +/* number value as double regardless of int/real storage; 0 if not a number */ +double json_number(const json *j); +/* integer value; for a real, the truncated value; 0 if not a number */ +int64_t json_integer(const json *j); +int json_truthy(const json *j); /* JSON_BOOL value, or 0 */ + +/* string bytes (NUL-terminated) and length; NULL/0 if not a string */ +const char *json_str(const json *j); +size_t json_strlen(const json *j); + +size_t json_array_size(const json *j); +json *json_array_get(const json *j, size_t i); /* borrowed */ + +json *json_object_get(const json *j, const char *key); /* borrowed, or NULL */ +size_t json_object_size(const json *j); +/* iterate object members by index; key/val borrowed */ +const char *json_object_key_at(const json *j, size_t i); +json *json_object_val_at(const json *j, size_t i); + +/* Deep-copy a value (and everything it owns). NULL on OOM or NULL input. */ +json *json_clone(const json *j); + +/* ---- parse / serialize / free ---- */ +/* + * Parse exactly one JSON value from buf[0..len). Trailing whitespace is + * allowed; trailing non-whitespace is an error. Returns NULL on any syntax + * error. If err is non-NULL it receives a short static description (do not + * free). + */ +json *json_parse(const char *buf, size_t len, const char **err); + +/* + * Serialize to a freshly malloc'd NUL-terminated ASCII string (caller frees). + * If sorted is non-zero, object members are emitted in ascending key order. + * Returns NULL on OOM. *out_len, when non-NULL, receives strlen of the result. + */ +char *json_dumps(const json *j, int sorted, size_t *out_len); + +void json_free(json *j); + +#ifdef __cplusplus +} +#endif + +#endif /* DC_JSON_H */ diff --git a/packages/device-connect-edge-c/include/dc/jsonrpc.h b/packages/device-connect-edge-c/include/dc/jsonrpc.h new file mode 100644 index 0000000..b07367e --- /dev/null +++ b/packages/device-connect-edge-c/include/dc/jsonrpc.h @@ -0,0 +1,71 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) 2024-2026, Arm Limited and Contributors. All rights reserved. + * + * dc/jsonrpc.h -- JSON-RPC 2.0 envelopes (wire_contract.md sec 4.1-4.2). + * + * Requests, responses, errors and notifications, plus the closed set of error + * codes a wire node uses. The envelope carries no binary; the binary trailer + * is handled separately by dc/frame.h. + * + * ASCII-only source (per CLAUDE.md). + */ + +#ifndef DC_JSONRPC_H +#define DC_JSONRPC_H + +#include "dc/json.h" + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* JSON-RPC / MHP error codes (wire_contract.md sec 4.2). */ +#define DC_ERR_PARSE (-32700) /* invalid JSON */ +#define DC_ERR_INVALID_REQ (-32600) /* not an object; jsonrpc != "2.0" */ +#define DC_ERR_METHOD_NF (-32601) /* unknown method / unknown invoke name */ +#define DC_ERR_INVALID_PARAMS (-32602)/* bad params / bad kind tag */ +#define DC_ERR_INTERNAL (-32603) /* uncaught error during dispatch */ +#define DC_ERR_SERVER (-32000) /* offset-routed verb, safety refusal */ + +/* A parsed inbound JSON-RPC request. Borrowed views into the owning json. */ +typedef struct { + const char *id; /* request id string, or NULL for a notification */ + const char *method; /* method name (never NULL on success) */ + json *params; /* borrowed; NULL if absent */ +} dc_rpc_request; + +/* + * Validate and view a request out of an already-parsed JSON value. + * Returns 0 on success; on failure returns the JSON-RPC error code to reply + * with (DC_ERR_INVALID_REQ when jsonrpc != "2.0" or shape is wrong -- + * clause C5). The request views alias into `root`, which must outlive use. + */ +int dc_rpc_parse_request(json *root, dc_rpc_request *out); + +/* + * Build envelopes as freshly malloc'd NUL-terminated JSON byte strings + * (caller frees). *out_len, when non-NULL, receives the length. NULL on OOM. + * + * For responses/errors, id may be NULL (emits JSON null id, as permitted for + * errors whose request id could not be determined). + * + * build_response takes ownership of `result` (it is embedded then freed). + */ +char *dc_rpc_build_response(const char *id, json *result, size_t *out_len); +char *dc_rpc_build_error(const char *id, int code, const char *message, + size_t *out_len); +/* Notification: no id. Takes ownership of `params`. */ +char *dc_rpc_build_notification(const char *method, json *params, + size_t *out_len); +/* Request: takes ownership of `params`. id is copied. */ +char *dc_rpc_build_request(const char *id, const char *method, + json *params, size_t *out_len); + +#ifdef __cplusplus +} +#endif + +#endif /* DC_JSONRPC_H */ diff --git a/packages/device-connect-edge-c/include/dc/runtime.h b/packages/device-connect-edge-c/include/dc/runtime.h new file mode 100644 index 0000000..899402e --- /dev/null +++ b/packages/device-connect-edge-c/include/dc/runtime.h @@ -0,0 +1,78 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) 2024-2026, Arm Limited and Contributors. All rights reserved. + * + * dc/runtime.h -- the Device Connect C edge SDK runtime. + * + * Connects a driver to a NATS broker, registers it with the portal registry, + * serves inbound commands on device-connect.{tenant}.{id}.cmd, keeps the lease + * alive with a heartbeat, answers requestRegistration pulls, and exposes + * device-to-device invoke_remote and event emission. + * + * Credentials: accepts the portal's native *.creds.json (nats.jwt + + * nats.nkey_seed, with device_id/tenant) or an nsc-chained *.creds; device_id + * and tenant are auto-detected from a *.creds.json when not supplied. + * + * ASCII-only source. + */ + +#ifndef DC_RUNTIME_H +#define DC_RUNTIME_H + +#include "dc/driver.h" +#include "dc/json.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#define DC_DEFAULT_DEVICE_TTL 30 + +typedef struct { + const char *server; /* nats URL; NULL -> $NATS_URL */ + const char *creds_file; /* .creds.json or chained .creds; NULL -> $NATS_CREDENTIALS_FILE */ + const char *device_id; /* NULL -> from creds.json */ + const char *tenant; /* NULL -> from creds.json, else "default" */ + int device_ttl; /* <=0 -> DC_DEFAULT_DEVICE_TTL */ +} dc_runtime_config; + +typedef struct dc_runtime dc_runtime; + +dc_runtime *dc_runtime_new(dc_driver *driver, const dc_runtime_config *cfg); +void dc_runtime_free(dc_runtime *r); + +/* Connect, subscribe the cmd subject, register with the registry, and publish + * the first heartbeat. Returns 0 on success. */ +int dc_runtime_start(dc_runtime *r); + +/* Drive periodic work (heartbeat + reconnect re-register). Call frequently. */ +void dc_runtime_tick(dc_runtime *r, double now); + +/* Blocking run loop until SIGINT/SIGTERM; calls tick internally. */ +void dc_runtime_run(dc_runtime *r); + +/* Graceful shutdown: announce departure and disconnect. */ +void dc_runtime_stop(dc_runtime *r); + +/* + * Device-to-device RPC: call `function` on `device_id` in the same tenant. + * Takes ownership of `params`. Returns the parsed JSON-RPC reply object + * (caller json_free) or NULL on transport failure. Check for an "error" key + * before reading "result". + */ +json *dc_runtime_invoke_remote(dc_runtime *r, const char *device_id, + const char *function, json *params, + int timeout_ms); + +/* Emit an event: publish to device-connect.{tenant}.{id}.event.{name}. + * Takes ownership of `params`. */ +void dc_runtime_emit(dc_runtime *r, const char *event, json *params); + +const char *dc_runtime_device_id(const dc_runtime *r); +const char *dc_runtime_registration_id(const dc_runtime *r); + +#ifdef __cplusplus +} +#endif + +#endif /* DC_RUNTIME_H */ diff --git a/packages/device-connect-edge-c/include/dc/security.h b/packages/device-connect-edge-c/include/dc/security.h new file mode 100644 index 0000000..f301ac8 --- /dev/null +++ b/packages/device-connect-edge-c/include/dc/security.h @@ -0,0 +1,57 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) 2024-2026, Arm Limited and Contributors. All rights reserved. + * + * dc/security.h -- transport security configuration (TLS + JWT/NKey). + * + * The wire contract is auth-agnostic (wire_contract.md sec 2.1); PR #153 adds + * security as an additive, off-by-default layer. v1 supports transport-level + * AuthN: the NATS C client connects with TLS and JWT/NKey credentials. This + * struct collects that config from the SAME environment the Python SDK uses + * (transport_layer/auth.py: NATS_JWT + NATS_NKEY_SEED, or + * NATS_CREDENTIALS_FILE) plus explicit fields; transport_nats applies it. + * + * AuthZ / verifiable-mandate ENFORCE (Ed25519 + JCS + trust bundle) is a + * later phase; the pre-dispatch hook seam for it already exists in dispatch.h. + * + * ASCII-only source (per CLAUDE.md). + */ + +#ifndef DC_SECURITY_H +#define DC_SECURITY_H + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct { + char *creds_file; /* nsc-generated NATS .creds (JWT + seed) */ + char *jwt; /* inline user JWT (alternative to creds_file) */ + char *nkey_seed; /* inline NKey seed (paired with jwt) */ + char *tls_ca; /* CA certificate path (PEM) */ + char *tls_cert; /* client certificate path (PEM) */ + char *tls_key; /* client private key path (PEM) */ + int tls_enable; /* 1 to require TLS */ + int verify_hostname; /* default 1 (matches SDK build_tls_context) */ +} dc_security; + +/* Initialise to safe defaults (TLS off, verify_hostname on, all paths NULL). */ +void dc_security_init(dc_security *s); + +/* + * Populate from environment, mirroring the SDK: + * NATS_CREDENTIALS_FILE -> creds_file + * NATS_JWT + NATS_NKEY_SEED -> jwt + nkey_seed + * MHP_TLS_CA / MHP_TLS_CERT / MHP_TLS_KEY -> tls_* (and tls_enable=1 if any) + * MHP_TLS_VERIFY_HOSTNAME=0 -> verify_hostname=0 + * Existing non-NULL fields are not overwritten. + */ +void dc_security_load_env(dc_security *s); + +void dc_security_free(dc_security *s); + +#ifdef __cplusplus +} +#endif + +#endif /* DC_SECURITY_H */ diff --git a/packages/device-connect-edge-c/include/dc/transport.h b/packages/device-connect-edge-c/include/dc/transport.h new file mode 100644 index 0000000..1b8804a --- /dev/null +++ b/packages/device-connect-edge-c/include/dc/transport.h @@ -0,0 +1,78 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) 2024-2026, Arm Limited and Contributors. All rights reserved. + * + * dc/transport.h -- the transport abstraction the node runs on. + * + * The wire contract is identical across transports (wire_contract.md sec 2.1); + * only the broker connection and subject-syntax translation differ. The node + * talks to this vtable; concrete backends (NATS in v1; MQTT/Zenoh/ZMQ later) + * implement it. An in-memory loopback backend (transport_mem) implements it for + * tests and for builds without a broker client. + * + * Subjects passed across this interface are in CANONICAL dotted form; the + * backend translates to native syntax at its boundary (sec 2.3). + * + * ASCII-only source (per CLAUDE.md). + */ + +#ifndef DC_TRANSPORT_H +#define DC_TRANSPORT_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* + * Delivery callback for a subscription. `reply` is the reply subject for a + * request-style message (NULL for fire-and-forget). Data aliases backend + * memory valid only for the duration of the call. + */ +typedef void (*dc_msg_cb)(void *user, const char *subject, + const uint8_t *data, size_t len, + const char *reply); + +typedef struct dc_transport dc_transport; + +struct dc_transport { + void *impl; + + /* fire-and-forget publish to a canonical subject. 0 ok, -1 fail. */ + int (*publish)(void *impl, const char *subject, const uint8_t *data, + size_t len); + + /* subscribe a callback to a canonical subject/pattern. 0 ok, -1 fail. */ + int (*subscribe)(void *impl, const char *pattern, dc_msg_cb cb, + void *user); + + /* + * request/reply: publish to `subject` and block for one reply up to + * timeout_ms. On success returns 0 and a freshly malloc'd reply buffer in + * *out (caller frees) with length *out_len. -1 on timeout/error. + */ + int (*request)(void *impl, const char *subject, const uint8_t *data, + size_t len, uint8_t **out, size_t *out_len, int timeout_ms); + + /* publish a reply to a request's reply subject. 0 ok, -1 fail. */ + int (*respond)(void *impl, const char *reply, const uint8_t *data, + size_t len); + + /* 1 if currently connected to the broker, else 0. */ + int (*connected)(void *impl); + + /* close + free the backend. */ + void (*close)(void *impl); +}; + +/* Wall-clock seconds (Unix epoch, sub-second). Defined in runtime.c; + * used by the NATS backend's reconnect bookkeeping. */ +double dc_now(void); + +#ifdef __cplusplus +} +#endif + +#endif /* DC_TRANSPORT_H */ diff --git a/packages/device-connect-edge-c/include/dc/transport_nats.h b/packages/device-connect-edge-c/include/dc/transport_nats.h new file mode 100644 index 0000000..5bde02e --- /dev/null +++ b/packages/device-connect-edge-c/include/dc/transport_nats.h @@ -0,0 +1,59 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) 2024-2026, Arm Limited and Contributors. All rights reserved. + * + * dc/transport_nats.h -- NATS backend for the wire transport. + * + * Implemented against the NATS C client (cnats). The whole backend is compiled + * only when DC_WITH_NATS is defined and cnats is available; otherwise the + * constructor is a stub that returns -1 with a diagnostic, so the library and + * the example still link (and fall back to the in-memory transport). + * + * NATS uses dotted subjects natively, so canonical subjects pass through + * untranslated (wire_contract.md sec 2.3). TLS + JWT/NKey come from the + * dc_security config (sec 2.1, PR #153 transport AuthN). The cnats + * client owns intra-blip reconnect (unlimited, wait + jitter); the outer + * exponential-backoff connect supervisor lives in the run loop (therm01). + * + * ASCII-only source (per CLAUDE.md). + */ + +#ifndef DC_TRANSPORT_NATS_H +#define DC_TRANSPORT_NATS_H + +#include "dc/security.h" +#include "dc/transport.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct { + const char *url; /* e.g. "nats://127.0.0.1:4222" */ + dc_security *security; /* borrowed; may be NULL */ + int reconnect_wait_ms; /* base reconnect wait; <=0 -> 250 */ + int reconnect_jitter_ms; /* added jitter; <=0 -> 250 */ +} dc_nats_config; + +/* + * Create + connect a NATS-backed transport. Returns 0 on success (fills `t`), + * -1 on failure (including "built without NATS"). On failure `t` is untouched. + */ +int dc_transport_nats_create(dc_transport *t, + const dc_nats_config *cfg); + +/* + * Poll for a reconnect that happened since the last poll. Returns 1 and writes + * the prior outage duration (seconds) to *outage_s when a reconnect occurred, + * else 0. The run loop calls this each tick and forwards to + * dc_node_on_reconnect from the main thread (so node state is touched by + * one thread only). Returns 0 for non-NATS transports. + */ +int dc_transport_nats_poll_reconnect(dc_transport *t, + double *outage_s); + +#ifdef __cplusplus +} +#endif + +#endif /* DC_TRANSPORT_NATS_H */ diff --git a/packages/device-connect-edge-c/src/driver.c b/packages/device-connect-edge-c/src/driver.c new file mode 100644 index 0000000..0af09da --- /dev/null +++ b/packages/device-connect-edge-c/src/driver.c @@ -0,0 +1,316 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) 2024-2026, Arm Limited and Contributors. All rights reserved. + * + * dc/driver.c -- Device Connect C edge SDK driver implementation. + * + * ASCII-only source. + */ + +#include "dc/driver.h" +#include "dc/jsonrpc.h" + +#include +#include +#include + +typedef struct { + char *name; + char *description; + json *params_schema; /* owned or NULL */ + dc_rpc_fn fn; + void *user; +} fn_entry; + +typedef struct { + char *name; + char *description; +} ev_entry; + +struct dc_driver { + char *device_type; + char *manufacturer; + char *model; + char *firmware_version; + char *description; + char *location; + char *availability; /* default "available" */ + fn_entry *fns; + size_t nfns, cfns; + ev_entry *evs; + size_t nevs, cevs; +}; + +static char *dupz(const char *s) { return s != NULL ? strdup(s) : NULL; } + +dc_driver *dc_driver_new(const char *device_type) { + dc_driver *d = (dc_driver *)calloc(1, sizeof(*d)); + if (d == NULL) { + return NULL; + } + d->device_type = dupz(device_type != NULL ? device_type : "device"); + d->availability = strdup("available"); + if (d->device_type == NULL || d->availability == NULL) { + dc_driver_free(d); + return NULL; + } + return d; +} + +void dc_driver_free(dc_driver *d) { + if (d == NULL) { + return; + } + for (size_t i = 0; i < d->nfns; i++) { + free(d->fns[i].name); + free(d->fns[i].description); + json_free(d->fns[i].params_schema); + } + free(d->fns); + for (size_t i = 0; i < d->nevs; i++) { + free(d->evs[i].name); + free(d->evs[i].description); + } + free(d->evs); + free(d->device_type); + free(d->manufacturer); + free(d->model); + free(d->firmware_version); + free(d->description); + free(d->location); + free(d->availability); + free(d); +} + +int dc_driver_add_function(dc_driver *d, const char *name, + const char *description, json *params_schema, + dc_rpc_fn fn, void *user) { + if (d == NULL || name == NULL || fn == NULL) { + json_free(params_schema); + return -1; + } + if (d->nfns == d->cfns) { + size_t nc = d->cfns ? d->cfns * 2 : 8; + fn_entry *ne = (fn_entry *)realloc(d->fns, nc * sizeof(fn_entry)); + if (ne == NULL) { + json_free(params_schema); + return -1; + } + d->fns = ne; + d->cfns = nc; + } + fn_entry *e = &d->fns[d->nfns]; + e->name = strdup(name); + e->description = dupz(description); + e->params_schema = params_schema; + e->fn = fn; + e->user = user; + if (e->name == NULL) { + free(e->description); + json_free(params_schema); + return -1; + } + d->nfns++; + return 0; +} + +int dc_driver_add_event(dc_driver *d, const char *name, + const char *description) { + if (d == NULL || name == NULL) { + return -1; + } + if (d->nevs == d->cevs) { + size_t nc = d->cevs ? d->cevs * 2 : 4; + ev_entry *ne = (ev_entry *)realloc(d->evs, nc * sizeof(ev_entry)); + if (ne == NULL) { + return -1; + } + d->evs = ne; + d->cevs = nc; + } + d->evs[d->nevs].name = strdup(name); + d->evs[d->nevs].description = dupz(description); + if (d->evs[d->nevs].name == NULL) { + return -1; + } + d->nevs++; + return 0; +} + +static void set_field(char **slot, const char *v) { + if (v == NULL) { + return; + } + free(*slot); + *slot = strdup(v); +} + +void dc_driver_set_identity(dc_driver *d, const char *manufacturer, + const char *model, const char *firmware_version, + const char *description) { + if (d == NULL) { + return; + } + set_field(&d->manufacturer, manufacturer); + set_field(&d->model, model); + set_field(&d->firmware_version, firmware_version); + set_field(&d->description, description); +} + +void dc_driver_set_location(dc_driver *d, const char *location) { + if (d != NULL) { + set_field(&d->location, location); + } +} + +void dc_driver_set_availability(dc_driver *d, const char *availability) { + if (d != NULL) { + set_field(&d->availability, availability); + } +} + +const char *dc_driver_device_type(const dc_driver *d) { + return d != NULL ? d->device_type : NULL; +} + +json *dc_driver_capabilities(const dc_driver *d) { + if (d == NULL) { + return NULL; + } + json *caps = json_object(); + json *fns = json_array(); + json *evs = json_array(); + if (caps == NULL || fns == NULL || evs == NULL) { + json_free(caps); + json_free(fns); + json_free(evs); + return NULL; + } + for (size_t i = 0; i < d->nfns; i++) { + json *f = json_object(); + json *schema = (d->fns[i].params_schema != NULL) + ? json_clone(d->fns[i].params_schema) + : json_object(); + if (f == NULL || schema == NULL) { + json_free(f); + json_free(schema); + json_free(caps); + json_free(fns); + json_free(evs); + return NULL; + } + json_object_set(f, "name", json_string(d->fns[i].name)); + json_object_set(f, "description", + json_string(d->fns[i].description + ? d->fns[i].description + : "")); + json_object_set(f, "parameters", schema); + json_object_set(f, "tags", json_array()); + json_array_append(fns, f); + } + for (size_t i = 0; i < d->nevs; i++) { + json *e = json_object(); + if (e == NULL) { + json_free(caps); + json_free(fns); + json_free(evs); + return NULL; + } + json_object_set(e, "name", json_string(d->evs[i].name)); + json_object_set(e, "description", + json_string(d->evs[i].description + ? d->evs[i].description + : "")); + json_array_append(evs, e); + } + json_object_set(caps, "description", + json_string(d->description ? d->description : "")); + json_object_set(caps, "functions", fns); + json_object_set(caps, "events", evs); + return caps; +} + +json *dc_driver_identity(const dc_driver *d) { + if (d == NULL) { + return NULL; + } + json *id = json_object(); + if (id == NULL) { + return NULL; + } + json_object_set(id, "device_type", json_string(d->device_type)); + if (d->manufacturer) { + json_object_set(id, "manufacturer", json_string(d->manufacturer)); + } + if (d->model) { + json_object_set(id, "model", json_string(d->model)); + } + if (d->firmware_version) { + json_object_set(id, "firmware_version", + json_string(d->firmware_version)); + } + if (d->description) { + json_object_set(id, "description", json_string(d->description)); + } + return id; +} + +json *dc_driver_status(const dc_driver *d) { + if (d == NULL) { + return NULL; + } + json *st = json_object(); + if (st == NULL) { + return NULL; + } + json_object_set(st, "availability", json_string(d->availability)); + json_object_set(st, "online", json_bool(1)); + if (d->location) { + json_object_set(st, "location", json_string(d->location)); + } + json_object_set(st, "busy_score", json_real(0.0)); + return st; +} + +static const fn_entry *find_fn(const dc_driver *d, const char *name) { + for (size_t i = 0; i < d->nfns; i++) { + if (strcmp(d->fns[i].name, name) == 0) { + return &d->fns[i]; + } + } + return NULL; +} + +int dc_driver_call(const dc_driver *d, const char *function, const json *params, + json **result_out, char *errmsg, size_t errcap) { + if (result_out != NULL) { + *result_out = NULL; + } + if (d == NULL || function == NULL) { + return DC_ERR_INVALID_PARAMS; + } + const fn_entry *e = find_fn(d, function); + if (e == NULL) { + if (errmsg) { + snprintf(errmsg, errcap, "no such function: %s", function); + } + return DC_ERR_METHOD_NF; + } + dc_rpc_result out; + memset(&out, 0, sizeof(out)); + e->fn(e->user, params, &out); + if (out.err_code != 0) { + if (errmsg) { + snprintf(errmsg, errcap, "%s", + out.err_msg[0] ? out.err_msg : "function error"); + } + json_free(out.result); + return out.err_code; + } + if (result_out != NULL) { + *result_out = out.result; + } else { + json_free(out.result); + } + return 0; +} diff --git a/packages/device-connect-edge-c/src/hash.c b/packages/device-connect-edge-c/src/hash.c new file mode 100644 index 0000000..46a64c7 --- /dev/null +++ b/packages/device-connect-edge-c/src/hash.c @@ -0,0 +1,124 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) 2024-2026, Arm Limited and Contributors. All rights reserved. + * + * dc/hash.c -- SHA-256 (FIPS 180-4) and hex helpers. + * + * Compact, portable, little/big-endian-neutral implementation operating on + * byte streams. ASCII-only source (per CLAUDE.md). + */ + +#include "dc/hash.h" + +#include +#include + +static uint32_t rotr(uint32_t x, unsigned n) { + return (x >> n) | (x << (32 - n)); +} + +static const uint32_t K[64] = { + 0x428a2f98u, 0x71374491u, 0xb5c0fbcfu, 0xe9b5dba5u, 0x3956c25bu, + 0x59f111f1u, 0x923f82a4u, 0xab1c5ed5u, 0xd807aa98u, 0x12835b01u, + 0x243185beu, 0x550c7dc3u, 0x72be5d74u, 0x80deb1feu, 0x9bdc06a7u, + 0xc19bf174u, 0xe49b69c1u, 0xefbe4786u, 0x0fc19dc6u, 0x240ca1ccu, + 0x2de92c6fu, 0x4a7484aau, 0x5cb0a9dcu, 0x76f988dau, 0x983e5152u, + 0xa831c66du, 0xb00327c8u, 0xbf597fc7u, 0xc6e00bf3u, 0xd5a79147u, + 0x06ca6351u, 0x14292967u, 0x27b70a85u, 0x2e1b2138u, 0x4d2c6dfcu, + 0x53380d13u, 0x650a7354u, 0x766a0abbu, 0x81c2c92eu, 0x92722c85u, + 0xa2bfe8a1u, 0xa81a664bu, 0xc24b8b70u, 0xc76c51a3u, 0xd192e819u, + 0xd6990624u, 0xf40e3585u, 0x106aa070u, 0x19a4c116u, 0x1e376c08u, + 0x2748774cu, 0x34b0bcb5u, 0x391c0cb3u, 0x4ed8aa4au, 0x5b9cca4fu, + 0x682e6ff3u, 0x748f82eeu, 0x78a5636fu, 0x84c87814u, 0x8cc70208u, + 0x90befffau, 0xa4506cebu, 0xbef9a3f7u, 0xc67178f2u}; + +static void sha256_block(uint32_t h[8], const uint8_t *p) { + uint32_t w[64]; + for (int i = 0; i < 16; i++) { + w[i] = ((uint32_t)p[i * 4] << 24) | ((uint32_t)p[i * 4 + 1] << 16) | + ((uint32_t)p[i * 4 + 2] << 8) | ((uint32_t)p[i * 4 + 3]); + } + for (int i = 16; i < 64; i++) { + uint32_t s0 = rotr(w[i - 15], 7) ^ rotr(w[i - 15], 18) ^ (w[i - 15] >> 3); + uint32_t s1 = rotr(w[i - 2], 17) ^ rotr(w[i - 2], 19) ^ (w[i - 2] >> 10); + w[i] = w[i - 16] + s0 + w[i - 7] + s1; + } + uint32_t a = h[0], b = h[1], c = h[2], d = h[3]; + uint32_t e = h[4], f = h[5], g = h[6], hh = h[7]; + for (int i = 0; i < 64; i++) { + uint32_t S1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25); + uint32_t ch = (e & f) ^ ((~e) & g); + uint32_t t1 = hh + S1 + ch + K[i] + w[i]; + uint32_t S0 = rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22); + uint32_t maj = (a & b) ^ (a & c) ^ (b & c); + uint32_t t2 = S0 + maj; + hh = g; + g = f; + f = e; + e = d + t1; + d = c; + c = b; + b = a; + a = t1 + t2; + } + h[0] += a; + h[1] += b; + h[2] += c; + h[3] += d; + h[4] += e; + h[5] += f; + h[6] += g; + h[7] += hh; +} + +void dc_sha256(const void *data, size_t len, + uint8_t digest[DC_SHA256_DIGEST]) { + uint32_t h[8] = {0x6a09e667u, 0xbb67ae85u, 0x3c6ef372u, 0xa54ff53au, + 0x510e527fu, 0x9b05688cu, 0x1f83d9abu, 0x5be0cd19u}; + const uint8_t *p = (const uint8_t *)data; + size_t full = len / 64; + for (size_t i = 0; i < full; i++) { + sha256_block(h, p + i * 64); + } + /* final block(s) with padding */ + uint8_t tail[128]; + size_t rem = len - full * 64; + memcpy(tail, p + full * 64, rem); + tail[rem] = 0x80; + size_t pad_to = (rem < 56) ? 64 : 128; + memset(tail + rem + 1, 0, pad_to - rem - 1); + uint64_t bits = (uint64_t)len * 8u; + for (int i = 0; i < 8; i++) { + tail[pad_to - 1 - i] = (uint8_t)(bits >> (8 * i)); + } + sha256_block(h, tail); + if (pad_to == 128) { + sha256_block(h, tail + 64); + } + for (int i = 0; i < 8; i++) { + digest[i * 4] = (uint8_t)(h[i] >> 24); + digest[i * 4 + 1] = (uint8_t)(h[i] >> 16); + digest[i * 4 + 2] = (uint8_t)(h[i] >> 8); + digest[i * 4 + 3] = (uint8_t)(h[i]); + } +} + +void dc_hex_encode(const uint8_t *bytes, size_t n, char *out) { + static const char hexd[] = "0123456789abcdef"; + for (size_t i = 0; i < n; i++) { + out[i * 2] = hexd[(bytes[i] >> 4) & 0xf]; + out[i * 2 + 1] = hexd[bytes[i] & 0xf]; + } + out[n * 2] = '\0'; +} + +char *dc_sha256_hex(const void *data, size_t len) { + uint8_t digest[DC_SHA256_DIGEST]; + dc_sha256(data, len, digest); + char *out = (char *)malloc(DC_SHA256_HEXLEN + 1); + if (out == NULL) { + return NULL; + } + dc_hex_encode(digest, DC_SHA256_DIGEST, out); + return out; +} diff --git a/packages/device-connect-edge-c/src/json.c b/packages/device-connect-edge-c/src/json.c new file mode 100644 index 0000000..89b2307 --- /dev/null +++ b/packages/device-connect-edge-c/src/json.c @@ -0,0 +1,1001 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) 2024-2026, Arm Limited and Contributors. All rights reserved. + * + * dc/json.c -- implementation of the tiny JSON reader/writer. + * + * ASCII-only source (per CLAUDE.md). + */ + +#include "dc/json.h" + +#include +#include +#include +#include + +struct json { + json_type type; + union { + int boolean; + int64_t inum; + double rnum; + struct { + char *bytes; + size_t len; + } str; + struct { + json **items; + size_t count; + size_t cap; + } arr; + struct { + char **keys; + json **vals; + size_t count; + size_t cap; + } obj; + } u; +}; + +/* ------------------------------------------------------------------ */ +/* constructors */ +/* ------------------------------------------------------------------ */ + +static json *node_new(json_type t) { + json *j = (json *)calloc(1, sizeof(*j)); + if (j != NULL) { + j->type = t; + } + return j; +} + +json *json_null(void) { return node_new(JSON_NULL); } + +json *json_bool(int b) { + json *j = node_new(JSON_BOOL); + if (j != NULL) { + j->u.boolean = b ? 1 : 0; + } + return j; +} + +json *json_int(int64_t v) { + json *j = node_new(JSON_INT); + if (j != NULL) { + j->u.inum = v; + } + return j; +} + +json *json_real(double v) { + json *j = node_new(JSON_REAL); + if (j != NULL) { + j->u.rnum = v; + } + return j; +} + +json *json_string_n(const char *s, size_t n) { + json *j = node_new(JSON_STRING); + if (j == NULL) { + return NULL; + } + j->u.str.bytes = (char *)malloc(n + 1); + if (j->u.str.bytes == NULL) { + free(j); + return NULL; + } + if (n > 0 && s != NULL) { + memcpy(j->u.str.bytes, s, n); + } + j->u.str.bytes[n] = '\0'; + j->u.str.len = n; + return j; +} + +json *json_string(const char *s) { + return json_string_n(s, s != NULL ? strlen(s) : 0); +} + +json *json_array(void) { return node_new(JSON_ARRAY); } +json *json_object(void) { return node_new(JSON_OBJECT); } + +/* ------------------------------------------------------------------ */ +/* container mutators */ +/* ------------------------------------------------------------------ */ + +static int grow_ptrs(void ***p, size_t *cap, size_t need) { + if (*cap >= need) { + return 0; + } + size_t ncap = (*cap == 0) ? 4 : (*cap * 2); + while (ncap < need) { + ncap *= 2; + } + void **np = (void **)realloc(*p, ncap * sizeof(void *)); + if (np == NULL) { + return -1; + } + *p = np; + *cap = ncap; + return 0; +} + +int json_array_append(json *arr, json *val) { + if (arr == NULL || arr->type != JSON_ARRAY || val == NULL) { + json_free(val); + return -1; + } + if (grow_ptrs((void ***)&arr->u.arr.items, &arr->u.arr.cap, + arr->u.arr.count + 1) != 0) { + json_free(val); + return -1; + } + arr->u.arr.items[arr->u.arr.count++] = val; + return 0; +} + +int json_object_set(json *obj, const char *key, json *val) { + if (obj == NULL || obj->type != JSON_OBJECT || key == NULL || val == NULL) { + json_free(val); + return -1; + } + /* replace existing key, preserving slot */ + for (size_t i = 0; i < obj->u.obj.count; i++) { + if (strcmp(obj->u.obj.keys[i], key) == 0) { + json_free(obj->u.obj.vals[i]); + obj->u.obj.vals[i] = val; + return 0; + } + } + if (grow_ptrs((void ***)&obj->u.obj.keys, &obj->u.obj.cap, + obj->u.obj.count + 1) != 0) { + json_free(val); + return -1; + } + /* keys and vals grow in lockstep; vals cap tracked via keys cap */ + json **nv = (json **)realloc(obj->u.obj.vals, + obj->u.obj.cap * sizeof(json *)); + if (nv == NULL) { + json_free(val); + return -1; + } + obj->u.obj.vals = nv; + char *kc = (char *)malloc(strlen(key) + 1); + if (kc == NULL) { + json_free(val); + return -1; + } + strcpy(kc, key); + obj->u.obj.keys[obj->u.obj.count] = kc; + obj->u.obj.vals[obj->u.obj.count] = val; + obj->u.obj.count++; + return 0; +} + +/* ------------------------------------------------------------------ */ +/* accessors */ +/* ------------------------------------------------------------------ */ + +json_type json_typeof(const json *j) { return j != NULL ? j->type : JSON_NULL; } + +int json_is_null(const json *j) { return j == NULL || j->type == JSON_NULL; } + +double json_number(const json *j) { + if (j == NULL) { + return 0.0; + } + if (j->type == JSON_INT) { + return (double)j->u.inum; + } + if (j->type == JSON_REAL) { + return j->u.rnum; + } + return 0.0; +} + +int64_t json_integer(const json *j) { + if (j == NULL) { + return 0; + } + if (j->type == JSON_INT) { + return j->u.inum; + } + if (j->type == JSON_REAL) { + return (int64_t)j->u.rnum; + } + return 0; +} + +int json_truthy(const json *j) { + return (j != NULL && j->type == JSON_BOOL) ? j->u.boolean : 0; +} + +const char *json_str(const json *j) { + return (j != NULL && j->type == JSON_STRING) ? j->u.str.bytes : NULL; +} + +size_t json_strlen(const json *j) { + return (j != NULL && j->type == JSON_STRING) ? j->u.str.len : 0; +} + +size_t json_array_size(const json *j) { + return (j != NULL && j->type == JSON_ARRAY) ? j->u.arr.count : 0; +} + +json *json_array_get(const json *j, size_t i) { + if (j == NULL || j->type != JSON_ARRAY || i >= j->u.arr.count) { + return NULL; + } + return j->u.arr.items[i]; +} + +json *json_object_get(const json *j, const char *key) { + if (j == NULL || j->type != JSON_OBJECT || key == NULL) { + return NULL; + } + for (size_t i = 0; i < j->u.obj.count; i++) { + if (strcmp(j->u.obj.keys[i], key) == 0) { + return j->u.obj.vals[i]; + } + } + return NULL; +} + +size_t json_object_size(const json *j) { + return (j != NULL && j->type == JSON_OBJECT) ? j->u.obj.count : 0; +} + +const char *json_object_key_at(const json *j, size_t i) { + if (j == NULL || j->type != JSON_OBJECT || i >= j->u.obj.count) { + return NULL; + } + return j->u.obj.keys[i]; +} + +json *json_object_val_at(const json *j, size_t i) { + if (j == NULL || j->type != JSON_OBJECT || i >= j->u.obj.count) { + return NULL; + } + return j->u.obj.vals[i]; +} + +json *json_clone(const json *j) { + if (j == NULL) { + return NULL; + } + switch (j->type) { + case JSON_NULL: + return json_null(); + case JSON_BOOL: + return json_bool(j->u.boolean); + case JSON_INT: + return json_int(j->u.inum); + case JSON_REAL: + return json_real(j->u.rnum); + case JSON_STRING: + return json_string_n(j->u.str.bytes, j->u.str.len); + case JSON_ARRAY: { + json *a = json_array(); + if (a == NULL) { + return NULL; + } + for (size_t i = 0; i < j->u.arr.count; i++) { + json *c = json_clone(j->u.arr.items[i]); + if (c == NULL || json_array_append(a, c) != 0) { + json_free(a); + return NULL; + } + } + return a; + } + case JSON_OBJECT: { + json *o = json_object(); + if (o == NULL) { + return NULL; + } + for (size_t i = 0; i < j->u.obj.count; i++) { + json *c = json_clone(j->u.obj.vals[i]); + if (c == NULL || json_object_set(o, j->u.obj.keys[i], c) != 0) { + json_free(o); + return NULL; + } + } + return o; + } + default: + return NULL; + } +} + +/* ------------------------------------------------------------------ */ +/* free */ +/* ------------------------------------------------------------------ */ + +void json_free(json *j) { + if (j == NULL) { + return; + } + switch (j->type) { + case JSON_STRING: + free(j->u.str.bytes); + break; + case JSON_ARRAY: + for (size_t i = 0; i < j->u.arr.count; i++) { + json_free(j->u.arr.items[i]); + } + free(j->u.arr.items); + break; + case JSON_OBJECT: + for (size_t i = 0; i < j->u.obj.count; i++) { + free(j->u.obj.keys[i]); + json_free(j->u.obj.vals[i]); + } + free(j->u.obj.keys); + free(j->u.obj.vals); + break; + default: + break; + } + free(j); +} + +/* ------------------------------------------------------------------ */ +/* growable output buffer */ +/* ------------------------------------------------------------------ */ + +typedef struct { + char *buf; + size_t len; + size_t cap; + int err; +} sbuf; + +static void sbuf_reserve(sbuf *s, size_t extra) { + if (s->err) { + return; + } + if (s->len + extra + 1 <= s->cap) { + return; + } + size_t ncap = (s->cap == 0) ? 64 : s->cap; + while (s->len + extra + 1 > ncap) { + ncap *= 2; + } + char *nb = (char *)realloc(s->buf, ncap); + if (nb == NULL) { + s->err = 1; + return; + } + s->buf = nb; + s->cap = ncap; +} + +static void sbuf_putc(sbuf *s, char c) { + sbuf_reserve(s, 1); + if (s->err) { + return; + } + s->buf[s->len++] = c; +} + +static void sbuf_put(sbuf *s, const char *p, size_t n) { + sbuf_reserve(s, n); + if (s->err) { + return; + } + memcpy(s->buf + s->len, p, n); + s->len += n; +} + +static void sbuf_puts(sbuf *s, const char *p) { sbuf_put(s, p, strlen(p)); } + +/* ------------------------------------------------------------------ */ +/* serialization */ +/* ------------------------------------------------------------------ */ + +static void dump_string(sbuf *s, const char *p, size_t n) { + static const char hexd[] = "0123456789abcdef"; + sbuf_putc(s, '"'); + for (size_t i = 0; i < n; i++) { + unsigned char c = (unsigned char)p[i]; + switch (c) { + case '"': + sbuf_put(s, "\\\"", 2); + break; + case '\\': + sbuf_put(s, "\\\\", 2); + break; + case '\b': + sbuf_put(s, "\\b", 2); + break; + case '\f': + sbuf_put(s, "\\f", 2); + break; + case '\n': + sbuf_put(s, "\\n", 2); + break; + case '\r': + sbuf_put(s, "\\r", 2); + break; + case '\t': + sbuf_put(s, "\\t", 2); + break; + default: + if (c < 0x20) { + char esc[6]; + esc[0] = '\\'; + esc[1] = 'u'; + esc[2] = '0'; + esc[3] = '0'; + esc[4] = hexd[(c >> 4) & 0xf]; + esc[5] = hexd[c & 0xf]; + sbuf_put(s, esc, 6); + } else { + /* bytes >= 0x20, including UTF-8 continuation bytes, pass + * through verbatim (already valid JSON). */ + sbuf_putc(s, (char)c); + } + break; + } + } + sbuf_putc(s, '"'); +} + +static void dump_real(sbuf *s, double v) { + if (!isfinite(v)) { + /* C20 / section 4.4: never emit NaN or Infinity. */ + sbuf_puts(s, "null"); + return; + } + char tmp[40]; + /* %.17g round-trips an IEEE-754 double exactly. */ + int n = snprintf(tmp, sizeof(tmp), "%.17g", v); + if (n < 0) { + s->err = 1; + return; + } + sbuf_put(s, tmp, (size_t)n); +} + +/* index permutation for sorted object emission */ +static int cmp_keys(const void *a, const void *b, const char **keys) { + size_t ia = *(const size_t *)a; + size_t ib = *(const size_t *)b; + return strcmp(keys[ia], keys[ib]); +} + +/* qsort_r is non-portable; do a simple insertion sort on the index array + * (object sizes here are tiny -- a handful of members). */ +static void sort_indices(size_t *idx, size_t n, char **keys) { + for (size_t i = 1; i < n; i++) { + size_t cur = idx[i]; + size_t j = i; + while (j > 0 && strcmp(keys[idx[j - 1]], keys[cur]) > 0) { + idx[j] = idx[j - 1]; + j--; + } + idx[j] = cur; + } + (void)cmp_keys; +} + +static void dump_value(sbuf *s, const json *j, int sorted) { + if (j == NULL) { + sbuf_puts(s, "null"); + return; + } + switch (j->type) { + case JSON_NULL: + sbuf_puts(s, "null"); + break; + case JSON_BOOL: + sbuf_puts(s, j->u.boolean ? "true" : "false"); + break; + case JSON_INT: { + char tmp[32]; + int n = snprintf(tmp, sizeof(tmp), "%lld", (long long)j->u.inum); + if (n < 0) { + s->err = 1; + } else { + sbuf_put(s, tmp, (size_t)n); + } + break; + } + case JSON_REAL: + dump_real(s, j->u.rnum); + break; + case JSON_STRING: + dump_string(s, j->u.str.bytes, j->u.str.len); + break; + case JSON_ARRAY: + sbuf_putc(s, '['); + for (size_t i = 0; i < j->u.arr.count; i++) { + if (i > 0) { + sbuf_putc(s, ','); + } + dump_value(s, j->u.arr.items[i], sorted); + } + sbuf_putc(s, ']'); + break; + case JSON_OBJECT: { + sbuf_putc(s, '{'); + size_t n = j->u.obj.count; + if (sorted && n > 1) { + size_t stackidx[16]; + size_t *idx = stackidx; + if (n > 16) { + idx = (size_t *)malloc(n * sizeof(size_t)); + if (idx == NULL) { + s->err = 1; + break; + } + } + for (size_t i = 0; i < n; i++) { + idx[i] = i; + } + sort_indices(idx, n, j->u.obj.keys); + for (size_t i = 0; i < n; i++) { + if (i > 0) { + sbuf_putc(s, ','); + } + size_t k = idx[i]; + dump_string(s, j->u.obj.keys[k], strlen(j->u.obj.keys[k])); + sbuf_putc(s, ':'); + dump_value(s, j->u.obj.vals[k], sorted); + } + if (idx != stackidx) { + free(idx); + } + } else { + for (size_t i = 0; i < n; i++) { + if (i > 0) { + sbuf_putc(s, ','); + } + dump_string(s, j->u.obj.keys[i], strlen(j->u.obj.keys[i])); + sbuf_putc(s, ':'); + dump_value(s, j->u.obj.vals[i], sorted); + } + } + sbuf_putc(s, '}'); + break; + } + default: + sbuf_puts(s, "null"); + break; + } +} + +char *json_dumps(const json *j, int sorted, size_t *out_len) { + sbuf s; + s.buf = NULL; + s.len = 0; + s.cap = 0; + s.err = 0; + dump_value(&s, j, sorted); + if (s.err) { + free(s.buf); + return NULL; + } + sbuf_reserve(&s, 1); + if (s.err) { + free(s.buf); + return NULL; + } + s.buf[s.len] = '\0'; + if (out_len != NULL) { + *out_len = s.len; + } + return s.buf; +} + +/* ------------------------------------------------------------------ */ +/* parser */ +/* ------------------------------------------------------------------ */ + +typedef struct { + const char *p; + const char *end; + const char *err; +} pstate; + +static void skip_ws(pstate *st) { + while (st->p < st->end) { + char c = *st->p; + if (c == ' ' || c == '\t' || c == '\n' || c == '\r') { + st->p++; + } else { + break; + } + } +} + +static json *parse_value(pstate *st); + +static int parse_hex4(pstate *st, unsigned *out) { + if (st->end - st->p < 4) { + return -1; + } + unsigned v = 0; + for (int i = 0; i < 4; i++) { + char c = st->p[i]; + v <<= 4; + if (c >= '0' && c <= '9') { + v |= (unsigned)(c - '0'); + } else if (c >= 'a' && c <= 'f') { + v |= (unsigned)(c - 'a' + 10); + } else if (c >= 'A' && c <= 'F') { + v |= (unsigned)(c - 'A' + 10); + } else { + return -1; + } + } + st->p += 4; + *out = v; + return 0; +} + +static void utf8_encode(sbuf *s, unsigned cp) { + if (cp < 0x80) { + sbuf_putc(s, (char)cp); + } else if (cp < 0x800) { + sbuf_putc(s, (char)(0xC0 | (cp >> 6))); + sbuf_putc(s, (char)(0x80 | (cp & 0x3F))); + } else if (cp < 0x10000) { + sbuf_putc(s, (char)(0xE0 | (cp >> 12))); + sbuf_putc(s, (char)(0x80 | ((cp >> 6) & 0x3F))); + sbuf_putc(s, (char)(0x80 | (cp & 0x3F))); + } else { + sbuf_putc(s, (char)(0xF0 | (cp >> 18))); + sbuf_putc(s, (char)(0x80 | ((cp >> 12) & 0x3F))); + sbuf_putc(s, (char)(0x80 | ((cp >> 6) & 0x3F))); + sbuf_putc(s, (char)(0x80 | (cp & 0x3F))); + } +} + +/* parse a string token (leading '"' already consumed) into a sbuf */ +static int parse_string_raw(pstate *st, sbuf *out) { + while (st->p < st->end) { + unsigned char c = (unsigned char)*st->p++; + if (c == '"') { + return 0; + } + if (c == '\\') { + if (st->p >= st->end) { + break; + } + char e = *st->p++; + switch (e) { + case '"': + sbuf_putc(out, '"'); + break; + case '\\': + sbuf_putc(out, '\\'); + break; + case '/': + sbuf_putc(out, '/'); + break; + case 'b': + sbuf_putc(out, '\b'); + break; + case 'f': + sbuf_putc(out, '\f'); + break; + case 'n': + sbuf_putc(out, '\n'); + break; + case 'r': + sbuf_putc(out, '\r'); + break; + case 't': + sbuf_putc(out, '\t'); + break; + case 'u': { + unsigned cp = 0; + if (parse_hex4(st, &cp) != 0) { + return -1; + } + if (cp >= 0xD800 && cp <= 0xDBFF) { + /* high surrogate; expect \uXXXX low surrogate */ + if (st->end - st->p >= 2 && st->p[0] == '\\' && + st->p[1] == 'u') { + st->p += 2; + unsigned lo = 0; + if (parse_hex4(st, &lo) != 0) { + return -1; + } + if (lo >= 0xDC00 && lo <= 0xDFFF) { + cp = 0x10000 + ((cp - 0xD800) << 10) + + (lo - 0xDC00); + } else { + return -1; + } + } else { + return -1; + } + } + utf8_encode(out, cp); + break; + } + default: + return -1; + } + } else if (c < 0x20) { + return -1; /* unescaped control char */ + } else { + sbuf_putc(out, (char)c); + } + if (out->err) { + return -1; + } + } + return -1; /* unterminated */ +} + +static json *parse_string(pstate *st) { + sbuf out; + out.buf = NULL; + out.len = 0; + out.cap = 0; + out.err = 0; + if (parse_string_raw(st, &out) != 0) { + free(out.buf); + st->err = "bad string"; + return NULL; + } + json *j = json_string_n(out.buf != NULL ? out.buf : "", out.len); + free(out.buf); + if (j == NULL) { + st->err = "oom"; + } + return j; +} + +static json *parse_number(pstate *st) { + const char *start = st->p; + int is_real = 0; + if (st->p < st->end && *st->p == '-') { + st->p++; + } + while (st->p < st->end) { + char c = *st->p; + if (c >= '0' && c <= '9') { + st->p++; + } else if (c == '.' || c == 'e' || c == 'E' || c == '+' || c == '-') { + is_real = 1; + st->p++; + } else { + break; + } + } + size_t n = (size_t)(st->p - start); + if (n == 0) { + st->err = "bad number"; + return NULL; + } + char tmp[64]; + if (n >= sizeof(tmp)) { + is_real = 1; /* very long; treat as real */ + } + char *buf = tmp; + char *heap = NULL; + if (n >= sizeof(tmp)) { + heap = (char *)malloc(n + 1); + if (heap == NULL) { + st->err = "oom"; + return NULL; + } + buf = heap; + } + memcpy(buf, start, n); + buf[n] = '\0'; + json *j; + if (is_real) { + j = json_real(strtod(buf, NULL)); + } else { + long long v = strtoll(buf, NULL, 10); + j = json_int((int64_t)v); + } + if (heap != NULL) { + free(heap); + } + if (j == NULL) { + st->err = "oom"; + } + return j; +} + +static int match_lit(pstate *st, const char *lit) { + size_t n = strlen(lit); + if ((size_t)(st->end - st->p) < n) { + return -1; + } + if (memcmp(st->p, lit, n) != 0) { + return -1; + } + st->p += n; + return 0; +} + +static json *parse_array(pstate *st) { + json *arr = json_array(); + if (arr == NULL) { + st->err = "oom"; + return NULL; + } + skip_ws(st); + if (st->p < st->end && *st->p == ']') { + st->p++; + return arr; + } + for (;;) { + json *v = parse_value(st); + if (v == NULL) { + json_free(arr); + return NULL; + } + if (json_array_append(arr, v) != 0) { + st->err = "oom"; + json_free(arr); + return NULL; + } + skip_ws(st); + if (st->p >= st->end) { + st->err = "unterminated array"; + json_free(arr); + return NULL; + } + char c = *st->p++; + if (c == ',') { + skip_ws(st); + continue; + } + if (c == ']') { + return arr; + } + st->err = "expected , or ]"; + json_free(arr); + return NULL; + } +} + +static json *parse_object(pstate *st) { + json *obj = json_object(); + if (obj == NULL) { + st->err = "oom"; + return NULL; + } + skip_ws(st); + if (st->p < st->end && *st->p == '}') { + st->p++; + return obj; + } + for (;;) { + skip_ws(st); + if (st->p >= st->end || *st->p != '"') { + st->err = "expected object key"; + json_free(obj); + return NULL; + } + st->p++; /* consume opening quote */ + sbuf key; + key.buf = NULL; + key.len = 0; + key.cap = 0; + key.err = 0; + if (parse_string_raw(st, &key) != 0) { + free(key.buf); + st->err = "bad key"; + json_free(obj); + return NULL; + } + skip_ws(st); + if (st->p >= st->end || *st->p != ':') { + free(key.buf); + st->err = "expected :"; + json_free(obj); + return NULL; + } + st->p++; + json *v = parse_value(st); + if (v == NULL) { + free(key.buf); + json_free(obj); + return NULL; + } + if (json_object_set(obj, key.buf != NULL ? key.buf : "", v) != 0) { + free(key.buf); + st->err = "oom"; + json_free(obj); + return NULL; + } + free(key.buf); + skip_ws(st); + if (st->p >= st->end) { + st->err = "unterminated object"; + json_free(obj); + return NULL; + } + char c = *st->p++; + if (c == ',') { + continue; + } + if (c == '}') { + return obj; + } + st->err = "expected , or }"; + json_free(obj); + return NULL; + } +} + +static json *parse_value(pstate *st) { + skip_ws(st); + if (st->p >= st->end) { + st->err = "unexpected end"; + return NULL; + } + char c = *st->p; + switch (c) { + case '{': + st->p++; + return parse_object(st); + case '[': + st->p++; + return parse_array(st); + case '"': + st->p++; + return parse_string(st); + case 't': + if (match_lit(st, "true") == 0) { + return json_bool(1); + } + st->err = "bad literal"; + return NULL; + case 'f': + if (match_lit(st, "false") == 0) { + return json_bool(0); + } + st->err = "bad literal"; + return NULL; + case 'n': + if (match_lit(st, "null") == 0) { + return json_null(); + } + st->err = "bad literal"; + return NULL; + default: + if (c == '-' || (c >= '0' && c <= '9')) { + return parse_number(st); + } + st->err = "unexpected token"; + return NULL; + } +} + +json *json_parse(const char *buf, size_t len, const char **err) { + pstate st; + st.p = buf; + st.end = buf + len; + st.err = NULL; + json *j = parse_value(&st); + if (j == NULL) { + if (err != NULL) { + *err = st.err != NULL ? st.err : "parse error"; + } + return NULL; + } + skip_ws(&st); + if (st.p != st.end) { + if (err != NULL) { + *err = "trailing data"; + } + json_free(j); + return NULL; + } + if (err != NULL) { + *err = NULL; + } + return j; +} diff --git a/packages/device-connect-edge-c/src/jsonrpc.c b/packages/device-connect-edge-c/src/jsonrpc.c new file mode 100644 index 0000000..631bde1 --- /dev/null +++ b/packages/device-connect-edge-c/src/jsonrpc.c @@ -0,0 +1,124 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) 2024-2026, Arm Limited and Contributors. All rights reserved. + * + * dc/jsonrpc.c -- JSON-RPC 2.0 envelope build/parse. + * + * ASCII-only source (per CLAUDE.md). + */ + +#include "dc/jsonrpc.h" + +#include +#include + +int dc_rpc_parse_request(json *root, dc_rpc_request *out) { + if (out == NULL) { + return DC_ERR_INTERNAL; + } + out->id = NULL; + out->method = NULL; + out->params = NULL; + if (root == NULL || json_typeof(root) != JSON_OBJECT) { + return DC_ERR_INVALID_REQ; /* C5: not an object */ + } + /* jsonrpc MUST be exactly "2.0" (C5). */ + json *ver = json_object_get(root, "jsonrpc"); + const char *vs = json_str(ver); + if (vs == NULL || strcmp(vs, "2.0") != 0) { + return DC_ERR_INVALID_REQ; + } + json *method = json_object_get(root, "method"); + const char *ms = json_str(method); + if (ms == NULL) { + return DC_ERR_INVALID_REQ; + } + out->method = ms; + /* id is optional (absent => notification). Accept string ids; a non-string + * id is tolerated but normalized to NULL for our reply path. */ + json *id = json_object_get(root, "id"); + out->id = json_str(id); /* NULL if absent or non-string */ + out->params = json_object_get(root, "params"); /* borrowed, may be NULL */ + return 0; +} + +/* shared envelope assembler: builds {"jsonrpc":"2.0", , } */ +static char *build_envelope(const char *id, int with_id, const char *body_key, + json *body_val, const char *method, + size_t *out_len) { + json *env = json_object(); + if (env == NULL) { + json_free(body_val); + return NULL; + } + if (json_object_set(env, "jsonrpc", json_string("2.0")) != 0) { + json_free(body_val); + json_free(env); + return NULL; + } + if (method != NULL) { + if (json_object_set(env, "method", json_string(method)) != 0) { + json_free(body_val); + json_free(env); + return NULL; + } + } + if (with_id) { + json *idv = (id != NULL) ? json_string(id) : json_null(); + if (json_object_set(env, "id", idv) != 0) { + json_free(body_val); + json_free(env); + return NULL; + } + } + if (body_key != NULL) { + if (json_object_set(env, body_key, body_val) != 0) { + json_free(env); + return NULL; + } + } else { + json_free(body_val); + } + char *s = json_dumps(env, 0, out_len); + json_free(env); + return s; +} + +char *dc_rpc_build_response(const char *id, json *result, + size_t *out_len) { + if (result == NULL) { + result = json_null(); + } + return build_envelope(id, 1, "result", result, NULL, out_len); +} + +char *dc_rpc_build_error(const char *id, int code, const char *message, + size_t *out_len) { + json *err = json_object(); + if (err == NULL) { + return NULL; + } + if (json_object_set(err, "code", json_int(code)) != 0 || + json_object_set(err, "message", + json_string(message != NULL ? message : "")) != 0) { + json_free(err); + return NULL; + } + return build_envelope(id, 1, "error", err, NULL, out_len); +} + +char *dc_rpc_build_notification(const char *method, json *params, + size_t *out_len) { + if (params == NULL) { + params = json_object(); + } + return build_envelope(NULL, 0, "params", params, method, out_len); +} + +char *dc_rpc_build_request(const char *id, const char *method, + json *params, size_t *out_len) { + if (params == NULL) { + params = json_object(); + } + return build_envelope(id, 1, "params", params, method, out_len); +} diff --git a/packages/device-connect-edge-c/src/runtime.c b/packages/device-connect-edge-c/src/runtime.c new file mode 100644 index 0000000..7e7196e --- /dev/null +++ b/packages/device-connect-edge-c/src/runtime.c @@ -0,0 +1,513 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) 2024-2026, Arm Limited and Contributors. All rights reserved. + * + * dc/runtime.c -- Device Connect C edge SDK runtime. + * + * ASCII-only source. + */ + +#include "dc/runtime.h" +#include "dc/jsonrpc.h" +#include "dc/security.h" +#include "dc/transport.h" +#include "dc/transport_nats.h" + +#include +#include +#include +#include +#include +#include +#include + +double dc_now(void) { + struct timeval tv; + gettimeofday(&tv, NULL); + return (double)tv.tv_sec + (double)tv.tv_usec / 1e6; +} + +static void iso_now(char *buf, size_t cap) { + time_t t = time(NULL); + struct tm tm_utc; + gmtime_r(&t, &tm_utc); + strftime(buf, cap, "%Y-%m-%dT%H:%M:%SZ", &tm_utc); +} + +struct dc_runtime { + dc_driver *driver; /* borrowed */ + dc_transport t; + int have_transport; + dc_security sec; + char *temp_creds; /* temp chained creds to unlink, or NULL */ + char *server; /* nats URL */ + char *tenant; + char *device_id; + int device_ttl; + char *subj_cmd; + char *subj_registry; + char *subj_heartbeat; + char *reg_id; + int registered; + double hb_interval; + double last_heartbeat; + unsigned reg_seq; +}; + +/* ------------------------------------------------------------------ */ +/* credentials: accept *.creds.json (DC) or chained *.creds */ +/* ------------------------------------------------------------------ */ + +static char *read_file(const char *path, size_t *len) { + FILE *f = fopen(path, "rb"); + if (f == NULL) { + return NULL; + } + fseek(f, 0, SEEK_END); + long n = ftell(f); + fseek(f, 0, SEEK_SET); + if (n < 0) { + fclose(f); + return NULL; + } + char *buf = (char *)malloc((size_t)n + 1); + if (buf == NULL) { + fclose(f); + return NULL; + } + size_t rd = fread(buf, 1, (size_t)n, f); + fclose(f); + buf[rd] = '\0'; + if (len) { + *len = rd; + } + return buf; +} + +/* If creds_path is a DC *.creds.json, convert to a temp nsc-chained .creds and + * extract device_id/tenant. Returns the path to use for cnats (temp or the + * original), or NULL on error. */ +static char *prepare_creds(dc_runtime *r, const char *creds_path) { + size_t len = 0; + char *content = read_file(creds_path, &len); + if (content == NULL) { + return NULL; + } + /* chained .creds already? use as-is */ + if (strstr(content, "BEGIN NATS USER JWT") != NULL) { + free(content); + return strdup(creds_path); + } + const char *err = NULL; + json *j = json_parse(content, len, &err); + free(content); + if (j == NULL) { + return NULL; + } + json *nats = json_object_get(j, "nats"); + const char *jwt = nats ? json_str(json_object_get(nats, "jwt")) : NULL; + const char *seed = nats ? json_str(json_object_get(nats, "nkey_seed")) + : NULL; + const char *did = json_str(json_object_get(j, "device_id")); + const char *tnt = json_str(json_object_get(j, "tenant")); + if (r->device_id == NULL && did != NULL) { + r->device_id = strdup(did); + } + if (r->tenant == NULL && tnt != NULL) { + r->tenant = strdup(tnt); + } + char *out = NULL; + if (jwt != NULL && seed != NULL) { + char tmpl[] = "/tmp/dc-edge-creds-XXXXXX"; + int fd = mkstemp(tmpl); + if (fd >= 0) { + FILE *f = fdopen(fd, "w"); + if (f != NULL) { + fprintf(f, + "-----BEGIN NATS USER JWT-----\n%s\n" + "------END NATS USER JWT------\n\n" + "-----BEGIN USER NKEY SEED-----\n%s\n" + "------END USER NKEY SEED------\n", + jwt, seed); + fclose(f); + out = strdup(tmpl); + r->temp_creds = strdup(tmpl); + } else { + close(fd); + } + } + } + json_free(j); + return out; +} + +/* ------------------------------------------------------------------ */ +/* subjects */ +/* ------------------------------------------------------------------ */ + +static char *fmt(const char *tmpl, const char *a, const char *b) { + int n = snprintf(NULL, 0, tmpl, a, b); + if (n < 0) { + return NULL; + } + char *s = (char *)malloc((size_t)n + 1); + if (s != NULL) { + snprintf(s, (size_t)n + 1, tmpl, a, b); + } + return s; +} + +/* ------------------------------------------------------------------ */ +/* registration params */ +/* ------------------------------------------------------------------ */ + +static json *build_register_params(dc_runtime *r) { + json *p = json_object(); + if (p == NULL) { + return NULL; + } + char ts[32]; + iso_now(ts, sizeof(ts)); + json *status = dc_driver_status(r->driver); + if (status != NULL) { + json_object_set(status, "ts", json_string(ts)); + } + json_object_set(p, "device_id", json_string(r->device_id)); + json_object_set(p, "device_ttl", json_int(r->device_ttl)); + json_object_set(p, "capabilities", dc_driver_capabilities(r->driver)); + json_object_set(p, "identity", dc_driver_identity(r->driver)); + json_object_set(p, "status", status); + return p; +} + +static void do_register(dc_runtime *r) { + json *params = build_register_params(r); + if (params == NULL) { + return; + } + char reqid[64]; + snprintf(reqid, sizeof(reqid), "reg-%s-%u", r->device_id, ++r->reg_seq); + size_t blen = 0; + char *bytes = dc_rpc_build_request(reqid, "registerDevice", params, &blen); + if (bytes == NULL) { + return; + } + uint8_t *reply = NULL; + size_t rn = 0; + int rc = r->t.request(r->t.impl, r->subj_registry, (const uint8_t *)bytes, + blen, &reply, &rn, 5000); + free(bytes); + if (rc != 0) { + fprintf(stderr, "[dc-edge] registerDevice got no reply; serving anyway\n"); + return; + } + const char *err = NULL; + json *env = json_parse((const char *)reply, rn, &err); + free(reply); + if (env != NULL) { + json *result = json_object_get(env, "result"); + const char *id = + result ? json_str(json_object_get(result, "device_registration_id")) + : NULL; + if (id != NULL) { + free(r->reg_id); + r->reg_id = strdup(id); + r->registered = 1; + fprintf(stderr, "[dc-edge] registered: registration_id=%s\n", id); + } else { + fprintf(stderr, "[dc-edge] registerDevice error: %.*s\n", (int)rn, + (const char *)"(see reply)"); + } + json_free(env); + } +} + +/* ------------------------------------------------------------------ */ +/* command handler */ +/* ------------------------------------------------------------------ */ + +static void on_cmd(void *user, const char *subject, const uint8_t *data, + size_t len, const char *reply) { + (void)subject; + dc_runtime *r = (dc_runtime *)user; + if (reply == NULL) { + return; + } + const char *err = NULL; + json *root = json_parse((const char *)data, len, &err); + char *env = NULL; + size_t en = 0; + if (root == NULL || json_typeof(root) != JSON_OBJECT) { + env = dc_rpc_build_error(NULL, DC_ERR_PARSE, "parse error", &en); + } else { + const char *ver = json_str(json_object_get(root, "jsonrpc")); + const char *id = json_str(json_object_get(root, "id")); + const char *method = json_str(json_object_get(root, "method")); + json *params = json_object_get(root, "params"); + if (ver == NULL || strcmp(ver, "2.0") != 0) { + env = dc_rpc_build_error(id, DC_ERR_INVALID_REQ, + "jsonrpc must be 2.0", &en); + } else if (method == NULL) { + env = dc_rpc_build_error(id, DC_ERR_INVALID_REQ, "missing method", + &en); + } else if (strcmp(method, "requestRegistration") == 0) { + json *p = build_register_params(r); + env = dc_rpc_build_response(id, p, &en); + } else { + char emsg[192]; + emsg[0] = '\0'; + json *result = NULL; + int rc = dc_driver_call(r->driver, method, params, &result, emsg, + sizeof(emsg)); + if (rc == 0) { + if (result == NULL) { + result = json_object(); + } + env = dc_rpc_build_response(id, result, &en); + } else { + env = dc_rpc_build_error(id, rc, emsg, &en); + } + } + } + json_free(root); + if (env != NULL) { + r->t.respond(r->t.impl, reply, (const uint8_t *)env, en); + free(env); + } +} + +/* ------------------------------------------------------------------ */ +/* lifecycle */ +/* ------------------------------------------------------------------ */ + +dc_runtime *dc_runtime_new(dc_driver *driver, const dc_runtime_config *cfg) { + if (driver == NULL || cfg == NULL) { + return NULL; + } + dc_runtime *r = (dc_runtime *)calloc(1, sizeof(*r)); + if (r == NULL) { + return NULL; + } + r->driver = driver; + r->device_ttl = + (cfg->device_ttl > 0) ? cfg->device_ttl : DC_DEFAULT_DEVICE_TTL; + r->device_id = cfg->device_id ? strdup(cfg->device_id) : NULL; + r->tenant = cfg->tenant ? strdup(cfg->tenant) : NULL; + dc_security_init(&r->sec); + + const char *creds = cfg->creds_file ? cfg->creds_file + : getenv("NATS_CREDENTIALS_FILE"); + if (creds != NULL) { + char *use = prepare_creds(r, creds); + if (use != NULL) { + r->sec.creds_file = use; /* owned by sec; freed in security_free */ + } + } + if (r->tenant == NULL) { + r->tenant = strdup("default"); + } + if (r->device_id == NULL) { + fprintf(stderr, "[dc-edge] no device_id (set config or use a " + ".creds.json that carries one)\n"); + dc_runtime_free(r); + return NULL; + } + r->subj_cmd = fmt("device-connect.%s.%s.cmd", r->tenant, r->device_id); + r->subj_heartbeat = + fmt("device-connect.%s.%s.heartbeat", r->tenant, r->device_id); + r->subj_registry = fmt("device-connect.%s.registry", r->tenant, ""); + /* fmt() with one %s ignores the 2nd arg harmlessly for registry */ + r->hb_interval = (r->device_ttl > 3) ? (double)r->device_ttl / 3.0 : 1.0; + r->server = cfg->server ? strdup(cfg->server) : NULL; + if (r->subj_cmd == NULL || r->subj_heartbeat == NULL || + r->subj_registry == NULL) { + dc_runtime_free(r); + return NULL; + } + return r; +} + +void dc_runtime_free(dc_runtime *r) { + if (r == NULL) { + return; + } + if (r->have_transport) { + r->t.close(r->t.impl); + } + if (r->temp_creds != NULL) { + unlink(r->temp_creds); + free(r->temp_creds); + } + dc_security_free(&r->sec); + free(r->server); + free(r->tenant); + free(r->device_id); + free(r->subj_cmd); + free(r->subj_heartbeat); + free(r->subj_registry); + free(r->reg_id); + free(r); +} + +int dc_runtime_start(dc_runtime *r) { + if (r == NULL) { + return -1; + } + const char *server = r->server ? r->server : getenv("NATS_URL"); + if (server == NULL) { + server = "nats://127.0.0.1:4222"; + } + dc_nats_config nc; + memset(&nc, 0, sizeof(nc)); + nc.url = server; + nc.security = &r->sec; + if (dc_transport_nats_create(&r->t, &nc) != 0) { + return -1; + } + r->have_transport = 1; + if (r->t.subscribe(r->t.impl, r->subj_cmd, on_cmd, r) != 0) { + return -1; + } + do_register(r); + return 0; +} + +void dc_runtime_tick(dc_runtime *r, double now) { + if (r == NULL) { + return; + } + /* reconnect re-arm: re-register on a reconnect after an outage >= ttl */ + double outage = 0.0; + if (dc_transport_nats_poll_reconnect(&r->t, &outage)) { + if (!r->registered || outage >= (double)r->device_ttl) { + do_register(r); + } + } + if (now - r->last_heartbeat >= r->hb_interval || r->last_heartbeat == 0.0) { + json *beat = json_object(); + if (beat != NULL) { + json_object_set(beat, "device_id", json_string(r->device_id)); + json_object_set(beat, "ts", json_real(now)); + size_t bn = 0; + char *s = json_dumps(beat, 0, &bn); + json_free(beat); + if (s != NULL) { + r->t.publish(r->t.impl, r->subj_heartbeat, (const uint8_t *)s, + bn); + free(s); + } + } + r->last_heartbeat = now; + } +} + +static volatile sig_atomic_t g_stop; +static void on_sig(int s) { + (void)s; + g_stop = 1; +} + +void dc_runtime_run(dc_runtime *r) { + if (r == NULL) { + return; + } + signal(SIGINT, on_sig); + signal(SIGTERM, on_sig); + g_stop = 0; + struct timespec ts = {0, 50 * 1000000L}; + while (!g_stop) { + dc_runtime_tick(r, dc_now()); + nanosleep(&ts, NULL); + } + dc_runtime_stop(r); +} + +void dc_runtime_stop(dc_runtime *r) { + if (r == NULL || !r->have_transport) { + return; + } + /* announce departure via a presence-style offline note on heartbeat ts=0 */ + json *beat = json_object(); + if (beat != NULL) { + json_object_set(beat, "device_id", json_string(r->device_id)); + json_object_set(beat, "departing", json_bool(1)); + size_t bn = 0; + char *s = json_dumps(beat, 0, &bn); + json_free(beat); + if (s != NULL) { + r->t.publish(r->t.impl, r->subj_heartbeat, (const uint8_t *)s, bn); + free(s); + } + } +} + +json *dc_runtime_invoke_remote(dc_runtime *r, const char *device_id, + const char *function, json *params, + int timeout_ms) { + if (r == NULL || device_id == NULL || function == NULL) { + json_free(params); + return NULL; + } + char reqid[64]; + snprintf(reqid, sizeof(reqid), "d2d-%u", ++r->reg_seq); + size_t blen = 0; + char *bytes = dc_rpc_build_request(reqid, function, + params ? params : json_object(), &blen); + if (bytes == NULL) { + return NULL; + } + char *subj = fmt("device-connect.%s.%s.cmd", r->tenant, device_id); + uint8_t *reply = NULL; + size_t rn = 0; + int rc = -1; + if (subj != NULL) { + rc = r->t.request(r->t.impl, subj, (const uint8_t *)bytes, blen, &reply, + &rn, timeout_ms > 0 ? timeout_ms : 5000); + } + free(bytes); + free(subj); + if (rc != 0) { + return NULL; + } + const char *err = NULL; + json *env = json_parse((const char *)reply, rn, &err); + free(reply); + return env; +} + +void dc_runtime_emit(dc_runtime *r, const char *event, json *params) { + if (r == NULL || event == NULL) { + json_free(params); + return; + } + char *subj = fmt("device-connect.%s.%s", r->tenant, r->device_id); + /* append .event.{name} */ + char *full = NULL; + if (subj != NULL) { + int n = snprintf(NULL, 0, "%s.event.%s", subj, event); + full = (char *)malloc((size_t)n + 1); + if (full != NULL) { + snprintf(full, (size_t)n + 1, "%s.event.%s", subj, event); + } + } + free(subj); + if (full == NULL) { + json_free(params); + return; + } + size_t bn = 0; + char *s = dc_rpc_build_notification(event, params ? params : json_object(), + &bn); + if (s != NULL) { + r->t.publish(r->t.impl, full, (const uint8_t *)s, bn); + free(s); + } + free(full); +} + +const char *dc_runtime_device_id(const dc_runtime *r) { + return r ? r->device_id : NULL; +} +const char *dc_runtime_registration_id(const dc_runtime *r) { + return r ? r->reg_id : NULL; +} diff --git a/packages/device-connect-edge-c/src/security.c b/packages/device-connect-edge-c/src/security.c new file mode 100644 index 0000000..0398275 --- /dev/null +++ b/packages/device-connect-edge-c/src/security.c @@ -0,0 +1,74 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) 2024-2026, Arm Limited and Contributors. All rights reserved. + * + * dc/security.c -- transport security config loading. + * + * ASCII-only source (per CLAUDE.md). + */ + +#include "dc/security.h" + +#include +#include + +void dc_security_init(dc_security *s) { + if (s == NULL) { + return; + } + memset(s, 0, sizeof(*s)); + s->verify_hostname = 1; +} + +static char *dup_env(const char *name) { + const char *v = getenv(name); + if (v == NULL || v[0] == '\0') { + return NULL; + } + return strdup(v); +} + +void dc_security_load_env(dc_security *s) { + if (s == NULL) { + return; + } + if (s->creds_file == NULL) { + s->creds_file = dup_env("NATS_CREDENTIALS_FILE"); + } + if (s->jwt == NULL) { + s->jwt = dup_env("NATS_JWT"); + } + if (s->nkey_seed == NULL) { + s->nkey_seed = dup_env("NATS_NKEY_SEED"); + } + if (s->tls_ca == NULL) { + s->tls_ca = dup_env("MHP_TLS_CA"); + } + if (s->tls_cert == NULL) { + s->tls_cert = dup_env("MHP_TLS_CERT"); + } + if (s->tls_key == NULL) { + s->tls_key = dup_env("MHP_TLS_KEY"); + } + if (s->tls_ca != NULL || s->tls_cert != NULL || s->tls_key != NULL) { + s->tls_enable = 1; + } + const char *vh = getenv("MHP_TLS_VERIFY_HOSTNAME"); + if (vh != NULL && strcmp(vh, "0") == 0) { + s->verify_hostname = 0; + } +} + +void dc_security_free(dc_security *s) { + if (s == NULL) { + return; + } + free(s->creds_file); + free(s->jwt); + free(s->nkey_seed); + free(s->tls_ca); + free(s->tls_cert); + free(s->tls_key); + memset(s, 0, sizeof(*s)); + s->verify_hostname = 1; +} diff --git a/packages/device-connect-edge-c/src/transport_nats.c b/packages/device-connect-edge-c/src/transport_nats.c new file mode 100644 index 0000000..5cc1087 --- /dev/null +++ b/packages/device-connect-edge-c/src/transport_nats.c @@ -0,0 +1,299 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) 2024-2026, Arm Limited and Contributors. All rights reserved. + * + * dc/transport_nats.c -- NATS backend (cnats). + * + * Built only when DC_WITH_NATS is defined; otherwise a stub. cnats is a + * fresh from-scratch integration; if any logic is later ported from another + * implementation, flag the file for co-attribution review (see PR #189). + * + * ASCII-only source (per CLAUDE.md). + */ + +#include "dc/transport_nats.h" + +#include + +#ifndef DC_WITH_NATS + +/* ---- stub: library was built without the NATS C client ---- */ +int dc_transport_nats_create(dc_transport *t, + const dc_nats_config *cfg) { + (void)t; + (void)cfg; + fprintf(stderr, + "[dc-edge] NATS transport unavailable: rebuild with " + "DC_WITH_NATS=1 and the NATS C client installed.\n"); + return -1; +} + +int dc_transport_nats_poll_reconnect(dc_transport *t, + double *outage_s) { + (void)t; + (void)outage_s; + return 0; +} + +#else /* DC_WITH_NATS */ + + +#include +#include +#include + +/* per-subscription bridge from the cnats handler to dc_msg_cb */ +typedef struct sub_bridge { + dc_msg_cb cb; + void *user; + natsSubscription *sub; + struct sub_bridge *next; +} sub_bridge; + +typedef struct { + natsConnection *conn; + natsOptions *opts; + sub_bridge *bridges; + /* reconnect tracking, written by cnats callback threads */ + volatile int reconnect_pending; + volatile double disconnect_ts; + volatile double last_outage; + int connected_flag; +} nats_impl; + +static void on_disconnected(natsConnection *nc, void *closure) { + (void)nc; + nats_impl *im = (nats_impl *)closure; + im->disconnect_ts = dc_now(); + im->connected_flag = 0; +} + +static void on_reconnected(natsConnection *nc, void *closure) { + (void)nc; + nats_impl *im = (nats_impl *)closure; + double now = dc_now(); + im->last_outage = (im->disconnect_ts > 0.0) ? (now - im->disconnect_ts) + : 0.0; + im->reconnect_pending = 1; + im->connected_flag = 1; +} + +static void on_closed(natsConnection *nc, void *closure) { + (void)nc; + nats_impl *im = (nats_impl *)closure; + im->connected_flag = 0; +} + +static void msg_handler(natsConnection *nc, natsSubscription *sub, + natsMsg *msg, void *closure) { + (void)nc; + (void)sub; + sub_bridge *b = (sub_bridge *)closure; + const char *reply = natsMsg_GetReply(msg); /* NULL if none */ + b->cb(b->user, natsMsg_GetSubject(msg), + (const uint8_t *)natsMsg_GetData(msg), + (size_t)natsMsg_GetDataLength(msg), reply); + natsMsg_Destroy(msg); +} + +static int nats_publish(void *impl, const char *subject, const uint8_t *data, + size_t len) { + nats_impl *im = (nats_impl *)impl; + natsStatus s = natsConnection_Publish(im->conn, subject, data, (int)len); + if (s != NATS_OK) { + return -1; + } + /* Flush so low-rate fire-and-forget publishes (presence, heartbeat) leave + * the client buffer promptly rather than waiting on the flusher; otherwise + * a registry can miss heartbeats and reap an otherwise-live device. */ + natsConnection_Flush(im->conn); + return 0; +} + +static int nats_subscribe(void *impl, const char *pattern, dc_msg_cb cb, + void *user) { + nats_impl *im = (nats_impl *)impl; + sub_bridge *b = (sub_bridge *)calloc(1, sizeof(*b)); + if (b == NULL) { + return -1; + } + b->cb = cb; + b->user = user; + natsStatus s = + natsConnection_Subscribe(&b->sub, im->conn, pattern, msg_handler, b); + if (s != NATS_OK) { + free(b); + return -1; + } + b->next = im->bridges; + im->bridges = b; + return 0; +} + +static int nats_request(void *impl, const char *subject, const uint8_t *data, + size_t len, uint8_t **out, size_t *out_len, + int timeout_ms) { + nats_impl *im = (nats_impl *)impl; + natsMsg *reply = NULL; + natsStatus s = natsConnection_Request(&reply, im->conn, subject, data, + (int)len, timeout_ms); + if (s != NATS_OK || reply == NULL) { + return -1; + } + int dlen = natsMsg_GetDataLength(reply); + uint8_t *buf = (uint8_t *)malloc(dlen > 0 ? (size_t)dlen : 1); + if (buf == NULL) { + natsMsg_Destroy(reply); + return -1; + } + if (dlen > 0) { + memcpy(buf, natsMsg_GetData(reply), (size_t)dlen); + } + natsMsg_Destroy(reply); + *out = buf; + *out_len = (size_t)dlen; + return 0; +} + +static int nats_respond(void *impl, const char *reply, const uint8_t *data, + size_t len) { + return nats_publish(impl, reply, data, len); +} + +static int nats_connected(void *impl) { + return ((nats_impl *)impl)->connected_flag; +} + +static void nats_close(void *impl) { + nats_impl *im = (nats_impl *)impl; + if (im == NULL) { + return; + } + sub_bridge *b = im->bridges; + while (b != NULL) { + sub_bridge *n = b->next; + if (b->sub != NULL) { + natsSubscription_Destroy(b->sub); + } + free(b); + b = n; + } + if (im->conn != NULL) { + natsConnection_Destroy(im->conn); + } + if (im->opts != NULL) { + natsOptions_Destroy(im->opts); + } + free(im); +} + +static int apply_security(natsOptions *opts, dc_security *sec) { + if (sec == NULL) { + return 0; + } + natsStatus s = NATS_OK; + if (sec->tls_enable || sec->tls_ca != NULL) { + s = natsOptions_SetSecure(opts, true); + if (s != NATS_OK) { + return -1; + } + if (sec->tls_ca != NULL) { + s = natsOptions_LoadCATrustedCertificates(opts, sec->tls_ca); + if (s != NATS_OK) { + return -1; + } + } + if (sec->tls_cert != NULL && sec->tls_key != NULL) { + s = natsOptions_LoadCertificatesChain(opts, sec->tls_cert, + sec->tls_key); + if (s != NATS_OK) { + return -1; + } + } + if (!sec->verify_hostname) { + /* best-effort: skip hostname verification when explicitly off */ + natsOptions_SkipServerVerification(opts, true); + } + } + if (sec->creds_file != NULL) { + s = natsOptions_SetUserCredentialsFromFiles(opts, sec->creds_file, + NULL); + if (s != NATS_OK) { + return -1; + } + } else if (sec->jwt != NULL && sec->nkey_seed != NULL) { + /* inline JWT + seed: cnats expects a chained .creds; for inline use a + * seed file is the supported path. We surface a clear error so the + * operator supplies NATS_CREDENTIALS_FILE instead. */ + fprintf(stderr, + "[dc-edge] inline NATS_JWT+NATS_NKEY_SEED not wired; supply " + "NATS_CREDENTIALS_FILE (a .creds file) instead.\n"); + return -1; + } + return 0; +} + +int dc_transport_nats_create(dc_transport *t, + const dc_nats_config *cfg) { + if (t == NULL || cfg == NULL || cfg->url == NULL) { + return -1; + } + nats_impl *im = (nats_impl *)calloc(1, sizeof(*im)); + if (im == NULL) { + return -1; + } + natsStatus s = natsOptions_Create(&im->opts); + if (s != NATS_OK) { + free(im); + return -1; + } + int wait = (cfg->reconnect_wait_ms > 0) ? cfg->reconnect_wait_ms : 250; + int jitter = + (cfg->reconnect_jitter_ms > 0) ? cfg->reconnect_jitter_ms : 250; + natsOptions_SetURL(im->opts, cfg->url); + natsOptions_SetMaxReconnect(im->opts, -1); /* unlimited */ + natsOptions_SetReconnectWait(im->opts, wait); + natsOptions_SetReconnectJitter(im->opts, jitter, jitter); + natsOptions_SetDisconnectedCB(im->opts, on_disconnected, im); + natsOptions_SetReconnectedCB(im->opts, on_reconnected, im); + natsOptions_SetClosedCB(im->opts, on_closed, im); + if (apply_security(im->opts, cfg->security) != 0) { + natsOptions_Destroy(im->opts); + free(im); + return -1; + } + s = natsConnection_Connect(&im->conn, im->opts); + if (s != NATS_OK) { + natsOptions_Destroy(im->opts); + free(im); + return -1; + } + im->connected_flag = 1; + + t->impl = im; + t->publish = nats_publish; + t->subscribe = nats_subscribe; + t->request = nats_request; + t->respond = nats_respond; + t->connected = nats_connected; + t->close = nats_close; + return 0; +} + +int dc_transport_nats_poll_reconnect(dc_transport *t, + double *outage_s) { + if (t == NULL || t->impl == NULL || t->publish != nats_publish) { + return 0; /* not a NATS transport */ + } + nats_impl *im = (nats_impl *)t->impl; + if (im->reconnect_pending) { + im->reconnect_pending = 0; + if (outage_s != NULL) { + *outage_s = im->last_outage; + } + return 1; + } + return 0; +} + +#endif /* DC_WITH_NATS */ diff --git a/packages/device-connect-edge-c/tests/Makefile b/packages/device-connect-edge-c/tests/Makefile new file mode 100644 index 0000000..4124109 --- /dev/null +++ b/packages/device-connect-edge-c/tests/Makefile @@ -0,0 +1,17 @@ +# SPDX-License-Identifier: Apache-2.0 +CC ?= cc +CFLAGS ?= -std=c11 -D_GNU_SOURCE -Wall -Wextra -O2 +ROOT = .. +INCLUDES = -I$(ROOT)/include -I. +# driver + the JSON/jsonrpc core (no NATS needed for these unit tests) +SRCS = $(ROOT)/src/json.c $(ROOT)/src/jsonrpc.c $(ROOT)/src/driver.c +TESTS = test_edge + +all: $(TESTS) +test_edge: test_edge.c $(SRCS) + $(CC) $(CFLAGS) $(INCLUDES) -o $@ $^ -lm +test: all + @fail=0; for t in $(TESTS); do echo "== $$t =="; ./$$t || fail=1; done; exit $$fail +clean: + rm -f $(TESTS) +.PHONY: all test clean diff --git a/packages/device-connect-edge-c/tests/dc_test.h b/packages/device-connect-edge-c/tests/dc_test.h new file mode 100644 index 0000000..7e74221 --- /dev/null +++ b/packages/device-connect-edge-c/tests/dc_test.h @@ -0,0 +1,13 @@ +/* SPDX-License-Identifier: Apache-2.0 + * Copyright (c) 2024-2026, Arm Limited and Contributors. All rights reserved. + * dc_test.h -- minimal self-contained test harness. ASCII-only. */ +#ifndef DC_TEST_H +#define DC_TEST_H +#include +#include +static int g_run, g_failed; static const char *g_t; +#define CHECK(e) do{ if(!(e)){ fprintf(stderr," FAIL %s:%d: %s\n",__FILE__,__LINE__,#e); g_failed++; } }while(0) +#define CHECK_STR(a,b) do{ const char*_a=(a),*_b=(b); if(!_a||!_b||strcmp(_a,_b)){ fprintf(stderr," FAIL %s:%d: \"%s\"!=\"%s\"\n",__FILE__,__LINE__,_a?_a:"(null)",_b?_b:"(null)"); g_failed++; } }while(0) +#define RUN(fn) do{ g_t=#fn; int b=g_failed; g_run++; fn(); fprintf(stderr," %s %s\n",(g_failed==b)?"ok ":"FAIL",#fn);}while(0) +#define REPORT() do{ fprintf(stderr,"%d tests, %d failures\n",g_run,g_failed); return g_failed?1:0;}while(0) +#endif diff --git a/packages/device-connect-edge-c/tests/test_edge.c b/packages/device-connect-edge-c/tests/test_edge.c new file mode 100644 index 0000000..5b05065 --- /dev/null +++ b/packages/device-connect-edge-c/tests/test_edge.c @@ -0,0 +1,111 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) 2024-2026, Arm Limited and Contributors. All rights reserved. + * + * test_edge.c -- unit tests for the edge driver + JSON core. + * + * ASCII-only source. + */ + +#include "dc/driver.h" +#include "dc/json.h" +#include "dc/jsonrpc.h" +#include "dc_test.h" + +#include + +static void rpc_reading(void *u, const json *p, dc_rpc_result *o) { + (void)u; + (void)p; + json *r = json_object(); + json_object_set(r, "temp_c", json_real(22.5)); + o->result = r; +} +static void rpc_fail(void *u, const json *p, dc_rpc_result *o) { + (void)u; + (void)p; + o->err_code = DC_ERR_INVALID_PARAMS; + snprintf(o->err_msg, sizeof(o->err_msg), "boom"); +} + +static dc_driver *make(void) { + dc_driver *d = dc_driver_new("temp_sensor"); + dc_driver_set_identity(d, "ACME", "T1", "0.1.0", "demo"); + dc_driver_add_function(d, "get_reading", "Read temp", NULL, rpc_reading, + NULL); + dc_driver_add_function(d, "boom", "fails", NULL, rpc_fail, NULL); + dc_driver_add_event(d, "reading_changed", "moved"); + return d; +} + +static int has(const char *hay, const char *needle) { + return hay && strstr(hay, needle) != NULL; +} + +static void test_capabilities(void) { + dc_driver *d = make(); + json *caps = dc_driver_capabilities(d); + char *s = json_dumps(caps, 1, NULL); + CHECK(has(s, "\"functions\"")); + CHECK(has(s, "\"events\"")); + CHECK(has(s, "get_reading")); + CHECK(has(s, "reading_changed")); + free(s); + json_free(caps); + dc_driver_free(d); +} + +static void test_identity_status(void) { + dc_driver *d = make(); + json *id = dc_driver_identity(d); + CHECK_STR(json_str(json_object_get(id, "device_type")), "temp_sensor"); + CHECK_STR(json_str(json_object_get(id, "manufacturer")), "ACME"); + json_free(id); + json *st = dc_driver_status(d); + /* DC healthy availability is "available" (portal online predicate). */ + CHECK_STR(json_str(json_object_get(st, "availability")), "available"); + json_free(st); + dc_driver_free(d); +} + +static void test_call(void) { + dc_driver *d = make(); + json *res = NULL; + char err[192]; + /* found -> 0, returns result */ + CHECK(dc_driver_call(d, "get_reading", NULL, &res, err, sizeof(err)) == 0); + CHECK(res && json_number(json_object_get(res, "temp_c")) == 22.5); + json_free(res); + /* unknown -> -32601 */ + res = NULL; + CHECK(dc_driver_call(d, "nope", NULL, &res, err, sizeof(err)) == + DC_ERR_METHOD_NF); + /* handler error propagates */ + res = NULL; + CHECK(dc_driver_call(d, "boom", NULL, &res, err, sizeof(err)) == + DC_ERR_INVALID_PARAMS); + CHECK_STR(err, "boom"); + dc_driver_free(d); +} + +static void test_jsonrpc_shapes(void) { + /* DC reply: result is the raw value */ + json *r = json_object(); + json_object_set(r, "temp_c", json_real(1.0)); + size_t n = 0; + char *s = dc_rpc_build_response("7", r, &n); + CHECK(has(s, "\"jsonrpc\":\"2.0\"") && has(s, "\"id\":\"7\"") && + has(s, "\"result\"")); + free(s); + s = dc_rpc_build_error("7", DC_ERR_METHOD_NF, "no such function", &n); + CHECK(has(s, "-32601") && has(s, "no such function")); + free(s); +} + +int main(void) { + RUN(test_capabilities); + RUN(test_identity_status); + RUN(test_call); + RUN(test_jsonrpc_shapes); + REPORT(); +}