Skip to content

Commit 12e839a

Browse files
10ne1gitster
authored andcommitted
submodule: fix case-folding gitdir filesystem colisions
Add a new check in validate_submodule_git_dir() to detect and prevent case-folding filesystem colisions. When this new check is triggered, a stricter casefolding aware URI encoding is used to percent-encode uppercase characters, e.g. Foo becomes %46oo. By using this check/retry mechanism the uppercase encoding is only applied when necessary, so case-sensitive filesystems are not affected. Signed-off-by: Adrian Ratiu <adrian.ratiu@collabora.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>
1 parent 829f0f6 commit 12e839a

File tree

4 files changed

+73
-2
lines changed

4 files changed

+73
-2
lines changed

submodule.c

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2280,7 +2280,7 @@ int validate_submodule_git_dir(char *git_dir, const char *submodule_name)
22802280
size_t len = strlen(git_dir), suffix_len = strlen(submodule_name);
22812281
char *p = git_dir + len - suffix_len;
22822282
bool suffixes_match = !strcmp(p, submodule_name);
2283-
int ret = 0;
2283+
int ret = 0, config_ignorecase = 0;
22842284

22852285
/*
22862286
* We prevent the contents of sibling submodules' git directories to
@@ -2318,6 +2318,42 @@ int validate_submodule_git_dir(char *git_dir, const char *submodule_name)
23182318
if (p && strchr(p, '/') != NULL)
23192319
return error("submodule gitdir name '%s' contains unexpected '/'", p);
23202320

2321+
/* Prevent conflicts on case-folding filesystems */
2322+
repo_config_get_bool(the_repository, "core.ignorecase", &config_ignorecase);
2323+
if (ignore_case || config_ignorecase) {
2324+
char *lower_gitdir = xstrdup(git_dir);
2325+
char *module_name = find_last_submodule_name(lower_gitdir);
2326+
2327+
if (module_name) {
2328+
for (p = module_name; *p; p++)
2329+
*p = tolower(*p);
2330+
2331+
/*
2332+
* If lower path is different and already exists, check for collision.
2333+
* Intentionally double-check to eliminate false-positives.
2334+
*/
2335+
if (strcmp(lower_gitdir, git_dir) && is_git_directory(lower_gitdir)) {
2336+
char *canonical = real_pathdup(git_dir, 0);
2337+
if (canonical) {
2338+
struct strbuf norm_git_dir = STRBUF_INIT;
2339+
strbuf_addstr(&norm_git_dir, git_dir);
2340+
strbuf_normalize_path(&norm_git_dir);
2341+
2342+
if (strcmp(canonical, norm_git_dir.buf))
2343+
ret = error(_("submodule git dir '%s' "
2344+
"collides with '%s'"),
2345+
canonical, norm_git_dir.buf);
2346+
2347+
strbuf_release(&norm_git_dir);
2348+
FREE_AND_NULL(canonical);
2349+
}
2350+
}
2351+
}
2352+
2353+
FREE_AND_NULL(lower_gitdir);
2354+
return ret;
2355+
}
2356+
23212357
return 0;
23222358
}
23232359

@@ -2653,13 +2689,20 @@ void submodule_name_to_gitdir(struct strbuf *buf, struct repository *r,
26532689
if (!validate_and_set_submodule_gitdir(buf, submodule_name))
26542690
return;
26552691

2656-
/* Case 2: Try URI-safe (RFC3986) encoding first, this fixes nested gitdirs */
2692+
/* Case 2.1: Try URI-safe (RFC3986) encoding first, this fixes nested gitdirs */
26572693
strbuf_reset(buf);
26582694
repo_git_path_append(r, buf, "modules/");
26592695
strbuf_addstr_urlencode(buf, submodule_name, is_rfc3986_unreserved);
26602696
if (!validate_and_set_submodule_gitdir(buf, submodule_name))
26612697
return;
26622698

2699+
/* Case 2.2: Try extended uppercase URI (RFC3986) encoding, to fix case-folding */
2700+
strbuf_reset(buf);
2701+
repo_git_path_append(r, buf, "modules/");
2702+
strbuf_addstr_urlencode(buf, submodule_name, is_casefolding_rfc3986_unreserved);
2703+
if (!validate_and_set_submodule_gitdir(buf, submodule_name))
2704+
return;
2705+
26632706
/* Case 3: error out */
26642707
die(_("Cannot construct a valid gitdir path for submodule '%s': "
26652708
"please set a unique git config for 'submodule.%s.gitdir'."),

t/t7425-submodule-encoding.sh

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,4 +143,19 @@ test_expect_success 'submodule git dir nesting detection must work with parallel
143143
verify_submodule_gitdir_path clone_parallel hippo/hooks modules/hippo%2fhooks
144144
'
145145

146+
test_expect_success 'verify case-folding conflict is correctly encoded' '
147+
git clone -c extensions.submoduleEncoding=true -c core.ignoreCase=true main cloned-folding &&
148+
(
149+
cd cloned-folding &&
150+
151+
git submodule add ../new-sub "folding" &&
152+
test_commit lowercase &&
153+
154+
git submodule add ../new-sub "FoldinG" &&
155+
test_commit uppercase
156+
) &&
157+
verify_submodule_gitdir_path cloned-folding "folding" "modules/folding" &&
158+
verify_submodule_gitdir_path cloned-folding "FoldinG" "modules/%46oldin%47"
159+
'
160+
146161
test_done

url.c

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,18 @@ int is_rfc3986_unreserved(char ch)
1414
ch == '-' || ch == '_' || ch == '.' || ch == '~';
1515
}
1616

17+
/*
18+
* This is a variant of is_rfc3986_unreserved() that treats uppercase
19+
* letters as "reserved". This forces them to be percent-encoded, allowing
20+
* 'Foo' (%46oo) and 'foo' (foo) to be distinct on case-folding filesystems.
21+
*/
22+
int is_casefolding_rfc3986_unreserved(char c)
23+
{
24+
return (c >= 'a' && c <= 'z') ||
25+
(c >= '0' && c <= '9') ||
26+
c == '-' || c == '.' || c == '_' || c == '~';
27+
}
28+
1729
int is_urlschemechar(int first_flag, int ch)
1830
{
1931
/*

url.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,5 +22,6 @@ void end_url_with_slash(struct strbuf *buf, const char *url);
2222
void str_end_url_with_slash(const char *url, char **dest);
2323

2424
int is_rfc3986_unreserved(char ch);
25+
int is_casefolding_rfc3986_unreserved(char c);
2526

2627
#endif /* URL_H */

0 commit comments

Comments
 (0)