Coverage Report

Created: 2023-11-27 07:10

/src/libgit2/src/libgit2/config_file.c
Line
Count
Source (jump to first uncovered line)
1
/*
2
 * Copyright (C) the libgit2 contributors. All rights reserved.
3
 *
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.
6
 */
7
8
#include "config.h"
9
10
#include "git2/config.h"
11
#include "git2/sys/config.h"
12
13
#include "array.h"
14
#include "str.h"
15
#include "config_backend.h"
16
#include "config_list.h"
17
#include "config_parse.h"
18
#include "filebuf.h"
19
#include "regexp.h"
20
#include "sysdir.h"
21
#include "wildmatch.h"
22
#include "hash.h"
23
24
/* Max depth for [include] directives */
25
10
#define MAX_INCLUDE_DEPTH 10
26
27
18
#define CONFIG_FILE_TYPE "file"
28
29
typedef struct config_file {
30
  git_futils_filestamp stamp;
31
  unsigned char checksum[GIT_HASH_SHA256_SIZE];
32
  char *path;
33
  git_array_t(struct config_file) includes;
34
} config_file;
35
36
typedef struct {
37
  git_config_backend parent;
38
  git_mutex values_mutex;
39
  git_config_list *config_list;
40
  const git_repository *repo;
41
  git_config_level_t level;
42
43
  git_array_t(git_config_parser) readers;
44
45
  bool locked;
46
  git_filebuf locked_buf;
47
  git_str locked_content;
48
49
  config_file file;
50
} config_file_backend;
51
52
typedef struct {
53
  const git_repository *repo;
54
  config_file *file;
55
  git_config_list *config_list;
56
  git_config_level_t level;
57
  unsigned int depth;
58
} config_file_parse_data;
59
60
static int config_file_read(git_config_list *config_list, const git_repository *repo, config_file *file, git_config_level_t level, int depth);
61
static int config_file_read_buffer(git_config_list *config_list, const git_repository *repo, config_file *file, git_config_level_t level, int depth, const char *buf, size_t buflen);
62
static int config_file_write(config_file_backend *cfg, const char *orig_key, const char *key, const git_regexp *preg, const char *value);
63
static char *escape_value(const char *ptr);
64
65
/**
66
 * Take the current values map from the backend and increase its
67
 * refcount. This is its own function to make sure we use the mutex to
68
 * avoid the map pointer from changing under us.
69
 */
