Skip to content

Commit dda83e6

Browse files
committed
Merge branch 'jk/shorten-unambiguous-ref-wo-sscanf'
sscanf(3) used in "git symbolic-ref --short" implementation found to be not working reliably on macOS in UTF-8 locales. Rewrite the code to avoid sscanf() altogether to work it around. * jk/shorten-unambiguous-ref-wo-sscanf: shorten_unambiguous_ref(): avoid sscanf() shorten_unambiguous_ref(): use NUM_REV_PARSE_RULES constant shorten_unambiguous_ref(): avoid integer truncation
2 parents 7dc55a0 + 613bef5 commit dda83e6

File tree

2 files changed

+82
-45
lines changed

2 files changed

+82
-45
lines changed

refs.c

Lines changed: 48 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1310,65 +1310,68 @@ int update_ref(const char *msg, const char *refname,
13101310
old_oid, flags, onerr);
13111311
}
13121312

1313+
/*
1314+
* Check that the string refname matches a rule of the form
1315+
* "{prefix}%.*s{suffix}". So "foo/bar/baz" would match the rule
1316+
* "foo/%.*s/baz", and return the string "bar".
1317+
*/
1318+
static const char *match_parse_rule(const char *refname, const char *rule,
1319+
size_t *len)
1320+
{
1321+
/*
1322+
* Check that rule matches refname up to the first percent in the rule.
1323+
* We can bail immediately if not, but otherwise we leave "rule" at the
1324+
* %-placeholder, and "refname" at the start of the potential matched
1325+
* name.
1326+
*/
1327+
while (*rule != '%') {
1328+
if (!*rule)
1329+
BUG("rev-parse rule did not have percent");
1330+
if (*refname++ != *rule++)
1331+
return NULL;
1332+
}
1333+
1334+
/*
1335+
* Check that our "%" is the expected placeholder. This assumes there
1336+
* are no other percents (placeholder or quoted) in the string, but
1337+
* that is sufficient for our rev-parse rules.
1338+
*/
1339+
if (!skip_prefix(rule, "%.*s", &rule))
1340+
return NULL;
1341+
1342+
/*
1343+
* And now check that our suffix (if any) matches.
1344+
*/
1345+
if (!strip_suffix(refname, rule, len))
1346+
return NULL;
1347+
1348+
return refname; /* len set by strip_suffix() */
1349+
}
1350+
13131351
char *refs_shorten_unambiguous_ref(struct ref_store *refs,
13141352
const char *refname, int strict)
13151353
{
13161354
int i;
1317-
static char **scanf_fmts;
1318-
static int nr_rules;
1319-
char *short_name;
13201355
struct strbuf resolved_buf = STRBUF_INIT;
13211356

1322-
if (!nr_rules) {
1323-
/*
1324-
* Pre-generate scanf formats from ref_rev_parse_rules[].
1325-
* Generate a format suitable for scanf from a
1326-
* ref_rev_parse_rules rule by interpolating "%s" at the
1327-
* location of the "%.*s".
1328-
*/
1329-
size_t total_len = 0;
1330-
size_t offset = 0;
1331-
1332-
/* the rule list is NULL terminated, count them first */
1333-
for (nr_rules = 0; ref_rev_parse_rules[nr_rules]; nr_rules++)
1334-
/* -2 for strlen("%.*s") - strlen("%s"); +1 for NUL */
1335-
total_len += strlen(ref_rev_parse_rules[nr_rules]) - 2 + 1;
1336-
1337-
scanf_fmts = xmalloc(st_add(st_mult(sizeof(char *), nr_rules), total_len));
1338-
1339-
offset = 0;
1340-
for (i = 0; i < nr_rules; i++) {
1341-
assert(offset < total_len);
1342-
scanf_fmts[i] = (char *)&scanf_fmts[nr_rules] + offset;
1343-
offset += xsnprintf(scanf_fmts[i], total_len - offset,
1344-
ref_rev_parse_rules[i], 2, "%s") + 1;
1345-
}
1346-
}
1347-
1348-
/* bail out if there are no rules */
1349-
if (!nr_rules)
1350-
return xstrdup(refname);
1351-
1352-
/* buffer for scanf result, at most refname must fit */
1353-
short_name = xstrdup(refname);
1354-
13551357
/* skip first rule, it will always match */
1356-
for (i = nr_rules - 1; i > 0 ; --i) {
1358+
for (i = NUM_REV_PARSE_RULES - 1; i > 0 ; --i) {
13571359
int j;
13581360
int rules_to_fail = i;
1359-
int short_name_len;
1361+
const char *short_name;
1362+
size_t short_name_len;
13601363

1361-
if (1 != sscanf(refname, scanf_fmts[i], short_name))
1364+
short_name = match_parse_rule(refname, ref_rev_parse_rules[i],
1365+
&short_name_len);
1366+
if (!short_name)
13621367
continue;
13631368

1364-
short_name_len = strlen(short_name);
1365-
13661369
/*
13671370
* in strict mode, all (except the matched one) rules
13681371
* must fail to resolve to a valid non-ambiguous ref
13691372
*/
13701373
if (strict)
1371-
rules_to_fail = nr_rules;
1374+
rules_to_fail = NUM_REV_PARSE_RULES;
13721375

13731376
/*
13741377
* check if the short name resolves to a valid ref,
@@ -1388,7 +1391,8 @@ char *refs_shorten_unambiguous_ref(struct ref_store *refs,
13881391
*/
13891392
strbuf_reset(&resolved_buf);
13901393
strbuf_addf(&resolved_buf, rule,
1391-
short_name_len, short_name);
1394+
cast_size_t_to_int(short_name_len),
1395+
short_name);
13921396
if (refs_ref_exists(refs, resolved_buf.buf))
13931397
break;
13941398
}
@@ -1399,12 +1403,11 @@ char *refs_shorten_unambiguous_ref(struct ref_store *refs,
13991403
*/
14001404
if (j == rules_to_fail) {
14011405
strbuf_release(&resolved_buf);
1402-
return short_name;
1406+
return xmemdupz(short_name, short_name_len);
14031407
}
14041408
}
14051409

14061410
strbuf_release(&resolved_buf);
1407-
free(short_name);
14081411
return xstrdup(refname);
14091412
}
14101413

