1 /* This test exercises the problem described in
2 ** https://github.com/libgit2/libgit2/pull/3568
3 ** where deleting a directory during a diff/status
4 ** operation can cause an access violation.
5 **
6 ** The "test_diff_racediffiter__basic() test confirms
7 ** the normal operation of diff on the given repo.
8 **
9 ** The "test_diff_racediffiter__racy_rmdir() test
10 ** uses the new diff progress callback to delete
11 ** a directory (after the initial readdir() and
12 ** before the directory itself is visited) causing
13 ** the recursion and iteration to fail.
14 */
15
16 #include "clar_libgit2.h"
17 #include "diff_helpers.h"
18
19 #define ARRAY_LEN(a) (sizeof(a) / sizeof(a[0]))
20
test_diff_racediffiter__initialize(void)21 void test_diff_racediffiter__initialize(void)
22 {
23 }
24
test_diff_racediffiter__cleanup(void)25 void test_diff_racediffiter__cleanup(void)
26 {
27 cl_git_sandbox_cleanup();
28 }
29
30 typedef struct
31 {
32 const char *path;
33 git_delta_t t;
34
35 } basic_payload;
36
notify_cb__basic(const git_diff * diff_so_far,const git_diff_delta * delta_to_add,const char * matched_pathspec,void * payload)37 static int notify_cb__basic(
38 const git_diff *diff_so_far,
39 const git_diff_delta *delta_to_add,
40 const char *matched_pathspec,
41 void *payload)
42 {
43 basic_payload *exp = (basic_payload *)payload;
44 basic_payload *e;
45
46 GIT_UNUSED(diff_so_far);
47 GIT_UNUSED(matched_pathspec);
48
49 for (e = exp; e->path; e++) {
50 if (strcmp(e->path, delta_to_add->new_file.path) == 0) {
51 cl_assert_equal_i(e->t, delta_to_add->status);
52 return 0;
53 }
54 }
55 cl_assert(0);
56 return GIT_ENOTFOUND;
57 }
58
test_diff_racediffiter__basic(void)59 void test_diff_racediffiter__basic(void)
60 {
61 git_diff_options opts = GIT_DIFF_OPTIONS_INIT;
62 git_repository *repo = cl_git_sandbox_init("diff");
63 git_diff *diff;
64
65 basic_payload exp_a[] = {
66 { "another.txt", GIT_DELTA_MODIFIED },
67 { "readme.txt", GIT_DELTA_MODIFIED },
68 { "zzzzz/", GIT_DELTA_IGNORED },
69 { NULL, 0 }
70 };
71
72 cl_must_pass(p_mkdir("diff/zzzzz", 0777));
73
74 opts.flags |= GIT_DIFF_INCLUDE_IGNORED | GIT_DIFF_RECURSE_UNTRACKED_DIRS;
75 opts.notify_cb = notify_cb__basic;
76 opts.payload = exp_a;
77
78 cl_git_pass(git_diff_index_to_workdir(&diff, repo, NULL, &opts));
79
80 git_diff_free(diff);
81 }
82
83
84 typedef struct {
85 bool first_time;
86 const char *dir;
87 basic_payload *basic_payload;
88 } racy_payload;
89
notify_cb__racy_rmdir(const git_diff * diff_so_far,const git_diff_delta * delta_to_add,const char * matched_pathspec,void * payload)90 static int notify_cb__racy_rmdir(
91 const git_diff *diff_so_far,
92 const git_diff_delta *delta_to_add,
93 const char *matched_pathspec,
94 void *payload)
95 {
96 racy_payload *pay = (racy_payload *)payload;
97
98 if (pay->first_time) {
99 cl_must_pass(p_rmdir(pay->dir));
100 pay->first_time = false;
101 }
102
103 return notify_cb__basic(diff_so_far, delta_to_add, matched_pathspec, pay->basic_payload);
104 }
105
test_diff_racediffiter__racy(void)106 void test_diff_racediffiter__racy(void)
107 {
108 git_diff_options opts = GIT_DIFF_OPTIONS_INIT;
109 git_repository *repo = cl_git_sandbox_init("diff");
110 git_diff *diff;
111
112 basic_payload exp_a[] = {
113 { "another.txt", GIT_DELTA_MODIFIED },
114 { "readme.txt", GIT_DELTA_MODIFIED },
115 { NULL, 0 }
116 };
117
118 racy_payload pay = { true, "diff/zzzzz", exp_a };
119
120 cl_must_pass(p_mkdir("diff/zzzzz", 0777));
121
122 opts.flags |= GIT_DIFF_INCLUDE_IGNORED | GIT_DIFF_RECURSE_UNTRACKED_DIRS;
123 opts.notify_cb = notify_cb__racy_rmdir;
124 opts.payload = &pay;
125
126 cl_git_pass(git_diff_index_to_workdir(&diff, repo, NULL, &opts));
127
128 git_diff_free(diff);
129 }
130