70
static int config_file_take_list(git_config_list **out, config_file_backend *b)
71
26.7k
{
72
26.7k
  int error;
73
74
26.7k
  if ((error = git_mutex_lock(&b->values_mutex)) < 0) {
75
0
    git_error_set(GIT_ERROR_OS, "failed to lock config backend");
76
0
    return error;
77
0
  }
78
79
26.7k
  git_config_list_incref(b->config_list);
80
26.7k
  *out = b->config_list;
81
82
26.7k
  git_mutex_unlock(&b->values_mutex);
83
84
26.7k
  return 0;
85
26.7k
}
86
87
static void config_file_clear(config_file *file)
88
4
{
89
4
  config_file *include;
90
4
  uint32_t i;
91
92
4
  if (file == NULL)
93
0
    return;
94
95
4
  git_array_foreach(file->includes, i, include) {
96
0
    config_file_clear(include);
97
0
  }
98
4
  git_array_clear(file->includes);
99
100
4
  git__free(file->path);
101
4
}
102
103
static int config_file_open(git_config_backend *cfg, git_config_level_t level, const git_repository *repo)
104
8
{
105
8
  config_file_backend *b = GIT_CONTAINER_OF(cfg, config_file_backend, parent);
106
8
  int res;
107
108
8
  b->level = level;
109
8
  b->repo = repo;
110
111
8
  if ((res = git_config_list_new(&b->config_list)) < 0)
112
0
    return res;
113
114
8
  if (!git_fs_path_exists(b->file.path))
115
4
    return 0;
116
117
  /*
118
   * git silently ignores configuration files that are not
119
   * readable.  We emulate that behavior.  This is particularly
120
   * important for sandboxed applications on macOS where the
121
   * git configuration files may not be readable.
122
   */
123
4
  if (p_access(b->file.path, R_OK) < 0)
124
0
    return GIT_ENOTFOUND;
125
126
4
  if (res < 0 || (res = config_file_read(b->config_list, repo, &b->file, level, 0)) < 0) {
127
0
    git_config_list_free(b->config_list);
128
0
    b->config_list = NULL;
129
0
  }
130
131
4
  return res;
132
4
}
133
134
static int config_file_is_modified(int *modified, config_file *file)
135
26.7k
{
136
26.7k
  config_file *include;
137
26.7k
  git_str buf = GIT_STR_INIT;
138
26.7k
  unsigned char checksum[GIT_HASH_SHA256_SIZE];
139
26.7k
  uint32_t i;
140
26.7k
  int error = 0;
141
142
26.7k
  *modified = 0;
143
144
26.7k
  if (!git_futils_filestamp_check(&file->stamp, file->path))
145
13.3k
    goto check_includes;
146
147
13.3k
  if ((error = git_futils_readbuffer(&buf, file->path)) < 0)
148
13.3k
    goto out;
149
150
0
  if ((error = git_hash_buf(checksum, buf.ptr, buf.size, GIT_HASH_ALGORITHM_SHA256)) < 0)
151
0
    goto out;
152
153
0
  if (memcmp(checksum, file->checksum, GIT_HASH_SHA256_SIZE) != 0) {
154
0
    *modified = 1;
155
0
    goto out;
156
0
  }
157
158
13.3k
check_includes:
159
13.3k
  git_array_foreach(file->includes, i, include) {
160
0
    if ((error = config_file_is_modified(modified, include)) < 0 || *modified)
161
0
      goto out;
162
0
  }
163
164
26.7k
out:
165
26.7k
  git_str_dispose(&buf);
166
167
26.7k
  return error;
168
13.3k
}
169
170
static void config_file_clear_includes(config_file_backend *cfg)
171
6
{
172
6
  config_file *include;
173
6
  uint32_t i;
174
175
6
  git_array_foreach(cfg->file.includes, i, include)
176
0
    config_file_clear(include);
177
6
  git_array_clear(cfg->file.includes);
178
6
}
179
180
static int config_file_set_entries(git_config_backend *cfg, git_config_list *config_list)
181
6
{
182
6
  config_file_backend *b = GIT_CONTAINER_OF(cfg, config_file_backend, parent);
183
6
  git_config_list *old = NULL;
184
6
  int error;
185
186
6
  if (b->parent.readonly) {
187
0
    git_error_set(GIT_ERROR_CONFIG, "this backend is read-only");
188
0
    return -1;
189
0
  }
190
191
6
  if ((error = git_mutex_lock(&b->values_mutex)) < 0) {
192
0
    git_error_set(GIT_ERROR_OS, "failed to lock config backend");
193
0
    goto out;
194
0
  }
195
196
6
  old = b->config_list;
197
6
  b->config_list = config_list;
198
199
6
  git_mutex_unlock(&b->values_mutex);
200
201
6
out:
202
6
  git_config_list_free(old);
203
6
  return error;
204
6
}
205
206
static int config_file_refresh_from_buffer(git_config_backend *cfg, const char *buf, size_t buflen)
207
6
{
208
6
  config_file_backend *b = GIT_CONTAINER_OF(cfg, config_file_backend, parent);
209
6
  git_config_list *config_list = NULL;
210
6
  int error;
211
212
6
  config_file_clear_includes(b);
213
214
6
  if ((error = git_config_list_new(&config_list)) < 0 ||
215
6
      (error = config_file_read_buffer(config_list, b->repo, &b->file,
216
6
               b->level, 0, buf, buflen)) < 0 ||
217
6
      (error = config_file_set_entries(cfg, config_list)) < 0)
218
0
    goto out;
219
220
6
  config_list = NULL;
221
6
out:
222
6
  git_config_list_free(config_list);
223
6
  return error;
224
6
}
225
226
static int config_file_refresh(git_config_backend *cfg)
227
26.7k
{
228
26.7k
  config_file_backend *b = GIT_CONTAINER_OF(cfg, config_file_backend, parent);
229
26.7k
  git_config_list *config_list = NULL;
230
26.7k
  int error, modified;
231
232
26.7k
  if (cfg->readonly)
233
0
    return 0;
234
235
26.7k
  if ((error = config_file_is_modified(&modified, &b->file)) < 0 && error != GIT_ENOTFOUND)
236
0
    goto out;
237
238
26.7k
  if (!modified)
239
26.7k
    return 0;
240
241
0
  config_file_clear_includes(b);
242
243
0
  if ((error = git_config_list_new(&config_list)) < 0 ||
244
0
      (error = config_file_read(config_list, b->repo, &b->file, b->level, 0)) < 0 ||
245
0
      (error = config_file_set_entries(cfg, config_list)) < 0)
246
0
    goto out;
247
248
0
  config_list = NULL;
249
0
out:
250
0
  git_config_list_free(config_list);
251
252
0
  return (error == GIT_ENOTFOUND) ? 0 : error;
253
0
}
254
255
static void config_file_free(git_config_backend *_backend)
256
4
{
257
4
  config_file_backend *backend = GIT_CONTAINER_OF(_backend, config_file_backend, parent);
258
259
4
  if (backend == NULL)
260
0
    return;
261
262
4
  config_file_clear(&backend->file);
263
4
  git_config_list_free(backend->config_list);
264
4
  git_mutex_free(&backend->values_mutex);
265
4
  git__free(backend);
266
4
}
267
268
static int config_file_iterator(
269
  git_config_iterator **iter,
270
  struct git_config_backend *backend)
271
26.7k
{
272
26.7k
  config_file_backend *b = GIT_CONTAINER_OF(backend, config_file_backend, parent);
273
26.7k
  git_config_list *dupped = NULL, *config_list = NULL;
274
26.7k
  int error;
275
276
26.7k
  if ((error = config_file_refresh(backend)) < 0 ||
277
26.7k
      (error = config_file_take_list(&config_list, b)) < 0 ||
278
26.7k
      (error = git_config_list_dup(&dupped, config_list)) < 0 ||
279
26.7k
      (error = git_config_list_iterator_new(iter, dupped)) < 0)
280
0
    goto out;
281
282
26.7k
out:
283
  /* Let iterator delete duplicated config_list when it's done */
284
26.7k
  git_config_list_free(config_list);
285
26.7k
  git_config_list_free(dupped);
286
26.7k
  return error;
287
26.7k
}
288
289
static int config_file_snapshot(git_config_backend **out, git_config_backend *backend)
290
26.7k
{
291
26.7k
  return git_config_backend_snapshot(out, backend);
292
26.7k
}
293
294
static int config_file_set(git_config_backend *cfg, const char *name, const char *value)
295
6
{
296
6
  config_file_backend *b = GIT_CONTAINER_OF(cfg, config_file_backend, parent);
297
6
  git_config_list *config_list;
298
6
  git_config_list_entry *existing;
299
6
  char *key, *esc_value = NULL;
300
6
  int error;
301
302
6
  if ((error = git_config__normalize_name(name, &key)) < 0)
303
0
    return error;
304
305
6
  if ((error = config_file_take_list(&config_list, b)) < 0)
306
0
    return error;
307
308
  /* Check whether we'd be modifying an included or multivar key */
309
6
  if ((error = git_config_list_get_unique(&existing, config_list, key)) < 0) {
310
6
    if (error != GIT_ENOTFOUND)
311
0
      goto out;
312
6
    error = 0;
313
6
  } else if ((!existing->base.value && !value) ||
314
0
       (existing->base.value && value && !strcmp(existing->base.value, value))) {
315
    /* don't update if old and new values already match */
316
0
    error = 0;
317
0
    goto out;
318
0
  }
319
320
  /* No early returns due to sanity checks, let's write it out and refresh */
321
6
  if (value) {
322
6
    esc_value = escape_value(value);
323
6
    GIT_ERROR_CHECK_ALLOC(esc_value);
324
6
  }
325
326
6
  if ((error = config_file_write(b, name, key, NULL, esc_value)) < 0)
327
0
    goto out;
328
329
6
out:
330
6
  git_config_list_free(config_list);
331
6
  git__free(esc_value);
332
6
  git__free(key);
333
6
  return error;
334
6
}
335
336
/*
337
 * Internal function that actually gets the value in string form
338
 */
