yanglint FEATURE schema mount support (#1901)

* modify existing context if provided

In ly_ctx_new_yldata(), check if *ctx is NULL.  If so, proceed as before
with allocating a new context.  Otherwise, load modules into the existing
context.

Signed-off-by: Eric Kinzie <ekinzie@labn.net>

* add parent-reference xpath helper to schema-mount plugin

This new function produces a list of schema nodes from the expanded
parent-reference xpath expressions.

Signed-off-by: Eric Kinzie <ekinzie@labn.net>

* add context lookup to schema-mount plugin

This function allocates a new context for a particular instance of the
yangmnt:mount-point extension.

Signed-off-by: Eric Kinzie <ekinzie@labn.net>

* yanglint: schema-mount extension callback

Add a commandline argument "-x" that accepts a file containing extension
data, used to create a context for the schema-mount extension.  This file
has the same format as what is provided to the "-Y" option.  A callback
function for the schema-mount extenion is registered if the new option
is specified.

This allows validating instance data for models that include a schema
mount point.

Signed-off-by: Eric Kinzie <ekinzie@labn.net>

* yanglint: print mounted schema trees

Display "mp" flag for mount point and print mounted tree.  Also display
the / and @ opts in node names.

Signed-off-by: Eric Kinzie <ekinzie@labn.net>

* add unit test for tree mount-point flag

Test is courtesy of aPiecek <piecek@cesnet.cz>.

Signed-off-by: Eric Kinzie <ekinzie@labn.net>

* add example data files for validation with schema-mount

From tools/lint/examples directory:

% ../../../build/yanglint \
    -f json -t config -p ../../../models -p . \
    -Y ./sm-context-main.xml -x ./sm-context-extension.xml sm-data.xml

% ../../../build/yanglint \
    -f tree -p ../../../models -p . \
    -Y ./sm-context-main.xml -x ./sm-context-extension.xml sm-main.yang

Signed-off-by: Eric Kinzie <ekinzie@labn.net>

Signed-off-by: Eric Kinzie <ekinzie@labn.net>
Co-authored-by: Eric Kinzie <ekinzie@labn.net>
diff --git a/tools/lint/main_ni.c b/tools/lint/main_ni.c
index 845d5f1..175daee 100644
--- a/tools/lint/main_ni.c
+++ b/tools/lint/main_ni.c
@@ -67,6 +67,12 @@
     const struct lysc_node *schema_node;
     const char *submodule;
 
+    /* name of file containing explicit context passed to callback
+     * for schema-mount extension.  This also causes a callback to
+     * be registered.
+     */
+    char *schema_context_filename;
+
     /* value of --format in case of schema format */
     LYS_OUTFORMAT schema_out_format;
     ly_bool feature_param_format;
@@ -112,6 +118,10 @@
 
     ly_out_free(c->out, NULL,  0);
     ly_ctx_destroy(c->ctx);
+
+    if (c->schema_context_filename) {
+        free(c->schema_context_filename);
+    }
 }
 
 static void
@@ -191,6 +201,11 @@
     printf("  -s SUBMODULE, --submodule=SUBMODULE\n"
             "                Print the specific submodule instead of the main module.\n\n");
 
+    printf("  -x FILE, --ext-data=FILE\n"
+            "                File containing the specific data required by an extension. Required by\n"
+            "                the schema-mount extension, for example, when the mounted data are\n"
+            "                expected in the file. File format is guessed.\n\n");
+
     printf("  -n, --not-strict\n"
             "                Do not require strict data parsing (silently skip unknown data),\n"
             "                has no effect for schemas.\n\n");
@@ -317,6 +332,22 @@
     return NULL;
 }
 
+static LY_ERR
+ext_data_clb(const struct lysc_ext_instance *ext, void *user_data, void **ext_data, ly_bool *ext_data_free)
+{
+    struct ly_ctx *ctx;
+    struct lyd_node *data = NULL;
+
+    ctx = ext->module->ctx;
+    if (user_data) {
+        lyd_parse_data_path(ctx, user_data, LYD_XML, LYD_PARSE_STRICT, LYD_VALIDATE_PRESENT, &data);
+    }
+
+    *ext_data = data;
+    *ext_data_free = 1;
+    return LY_SUCCESS;
+}
+
 static int
 fill_context_inputs(int argc, char *argv[], struct context *c)
 {
@@ -334,8 +365,13 @@
         ly_set_erase(&c->schema_features, free_features);
 
         /* create context from the yang-library file */
+        if (ly_ctx_new(searchdir, c->ctx_options, &c->ctx)) {
+            YLMSG_E("Unable to create libyang context\n");
+            return -1;
+        }
+        ly_ctx_set_ext_data_clb(c->ctx, ext_data_clb, c->schema_context_filename);
         if (ly_ctx_new_ylpath(searchdir, c->yang_lib_file, LYD_UNKNOWN, c->ctx_options, &c->ctx)) {
-            YLMSG_E("Unable to create libyang context from yang-library data.\n");
+            YLMSG_E("Unable to modify libyang context with yang-library data.\n");
             return -1;
         }
     } else {
@@ -350,6 +386,13 @@
             return -1;
         }
 
+        if (c->schema_context_filename) {
+            if (ly_ctx_set_ext_data_clb(c->ctx, ext_data_clb, c->schema_context_filename)) {
+                YLMSG_E("Unable to set extension callback data.\n");
+                return -1;
+            }
+        }
+
         /* set the rest of searchdirs */
         for (uint32_t i = 1; i < c->searchpaths.count; ++i) {
             ly_ctx_set_searchdir(c->ctx, c->searchpaths.objs[i]);
@@ -495,6 +538,7 @@
         {"schema-node",       required_argument, NULL, 'P'},
         {"single-node",       no_argument,       NULL, 'q'},
         {"submodule",         required_argument, NULL, 's'},
+        {"ext-data",          required_argument, NULL, 'x'},
         {"not-strict",        no_argument,       NULL, 'n'},
         {"present",           no_argument,       NULL, 'e'},
         {"type",              required_argument, NULL, 't'},
@@ -520,9 +564,9 @@
 
     opterr = 0;
 #ifndef NDEBUG
-    while ((opt = getopt_long(argc, argv, "hvVQf:p:DF:iP:qs:net:d:lL:o:O:R:myY:G:", options, &opt_index)) != -1)
+    while ((opt = getopt_long(argc, argv, "hvVQf:p:DF:iP:qs:net:d:lL:o:O:R:myY:x:G:", options, &opt_index)) != -1)
 #else
-    while ((opt = getopt_long(argc, argv, "hvVQf:p:DF:iP:qs:net:d:lL:o:O:R:myY:", options, &opt_index)) != -1)
+    while ((opt = getopt_long(argc, argv, "hvVQf:p:DF:iP:qs:net:d:lL:o:O:R:myY:x:", options, &opt_index)) != -1)
 #endif
     {
         switch (opt) {
@@ -645,6 +689,10 @@
             c->submodule = optarg;
             break;
 
+        case 'x': /* --ext-data */
+            c->schema_context_filename = strdup(optarg);
+            break;
+
         case 'n': /* --not-strict */
             c->data_parse_options &= ~LYD_PARSE_STRICT;
             break;