diff --git a/src/config_updater.c b/src/config_updater.c index 127b8543..0bd13e3e 100644 --- a/src/config_updater.c +++ b/src/config_updater.c @@ -73,9 +73,10 @@ static int config_field_line(char *line, int *section, const struct config_field return -1; } -/* Собрать последние текстовые значения и конец последней секции каждого вида. */ +/* Собрать последние значения и точку вставки до завершающих пустых строк каждой секции. */ static void config_scan(const char *text, size_t length, struct config_field fields[CFG_FIELD_COUNT], size_t ends[CFG_SECTION_COUNT]) { int section = CFG_OTHER_SECTION; + size_t content_end = 0; for (int i = 0; i < CFG_SECTION_COUNT; i++) ends[i] = SIZE_MAX; for (size_t pos = 0; pos < length;) { const char *nl = memchr(text + pos, '\n', length - pos); @@ -84,7 +85,8 @@ static void config_scan(const char *text, size_t length, struct config_field fie memcpy(line, text + pos, end - pos); line[end - pos] = '\0'; char *start = line; while (isspace((unsigned char)*start)) start++; - if (*start == '[') ends[section] = pos; + int nonempty = *start != '\0'; + if (*start == '[') ends[section] = content_end; int index = config_field_line(line, §ion, fields, value); if (index >= 0) { strcpy(fields[index].value, value); fields[index].present = 1; @@ -100,9 +102,10 @@ static void config_scan(const char *text, size_t length, struct config_field fie strcasecmp(value, "false") && strcasecmp(value, "true")) fields[index].invalid = 1; } else if (config_ini_hex(value, NULL, SC_PRIVKEY_SIZE)) fields[index].invalid = 1; } + if (nonempty) content_end = end; pos = end; } - ends[section] = length; + ends[section] = content_end; } /* Вставить отсутствующие поля в нужную секцию, при необходимости создать заголовок. */ @@ -117,6 +120,7 @@ static int config_insert_missing(struct strbuf *output, const struct config_fiel started = 1; } if (strbuf_addf(output, "%s=%s%s", fields[i].key, fields[i].value, eol) < 0) return -1; + DEBUG_DEBUG(DEBUG_CATEGORY_CONFIG, "inserted [%s] %s before section separator", config_section_names[section], fields[i].key); } return 0; } diff --git a/src/config_updater_doc.md b/src/config_updater_doc.md index 0ac6c370..771e2ab1 100644 --- a/src/config_updater_doc.md +++ b/src/config_updater_doc.md @@ -36,7 +36,9 @@ HEX проверяется посимвольно до декодировани Корректная итоговая идентичность/REALITY не меняется и не генерируется заново только из-за раннего дубля. Файл без таких ошибок остаётся побайтно неизменным. Если поле требует исправления, обновляются все его вхождения в соответствующих секциях. Недостающие поля добавляются -в конец последней подходящей секции; отсутствующая секция создаётся в конце файла. +в конец последней подходящей секции, перед её завершающими пустыми строками. +Разделитель перед следующей секцией остаётся после добавленных полей; пробелы и CRLF в нём сохраняются. +Отсутствующая секция создаётся в конце файла. Посторонние строки сохраняются побайтно. В изменяемых строках сохраняются пробелы, комментарии и окончания строк вокруг заменяемого значения. Добавляемые строки используют diff --git a/tests/test_config_updater.c b/tests/test_config_updater.c index 9117a54c..d27a4b27 100644 --- a/tests/test_config_updater.c +++ b/tests/test_config_updater.c @@ -367,6 +367,45 @@ static void test_allocation_failures(void) { DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "PASS allocation failures: every allocation, original bytes and resource balance"); } +/* Недостающие поля остаются в блоке; исходные пустые строки продолжают отделять секции. */ +static void test_insertion_before_separator(void) { + const struct { const char *input, *anchor, *first, *last, *suffix; } cases[] = { + {"[chatserver]\nstorage_autoload=1\n\n[gui]\nutun_enabled=no\n", + "storage_autoload=1", "add_key=", "add_key=", "\n[gui]\nutun_enabled=no\n"}, + {"[chatserver]\nstorage_autoload=1\n\n\n", + "storage_autoload=1", "add_key=", "add_key=", "\n\n"}, + {"[chatserver]\n\n[gui]\nutun_enabled=no\n", + "[chatserver]", "add_key=", "add_key=", "\n[gui]\nutun_enabled=no\n"}, + {"[chatserver]\nstorage_autoload=0\n\n[chatserver]\nstorage_autoload=1\n\n[gui]\nutun_enabled=no\n", + "storage_autoload=1", "add_key=", "add_key=", "\n[gui]\nutun_enabled=no\n"}, + {"[chatserver]\nstorage_autoload=1\n \t\n\n[gui]\nutun_enabled=no\n", + "storage_autoload=1", "add_key=", "add_key=", " \t\n\n[gui]\nutun_enabled=no\n"}, + {"[global]\nmy_private_key=%s\n\n[gui]\nutun_enabled=no\n", + "my_private_key=", "my_public_key=", "my_node_id=", "\n[gui]\nutun_enabled=no\n"}, + {"[reality]\nenabled=1\n\n[gui]\nutun_enabled=no\n", + "enabled=1", "private_key=", "short_ids=", "\n[gui]\nutun_enabled=no\n"} + }; + for (int crlf = 0; crlf < 2; crlf++) for (size_t i = 0; i < sizeof(cases) / sizeof(*cases); i++) { + char plain[2048], input[4096], suffix[4096], after[8192], unchanged[8192]; + snprintf(plain, sizeof(plain), cases[i].input, private_hex); + size_t pos = 0; + for (const char* p = plain; *p; p++) { if (crlf && *p == '\n') input[pos++] = '\r'; input[pos++] = *p; } + input[pos] = 0; pos = 0; + for (const char* p = cases[i].suffix; *p; p++) { if (crlf && *p == '\n') suffix[pos++] = '\r'; suffix[pos++] = *p; } + suffix[pos] = 0; + const char* eol = crlf ? "\r\n" : "\n"; + write_text(input); assert(!config_ensure_keys_and_node_id(path)); read_text(after, sizeof(after)); + const char* anchor = strstr(after, cases[i].anchor); assert(anchor); + const char* first = strstr(anchor, cases[i].first); + assert(first && first == strstr(anchor, eol) + strlen(eol)); + const char* last = strstr(first, cases[i].last); assert(last); + const char* end = strstr(last, eol); assert(end); + assert(!strncmp(end + strlen(eol), suffix, strlen(suffix))); + assert(!config_ensure_keys_and_node_id(path)); read_text(unchanged, sizeof(unchanged)); assert(!strcmp(after, unchanged)); + } + DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "PASS field insertion: 14 LF/CRLF separator, EOF, empty/repeated section and stable-repeat cases"); +} + static void test_server_key(void) { char before[8192], after[8192]; uint8_t key[32]; write_text("[chatserver]\n# server credential\n"); @@ -401,6 +440,7 @@ int main(void) { test_reality_and_validation(); test_failures(); test_allocation_failures(); + test_insertion_before_separator(); test_server_key(); assert(!unlink(path) && !rmdir(directory)); DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "PASS config updater regression suite");