339
static int config_file_get(git_config_backend *cfg, const char *key, git_config_entry **out)
340
8
{
341
8
  config_file_backend *h = GIT_CONTAINER_OF(cfg, config_file_backend, parent);
342
8
  git_config_list *config_list = NULL;
343
8
  git_config_list_entry *entry;
344
8
  int error = 0;
345
346
8
  if (!h->parent.readonly && ((error = config_file_refresh(cfg)) < 0))
347
0
    return error;
348
349
8
  if ((error = config_file_take_list(&config_list, h)) < 0)
350
0
    return error;
351
352
8
  if ((error = (git_config_list_get(&entry, config_list, key))) < 0) {
353
8
    git_config_list_free(config_list);
354
8
    return error;
355
8
  }
356
357
0
  *out = &entry->base;
358
359
0
  return 0;
360
8
}
361
362
static int config_file_set_multivar(
363
  git_config_backend *cfg, const char *name, const char *regexp, const char *value)
364
0
{
365
0
  config_file_backend *b = GIT_CONTAINER_OF(cfg, config_file_backend, parent);
366
0
  git_regexp preg;
367
0
  int result;
368
0
  char *key;
369
370
0
  GIT_ASSERT_ARG(regexp);
371
372
0
  if ((result = git_config__normalize_name(name, &key)) < 0)
373
0
    return result;
374
375
0
  if ((result = git_regexp_compile(&preg, regexp, 0)) < 0)
376
0
    goto out;
377
378
  /* If we do have it, set call config_file_write() and reload */
379
0
  if ((result = config_file_write(b, name, key, &preg, value)) < 0)
380
0
    goto out;
381
382
0
out:
383
0
  git__free(key);
384
0
  git_regexp_dispose(&preg);
385
386
0
  return result;
387
0
}
388
389
static int config_file_delete(git_config_backend *cfg, const char *name)
390
4
{
391
4
  config_file_backend *b = GIT_CONTAINER_OF(cfg, config_file_backend, parent);
392
4
  git_config_list *config_list = NULL;
393
4
  git_config_list_entry *entry;
394
4
  char *key = NULL;
395
4
  int error;
396
397
4
  if ((error = git_config__normalize_name(name, &key)) < 0)
398
0
    goto out;
399
400
4
  if ((error = config_file_take_list(&config_list, b)) < 0)
401
0
    goto out;
402
403
  /* Check whether we'd be modifying an included or multivar key */
404
4
  if ((error = git_config_list_get_unique(&entry, config_list, key)) < 0) {
405
4
    if (error == GIT_ENOTFOUND)
406
4
      git_error_set(GIT_ERROR_CONFIG, "could not find key '%s' to delete", name);
407
4
    goto out;
408
4
  }
409
410
0
  if ((error = config_file_write(b, name, entry->base.name, NULL, NULL)) < 0)
411
0
    goto out;
412
413
4
out:
414
4
  git_config_list_free(config_list);
415
4
  git__free(key);
416
4
  return error;
417
0
}
418
419
static int config_file_delete_multivar(git_config_backend *cfg, const char *name, const char *regexp)
420
0
{
421
0
  config_file_backend *b = GIT_CONTAINER_OF(cfg, config_file_backend, parent);
422
0
  git_config_list *config_list = NULL;
423
0
  git_config_list_entry *entry = NULL;
424
0
  git_regexp preg = GIT_REGEX_INIT;
425
0
  char *key = NULL;
426
0
  int result;
427
428
0
  if ((result = git_config__normalize_name(name, &key)) < 0)
429
0
    goto out;
430
431
0
  if ((result = config_file_take_list(&config_list, b)) < 0)
432
0
    goto out;
433
434
0
  if ((result = git_config_list_get(&entry, config_list, key)) < 0) {
435
0
    if (result == GIT_ENOTFOUND)
436
0
      git_error_set(GIT_ERROR_CONFIG, "could not find key '%s' to delete", name);
437
0
    goto out;
438
0
  }
439
440
0
  if ((result = git_regexp_compile(&preg, regexp, 0)) < 0)
441
0
    goto out;
442
443
0
  if ((result = config_file_write(b, name, key, &preg, NULL)) < 0)
444
0
    goto out;
445
446
0
out:
447
0
  git_config_list_free(config_list);
448
0
  git__free(key);
449
0
  git_regexp_dispose(&preg);
450
0
  return result;
451
0
}
452
453
static int config_file_lock(git_config_backend *_cfg)
454
0
{
455
0
  config_file_backend *cfg = GIT_CONTAINER_OF(_cfg, config_file_backend, parent);
456
0
  int error;
457
458
0
  if ((error = git_filebuf_open(&cfg->locked_buf, cfg->file.path, 0, GIT_CONFIG_FILE_MODE)) < 0)
459
0
    return error;
460
461
0
  error = git_futils_readbuffer(&cfg->locked_content, cfg->file.path);
462
0
  if (error < 0 && error != GIT_ENOTFOUND) {
463
0
    git_filebuf_cleanup(&cfg->locked_buf);
464
0
    return error;
465
0
  }
466
467
0
  cfg->locked = true;
468
0
  return 0;
469
470
0
}
471
472
static int config_file_unlock(git_config_backend *_cfg, int success)
473
0
{
474
0
  config_file_backend *cfg = GIT_CONTAINER_OF(_cfg, config_file_backend, parent);
475
0
  int error = 0;
476
477
0
  if (success) {
478
0
    git_filebuf_write(&cfg->locked_buf, cfg->locked_content.ptr, cfg->locked_content.size);
479
0
    error = git_filebuf_commit(&cfg->locked_buf);
480
0
  }
481
482
0
  git_filebuf_cleanup(&cfg->locked_buf);
483
0
  git_str_dispose(&cfg->locked_content);
484
0
  cfg->locked = false;
485
486
0
  return error;
487
0
}
488
489
int git_config_backend_from_file(git_config_backend **out, const char *path)
490
8
{
491
8
  config_file_backend *backend;
492
493
8
  backend = git__calloc(1, sizeof(config_file_backend));
494
8
  GIT_ERROR_CHECK_ALLOC(backend);
495
496
8
  backend->parent.version = GIT_CONFIG_BACKEND_VERSION;
497
8
  git_mutex_init(&backend->values_mutex);
498
499
8
  backend->file.path = git__strdup(path);
500
8
  GIT_ERROR_CHECK_ALLOC(backend->file.path);
501
8
  git_array_init(backend->file.includes);
502
503
8
  backend->parent.open = config_file_open;
504
8
  backend->parent.get = config_file_get;
505
8
  backend->parent.set = config_file_set;
506
8
  backend->parent.set_multivar = config_file_set_multivar;
507
8
  backend->parent.del = config_file_delete;
508
8
  backend->parent.del_multivar = config_file_delete_multivar;
509
8
  backend->parent.iterator = config_file_iterator;
510
8
  backend->parent.snapshot = config_file_snapshot;
511
8
  backend->parent.lock = config_file_lock;
512
8
  backend->parent.unlock = config_file_unlock;
513
8
  backend->parent.free = config_file_free;
514
515
8
  *out = (git_config_backend *)backend;
516
517
8
  return 0;
518
8
}
519
520
static int included_path(git_str *out, const char *dir, const char *path)
521
0
{
522
  /* From the user's home */
523
0
  if (path[0] == '~' && path[1] == '/')
524
0
    return git_sysdir_expand_homedir_file(out, &path[1]);
525
526
0
  return git_fs_path_join_unrooted(out, path, dir, NULL);
527
0
}
528
529
/* Escape the values to write them to the file */
530
static char *escape_value(const char *ptr)
531
6
{
532
6
  git_str buf;
533
6
  size_t len;
534
6
  const char *esc;
535
536
6
  GIT_ASSERT_ARG_WITH_RETVAL(ptr, NULL);
537
538
6
  len = strlen(ptr);
539
6
  if (!len)
540
0
    return git__calloc(1, sizeof(char));
541
542
6
  if (git_str_init(&buf, len) < 0)
543
0
    return NULL;
544
545
24
  while (*ptr != '\0') {
546
18
    if ((esc = strchr(git_config_escaped, *ptr)) != NULL) {
547
0
      git_str_putc(&buf, '\\');
548
0
      git_str_putc(&buf, git_config_escapes[esc - git_config_escaped]);
549
18
    } else {
550
18
      git_str_putc(&buf, *ptr);
551
18
    }
552
18
    ptr++;
553
18
  }
554
555
6
  if (git_str_oom(&buf))
556
0
    return NULL;
557
558
6
  return git_str_detach(&buf);
559
6
}
560
561
static int parse_include(config_file_parse_data *parse_data, const char *file)
562
0
{
563
0
  config_file *include;
564
0
  git_str path = GIT_STR_INIT;
565
0
  char *dir;
566
0
  int result;
567
568
0
  if (!file)
569
0
    return 0;
570
571
0
  if ((result = git_fs_path_dirname_r(&path, parse_data->file->path)) < 0)
572
0
    return result;
573
574
0
  dir = git_str_detach(&path);
575
0
  result = included_path(&path, dir, file);
576
0
  git__free(dir);
577
578
0
  if (result < 0)
579
0
    return result;
580
581
0
  include = git_array_alloc(parse_data->file->includes);
582
0
  GIT_ERROR_CHECK_ALLOC(include);
583
0
  memset(include, 0, sizeof(*include));
584
0
  git_array_init(include->includes);
585
0
  include->path = git_str_detach(&path);
586
587
0
  result = config_file_read(parse_data->config_list, parse_data->repo, include,
588
0
          parse_data->level, parse_data->depth+1);
589
590
0
  if (result == GIT_ENOTFOUND) {
591
0
    git_error_clear();
592
0
    result = 0;
593
0
  }
594
595
0
  return result;
596
0
}
597
598
static int do_match_gitdir(
599
  int *matches,
600
  const git_repository *repo,
601
  const char *cfg_file,
602
  const char *condition,
603
  bool case_insensitive)
