Skip to content

Commit 0df86b6

Browse files
thaliaarchigitster
authored andcommitted
fast-import: tighten path unquoting
Path parsing in fast-import is inconsistent and many unquoting errors are suppressed or not checked. <path> appears in the grammar in these places: filemodify ::= 'M' SP <mode> (<dataref> | 'inline') SP <path> LF filedelete ::= 'D' SP <path> LF filecopy ::= 'C' SP <path> SP <path> LF filerename ::= 'R' SP <path> SP <path> LF ls ::= 'ls' SP <dataref> SP <path> LF ls-commit ::= 'ls' SP <path> LF and fast-import.c parses them in five different ways: 1. For filemodify and filedelete: Try to unquote <path>. If it unquotes without errors, use the unquoted version; otherwise, treat it as literal bytes to the end of the line (including any number of SP). 2. For filecopy (source) and filerename (source): Try to unquote <path>. If it unquotes without errors, use the unquoted version; otherwise, treat it as literal bytes up to, but not including, the next SP. 3. For filecopy (dest) and filerename (dest): Like 1., but an unquoted empty string is forbidden. 4. For ls: If <path> starts with `"`, unquote it and report parse errors; otherwise, treat it as literal bytes to the end of the line (including any number of SP). 5. For ls-commit: Unquote <path> and report parse errors. (It must start with `"` to disambiguate from ls.) In the first three, any errors from trying to unquote a string are suppressed, so a quoted string that contains invalid escapes would be interpreted as literal bytes. For example, `"\xff"` would fail to unquote (because hex escapes are not supported), and it would instead be interpreted as the byte sequence '"', '\\', 'x', 'f', 'f', '"', which is certainly not intended. Some front-ends erroneously use their language's standard quoting routine instead of matching Git's, which could silently introduce escapes that would be incorrectly parsed due to this and lead to data corruption. The documentation states “To use a source path that contains SP the path must be quoted.”, so it is expected that some implementations depend on spaces being allowed in paths in the final position. Thus we have two documented ways to parse paths, so simplify the implementation to that. Now we have: 1. `parse_path_eol` for filemodify, filedelete, filecopy (dest), filerename (dest), ls, and ls-commit: If <path> starts with `"`, unquote it and report parse errors; otherwise, treat it as literal bytes to the end of the line (including any number of SP). 2. `parse_path_space` for filecopy (source) and filerename (source): If <path> starts with `"`, unquote it and report parse errors; otherwise, treat it as literal bytes up to, but not including, the next SP. It must be followed by SP. There remain two special cases: The dest <path> in filecopy and rename cannot be an unquoted empty string (this will be addressed subsequently) and <path> in ls-commit must be quoted to disambiguate it from ls. Signed-off-by: Thalia Archibald <[email protected]> Signed-off-by: Junio C Hamano <[email protected]>
1 parent 91ec36f commit 0df86b6

File tree

2 files changed

+322
-44
lines changed

2 files changed

+322
-44
lines changed

builtin/fast-import.c

Lines changed: 65 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -2258,10 +2258,60 @@ static uintmax_t parse_mark_ref_space(const char **p)
22582258
return mark;
22592259
}
22602260

2261+
/*
2262+
* Parse the path string into the strbuf. The path can either be quoted with
2263+
* escape sequences or unquoted without escape sequences. Unquoted strings may
2264+
* contain spaces only if `is_last_field` is nonzero; otherwise, it stops
2265+
* parsing at the first space.
2266+
*/
2267+
static void parse_path(struct strbuf *sb, const char *p, const char **endp,
2268+
int is_last_field, const char *field)
2269+
{
2270+
if (*p == '"') {
2271+
if (unquote_c_style(sb, p, endp))
2272+
die("Invalid %s: %s", field, command_buf.buf);
2273+
} else {
2274+
/*
2275+
* Unless we are parsing the last field of a line,
2276+
* SP is the end of this field.
2277+
*/
2278+
*endp = is_last_field
2279+
? p + strlen(p)
2280+
: strchrnul(p, ' ');
2281+
strbuf_add(sb, p, *endp - p);
2282+
}
2283+
}
2284+
2285+
/*
2286+
* Parse the path string into the strbuf, and complain if this is not the end of
2287+
* the string. Unquoted strings may contain spaces.
2288+
*/
2289+
static void parse_path_eol(struct strbuf *sb, const char *p, const char *field)
2290+
{
2291+
const char *end;
2292+
2293+
parse_path(sb, p, &end, 1, field);
2294+
if (*end)
2295+
die("Garbage after %s: %s", field, command_buf.buf);
2296+
}
2297+
2298+
/*
2299+
* Parse the path string into the strbuf, and ensure it is followed by a space.
2300+
* Unquoted strings may not contain spaces. Update *endp to point to the first
2301+
* character after the space.
2302+
*/
2303+
static void parse_path_space(struct strbuf *sb, const char *p,
2304+
const char **endp, const char *field)
2305+
{
2306+
parse_path(sb, p, endp, 0, field);
2307+
if (**endp != ' ')
2308+
die("Missing space after %s: %s", field, command_buf.buf);
2309+
(*endp)++;
2310+
}
2311+
22612312
static void file_change_m(const char *p, struct branch *b)
22622313
{
22632314
static struct strbuf uq = STRBUF_INIT;
2264-
const char *endp;
22652315
struct object_entry *oe;
22662316
struct object_id oid;
22672317
uint16_t mode, inline_data = 0;
@@ -2299,11 +2349,8 @@ static void file_change_m(const char *p, struct branch *b)
22992349
}
23002350

