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"
14 const char *git_config_escapes
= "ntb\"\\";
15 const char *git_config_escaped
= "\n\t\b\"\\";
17 static void set_parse_error(git_config_parser
*reader
, int col
, const char *error_str
)
20 git_error_set(GIT_ERROR_CONFIG
,
21 "failed to parse config file: %s (in %s:%"PRIuZ
", column %d)",
22 error_str
, reader
->path
, reader
->ctx
.line_num
, col
);
24 git_error_set(GIT_ERROR_CONFIG
,
25 "failed to parse config file: %s (in %s:%"PRIuZ
")",
26 error_str
, reader
->path
, reader
->ctx
.line_num
);
30 GIT_INLINE(int) config_keychar(int c
)
32 return isalnum(c
) || c
== '-';
35 static int strip_comments(char *line
, int in_quotes
)
37 int quote_count
= in_quotes
, backslash_count
= 0;
40 for (ptr
= line
; *ptr
; ++ptr
) {
41 if (ptr
[0] == '"' && ptr
> line
&& ptr
[-1] != '\\')
44 if ((ptr
[0] == ';' || ptr
[0] == '#') &&
45 (quote_count
% 2) == 0 &&
46 (backslash_count
% 2) == 0) {
57 /* skip any space at the end */
58 while (ptr
> line
&& git__isspace(ptr
[-1])) {
67 static int parse_subsection_header(git_config_parser
*reader
, const char *line
, size_t pos
, const char *base_name
, char **section_name
)
70 const char *first_quote
, *last_quote
;
71 const char *line_start
= line
;
72 git_buf buf
= GIT_BUF_INIT
;
73 size_t quoted_len
, alloc_len
, base_name_len
= strlen(base_name
);
75 /* Skip any additional whitespace before our section name */
76 while (git__isspace(line
[pos
]))
79 /* We should be at the first quotation mark. */
80 if (line
[pos
] != '"') {
81 set_parse_error(reader
, 0, "missing quotation marks in section header");
85 first_quote
= &line
[pos
];
86 last_quote
= strrchr(line
, '"');
87 quoted_len
= last_quote
- first_quote
;
89 if ((last_quote
- line
) > INT_MAX
) {
90 set_parse_error(reader
, 0, "invalid section header, line too long");
94 if (quoted_len
== 0) {
95 set_parse_error(reader
, 0, "missing closing quotation mark in section header");
99 GIT_ERROR_CHECK_ALLOC_ADD(&alloc_len
, base_name_len
, quoted_len
);
100 GIT_ERROR_CHECK_ALLOC_ADD(&alloc_len
, alloc_len
, 2);
102 if (git_buf_grow(&buf
, alloc_len
) < 0 ||
103 git_buf_printf(&buf
, "%s.", base_name
) < 0)
112 * At the end of each iteration, whatever is stored in c will be
113 * added to the string. In case of error, jump to out
119 set_parse_error(reader
, 0, "unexpected end-of-line in section header");
129 set_parse_error(reader
, rpos
, "unexpected end-of-line in section header");
137 git_buf_putc(&buf
, (char)c
);
139 } while (line
+ rpos
< last_quote
);
142 if (git_buf_oom(&buf
))
145 if (line
[rpos
] != '"' || line
[rpos
+ 1] != ']') {
146 set_parse_error(reader
, rpos
, "unexpected text after closing quotes");
147 git_buf_dispose(&buf
);
151 *section_name
= git_buf_detach(&buf
);
152 return (int)(&line
[rpos
+ 2] - line_start
); /* rpos is at the closing quote */
155 git_buf_dispose(&buf
);
160 static int parse_section_header(git_config_parser
*reader
, char **section_out
)
162 char *name
, *name_end
;
163 int name_length
, c
, pos
;
168 git_parse_advance_ws(&reader
->ctx
);
169 line
= git__strndup(reader
->ctx
.line
, reader
->ctx
.line_len
);
173 /* find the end of the variable's name */
174 name_end
= strrchr(line
, ']');
175 if (name_end
== NULL
) {
177 set_parse_error(reader
, 0, "missing ']' in section header");
181 GIT_ERROR_CHECK_ALLOC_ADD(&line_len
, (size_t)(name_end
- line
), 1);
182 name
= git__malloc(line_len
);
183 GIT_ERROR_CHECK_ALLOC(name
);
188 /* Make sure we were given a section header */
195 if (git__isspace(c
)){
196 name
[name_length
] = '\0';
197 result
= parse_subsection_header(reader
, line
, pos
, name
, section_out
);
203 if (!config_keychar(c
) && c
!= '.') {
204 set_parse_error(reader
, pos
, "unexpected character in header");
208 name
[name_length
++] = (char)git__tolower(c
);
210 } while ((c
= line
[pos
++]) != ']');
212 if (line
[pos
- 1] != ']') {
213 set_parse_error(reader
, pos
, "unexpected end of file");
219 name
[name_length
] = 0;
230 static int skip_bom(git_parse_ctx
*parser
)
232 git_buf buf
= GIT_BUF_INIT_CONST(parser
->content
, parser
->content_len
);
234 int bom_offset
= git_buf_text_detect_bom(&bom
, &buf
);
236 if (bom
== GIT_BOM_UTF8
)
237 git_parse_advance_chars(parser
, bom_offset
);
239 /* TODO: reference implementation is pretty stupid with BoM */
247 integer = digit { digit }
248 alphabet = "a".."z" + "A" .. "Z"
250 section_char = alphabet | "." | "-"
251 extension_char = (* any character except newline *)
252 any_char = (* any character *)
253 variable_char = "alphabet" | "-"
259 section = header { definition }
261 header = "[" section [subsection | subsection_ext] "]"
263 subsection = "." section
264 subsection_ext = "\"" extension "\""
266 section = section_char { section_char }
267 extension = extension_char { extension_char }
269 definition = variable_name ["=" variable_value] "\n"
271 variable_name = variable_char { variable_char }
272 variable_value = string | boolean | integer
274 string = quoted_string | plain_string
275 quoted_string = "\"" plain_string "\""
276 plain_string = { any_char }
278 boolean = boolean_true | boolean_false
279 boolean_true = "yes" | "1" | "true" | "on"
280 boolean_false = "no" | "0" | "false" | "off"
283 /* '\"' -> '"' etc */
284 static int unescape_line(
285 char **out
, bool *is_multi
, const char *ptr
, int quote_count
)
287 char *str
, *fixed
, *esc
;
288 size_t ptr_len
= strlen(ptr
), alloc_len
;
292 if (GIT_ADD_SIZET_OVERFLOW(&alloc_len
, ptr_len
, 1) ||
293 (str
= git__malloc(alloc_len
)) == NULL
) {
299 while (*ptr
!= '\0') {
302 } else if (*ptr
!= '\\') {
305 /* backslash, check the next char */
307 /* if we're at the end, it's a multiline, so keep the backslash */
312 if ((esc
= strchr(git_config_escapes
, *ptr
)) != NULL
) {
313 *fixed
++ = git_config_escaped
[esc
- git_config_escapes
];
316 git_error_set(GIT_ERROR_CONFIG
, "invalid escape at %s", ptr
);
330 static int parse_multiline_variable(git_config_parser
*reader
, git_buf
*value
, int in_quotes
)
333 bool multiline
= true;
336 char *line
= NULL
, *proc_line
= NULL
;
339 /* Check that the next line exists */
340 git_parse_advance_line(&reader
->ctx
);
341 line
= git__strndup(reader
->ctx
.line
, reader
->ctx
.line_len
);
342 GIT_ERROR_CHECK_ALLOC(line
);
345 * We've reached the end of the file, there is no continuation.
346 * (this is not an error).
348 if (line
[0] == '\0') {
353 /* If it was just a comment, pretend it didn't exist */
354 quote_count
= strip_comments(line
, !!in_quotes
);
358 if ((error
= unescape_line(&proc_line
, &multiline
,
359 line
, in_quotes
)) < 0)
362 /* Add this line to the multiline var */
363 if ((error
= git_buf_puts(value
, proc_line
)) < 0)
368 git__free(proc_line
);
369 in_quotes
= quote_count
;
374 git__free(proc_line
);
381 GIT_INLINE(bool) is_namechar(char c
)
383 return isalnum(c
) || c
== '-';
386 static int parse_name(
387 char **name
, const char **value
, git_config_parser
*reader
, const char *line
)
389 const char *name_end
= line
, *value_start
;
394 while (*name_end
&& is_namechar(*name_end
))
397 if (line
== name_end
) {
398 set_parse_error(reader
, 0, "invalid configuration key");
402 value_start
= name_end
;
404 while (*value_start
&& git__isspace(*value_start
))
407 if (*value_start
== '=') {
408 *value
= value_start
+ 1;
409 } else if (*value_start
) {
410 set_parse_error(reader
, 0, "invalid configuration key");
414 if ((*name
= git__strndup(line
, name_end
- line
)) == NULL
)
420 static int parse_variable(git_config_parser
*reader
, char **var_name
, char **var_value
)
422 const char *value_start
= NULL
;
423 char *line
= NULL
, *name
= NULL
, *value
= NULL
;
424 int quote_count
, error
;
430 git_parse_advance_ws(&reader
->ctx
);
431 line
= git__strndup(reader
->ctx
.line
, reader
->ctx
.line_len
);
432 GIT_ERROR_CHECK_ALLOC(line
);
434 quote_count
= strip_comments(line
, 0);
436 if ((error
= parse_name(&name
, &value_start
, reader
, line
)) < 0)
440 * Now, let's try to parse the value
442 if (value_start
!= NULL
) {
443 while (git__isspace(value_start
[0]))
446 if ((error
= unescape_line(&value
, &multiline
, value_start
, 0)) < 0)
450 git_buf multi_value
= GIT_BUF_INIT
;
451 git_buf_attach(&multi_value
, value
, 0);
454 if (parse_multiline_variable(reader
, &multi_value
, quote_count
% 2) < 0 ||
455 git_buf_oom(&multi_value
)) {
457 git_buf_dispose(&multi_value
);
461 value
= git_buf_detach(&multi_value
);
477 int git_config_parser_init(git_config_parser
*out
, const char *path
, const char *data
, size_t datalen
)
480 return git_parse_ctx_init(&out
->ctx
, data
, datalen
);
483 void git_config_parser_dispose(git_config_parser
*parser
)
485 git_parse_ctx_clear(&parser
->ctx
);
488 int git_config_parse(
489 git_config_parser
*parser
,
490 git_config_parser_section_cb on_section
,
491 git_config_parser_variable_cb on_variable
,
492 git_config_parser_comment_cb on_comment
,
493 git_config_parser_eof_cb on_eof
,
497 char *current_section
= NULL
, *var_name
= NULL
, *var_value
= NULL
;
504 for (; ctx
->remain_len
> 0; git_parse_advance_line(ctx
)) {
505 const char *line_start
;
510 line_start
= ctx
->line
;
511 line_len
= ctx
->line_len
;
514 * Get either first non-whitespace character or, if that does
515 * not exist, the first whitespace character. This is required
516 * to preserve whitespaces when writing back the file.
518 if (git_parse_peek(&c
, ctx
, GIT_PARSE_PEEK_SKIP_WHITESPACE
) < 0 &&
519 git_parse_peek(&c
, ctx
, 0) < 0)
523 case '[': /* section header, new section begins */
524 git__free(current_section
);
525 current_section
= NULL
;
527 result
= parse_section_header(parser
, ¤t_section
);
531 git_parse_advance_chars(ctx
, result
);
534 result
= on_section(parser
, current_section
, line_start
, line_len
, payload
);
536 * After we've parsed the section header we may not be
537 * done with the line. If there's still data in there,
538 * run the next loop with the rest of the current line
539 * instead of moving forward.
542 if (!git_parse_peek(&c
, ctx
, GIT_PARSE_PEEK_SKIP_WHITESPACE
))
547 case '\n': /* comment or whitespace-only */
554 result
= on_comment(parser
, line_start
, line_len
, payload
);
558 default: /* assume variable declaration */
559 if ((result
= parse_variable(parser
, &var_name
, &var_value
)) == 0 && on_variable
) {
560 result
= on_variable(parser
, current_section
, var_name
, var_value
, line_start
, line_len
, payload
);
562 git__free(var_value
);
573 result
= on_eof(parser
, current_section
, payload
);
576 git__free(current_section
);