604
0
{
605
0
  git_str pattern = GIT_STR_INIT, gitdir = GIT_STR_INIT;
606
0
  int error;
607
608
0
  if (condition[0] == '.' && git_fs_path_is_dirsep(condition[1])) {
609
0
    git_fs_path_dirname_r(&pattern, cfg_file);
610
0
    git_str_joinpath(&pattern, pattern.ptr, condition + 2);
611
0
  } else if (condition[0] == '~' && git_fs_path_is_dirsep(condition[1]))
612
0
    git_sysdir_expand_homedir_file(&pattern, condition + 1);
613
0
  else if (!git_fs_path_is_absolute(condition))
614
0
    git_str_joinpath(&pattern, "**", condition);
615
0
  else
616
0
    git_str_sets(&pattern, condition);
617
618
0
  if (git_fs_path_is_dirsep(condition[strlen(condition) - 1]))
619
0
    git_str_puts(&pattern, "**");
620
621
0
  if (git_str_oom(&pattern)) {
622
0
    error = -1;
623
0
    goto out;
624
0
  }
625
626
0
  if ((error = git_repository__item_path(&gitdir, repo, GIT_REPOSITORY_ITEM_GITDIR)) < 0)
627
0
    goto out;
628
629
0
  if (git_fs_path_is_dirsep(gitdir.ptr[gitdir.size - 1]))
630
0
    git_str_truncate(&gitdir, gitdir.size - 1);
631
632
0
  *matches = wildmatch(pattern.ptr, gitdir.ptr,
633
0
           WM_PATHNAME | (case_insensitive ? WM_CASEFOLD : 0)) == WM_MATCH;
634
0
out:
635
0
  git_str_dispose(&pattern);
636
0
  git_str_dispose(&gitdir);
637
0
  return error;
638
0
}
639
640
static int conditional_match_gitdir(
641
  int *matches,
642
  const git_repository *repo,
643
  const char *cfg_file,
644
  const char *value)