23012351
strbuf_reset(&uq);
2302-
if (!unquote_c_style(&uq, p, &endp)) {
2303-
if (*endp)
2304-
die("Garbage after path in: %s", command_buf.buf);
2305-
p = uq.buf;
2306-
}
2352+
parse_path_eol(&uq, p, "path");
2353+
p = uq.buf;
23072354

23082355
/* Git does not track empty, non-toplevel directories. */
23092356
if (S_ISDIR(mode) && is_empty_tree_oid(&oid) && *p) {
@@ -2367,48 +2414,29 @@ static void file_change_m(const char *p, struct branch *b)
23672414
static void file_change_d(const char *p, struct branch *b)
23682415
{
23692416
static struct strbuf uq = STRBUF_INIT;
2370-
const char *endp;
23712417

23722418
strbuf_reset(&uq);
2373-
if (!unquote_c_style(&uq, p, &endp)) {
2374-
if (*endp)
2375-
die("Garbage after path in: %s", command_buf.buf);
2376-
p = uq.buf;
2377-
}
2419+
parse_path_eol(&uq, p, "path");
2420+
p = uq.buf;
23782421
tree_content_remove(&b->branch_tree, p, NULL, 1);
23792422
}
23802423

2381-
static void file_change_cr(const char *s, struct branch *b, int rename)
2424+
static void file_change_cr(const char *p, struct branch *b, int rename)
23822425
{
2383-
const char *d;
2426+
const char *s, *d;
23842427
static struct strbuf s_uq = STRBUF_INIT;
23852428
static struct strbuf d_uq = STRBUF_INIT;
2386-
const char *endp;
23872429
struct tree_entry leaf;
23882430

23892431
strbuf_reset(&s_uq);
2390-
if (!unquote_c_style(&s_uq, s, &endp)) {
2391-
if (*endp != ' ')
2392-
die("Missing space after source: %s", command_buf.buf);
2393-
} else {
2394-
endp = strchr(s, ' ');
2395-
if (!endp)
2396-
die("Missing space after source: %s", command_buf.buf);
2397-
strbuf_add(&s_uq, s, endp - s);
2398-
}
2432+
parse_path_space(&s_uq, p, &p, "source");
23992433
s = s_uq.buf;
24002434

2401-
endp++;
2402-
if (!*endp)
2435+
if (!*p)
24032436
die("Missing dest: %s", command_buf.buf);
2404-
2405-
d = endp;
24062437
strbuf_reset(&d_uq);
2407-
if (!unquote_c_style(&d_uq, d, &endp)) {
2408-
if (*endp)
2409-
die("Garbage after dest in: %s", command_buf.buf);
2410-
d = d_uq.buf;
2411-
}
2438+
parse_path_eol(&d_uq, p, "dest");
2439+
d = d_uq.buf;
24122440

24132441
memset(&leaf, 0, sizeof(leaf));
24142442
if (rename)
@@ -3152,6 +3180,7 @@ static void print_ls(int mode, const unsigned char *hash, const char *path)
31523180

31533181
static void parse_ls(const char *p, struct branch *b)
31543182
{
3183+
static struct strbuf uq = STRBUF_INIT;
31553184
struct tree_entry *root = NULL;
31563185
struct tree_entry leaf = {NULL};
31573186

@@ -3168,16 +3197,9 @@ static void parse_ls(const char *p, struct branch *b)
31683197
root->versions[1].mode = S_IFDIR;
31693198
load_tree(root);
31703199
}
3171-
if (*p == '"') {
3172-
static struct strbuf uq = STRBUF_INIT;
3173-
const char *endp;
3174-
strbuf_reset(&uq);
3175-
if (unquote_c_style(&uq, p, &endp))
3176-
die("Invalid path: %s", command_buf.buf);
3177-
if (*endp)
3178-
die("Garbage after path in: %s", command_buf.buf);
3179-
p = uq.buf;
3180-
}
3200+
strbuf_reset(&uq);
3201+
parse_path_eol(&uq, p, "path");
3202+
p = uq.buf;
31813203
tree_content_get(root, p, &leaf, 1);
31823204
/*
31833205
* A directory in preparation would have a sha1 of zero

0 commit comments

Comments
 (0)