/** * @file nodes_config.c * @brief Dot-indexed node configuration parser and server-side node creator. */ #include "nodes_config.h" #include "config.h" #include #include #include #include #include #include #include #include #include /* ======================================================================== * Type Map * ======================================================================== */ typedef struct { const char *name; node_type type; int ua_type_index; } type_map_entry; static const type_map_entry _s_type_map[] = { { "bool", NODE_TYPE_BOOL, UA_TYPES_BOOLEAN }, { "int16", NODE_TYPE_INT16, UA_TYPES_INT16 }, { "uint16", NODE_TYPE_UINT16, UA_TYPES_UINT16 }, { "int32", NODE_TYPE_INT32, UA_TYPES_INT32 }, { "uint32", NODE_TYPE_UINT32, UA_TYPES_UINT32 }, { "int64", NODE_TYPE_INT64, UA_TYPES_INT64 }, { "uint64", NODE_TYPE_UINT64, UA_TYPES_UINT64 }, { "float", NODE_TYPE_FLOAT, UA_TYPES_FLOAT }, { "double", NODE_TYPE_DOUBLE, UA_TYPES_DOUBLE }, { "string", NODE_TYPE_STRING, UA_TYPES_STRING }, }; #define TYPE_MAP_SIZE (sizeof (_s_type_map) / sizeof (_s_type_map[0])) /* ======================================================================== * Static Helpers * ======================================================================== */ /** * @brief Trims leading and trailing whitespace from a string in place. * * Returns a pointer past any leading whitespace and writes a NUL * terminator after the last non-whitespace character. */ static char * _s_trim (char *s) { while (*s == ' ' || *s == '\t') s++; size_t len = strlen (s); while (len > 0 && (s[len - 1] == ' ' || s[len - 1] == '\t')) len--; s[len] = '\0'; return s; } /** * @brief Resolves a type string to a node_type and array flag. * * Strips a trailing "[]" suffix if present (setting is_array to true), * then looks up the base name in _s_type_map. * * @return 0 on success, -1 if the type name is unknown. */ static int _s_resolve_type (const char *str, node_type *out_type, UA_Boolean *out_is_array) { char buf[64]; size_t len = strlen (str); if (len >= sizeof (buf)) return -1; memcpy (buf, str, len + 1); *out_is_array = false; if (len >= 2 && buf[len - 2] == '[' && buf[len - 1] == ']') { buf[len - 2] = '\0'; *out_is_array = true; } for (size_t i = 0; i < TYPE_MAP_SIZE; i++) { if (strcmp (buf, _s_type_map[i].name) == 0) { *out_type = _s_type_map[i].type; return 0; } } return -1; } /** * @brief Resolves an access level string to a node_access_level enum. * * @return 0 on success, -1 if the string is unknown. */ static int _s_resolve_access (const char *str, node_access_level *out) { if (strcmp (str, "read") == 0) { *out = NODE_ACCESS_READ; return 0; } if (strcmp (str, "readwrite") == 0) { *out = NODE_ACCESS_READWRITE; return 0; } return -1; } /** * @brief Returns the UA_TYPES index for a node_type enum value. */ static int _s_ua_type_index (node_type t) { for (size_t i = 0; i < TYPE_MAP_SIZE; i++) { if (_s_type_map[i].type == t) return _s_type_map[i].ua_type_index; } return -1; } /** * @brief Scans config entries for the highest N in node.N.* keys. * * @return The maximum index found, or -1 if no node.* keys exist. */ static int _s_find_max_node_index (const config *cfg) { int max_idx = -1; for (size_t i = 0; i < cfg->count; i++) { int idx; int consumed; if (sscanf (cfg->entries[i].key, "node.%d.%n", &idx, &consumed) >= 1 && consumed > 0 && idx >= 0) { if (idx > max_idx) max_idx = idx; } } return max_idx; } /** * @brief Parses a single scalar value string into a typed buffer. * * For numeric types uses strtol/strtod family with range checks. * For bool accepts "true"/"false". For string creates a UA_String * via UA_String_fromChars (written to out_str). * * @param str The value string to parse. * @param type The target data type. * @param out Buffer to write the parsed value into (must be large * enough for the target type; unused for string). * @param out_str Output for UA_String when type == NODE_TYPE_STRING. * @return 0 on success, -1 on parse error. */ static int _s_parse_scalar_value (const char *str, node_type type, void *out, UA_String *out_str) { char *endptr; errno = 0; switch (type) { case NODE_TYPE_BOOL: if (strcmp (str, "true") == 0) *(UA_Boolean *)out = true; else if (strcmp (str, "false") == 0) *(UA_Boolean *)out = false; else return -1; return 0; case NODE_TYPE_INT16: { long v = strtol (str, &endptr, 10); if (*endptr != '\0' || errno == ERANGE || v < INT16_MIN || v > INT16_MAX) return -1; *(UA_Int16 *)out = (UA_Int16)v; return 0; } case NODE_TYPE_UINT16: { unsigned long v = strtoul (str, &endptr, 10); if (*endptr != '\0' || errno == ERANGE || v > UINT16_MAX) return -1; *(UA_UInt16 *)out = (UA_UInt16)v; return 0; } case NODE_TYPE_INT32: { long v = strtol (str, &endptr, 10); if (*endptr != '\0' || errno == ERANGE || v < INT32_MIN || v > INT32_MAX) return -1; *(UA_Int32 *)out = (UA_Int32)v; return 0; } case NODE_TYPE_UINT32: { unsigned long v = strtoul (str, &endptr, 10); if (*endptr != '\0' || errno == ERANGE || v > UINT32_MAX) return -1; *(UA_UInt32 *)out = (UA_UInt32)v; return 0; } case NODE_TYPE_INT64: { long long v = strtoll (str, &endptr, 10); if (*endptr != '\0' || errno == ERANGE) return -1; *(UA_Int64 *)out = (UA_Int64)v; return 0; } case NODE_TYPE_UINT64: { unsigned long long v = strtoull (str, &endptr, 10); if (*endptr != '\0' || errno == ERANGE) return -1; *(UA_UInt64 *)out = (UA_UInt64)v; return 0; } case NODE_TYPE_FLOAT: { float v = strtof (str, &endptr); if (*endptr != '\0' || errno == ERANGE) return -1; *(UA_Float *)out = (UA_Float)v; return 0; } case NODE_TYPE_DOUBLE: { double v = strtod (str, &endptr); if (*endptr != '\0' || errno == ERANGE) return -1; *(UA_Double *)out = (UA_Double)v; return 0; } case NODE_TYPE_STRING: *out_str = UA_String_fromChars (str); return 0; } return -1; } /** * @brief Counts the number of comma-separated elements in a string. */ static size_t _s_count_elements (const char *str) { if (*str == '\0') return 0; size_t count = 1; for (const char *p = str; *p; p++) { if (*p == ',') count++; } return count; } /** * @brief Parses a comma-separated value string into a UA array. * * Tokenizes by comma, trims whitespace, parses each element with * _s_parse_scalar_value, and writes into a heap-allocated array. * * @param str The comma-separated value string. * @param type The node data type. * @param ua_type_index The UA_TYPES index for the data type. * @param out_array Output: heap-allocated array (free with * UA_Array_delete). * @param out_size Output: number of elements. * @return 0 on success, -1 on error. */ static int _s_parse_array_value (const char *str, node_type type, int ua_type_index, void **out_array, size_t *out_size) { size_t count = _s_count_elements (str); if (count == 0) { *out_array = NULL; *out_size = 0; return 0; } const UA_DataType *dt = &UA_TYPES[ua_type_index]; void *array = UA_Array_new (count, dt); if (!array) return -1; char *tmp = strdup (str); if (!tmp) { UA_Array_delete (array, count, dt); return -1; } size_t i = 0; char *saveptr; char *token = strtok_r (tmp, ",", &saveptr); while (token && i < count) { char *trimmed = _s_trim (token); if (*trimmed == '\0') { UA_LOG_ERROR (UA_Log_Stdout, UA_LOGCATEGORY_USERLAND, "NodesConfig: empty element at position %zu", i); goto fail; } /* Parse into the array element at offset i. */ void *elem = (char *)array + i * dt->memSize; UA_String str_val = UA_STRING_NULL; if (_s_parse_scalar_value (trimmed, type, elem, &str_val) != 0) { UA_LOG_ERROR (UA_Log_Stdout, UA_LOGCATEGORY_USERLAND, "NodesConfig: failed to parse array element '%s'", trimmed); goto fail; } /* For strings, copy into the array slot. */ if (type == NODE_TYPE_STRING) *(UA_String *)elem = str_val; i++; token = strtok_r (NULL, ",", &saveptr); } free (tmp); *out_array = array; *out_size = count; return 0; fail: free (tmp); UA_Array_delete (array, count, dt); return -1; } /* ======================================================================== * Parsing: nodes_config_load * ======================================================================== */ /** * @brief Frees a partially populated raw node array. */ static void _s_free_raw_nodes (node_config *raw, size_t count) { for (size_t i = 0; i < count; i++) { free (raw[i].name); free (raw[i].description); free (raw[i].value_str); } free (raw); } int nodes_config_load (const char *path, nodes_config *out) { memset (out, 0, sizeof (*out)); config cfg = { 0 }; if (config_load (path, &cfg) != 0) return -1; int max_idx = _s_find_max_node_index (&cfg); if (max_idx < 0) { /* No node.* keys — empty config, not an error. */ config_free (&cfg); return 0; } size_t raw_count = (size_t)(max_idx + 1); node_config *raw = calloc (raw_count, sizeof (node_config)); if (!raw) { UA_LOG_ERROR (UA_Log_Stdout, UA_LOGCATEGORY_USERLAND, "NodesConfig: out of memory"); config_free (&cfg); return -1; } /* Track which fields have been set to detect unknown fields and distinguish "not set" from "set to default enum value 0". */ UA_Boolean *type_set = calloc (raw_count, sizeof (UA_Boolean)); UA_Boolean *access_set = calloc (raw_count, sizeof (UA_Boolean)); if (!type_set || !access_set) { free (type_set); free (access_set); _s_free_raw_nodes (raw, raw_count); config_free (&cfg); return -1; } int rc = 0; for (size_t i = 0; i < cfg.count; i++) { int idx; int consumed = 0; if (sscanf (cfg.entries[i].key, "node.%d.%n", &idx, &consumed) < 1 || consumed == 0 || idx < 0 || (size_t)idx >= raw_count) continue; const char *field = cfg.entries[i].key + consumed; const char *value = cfg.entries[i].value; if (strcmp (field, "name") == 0) { free (raw[idx].name); raw[idx].name = strdup (value); } else if (strcmp (field, "description") == 0) { free (raw[idx].description); raw[idx].description = strdup (value); } else if (strcmp (field, "type") == 0) { if (_s_resolve_type (value, &raw[idx].type, &raw[idx].is_array) != 0) { UA_LOG_ERROR (UA_Log_Stdout, UA_LOGCATEGORY_USERLAND, "NodesConfig: unknown type '%s' for node.%d", value, idx); rc = -1; goto done; } type_set[idx] = true; } else if (strcmp (field, "value") == 0) { free (raw[idx].value_str); raw[idx].value_str = strdup (value); } else if (strcmp (field, "accessLevel") == 0) { if (_s_resolve_access (value, &raw[idx].access) != 0) { UA_LOG_ERROR ( UA_Log_Stdout, UA_LOGCATEGORY_USERLAND, "NodesConfig: unknown accessLevel '%s' for node.%d " "(expected 'read' or 'readwrite')", value, idx); rc = -1; goto done; } access_set[idx] = true; } else { UA_LOG_ERROR (UA_Log_Stdout, UA_LOGCATEGORY_USERLAND, "NodesConfig: unknown field '%s' for node.%d", field, idx); rc = -1; goto done; } } /* Compact: count populated entries (those with a name). */ size_t populated = 0; for (size_t i = 0; i < raw_count; i++) { if (raw[i].name) populated++; } if (populated == 0) { /* All indices were gaps. */ goto done; } /* Validate required fields for each populated node. */ for (size_t i = 0; i < raw_count; i++) { if (!raw[i].name) continue; if (!type_set[i]) { UA_LOG_ERROR (UA_Log_Stdout, UA_LOGCATEGORY_USERLAND, "NodesConfig: node.%zu missing required key 'type'", i); rc = -1; goto done; } if (!raw[i].value_str) { UA_LOG_ERROR (UA_Log_Stdout, UA_LOGCATEGORY_USERLAND, "NodesConfig: node.%zu missing required key 'value'", i); rc = -1; goto done; } if (!access_set[i]) { UA_LOG_ERROR ( UA_Log_Stdout, UA_LOGCATEGORY_USERLAND, "NodesConfig: node.%zu missing required key 'accessLevel'", i); rc = -1; goto done; } } /* Build the compacted output array. */ out->nodes = calloc (populated, sizeof (node_config)); if (!out->nodes) { UA_LOG_ERROR (UA_Log_Stdout, UA_LOGCATEGORY_USERLAND, "NodesConfig: out of memory"); rc = -1; goto done; } size_t j = 0; for (size_t i = 0; i < raw_count; i++) { if (!raw[i].name) continue; out->nodes[j] = raw[i]; /* Transfer ownership: clear the raw slot so _s_free_raw_nodes does not double-free these strings. */ memset (&raw[i], 0, sizeof (node_config)); j++; } out->count = populated; done: free (type_set); free (access_set); _s_free_raw_nodes (raw, raw_count); config_free (&cfg); if (rc != 0) nodes_config_free (out); return rc; } /* ======================================================================== * Node Creation: nodes_config_add * ======================================================================== */ int nodes_config_add (UA_Server *server, const nodes_config *nc) { int failures = 0; for (size_t i = 0; i < nc->count; i++) { const node_config *n = &nc->nodes[i]; int ua_idx = _s_ua_type_index (n->type); if (ua_idx < 0) { UA_LOG_ERROR (UA_Log_Stdout, UA_LOGCATEGORY_SERVER, "NodesConfig: internal error resolving type for '%s'", n->name); failures++; continue; } const UA_DataType *dt = &UA_TYPES[ua_idx]; UA_VariableAttributes attr = UA_VariableAttributes_default; attr.displayName = UA_LOCALIZEDTEXT ("en-US", n->name); if (n->description) attr.description = UA_LOCALIZEDTEXT ("en-US", n->description); attr.dataType = dt->typeId; if (n->access == NODE_ACCESS_READ) attr.accessLevel = UA_ACCESSLEVELMASK_READ; else attr.accessLevel = UA_ACCESSLEVELMASK_READ | UA_ACCESSLEVELMASK_WRITE; /* Parse value and set the variant. */ UA_Boolean value_ok = false; /* Union large enough for any scalar type. */ union { UA_Boolean b; UA_Int16 i16; UA_UInt16 u16; UA_Int32 i32; UA_UInt32 u32; UA_Int64 i64; UA_UInt64 u64; UA_Float f; UA_Double d; } scalar_buf; UA_String string_buf = UA_STRING_NULL; void *array_buf = NULL; size_t array_size = 0; UA_UInt32 array_dim = 0; if (!n->is_array) { /* Scalar */ if (_s_parse_scalar_value (n->value_str, n->type, &scalar_buf, &string_buf) != 0) { UA_LOG_ERROR (UA_Log_Stdout, UA_LOGCATEGORY_SERVER, "NodesConfig: failed to parse value '%s' " "for node '%s'", n->value_str, n->name); failures++; continue; } if (n->type == NODE_TYPE_STRING) UA_Variant_setScalar (&attr.value, &string_buf, dt); else UA_Variant_setScalar (&attr.value, &scalar_buf, dt); attr.valueRank = UA_VALUERANK_SCALAR; value_ok = true; } else { /* Array */ if (_s_parse_array_value (n->value_str, n->type, ua_idx, &array_buf, &array_size) != 0) { UA_LOG_ERROR (UA_Log_Stdout, UA_LOGCATEGORY_SERVER, "NodesConfig: failed to parse array value " "for node '%s'", n->name); failures++; continue; } UA_Variant_setArray (&attr.value, array_buf, array_size, dt); attr.valueRank = UA_VALUERANK_ONE_DIMENSION; array_dim = (UA_UInt32)array_size; attr.arrayDimensions = &array_dim; attr.arrayDimensionsSize = 1; value_ok = true; } if (!value_ok) { failures++; continue; } UA_NodeId node_id = UA_NODEID_STRING (1, n->name); UA_QualifiedName browse_name = UA_QUALIFIEDNAME (1, n->name); UA_StatusCode rv = UA_Server_addVariableNode ( server, node_id, UA_NODEID_NUMERIC (0, UA_NS0ID_OBJECTSFOLDER), UA_NODEID_NUMERIC (0, UA_NS0ID_ORGANIZES), browse_name, UA_NODEID_NUMERIC (0, UA_NS0ID_BASEDATAVARIABLETYPE), attr, NULL, NULL); if (rv != UA_STATUSCODE_GOOD) { UA_LOG_ERROR (UA_Log_Stdout, UA_LOGCATEGORY_SERVER, "NodesConfig: failed to add node '%s': %s", n->name, UA_StatusCode_name (rv)); failures++; } else { UA_LOG_INFO (UA_Log_Stdout, UA_LOGCATEGORY_SERVER, "NodesConfig: added node '%s' " "(ns=1;s=%s, %s%s, %s)", n->name, n->name, _s_type_map[n->type].name, n->is_array ? "[]" : "", n->access == NODE_ACCESS_READ ? "read" : "readwrite"); } /* Clean up parsed values. UA_Server_addVariableNode deep-copies everything, so our local buffers can be freed now. */ if (n->type == NODE_TYPE_STRING && !n->is_array) UA_String_clear (&string_buf); if (array_buf) UA_Array_delete (array_buf, array_size, dt); } return failures > 0 ? -1 : 0; } /* ======================================================================== * Cleanup * ======================================================================== */ void nodes_config_free (nodes_config *nc) { for (size_t i = 0; i < nc->count; i++) { free (nc->nodes[i].name); free (nc->nodes[i].description); free (nc->nodes[i].value_str); } free (nc->nodes); memset (nc, 0, sizeof (*nc)); }