645
0
{
646
0
  return do_match_gitdir(matches, repo, cfg_file, value, false);
647
0
}
648
649
static int conditional_match_gitdir_i(
650
  int *matches,
651
  const git_repository *repo,
652
  const char *cfg_file,
653
  const char *value)
654
0
{
655
0
  return do_match_gitdir(matches, repo, cfg_file, value, true);
656
0
}
657
658
static int conditional_match_onbranch(
659
  int *matches,
660
  const git_repository *repo,
661
  const char *cfg_file,
662
  const char *condition)
663
0
{
664
0
  git_str reference = GIT_STR_INIT, buf = GIT_STR_INIT;
665
0
  int error;
666
667
0
  GIT_UNUSED(cfg_file);
668
669
  /*
670
   * NOTE: you cannot use `git_repository_head` here. Looking up the
671
   * HEAD reference will create the ODB, which causes us to read the
672
   * repo's config for keys like core.precomposeUnicode. As we're
673
   * just parsing the config right now, though, this would result in
674
   * an endless recursion.
675
   */
676
677
0
  if ((error = git_str_joinpath(&buf, git_repository_path(repo), GIT_HEAD_FILE)) < 0 ||
678
0
      (error = git_futils_readbuffer(&reference, buf.ptr)) < 0)
679
0
    goto out;
680
0
  git_str_rtrim(&reference);
681
682
0
  if (git__strncmp(reference.ptr, GIT_SYMREF, strlen(GIT_SYMREF)))
683
0
    goto out;
684
0
  git_str_consume(&reference, reference.ptr + strlen(GIT_SYMREF));
685
686
0
  if (git__strncmp(reference.ptr, GIT_REFS_HEADS_DIR, strlen(GIT_REFS_HEADS_DIR)))
687
0
    goto out;
688
0
  git_str_consume(&reference, reference.ptr + strlen(GIT_REFS_HEADS_DIR));
689
690
  /*
691
   * If the condition ends with a '/', then we should treat it as if
692
   * it had '**' appended.
693
   */
694
0
  if ((error = git_str_sets(&buf, condition)) < 0)
695
0
    goto out;
696
0
  if (git_fs_path_is_dirsep(condition[strlen(condition) - 1]) &&
697
0
      (error = git_str_puts(&buf, "**")) < 0)
698
0
    goto out;
699
700
0
  *matches = wildmatch(buf.ptr, reference.ptr, WM_PATHNAME) == WM_MATCH;
701
0
out:
702
0
  git_str_dispose(&reference);
703
0
  git_str_dispose(&buf);
704
705
0
  return error;
706
707
0
}
708
709
static const struct {
710
  const char *prefix;
711
  int (*matches)(int *matches, const git_repository *repo, const char *cfg, const char *value);
712
} conditions[] = {
713
  { "gitdir:", conditional_match_gitdir },
714
  { "gitdir/i:", conditional_match_gitdir_i },
715
  { "onbranch:", conditional_match_onbranch }
716
};
717
718
static int parse_conditional_include(config_file_parse_data *parse_data, const char *section, const char *file)
719
0
{
720
0
  char *condition;
721
0
  size_t section_len, i;
722
0
  int error = 0, matches;
723
724
0
  if (!parse_data->repo || !file)
725
0
    return 0;
726
727
0
  section_len = strlen(section);
728
729
  /*
730
   * We checked that the string starts with `includeIf.` and ends
731
   * in `.path` to get here.  Make sure it consists of more.
732
   */
733
0
  if (section_len < CONST_STRLEN("includeIf.") + CONST_STRLEN(".path"))
734
0
    return 0;
735
736
0
  condition = git__substrdup(section + CONST_STRLEN("includeIf."),
737
0
    section_len - CONST_STRLEN("includeIf.") - CONST_STRLEN(".path"));
738
739
0
  GIT_ERROR_CHECK_ALLOC(condition);
740
741
0
  for (i = 0; i < ARRAY_SIZE(conditions); i++) {
742
0
    if (git__prefixcmp(condition, conditions[i].prefix))
743
0
      continue;
744
745
0
    if ((error = conditions[i].matches(&matches,
746
0
               parse_data->repo,
747
0
               parse_data->file->path,
748
0
               condition + strlen(conditions[i].prefix))) < 0)
749
0
      break;
750
751
0
    if (matches)
752
0
      error = parse_include(parse_data, file);
753
754
0
    break;
755
0
  }
756
757
0
  git__free(condition);
758
0
  return error;
759
0
}
760
761
static int read_on_variable(
762
  git_config_parser *reader,
763
  const char *current_section,
764
  const char *var_name,
765
  const char *var_value,
766
  const char *line,
767
  size_t line_len,
768
  void *data)
