From e07bd039a0114e06c4cee091e4c2a4183ca4fc2b Mon Sep 17 00:00:00 2001 From: Juraj Budai Date: Tue, 14 Oct 2025 14:45:22 +0200 Subject: [PATCH] diff BUGFIX user-ordered replace operation --- src/diff.c | 243 ++++++++++++++++++++++++----- tests/utests/data/test_diff.c | 282 ++++++++++++++++++++++++++++++++++ 2 files changed, 486 insertions(+), 39 deletions(-) diff --git a/src/diff.c b/src/diff.c index 9f5fe05d5..ff1eefc70 100644 --- a/src/diff.c +++ b/src/diff.c @@ -2004,6 +2004,196 @@ lyd_diff_change_op(struct lyd_node *node, enum lyd_diff_op op) } } +/** + * @brief In user-ordered lists, certain operations on sibling nodes can result in logically identical changes. + * However, applying the first change may cause the second one to fail. + * Check whether this diff node is redundant. + * + * @param[in,out] diff The node whose metadata has been modified. + * @param[in] child The child of diff node. + * @return 0 if not, non-zero if it is. + */ +static ly_bool +lyd_diff_is_redundant_userord_move(struct lyd_node **diff, struct lyd_node *child) +{ + LY_ERR ret = LY_SUCCESS; + struct lyd_meta *meta1, *meta2; + struct lyd_meta *orig_val_meta = NULL, *val_meta = NULL; + struct lyd_node *diff_iter = *diff; + char *buff1 = NULL, *buff2 = NULL; + const char *llist_value1 = NULL, *llist_value2 = NULL; + const char *name = NULL, *name_iter = NULL; + size_t bufflen1 = 0, buffused1 = 0; + size_t bufflen2 = 0, buffused2 = 0; + const char *orig_meta_name, *meta_name; + + /* get metadata names */ + if (lysc_is_dup_inst_list((*diff)->schema)) { + meta_name = "yang:position"; + orig_meta_name = "yang:orig-position"; + } else if ((*diff)->schema->nodetype == LYS_LIST) { + meta_name = "yang:key"; + orig_meta_name = "yang:orig-key"; + } else { + meta_name = "yang:value"; + orig_meta_name = "yang:orig-value"; + } + + /* check for redundant move */ + orig_val_meta = lyd_find_meta((*diff)->meta, NULL, orig_meta_name); + val_meta = lyd_find_meta((*diff)->meta, NULL, meta_name); + assert(orig_val_meta && val_meta); + + if (!lyd_compare_meta(orig_val_meta, val_meta)) { + /* there is actually no move */ + lyd_free_meta_single(orig_val_meta); + lyd_free_meta_single(val_meta); + if (child) { + /* change operation to NONE, we have siblings */ + lyd_diff_change_op((*diff), LYD_DIFF_OP_NONE); + goto cleanup; + } + + /* redundant node, BUT !! + * In diff the move operation is always converted to be INSERT_AFTER, which is fine + * because the data that this is applied on should not change for the diff lifetime. + * However, when we are merging 2 diffs, this conversion is actually lossy because + * if the data change, the move operation can also change its meaning. In this specific + * case the move operation will be lost. But it can be considered a feature, it is not supported. + */ + ret = 1; + goto cleanup; + } + + /* itereate throught previous nodes and look for logically identical changes */ + diff_iter = (*diff)->prev; + while (diff_iter != (*diff)) { + + meta1 = lyd_find_meta((*diff)->meta, NULL, meta_name); + meta2 = lyd_find_meta(diff_iter->meta, NULL, orig_meta_name); + + name = lyd_get_meta_value(meta1); + name_iter = lyd_get_meta_value(meta2); + + if (!name || !name_iter) { + goto next_iter; + } + + /* if keys don't match, skip - not a candidate for cyclic change */ + if (strcmp(name, name_iter)) { + goto next_iter; + } + + meta1 = lyd_find_meta((*diff)->meta, NULL, orig_meta_name); + meta2 = lyd_find_meta(diff_iter->meta, NULL, meta_name); + + /* store string values of metadata to compare later */ + name = lyd_get_meta_value(meta1); + name_iter = lyd_get_meta_value(meta2); + + if (!name || !name_iter) { + goto next_iter; + } + + if ((*diff)->schema->nodetype == LYS_LIST) { + + /* reuse buffers by resetting used size */ + buffused1 = buffused2 = 0; + LY_CHECK_GOTO(ret = lyd_path_list_predicate(*diff, &buff1, &bufflen1, &buffused1, 0), cleanup); + LY_CHECK_GOTO(ret = lyd_path_list_predicate(diff_iter, &buff2, &bufflen2, &buffused2, 0), cleanup); + + /* compare path predicates with metadata - check if this is a reversed operation */ + if (!strcmp(buff1, name_iter) && !strcmp(buff2, name)) { + + /* found a cyclic change - remove and free the node */ + ret = 1; + goto cleanup; + } + } else { + llist_value1 = lyd_get_value(*diff); + llist_value2 = lyd_get_value(diff_iter); + + /* compare vlaue of data node with metadata - check if this is a reversed operation */ + if (!strcmp(llist_value1, name_iter) && !strcmp(llist_value2, name)) { + + /* found a cyclic change - remove and free the node */ + ret = 1; + goto cleanup; + } + } + +next_iter: + diff_iter = diff_iter->prev; + } + +cleanup: + free(buff1); + free(buff2); + return ret; +} + +/** + * @brief Propagate key/value metadata from a list or leaf-list node + * to its sibling nodes that reference it via key/value. + * + * This is used to ensure correct ordering in user-ordered lists + * or leaf-lists by updating the corresponding metadata in sibling + * nodes before the reference node changes. + * + * @param[in] diff Node from lyd_diff_merge_replace(). + * @param[in] meta_name Name of the metadata ("key" or "value"). + * @return LY_ERR value. + */ +static LY_ERR +lyd_diff_propagate_meta(struct lyd_node *diff, const char *meta_name) +{ + LY_ERR ret = LY_SUCCESS; + const struct lys_module *mod; + struct lyd_meta *meta1, *meta2; + struct lyd_node *diff_iter = NULL; + char *buff = NULL; + size_t bufflen = 0, buffused = 0; + const char *meta_value; + + /* get "yang" module for the metadata */ + mod = ly_ctx_get_module_latest(LYD_CTX(diff), "yang"); + assert(mod); + + /* + * iterate through all siblings of the diff + * if a sibling references the diff node via metadata, update it + */ + LY_LIST_FOR(diff, diff_iter) { + /* find the relevant metadata on the current sibling */ + if ((meta1 = lyd_find_meta(diff_iter->meta, mod, meta_name))) { + if (diff->schema->nodetype == LYS_LEAFLIST) { + if (!strcmp(lyd_get_meta_value(meta1), lyd_get_value(diff))) { + /* replace the old metadata with the updated one from the changed node */ + lyd_diff_del_meta(diff_iter, meta_name); + meta2 = lyd_find_meta(diff->meta, mod, meta_name); + LY_CHECK_GOTO((ret = lyd_dup_meta_single(meta2, diff_iter, NULL)), cleanup); + } + } else { + buffused = 0; + + LY_CHECK_GOTO((ret = lyd_path_list_predicate(diff, &buff, &bufflen, &buffused, 0)), cleanup); + meta_value = lyd_get_meta_value(meta1); + + /* if the path predicate matches, replace the metadata */ + if (!strcmp(buff, meta_value)) { + meta1 = lyd_find_meta(diff_iter->meta, mod, meta_name); + meta2 = lyd_find_meta(diff->meta, mod, meta_name); + LY_CHECK_GOTO((ret = lyd_change_meta(meta1, lyd_get_meta_value(meta2))), cleanup); + } + } + } + } + +cleanup: + free(buff); + return ret; +} + /** * @brief Update operations on a diff node when the new operation is REPLACE. * @@ -2043,10 +2233,14 @@ lyd_diff_merge_replace(struct lyd_node *diff_match, enum lyd_diff_op cur_op, con meta_name = "value"; } + /* update sibling nodes which reference the diff_match by key/value */ + LY_CHECK_RET(lyd_diff_propagate_meta(diff_match, meta_name)); + lyd_diff_del_meta(diff_match, meta_name); meta = lyd_find_meta(src_diff->meta, mod, meta_name); LY_CHECK_ERR_RET(!meta, LOGERR_META(ctx, meta_name, src_diff), LY_EINVAL); LY_CHECK_RET(lyd_dup_meta_single(meta, diff_match, NULL)); + break; case LYS_LEAF: /* replaced with the exact same value, impossible */ @@ -2429,10 +2623,10 @@ static ly_bool lyd_diff_is_redundant(struct lyd_node *diff) { enum lyd_diff_op op; - struct lyd_meta *meta, *orig_val_meta = NULL, *val_meta = NULL; + struct lyd_meta *meta; struct lyd_node *child; const struct lys_module *mod; - const char *str, *orig_meta_name, *meta_name; + const char *str; assert(diff); @@ -2449,42 +2643,12 @@ lyd_diff_is_redundant(struct lyd_node *diff) LY_CHECK_RET(lyd_diff_get_op(diff, &op, NULL), 0); if ((op == LYD_DIFF_OP_REPLACE) && lysc_is_userordered(diff->schema)) { - /* get metadata names */ - if (lysc_is_dup_inst_list(diff->schema)) { - meta_name = "position"; - orig_meta_name = "orig-position"; - } else if (diff->schema->nodetype == LYS_LIST) { - meta_name = "key"; - orig_meta_name = "orig-key"; - } else { - meta_name = "value"; - orig_meta_name = "orig-value"; - } - - /* check for redundant move */ - orig_val_meta = lyd_find_meta(diff->meta, mod, orig_meta_name); - val_meta = lyd_find_meta(diff->meta, mod, meta_name); - assert(orig_val_meta && val_meta); - - if (!lyd_compare_meta(orig_val_meta, val_meta)) { - /* there is actually no move */ - lyd_free_meta_single(orig_val_meta); - lyd_free_meta_single(val_meta); - if (child) { - /* change operation to NONE, we have siblings */ - lyd_diff_change_op(diff, LYD_DIFF_OP_NONE); - return 0; - } - /* redundant node, BUT !! - * In diff the move operation is always converted to be INSERT_AFTER, which is fine - * because the data that this is applied on should not change for the diff lifetime. - * However, when we are merging 2 diffs, this conversion is actually lossy because - * if the data change, the move operation can also change its meaning. In this specific - * case the move operation will be lost. But it can be considered a feature, it is not supported. - */ - return 1; - } + /** userordered lists can have different nodes that lead to identical changes. + * if such a redundant node is detected, this function returns non-zero. + */ + LY_CHECK_RET(lyd_diff_is_redundant_userord_move(&diff, child), 1); + } else if (op == LYD_DIFF_OP_NONE) { if (!diff->schema) { /* opaque node with none must be redundant */ @@ -3108,6 +3272,7 @@ lyd_diff_reverse_userord(struct lyd_node *node, const struct lysc_node **schema_ do { --u; LY_CHECK_GOTO(rc = lyd_insert_after(anchor, (*nodes_p)[u]), cleanup); + anchor = (*nodes_p)[u]; } while (u); } @@ -3268,13 +3433,13 @@ lyd_diff_reverse_siblings_r(struct lyd_node *sibling, const struct lys_module *y LY_CHECK_GOTO(rc = lyd_diff_reverse_siblings_r(lyd_child(iter), yang_mod), cleanup); if (lysc_is_userordered(iter->schema)) { - /* special user-ordered nodes processing */ + /* special user-ordered nodes processing (collect all the nodes) */ LY_CHECK_GOTO(rc = lyd_diff_reverse_userord(iter, &userord_schema, &userord), cleanup); } } if (userord_schema) { - /* finish user-ordered nodes processing */ + /* finish user-ordered nodes processing - reverse the order of the nodes */ LY_CHECK_GOTO(rc = lyd_diff_reverse_userord(NULL, &userord_schema, &userord), cleanup); } diff --git a/tests/utests/data/test_diff.c b/tests/utests/data/test_diff.c index 59ec8a9aa..612d9e0cb 100644 --- a/tests/utests/data/test_diff.c +++ b/tests/utests/data/test_diff.c @@ -90,6 +90,10 @@ const char *schema = " leaf l3 {type string;}" " }" " }" + " list person {key \"name surname\"; ordered-by user;" + " leaf name {type string;}" + " leaf surname {type string;}" + " }" " leaf-list dllist {type uint8; default \"1\"; default \"2\"; default \"3\";}" " list list {key \"name\";" " leaf name {type string;}" @@ -1418,6 +1422,280 @@ test_metadata(void **state) TEST_DIFF_3(xml1, xml2, xml3, LYD_DIFF_META, out_diff_1, out_diff_2, out_merge); } +static void +test_userord_list_reverse(void **state) +{ + (void) state; + struct lyd_node *tree1 = NULL, *tree2 = NULL; + struct lyd_node *diff1_2 = NULL, *diff_reverse = NULL; + char *final_xml; + + char *xml1 = + "\n" + " \n" + " alice\n" + " chalice\n" + " \n" + " \n" + " bob\n" + " drop\n" + " \n" + " \n" + " jake\n" + " fake\n" + " \n" + "\n"; + + char *xml2 = + "\n" + " \n" + " jake\n" + " fake\n" + " \n" + " \n" + " bob\n" + " drop\n" + " \n" + " \n" + " alice\n" + " chalice\n" + " \n" + " \n" + " roland\n" + " doland\n" + " \n" + "\n"; + + CHECK_PARSE_LYD(xml1, tree1); + CHECK_PARSE_LYD(xml2, tree2); + + lyd_diff_siblings(tree1, tree2, 0, &diff1_2); + + lyd_diff_reverse_all(diff1_2, &diff_reverse); + + lyd_diff_apply_all(&tree2, diff_reverse); + + lyd_print_mem(&final_xml, tree2, LYD_XML, LYD_PRINT_WITHSIBLINGS); + + assert_string_equal(final_xml, xml1); + + lyd_free_tree(diff1_2); + lyd_free_tree(diff_reverse); + + lyd_free_tree(tree1); + lyd_free_tree(tree2); + free(final_xml); +} + +static void +test_userord_conflicting_replace_list(void **state) +{ + (void) state; + struct lyd_node *diff_tree1 = NULL, *diff_tree2 = NULL; + char *final_xml; + + char *diff_xml1 = + "\n" + " " + " roland" + " doland" + " " + " " + " jake" + " fake" + " " + " " + " bob" + " drop" + " " + "\n"; + + char *diff_xml2 = + "\n" + " " + " bob" + " drop" + " " + "\n"; + + CHECK_PARSE_LYD(diff_xml1, diff_tree1); + CHECK_PARSE_LYD(diff_xml2, diff_tree2); + + lyd_diff_merge_all(&diff_tree1, diff_tree2, 0); + + lyd_print_mem(&final_xml, diff_tree1, LYD_XML, LYD_PRINT_WITHSIBLINGS); + + char *result_xml = + "\n" + " \n" + " roland\n" + " doland\n" + " \n" + " \n" + " jake\n" + " fake\n" + " \n" + "\n"; + + assert_string_equal(final_xml, result_xml); + + lyd_free_tree(diff_tree1); + lyd_free_tree(diff_tree2); + free(final_xml); +} + +static void +test_userord_conflicting_replace_list2(void **state) +{ + (void) state; + struct lyd_node *tree1 = NULL, *tree2 = NULL, *tree3 = NULL; + struct lyd_node *diff1_2 = NULL, *diff3_1 = NULL, *diff1_3 = NULL; + char *final_xml; + + char *xml1 = + "\n" + " \n" + " alice\n" + " chalice\n" + " \n" + " \n" + " bob\n" + " drop\n" + " \n" + " \n" + " jake\n" + " fake\n" + " \n" + " \n" + " roland\n" + " doland\n" + " \n" + "\n"; + + char *xml2 = + "\n" + " \n" + " bob\n" + " drop\n" + " \n" + " \n" + " alice\n" + " chalice\n" + " \n" + " \n" + " jake\n" + " fake\n" + " \n" + " \n" + " roland\n" + " doland\n" + " \n" + "\n"; + + char *xml3 = + "\n" + " \n" + " jake\n" + " fake\n" + " \n" + " \n" + " bob\n" + " drop\n" + " \n" + " \n" + " alice\n" + " chalice\n" + " \n" + " \n" + " roland\n" + " doland\n" + " \n" + "\n"; + + CHECK_PARSE_LYD(xml1, tree1); + CHECK_PARSE_LYD(xml2, tree2); + CHECK_PARSE_LYD(xml3, tree3); + + lyd_diff_siblings(tree1, tree2, 0, &diff1_2); + lyd_diff_siblings(tree1, tree3, 0, &diff1_3); + lyd_diff_reverse_all(diff1_3, &diff3_1); + + lyd_diff_merge_all(&diff3_1, diff1_2, 0); + + lyd_diff_apply_all(&tree3, diff3_1); + + lyd_print_mem(&final_xml, tree3, LYD_XML, LYD_PRINT_WITHSIBLINGS); + + assert_string_equal(final_xml, xml2); + + lyd_free_tree(diff1_2); + lyd_free_tree(diff1_3); + lyd_free_tree(diff3_1); + + lyd_free_tree(tree1); + lyd_free_tree(tree2); + lyd_free_tree(tree3); + free(final_xml); +} + +static void +test_userord_conflicting_replace_llist(void **state) +{ + (void) state; + struct lyd_node *tree1 = NULL, *tree2 = NULL, *tree3 = NULL; + struct lyd_node *diff1_2 = NULL, *diff3_1 = NULL, *diff1_3 = NULL; + char *final_xml; + + char *xml1 = + "\n" + " 1\n" + " 2\n" + " 3\n" + " 4\n" + "\n"; + + char *xml2 = + "\n" + " 2\n" + " 1\n" + " 3\n" + " 4\n" + "\n"; + + char *xml3 = + "\n" + " 3\n" + " 2\n" + " 1\n" + " 4\n" + "\n"; + + CHECK_PARSE_LYD(xml1, tree1); + CHECK_PARSE_LYD(xml2, tree2); + CHECK_PARSE_LYD(xml3, tree3); + + lyd_diff_siblings(tree1, tree2, 0, &diff1_2); + lyd_diff_siblings(tree1, tree3, 0, &diff1_3); + lyd_diff_reverse_all(diff1_3, &diff3_1); + + lyd_diff_merge_all(&diff3_1, diff1_2, 0); + + lyd_diff_apply_all(&tree3, diff3_1); + + lyd_print_mem(&final_xml, tree3, LYD_XML, LYD_PRINT_WITHSIBLINGS); + + assert_string_equal(final_xml, xml2); + + lyd_free_tree(diff1_2); + lyd_free_tree(diff1_3); + lyd_free_tree(diff3_1); + + lyd_free_tree(tree1); + lyd_free_tree(tree2); + lyd_free_tree(tree3); + free(final_xml); +} + int main(void) { @@ -1442,6 +1720,10 @@ main(void) UTEST(test_state_llist, setup), UTEST(test_wd, setup), UTEST(test_metadata, setup), + UTEST(test_userord_list_reverse, setup), + UTEST(test_userord_conflicting_replace_list, setup), + UTEST(test_userord_conflicting_replace_list2, setup), + UTEST(test_userord_conflicting_replace_llist, setup), }; return cmocka_run_group_tests(tests, NULL, NULL);