1 /*
2 * SPDX-FileCopyrightText: 2015-2021 Espressif Systems (Shanghai) CO LTD
3 *
4 * SPDX-License-Identifier: Apache-2.0
5 */
6
7 #define LOG_TAG "bt_osi_config"
8 #include "esp_system.h"
9 #include "nvs_flash.h"
10 #include "nvs.h"
11
12 #include <ctype.h>
13 #include <errno.h>
14 #include <stdio.h>
15 #include <stdlib.h>
16 #include <string.h>
17
18 #include "bt_common.h"
19 #include "osi/allocator.h"
20 #include "osi/config.h"
21 #include "osi/list.h"
22
23 #define CONFIG_FILE_MAX_SIZE (1536)//1.5k
24 #define CONFIG_FILE_DEFAULE_LENGTH (2048)
25 #define CONFIG_KEY "bt_cfg_key"
26 typedef struct {
27 char *key;
28 char *value;
29 } entry_t;
30
31 typedef struct {
32 char *name;
33 list_t *entries;
34 } section_t;
35
36 struct config_t {
37 list_t *sections;
38 };
39
40 // Empty definition; this type is aliased to list_node_t.
41 struct config_section_iter_t {};
42
43 static void config_parse(nvs_handle_t fp, config_t *config);
44
45 static section_t *section_new(const char *name);
46 static void section_free(void *ptr);
47 static section_t *section_find(const config_t *config, const char *section);
48
49 static entry_t *entry_new(const char *key, const char *value);
50 static void entry_free(void *ptr);
51 static entry_t *entry_find(const config_t *config, const char *section, const char *key);
52
config_new_empty(void)53 config_t *config_new_empty(void)
54 {
55 config_t *config = osi_calloc(sizeof(config_t));
56 if (!config) {
57 OSI_TRACE_ERROR("%s unable to allocate memory for config_t.\n", __func__);
58 goto error;
59 }
60
61 config->sections = list_new(section_free);
62 if (!config->sections) {
63 OSI_TRACE_ERROR("%s unable to allocate list for sections.\n", __func__);
64 goto error;
65 }
66
67 return config;
68
69 error:;
70 config_free(config);
71 return NULL;
72 }
73
config_new(const char * filename)74 config_t *config_new(const char *filename)
75 {
76 assert(filename != NULL);
77
78 config_t *config = config_new_empty();
79 if (!config) {
80 return NULL;
81 }
82
83 esp_err_t err;
84 nvs_handle_t fp;
85 err = nvs_open(filename, NVS_READWRITE, &fp);
86 if (err != ESP_OK) {
87 if (err == ESP_ERR_NVS_NOT_INITIALIZED) {
88 OSI_TRACE_ERROR("%s: NVS not initialized. "
89 "Call nvs_flash_init before initializing bluetooth.", __func__);
90 } else {
91 OSI_TRACE_ERROR("%s unable to open NVS namespace '%s'\n", __func__, filename);
92 }
93 config_free(config);
94 return NULL;
95 }
96
97 config_parse(fp, config);
98 nvs_close(fp);
99 return config;
100 }
101
config_free(config_t * config)102 void config_free(config_t *config)
103 {
104 if (!config) {
105 return;
106 }
107
108 list_free(config->sections);
109 osi_free(config);
110 }
111
config_has_section(const config_t * config,const char * section)112 bool config_has_section(const config_t *config, const char *section)
113 {
114 assert(config != NULL);
115 assert(section != NULL);
116
117 return (section_find(config, section) != NULL);
118 }
119
config_has_key(const config_t * config,const char * section,const char * key)120 bool config_has_key(const config_t *config, const char *section, const char *key)
121 {
122 assert(config != NULL);
123 assert(section != NULL);
124 assert(key != NULL);
125
126 return (entry_find(config, section, key) != NULL);
127 }
128
config_has_key_in_section(config_t * config,const char * key,char * key_value)129 bool config_has_key_in_section(config_t *config, const char *key, char *key_value)
130 {
131 OSI_TRACE_DEBUG("key = %s, value = %s", key, key_value);
132 for (const list_node_t *node = list_begin(config->sections); node != list_end(config->sections); node = list_next(node)) {
133 const section_t *section = (const section_t *)list_node(node);
134
135 for (const list_node_t *node = list_begin(section->entries); node != list_end(section->entries); node = list_next(node)) {
136 entry_t *entry = list_node(node);
137 OSI_TRACE_DEBUG("entry->key = %s, entry->value = %s", entry->key, entry->value);
138 if (!strcmp(entry->key, key) && !strcmp(entry->value, key_value)) {
139 OSI_TRACE_DEBUG("%s, the irk aready in the flash.", __func__);
140 return true;
141 }
142 }
143 }
144
145 return false;
146 }
147
config_get_int(const config_t * config,const char * section,const char * key,int def_value)148 int config_get_int(const config_t *config, const char *section, const char *key, int def_value)
149 {
150 assert(config != NULL);
151 assert(section != NULL);
152 assert(key != NULL);
153
154 entry_t *entry = entry_find(config, section, key);
155 if (!entry) {
156 return def_value;
157 }
158
159 char *endptr;
160 int ret = strtol(entry->value, &endptr, 0);
161 return (*endptr == '\0') ? ret : def_value;
162 }
163
config_get_bool(const config_t * config,const char * section,const char * key,bool def_value)164 bool config_get_bool(const config_t *config, const char *section, const char *key, bool def_value)
165 {
166 assert(config != NULL);
167 assert(section != NULL);
168 assert(key != NULL);
169
170 entry_t *entry = entry_find(config, section, key);
171 if (!entry) {
172 return def_value;
173 }
174
175 if (!strcmp(entry->value, "true")) {
176 return true;
177 }
178 if (!strcmp(entry->value, "false")) {
179 return false;
180 }
181
182 return def_value;
183 }
184
config_get_string(const config_t * config,const char * section,const char * key,const char * def_value)185 const char *config_get_string(const config_t *config, const char *section, const char *key, const char *def_value)
186 {
187 assert(config != NULL);
188 assert(section != NULL);
189 assert(key != NULL);
190
191 entry_t *entry = entry_find(config, section, key);
192 if (!entry) {
193 return def_value;
194 }
195
196 return entry->value;
197 }
198
config_set_int(config_t * config,const char * section,const char * key,int value)199 void config_set_int(config_t *config, const char *section, const char *key, int value)
200 {
201 assert(config != NULL);
202 assert(section != NULL);
203 assert(key != NULL);
204
205 char value_str[32] = { 0 };
206 sprintf(value_str, "%d", value);
207 config_set_string(config, section, key, value_str, false);
208 }
209
config_set_bool(config_t * config,const char * section,const char * key,bool value)210 void config_set_bool(config_t *config, const char *section, const char *key, bool value)
211 {
212 assert(config != NULL);
213 assert(section != NULL);
214 assert(key != NULL);
215
216 config_set_string(config, section, key, value ? "true" : "false", false);
217 }
218
config_set_string(config_t * config,const char * section,const char * key,const char * value,bool insert_back)219 void config_set_string(config_t *config, const char *section, const char *key, const char *value, bool insert_back)
220 {
221 section_t *sec = section_find(config, section);
222 if (!sec) {
223 sec = section_new(section);
224 if (insert_back) {
225 list_append(config->sections, sec);
226 } else {
227 list_prepend(config->sections, sec);
228 }
229 }
230
231 for (const list_node_t *node = list_begin(sec->entries); node != list_end(sec->entries); node = list_next(node)) {
232 entry_t *entry = list_node(node);
233 if (!strcmp(entry->key, key)) {
234 osi_free(entry->value);
235 entry->value = osi_strdup(value);
236 return;
237 }
238 }
239
240 entry_t *entry = entry_new(key, value);
241 list_append(sec->entries, entry);
242 }
243
config_remove_section(config_t * config,const char * section)244 bool config_remove_section(config_t *config, const char *section)
245 {
246 assert(config != NULL);
247 assert(section != NULL);
248
249 section_t *sec = section_find(config, section);
250 if (!sec) {
251 return false;
252 }
253
254 return list_remove(config->sections, sec);
255 }
256
config_update_newest_section(config_t * config,const char * section)257 bool config_update_newest_section(config_t *config, const char *section)
258 {
259 assert(config != NULL);
260 assert(section != NULL);
261
262 list_node_t *first_node = list_begin(config->sections);
263 if (first_node == NULL) {
264 return false;
265 }
266 section_t *first_sec = list_node(first_node);
267 if (strcmp(first_sec->name, section) == 0) {
268 return true;
269 }
270
271 for (const list_node_t *node = list_begin(config->sections); node != list_end(config->sections); node = list_next(node)) {
272 section_t *sec = list_node(node);
273 if (strcmp(sec->name, section) == 0) {
274 list_delete(config->sections, sec);
275 list_prepend(config->sections, sec);
276 return true;
277 }
278 }
279
280 return false;
281 }
282
config_remove_key(config_t * config,const char * section,const char * key)283 bool config_remove_key(config_t *config, const char *section, const char *key)
284 {
285 assert(config != NULL);
286 assert(section != NULL);
287 assert(key != NULL);
288 bool ret;
289
290 section_t *sec = section_find(config, section);
291 entry_t *entry = entry_find(config, section, key);
292 if (!sec || !entry) {
293 return false;
294 }
295
296 ret = list_remove(sec->entries, entry);
297 if (list_length(sec->entries) == 0) {
298 OSI_TRACE_DEBUG("%s remove section name:%s",__func__, section);
299 ret &= config_remove_section(config, section);
300 }
301 return ret;
302 }
303
config_section_begin(const config_t * config)304 const config_section_node_t *config_section_begin(const config_t *config)
305 {
306 assert(config != NULL);
307 return (const config_section_node_t *)list_begin(config->sections);
308 }
309
config_section_end(const config_t * config)310 const config_section_node_t *config_section_end(const config_t *config)
311 {
312 assert(config != NULL);
313 return (const config_section_node_t *)list_end(config->sections);
314 }
315
config_section_next(const config_section_node_t * node)316 const config_section_node_t *config_section_next(const config_section_node_t *node)
317 {
318 assert(node != NULL);
319 return (const config_section_node_t *)list_next((const list_node_t *)node);
320 }
321
config_section_name(const config_section_node_t * node)322 const char *config_section_name(const config_section_node_t *node)
323 {
324 assert(node != NULL);
325 const list_node_t *lnode = (const list_node_t *)node;
326 const section_t *section = (const section_t *)list_node(lnode);
327 return section->name;
328 }
329
get_config_size(const config_t * config)330 static int get_config_size(const config_t *config)
331 {
332 assert(config != NULL);
333
334 int w_len = 0, total_size = 0;
335
336 for (const list_node_t *node = list_begin(config->sections); node != list_end(config->sections); node = list_next(node)) {
337 const section_t *section = (const section_t *)list_node(node);
338 w_len = strlen(section->name) + strlen("[]\n");// format "[section->name]\n"
339 total_size += w_len;
340
341 for (const list_node_t *enode = list_begin(section->entries); enode != list_end(section->entries); enode = list_next(enode)) {
342 const entry_t *entry = (const entry_t *)list_node(enode);
343 w_len = strlen(entry->key) + strlen(entry->value) + strlen(" = \n");// format "entry->key = entry->value\n"
344 total_size += w_len;
345 }
346
347 // Only add a separating newline if there are more sections.
348 if (list_next(node) != list_end(config->sections)) {
349 total_size ++; //'\n'
350 } else {
351 break;
352 }
353 }
354 total_size ++; //'\0'
355 return total_size;
356 }
357
get_config_size_from_flash(nvs_handle_t fp)358 static int get_config_size_from_flash(nvs_handle_t fp)
359 {
360 assert(fp != 0);
361
362 esp_err_t err;
363 const size_t keyname_bufsz = sizeof(CONFIG_KEY) + 5 + 1; // including log10(sizeof(i))
364 char *keyname = osi_calloc(keyname_bufsz);
365 if (!keyname){
366 OSI_TRACE_ERROR("%s, malloc error\n", __func__);
367 return 0;
368 }
369 size_t length = CONFIG_FILE_DEFAULE_LENGTH;
370 size_t total_length = 0;
371 uint16_t i = 0;
372 snprintf(keyname, keyname_bufsz, "%s%d", CONFIG_KEY, 0);
373 err = nvs_get_blob(fp, keyname, NULL, &length);
374 if (err == ESP_ERR_NVS_NOT_FOUND) {
375 osi_free(keyname);
376 return 0;
377 }
378 if (err != ESP_OK) {
379 OSI_TRACE_ERROR("%s, error %d\n", __func__, err);
380 osi_free(keyname);
381 return 0;
382 }
383 total_length += length;
384 while (length == CONFIG_FILE_MAX_SIZE) {
385 length = CONFIG_FILE_DEFAULE_LENGTH;
386 snprintf(keyname, keyname_bufsz, "%s%d", CONFIG_KEY, ++i);
387 err = nvs_get_blob(fp, keyname, NULL, &length);
388
389 if (err == ESP_ERR_NVS_NOT_FOUND) {
390 break;
391 }
392 if (err != ESP_OK) {
393 OSI_TRACE_ERROR("%s, error %d\n", __func__, err);
394 osi_free(keyname);
395 return 0;
396 }
397 total_length += length;
398 }
399 osi_free(keyname);
400 return total_length;
401 }
402
config_save(const config_t * config,const char * filename)403 bool config_save(const config_t *config, const char *filename)
404 {
405 assert(config != NULL);
406 assert(filename != NULL);
407 assert(*filename != '\0');
408
409 esp_err_t err;
410 int err_code = 0;
411 nvs_handle_t fp;
412 char *line = osi_calloc(1024);
413 const size_t keyname_bufsz = sizeof(CONFIG_KEY) + 5 + 1; // including log10(sizeof(i))
414 char *keyname = osi_calloc(keyname_bufsz);
415 int config_size = get_config_size(config);
416 char *buf = osi_calloc(config_size);
417 if (!line || !buf || !keyname) {
418 err_code |= 0x01;
419 goto error;
420 }
421
422 err = nvs_open(filename, NVS_READWRITE, &fp);
423 if (err != ESP_OK) {
424 if (err == ESP_ERR_NVS_NOT_INITIALIZED) {
425 OSI_TRACE_ERROR("%s: NVS not initialized. "
426 "Call nvs_flash_init before initializing bluetooth.", __func__);
427 }
428 err_code |= 0x02;
429 goto error;
430 }
431
432 int w_cnt, w_cnt_total = 0;
433 for (const list_node_t *node = list_begin(config->sections); node != list_end(config->sections); node = list_next(node)) {
434 const section_t *section = (const section_t *)list_node(node);
435 w_cnt = snprintf(line, 1024, "[%s]\n", section->name);
436 if(w_cnt < 0) {
437 OSI_TRACE_ERROR("snprintf error w_cnt %d.",w_cnt);
438 err_code |= 0x10;
439 goto error;
440 }
441 if(w_cnt_total + w_cnt > config_size) {
442 OSI_TRACE_ERROR("%s, memcpy size (w_cnt + w_cnt_total = %d) is larger than buffer size (config_size = %d).", __func__, (w_cnt + w_cnt_total), config_size);
443 err_code |= 0x20;
444 goto error;
445 }
446 OSI_TRACE_DEBUG("section name: %s, w_cnt + w_cnt_total = %d\n", section->name, w_cnt + w_cnt_total);
447 memcpy(buf + w_cnt_total, line, w_cnt);
448 w_cnt_total += w_cnt;
449
450 for (const list_node_t *enode = list_begin(section->entries); enode != list_end(section->entries); enode = list_next(enode)) {
451 const entry_t *entry = (const entry_t *)list_node(enode);
452 OSI_TRACE_DEBUG("(key, val): (%s, %s)\n", entry->key, entry->value);
453 w_cnt = snprintf(line, 1024, "%s = %s\n", entry->key, entry->value);
454 if(w_cnt < 0) {
455 OSI_TRACE_ERROR("snprintf error w_cnt %d.",w_cnt);
456 err_code |= 0x10;
457 goto error;
458 }
459 if(w_cnt_total + w_cnt > config_size) {
460 OSI_TRACE_ERROR("%s, memcpy size (w_cnt + w_cnt_total = %d) is larger than buffer size.(config_size = %d)", __func__, (w_cnt + w_cnt_total), config_size);
461 err_code |= 0x20;
462 goto error;
463 }
464 OSI_TRACE_DEBUG("%s, w_cnt + w_cnt_total = %d", __func__, w_cnt + w_cnt_total);
465 memcpy(buf + w_cnt_total, line, w_cnt);
466 w_cnt_total += w_cnt;
467 }
468
469 // Only add a separating newline if there are more sections.
470 if (list_next(node) != list_end(config->sections)) {
471 buf[w_cnt_total] = '\n';
472 w_cnt_total += 1;
473 } else {
474 break;
475 }
476 }
477 buf[w_cnt_total] = '\0';
478 if (w_cnt_total < CONFIG_FILE_MAX_SIZE) {
479 snprintf(keyname, keyname_bufsz, "%s%d", CONFIG_KEY, 0);
480 err = nvs_set_blob(fp, keyname, buf, w_cnt_total);
481 if (err != ESP_OK) {
482 nvs_close(fp);
483 err_code |= 0x04;
484 goto error;
485 }
486 }else {
487 int count = (w_cnt_total / CONFIG_FILE_MAX_SIZE);
488 assert(count <= 0xFF);
489 for (uint8_t i = 0; i <= count; i++)
490 {
491 snprintf(keyname, keyname_bufsz, "%s%d", CONFIG_KEY, i);
492 if (i == count) {
493 err = nvs_set_blob(fp, keyname, buf + i*CONFIG_FILE_MAX_SIZE, w_cnt_total - i*CONFIG_FILE_MAX_SIZE);
494 OSI_TRACE_DEBUG("save keyname = %s, i = %d, %d\n", keyname, i, w_cnt_total - i*CONFIG_FILE_MAX_SIZE);
495 }else {
496 err = nvs_set_blob(fp, keyname, buf + i*CONFIG_FILE_MAX_SIZE, CONFIG_FILE_MAX_SIZE);
497 OSI_TRACE_DEBUG("save keyname = %s, i = %d, %d\n", keyname, i, CONFIG_FILE_MAX_SIZE);
498 }
499 if (err != ESP_OK) {
500 nvs_close(fp);
501 err_code |= 0x04;
502 goto error;
503 }
504 }
505 }
506
507 err = nvs_commit(fp);
508 if (err != ESP_OK) {
509 nvs_close(fp);
510 err_code |= 0x08;
511 goto error;
512 }
513
514 nvs_close(fp);
515 osi_free(line);
516 osi_free(buf);
517 osi_free(keyname);
518 return true;
519
520 error:
521 if (buf) {
522 osi_free(buf);
523 }
524 if (line) {
525 osi_free(line);
526 }
527 if (keyname) {
528 osi_free(keyname);
529 }
530 if (err_code) {
531 OSI_TRACE_ERROR("%s, err_code: 0x%x\n", __func__, err_code);
532 }
533 return false;
534 }
535
trim(char * str)536 static char *trim(char *str)
537 {
538 while (isspace((unsigned char)(*str))) {
539 ++str;
540 }
541
542 if (!*str) {
543 return str;
544 }
545
546 char *end_str = str + strlen(str) - 1;
547 while (end_str > str && isspace((unsigned char)(*end_str))) {
548 --end_str;
549 }
550
551 end_str[1] = '\0';
552 return str;
553 }
554
config_parse(nvs_handle_t fp,config_t * config)555 static void config_parse(nvs_handle_t fp, config_t *config)
556 {
557 assert(fp != 0);
558 assert(config != NULL);
559
560 esp_err_t err;
561 int line_num = 0;
562 int err_code = 0;
563 uint16_t i = 0;
564 size_t length = CONFIG_FILE_DEFAULE_LENGTH;
565 size_t total_length = 0;
566 char *line = osi_calloc(1024);
567 char *section = osi_calloc(1024);
568 const size_t keyname_bufsz = sizeof(CONFIG_KEY) + 5 + 1; // including log10(sizeof(i))
569 char *keyname = osi_calloc(keyname_bufsz);
570 int buf_size = get_config_size_from_flash(fp);
571 char *buf = NULL;
572
573 if(buf_size == 0) { //First use nvs
574 goto error;
575 }
576 buf = osi_calloc(buf_size);
577 if (!line || !section || !buf || !keyname) {
578 err_code |= 0x01;
579 goto error;
580 }
581 snprintf(keyname, keyname_bufsz, "%s%d", CONFIG_KEY, 0);
582 err = nvs_get_blob(fp, keyname, buf, &length);
583 if (err == ESP_ERR_NVS_NOT_FOUND) {
584 goto error;
585 }
586 if (err != ESP_OK) {
587 err_code |= 0x02;
588 goto error;
589 }
590 total_length += length;
591 while (length == CONFIG_FILE_MAX_SIZE) {
592 length = CONFIG_FILE_DEFAULE_LENGTH;
593 snprintf(keyname, keyname_bufsz, "%s%d", CONFIG_KEY, ++i);
594 err = nvs_get_blob(fp, keyname, buf + CONFIG_FILE_MAX_SIZE * i, &length);
595
596 if (err == ESP_ERR_NVS_NOT_FOUND) {
597 break;
598 }
599 if (err != ESP_OK) {
600 err_code |= 0x02;
601 goto error;
602 }
603 total_length += length;
604 }
605 char *p_line_end;
606 char *p_line_bgn = buf;
607 strcpy(section, CONFIG_DEFAULT_SECTION);
608
609 while ( (p_line_bgn < buf + total_length - 1) && (p_line_end = strchr(p_line_bgn, '\n'))) {
610
611 // get one line
612 int line_len = p_line_end - p_line_bgn;
613 if (line_len > 1023) {
614 OSI_TRACE_WARNING("%s exceed max line length on line %d.\n", __func__, line_num);
615 break;
616 }
617 memcpy(line, p_line_bgn, line_len);
618 line[line_len] = '\0';
619 p_line_bgn = p_line_end + 1;
620 char *line_ptr = trim(line);
621 ++line_num;
622
623 // Skip blank and comment lines.
624 if (*line_ptr == '\0' || *line_ptr == '#') {
625 continue;
626 }
627
628 if (*line_ptr == '[') {
629 size_t len = strlen(line_ptr);
630 if (line_ptr[len - 1] != ']') {
631 OSI_TRACE_WARNING("%s unterminated section name on line %d.\n", __func__, line_num);
632 continue;
633 }
634 strncpy(section, line_ptr + 1, len - 2);
635 section[len - 2] = '\0';
636 } else {
637 char *split = strchr(line_ptr, '=');
638 if (!split) {
639 OSI_TRACE_DEBUG("%s no key/value separator found on line %d.\n", __func__, line_num);
640 continue;
641 }
642 *split = '\0';
643 config_set_string(config, section, trim(line_ptr), trim(split + 1), true);
644 }
645 }
646
647 error:
648 if (buf) {
649 osi_free(buf);
650 }
651 if (line) {
652 osi_free(line);
653 }
654 if (section) {
655 osi_free(section);
656 }
657 if (keyname) {
658 osi_free(keyname);
659 }
660 if (err_code) {
661 OSI_TRACE_ERROR("%s returned with err code: %d\n", __func__, err_code);
662 }
663 }
664
section_new(const char * name)665 static section_t *section_new(const char *name)
666 {
667 section_t *section = osi_calloc(sizeof(section_t));
668 if (!section) {
669 return NULL;
670 }
671
672 section->name = osi_strdup(name);
673 section->entries = list_new(entry_free);
674 return section;
675 }
676
section_free(void * ptr)677 static void section_free(void *ptr)
678 {
679 if (!ptr) {
680 return;
681 }
682
683 section_t *section = ptr;
684 osi_free(section->name);
685 list_free(section->entries);
686 osi_free(section);
687 }
688
section_find(const config_t * config,const char * section)689 static section_t *section_find(const config_t *config, const char *section)
690 {
691 for (const list_node_t *node = list_begin(config->sections); node != list_end(config->sections); node = list_next(node)) {
692 section_t *sec = list_node(node);
693 if (!strcmp(sec->name, section)) {
694 return sec;
695 }
696 }
697
698 return NULL;
699 }
700
entry_new(const char * key,const char * value)701 static entry_t *entry_new(const char *key, const char *value)
702 {
703 entry_t *entry = osi_calloc(sizeof(entry_t));
704 if (!entry) {
705 return NULL;
706 }
707
708 entry->key = osi_strdup(key);
709 entry->value = osi_strdup(value);
710 return entry;
711 }
712
entry_free(void * ptr)713 static void entry_free(void *ptr)
714 {
715 if (!ptr) {
716 return;
717 }
718
719 entry_t *entry = ptr;
720 osi_free(entry->key);
721 osi_free(entry->value);
722 osi_free(entry);
723 }
724
entry_find(const config_t * config,const char * section,const char * key)725 static entry_t *entry_find(const config_t *config, const char *section, const char *key)
726 {
727 section_t *sec = section_find(config, section);
728 if (!sec) {
729 return NULL;
730 }
731
732 for (const list_node_t *node = list_begin(sec->entries); node != list_end(sec->entries); node = list_next(node)) {
733 entry_t *entry = list_node(node);
734 if (!strcmp(entry->key, key)) {
735 return entry;
736 }
737 }
738
739 return NULL;
740 }
741