769
18
{
770
18
  config_file_parse_data *parse_data = (config_file_parse_data *)data;
771
18
  git_str buf = GIT_STR_INIT;
772
18
  git_config_list_entry *entry;
773
18
  const char *c;
774
18
  int result = 0;
775
776
18
  GIT_UNUSED(reader);
777
18
  GIT_UNUSED(line);
778
18
  GIT_UNUSED(line_len);
779
780
18
  if (current_section) {
781
    /* TODO: Once warnings lang, we should likely warn
782
     * here. Git appears to warn in most cases if it sees
783
     * un-namespaced config options.
784
     */
785
18
    git_str_puts(&buf, current_section);
786
18
    git_str_putc(&buf, '.');
787
18
  }
788
789
220
  for (c = var_name; *c; c++)
790
202
    git_str_putc(&buf, git__tolower(*c));
791
792
18
  if (git_str_oom(&buf))
793
0
    return -1;
794
795
18
  entry = git__calloc(1, sizeof(git_config_list_entry));
796
18
  GIT_ERROR_CHECK_ALLOC(entry);
797
798
18
  entry->base.name = git_str_detach(&buf);
799
18
  GIT_ERROR_CHECK_ALLOC(entry->base.name);
800
801
18
  if (var_value) {
802
18
    entry->base.value = git__strdup(var_value);
803
18
    GIT_ERROR_CHECK_ALLOC(entry->base.value);
804
18
  }
805
806
18
  entry->base.backend_type = git_config_list_add_string(parse_data->config_list, CONFIG_FILE_TYPE);
807
18
  GIT_ERROR_CHECK_ALLOC(entry->base.backend_type);
808
809
18
  entry->base.origin_path = git_config_list_add_string(parse_data->config_list, parse_data->file->path);
810
18
  GIT_ERROR_CHECK_ALLOC(entry->base.origin_path);
811
812
18
  entry->base.level = parse_data->level;
813
18
  entry->base.include_depth = parse_data->depth;
814
18
  entry->base.free = git_config_list_entry_free;
815
18
  entry->config_list = parse_data->config_list;
816
817
18
  if ((result = git_config_list_append(parse_data->config_list, entry)) < 0)
818
0
    return result;
819
820
18
  result = 0;
821
822
  /* Add or append the new config option */
823
18
  if (!git__strcmp(entry->base.name, "include.path"))
824
0
    result = parse_include(parse_data, entry->base.value);
825
18
  else if (!git__prefixcmp(entry->base.name, "includeif.") &&
826
18
           !git__suffixcmp(entry->base.name, ".path"))
827
0
    result = parse_conditional_include(parse_data, entry->base.name, entry->base.value);
828
829
18
  return result;
830
18
}
831
832
static int config_file_read_buffer(
833
  git_config_list *config_list,
834
  const git_repository *repo,
835
  config_file *file,
836
  git_config_level_t level,
837
  int depth,
838
  const char *buf,
839
  size_t buflen)
840
10
{
841
10
  config_file_parse_data parse_data;
842
10
  git_config_parser reader;
843
10
  int error;
844
845
10
  if (depth >= MAX_INCLUDE_DEPTH) {
846
0
    git_error_set(GIT_ERROR_CONFIG, "maximum config include depth reached");
847
0
    return -1;
848
0
  }
849
850
  /* Initialize the reading position */
851
10
  reader.path = file->path;
852
10
  git_parse_ctx_init(&reader.ctx, buf, buflen);
853
854
  /* If the file is empty, there's nothing for us to do */
855
10
  if (!reader.ctx.content || *reader.ctx.content == '\0') {
856
2
    error = 0;
857
2
    goto out;
858
2
  }
859
860
8
  parse_data.repo = repo;
861
8
  parse_data.file = file;
862
8
  parse_data.config_list = config_list;
863
8
  parse_data.level = level;
864
8
  parse_data.depth = depth;
865
866
8
  error = git_config_parse(&reader, NULL, read_on_variable, NULL, NULL, &parse_data);
867
868
10
out:
869
10
  return error;
870
8
}
871
872
static int config_file_read(
873
  git_config_list *config_list,
874
  const git_repository *repo,
875
  config_file *file,
876
  git_config_level_t level,
877
  int depth)
