diff --git a/CMakeLists.txt b/CMakeLists.txt index a4ea90fb1..c5975887d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -149,6 +149,7 @@ set(libsrc src/parser_yang.c src/parser_yin.c src/printer_schema.c + src/printer_sid.c src/printer_yang.c src/printer_yin.c src/printer_tree.c diff --git a/modules/ietf-sid-file@2024-07-31.yang b/modules/ietf-sid-file@2024-07-31.yang new file mode 100644 index 000000000..42bfd483f --- /dev/null +++ b/modules/ietf-sid-file@2024-07-31.yang @@ -0,0 +1,364 @@ +module ietf-sid-file { + yang-version 1.1; + namespace "urn:ietf:params:xml:ns:yang:ietf-sid-file"; + prefix sid; + + import ietf-yang-types { + prefix yang; + reference + "RFC 6991: Common YANG Data Types"; + } + import ietf-yang-structure-ext { + prefix sx; + reference + "RFC 8791: YANG Data Structure Extensions"; + } + + organization + "IETF CORE Working Group"; + + contact + "WG Web: + + WG List: + + Editor: Michel Veillette + + + Editor: Andy Bierman + + + Editor: Alexander Pelov + + + Editor: Ivaylo Petrov + "; + + description + "This module defines the structure of the '.sid' files. + + Each '.sid' file contains the mapping between each + string identifier defined by a YANG module and a + corresponding numeric value called a YANG SID. + + The key words 'MUST', 'MUST NOT', 'REQUIRED', 'SHALL', 'SHALL + NOT', 'SHOULD', 'SHOULD NOT', 'RECOMMENDED', 'NOT RECOMMENDED', + 'MAY', and 'OPTIONAL' in this document are to be interpreted as + described in BCP 14 (RFC 2119) (RFC 8174) when, and only when, + they appear in all capitals, as shown here. + + Copyright (c) 2024 IETF Trust and the persons identified as + authors of the code. All rights reserved. + + Redistribution and use in source and binary forms, with or + without modification, is permitted pursuant to, and subject to + the license terms contained in, the Revised BSD License set + forth in Section 4.c of the IETF Trust's Legal Provisions + Relating to IETF Documents + (https://trustee.ietf.org/license-info). + + This version of this YANG module is part of RFC 9595; see + the RFC itself for full legal notices."; + + revision 2024-07-31 { + description + "Initial revision."; + reference + "RFC 9595: YANG Schema Item iDentifier (YANG SID)"; + } + + typedef revision-identifier { + type string { + pattern '[0-9]{4}-[0-9]{2}-[0-9]{2}'; + } + description + "Represents a date in YYYY-MM-DD format."; + } + + typedef sid-file-version-identifier { + type uint32; + description + "Represents the version of a '.sid' file."; + } + + typedef sid { + type uint64 { + range "0..9223372036854775807"; + } + description + "YANG Schema Item iDentifier."; + reference + "RFC 9595: YANG Schema Item iDentifier (YANG SID)"; + } + + typedef schema-node-path { + type string { + pattern + '/[a-zA-Z_][a-zA-Z0-9\-_.]*:[a-zA-Z_][a-zA-Z0-9\-_.]*' + + '(/[a-zA-Z_][a-zA-Z0-9\-_.]*(:[a-zA-Z_][a-zA-Z0-9\-_.]*)?)*'; + } + description + "A schema-node path is an absolute YANG schema-node + identifier as defined by the YANG ABNF rule + 'absolute-schema-nodeid', except that module names are used + instead of prefixes. + + This string additionally follows the following rules: + + - The leftmost (top-level) data node name is always in the + namespace-qualified form. + - Any subsequent schema-node name is in the + namespace-qualified form if the node is defined in a + module other than its parent node. Otherwise, the + simple form is used. No predicates are allowed."; + reference + "RFC 5234 (STD 68): Augmented BNF for Syntax Specifications: + ABNF + RFC 7950: The YANG 1.1 Data Modeling Language, + Section 6.5: Schema Node Identifier"; + } + + sx:structure sid-file { + uses sid-file-contents; + } + + grouping sid-file { + description + "A grouping that contains a YANG container + representing the file structure of a '.sid' file."; + + container sid-file { + description + "A wrapper container that together with the 'sx:structure' + extension marks the YANG data structures inside as not + being intended to be implemented as part of a + configuration datastore or as an operational state within + the server."; + uses sid-file-contents; + } + } + + grouping sid-file-contents { + description + "A grouping that defines the contents of a container that + represents the file structure of a '.sid' file."; + + leaf module-name { + type yang:yang-identifier; + mandatory true; + description + "Name of the YANG module associated with this + '.sid' file."; + } + + leaf module-revision { + type revision-identifier; + description + "Revision of the YANG module associated with this '.sid' + file. + This leaf is not present if no revision statement is + defined in the YANG module."; + } + + leaf sid-file-version { + type sid-file-version-identifier; + default 0; + description + "Optional leaf that specifies the version number of the + '.sid' file. '.sid' files and the version sequence are + specific to a given YANG module revision. This number + starts at zero when there is a new YANG module revision + and increases monotonically. This number can distinguish + updates to the '.sid' file - for instance, as the result + of new processing or reported errata."; + } + + leaf sid-file-status { + type enumeration { + enum unpublished { + description + "This '.sid' file is unpublished (BCP 216) and is + also called a work-in-progress or workfile. + This may be when it accompanies an unpublished YANG + module or when only the '.sid' file itself is + unpublished. + The 'item' list MAY contain entries with a status + value of 'unstable'."; + reference + "RFC 8407 (BCP 216): Guidelines for Authors and + Reviewers of Documents Containing + YANG Data Models"; + } + enum published { + description + "This '.sid' file is published. This status + applies to '.sid' files for published YANG modules. + The 'item' list MUST NOT contain entries with a + status value of 'unstable'."; + } + } + default "published"; + description + "Optional leaf that specifies the status of the + '.sid' file."; + } + + leaf description { + type string; + description + "Free-form meta-information about the generated file. It + might include a '.sid' file generation tool and time, + among other things."; + } + + list dependency-revision { + key "module-name"; + + description + "Information about the revision used during the + '.sid' file generation of each dependency, i.e., each + YANG module that the YANG module associated with this + '.sid' file imported."; + + leaf module-name { + type yang:yang-identifier; + description + "YANG module name of this dependency."; + } + leaf module-revision { + type revision-identifier; + mandatory true; + description + "Revision of the YANG module of this dependency."; + } + } + + list assignment-range { + key "entry-point"; + description + "YANG-SID Range(s) allocated to the YANG module + identified by 'module-name' and 'module-revision'. + + - The first available value in the YANG-SID Range is + 'entry-point', and the last available value in the + range is ('entry-point' + size - 1). + - The YANG-SID Ranges specified by all + 'assignment-range' entries MUST NOT overlap."; + + leaf entry-point { + type sid; + description + "Lowest YANG SID available for assignment."; + } + + leaf size { + type uint64; + mandatory true; + description + "Number of YANG SIDs available for assignment."; + } + } + + list item { + key "namespace identifier"; + unique "sid"; + + description + "Each entry within this list defines the mapping between + a YANG item string identifier and a YANG SID. This list + MUST include a mapping entry for each YANG item defined + by the YANG module identified by 'module-name' and + 'module-revision'."; + + leaf status { + type enumeration { + enum stable { + value 0; + description + "This SID allocation has been published as the + stable allocation for the given namespace and + identifier."; + } + enum unstable { + value 1; + description + "This SID allocation has been done during a + development process; it is not yet stable."; + } + enum obsolete { + value 2; + description + "This SID allocation is no longer in use. It is + recorded to avoid reallocation of its SID value."; + } + } + default "stable"; + description + "The status field contains information about the + stability of the allocation. For each specific SID + value, over time it can only transition from + 'unstable' to 'stable', and possibly from 'stable' to + 'obsolete'."; + } + + leaf namespace { + type enumeration { + enum module { + value 0; + description + "All module and submodule names share the same + global module identifier namespace."; + } + enum identity { + value 1; + description + "All identity names defined in a module and its + submodules share the same identity identifier + namespace."; + } + enum feature { + value 2; + description + "All feature names defined in a module and its + submodules share the same feature identifier + namespace."; + } + enum data { + value 3; + description + "The namespace for all data nodes, as defined in + YANG."; + } + } + description + "Namespace of the YANG item for this mapping entry."; + } + + leaf identifier { + type union { + type yang:yang-identifier; + type schema-node-path; + } + description + "String identifier of the YANG item for this mapping + entry. + + If the corresponding 'namespace' field is 'module', + 'feature', or 'identity', then this field MUST + contain a valid YANG identifier string. + + If the corresponding 'namespace' field is 'data', + then this field MUST contain a valid schema-node + path."; + } + + leaf sid { + type sid; + mandatory true; + description + "YANG SID assigned to the YANG item for this mapping + entry."; + } + } + } +} diff --git a/src/printer_sid.c b/src/printer_sid.c new file mode 100644 index 000000000..39da1e434 --- /dev/null +++ b/src/printer_sid.c @@ -0,0 +1,975 @@ +/** + * @file printer_sid.c + * @author Petr Hanzlik + * @brief Generation and update of .sid files (RFC 9595). + * + * Copyright (c) 2026 CESNET, z.s.p.o. + * + * This source code is licensed under BSD 3-Clause License (the "License"). + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://opensource.org/BSD-3-Clause + */ + +#define _GNU_SOURCE + +#include +#include +#include +#include +#include + +#include "compat.h" +#include "context.h" +#include "log.h" +#include "ly_common.h" +#include "tree_data.h" +#include "tree_schema.h" +#include "tree_schema_internal.h" +#include "version.h" + +/** + * @brief Internal representation of a single SID item before it is written + * into the ietf-sid-file data tree. + * + * Ownership of the strings depends on the producer and must be respected by every cleanup path; the type itself does not express it. + */ +struct sid_item { + char *ns; /**< Namespace string: "module", "identity", "feature", or "data". */ + char *ident; /**< Identifier: module/submodule name, identity/feature name, or absolute schema-node path. */ + uint64_t sid; /**< SID assigned to the item (0 until assigned). */ + char *status; /**< Item status: "unstable"/"obsolete", or NULL. */ +}; + +/** + * @brief Collection state for gathering SID items, also passed as data to the DFS callback collecting "data" namespace items. + */ +struct sid_collect_data { + struct sid_item *items; /**< Items array being filled. */ + uint64_t count; /**< Number of items collected so far; exceeding @p max, surplus items are only counted. */ + uint64_t max; /**< Maximum number of items (capacity of the array). */ + struct ly_ctx *ctx; /**< libyang context used for error logging. */ +}; + +/** + * @brief Assignment range of an .sid file. + */ +struct sid_range { + uint64_t entry_point; /**< First SID of the range. */ + uint64_t size; /**< Number of SIDs in the range. */ +}; + +/** + * @brief Comparator for qsort that sorts SID items. + * + * Sort order alphabetical: + * 1. Namespace descending. + * 2. Identifier ascending. + * + * @param[in] a First item (struct sid_item *). + * @param[in] b Second item (struct sid_item *). + * @return <0 if @p a comes before @p b, 0 if equal, >0 if @p a comes after @p b. + */ +static int +compare_sid(const void *a, const void *b) +{ + const struct sid_item *item_a = a, *item_b = b; + char ns_a, ns_b; + + ns_a = item_a->ns[0]; + ns_b = item_b->ns[0]; + + if (ns_a == ns_b) { + return strcmp(item_a->ident, item_b->ident); + } + + return ns_b - ns_a; +} + +/** + * @brief Add one collected item to the collection array. + * + * @param[in,out] collect Collection state (items/count/max, ctx used for error logging). + * @param[in] ns Namespace string: "module", "identity", "feature", or "data". + * @param[in] name Identifier string to duplicate into the item. + * @return LY_SUCCESS on success (also if the capacity is exceeded, the item is then only counted and the caller reports the overflow after full collection). + * @return LY_EMEM on memory allocation failure. + */ +static LY_ERR +sid_item_add(struct sid_collect_data *collect, const char *ns, const char *name) +{ + /* overflow is only counted here; the exact number of excess items is + * reported by the caller after the whole schema has been collected */ + if (collect->count >= collect->max) { + collect->count++; + return LY_SUCCESS; + } + + collect->items[collect->count].ns = (char *)ns; + collect->items[collect->count].ident = strdup(name); + LY_CHECK_ERR_RET(!collect->items[collect->count].ident, LOGMEM(collect->ctx), LY_EMEM); + + collect->count++; + return LY_SUCCESS; +} + +/** + * @brief Build the schema-node-path identifier of a compiled node as required by RFC 9595: + * like ::lysc_path with ::LYSC_PATH_LOG, but choice and case node names are omitted + * (they never get a SID and never appear in identifiers), while input and output are kept. + * + * @param[in] node Node to build the identifier for. + * @return Newly allocated path, the caller frees it. + * @return NULL on memory allocation failure. + */ +static char * +sid_node_path(const struct lysc_node *node) +{ + const struct lysc_node *iter, *par; + char *path = NULL; + int len = 0; + + for (iter = node; iter && (len >= 0); iter = iter->parent) { + char *s; + + if (iter->nodetype & (LYS_CHOICE | LYS_CASE)) { + /* never part of a SID identifier, but children must be reached */ + continue; + } + + /* nearest non-choice/case ancestor, for the module prefix decision */ + for (par = iter->parent; par && (par->nodetype & (LYS_CHOICE | LYS_CASE)); par = par->parent) {} + + s = path; + if (!par || (par->module != iter->module)) { + /* module prefix (top-level node or module change through an augment) */ + len = asprintf(&path, "/%s:%s%s", iter->module->name, iter->name, s ? s : ""); + } else { + len = asprintf(&path, "/%s%s", iter->name, s ? s : ""); + } + free(s); + } + + if (len < 0) { + free(path); + return NULL; + } + return path; +} + +/** + * @brief Get the top-level schema node of a compiled extension instance that defines its own + * data tree outside the standard module trees (rc:yang-data and sx:structure). + * + * @param[in] ext Compiled extension instance to examine. + * @return The top node of the extension-defined data tree, or NULL if not applicable. + */ +static struct lysc_node * +sid_ext_top(const struct lysc_ext_instance *ext) +{ + if (!ext->compiled) { + return NULL; + } + + if (!strcmp(ext->def->name, "yang-data") && !strcmp(ext->def->module->name, "ietf-restconf")) { + /* yangdata.c compiles the template directly as the single real top container */ + return (struct lysc_node *)ext->compiled; + } + if (!strcmp(ext->def->name, "structure") && !strcmp(ext->def->module->name, "ietf-yang-structure-ext")) { + /* structure.c: struct lysc_ext_instance_structure stores the synthetic top container + as its first member (top_cont), so read it via the first member pointer */ + return *(struct lysc_node **)ext->compiled; + } + return NULL; +} + +/** + * @brief DFS callback for ::lysc_module_dfs_full that collects each schema node as a "data" namespace item. + * + * @param[in] node Current schema node. + * @param[in,out] data Pointer to ::sid_collect_data. + * @param[out] dfs_continue Unused, the whole tree is always traversed. + * @return LY_SUCCESS on success. + * @return LY_EMEM on allocation failure. + */ +static LY_ERR +collect_data_cb(struct lysc_node *node, void *data, ly_bool *UNUSED(dfs_continue)) +{ + struct sid_collect_data *collect_data = data; + LY_ERR rc; + char *path; + + if (node->nodetype & (LYS_CHOICE | LYS_CASE)) { + return LY_SUCCESS; /* choice/case get no item, but their subtree is still traversed */ + } + + path = sid_node_path(node); + LY_CHECK_ERR_RET(!path, LOGMEM(collect_data->ctx), LY_EMEM); + + rc = sid_item_add(collect_data, "data", path); + free(path); + return rc; +} + +/** + * @brief Add assignment range to the sid-file data tree. + * + * @param[in] tree Sid-file root to add the range to. + * @param[in] entry_point First SID of the range. + * @param[in] size Number of SIDs in the range. + * @return LY_SUCCESS on success. + * @return LY_EMEM on memory allocation failure. + * @return LY_ERR on other errors. + */ +static LY_ERR +sid_range_add(struct lyd_node *tree, uint64_t entry_point, uint64_t size) +{ + LY_ERR rc = LY_SUCCESS; + char size_str[21]; + char *item_path = NULL; + + /* the whole new range must fit inside the SID data type bound 2^63-1; */ + LY_CHECK_ERR_GOTO(size - 1 > (((uint64_t)1 << 63) - 1) - entry_point, + (LOGERR(LYD_CTX(tree), LY_EINVAL, + "The assignment range [%" PRIu64 ", %" PRIu64 "] exceeds the SID data type bounds.", + entry_point, entry_point + size - 1), rc = LY_EINVAL), cleanup); + + snprintf(size_str, sizeof size_str, "%" PRIu64, size); + + LY_CHECK_ERR_GOTO(asprintf(&item_path, "assignment-range[entry-point='%" PRIu64 "']/size", entry_point) == -1, + rc = LY_EMEM, cleanup); + LY_CHECK_GOTO((rc = lyd_new_path(tree, NULL, item_path, size_str, 0, NULL)), cleanup); + +cleanup: + free(item_path); + return rc; +} + +/** + * @brief Build the .sid file skeleton: root structure, module identity, status, description, dependency-revision list, and assignment-range list. + * + * @param[in] module Module used for module-name, module-revision, and dependency-revision entries. + * @param[in] ranges Array of assignment ranges. + * @param[in] range_count Number of elements in @p ranges. + * @param[in] version Value of the sid-file-version leaf, 0 to omit it + * @param[in] status Status of the SID file; for ::LYS_SID_FILE_PUBLISHED the "sid-file-status" leaf is + * omitted (its schema default is "published"). + * @param[in] description Optional description string, NULL to auto-generate. + * @param[out] tree Generated /ietf-sid-file:sid-file node, NULL if creation failed. + * @return LY_SUCCESS on success. + * @return LY_EMEM on memory allocation failure. + * @return LY_ERR on other errors. + */ +static LY_ERR +sid_build_skeleton(const struct lys_module *module, const struct sid_range *ranges, size_t range_count, + uint32_t version, LYS_SID_FILE_STATUS status, const char *description, struct lyd_node **tree) +{ + LY_ERR rc = LY_SUCCESS; + const struct lysp_import *imports; + char ver_str[11]; + char *item_path = NULL, *desc_buf = NULL, *time_str = NULL; + time_t now = time(NULL); + LY_ARRAY_COUNT_TYPE iter; + + *tree = NULL; + + LY_CHECK_GOTO((rc = lyd_new_path(NULL, module->ctx, "/ietf-sid-file:sid-file", NULL, 0, tree)), cleanup); + + LY_CHECK_GOTO((rc = lyd_new_path(*tree, NULL, "module-name", module->name, 0, NULL)), cleanup); + + if (module->revision) { + LY_CHECK_GOTO((rc = lyd_new_path(*tree, NULL, "module-revision", module->revision, 0, NULL)), cleanup); + } + + if (version) { + snprintf(ver_str, sizeof ver_str, "%" PRIu32, version); + LY_CHECK_GOTO((rc = lyd_new_path(*tree, NULL, "sid-file-version", ver_str, 0, NULL)), cleanup); + } + + if (status == LYS_SID_FILE_UNPUBLISHED) { + LY_CHECK_GOTO((rc = lyd_new_path(*tree, NULL, "sid-file-status", "unpublished", 0, NULL)), cleanup); + } + + if (!description) { + /* auto-generate with libyang version and UTC timestamp */ + LY_CHECK_GOTO((rc = ly_time_time2str(now, NULL, &time_str)), cleanup); + LY_CHECK_ERR_GOTO(asprintf(&desc_buf, "Generated by libyang %s, at %s", ly_version_so_str(), time_str) == -1, + rc = LY_EMEM, cleanup); + description = desc_buf; + } + LY_CHECK_GOTO((rc = lyd_new_path(*tree, NULL, "description", description, 0, NULL)), cleanup); + free(desc_buf); + desc_buf = NULL; + + /* dependency-revision: the ietf-sid-file model requires the module revision here, + but the 'revision' statement is optional in YANG and some modules simply do + not have one, so it can happen that an import has no revision to record and the entry is skipped. */ + imports = module->parsed->imports; + LY_ARRAY_FOR(imports, iter) { + if (imports[iter].module->revision) { + LY_CHECK_ERR_GOTO(asprintf(&item_path, "dependency-revision[module-name='%s']/module-revision", + imports[iter].name) == -1, rc = LY_EMEM, cleanup); + LY_CHECK_GOTO((rc = lyd_new_path(*tree, NULL, item_path, imports[iter].module->revision, 0, NULL)), cleanup); + free(item_path); + item_path = NULL; + } + } + + /* One .sid file may legally declare several assignment ranges. */ + for (size_t i = 0; i < range_count; i++) { + LY_CHECK_GOTO((rc = sid_range_add(*tree, ranges[i].entry_point, ranges[i].size)), cleanup); + } + +cleanup: + if (rc) { + lyd_free_all(*tree); + *tree = NULL; + } + free(item_path); + free(desc_buf); + free(time_str); + return rc; +} + +/** + * @brief Collect all SID items of a module ("module", "identity", "feature", and "data" namespaces) and sort them. + * + * @param[in,out] callback_data Collection state: preallocated items array, capacity in max, ctx set for error logging. + * @param[in] module Compiled module to collect the items from. + * @return LY_SUCCESS on success, the sorted items and their count are in @p callback_data. + * @return LY_EINVAL if the capacity (the assignment range size) is too small for all collected items. + * @return LY_EMEM on memory allocation failure. + * @return LY_ERR on other errors. + */ +static LY_ERR +sid_collect_items(struct sid_collect_data *callback_data, const struct lys_module *module) +{ + LY_ERR rc; + LY_ARRAY_COUNT_TYPE i; + uint32_t feature_idx = 0; + const struct lysp_feature *feature = NULL; + + /* module namespace: the module name itself (RFC 9595) */ + LY_CHECK_RET((rc = sid_item_add(callback_data, "module", module->name)), rc); + + /* module namespace: each submodule name (RFC 9595) */ + LY_ARRAY_FOR(module->submodules, i) { + LY_CHECK_RET((rc = sid_item_add(callback_data, "module", module->submodules[i].name)), rc); + } + + /* identity namespace: all identities defined in the module and its submodules. */ + LY_ARRAY_FOR(module->identities, i) { + LY_CHECK_RET((rc = sid_item_add(callback_data, "identity", module->identities[i].name)), rc); + } + + /* feature namespace: all features defined in the module and its submodules.*/ + while ((feature = lysp_feature_next(feature, module->parsed, &feature_idx))) { + LY_CHECK_RET((rc = sid_item_add(callback_data, "feature", feature->name)), rc); + } + + /* data namespace: walk the entire compiled schema tree depth-first. + lysc_module_dfs_full traverses all nodes including RPCs, actions, + notifications, input, output, choice and case nodes. sid_node_path() builds the + RFC 9595 schema-node-path identifiers (choice/case names omitted). */ + LY_CHECK_RET((rc = lysc_module_dfs_full(module, collect_data_cb, callback_data)), rc); + + /* data namespace: also traverse the compiled trees of rc:yang-data and sx:structure + top-level extension instances (submodule instances are compiled into the main module); + these subtrees are not part of the standard module trees. */ + LY_ARRAY_FOR(module->compiled->exts, i) { + struct lysc_node *top = sid_ext_top(&module->compiled->exts[i]); + + if (top) { + LY_CHECK_RET((rc = lysc_tree_dfs_full(top, collect_data_cb, callback_data)), rc); + } + } + + /* the range must cover all collected items; the excess is counted by sid_item_add */ + LY_CHECK_RET(callback_data->count > callback_data->max, (LOGERR(module->ctx, LY_EINVAL, ".sid assignment range(s) size %" PRIu64 " is too small, %" PRIu64 " extra SID(s) are required.", + callback_data->max, callback_data->count - callback_data->max), LY_EINVAL)); + + qsort(callback_data->items, callback_data->count, sizeof(struct sid_item), compare_sid); + + return LY_SUCCESS; +} + +/** + * @brief Free the collected items array including all owned identifiers. + * + * @param[in,out] callback_data Collection state; its items array is freed and set to NULL. + */ +static void +sid_collect_data_free(struct sid_collect_data *callback_data) +{ + if (!callback_data || !callback_data->items) { + return; + } + + /* The count may exceed the maximum capacity when the assignment range was too small. */ + for (uint64_t i = 0; (i < callback_data->count) && (i < callback_data->max); i++) { + free(callback_data->items[i].ident); + } + free(callback_data->items); + callback_data->items = NULL; +} + +/** + * @brief Emit an item entry into the sid-file data tree. + * + * @param[in] item Item to add to sid-file. + * @param[in, out] tree Sid-file root to add the item to. + * @return LY_SUCCESS on success. + * @return LY_EMEM on memory allocation failure. + * @return LY_ERR on other errors. + */ +static LY_ERR +sid_emit_item(const struct sid_item *item, struct lyd_node *tree) +{ + LY_ERR rc = LY_SUCCESS; + char sid_str[21]; + char *item_path = NULL; + + snprintf(sid_str, sizeof sid_str, "%" PRIu64, item->sid); + LY_CHECK_ERR_GOTO(asprintf(&item_path, "item[namespace='%s'][identifier='%s']/sid", item->ns, item->ident) == -1, + rc = LY_EMEM, cleanup); + LY_CHECK_GOTO((rc = lyd_new_path(tree, NULL, item_path, sid_str, 0, NULL)), cleanup); + free(item_path); + item_path = NULL; + + if (item->status) { + LY_CHECK_ERR_GOTO(asprintf(&item_path, "item[namespace='%s'][identifier='%s']/status", item->ns, item->ident) == -1, + rc = LY_EMEM, cleanup); + LY_CHECK_GOTO((rc = lyd_new_path(tree, NULL, item_path, item->status, 0, NULL)), cleanup); + free(item_path); + item_path = NULL; + } + +cleanup: + free(item_path); + return rc; +} + +LY_ERR +sid_file_gen(const struct lys_module *module, uint64_t entry_point, uint64_t size, + LYS_SID_FILE_STATUS status, const char *description, struct lyd_node **sid_file) +{ + LY_ERR rc = LY_SUCCESS; + struct lyd_node *tree = NULL; + struct sid_item *items = NULL; + struct sid_collect_data cb_data = {0}; + struct sid_range range = {entry_point, size}; + uint64_t sid; + + /* --- Phase 1: build the sid-file skeleton --- */ + LY_CHECK_GOTO((rc = sid_build_skeleton(module, &range, 1, 0, status, description, &tree)), cleanup); + + /* --- Phase 2: collect items from the compiled schema and sort --- */ + items = calloc(size, sizeof *items); + LY_CHECK_ERR_GOTO(!items, rc = LY_EMEM, cleanup); + + cb_data.items = items; + cb_data.max = size; + cb_data.ctx = module->ctx; + + LY_CHECK_GOTO((rc = sid_collect_items(&cb_data, module)), cleanup); + + /* --- Phase 3: assign SIDs and build the item list --- */ + + /* assign sequential SIDs in sorted order */ + sid = entry_point; + for (uint64_t i = 0; i < cb_data.count; i++) { + items[i].sid = sid++; + items[i].status = (status == LYS_SID_FILE_UNPUBLISHED) ? (char *)"unstable" : NULL; + + LY_CHECK_GOTO((rc = sid_emit_item(&items[i], tree)), cleanup); + } + + *sid_file = tree; + tree = NULL; + +cleanup: + lyd_free_all(tree); + sid_collect_data_free(&cb_data); + return rc; +} + +/** + * @brief One item extracted from a previous .sid file. + * + * All strings inside @p item are OWNED by this array entry (strdup'd during + * extraction) and freed per entry by ::sid_old_items_free. + */ +struct sid_old_item { + struct sid_item item; /**< Namespace/identifier/SID/status from the previous file. */ + ly_bool matched; /**< true once a current schema item claims this entry during merge. + * An obsolete item matched back into the schema keeps its SID and + * is revived: its status is cleared to stable (NULL) at merge time. */ +}; + +/** + * @brief Comparator for qsort that sorts previous items the same way as ::compare_sid + * + * Sort order alphabetical: + * 1. Namespace descending. + * 2. Identifier ascending. + * + * @param[in] a First item (struct sid_old_item *). + * @param[in] b Second item (struct sid_old_item *). + * @return <0 if @p a comes before @p b, 0 if equal, >0 if @p a comes after @p b. + */ +static int +compare_old(const void *a, const void *b) +{ + const struct sid_old_item *item_a = a, *item_b = b; + + return compare_sid(&item_a->item, &item_b->item); +} + +/** + * @brief Look up an item by (namespace, identifier) in the sorted array of + * previous items. + * + * The array must be sorted with ::compare_old and must not contain duplicate + * (namespace, identifier) keys - when the previous file has duplicates, the + * first occurrence is kept during extraction and the rest is dropped. + * + * @param[in] olds Sorted array of previous items. + * @param[in] count Number of elements in @p olds. + * @param[in] ns Namespace of the item. + * @param[in] ident Identifier of the item. + * @return Pointer to the matching item. + * @return NULL if not present. + */ +static struct sid_old_item * +sid_old_find(const struct sid_old_item *olds, uint64_t count, const char *ns, const char *ident) +{ + uint64_t lo = 0, hi = count, mid; + int cmp; + + while (lo < hi) { + mid = lo + (hi - lo) / 2; + if (olds[mid].item.ns[0] == ns[0]) { + cmp = strcmp(ident, olds[mid].item.ident); + } else { + cmp = olds[mid].item.ns[0] - ns[0]; + } + if (!cmp) { + return (struct sid_old_item *)&olds[mid]; + } else if (cmp < 0) { + hi = mid; + } else { + lo = mid + 1; + } + } + return NULL; +} + +/** + * @brief Free an array of previous items including all owned strings. + * + * @param[in,out] olds Array to free, set to NULL afterwards. + * @param[in] count Number of elements in @p olds. + */ +static void +sid_old_items_free(struct sid_old_item **olds, uint64_t count) +{ + if (*olds) { + for (uint64_t i = 0; i < count; i++) { + free((*olds)[i].item.ns); + free((*olds)[i].item.ident); + free((*olds)[i].item.status); + } + free(*olds); + *olds = NULL; + } +} + +/** + * @brief Get the canonical string value of a direct leaf child of a data node. + * + * @param[in] node Parent data node (sid-file root or a list instance). + * @param[in] name Schema name of the leaf child. + * @return Canonical value (borrowed, do not free). + * @return NULL if the leaf is missing. + */ +static const char * +sid_child_value(const struct lyd_node *node, const char *name) +{ + const struct lyd_node *child; + + LY_LIST_FOR(lyd_child(node), child) { + if (!strcmp(child->schema->name, name)) { + return lyd_get_value(child); + } + } + return NULL; +} + +/** + * @brief Find a specific child terminal node (leaf or leaf-list) by schema name. + * + * @param[in] node Parent data node whose children will be searched. + * @param[in] name Schema identifier (name) of the target child node to match. + * + * @return Pointer to the matched terminal node (`struct lyd_node_term *`). + * @return NULL if no matching child is found. + */ +static struct lyd_node_term * +sid_child_term(const struct lyd_node *node, const char *name) +{ + const struct lyd_node *child; + + LY_LIST_FOR(lyd_child(node), child) { + if (!strcmp(child->schema->name, name)) { + return (struct lyd_node_term *)child; + } + } + return NULL; +} + +LY_ERR +sid_range_append(struct lyd_node *tree, uint64_t entry_point, uint64_t size) +{ + LY_ERR rc = LY_SUCCESS; + const struct lyd_node *child = NULL; + uint64_t ep = 0, sz = 0; + struct lyd_node_term *term = NULL; + + LY_CHECK_ERR_GOTO(!tree->schema || strcmp(tree->schema->name, "sid-file"), + (LOGERR(LYD_CTX(tree), LY_EINVAL, + "Invalid previous .sid file data tree (expected the \"sid-file\" top node)."), + rc = LY_EINVAL), cleanup); + + /* the new range must not overlap any already declared assignment-range */ + for (child = lyd_child(tree); child; child = child->next) { + if (strcmp(child->schema->name, "assignment-range")) { + continue; + } + term = sid_child_term(child, "entry-point"); + LY_CHECK_ERR_GOTO(!term, + (LOGERR(LYD_CTX(tree), LY_EINVAL, "Missing required data in the previous .sid file."), rc = LY_EINVAL), cleanup); + ep = term->value.uint64; + term = sid_child_term(child, "size"); + LY_CHECK_ERR_GOTO(!term, + (LOGERR(LYD_CTX(tree), LY_EINVAL, "Missing required data in the previous .sid file."), rc = LY_EINVAL), cleanup); + sz = term->value.uint64; + + /* unsigned wrap-around makes a single subtraction sufficient per direction */ + LY_CHECK_ERR_GOTO((ep - entry_point < size) || (entry_point - ep < sz), + (LOGERR(LYD_CTX(tree), LY_EINVAL, + "The assignment range [%" PRIu64 ", %" PRIu64 "] overlaps the existing assignment range [%" PRIu64 ", %" PRIu64 "].", + entry_point, entry_point + size - 1, ep, ep + sz - 1), rc = LY_EINVAL), cleanup); + } + + rc = sid_range_add(tree, entry_point, size); + +cleanup: + return rc; +} + +/** + * @brief Extract the data needed to update an existing .sid file. + * + * @param[in] module Module the file belongs to; its name must match the file. + * @param[in] prev Root node of the previous .sid data tree. + * @param[out] ranges Newly allocated assignment ranges from the file. + * @param[out] ranges_count Number of elements in @p ranges. + * @param[out] version sid-file-version from the file, 0 if the leaf is absent. + * @param[out] items Newly allocated items with dense entries (duplicate keys excluded), in file order; free with ::sid_old_items_free. + * @param[out] items_count Number of used entries in @p items. + * @return LY_SUCCESS on success. + * @return LY_EINVAL on invalid file content (name mismatch, missing data, no ranges). + * @return LY_EMEM on memory allocation failure. + * @return LY_ERR on other errors. + */ +static LY_ERR +sid_extract_file(const struct lys_module *module, const struct lyd_node *prev, struct sid_range **ranges, + size_t *ranges_count, uint32_t *version, struct sid_old_item **items, uint64_t *items_count) +{ + LY_ERR rc = LY_SUCCESS; + const struct lyd_node *child = NULL; + struct sid_range *extracted_ranges = NULL; + struct sid_old_item *extracted_items = NULL; + size_t range_idx = 0, total_ranges = 0; + uint64_t item_idx = 0, total_items = 0, sid_value; + const char *name = NULL, *ns = NULL, *ident = NULL, *status = NULL; + char *ns_copy = NULL, *ident_copy = NULL, *status_copy = NULL; + struct lyd_node_term *term = NULL; + ly_bool duplicate = 0; + + /* the leaf is optional with default 0 in the schema */ + *version = 0; + + /* module identity, version, counts of both lists */ + LY_LIST_FOR(lyd_child(prev), child) { + if (!child->schema) { + continue; + } + if (!strcmp(child->schema->name, "module-name")) { + name = lyd_get_value(child); + LY_CHECK_ERR_GOTO(strcmp(name, module->name), + (LOGERR(module->ctx, LY_EINVAL, "The .sid file module name \"%s\" does not match the " + "module \"%s\".", name, module->name), rc = LY_EINVAL), cleanup); + } else if (!strcmp(child->schema->name, "sid-file-version")) { + *version = ((const struct lyd_node_term *)child)->value.uint32; + } else if (!strcmp(child->schema->name, "assignment-range")) { + total_ranges++; + } else if (!strcmp(child->schema->name, "item")) { + total_items++; + } + } + + LY_CHECK_ERR_GOTO(!total_ranges, + (LOGERR(module->ctx, LY_EINVAL, "The .sid file \"%s\" has no assignment-range.", + module->name), rc = LY_EINVAL), cleanup); + + extracted_ranges = calloc(total_ranges, sizeof *extracted_ranges); + LY_CHECK_ERR_GOTO(!extracted_ranges, (LOGMEM(module->ctx), rc = LY_EMEM), cleanup); + if (total_items) { + extracted_items = calloc(total_items, sizeof *extracted_items); + LY_CHECK_ERR_GOTO(!extracted_items, (LOGMEM(module->ctx), rc = LY_EMEM), cleanup); + } + + /* fill ranges and items */ + LY_LIST_FOR(lyd_child(prev), child) { + if (!child->schema) { + continue; + } + + if (!strcmp(child->schema->name, "assignment-range")) { + term = sid_child_term(child, "entry-point"); + LY_CHECK_ERR_GOTO(!term, + (LOGERR(module->ctx, LY_EINVAL, "Missing required data in the previous .sid file."), + rc = LY_EINVAL), cleanup); + extracted_ranges[range_idx].entry_point = term->value.uint64; + + term = sid_child_term(child, "size"); + LY_CHECK_ERR_GOTO(!term, + (LOGERR(module->ctx, LY_EINVAL, "Missing required data in the previous .sid file."), + rc = LY_EINVAL), cleanup); + extracted_ranges[range_idx].size = term->value.uint64; + range_idx++; + + } else if (!strcmp(child->schema->name, "item")) { + ns = sid_child_value(child, "namespace"); + ident = sid_child_value(child, "identifier"); + term = sid_child_term(child, "sid"); + LY_CHECK_ERR_GOTO(!ns || !ident || !term, + (LOGERR(module->ctx, LY_EINVAL, "Missing required data in the previous .sid file."), + rc = LY_EINVAL), cleanup); + sid_value = term->value.uint64; + + /* duplicate keys: the first occurrence wins */ + duplicate = 0; + for (uint64_t i = 0; i < item_idx; i++) { + if (!strcmp(extracted_items[i].item.ns, ns) && !strcmp(extracted_items[i].item.ident, ident)) { + duplicate = 1; + break; + } + } + if (duplicate) { + continue; + } + + status = sid_child_value(child, "status"); + + ns_copy = strdup(ns); + LY_CHECK_ERR_GOTO(!ns_copy, (LOGMEM(module->ctx), rc = LY_EMEM), cleanup); + ident_copy = strdup(ident); + LY_CHECK_ERR_GOTO(!ident_copy, (free(ns_copy), LOGMEM(module->ctx), rc = LY_EMEM), cleanup); + /* "stable" is the schema default and is stored as NULL */ + status_copy = (status && strcmp(status, "stable")) ? strdup(status) : NULL; + LY_CHECK_ERR_GOTO(status && strcmp(status, "stable") && !status_copy, + (free(ns_copy), free(ident_copy), LOGMEM(module->ctx), rc = LY_EMEM), cleanup); + + extracted_items[item_idx].item.ns = ns_copy; + extracted_items[item_idx].item.ident = ident_copy; + extracted_items[item_idx].item.sid = sid_value; + extracted_items[item_idx].item.status = status_copy; + extracted_items[item_idx].matched = 0; + item_idx++; + } + } + + *ranges = extracted_ranges; + *ranges_count = total_ranges; + *items = extracted_items; + *items_count = item_idx; + return LY_SUCCESS; + +cleanup: + free(extracted_ranges); + sid_old_items_free(&extracted_items, item_idx); + return rc; +} + +/** + * @brief Comparator for qsort that sorts assignment ranges by entry-point ascending. + * + * @param[in] a First range (struct sid_range *). + * @param[in] b Second range (struct sid_range *). + * @return <0 if @p a comes before @p b, 0 if equal, >0 if @p a comes after @p b. + */ +static int +compare_range(const void *a, const void *b) +{ + const struct sid_range *ra = a, *rb = b; + + if (ra->entry_point == rb->entry_point) { + return 0; + } + return (ra->entry_point > rb->entry_point) ? 1 : -1; +} + +LY_ERR +sid_file_update(const struct lys_module *module, const struct lyd_node *prev_sid_file, + LYS_SID_FILE_STATUS status, const char *description, struct lyd_node **sid_file) +{ + LY_ERR rc = LY_SUCCESS; + struct sid_range *ranges = NULL; + struct sid_old_item *olds = NULL, *match = NULL; + struct sid_item *items = NULL, *merged = NULL; + struct sid_collect_data cb_data = {0}; + uint64_t *next_free = NULL; + size_t ranges_count = 0, alloc_range_idx = 0; + uint64_t old_count = 0, capacity = 0, merged_count = 0, extra = 0; + uint32_t version = 0; + ly_bool in_range = 0; + struct lyd_node *tree = NULL; + + /* --- step 1: validation and extraction from the previous file --- */ + LY_CHECK_ERR_GOTO(!prev_sid_file->schema || strcmp(prev_sid_file->schema->name, "sid-file"), + (LOGERR(module->ctx, LY_EINVAL, + "Invalid previous .sid file data tree (expected the \"sid-file\" top node)."), + rc = LY_EINVAL), cleanup); + + LY_CHECK_GOTO((rc = sid_extract_file(module, prev_sid_file, + &ranges, &ranges_count, &version, &olds, &old_count)), cleanup); + + /* assignment order: ranges ascending by entry-point */ + qsort(ranges, ranges_count, sizeof *ranges, compare_range); + + /* every old SID must lie inside some declared range; track the highest used SID per range so new items start right after it */ + next_free = calloc(ranges_count, sizeof *next_free); + LY_CHECK_ERR_GOTO(!next_free, (LOGMEM(module->ctx), rc = LY_EMEM), cleanup); + for (size_t j = 0; j < ranges_count; j++) { + next_free[j] = ranges[j].entry_point; + } + for (uint64_t i = 0; i < old_count; i++) { + in_range = 0; + for (size_t j = 0; j < ranges_count; j++) { + if ((olds[i].item.sid >= ranges[j].entry_point) && (olds[i].item.sid - ranges[j].entry_point < ranges[j].size)) { + in_range = 1; + if (olds[i].item.sid >= next_free[j]) { + next_free[j] = olds[i].item.sid + 1; + } + break; + } + } + LY_CHECK_ERR_GOTO(!in_range, + (LOGERR(module->ctx, LY_EINVAL, + "The previous .sid file assigns SID %" PRIu64 " (\"%s\"), which is " + "outside of any assignment range.", olds[i].item.sid, olds[i].item.ident), + rc = LY_EINVAL), cleanup); + } + + /* --- step 2: collect current schema items --- */ + for (size_t i = 0; i < ranges_count; i++) { + capacity += ranges[i].size; + } + items = calloc(capacity, sizeof *items); + LY_CHECK_ERR_GOTO(!items, (LOGMEM(module->ctx), rc = LY_EMEM), cleanup); + + cb_data.items = items; + cb_data.max = capacity; + cb_data.ctx = module->ctx; + + LY_CHECK_GOTO((rc = sid_collect_items(&cb_data, module)), cleanup); + + /* --- step 3: merge collected items with previous items --- */ + + /* sort olds by (namespace, identifier) for binary lookup */ + qsort(olds, old_count, sizeof *olds, compare_old); + + /* merged array holds: matched items (old SID), new items (free SID), and obsolete items */ + merged = calloc(cb_data.count + old_count, sizeof *merged); + LY_CHECK_ERR_GOTO(!merged, (LOGMEM(module->ctx), rc = LY_EMEM), cleanup); + + for (uint64_t i = 0; i < cb_data.count; i++) { + match = sid_old_find(olds, old_count, cb_data.items[i].ns, cb_data.items[i].ident); + if (match) { + match->matched = 1; + merged[merged_count].ns = cb_data.items[i].ns; + merged[merged_count].ident = cb_data.items[i].ident; + merged[merged_count].sid = match->item.sid; + /* preserve old status; obsolete-matched is revived to stable (NULL) */ + merged[merged_count].status = (match->item.status && strcmp(match->item.status, "obsolete")) ? + match->item.status : NULL; + } else { + /* new item: first SID after the highest used one, walking ranges forward */ + for ( ;; ) { + if (alloc_range_idx >= ranges_count) { + /* count the remaining items (this one included) that still need a new SID */ + for (uint64_t j = i; j < cb_data.count; j++) { + if (!sid_old_find(olds, old_count, cb_data.items[j].ns, cb_data.items[j].ident)) { + extra++; + } + } + LOGERR(module->ctx, LY_EINVAL, + ".sid assignment range(s) size %" PRIu64 " is too small, %" PRIu64 " extra SID(s) are required.", + capacity, extra); + rc = LY_EINVAL; + goto cleanup; + } + if (next_free[alloc_range_idx] - ranges[alloc_range_idx].entry_point < + ranges[alloc_range_idx].size) { + break; + } + alloc_range_idx++; + } + merged[merged_count].ns = cb_data.items[i].ns; + merged[merged_count].ident = cb_data.items[i].ident; + merged[merged_count].sid = next_free[alloc_range_idx]++; + merged[merged_count].status = (status == LYS_SID_FILE_UNPUBLISHED) ? "unstable" : NULL; + } + merged_count++; + } + + /* unmatched old items are kept as obsolete (RFC 9595: MUST NOT be removed) */ + for (uint64_t i = 0; i < old_count; i++) { + if (!olds[i].matched) { + merged[merged_count] = olds[i].item; + merged[merged_count].status = (char *)"obsolete"; + merged_count++; + } + } + + /* --- step 4: build the updated .sid file data tree --- */ + + LY_CHECK_GOTO((rc = sid_build_skeleton(module, ranges, ranges_count, version + 1, + status, description, &tree)), cleanup); + + /* published files do not contain unstable items (finalize) */ + for (uint64_t i = 0; i < merged_count; i++) { + if (status == LYS_SID_FILE_PUBLISHED) { + if (merged[i].status && !strcmp(merged[i].status, "unstable")) { + merged[i].status = NULL; + } + } + + LY_CHECK_GOTO((rc = sid_emit_item(&merged[i], tree)), cleanup); + } + + *sid_file = tree; + tree = NULL; + +cleanup: + lyd_free_all(tree); + free(merged); + free(next_free); + free(ranges); + sid_old_items_free(&olds, old_count); + sid_collect_data_free(&cb_data); + return rc; +} diff --git a/src/tree_schema.c b/src/tree_schema.c index 6d242d09b..78576479c 100644 --- a/src/tree_schema.c +++ b/src/tree_schema.c @@ -3168,3 +3168,36 @@ lys_search_localfile(const char * const *searchpaths, ly_bool cwd, const char *n return ret; } + +LIBYANG_API_DEF LY_ERR +lys_sid_gen(const struct lys_module *module, uint64_t entry_point, uint64_t size, + LYS_SID_FILE_STATUS status, const char *description, struct lyd_node **sid_file) +{ + LY_CHECK_ARG_RET(NULL, module, sid_file, LY_EINVAL); + LY_CHECK_ARG_RET(NULL, module->compiled, module->parsed, size, LY_EINVAL); + LY_CHECK_ARG_RET(NULL, ly_ctx_get_module_implemented(module->ctx, "ietf-sid-file"), LY_ENOTFOUND); + + *sid_file = NULL; + return sid_file_gen(module, entry_point, size, status, description, sid_file); +} + +LIBYANG_API_DEF LY_ERR +lys_sid_update(const struct lys_module *module, const struct lyd_node *prev_sid_file, + LYS_SID_FILE_STATUS status, const char *description, struct lyd_node **sid_file) +{ + LY_CHECK_ARG_RET(NULL, module, prev_sid_file, sid_file, LY_EINVAL); + LY_CHECK_ARG_RET(NULL, module->compiled, module->parsed, LY_EINVAL); + LY_CHECK_ARG_RET(NULL, ly_ctx_get_module_implemented(module->ctx, "ietf-sid-file"), LY_ENOTFOUND); + + *sid_file = NULL; + return sid_file_update(module, prev_sid_file, status, description, sid_file); +} + +LIBYANG_API_DEF LY_ERR +lys_sid_range_add(struct lyd_node *sid_file, uint64_t entry_point, uint64_t size) +{ + LY_CHECK_ARG_RET(NULL, sid_file, LY_EINVAL); + LY_CHECK_ARG_RET(NULL, size, LY_EINVAL); + + return sid_range_append(sid_file, entry_point, size); +} diff --git a/src/tree_schema.h b/src/tree_schema.h index 8e1f18b9d..07ff9e46f 100644 --- a/src/tree_schema.h +++ b/src/tree_schema.h @@ -2340,6 +2340,86 @@ LIBYANG_API_DECL const char *lys_nodetype2str(uint16_t nodetype); */ LIBYANG_API_DECL const char *lyxp_get_expr(const struct lyxp_expr *path); +/** + * @brief Status of a generated .sid file, mirroring the 'sid-file-status' leaf + * from ietf-sid-file (RFC 9595, sec. 4). + */ +typedef enum { + LYS_SID_FILE_UNPUBLISHED = 0, /**< work-in-progress file, items have status 'unstable' */ + LYS_SID_FILE_PUBLISHED = 1 /**< stable file, does not contain 'unstable' items */ +} LYS_SID_FILE_STATUS; + +/** + * @brief Generate a .sid file content of a module as an ietf-sid-file data tree. + * + * @note For complete SID coverage, all features of @p module must be enabled + * before calling this function. Schema nodes guarded by `if-feature` that are + * not enabled are absent from the compiled tree and will not be collected. + * + * @param[in] module Implemented module to collect items from. Its context must have the "ietf-sid-file" module implemented. + * @param[in] entry_point First SID of the assignment range. + * @param[in] size Number of SIDs available in the range, must cover all items. + * @param[in] status Status of the SID file (::LYS_SID_FILE_UNPUBLISHED or ::LYS_SID_FILE_PUBLISHED). + * When ::LYS_SID_FILE_UNPUBLISHED, every item gets status "unstable"; a published file + * contains no "unstable" items. + * @param[in] description Optional description string written to the "description" leaf, NULL to auto-generate + * "Generated by libyang \, at \". + * @param[out] sid_file Generated data tree ("/ietf-sid-file:sid-file"), caller must free with ::lyd_free_all(). + * @return LY_SUCCESS on success. + * @return LY_EINVAL if @p module is not implemented, @p size is zero or smaller than the number of collected items, @p module or @p sid_file is NULL. + * @return LY_ENOTFOUND if "ietf-sid-file" is not implemented in the module's context. + * @return LY_ERR on other errors. + */ +LIBYANG_API_DECL LY_ERR lys_sid_gen(const struct lys_module *module, uint64_t entry_point, uint64_t size, + LYS_SID_FILE_STATUS status, const char *description, struct lyd_node **sid_file); + +/** + * @brief Update a .sid file content of a module from a previous .sid file. + * + * Merges items from the previous .sid file with items collected from the current + * compiled schema. Items that still exist keep their previously assigned SIDs and + * statuses; new items get the first SID after the highest used SID in each + * assignment range; items that no longer exist in the schema are retained with + * status "obsolete" (RFC 9595 The sid-file-version is incremented by 1. + * + * @note For complete SID coverage, all features of @p module must be enabled + * before calling this function. Schema nodes guarded by `if-feature` that are + * not enabled are absent from the compiled tree and will not be collected. + * + * @param[in] module Implemented module to collect items from. Its context must have the "ietf-sid-file" module implemented. + * @param[in] prev_sid_file Root node of the previous .sid file data tree (parsed from JSON with ::lyd_parse_data_mem). + * @param[in] status Status of the SID file (::LYS_SID_FILE_UNPUBLISHED or ::LYS_SID_FILE_PUBLISHED). + * When ::LYS_SID_FILE_PUBLISHED, all "unstable" item statuses are stripped (set to the default "stable"); + * "obsolete" statuses are always preserved. + * @param[in] description Optional description string written to the "description" leaf, NULL to auto-generate + * "Generated by libyang \, at \". + * @param[out] sid_file Generated data tree ("/ietf-sid-file:sid-file"), caller must free with ::lyd_free_all(). + * @return LY_SUCCESS on success. + * @return LY_EINVAL if @p module is not implemented, or on invalid previous file content (module name mismatch, SID outside any range, no free SID left), + * or @p module, @p prev_sid_file, or @p sid_file is NULL. + * @return LY_ENOTFOUND if "ietf-sid-file" is not implemented in the module's context. + * @return LY_ERR on other errors. + */ +LIBYANG_API_DECL LY_ERR lys_sid_update(const struct lys_module *module, const struct lyd_node *prev_sid_file, + LYS_SID_FILE_STATUS status, const char *description, struct lyd_node **sid_file); + +/** + * @brief Append a new assignment range to an existing .sid file data tree. + * + * The range is appended directly into @p sid_file (in place). + * + * @param[in] sid_file Data tree ("/ietf-sid-file:sid-file") to add the range to. + * @param[in] entry_point First SID of the new range. + * @param[in] size Number of SIDs in the new range, must be non-zero. + * @return LY_SUCCESS on success. + * @return LY_EINVAL if @p sid_file is NULL. + * @return LY_EINVAL if @p size is zero, @p sid_file does not have the sid-file + * root node, the range exceeds the SID data type bounds, the existing ranges + * miss required data, or the range overlaps an existing one. + * @return LY_ERR on other errors. + */ +LIBYANG_API_DECL LY_ERR lys_sid_range_add(struct lyd_node *sid_file, uint64_t entry_point, uint64_t size); + /** @} schematree */ #ifdef __cplusplus diff --git a/src/tree_schema_internal.h b/src/tree_schema_internal.h index 75f4303e8..e36855a0d 100644 --- a/src/tree_schema_internal.h +++ b/src/tree_schema_internal.h @@ -894,4 +894,49 @@ void *ly_ctx_compiled_addr_ht_get(const struct ly_ht *addr_ht, const void *addr, */ LY_ERR ly_ctx_compiled_addr_ht_add(struct ly_ht *addr_ht, const void *orig_addr, const void *addr); +/** + * @brief Generate a .sid file data tree. Internal implementation of ::lys_sid_gen, expects arguments already validated. + * + * @param[in] module Implemented module to collect items from. Its context must have the "ietf-sid-file" module implemented. + * @param[in] entry_point First SID of the assignment range. + * @param[in] size Number of SIDs available in the range, must cover all items. + * @param[in] status Status of the SID file (::LYS_SID_FILE_UNPUBLISHED or ::LYS_SID_FILE_PUBLISHED). + * @param[in] description Optional description string written to the "description" leaf, NULL to auto-generate + * "Generated by libyang \, at \". + * @param[out] sid_file Generated data tree ("/ietf-sid-file:sid-file"), caller must free with ::lyd_free_all(). + * @return LY_SUCCESS on success. + * @return LY_EINVAL if @p size is smaller than the number of collected items. + * @return LY_ERR on other errors. + */ +LY_ERR sid_file_gen(const struct lys_module *module, uint64_t entry_point, uint64_t size, + LYS_SID_FILE_STATUS status, const char *description, struct lyd_node **sid_file); + +/** + * @brief Update a .sid file data tree. Internal implementation of ::lys_sid_update, expects arguments already validated. + * + * @param[in] module Implemented module to collect items from. Its context must have the "ietf-sid-file" module implemented. + * @param[in] prev_sid_file Root node of the previous .sid file data tree. + * @param[in] status Status of the SID file (::LYS_SID_FILE_UNPUBLISHED or ::LYS_SID_FILE_PUBLISHED). + * @param[in] description Optional description string written to the "description" leaf, NULL to auto-generate + * "Generated by libyang \, at \". + * @param[out] sid_file Generated data tree ("/ietf-sid-file:sid-file"), caller must free with ::lyd_free_all(). + * @return LY_SUCCESS on success. + * @return LY_EINVAL on invalid previous file or if the assignment ranges cannot cover all items. + * @return LY_ERR on other errors. + */ +LY_ERR sid_file_update(const struct lys_module *module, const struct lyd_node *prev_sid_file, + LYS_SID_FILE_STATUS status, const char *description, struct lyd_node **sid_file); + +/** + * @brief Append a new assignment range to a .sid file data tree. Internal implementation of ::lys_sid_range_add, expects arguments already validated. + * + * @param[in] tree Data tree ("/ietf-sid-file:sid-file") to add the range to. + * @param[in] entry_point First SID of the new range. + * @param[in] size Number of SIDs in the new range. + * @return LY_SUCCESS on success. + * @return LY_EINVAL if @p tree is not the sid-file root node, the range exceeds the SID data type bound (2^63-1), or overlaps an already declared range. + * @return LY_ERR on other errors. + */ +LY_ERR sid_range_append(struct lyd_node *tree, uint64_t entry_point, uint64_t size); + #endif /* LY_TREE_SCHEMA_INTERNAL_H_ */ diff --git a/tests/utests/CMakeLists.txt b/tests/utests/CMakeLists.txt index a77d60d75..70ba43293 100644 --- a/tests/utests/CMakeLists.txt +++ b/tests/utests/CMakeLists.txt @@ -59,6 +59,7 @@ ly_add_utest(NAME yang SOURCES schema/test_yang.c) ly_add_utest(NAME yin SOURCES schema/test_yin.c) ly_add_utest(NAME tree_schema_compile SOURCES schema/test_tree_schema_compile.c) ly_add_utest(NAME printer_tree SOURCES schema/test_printer_tree.c) +ly_add_utest(NAME printer_sid SOURCES schema/test_printer_sid.c) ly_add_utest(NAME tree_data SOURCES data/test_tree_data.c) ly_add_utest(NAME tree_data_sorted SOURCES data/test_tree_data_sorted.c) diff --git a/tests/utests/schema/test_printer_sid.c b/tests/utests/schema/test_printer_sid.c new file mode 100644 index 000000000..b35bd8abb --- /dev/null +++ b/tests/utests/schema/test_printer_sid.c @@ -0,0 +1,465 @@ +/** + * @file test_printer_sid.c + * @author: Petr Hanzlik + * @brief unit tests for functions from printer_sid.c + * + * Copyright (c) 2026 CESNET, z.s.p.o. + * + * This source code is licensed under BSD 3-Clause License (the "License"). + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://opensource.org/licenses/BSD-3-Clause + */ +#define _UTEST_MAIN_ +#include "utests.h" + +#include "context.h" +#include "tree_data.h" +#include "tree_schema.h" + +/* 3 SID items: module/a01, /a01:cont, /a01:cont/l */ +static const char *mod_v1 = + "module a01 {\n" + " yang-version 1.1;\n" + " namespace \"urn:a01\";\n" + " prefix a01;\n" + " revision 2024-01-01;\n" + " container cont {\n" + " leaf l {\n" + " type string;\n" + " }\n" + " }\n" + "}\n"; + +/* 5 SID items: v1 plus /a01:cont/l2 and /a01:cont/l3 */ +static const char *mod_v2 = + "module a01 {\n" + " yang-version 1.1;\n" + " namespace \"urn:a01\";\n" + " prefix a01;\n" + " revision 2024-02-02;\n" + " container cont {\n" + " leaf l {\n" + " type string;\n" + " }\n" + " leaf l2 {\n" + " type string;\n" + " }\n" + " leaf l3 {\n" + " type string;\n" + " }\n" + " }\n" + "}\n"; + +/* 4 SID items: compared to v1 drops /a01:cont/l and adds /a01:cont/m and /a01:cont/n */ +static const char *mod_v3 = + "module a01 {\n" + " yang-version 1.1;\n" + " namespace \"urn:a01\";\n" + " prefix a01;\n" + " revision 2024-03-03;\n" + " container cont {\n" + " leaf m {\n" + " type string;\n" + " }\n" + " leaf n {\n" + " type string;\n" + " }\n" + " }\n" + "}\n"; + +/* 7 SID items: compared to v3 keeps only /a01:cont and adds 5 new leaves; + the dropped items /a01:cont/l, /a01:cont/m and /a01:cont/n become obsolete + and keep holding SIDs 102, 200 and 300 */ +static const char *mod_v4 = + "module a01 {\n" + " yang-version 1.1;\n" + " namespace \"urn:a01\";\n" + " prefix a01;\n" + " revision 2024-04-04;\n" + " container cont {\n" + " leaf p1 {\n" + " type string;\n" + " }\n" + " leaf p2 {\n" + " type string;\n" + " }\n" + " leaf p3 {\n" + " type string;\n" + " }\n" + " leaf p4 {\n" + " type string;\n" + " }\n" + " leaf p5 {\n" + " type string;\n" + " }\n" + " }\n" + "}\n"; + +/** + * @brief Compare the compact JSON print of a data tree with an expected string. + */ +static void +check_json_tree(const struct lyd_node *tree, const char *expected) +{ + char *buffer = NULL; + + assert_int_equal(LY_SUCCESS, lyd_print_mem(&buffer, (struct lyd_node *)tree, LYD_JSON, LYD_PRINT_SHRINK)); + CHECK_STRING(buffer, expected); + free(buffer); +} + +/** + * @brief Create a context with the given module implemented and ietf-sid-file loaded. + */ +static void +new_sid_ctx(struct ly_ctx **ctx, const char *mod_str, struct lys_module **mod) +{ + struct ly_in *in; + + assert_int_equal(LY_SUCCESS, ly_ctx_new(TESTS_SRC "/../modules", LY_CTX_DISABLE_SEARCHDIR_CWD, ctx)); + assert_int_equal(LY_SUCCESS, ly_in_new_memory(mod_str, &in)); + assert_int_equal(LY_SUCCESS, lys_parse(*ctx, in, LYS_IN_YANG, NULL, mod)); + ly_in_free(in, 0); + assert_non_null(ly_ctx_load_module(*ctx, "ietf-sid-file", NULL, NULL)); +} + +/** + * @brief Check and clear the last error logged in @p ctx (CHECK_LOG_CTX for private contexts). + */ +static void +check_log_ctx(struct ly_ctx *ctx, const char *msg) +{ + struct ly_err_item *e = (struct ly_err_item *)ly_err_last(ctx); + + assert_non_null(e); + CHECK_STRING(e->msg, msg); + assert_null(e->data_path); + assert_null(e->schema_path); + assert_int_equal(e->line, 0); + ly_err_clean(ctx, e); +} + +/** + * @brief Test case: argument checks of the public API functions lys_sid_gen(), + * lys_sid_update() and lys_sid_range_add(). These fail in the API wrapper without + * logging any error into the context. + */ +static void +test_args(void **state) +{ + struct lys_module *mod; + struct lyd_node *sid_file = NULL, *updated = NULL; + + UTEST_ADD_MODULE(mod_v1, LYS_IN_YANG, NULL, &mod); + + /* lys_sid_gen */ + assert_int_equal(LY_EINVAL, lys_sid_gen(NULL, 100, 3, LYS_SID_FILE_UNPUBLISHED, "d", &sid_file)); + assert_int_equal(LY_EINVAL, lys_sid_gen(mod, 100, 0, LYS_SID_FILE_UNPUBLISHED, "d", &sid_file)); + assert_int_equal(LY_EINVAL, lys_sid_gen(mod, 100, 3, LYS_SID_FILE_UNPUBLISHED, "d", NULL)); + + /* the ietf-sid-file module is not implemented in the context yet */ + assert_int_equal(LY_ENOTFOUND, lys_sid_gen(mod, 100, 3, LYS_SID_FILE_UNPUBLISHED, "d", &sid_file)); + assert_non_null(ly_ctx_load_module(_UC->ctx, "ietf-sid-file", NULL, NULL)); + assert_int_equal(LY_SUCCESS, lys_sid_gen(mod, 100, 3, LYS_SID_FILE_UNPUBLISHED, "d", &sid_file)); + assert_non_null(sid_file); + + /* lys_sid_update */ + assert_int_equal(LY_EINVAL, lys_sid_update(NULL, sid_file, LYS_SID_FILE_UNPUBLISHED, "d", &updated)); + assert_int_equal(LY_EINVAL, lys_sid_update(mod, NULL, LYS_SID_FILE_UNPUBLISHED, "d", &updated)); + assert_int_equal(LY_EINVAL, lys_sid_update(mod, sid_file, LYS_SID_FILE_UNPUBLISHED, "d", NULL)); + + /* lys_sid_range_add */ + assert_int_equal(LY_EINVAL, lys_sid_range_add(NULL, 200, 1)); + assert_int_equal(LY_EINVAL, lys_sid_range_add(sid_file, 200, 0)); + + lyd_free_all(sid_file); +} + +/** + * @brief Test case: lys_sid_gen() fails on an undersized assignment range + * and generates the expected data tree with a sufficient one. + */ +static void +test_gen(void **state) +{ + struct lys_module *mod; + struct lyd_node *sid_file = NULL; + + UTEST_ADD_MODULE(mod_v1, LYS_IN_YANG, NULL, &mod); + assert_non_null(ly_ctx_load_module(_UC->ctx, "ietf-sid-file", NULL, NULL)); + + /* the module has 3 SID items but the range covers only 2 */ + assert_int_equal(LY_EINVAL, lys_sid_gen(mod, 100, 2, LYS_SID_FILE_UNPUBLISHED, "d", &sid_file)); + assert_null(sid_file); + CHECK_LOG_CTX(".sid assignment range(s) size 2 is too small, 1 extra SID(s) are required.", NULL, 0); + + /* exact range size */ + assert_int_equal(LY_SUCCESS, lys_sid_gen(mod, 100, 3, LYS_SID_FILE_UNPUBLISHED, "d", &sid_file)); + assert_non_null(sid_file); + check_json_tree(sid_file, + "{\"ietf-sid-file:sid-file\":{" + "\"module-name\":\"a01\"," + "\"module-revision\":\"2024-01-01\"," + "\"sid-file-status\":\"unpublished\"," + "\"description\":\"d\"," + "\"assignment-range\":[{\"entry-point\":\"100\",\"size\":\"3\"}]," + "\"item\":[" + "{\"namespace\":\"module\",\"identifier\":\"a01\",\"status\":\"unstable\",\"sid\":\"100\"}," + "{\"namespace\":\"data\",\"identifier\":\"/a01:cont\",\"status\":\"unstable\",\"sid\":\"101\"}," + "{\"namespace\":\"data\",\"identifier\":\"/a01:cont/l\",\"status\":\"unstable\",\"sid\":\"102\"}" + "]}}"); + + lyd_free_all(sid_file); +} + +/** + * @brief Test case: chained update scenario on a .sid file: undersized + * updates fail, assignment ranges are added (zero-size and overlapping rejected), + * an update with removed/added items succeeds and is finally republished as stable. + * A last update fails on range exhaustion: obsolete items keep holding their SIDs, + * so no free SID is left in any range even though the raw item count fits. + */ +static void +test_flow(void **state) +{ + struct lys_module *mod, *mod2, *mod3, *mod4; + struct ly_ctx *ctx2 = NULL, *ctx3 = NULL, *ctx4 = NULL; + struct lyd_node *prev = NULL, *updated = NULL, *published = NULL; + struct lyd_node *node; + char *json = NULL; + + /* --- stage with v1 --- + generate the initial .sid file with the range [100, 102] */ + UTEST_ADD_MODULE(mod_v1, LYS_IN_YANG, NULL, &mod); + assert_non_null(ly_ctx_load_module(_UC->ctx, "ietf-sid-file", NULL, NULL)); + assert_int_equal(LY_SUCCESS, lys_sid_gen(mod, 100, 3, LYS_SID_FILE_UNPUBLISHED, "d", &prev)); + assert_non_null(prev); + assert_int_equal(LY_SUCCESS, lyd_print_mem(&json, prev, LYD_JSON, LYD_PRINT_SHRINK)); + lyd_free_all(prev); + prev = NULL; + + /* --- stage with v2 --- */ + new_sid_ctx(&ctx2, mod_v2, &mod2); + assert_int_equal(LY_SUCCESS, lyd_parse_data_mem(ctx2, json, LYD_JSON, 0, LYD_VALIDATE_PRESENT, &prev)); + free(json); + json = NULL; + + /* v2 has 5 items but the ranges cover only 3 */ + assert_int_equal(LY_EINVAL, lys_sid_update(mod2, prev, LYS_SID_FILE_UNPUBLISHED, "d", &updated)); + assert_null(updated); + check_log_ctx(ctx2, ".sid assignment range(s) size 3 is too small, 2 extra SID(s) are required."); + + /* overlapping range rejected */ + assert_int_equal(LY_EINVAL, lys_sid_range_add(prev, 102, 5)); + check_log_ctx(ctx2, "The assignment range [102, 106] overlaps the existing assignment range [100, 102]."); + + /* valid but still insufficient range is appended */ + assert_int_equal(LY_SUCCESS, lys_sid_range_add(prev, 200, 1)); + assert_int_equal(LY_SUCCESS, + lyd_find_path(prev, "/ietf-sid-file:sid-file/assignment-range[entry-point='100']/size", 0, &node)); + CHECK_STRING(lyd_get_value(node), "3"); + assert_int_equal(LY_SUCCESS, + lyd_find_path(prev, "/ietf-sid-file:sid-file/assignment-range[entry-point='200']/size", 0, &node)); + CHECK_STRING(lyd_get_value(node), "1"); + + /* total capacity 4 still does not cover the 5 items of v2 */ + assert_int_equal(LY_EINVAL, lys_sid_update(mod2, prev, LYS_SID_FILE_UNPUBLISHED, "d", &updated)); + assert_null(updated); + check_log_ctx(ctx2, ".sid assignment range(s) size 4 is too small, 1 extra SID(s) are required."); + + /* another range provides enough SIDs */ + assert_int_equal(LY_SUCCESS, lys_sid_range_add(prev, 300, 5)); + + /* carry the .sid file over to the next stage */ + assert_int_equal(LY_SUCCESS, lyd_print_mem(&json, prev, LYD_JSON, LYD_PRINT_SHRINK)); + lyd_free_all(prev); + prev = NULL; + ly_ctx_destroy(ctx2); + ctx2 = NULL; + + /* --- stage with v3 --- */ + new_sid_ctx(&ctx3, mod_v3, &mod3); + assert_int_equal(LY_SUCCESS, lyd_parse_data_mem(ctx3, json, LYD_JSON, 0, LYD_VALIDATE_PRESENT, &prev)); + free(json); + json = NULL; + + /* update to v3 - drops /a01:cont/l (kept as obsolete), adds /a01:cont/m and /a01:cont/n + which get the next free SIDs walking the ranges forward (200, then 300) */ + assert_int_equal(LY_SUCCESS, lys_sid_update(mod3, prev, LYS_SID_FILE_UNPUBLISHED, "d", &updated)); + assert_non_null(updated); + check_json_tree(updated, + "{\"ietf-sid-file:sid-file\":{" + "\"module-name\":\"a01\"," + "\"module-revision\":\"2024-03-03\"," + "\"sid-file-version\":1," + "\"sid-file-status\":\"unpublished\"," + "\"description\":\"d\"," + "\"assignment-range\":[" + "{\"entry-point\":\"100\",\"size\":\"3\"}," + "{\"entry-point\":\"200\",\"size\":\"1\"}," + "{\"entry-point\":\"300\",\"size\":\"5\"}]," + "\"item\":[" + "{\"namespace\":\"module\",\"identifier\":\"a01\",\"status\":\"unstable\",\"sid\":\"100\"}," + "{\"namespace\":\"data\",\"identifier\":\"/a01:cont\",\"status\":\"unstable\",\"sid\":\"101\"}," + "{\"namespace\":\"data\",\"identifier\":\"/a01:cont/l\",\"status\":\"obsolete\",\"sid\":\"102\"}," + "{\"namespace\":\"data\",\"identifier\":\"/a01:cont/m\",\"status\":\"unstable\",\"sid\":\"200\"}," + "{\"namespace\":\"data\",\"identifier\":\"/a01:cont/n\",\"status\":\"unstable\",\"sid\":\"300\"}" + "]}}"); + lyd_free_all(prev); + prev = NULL; + + /* republish the file as stable - the version is incremented, the sid-file-status leaf + and unstable item statuses disappear, the obsolete item is preserved */ + assert_int_equal(LY_SUCCESS, lys_sid_update(mod3, updated, LYS_SID_FILE_PUBLISHED, "d", &published)); + assert_non_null(published); + check_json_tree(published, + "{\"ietf-sid-file:sid-file\":{" + "\"module-name\":\"a01\"," + "\"module-revision\":\"2024-03-03\"," + "\"sid-file-version\":2," + "\"description\":\"d\"," + "\"assignment-range\":[" + "{\"entry-point\":\"100\",\"size\":\"3\"}," + "{\"entry-point\":\"200\",\"size\":\"1\"}," + "{\"entry-point\":\"300\",\"size\":\"5\"}]," + "\"item\":[" + "{\"namespace\":\"module\",\"identifier\":\"a01\",\"sid\":\"100\"}," + "{\"namespace\":\"data\",\"identifier\":\"/a01:cont\",\"sid\":\"101\"}," + "{\"namespace\":\"data\",\"identifier\":\"/a01:cont/l\",\"status\":\"obsolete\",\"sid\":\"102\"}," + "{\"namespace\":\"data\",\"identifier\":\"/a01:cont/m\",\"sid\":\"200\"}," + "{\"namespace\":\"data\",\"identifier\":\"/a01:cont/n\",\"sid\":\"300\"}" + "]}}"); + lyd_free_all(updated); + updated = NULL; + + /* --- stage with v4 --- */ + /* v4 has 7 items, so the raw size check (capacity 9) passes, but the obsolete + items l/m/n still lock SIDs 102/200/300 and only 301-304 are free - the 5th + new leaf finds no free SID in any assignment range */ + assert_int_equal(LY_SUCCESS, lyd_print_mem(&json, published, LYD_JSON, LYD_PRINT_SHRINK)); + lyd_free_all(published); + ly_ctx_destroy(ctx3); + + new_sid_ctx(&ctx4, mod_v4, &mod4); + assert_int_equal(LY_SUCCESS, lyd_parse_data_mem(ctx4, json, LYD_JSON, 0, LYD_VALIDATE_PRESENT, &prev)); + free(json); + json = NULL; + + assert_int_equal(LY_EINVAL, lys_sid_update(mod4, prev, LYS_SID_FILE_UNPUBLISHED, "d", &updated)); + assert_null(updated); + check_log_ctx(ctx4, ".sid assignment range(s) size 9 is too small, 1 extra SID(s) are required."); + + lyd_free_all(prev); + ly_ctx_destroy(ctx4); +} + +/** + * @brief Test case: SID items are also collected from the compiled trees of extension + * instances that define their own data trees - sx:structure (including contents of + * merged sx:augment-structure, the structure root itself gets a SID as the instance-data + * root member name) and rc:yang-data. + */ +static void +test_exts(void **state) +{ + struct lys_module *mod; + struct lyd_node *sid_file = NULL; + static const char *mod_struct = + "module s1 {\n" + " yang-version 1.1;\n" + " namespace \"urn:s1\";\n" + " prefix s1;\n" + " import ietf-yang-structure-ext { prefix sx; }\n" + " revision 2024-01-01;\n" + " container c {\n" + " leaf l { type string; }\n" + " }\n" + " sx:structure struct {\n" + " container x {\n" + " list l {\n" + " key \"id\";\n" + " leaf id { type uint32; }\n" + " leaf v { type string; }\n" + " }\n" + " }\n" + " }\n" + " sx:augment-structure \"/s1:struct/x\" {\n" + " leaf al { type string; }\n" + " }\n" + "}\n"; + static const char *mod_yangdata = + "module yd1 {\n" + " yang-version 1.1;\n" + " namespace \"urn:yd1\";\n" + " prefix yd1;\n" + " import ietf-restconf { revision-date 2017-01-26; prefix rc; }\n" + " revision 2024-01-01;\n" + " rc:yang-data template {\n" + " container y {\n" + " leaf l { type string; }\n" + " }\n" + " }\n" + "}\n"; + + /* sx:structure: 9 SID items, the root "struct" included, augments merged in */ + UTEST_ADD_MODULE(mod_struct, LYS_IN_YANG, NULL, &mod); + assert_non_null(ly_ctx_load_module(_UC->ctx, "ietf-sid-file", NULL, NULL)); + assert_int_equal(LY_SUCCESS, lys_sid_gen(mod, 100, 9, LYS_SID_FILE_UNPUBLISHED, "d", &sid_file)); + assert_non_null(sid_file); + check_json_tree(sid_file, + "{\"ietf-sid-file:sid-file\":{" + "\"module-name\":\"s1\"," + "\"module-revision\":\"2024-01-01\"," + "\"sid-file-status\":\"unpublished\"," + "\"description\":\"d\"," + "\"dependency-revision\":[{\"module-name\":\"ietf-yang-structure-ext\",\"module-revision\":\"2020-06-17\"}]," + "\"assignment-range\":[{\"entry-point\":\"100\",\"size\":\"9\"}]," + "\"item\":[" + "{\"namespace\":\"module\",\"identifier\":\"s1\",\"status\":\"unstable\",\"sid\":\"100\"}," + "{\"namespace\":\"data\",\"identifier\":\"/s1:c\",\"status\":\"unstable\",\"sid\":\"101\"}," + "{\"namespace\":\"data\",\"identifier\":\"/s1:c/l\",\"status\":\"unstable\",\"sid\":\"102\"}," + "{\"namespace\":\"data\",\"identifier\":\"/s1:struct\",\"status\":\"unstable\",\"sid\":\"103\"}," + "{\"namespace\":\"data\",\"identifier\":\"/s1:struct/x\",\"status\":\"unstable\",\"sid\":\"104\"}," + "{\"namespace\":\"data\",\"identifier\":\"/s1:struct/x/al\",\"status\":\"unstable\",\"sid\":\"105\"}," + "{\"namespace\":\"data\",\"identifier\":\"/s1:struct/x/l\",\"status\":\"unstable\",\"sid\":\"106\"}," + "{\"namespace\":\"data\",\"identifier\":\"/s1:struct/x/l/id\",\"status\":\"unstable\",\"sid\":\"107\"}," + "{\"namespace\":\"data\",\"identifier\":\"/s1:struct/x/l/v\",\"status\":\"unstable\",\"sid\":\"108\"}" + "]}}"); + lyd_free_all(sid_file); + sid_file = NULL; + + /* rc:yang-data: 3 SID items, the "template" name is not among them */ + assert_int_equal(LY_SUCCESS, ly_ctx_set_searchdir(_UC->ctx, TESTS_DIR_MODULES_YANG)); + UTEST_ADD_MODULE(mod_yangdata, LYS_IN_YANG, NULL, &mod); + assert_int_equal(LY_SUCCESS, lys_sid_gen(mod, 100, 3, LYS_SID_FILE_UNPUBLISHED, "d", &sid_file)); + assert_non_null(sid_file); + check_json_tree(sid_file, + "{\"ietf-sid-file:sid-file\":{" + "\"module-name\":\"yd1\"," + "\"module-revision\":\"2024-01-01\"," + "\"sid-file-status\":\"unpublished\"," + "\"description\":\"d\"," + "\"dependency-revision\":[{\"module-name\":\"ietf-restconf\",\"module-revision\":\"2017-01-26\"}]," + "\"assignment-range\":[{\"entry-point\":\"100\",\"size\":\"3\"}]," + "\"item\":[" + "{\"namespace\":\"module\",\"identifier\":\"yd1\",\"status\":\"unstable\",\"sid\":\"100\"}," + "{\"namespace\":\"data\",\"identifier\":\"/yd1:y\",\"status\":\"unstable\",\"sid\":\"101\"}," + "{\"namespace\":\"data\",\"identifier\":\"/yd1:y/l\",\"status\":\"unstable\",\"sid\":\"102\"}" + "]}}"); + lyd_free_all(sid_file); +} + +int +main(void) +{ + const struct CMUnitTest tests[] = { + UTEST(test_args), + UTEST(test_gen), + UTEST(test_flow), + UTEST(test_exts), + }; + + return cmocka_run_group_tests(tests, NULL, NULL); +} diff --git a/tests/yanglint/data/test.sid b/tests/yanglint/data/test.sid new file mode 100644 index 000000000..19f38524f --- /dev/null +++ b/tests/yanglint/data/test.sid @@ -0,0 +1,40 @@ +{ + "ietf-sid-file:sid-file": { + "module-name": "test-sid", + "module-revision": "2026-08-11", + "sid-file-status": "unpublished", + "description": "Generated by libyang 5.6.12, at 2026-08-17 08:37:22", + "assignment-range": [ + { + "entry-point": "1000", + "size": "20" + } + ], + "item": [ + { + "namespace": "module", + "identifier": "test-sid", + "status": "unstable", + "sid": "1000" + }, + { + "namespace": "data", + "identifier": "/test-sid:test-container", + "status": "unstable", + "sid": "1001" + }, + { + "namespace": "data", + "identifier": "/test-sid:test-container/test-leaf1", + "status": "unstable", + "sid": "1002" + }, + { + "namespace": "data", + "identifier": "/test-sid:test-container/test-leaf2", + "status": "unstable", + "sid": "1003" + } + ] + } +} diff --git a/tests/yanglint/interactive/sid.test b/tests/yanglint/interactive/sid.test new file mode 100644 index 000000000..92f7ab15a --- /dev/null +++ b/tests/yanglint/interactive/sid.test @@ -0,0 +1,171 @@ +source [expr {[info exists ::env(TESTS_DIR)] ? "$env(TESTS_DIR)/interactive/ly.tcl" : "ly.tcl"}] + +set mdir $::env(YANG_MODULES_DIR) +set ddir $::env(TESTS_DIR)/data + +test sid_generate {} { +-setup $ly_setup -cleanup $ly_cleanup -body { + set pattern "(?s)^" + append pattern {.*"module-name": "test-sid",} + append pattern {.*"module-revision": "2026-08-11",} + append pattern {.*"sid-file-status": "unpublished",} + append pattern {.*"description": "Generated by libyang } + append pattern {.*"entry-point": "1000",} + append pattern {.*"size": "20"} + append pattern {.*"identifier": "test-sid",} + append pattern {.*"status": "unstable",} + append pattern {.*"sid": "1000"} + append pattern {.*"identifier": "/test-sid:test-container",} + append pattern {.*"status": "unstable",} + append pattern {.*"sid": "1001"} + append pattern {.*"identifier": "/test-sid:test-container/test-leaf1",} + append pattern {.*"status": "unstable",} + append pattern {.*"sid": "1002"} + append pattern {.*"identifier": "/test-sid:test-container/test-leaf2",} + append pattern {.*"status": "unstable",} + append pattern {.*"sid": "1003"} + + ly_cmd "verb error" + ly_cmd "add $mdir/test-sid-v1.yang" + ly_cmd "sid generate 1000:20 test-sid" $pattern +}} + +test sid_generate_published {} { +-setup $ly_setup -cleanup $ly_cleanup -body { + set pattern "(?s)^" + append pattern {(?=.*"module-name": "test-sid",)} + append pattern {(?=.*"module-revision": "2026-08-11",)} + append pattern {(?=.*"entry-point": "1000",)} + append pattern {(?=.*"size": "20")} + append pattern {(?=.*"identifier": "test-sid",)} + append pattern {(?=.*"sid": "1000")} + append pattern {(?=.*"identifier": "/test-sid:test-container",)} + append pattern {(?=.*"sid": "1001")} + append pattern {(?=.*"identifier": "/test-sid:test-container/test-leaf1",)} + append pattern {(?=.*"sid": "1002")} + append pattern {(?=.*"identifier": "/test-sid:test-container/test-leaf2",)} + append pattern {(?=.*"sid": "1003")} + append pattern {(?!.*sid-file-status)} + append pattern {(?!.*"status")} + + ly_cmd "verb error" + ly_cmd "add $mdir/test-sid-v1.yang" + ly_cmd "sid generate 1000:20 published test-sid" $pattern +}} + +test sid_update {} { +-setup $ly_setup -cleanup $ly_cleanup -body { + set pattern "(?s)^" + append pattern {.*"module-name": "test-sid",} + append pattern {.*"module-revision": "2026-08-11",} + append pattern {.*"sid-file-version": 1,} + append pattern {.*"sid-file-status": "unpublished",} + append pattern {.*"entry-point": "1000",} + append pattern {.*"size": "20"} + append pattern {.*"identifier": "test-sid",} + append pattern {.*"status": "unstable",} + append pattern {.*"sid": "1000"} + append pattern {.*"identifier": "/test-sid:test-container",} + append pattern {.*"status": "unstable",} + append pattern {.*"sid": "1001"} + append pattern {.*"identifier": "/test-sid:test-container/test-leaf1",} + append pattern {.*"status": "unstable",} + append pattern {.*"sid": "1002"} + append pattern {.*"identifier": "/test-sid:test-container/test-leaf2",} + append pattern {.*"status": "obsolete",} + append pattern {.*"sid": "1003"} + append pattern {.*"identifier": "/test-sid:test-container/test-leaf3",} + append pattern {.*"status": "unstable",} + append pattern {.*"sid": "1004"} + + ly_cmd "verb error" + ly_cmd "add $mdir/test-sid-v2.yang" + ly_cmd "sid update $ddir/test.sid test-sid" $pattern +}} + +test sid_update_published {} { +-setup $ly_setup -cleanup $ly_cleanup -body { + set pattern "(?s)^" + append pattern {(?=.*"module-name": "test-sid",)} + append pattern {(?=.*"module-revision": "2026-08-11",)} + append pattern {(?=.*"sid-file-version": 1,)} + append pattern {(?=.*"entry-point": "1000",)} + append pattern {(?=.*"size": "20")} + append pattern {(?=.*"identifier": "test-sid",)} + append pattern {(?=.*"sid": "1000")} + append pattern {(?=.*"identifier": "/test-sid:test-container",)} + append pattern {(?=.*"sid": "1001")} + append pattern {(?=.*"identifier": "/test-sid:test-container/test-leaf1",)} + append pattern {(?=.*"sid": "1002")} + append pattern {(?=.*"identifier": "/test-sid:test-container/test-leaf2",)} + append pattern {(?=.*"status": "obsolete",)} + append pattern {(?=.*"sid": "1003")} + append pattern {(?=.*"identifier": "/test-sid:test-container/test-leaf3",)} + append pattern {(?=.*"sid": "1004")} + append pattern {(?!.*sid-file-status)} + append pattern {(?!.*"unstable")} + + ly_cmd "verb error" + ly_cmd "add $mdir/test-sid-v2.yang" + ly_cmd "sid update $ddir/test.sid published test-sid" $pattern +}} + +test sid_range_add {} { +-setup $ly_setup -cleanup $ly_cleanup -body { + set pattern "^(?!.*sid-file-version)" + append pattern {.*"module-name": "test-sid",} + append pattern {.*"module-revision": "2026-08-11",} + append pattern {.*"description": "Generated by libyang } + append pattern {.*"entry-point": "1000",} + append pattern {.*"size": "20"} + append pattern {.*"entry-point": "2000",} + append pattern {.*"size": "10"} + append pattern {.*"identifier": "test-sid",} + append pattern {.*"status": "unstable",} + append pattern {.*"sid": "1000"} + append pattern {.*"identifier": "/test-sid:test-container",} + append pattern {.*"status": "unstable",} + append pattern {.*"sid": "1001"} + append pattern {.*"identifier": "/test-sid:test-container/test-leaf1",} + append pattern {.*"status": "unstable",} + append pattern {.*"sid": "1002"} + append pattern {.*"identifier": "/test-sid:test-container/test-leaf2",} + append pattern {.*"status": "unstable",} + append pattern {.*"sid": "1003"} + + ly_cmd "sid range-add 2000:10 $ddir/test.sid" $pattern +}} + +test sid_generate_output_file {Test generating .sid file into a file} { +-setup $ly_setup -cleanup $ly_cleanup -body { + set pattern "(?s)^" + append pattern {.*"module-name": "test-sid",} + append pattern {.*"module-revision": "2026-08-11",} + append pattern {.*"sid-file-status": "unpublished",} + append pattern {.*"entry-point": "1000",} + append pattern {.*"size": "20"} + append pattern {.*"identifier": "test-sid",} + append pattern {.*"sid": "1000"} + append pattern {.*"identifier": "/test-sid:test-container/test-leaf2",} + append pattern {.*"sid": "1003"} + + ly_cmd "verb error" + ly_cmd "add $mdir/test-sid-v1.yang" + ly_cmd "sid -o test_output.sid generate 1000:20 test-sid" + + if {![file exists "test_output.sid"]} { + error "Test failed: yanglint did not create 'test_output.sid'!" + } + + set fp [open "test_output.sid" r] + set file_data [read $fp] + close $fp + + if {![regexp $pattern $file_data]} { + error "Test failed: The file content did not match!\nFile contained:\n$file_data" + } + + file delete "test_output.sid" +}} + +cleanupTests diff --git a/tests/yanglint/modules/test-sid-v1.yang b/tests/yanglint/modules/test-sid-v1.yang new file mode 100644 index 000000000..bf58b0e65 --- /dev/null +++ b/tests/yanglint/modules/test-sid-v1.yang @@ -0,0 +1,18 @@ +module test-sid { + namespace "test:module:test-sid"; + prefix test-sid; + + description + "Test module for testing sid generation, update and range-add."; + + revision 2026-08-11; + + container test-container { + leaf test-leaf1 { + type string; + } + leaf test-leaf2 { + type string; + } + } +} diff --git a/tests/yanglint/modules/test-sid-v2.yang b/tests/yanglint/modules/test-sid-v2.yang new file mode 100644 index 000000000..474bf0378 --- /dev/null +++ b/tests/yanglint/modules/test-sid-v2.yang @@ -0,0 +1,18 @@ +module test-sid { + namespace "test:module:test-sid"; + prefix test-sid; + + description + "Test module for testing sid generation, update and range-add."; + + revision 2026-08-11; + + container test-container { + leaf test-leaf1 { + type string; + } + leaf test-leaf3 { + type string; + } + } +} diff --git a/tests/yanglint/non-interactive/sid.test b/tests/yanglint/non-interactive/sid.test new file mode 100644 index 000000000..dba0de095 --- /dev/null +++ b/tests/yanglint/non-interactive/sid.test @@ -0,0 +1,134 @@ +source [expr {[info exists ::env(TESTS_DIR)] ? "$env(TESTS_DIR)/non-interactive/ly.tcl" : "ly.tcl"}] + +set mdir $::env(YANG_MODULES_DIR) +set ddir $::env(TESTS_DIR)/data + +test sid_generate {} { + set pattern "(?s)^" + append pattern {.*"module-name": "test-sid",} + append pattern {.*"module-revision": "2026-08-11",} + append pattern {.*"sid-file-status": "unpublished",} + append pattern {.*"description": "Generated by libyang } + append pattern {.*"entry-point": "1000",} + append pattern {.*"size": "20"} + append pattern {.*"namespace": "module",} + append pattern {.*"identifier": "test-sid",} + append pattern {.*"status": "unstable",} + append pattern {.*"sid": "1000"} + append pattern {.*"namespace": "data",} + append pattern {.*"identifier": "/test-sid:test-container",} + append pattern {.*"status": "unstable",} + append pattern {.*"sid": "1001"} + append pattern {.*"identifier": "/test-sid:test-container/test-leaf1",} + append pattern {.*"status": "unstable",} + append pattern {.*"sid": "1002"} + append pattern {.*"identifier": "/test-sid:test-container/test-leaf2",} + append pattern {.*"status": "unstable",} + append pattern {.*"sid": "1003"} + append pattern ".*" + + ly_cmd "-Q -g 1000:20 $mdir/test-sid-v1.yang" $pattern +} {} + +test sid_generate_published {} { + set pattern "(?s)^" + append pattern {(?=.*"module-name": "test-sid")} + append pattern {(?=.*"module-revision": "2026-08-11")} + append pattern {(?=.*"entry-point": "1000")} + append pattern {(?=.*"size": "20")} + append pattern {(?=.*"identifier": "/test-sid:test-container/test-leaf2")} + append pattern {(?=.*"sid": "1003")} + append pattern {(?!.*sid-file-status)} + append pattern {(?!.*"status")} + append pattern ".*" + + ly_cmd "-Q -g 1000:20 -U $mdir/test-sid-v1.yang" $pattern +} {} + +test sid_update {} { + set pattern "(?s)^" + append pattern {.*"module-name": "test-sid",} + append pattern {.*"module-revision": "2026-08-11",} + append pattern {.*"sid-file-version": 1,} + append pattern {.*"sid-file-status": "unpublished",} + append pattern {.*"description": "Generated by libyang } + append pattern {.*"entry-point": "1000",} + append pattern {.*"size": "20"} + append pattern {.*"namespace": "module",} + append pattern {.*"identifier": "test-sid",} + append pattern {.*"status": "unstable",} + append pattern {.*"sid": "1000"} + append pattern {.*"identifier": "/test-sid:test-container",} + append pattern {.*"status": "unstable",} + append pattern {.*"sid": "1001"} + append pattern {.*"identifier": "/test-sid:test-container/test-leaf1",} + append pattern {.*"status": "unstable",} + append pattern {.*"sid": "1002"} + append pattern {.*"identifier": "/test-sid:test-container/test-leaf2",} + append pattern {.*"status": "obsolete",} + append pattern {.*"sid": "1003"} + append pattern {.*"identifier": "/test-sid:test-container/test-leaf3",} + append pattern {.*"status": "unstable",} + append pattern {.*"sid": "1004"} + append pattern ".*" + + ly_cmd "-Q -u $ddir/test.sid $mdir/test-sid-v2.yang" $pattern +} {} + +test sid_update_published {} { + set pattern "(?s)^" + append pattern {(?=.*"module-name": "test-sid")} + append pattern {(?=.*"sid-file-version": 1,)} + append pattern {(?=.*"identifier": "/test-sid:test-container/test-leaf2")} + append pattern {(?=.*"status": "obsolete")} + append pattern {(?=.*"sid": "1003")} + append pattern {(?=.*"identifier": "/test-sid:test-container/test-leaf3")} + append pattern {(?=.*"sid": "1004")} + append pattern {(?!.*sid-file-status)} + append pattern {(?!.*"unstable")} + append pattern ".*" + + ly_cmd "-Q -u $ddir/test.sid -U $mdir/test-sid-v2.yang" $pattern +} {} + +test sid_update_range_add {} { + set pattern "(?s)^" + append pattern {.*"module-name": "test-sid",} + append pattern {.*"module-revision": "2026-08-11",} + append pattern {.*"sid-file-version": 1,} + append pattern {.*"sid-file-status": "unpublished",} + append pattern {.*"description": "Generated by libyang } + append pattern {.*"entry-point": "1000",} + append pattern {.*"size": "20"} + append pattern {.*"entry-point": "2000",} + append pattern {.*"size": "10"} + append pattern {.*"identifier": "/test-sid:test-container/test-leaf2",} + append pattern {.*"status": "obsolete",} + append pattern {.*"sid": "1003"} + append pattern {.*"identifier": "/test-sid:test-container/test-leaf3",} + append pattern {.*"status": "unstable",} + append pattern {.*"sid": "1004"} + append pattern ".*" + + ly_cmd "-Q -u $ddir/test.sid -r 2000:10 $mdir/test-sid-v2.yang" $pattern +} {} + +test sid_duplicate_range {} { + set pattern "can be specified only once" + + ly_cmd_err "-g 1000:20 -r 2000:10 $mdir/test-sid-v1.yang" $pattern +} {} + +test sid_range_add_without_update {} { + set pattern "can be used only in combination with" + + ly_cmd_err "-r 2000:10 $mdir/test-sid-v1.yang" $pattern +} {} + +test sid_generate_with_update {} { + set pattern "cannot be combined with" + + ly_cmd_err "-g 1000:20 -u $ddir/test.sid $mdir/test-sid-v2.yang" $pattern +} {} + +cleanupTests diff --git a/tools/lint/CMakeLists.txt b/tools/lint/CMakeLists.txt index 58b99fd57..55f014d1b 100644 --- a/tools/lint/CMakeLists.txt +++ b/tools/lint/CMakeLists.txt @@ -15,6 +15,7 @@ set(lintsrc cmd_verb.c cmd_debug.c cmd_sample.c + cmd_sid.c ietf.c yl_opt.c yl_schema_features.c diff --git a/tools/lint/cmd.c b/tools/lint/cmd.c index 5198470f9..2938380df 100644 --- a/tools/lint/cmd.c +++ b/tools/lint/cmd.c @@ -76,6 +76,10 @@ COMMAND commands[] = { "sample", cmd_sample_opt, cmd_sample_dep, cmd_sample_exec, NULL, cmd_sample_help, NULL, "Generate a sample data skeleton for a module", "f:ho:" }, + { + "sid", cmd_sid_opt, cmd_sid_dep, cmd_sid_exec, NULL, cmd_sid_help, NULL, + "Generate, update or extend a .sid file of a loaded module (RFC 9595)", "ho:f:" + }, { "list", cmd_list_opt, cmd_list_dep, cmd_list_exec, NULL, cmd_list_help, NULL, "List all the loaded modules", "f:h" diff --git a/tools/lint/cmd.h b/tools/lint/cmd.h index 2077dd777..29425082c 100644 --- a/tools/lint/cmd.h +++ b/tools/lint/cmd.h @@ -74,6 +74,7 @@ enum COMMAND_INDEX { CMD_PRINT, CMD_DATA, CMD_SAMPLE, + CMD_SID, CMD_LIST, CMD_FEATURE, CMD_SEARCHPATH, @@ -300,6 +301,33 @@ void cmd_sample_help(void); */ int cmd_sample_exec(struct ly_ctx **ctx, struct yl_opt *yo, const char *posv); +/* cmd_sid.c */ + +/** + * @copydoc cmd_add_opt + */ +int cmd_sid_opt(struct yl_opt *yo, const char *cmdline, char ***posv, int *posc); + +/** + * @copydoc cmd_add_dep + */ +int cmd_sid_dep(struct yl_opt *yo, int posc); +void cmd_sid_help(void); + +/** + * @brief Process the .sid file of a module (generate/update/range-add, RFC 9595) based on + * the set SID options: no previous file generates, a previous file updates (optionally + * adding a new assignment range first), both a range and a previous file in the interactive + * mode just extends the previous file with the new range. + * + * @param[in,out] ctx context for libyang. + * @param[in,out] yo context for yanglint, holds all the SID options. + * @param[in] posv Name of the module to process for generation/update, NULL for the + * standalone (interactive) range addition. + * @return 0 on success, 1 on failure. + */ +int cmd_sid_exec(struct ly_ctx **ctx, struct yl_opt *yo, const char *posv); + /* cmd_searchpath.c */ /** diff --git a/tools/lint/cmd_add.c b/tools/lint/cmd_add.c index 00f96a188..f9b13b42a 100644 --- a/tools/lint/cmd_add.c +++ b/tools/lint/cmd_add.c @@ -130,7 +130,8 @@ store_parsed_module(const char *filepath, struct lys_module *mod, struct yl_opt { assert(!yo->interactive); - if (yo->schema_out_format || yo->feature_param_format || yo->sample_skeleton || yo->ietf_validation) { + if (yo->schema_out_format || yo->feature_param_format || yo->sample_skeleton || yo->ietf_validation || + yo->sid_range || yo->sid_prev_path) { if (ly_set_add(&yo->schema_modules, (void *)mod, 1, NULL)) { YLMSG_E("Storing parsed schema module (%s) for print failed.", filepath); return 1; diff --git a/tools/lint/cmd_sid.c b/tools/lint/cmd_sid.c new file mode 100644 index 000000000..5eb22b610 --- /dev/null +++ b/tools/lint/cmd_sid.c @@ -0,0 +1,424 @@ +/** + * @file cmd_sid.c + * @author Petr Hanzlik + * @brief Processing of .sid files in the libyang's yanglint tool. + * + * Copyright (c) 2026 CESNET, z.s.p.o. + * + * This source code is licensed under BSD 3-Clause License (the "License"). + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://opensource.org/licenses/BSD-3-Clause + */ + +#define _GNU_SOURCE + +#include "cmd.h" + +#include +#include +#include +#include +#include + +#include "libyang.h" + +#include "common.h" +#include "yl_opt.h" + +/* processes the positional arguments specific for a .sid file operation (defined below) */ +static int process_sid_args(const char *op, int *argc, char **argv, struct yl_opt *yo); + +void +cmd_sid_help(void) +{ + printf("Usage: sid [-o OUTFILE] [-f FORMAT] [arguments]\n" + " Generate, update or extend a .sid file of a loaded schema module (RFC 9595).\n\n" + " Operations:\n" + " generate EP:SIZE [published] [@revision]\n" + " Generate a new .sid file for the module.\n" + " update [published] [@revision]\n" + " Update an existing .sid file for the module.\n" + " range-add EP:SIZE \n" + " Add a new assignment range to an existing .sid file.\n\n" + " EP is the first SID of the new assignment range, SIZE the number of SIDs to\n" + " assign (must not be 0).\n" + " Without the 'published' keyword, the status is 'unpublished'.\n" + " -o OUTFILE, --output=OUTFILE\n" + " Write the output to OUTFILE instead of stdout.\n" + " -f FORMAT, --format=FORMAT\n" + " Output data format (json (default), xml or lyb).\n" + " -h, --help Display this help message.\n"); +} + +int +cmd_sid_dep(struct yl_opt *yo, int posc) +{ + if (yo->sid_range && yo->sid_prev_path) { + /* interactive range-add */ + if (posc) { + YLMSG_E("Unexpected module name argument(s) for the SID range-add operation.\n"); + return 1; + } + } else if (yo->interactive && !posc) { + /* generate or update needs the target module */ + YLMSG_E("Missing the module name to process.\n"); + return 1; + } + + return 0; +} + +int +cmd_sid_opt(struct yl_opt *yo, const char *cmdline, char ***posv, int *posc) +{ + int rc = 0, argc = 0; + int opt, opt_index; + const char *op; + struct option options[] = { + {"output", required_argument, NULL, 'o'}, + {"format", required_argument, NULL, 'f'}, + {"help", no_argument, NULL, 'h'}, + {NULL, 0, NULL, 0} + }; + + if ((rc = parse_cmdline(cmdline, &argc, &yo->argv))) { + return rc; + } + + optind = 0; + while ((opt = getopt_long(argc, yo->argv, commands[CMD_SID].optstring, options, &opt_index)) != -1) { + switch (opt) { + case 'o': /* --output */ + if (yo->out) { + ly_out_free(yo->out, NULL, 0); + yo->out = NULL; + } + if (ly_out_new_filepath(optarg, &yo->out)) { + YLMSG_E("Unable to open output file %s.\n", optarg); + return 1; + } + break; + case 'f': /* --format */ + if (yo->data_out_format) { + YLMSG_E("The output data format can be specified only once.\n"); + return 1; + } + if (yl_opt_update_data_out_format(optarg, yo)) { + YLMSG_E("Invalid output data format \"%s\".\n", optarg); + return 1; + } + break; + case 'h': /* --help */ + cmd_sid_help(); + return 1; + default: + YLMSG_E("Unknown option.\n"); + return 1; + } + } + + /* the first positional argument must be the operation */ + if (optind >= argc) { + YLMSG_E("Missing the SID operation (generate, update or range-add).\n"); + return 1; + } + if (strcmp(yo->argv[optind], "generate") && strcmp(yo->argv[optind], "update") && + strcmp(yo->argv[optind], "range-add")) { + YLMSG_E("Unknown SID operation \"%s\" (use generate, update or range-add).\n", yo->argv[optind]); + return 1; + } + op = yo->argv[optind]; + optind++; + + /* parse the SID-specific positional arguments */ + if (process_sid_args(op, &argc, yo->argv, yo)) { + return 1; + } + + *posc = argc - optind; + *posv = &yo->argv[optind]; + + return 0; +} + +/** + * @brief Parse a positional argument in the EP:SIZE SID assignment range format. + * + * @param[in] sid_range Argument to parse. + * @param[out] entry_point Parsed entry point. + * @param[out] range_size Parsed range size. + * @return 0 if @p sid_range is a valid EP:SIZE range, 1 otherwise. + */ +static int +parse_sid_range(const char *sid_range, uint64_t *entry_point, uint64_t *range_size) +{ + char *end; + unsigned long long ep, len; + + errno = 0; + ep = strtoull(sid_range, &end, 10); + if ((end == sid_range) || errno || (*end != ':')) { + return 1; + } + end++; + + errno = 0; + len = strtoull(end, &end, 10); + if (!end || *end || errno) { + return 1; + } + + *entry_point = (uint64_t)ep; + *range_size = (uint64_t)len; + return 0; +} + +/** + * @brief Process the positional arguments of the interactive 'sid' command (an optional + * EP:SIZE range, an optional 'published' keyword, and the previous .sid file for the + * 'update'/'range-add' operations) and store them into the SID fields of ::yl_opt. + * + * @param[in] op The sid command operation ("generate", "update", or "range-add"). + * @param[in,out] argc Number of parsed arguments, updated to the remaining ones. + * @param[in,out] argv Parsed arguments, updated to the remaining ones. + * @param[in,out] yo yanglint options to store the SID values into. + * @return 0 on success, 1 on error (message already printed). + */ +static int +process_sid_args(const char *op, int *argc, char **argv, struct yl_opt *yo) +{ + int i, j; + uint64_t ep, size; + size_t len; + + for (i = optind, j = optind; i < *argc; i++) { + /* the EP:SIZE assignment range */ + if (!parse_sid_range(argv[i], &ep, &size)) { + if (!strcmp(op, "update")) { + YLMSG_E("The EP:SIZE range is not a valid argument for the SID update operation."); + return 1; + } + if (yo->sid_range) { + YLMSG_E("The EP:SIZE range can be specified only once."); + return 1; + } + if (!size) { + YLMSG_E("The SID range size must not be zero."); + return 1; + } + yo->sid_range = strdup(argv[i]); + if (!yo->sid_range) { + YLMSG_E("Memory allocation failed."); + return 1; + } + continue; + } + + /* the 'published' status keyword */ + if (!strcmp(argv[i], "published")) { + if (!strcmp(op, "range-add")) { + YLMSG_E("The 'published' status is not a valid argument for the SID range-add operation."); + return 1; + } + if (yo->sid_publish) { + YLMSG_E("The 'published' status can be specified only once."); + return 1; + } + yo->sid_publish = 1; + continue; + } + + /* the .sid file */ + len = strlen(argv[i]); + if ((len > 4) && !strcmp(argv[i] + len - 4, ".sid")) { + if (!strcmp(op, "generate")) { + YLMSG_E("A .sid file is not a valid argument for the SID generate operation."); + return 1; + } + if (yo->sid_prev_path) { + YLMSG_E("Only a single previous .sid file can be specified."); + return 1; + } + yo->sid_prev_path = strdup(argv[i]); + if (!yo->sid_prev_path) { + YLMSG_E("Memory allocation failed."); + return 1; + } + continue; + } + + /* keep all the other arguments for further processing */ + argv[j] = argv[i]; + j++; + } + argv[j] = NULL; + *argc = j; + + /* check the mandatory arguments */ + if (strcmp(op, "update") && !yo->sid_range) { + YLMSG_E("Missing the SID assignment range EP:SIZE."); + return 1; + } + if (strcmp(op, "generate") && !yo->sid_prev_path) { + YLMSG_E("Missing the previous .sid file to process."); + return 1; + } + + return 0; +} + +/** + * @brief Check the EP:SIZE SID assignment range option and parse it. + * + * @param[in] yo yanglint options with the range string (must be set). + * @param[out] ep Parsed entry point. + * @param[out] size Parsed range size. + * @return 0 on success, 1 on failure with an error message printed. + */ +static int +sid_check_range(const struct yl_opt *yo, uint64_t *ep, uint64_t *size) +{ + if (parse_sid_range(yo->sid_range, ep, size) || !*size) { + YLMSG_E("Bad format of the SID range \"%s\", expected EP:SIZE with a nonzero SIZE.", yo->sid_range); + return 1; + } + return 0; +} + +/** + * @brief Print the resulting .sid file data tree in the requested data format (JSON by default) + * to the prepared output. + * + * @param[in,out] yo yanglint options with the prepared output handler. + * @param[in] sid_file The .sid file data tree to print. + * @return 0 on success, 1 on failure. + */ +static int +sid_print(struct yl_opt *yo, struct lyd_node *sid_file) +{ + int rc = 0; + uint8_t out_alloc = 0; + LYD_FORMAT format = LYD_JSON; + + if (yo->data_out_format) { + format = yo->data_out_format; + } + + if (!yo->out) { + if (ly_out_new_file(stdout, &yo->out)) { + YLMSG_E("Unable to allocate output handler."); + return 1; + } + out_alloc = 1; + } + + if (lyd_print_all(yo->out, sid_file, format, 0)) { + YLMSG_E("Printing the .sid file failed."); + rc = 1; + } else { + ly_print_flush(yo->out); + } + + if (out_alloc) { + ly_out_free(yo->out, NULL, 0); + yo->out = NULL; + } + + return rc; +} + +int +cmd_sid_exec(struct ly_ctx **ctx, struct yl_opt *yo, const char *posv) +{ + int rc = 1; + const struct lys_module *mod = NULL; + struct lyd_node *prev = NULL, *sid_file = NULL; + char *name = NULL, *revision; + uint64_t ep = 0, size = 0; + LYS_SID_FILE_STATUS status = yo->sid_publish ? LYS_SID_FILE_PUBLISHED : LYS_SID_FILE_UNPUBLISHED; + + /* make sure the ietf-sid-file module is implemented in the context */ + if (!ly_ctx_get_module_implemented(*ctx, "ietf-sid-file") && + !ly_ctx_load_module(*ctx, "ietf-sid-file", NULL, NULL)) { + YLMSG_E("Unable to load the ietf-sid-file module required for SID processing."); + return 1; + } + + if (yo->sid_range && sid_check_range(yo, &ep, &size)) { + return 1; + } + + /* generate and update always have the target module, the standalone range-add does not */ + if (posv) { + name = strdup(posv); + if (!name) { + YLMSG_E("Memory allocation failed."); + return 1; + } + revision = strchr(name, '@'); + if (revision) { + revision[0] = '\0'; + revision++; + } + + mod = revision ? + ly_ctx_get_module(*ctx, name, revision) : + ly_ctx_get_module_latest(*ctx, name); + + if (!mod) { + YLMSG_E("Module \"%s\" not loaded.", name); + goto cleanup; + } + if (!mod->compiled && lys_set_implemented((struct lys_module *)mod, NULL)) { + YLMSG_E("Implementing module \"%s\" failed.", mod->name); + goto cleanup; + } + } + + if (yo->sid_prev_path) { + /* parse the previous .sid file */ + if (lyd_parse_data_path(*ctx, yo->sid_prev_path, LYD_JSON, 0, LYD_VALIDATE_PRESENT, &prev)) { + YLMSG_E("Failed to parse the previous .sid file \"%s\".", yo->sid_prev_path); + goto cleanup; + } + } + + if (yo->sid_range && yo->sid_prev_path) { + /* add the new assignment range first */ + if (lys_sid_range_add(prev, ep, size)) { + YLMSG_E("Adding a range into the .sid file \"%s\" failed.", yo->sid_prev_path); + goto cleanup; + } + if (yo->interactive) { + /* standalone range-add prints the previous file with the added range */ + sid_file = prev; + prev = NULL; + } + } + + if (!sid_file) { + if (prev) { + /* update */ + if (lys_sid_update(mod, prev, status, NULL, &sid_file)) { + YLMSG_E("Updating the .sid file of module \"%s\" failed.", mod->name); + goto cleanup; + } + } else { + /* generation */ + if (lys_sid_gen(mod, ep, size, status, NULL, &sid_file)) { + YLMSG_E("Generating the .sid file for module \"%s\" failed.", mod->name); + goto cleanup; + } + } + } + + rc = sid_print(yo, sid_file); + +cleanup: + free(name); + lyd_free_all(prev); + lyd_free_all(sid_file); + return rc; +} diff --git a/tools/lint/completion.c b/tools/lint/completion.c index 78d8a267d..8c75a2e91 100644 --- a/tools/lint/completion.c +++ b/tools/lint/completion.c @@ -518,6 +518,8 @@ complete_cmd(const char *buf, const char *hint, linenoiseCompletions *lc) {CMD_DATA, NULL, NULL}, {CMD_LIST, NULL, get_list_format_arg}, {CMD_FEATURE, NULL, get_model_completion}, + {CMD_SID, "-o", NULL}, + {CMD_SID, NULL, get_model_completion}, {CMD_VERB, NULL, get_verb_arg}, {CMD_DEBUG, NULL, get_debug_arg}, }; diff --git a/tools/lint/main_ni.c b/tools/lint/main_ni.c index a40336da3..7193a1492 100644 --- a/tools/lint/main_ni.c +++ b/tools/lint/main_ni.c @@ -227,7 +227,30 @@ help(int shortout) " Allow usage of JSON empty values ('null') within input data\n\n"); printf(" -T, --ietf\n" - " Enable stricter YANG model validation according to IETF rules.\n\n"); + " Enable stricter YANG model validation according to IETF rules.\n\n"); + + printf(" -g EP:SIZE, --sid-generate=EP:SIZE\n" + " Generate a new .sid file (RFC 9595) of the loaded schema modules\n" + " with the given SID assignment range, print it.The output format\n" + " can be set by -f (default json).\n" + " EP is the first SID of the new assignment range, SIZE the number\n" + " of SIDs to assign (must not be 0).\n\n"); + + printf(" -u FILE, --sid-update=FILE\n" + " Update an existing .sid file of the loaded schema modules. and print it.\n" + " The output format can be set by -f (default json).\n" + " Can be combined with --sid-range-add to add a new assignment\n" + " range into the updated file first.\n\n"); + + printf(" -r EP:SIZE, --sid-range-add=EP:SIZE\n" + " Add a new assignment range into the updated .sid file\n" + " (only in combination with --sid-update).\n" + " EP is the first SID of the new assignment range, SIZE the number\n" + " of SIDs to assign (must not be 0).\n\n"); + + printf(" -U, --sid-publish\n" + " Generate/update the .sid file with the 'published' status\n" + " (otherwise 'unpublished').\n\n"); printf(" -G GROUPS, --debug=GROUPS\n" #ifndef NDEBUG @@ -528,6 +551,10 @@ process_args(int argc, char *argv[], struct yl_opt *yo, struct ly_ctx **ctx) {"debug", required_argument, NULL, 'G'}, {"sample-skeleton", required_argument, NULL, 'S'}, {"ietf", no_argument, NULL, 'T'}, + {"sid-generate", required_argument, NULL, 'g'}, + {"sid-update", required_argument, NULL, 'u'}, + {"sid-range-add", required_argument, NULL, 'r'}, + {"sid-publish", no_argument, NULL, 'U'}, {NULL, 0, NULL, 0} }; uint8_t data_type_set = 0; @@ -539,7 +566,7 @@ process_args(int argc, char *argv[], struct yl_opt *yo, struct ly_ctx **ctx) yo->line_length = 0; opterr = 0; - while ((opt = getopt_long(argc, argv, "hvVQf:I:p:DF:iP:qs:neE:At:d:lL:o:O:R:myY:XJx:G:S:T", options, &opt_index)) != -1) { + while ((opt = getopt_long(argc, argv, "hvVQf:I:p:DF:iP:qs:neE:At:d:lL:o:O:R:myY:XJx:G:S:Tg:u:r:U", options, &opt_index)) != -1) { switch (opt) { case 'h': /* --help */ help(0); @@ -746,6 +773,36 @@ process_args(int argc, char *argv[], struct yl_opt *yo, struct ly_ctx **ctx) yo->ietf_validation = 1; break; + case 'g': /* --sid-generate */ + case 'r': /* --sid-range-add */ + if (yo->sid_range) { + YLMSG_E("The SID range option (--sid-generate/--sid-range-add) can be specified only once."); + return -1; + } + yo->sid_range = strdup(optarg); + if (!yo->sid_range) { + YLMSG_E("Memory allocation failed."); + return -1; + } + yo->sid_range_add = (opt == 'r'); + break; + + case 'u': /* --sid-update */ + if (yo->sid_prev_path) { + YLMSG_E("The --sid-update option can be specified only once."); + return -1; + } + yo->sid_prev_path = strdup(optarg); + if (!yo->sid_prev_path) { + YLMSG_E("Memory allocation failed."); + return -1; + } + break; + + case 'U': /* --sid-publish */ + yo->sid_publish = 1; + break; + default: YLMSG_E("Invalid option or missing argument: -%c.", optopt); return -1; @@ -765,6 +822,18 @@ process_args(int argc, char *argv[], struct yl_opt *yo, struct ly_ctx **ctx) if (cmd_print_dep(yo, 0)) { return -1; } + if (yo->sid_publish && !yo->sid_range && !yo->sid_prev_path) { + YLMSG_E("The --sid-publish option requires --sid-generate or --sid-update."); + return -1; + } + if (yo->sid_range_add && !yo->sid_prev_path) { + YLMSG_E("The --sid-range-add option can be used only in combination with --sid-update."); + return -1; + } + if (yo->sid_prev_path && yo->sid_range && !yo->sid_range_add) { + YLMSG_E("The --sid-generate option cannot be combined with --sid-update, use --sid-range-add to add a range."); + return -1; + } /* add the default search path */ search_path = getenv("YANGLINT_INTERNAL_MODULES_DIR"); @@ -860,6 +929,18 @@ main_ni(int argc, char *argv[]) goto cleanup; } } + } else if (yo.sid_range || yo.sid_prev_path) { + if (!yo.schema_modules.count) { + YLMSG_E("No schema module provided for the .sid file processing."); + ret = 1; + goto cleanup; + } + for (u = 0; u < yo.schema_modules.count; ++u) { + yo.last_one = (u + 1) == yo.schema_modules.count; + if ((ret = cmd_sid_exec(&ctx, &yo, ((struct lys_module *)yo.schema_modules.objs[u])->name))) { + goto cleanup; + } + } } /* do the data validation despite the schema was printed */ diff --git a/tools/lint/yl_opt.c b/tools/lint/yl_opt.c index 9912014b7..771f883ed 100644 --- a/tools/lint/yl_opt.c +++ b/tools/lint/yl_opt.c @@ -86,6 +86,10 @@ yl_opt_erase(struct yl_opt *yo) /* context */ free(yo->searchpaths); + /* .sid file processing */ + free(yo->sid_range); + free(yo->sid_prev_path); + /* --reply-rpc */ ly_in_free(yo->reply_rpc.in, 1); diff --git a/tools/lint/yl_opt.h b/tools/lint/yl_opt.h index 7e01909cb..7b5f07bf0 100644 --- a/tools/lint/yl_opt.h +++ b/tools/lint/yl_opt.h @@ -18,6 +18,7 @@ #include "parser_data.h" /* enum lyd_type */ #include "printer_schema.h" /* LYS_OUTFORMAT */ #include "set.h" /* ly_set */ +#include "tree_schema.h" /* LYS_SID_FILE_STATUS */ /** * @brief Data connected with a file provided on a command line as a file path. @@ -143,6 +144,21 @@ struct yl_opt { /* flag for --sample*/ uint8_t sample_skeleton; + /* + * .sid file processing (RFC 9595) + */ + /* SID assignment range in the EP:SIZE format (generation or range addition) */ + char *sid_range; + + /* the range was provided by --sid-range-add (to be combined with --sid-update), not --sid-generate */ + uint8_t sid_range_add; + + /* generate/update the .sid file with the 'published' status */ + uint8_t sid_publish; + + /* path to the previous .sid file (update/range-add input) */ + char *sid_prev_path; + char **argv; };