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