2 * Copyright (C) the libgit2 contributors. All rights reserved.
4 * This file is part of libgit2, distributed under the GNU GPL v2 with
5 * a Linking Exception. For full terms see the included COPYING file.
8 #include "config_parse.h"
12 const char *git_config_escapes
= "ntb\"\\";
13 const char *git_config_escaped
= "\n\t\b\"\\";
15 static void set_parse_error(git_config_parser
*reader
, int col
, const char *error_str
)
18 git_error_set(GIT_ERROR_CONFIG
,
19 "failed to parse config file: %s (in %s:%"PRIuZ
", column %d)",
20 error_str
, reader
->path
, reader
->ctx
.line_num
, col
);
22 git_error_set(GIT_ERROR_CONFIG
,
23 "failed to parse config file: %s (in %s:%"PRIuZ
")",
24 error_str
, reader
->path
, reader
->ctx
.line_num
);
28 GIT_INLINE(int) config_keychar(int c
)
30 return isalnum(c
) || c
== '-';
33 static int strip_comments(char *line
, int in_quotes
)
35 int quote_count
= in_quotes
, backslash_count
= 0;
38 for (ptr
= line
; *ptr
; ++ptr
) {
39 if (ptr
[0] == '"' && ((ptr
> line
&& ptr
[-1] != '\\') || ptr
== line
))
42 if ((ptr
[0] == ';' || ptr
[0] == '#') &&
43 (quote_count
% 2) == 0 &&
44 (backslash_count
% 2) == 0) {
55 /* skip any space at the end */
56 while (ptr
> line
&& git__isspace(ptr
[-1])) {
65 static int parse_subsection_header(git_config_parser
*reader
, const char *line
, size_t pos
, const char *base_name
, char **section_name
)
68 const char *first_quote
, *last_quote
;
69 const char *line_start
= line
;
70 git_str buf
= GIT_STR_INIT
;
71 size_t quoted_len
, alloc_len
, base_name_len
= strlen(base_name
);
73 /* Skip any additional whitespace before our section name */
74 while (git__isspace(line
[pos
]))
77 /* We should be at the first quotation mark. */
78 if (line
[pos
] != '"') {
79 set_parse_error(reader
, 0, "missing quotation marks in section header");
83 first_quote
= &line
[pos
];
84 last_quote
= strrchr(line
, '"');
85 quoted_len
= last_quote
- first_quote
;
87 if ((last_quote
- line
) > INT_MAX
) {
88 set_parse_error(reader
, 0, "invalid section header, line too long");
92 if (quoted_len
== 0) {
93 set_parse_error(reader
, 0, "missing closing quotation mark in section header");
97 GIT_ERROR_CHECK_ALLOC_ADD(&alloc_len
, base_name_len
, quoted_len
);
98 GIT_ERROR_CHECK_ALLOC_ADD(&alloc_len
, alloc_len
, 2);
100 if (git_str_grow(&buf
, alloc_len
) < 0 ||
101 git_str_printf(&buf
, "%s.", base_name
) < 0)
110 * At the end of each iteration, whatever is stored in c will be
111 * added to the string. In case of error, jump to out
117 set_parse_error(reader
, 0, "unexpected end-of-line in section header");
127 set_parse_error(reader
, rpos
, "unexpected end-of-line in section header");
135 git_str_putc(&buf
, (char)c
);
137 } while (line
+ rpos
< last_quote
);
140 if (git_str_oom(&buf
))
143 if (line
[rpos
] != '"' || line
[rpos
+ 1] != ']') {
144 set_parse_error(reader
, rpos
, "unexpected text after closing quotes");
145 git_str_dispose(&buf
);
149 *section_name
= git_str_detach(&buf
);
150 return (int)(&line
[rpos
+ 2] - line_start
); /* rpos is at the closing quote */
153 git_str_dispose(&buf
);
158 static int parse_section_header(git_config_parser
*reader
, char **section_out
)
160 char *name
, *name_end
;
161 int name_length
, c
, pos
;
166 git_parse_advance_ws(&reader
->ctx
);
167 line
= git__strndup(reader
->ctx
.line
, reader
->ctx
.line_len
);
171 /* find the end of the variable's name */
172 name_end
= strrchr(line
, ']');
173 if (name_end
== NULL
) {
175 set_parse_error(reader
, 0, "missing ']' in section header");
179 GIT_ERROR_CHECK_ALLOC_ADD(&line_len
, (size_t)(name_end
- line
), 1);
180 name
= git__malloc(line_len
);
181 GIT_ERROR_CHECK_ALLOC(name
);
186 /* Make sure we were given a section header */
188 GIT_ASSERT(c
== '[');
193 if (git__isspace(c
)){
194 name
[name_length
] = '\0';
195 result
= parse_subsection_header(reader
, line
, pos
, name
, section_out
);
201 if (!config_keychar(c
) && c
!= '.') {
202 set_parse_error(reader
, pos
, "unexpected character in header");
206 name
[name_length
++] = (char)git__tolower(c
);
208 } while ((c
= line
[pos
++]) != ']');
210 if (line
[pos
- 1] != ']') {
211 set_parse_error(reader
, pos
, "unexpected end of file");
217 name
[name_length
] = 0;
228 static int skip_bom(git_parse_ctx
*parser
)
230 git_str buf
= GIT_STR_INIT_CONST(parser
->content
, parser
->content_len
);
232 int bom_offset
= git_str_detect_bom(&bom
, &buf
);
234 if (bom
== GIT_STR_BOM_UTF8
)
235 git_parse_advance_chars(parser
, bom_offset
);
237 /* TODO: reference implementation is pretty stupid with BoM */
245 integer = digit { digit }
246 alphabet = "a".."z" + "A" .. "Z"
248 section_char = alphabet | "." | "-"
249 extension_char = (* any character except newline *)
250 any_char = (* any character *)
251 variable_char = "alphabet" | "-"
257 section = header { definition }
259 header = "[" section [subsection | subsection_ext] "]"
261 subsection = "." section
262 subsection_ext = "\"" extension "\""
264 section = section_char { section_char }
265 extension = extension_char { extension_char }
267 definition = variable_name ["=" variable_value] "\n"
269 variable_name = variable_char { variable_char }
270 variable_value = string | boolean | integer
272 string = quoted_string | plain_string
273 quoted_string = "\"" plain_string "\""
274 plain_string = { any_char }
276 boolean = boolean_true | boolean_false
277 boolean_true = "yes" | "1" | "true" | "on"
278 boolean_false = "no" | "0" | "false" | "off"
281 /* '\"' -> '"' etc */
282 static int unescape_line(
283 char **out
, bool *is_multi
, const char *ptr
, int quote_count
)
285 char *str
, *fixed
, *esc
;
286 size_t ptr_len
= strlen(ptr
), alloc_len
;
290 if (GIT_ADD_SIZET_OVERFLOW(&alloc_len
, ptr_len
, 1) ||
291 (str
= git__malloc(alloc_len
)) == NULL
) {
297 while (*ptr
!= '\0') {
300 } else if (*ptr
!= '\\') {
303 /* backslash, check the next char */
305 /* if we're at the end, it's a multiline, so keep the backslash */
310 if ((esc
= strchr(git_config_escapes
, *ptr
)) != NULL
) {
311 *fixed
++ = git_config_escaped
[esc
- git_config_escapes
];
314 git_error_set(GIT_ERROR_CONFIG
, "invalid escape at %s", ptr
);
328 static int parse_multiline_variable(git_config_parser
*reader
, git_str
*value
, int in_quotes
, size_t *line_len
)
331 bool multiline
= true;
334 char *line
= NULL
, *proc_line
= NULL
;
337 /* Check that the next line exists */
338 git_parse_advance_line(&reader
->ctx
);
339 line
= git__strndup(reader
->ctx
.line
, reader
->ctx
.line_len
);
340 GIT_ERROR_CHECK_ALLOC(line
);
341 if (GIT_ADD_SIZET_OVERFLOW(line_len
, *line_len
, reader
->ctx
.line_len
)) {
347 * We've reached the end of the file, there is no continuation.
348 * (this is not an error).
350 if (line
[0] == '\0') {
355 /* If it was just a comment, pretend it didn't exist */
356 quote_count
= strip_comments(line
, in_quotes
);
360 if ((error
= unescape_line(&proc_line
, &multiline
,
361 line
, in_quotes
)) < 0)
364 /* Add this line to the multiline var */
365 if ((error
= git_str_puts(value
, proc_line
)) < 0)
370 git__free(proc_line
);
371 in_quotes
= quote_count
;
376 git__free(proc_line
);
383 GIT_INLINE(bool) is_namechar(char c
)
385 return isalnum(c
) || c
== '-';
388 static int parse_name(
389 char **name
, const char **value
, git_config_parser
*reader
, const char *line
)
391 const char *name_end
= line
, *value_start
;
396 while (*name_end
&& is_namechar(*name_end
))
399 if (line
== name_end
) {
400 set_parse_error(reader
, 0, "invalid configuration key");
404 value_start
= name_end
;
406 while (*value_start
&& git__isspace(*value_start
))
409 if (*value_start
== '=') {
410 *value
= value_start
+ 1;
411 } else if (*value_start
) {
412 set_parse_error(reader
, 0, "invalid configuration key");
416 if ((*name
= git__strndup(line
, name_end
- line
)) == NULL
)
422 static int parse_variable(git_config_parser
*reader
, char **var_name
, char **var_value
, size_t *line_len
)
424 const char *value_start
= NULL
;
425 char *line
= NULL
, *name
= NULL
, *value
= NULL
;
426 int quote_count
, error
;
432 git_parse_advance_ws(&reader
->ctx
);
433 line
= git__strndup(reader
->ctx
.line
, reader
->ctx
.line_len
);
434 GIT_ERROR_CHECK_ALLOC(line
);
436 quote_count
= strip_comments(line
, 0);
438 if ((error
= parse_name(&name
, &value_start
, reader
, line
)) < 0)
442 * Now, let's try to parse the value
444 if (value_start
!= NULL
) {
445 while (git__isspace(value_start
[0]))
448 if ((error
= unescape_line(&value
, &multiline
, value_start
, 0)) < 0)
452 git_str multi_value
= GIT_STR_INIT
;
453 git_str_attach(&multi_value
, value
, 0);
456 if (parse_multiline_variable(reader
, &multi_value
, quote_count
% 2, line_len
) < 0 ||
457 git_str_oom(&multi_value
)) {
459 git_str_dispose(&multi_value
);
463 value
= git_str_detach(&multi_value
);
479 int git_config_parser_init(git_config_parser
*out
, const char *path
, const char *data
, size_t datalen
)
482 return git_parse_ctx_init(&out
->ctx
, data
, datalen
);
485 void git_config_parser_dispose(git_config_parser
*parser
)
487 git_parse_ctx_clear(&parser
->ctx
);
490 int git_config_parse(
491 git_config_parser
*parser
,
492 git_config_parser_section_cb on_section
,
493 git_config_parser_variable_cb on_variable
,
494 git_config_parser_comment_cb on_comment
,
495 git_config_parser_eof_cb on_eof
,
499 char *current_section
= NULL
, *var_name
= NULL
, *var_value
= NULL
;
506 for (; ctx
->remain_len
> 0; git_parse_advance_line(ctx
)) {
507 const char *line_start
;
512 line_start
= ctx
->line
;
513 line_len
= ctx
->line_len
;
516 * Get either first non-whitespace character or, if that does
517 * not exist, the first whitespace character. This is required
518 * to preserve whitespaces when writing back the file.
520 if (git_parse_peek(&c
, ctx
, GIT_PARSE_PEEK_SKIP_WHITESPACE
) < 0 &&
521 git_parse_peek(&c
, ctx
, 0) < 0)
525 case '[': /* section header, new section begins */
526 git__free(current_section
);
527 current_section
= NULL
;
529 result
= parse_section_header(parser
, ¤t_section
);
533 git_parse_advance_chars(ctx
, result
);
536 result
= on_section(parser
, current_section
, line_start
, line_len
, payload
);
538 * After we've parsed the section header we may not be
539 * done with the line. If there's still data in there,
540 * run the next loop with the rest of the current line
541 * instead of moving forward.
544 if (!git_parse_peek(&c
, ctx
, GIT_PARSE_PEEK_SKIP_WHITESPACE
))
549 case '\n': /* comment or whitespace-only */
556 result
= on_comment(parser
, line_start
, line_len
, payload
);
560 default: /* assume variable declaration */
561 if ((result
= parse_variable(parser
, &var_name
, &var_value
, &line_len
)) == 0 && on_variable
) {
562 result
= on_variable(parser
, current_section
, var_name
, var_value
, line_start
, line_len
, payload
);
564 git__free(var_value
);
575 result
= on_eof(parser
, current_section
, payload
);
578 git__free(current_section
);