t/t1401-symbolic-ref.sh

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,4 +189,38 @@ test_expect_success 'symbolic-ref pointing at another' '
189189
test_cmp expect actual
190190
'
191191

192+
test_expect_success 'symbolic-ref --short handles complex utf8 case' '
193+
name="测试-加-增加-加-增加" &&
194+
git symbolic-ref TEST_SYMREF "refs/heads/$name" &&
195+
# In the real world, we saw problems with this case only
196+
# when the locale includes UTF-8. Set it here to try to make things as
197+
# hard as possible for us to pass, but in practice we should do the
198+
# right thing regardless (and of course some platforms may not even
199+
# have this locale).
200+
LC_ALL=en_US.UTF-8 git symbolic-ref --short TEST_SYMREF >actual &&
201+
echo "$name" >expect &&
202+
test_cmp expect actual
203+
'
204+
205+
test_expect_success 'symbolic-ref --short handles name with suffix' '
206+
git symbolic-ref TEST_SYMREF "refs/remotes/origin/HEAD" &&
207+
git symbolic-ref --short TEST_SYMREF >actual &&
208+
echo "origin" >expect &&
209+
test_cmp expect actual
210+
'
211+
212+
test_expect_success 'symbolic-ref --short handles almost-matching name' '
213+
git symbolic-ref TEST_SYMREF "refs/headsXfoo" &&
214+
git symbolic-ref --short TEST_SYMREF >actual &&
215+
echo "headsXfoo" >expect &&
216+
test_cmp expect actual
217+
'
218+
219+
test_expect_success 'symbolic-ref --short handles name with percent' '
220+
git symbolic-ref TEST_SYMREF "refs/heads/%foo" &&
221+
git symbolic-ref --short TEST_SYMREF >actual &&
222+
echo "%foo" >expect &&
223+
test_cmp expect actual
224+
'
225+
192226
test_done

0 commit comments

Comments
 (0)