878
4
{
879
4
  git_str contents = GIT_STR_INIT;
880
4
  struct stat st;
881
4
  int error;
882
883
4
  if (p_stat(file->path, &st) < 0) {
884
0
    error = git_fs_path_set_error(errno, file->path, "stat");
885
0
    goto out;
886
0
  }
887
888
4
  if ((error = git_futils_readbuffer(&contents, file->path)) < 0)
889
0
    goto out;
890
891
4
  git_futils_filestamp_set_from_stat(&file->stamp, &st);
892
4
  if ((error = git_hash_buf(file->checksum, contents.ptr, contents.size, GIT_HASH_ALGORITHM_SHA256)) < 0)
893
0
    goto out;
894
895
4
  if ((error = config_file_read_buffer(config_list, repo, file, level, depth,
896
4
               contents.ptr, contents.size)) < 0)
897
0
    goto out;
898
899
4
out:
900
4
  git_str_dispose(&contents);
901
4
  return error;
902
4
}
903
904
static int write_section(git_str *fbuf, const char *key)
905
2
{
906
2
  int result;
907
2
  const char *dot;
908
2
  git_str buf = GIT_STR_INIT;
909
910
  /* All of this just for [section "subsection"] */
911
2
  dot = strchr(key, '.');
912
2
  git_str_putc(&buf, '[');
913
2
  if (dot == NULL) {
914
2
    git_str_puts(&buf, key);
915
2
  } else {
916
0
    char *escaped;
917
0
    git_str_put(&buf, key, dot - key);
918
0
    escaped = escape_value(dot + 1);
919
0
    GIT_ERROR_CHECK_ALLOC(escaped);
920
0
    git_str_printf(&buf, " \"%s\"", escaped);
921
0
    git__free(escaped);
922
0
  }
923
2
  git_str_puts(&buf, "]\n");
924
925
2
  if (git_str_oom(&buf))
926
0
    return -1;
927
928
2
  result = git_str_put(fbuf, git_str_cstr(&buf), buf.size);
929
2
  git_str_dispose(&buf);
930
931
2
  return result;
932
2
}
933
934
static const char *quotes_for_value(const char *value)
935
6
{
936
6
  const char *ptr;
937
938
6
  if (value[0] == ' ' || value[0] == '\0')
939
0
    return "\"";
940
941
24
  for (ptr = value; *ptr; ++ptr) {
942
18
    if (*ptr == ';' || *ptr == '#')
943
0
      return "\"";
944
18
  }
945
946
6
  if (ptr[-1] == ' ')
947
0
    return "\"";
948
949
6
  return "";
950
6
}
951
952
struct write_data {
953
  git_str *buf;
954
  git_str buffered_comment;
955
  unsigned int in_section : 1,
956
    preg_replaced : 1;
957
  const char *orig_section;
958
  const char *section;
959
  const char *orig_name;
960
  const char *name;
961
  const git_regexp *preg;
962
  const char *value;
963
};
964
965
static int write_line_to(git_str *buf, const char *line, size_t line_len)
966
10
{
967
10
  int result = git_str_put(buf, line, line_len);
968
969
10
  if (!result && line_len && line[line_len-1] != '\n')
970
0
    result = git_str_printf(buf, "\n");
971
972
10
  return result;
973
10
}
974
975
static int write_line(struct write_data *write_data, const char *line, size_t line_len)
976
10
{
977
10
  return write_line_to(write_data->buf, line, line_len);
978
10
}
979
980
static int write_value(struct write_data *write_data)
981
6
{
982
6
  const char *q;
983
6
  int result;
984
985
6
  q = quotes_for_value(write_data->value);
986
6
  result = git_str_printf(write_data->buf,
987
6
    "\t%s = %s%s%s\n", write_data->orig_name, q, write_data->value, q);
988
989
  /* If we are updating a single name/value, we're done.  Setting `value`
990
   * to `NULL` will prevent us from trying to write it again later (in
991
   * `write_on_section`) if we see the same section repeated.
992
   */
993
6
  if (!write_data->preg)
994
6
    write_data->value = NULL;
995
996
6
  return result;
997
6
}
998
999
static int write_on_section(
1000
  git_config_parser *reader,
1001
  const char *current_section,
1002
  const char *line,
1003
  size_t line_len,
1004
  void *data)
1005
4
{
1006
4
  struct write_data *write_data = (struct write_data *)data;
1007
4
  int result = 0;
1008
1009
4
  GIT_UNUSED(reader);
1010
1011
  /* If we were previously in the correct section (but aren't anymore)
1012
   * and haven't written our value (for a simple name/value set, not
1013
   * a multivar), then append it to the end of the section before writing
1014
   * the new one.
1015
   */
1016
4
  if (write_data->in_section && !write_data->preg && write_data->value)
1017
0
    result = write_value(write_data);
1018
1019
4
  write_data->in_section = strcmp(current_section, write_data->section) == 0;
1020
1021
  /*
1022
   * If there were comments just before this section, dump them as well.
1023
   */
1024
4
  if (!result) {
1025
4
    result = git_str_put(write_data->buf, write_data->buffered_comment.ptr, write_data->buffered_comment.size);
1026
4
    git_str_clear(&write_data->buffered_comment);
1027
4
  }
1028
1029
4
  if (!result)
1030
4
    result = write_line(write_data, line, line_len);
1031
1032
4
  return result;
1033
4
}
1034
1035
static int write_on_variable(
1036
  git_config_parser *reader,
1037
  const char *current_section,
1038
  const char *var_name,
1039
  const char *var_value,
1040
  const char *line,
1041
  size_t line_len,
1042
  void *data)
1043
6
{
1044
6
  struct write_data *write_data = (struct write_data *)data;
1045
6
  bool has_matched = false;
1046
6
  int error;
1047
1048
6
  GIT_UNUSED(reader);
1049
6
  GIT_UNUSED(current_section);
1050
1051
  /*
1052
   * If there were comments just before this variable, let's dump them as well.
1053
   */
1054
6
  if ((error = git_str_put(write_data->buf, write_data->buffered_comment.ptr, write_data->buffered_comment.size)) < 0)
1055
0
    return error;
1056
1057
6
  git_str_clear(&write_data->buffered_comment);
1058
1059
  /* See if we are to update this name/value pair; first examine name */
1060
6
  if (write_data->in_section &&
1061
6
    strcasecmp(write_data->name, var_name) == 0)
1062
0
    has_matched = true;
1063
1064
  /* If we have a regex to match the value, see if it matches */
1065
6
  if (has_matched && write_data->preg != NULL)
1066
0
    has_matched = (git_regexp_match(write_data->preg, var_value) == 0);
1067
1068
  /* If this isn't the name/value we're looking for, simply dump the
1069
   * existing data back out and continue on.
1070
   */
1071
6
  if (!has_matched)
1072
6
    return write_line(write_data, line, line_len);
1073
1074
0
  write_data->preg_replaced = 1;
1075
1076
  /* If value is NULL, we are deleting this value; write nothing. */
1077
0
  if (!write_data->value)
1078
0
    return 0;
1079
1080
0
  return write_value(write_data);
1081
0
}
1082
1083
static int write_on_comment(git_config_parser *reader, const char *line, size_t line_len, void *data)
1084
0
{
1085
0
  struct write_data *write_data;
1086
1087
0
  GIT_UNUSED(reader);
1088
1089
0
  write_data = (struct write_data *)data;
1090
0
  return write_line_to(&write_data->buffered_comment, line, line_len);
1091
0
}
1092
1093
static int write_on_eof(
1094
  git_config_parser *reader, const char *current_section, void *data)
