/** * @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 /* ======================================================================== * 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 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; } /* ======================================================================== * 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); } 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, "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 (!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 * ======================================================================== */ /** Default number of elements for array-typed nodes. */ #define NODES_DEFAULT_ARRAY_SIZE 5 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; /* Initialize with zero/default values. The caller should call nodes_config_randomize() afterwards to assign random values. */ void *array_buf = NULL; UA_UInt32 array_dim = 0; if (!n->is_array) { 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; } zero = { 0 }; if (n->type == NODE_TYPE_STRING) { UA_String empty_str = UA_STRING (""); UA_Variant_setScalarCopy (&attr.value, &empty_str, dt); } else { UA_Variant_setScalar (&attr.value, &zero, dt); } attr.valueRank = UA_VALUERANK_SCALAR; } else { array_buf = UA_Array_new (NODES_DEFAULT_ARRAY_SIZE, dt); if (!array_buf) { UA_LOG_ERROR (UA_Log_Stdout, UA_LOGCATEGORY_SERVER, "NodesConfig: out of memory for node '%s'", n->name); failures++; continue; } UA_Variant_setArray (&attr.value, array_buf, NODES_DEFAULT_ARRAY_SIZE, dt); attr.valueRank = UA_VALUERANK_ONE_DIMENSION; array_dim = (UA_UInt32)NODES_DEFAULT_ARRAY_SIZE; attr.arrayDimensions = &array_dim; attr.arrayDimensionsSize = 1; } 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"); } /* UA_Server_addVariableNode deep-copies everything. */ if (n->type == NODE_TYPE_STRING && !n->is_array) UA_Variant_clear (&attr.value); if (array_buf) UA_Array_delete (array_buf, NODES_DEFAULT_ARRAY_SIZE, dt); } return failures > 0 ? -1 : 0; } /* ======================================================================== * Random Value Generation: nodes_config_randomize * ======================================================================== */ /** * @brief Fills a buffer with a random value for the given node type. * * For non-string types the value is written into @p out. * For NODE_TYPE_STRING a heap-allocated UA_String is written into * @p out_str (the caller must UA_String_clear it). */ static void _s_random_scalar (node_type type, void *out, UA_String *out_str) { switch (type) { case NODE_TYPE_BOOL: *(UA_Boolean *)out = (UA_Boolean)(rand () % 2); break; case NODE_TYPE_INT16: *(UA_Int16 *)out = (UA_Int16)(rand () % 201 - 100); break; case NODE_TYPE_UINT16: *(UA_UInt16 *)out = (UA_UInt16)(rand () % 1000); break; case NODE_TYPE_INT32: *(UA_Int32 *)out = (UA_Int32)(rand () % 2001 - 1000); break; case NODE_TYPE_UINT32: *(UA_UInt32 *)out = (UA_UInt32)(rand () % 10000); break; case NODE_TYPE_INT64: *(UA_Int64 *)out = (UA_Int64)(rand () % 2001 - 1000); break; case NODE_TYPE_UINT64: *(UA_UInt64 *)out = (UA_UInt64)(rand () % 10000); break; case NODE_TYPE_FLOAT: *(UA_Float *)out = (UA_Float)rand () / (UA_Float)RAND_MAX * 100.0f; break; case NODE_TYPE_DOUBLE: *(UA_Double *)out = (UA_Double)rand () / (UA_Double)RAND_MAX * 100.0; break; case NODE_TYPE_STRING: { char buf[32]; snprintf (buf, sizeof (buf), "str_%d", rand () % 10000); *out_str = UA_String_fromChars (buf); break; } } } int nodes_config_randomize (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) { failures++; continue; } const UA_DataType *dt = &UA_TYPES[ua_idx]; UA_NodeId node_id = UA_NODEID_STRING (1, n->name); UA_Variant value; UA_Variant_init (&value); if (!n->is_array) { 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; } buf; UA_String str_buf = UA_STRING_NULL; _s_random_scalar (n->type, &buf, &str_buf); if (n->type == NODE_TYPE_STRING) { UA_Variant_setScalarCopy (&value, &str_buf, dt); UA_String_clear (&str_buf); } else { UA_Variant_setScalarCopy (&value, &buf, dt); } } else { void *array = UA_Array_new (NODES_DEFAULT_ARRAY_SIZE, dt); if (!array) { failures++; continue; } for (size_t j = 0; j < NODES_DEFAULT_ARRAY_SIZE; j++) { void *elem = (char *)array + j * dt->memSize; UA_String str_buf = UA_STRING_NULL; _s_random_scalar (n->type, elem, &str_buf); if (n->type == NODE_TYPE_STRING) *(UA_String *)elem = str_buf; } UA_Variant_setArrayCopy (&value, array, NODES_DEFAULT_ARRAY_SIZE, dt); UA_Array_delete (array, NODES_DEFAULT_ARRAY_SIZE, dt); } UA_StatusCode rv = UA_Server_writeValue (server, node_id, value); UA_Variant_clear (&value); if (rv != UA_STATUSCODE_GOOD) failures++; } 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); memset (nc, 0, sizeof (*nc)); }