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;
};