From 6fbf26b3d0c08372d892b84e58f9f53c08c53872 Mon Sep 17 00:00:00 2001 From: Colden Cullen Date: Thu, 1 Jan 2026 17:44:39 -0800 Subject: [PATCH] Add support for arbitrary index extensions This allows unknown optional extensions loaded from an index to be loaded and re-saved. It also allows callers to manually manipulate those extensions while in memory. --- include/git2/index.h | 44 ++++++++++++ src/libgit2/index.c | 120 ++++++++++++++++++++++++++++++-- src/libgit2/index.h | 1 + tests/libgit2/index/extension.c | 87 +++++++++++++++++++++++ 4 files changed, 248 insertions(+), 4 deletions(-) create mode 100644 tests/libgit2/index/extension.c diff --git a/include/git2/index.h b/include/git2/index.h index 6aadbef6b73..a1cb5256cd0 100644 --- a/include/git2/index.h +++ b/include/git2/index.h @@ -939,6 +939,50 @@ GIT_EXTERN(int) git_index_conflict_next( GIT_EXTERN(void) git_index_conflict_iterator_free( git_index_conflict_iterator *iterator); +/** + * Find an index extension by its signature + * + * @param out the extension's data (not including the header) + * @param index an existing index object + * @param signature the 4 character signature of the desired extension + * @return 0 (no error), GIT_ENOTFOUND (extension not present), or an error code + * (negative value) + */ +GIT_EXTERN(int) git_index_extension_get( + git_buf *out, + git_index *index, + const char *signature); + +/** + * Add an extension to the index + * + * @param index an existing index object + * @param signature the 4 character signature of the desired extension + * @param data the contents of the extension (not including the header) + * @param data_len the length of the data + * @param allow_overwrite whether the new extension should overwrite an existing + * one with the same signature + * @return 0 (no error), GIT_EEXISTS (extension already present), or an error code + * (negative value) + */ +GIT_EXTERN(int) git_index_extension_add( + git_index *index, + const char *signature, + const char *data, size_t data_len, + int allow_overwrite); + +/** + * Remove an index extension by its signature + * + * @param index an existing index object + * @param signature the 4 character signature of the desired extension + * @return 0 (no error), GIT_ENOTFOUND (extension not present), or an error code + * (negative value) + */ +GIT_EXTERN(int) git_index_extension_remove( + git_index *index, + const char *signature); + /** @} */ GIT_END_DECL diff --git a/src/libgit2/index.c b/src/libgit2/index.c index e8a692169ca..3dfae0c3c37 100644 --- a/src/libgit2/index.c +++ b/src/libgit2/index.c @@ -10,6 +10,7 @@ #include #include "repository.h" +#include "str.h" #include "tree.h" #include "tree-cache.h" #include "hash.h" @@ -306,6 +307,36 @@ static void index_entry_reuc_free(git_index_reuc_entry *reuc) git__free(reuc); } +static void *extension_data(const struct index_extension *ext) +{ + return ((char *)ext) + ext->extension_size; +} + +static int extension_srch(const void *key, const void *array_member) +{ + const struct index_extension *extension = array_member; + + return strncmp(key, extension->signature, sizeof(extension->signature)); +} + +static int extension_cmp(const void *a, const void *b) +{ + const struct index_extension *info_a = a; + const struct index_extension *info_b = b; + + return strncmp( + info_a->signature, + info_b->signature, + sizeof(info_a->signature)); +} + +static int extension_ondup(void **existing, void *new) +{ + const struct index_extension *info = *existing; + git_error_set(GIT_ERROR_INDEX, "found duplicate extension '%.4s'", info->signature); + return GIT_EEXISTS; +} + static void index_entry_free(git_index_entry *entry) { if (!entry) @@ -422,6 +453,7 @@ int git_index_open_ext( if (git_vector_init(&index->entries, 32, git_index_entry_cmp) < 0 || git_vector_init(&index->names, 8, conflict_name_cmp) < 0 || git_vector_init(&index->reuc, 8, reuc_cmp) < 0 || + git_vector_init(&index->extensions, 8, extension_cmp) < 0 || git_vector_init(&index->deleted, 8, git_index_entry_cmp) < 0) goto fail; @@ -473,6 +505,7 @@ static void index_free(git_index *index) git_vector_dispose(&index->entries); git_vector_dispose(&index->names); git_vector_dispose(&index->reuc); + git_vector_dispose_deep(&index->extensions); git_vector_dispose(&index->deleted); git__free(index->index_file_path); @@ -551,6 +584,7 @@ int git_index_clear(git_index *index) goto done; index_free_deleted(index); + git_vector_dispose_deep(&index->extensions); if ((error = git_index_name_clear(index)) < 0 || (error = git_index_reuc_clear(index)) < 0) @@ -2121,6 +2155,68 @@ void git_index_conflict_iterator_free(git_index_conflict_iterator *iterator) git__free(iterator); } +int git_index_extension_get(git_buf *out, git_index *index, const char *signature) +{ + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(index); + GIT_ASSERT_ARG(signature); + + int error; + size_t pos; + + error = git_vector_bsearch2(&pos, &index->extensions, extension_srch, signature); + if (error < 0) { + return error; + } + + struct index_extension *ext = git_vector_get(&index->extensions, pos); + + error = git_buf_set(out, extension_data(ext), ext->extension_size); + if (error < 0) { + return error; + } + + return GIT_OK; +} + +int git_index_extension_add( + git_index *index, + const char *signature, + const char *data, + size_t data_len, + int allow_overwrite) +{ + GIT_ASSERT_ARG(index); + GIT_ASSERT_ARG(signature); + + size_t alloc_size; + GIT_ERROR_CHECK_ALLOC_ADD(&alloc_size, sizeof(struct index_extension), data_len) + struct index_extension *ext = git__malloc(alloc_size); + GIT_ERROR_CHECK_ALLOC(ext); + + memcpy(ext->signature, signature, sizeof(ext->signature)); + ext->extension_size = data_len; + memcpy(extension_data(ext), data, data_len); + + int (*on_dup)(void **old, void *new) = allow_overwrite ? NULL : extension_ondup; + + return git_vector_insert_sorted(&index->extensions, ext, on_dup); +} + +int git_index_extension_remove(git_index *index, const char *signature) +{ + GIT_ASSERT_ARG(index); + GIT_ASSERT_ARG(signature); + + size_t pos; + + if (git_vector_bsearch2(&pos, &index->extensions, extension_srch, signature) < 0) { + return GIT_ENOTFOUND; + } + + return git_vector_remove(&index->extensions, pos); +} + size_t git_index_name_entrycount(git_index *index) { GIT_ASSERT_ARG(index); @@ -2701,12 +2797,19 @@ static int read_extension(size_t *read_len, git_index *index, size_t checksum_si } else if (memcmp(dest.signature, INDEX_EXT_CONFLICT_NAME_SIG, 4) == 0) { if (read_conflict_names(index, buffer + 8, dest.extension_size) < 0) return -1; + } else { + /* else, unsupported extension. We cannot parse this, + * but we can save it */ + struct git_index_extension *ext = git__malloc(total_size); + GIT_ERROR_CHECK_ALLOC(ext); + + /* Copy the extension (our struct is binary-compatible with the spec) */ + memcpy(ext, buffer, total_size); + + git_vector_insert_sorted(&index->extensions, ext, extension_ondup); } - /* else, unsupported extension. We cannot parse this, but we can skip - * it by returning `total_size */ } else { - /* we cannot handle non-ignorable extensions; - * in fact they aren't even defined in the standard */ + /* we cannot handle non-ignorable extensions */ git_error_set(GIT_ERROR_INDEX, "unsupported mandatory extension: '%.4s'", dest.signature); return -1; } @@ -3249,6 +3352,15 @@ static int write_index( if (index->reuc.length > 0 && write_reuc_extension(index, file) < 0) return -1; + /* write any unknown extension */ + size_t i; + struct index_extension *ext; + git_vector_foreach(&index->extensions, i, ext) { + git_str ext_data = GIT_STR_INIT_CONST(extension_data(ext), ext->extension_size); + if (write_extension(file, ext, &ext_data) < 0) + return -1; + } + /* get out the hash for all the contents we've appended to the file */ git_filebuf_hash(checksum, file); diff --git a/src/libgit2/index.h b/src/libgit2/index.h index 588fe434adf..c884c583aa9 100644 --- a/src/libgit2/index.h +++ b/src/libgit2/index.h @@ -52,6 +52,7 @@ struct git_index { git_vector names; git_vector reuc; + git_vector extensions; git_vector_cmp entries_cmp_path; git_vector_cmp entries_search; diff --git a/tests/libgit2/index/extension.c b/tests/libgit2/index/extension.c new file mode 100644 index 00000000000..0234fd6137f --- /dev/null +++ b/tests/libgit2/index/extension.c @@ -0,0 +1,87 @@ +#include "clar.h" +#include "clar_libgit2.h" + +#include "git2/errors.h" +#include "git2/index.h" + +static git_repository *g_repo = NULL; +static git_index *g_index = NULL; + +static const char test_ext_signature[4] = {'T', 'E', 'S', 'T'}; +static const char test_ext_data1[] = "This data is for testing purposes ONLY."; +static const char test_ext_data2[] = "This data has been overwritten."; + +void test_index_extension__initialize(void) +{ + g_repo = cl_git_sandbox_init("testrepo"); + cl_git_pass(git_repository_index(&g_index, g_repo)); + +} + +void test_index_extension__cleanup(void) +{ + git_index_free(g_index); + cl_git_sandbox_cleanup(); + g_repo = NULL; + + cl_git_pass(git_libgit2_opts(GIT_OPT_ENABLE_STRICT_OBJECT_CREATION, 1)); +} + +void test_index_extension__nonexistant(void) +{ + git_buf ext; + + cl_git_fail_with(GIT_ENOTFOUND, git_index_extension_get(&ext, g_index, test_ext_signature)); + cl_git_fail_with(GIT_ENOTFOUND, git_index_extension_remove(g_index, test_ext_signature)); +} + +void test_index_extension__add(void) +{ + git_buf ext; + + cl_git_pass(git_index_extension_add(g_index, test_ext_signature, test_ext_data1, sizeof(test_ext_data1), false)); + cl_git_pass(git_index_extension_get(&ext, g_index, test_ext_signature)); + cl_assert_equal_strn(test_ext_data1, ext.ptr, sizeof(test_ext_data1)); +} + +void test_index_extension__overwrite(void) +{ + git_buf ext; + + cl_git_pass(git_index_extension_add(g_index, test_ext_signature, test_ext_data1, sizeof(test_ext_data1), false)); + cl_git_pass(git_index_extension_get(&ext, g_index, test_ext_signature)); + cl_assert_equal_strn(test_ext_data1, ext.ptr, sizeof(test_ext_data1)); + + cl_git_fail_with(GIT_EEXISTS, git_index_extension_add(g_index, test_ext_signature, test_ext_data2, sizeof(test_ext_data2), false)); + + cl_git_pass(git_index_extension_add(g_index, test_ext_signature, test_ext_data2, sizeof(test_ext_data2), true)); + cl_git_pass(git_index_extension_get(&ext, g_index, test_ext_signature)); + cl_assert_equal_strn(test_ext_data2, ext.ptr, sizeof(test_ext_data2)); +} + +void test_index_extension__remove(void) +{ + git_buf ext; + + cl_git_pass(git_index_extension_add(g_index, test_ext_signature, test_ext_data1, sizeof(test_ext_data1), false)); + cl_git_pass(git_index_extension_get(&ext, g_index, test_ext_signature)); + cl_assert_equal_strn(test_ext_data1, ext.ptr, sizeof(test_ext_data1)); + + cl_git_pass(git_index_extension_remove(g_index, test_ext_signature)); + cl_git_fail_with(GIT_ENOTFOUND, git_index_extension_get(&ext, g_index, test_ext_signature)); +} + +void test_index_extension__write_read(void) +{ + git_buf ext; + + cl_git_pass(git_index_extension_add(g_index, test_ext_signature, test_ext_data1, sizeof(test_ext_data1), false)); + + cl_git_pass(git_index_write(g_index)); + git_index_clear(g_index); + + cl_git_pass(git_index_read(g_index, true)); + + cl_git_pass(git_index_extension_get(&ext, g_index, test_ext_signature)); + cl_assert_equal_strn(test_ext_data1, ext.ptr, sizeof(test_ext_data1)); +}