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