1095
6
{
1096
6
  struct write_data *write_data = (struct write_data *)data;
1097
6
  int result = 0;
1098
1099
6
  GIT_UNUSED(reader);
1100
1101
  /*
1102
   * If we've buffered comments when reaching EOF, make sure to dump them.
1103
   */
1104
6
  if ((result = git_str_put(write_data->buf, write_data->buffered_comment.ptr, write_data->buffered_comment.size)) < 0)
1105
0
    return result;
1106
1107
  /* If we are at the EOF and have not written our value (again, for a
1108
   * simple name/value set, not a multivar) then we have never seen the
1109
   * section in question and should create a new section and write the
1110
   * value.
1111
   */
1112
6
  if ((!write_data->preg || !write_data->preg_replaced) && write_data->value) {
1113
    /* write the section header unless we're already in it */
1114
6
    if (!current_section || strcmp(current_section, write_data->section))
1115
2
      result = write_section(write_data->buf, write_data->orig_section);
1116
1117
6
    if (!result)
1118
6
      result = write_value(write_data);
1119
6
  }
1120
1121
6
  return result;
1122
6
}
1123
1124
/*
1125
 * This is pretty much the parsing, except we write out anything we don't have
1126
 */
1127
static int config_file_write(
1128
  config_file_backend *cfg,
1129
  const char *orig_key,
1130
  const char *key,
1131
  const git_regexp *preg,
1132
  const char *value)
1133
1134
6
{
1135
6
  char *orig_section = NULL, *section = NULL, *orig_name, *name, *ldot;
1136
6
  git_str buf = GIT_STR_INIT, contents = GIT_STR_INIT;
1137
6
  git_config_parser parser = GIT_CONFIG_PARSER_INIT;
1138
6
  git_filebuf file = GIT_FILEBUF_INIT;
1139
6
  struct write_data write_data;
1140
6
  int error;
1141
1142
6
  memset(&write_data, 0, sizeof(write_data));
1143
1144
6
  if (cfg->locked) {
1145
0
    error = git_str_puts(&contents, git_str_cstr(&cfg->locked_content) == NULL ? "" : git_str_cstr(&cfg->locked_content));
1146
6
  } else {
1147
6
    if ((error = git_filebuf_open(&file, cfg->file.path,
1148
6
        GIT_FILEBUF_HASH_SHA256,
1149
6
        GIT_CONFIG_FILE_MODE)) < 0)
1150
0
      goto done;
1151
1152
    /* We need to read in our own config file */
1153
6
    error = git_futils_readbuffer(&contents, cfg->file.path);
1154
6
  }
1155
6
  if (error < 0 && error != GIT_ENOTFOUND)
1156
0
    goto done;
1157
1158
6
  if ((git_config_parser_init(&parser, cfg->file.path, contents.ptr, contents.size)) < 0)
1159
0
    goto done;
1160
1161
6
  ldot = strrchr(key, '.');
1162
6
  name = ldot + 1;
1163
6
  section = git__strndup(key, ldot - key);
1164
6
  GIT_ERROR_CHECK_ALLOC(section);
1165
1166
6
  ldot = strrchr(orig_key, '.');
1167
6
  orig_name = ldot + 1;
1168
6
  orig_section = git__strndup(orig_key, ldot - orig_key);
1169
6
  GIT_ERROR_CHECK_ALLOC(orig_section);
1170
1171
6
  write_data.buf = &buf;
1172
6
  write_data.orig_section = orig_section;
1173
6
  write_data.section = section;
1174
6
  write_data.orig_name = orig_name;
1175
6
  write_data.name = name;
1176
6
  write_data.preg = preg;
1177
6
  write_data.value = value;
1178
1179
6
  if ((error = git_config_parse(&parser, write_on_section, write_on_variable,
1180
6
              write_on_comment, write_on_eof, &write_data)) < 0)
1181
0
    goto done;
1182
1183
6
  if (cfg->locked) {
1184
0
    size_t len = buf.asize;
1185
    /* Update our copy with the modified contents */
1186
0
    git_str_dispose(&cfg->locked_content);
1187
0
    git_str_attach(&cfg->locked_content, git_str_detach(&buf), len);
1188
6
  } else {
1189
6
    git_filebuf_write(&file, git_str_cstr(&buf), git_str_len(&buf));
1190
1191
6
    if ((error = git_filebuf_commit(&file)) < 0)
1192
0
      goto done;
1193
1194
6
    if ((error = config_file_refresh_from_buffer(&cfg->parent, buf.ptr, buf.size)) < 0)
1195
0
      goto done;
1196
6
  }
1197
1198
6
done:
1199
6
  git__free(section);
1200
6
  git__free(orig_section);
1201
6
  git_str_dispose(&write_data.buffered_comment);
1202
6
  git_str_dispose(&buf);
1203
6
  git_str_dispose(&contents);
1204
6
  git_filebuf_cleanup(&file);
1205
6
  git_config_parser_dispose(&parser);
1206
1207
6
  return error;
1208
6
}