CDC Badge OS
Firmware for the CDC Badge v1.0 hardware security key
Loading...
Searching...
No Matches
vcard_store.cpp
Go to the documentation of this file.
2
3#include "cdc_core/Hash.h"
4#include "cdc_log.h"
5#include "nvs.h"
6#include "nvs_flash.h"
7#include "esp_attr.h"
8
9#include <cctype>
10#include <cstdio>
11#include <cstring>
12
13static const char* TAG = "VCARD";
14
15static constexpr const char* VCARD_NAMESPACE = "mod_vcard";
16static constexpr const char* VCARD_KEY_OWN = "own";
17
18EXT_RAM_BSS_ATTR static char g_own_vcard[VCARD_MAX_LEN + 1];
19static bool g_own_loaded = false;
20static bool g_own_present = false;
21
22#ifdef __DOXYGEN__
23namespace cdc::mod_vcard {
24#endif
25
26typedef struct {
27 bool used;
28 uint32_t hash;
29 char last_name[32];
30 char display[64];
32
33#ifdef __DOXYGEN__
34} // namespace cdc::mod_vcard
35#endif
36
37EXT_RAM_BSS_ATTR static vcard_meta_t g_cards[VCARD_MAX_CARDS];
38static bool g_cards_loaded = false;
39static uint16_t g_card_count = 0;
40
47static uint32_t fnv1a_hash(const char* data, size_t len) {
48 return cdc::core::hash::fnv1a_32(reinterpret_cast<const uint8_t*>(data), len);
49}
50
57static void vcard_key_for_slot(char* out, size_t out_len, uint16_t slot) {
58 snprintf(out, out_len, "c%02u", static_cast<unsigned>(slot));
59}
60
65static void vcard_trim_cr(char* line) {
66 size_t len = strlen(line);
67 while (len > 0 && (line[len - 1] == '\r' || line[len - 1] == '\n')) {
68 line[len - 1] = '\0';
69 len--;
70 }
71}
72
79static bool vcard_line_has_content(const char* line, size_t len) {
80 const char* colon = static_cast<const char*>(memchr(line, ':', len));
81 if (!colon) return true;
82
83 const char* val = colon + 1;
84 size_t val_len = len - static_cast<size_t>(val - line);
85
86 while (val_len > 0 && (val[val_len - 1] == '\r' || val[val_len - 1] == '\n' ||
87 val[val_len - 1] == ' ' || val[val_len - 1] == '\t')) {
88 val_len--;
89 }
90
91 if (val_len == 0) return false;
92
93 if (len > 2 && (line[0] == 'N' && (line[1] == ':' || line[1] == ';'))) {
94 bool has_real_content = false;
95 for (size_t i = 0; i < val_len; i++) {
96 if (val[i] != ';' && val[i] != ' ' && val[i] != '\t') {
97 has_real_content = true;
98 break;
99 }
100 }
101 if (!has_real_content) return false;
102 }
103
104 if (len > 5 && strncasecmp(line, "IMPP:", 5) == 0) {
105 const char* second_colon = strchr(val, ':');
106 if (second_colon) {
107 const char* actual_val = second_colon + 1;
108 size_t actual_len = val_len - static_cast<size_t>(actual_val - val);
109 while (actual_len > 0 && (actual_val[actual_len - 1] == '\r' ||
110 actual_val[actual_len - 1] == '\n' || actual_val[actual_len - 1] == ' ')) {
111 actual_len--;
112 }
113 if (actual_len == 0) return false;
114 }
115 }
116
117 return true;
118}
119
126size_t vcard_filter_empty_fields(char* vcard, size_t len) {
127 char result[VCARD_MAX_LEN + 1];
128 size_t out_pos = 0;
129
130 const char* p = vcard;
131 const char* end = vcard + len;
132
133 while (p < end) {
134 const char* line_start = p;
135 const char* line_end = p;
136 while (line_end < end && *line_end != '\n') {
137 line_end++;
138 }
139 size_t line_len = static_cast<size_t>(line_end - line_start);
140
141 bool keep = false;
142 if (line_len >= 6 && strncasecmp(line_start, "BEGIN:", 6) == 0) keep = true;
143 else if (line_len >= 8 && strncasecmp(line_start, "VERSION:", 8) == 0) keep = true;
144 else if (line_len >= 4 && strncasecmp(line_start, "END:", 4) == 0) keep = true;
145 else keep = vcard_line_has_content(line_start, line_len);
146
147 if (keep && out_pos + line_len + 1 < sizeof(result)) {
148 memcpy(result + out_pos, line_start, line_len);
149 out_pos += line_len;
150 result[out_pos++] = '\n';
151 }
152
153 p = line_end;
154 if (p < end && *p == '\n') p++;
155 }
156
157 result[out_pos] = '\0';
158 memcpy(vcard, result, out_pos + 1);
159 return out_pos;
160}
161
170static bool vcard_extract_line(const char* vcard, const char* prefix, char* out, size_t out_len) {
171 if (!vcard || !prefix || !out || out_len == 0) return false;
172 size_t prefix_len = strlen(prefix);
173 const char* p = vcard;
174 while (*p) {
175 const char* line_start = p;
176 const char* line_end = strpbrk(p, "\r\n");
177 size_t line_len = line_end ? static_cast<size_t>(line_end - line_start) : strlen(line_start);
178
179 if (line_len >= prefix_len && strncmp(line_start, prefix, prefix_len) == 0) {
180 size_t copy_len = line_len - prefix_len;
181 if (copy_len >= out_len) copy_len = out_len - 1;
182 memcpy(out, line_start + prefix_len, copy_len);
183 out[copy_len] = '\0';
184 vcard_trim_cr(out);
185 return true;
186 }
187
188 if (!line_end) break;
189 p = line_end + 1;
190 if (*p == '\n') p++;
191 }
192 return false;
193}
194
203static void vcard_parse_names(const char* vcard, char* last, size_t last_len,
204 char* display, size_t display_len) {
205 if (!last || last_len == 0 || !display || display_len == 0) return;
206 last[0] = '\0';
207 display[0] = '\0';
208
209 char fn[64] = {0};
210 vcard_extract_line(vcard, "FN:", fn, sizeof(fn));
211
212 char n_line[128] = {0};
213 if (!vcard_extract_line(vcard, "N:", n_line, sizeof(n_line))) {
214 const char* n_tag = strstr(vcard, "\nN;");
215 if (n_tag) {
216 const char* val = strchr(n_tag, ':');
217 if (val) {
218 val++;
219 size_t copy_len = 0;
220 while (val[copy_len] && val[copy_len] != '\r' && val[copy_len] != '\n') {
221 copy_len++;
222 }
223 if (copy_len >= sizeof(n_line)) copy_len = sizeof(n_line) - 1;
224 memcpy(n_line, val, copy_len);
225 n_line[copy_len] = '\0';
226 }
227 }
228 }
229
230 if (n_line[0] != '\0') {
231 char* family = n_line;
232 char* given = strchr(n_line, ';');
233 if (given) {
234 *given = '\0';
235 given++;
236 char* next_semi = strchr(given, ';');
237 if (next_semi) *next_semi = '\0';
238 }
239 if (family && *family) {
240 strncpy(last, family, last_len - 1);
241 last[last_len - 1] = '\0';
242 }
243 if (display[0] == '\0') {
244 if (given && *given && family && *family) {
245 snprintf(display, display_len, "%s %s", given, family);
246 } else if (given && *given) {
247 snprintf(display, display_len, "%s", given);
248 } else if (family && *family) {
249 snprintf(display, display_len, "%s", family);
250 }
251 }
252 }
253
254 if (display[0] == '\0' && fn[0] != '\0') {
255 strncpy(display, fn, display_len - 1);
256 display[display_len - 1] = '\0';
257 }
258
259 if (display[0] == '\0') {
260 strncpy(display, "vCard", display_len - 1);
261 display[display_len - 1] = '\0';
262 }
263}
264
271static void set_err(char* err, size_t err_len, const char* msg) {
272 if (!err || err_len == 0) return;
273 strncpy(err, msg ? msg : "", err_len - 1);
274 err[err_len - 1] = '\0';
275}
276
285static bool vcard_validate(const char* vcard, size_t len, char* err, size_t err_len) {
286 if (!vcard || len == 0) {
287 set_err(err, err_len, "Empty vCard");
288 return false;
289 }
290 if (len > VCARD_MAX_LEN) {
291 set_err(err, err_len, "vCard too large");
292 return false;
293 }
294 if (memchr(vcard, '\0', len) != nullptr) {
295 set_err(err, err_len, "vCard contains NUL");
296 return false;
297 }
298 if (!strstr(vcard, "BEGIN:VCARD")) {
299 set_err(err, err_len, "Missing BEGIN:VCARD");
300 return false;
301 }
302 if (!strstr(vcard, "VERSION:4.0")) {
303 set_err(err, err_len, "Missing VERSION:4.0");
304 return false;
305 }
306 if (!strstr(vcard, "END:VCARD")) {
307 set_err(err, err_len, "Missing END:VCARD");
308 return false;
309 }
310 return true;
311}
312
316static void vcard_load_own(void) {
317 if (g_own_loaded) return;
318 g_own_vcard[0] = '\0';
319 g_own_present = false;
320
321 nvs_handle_t nvs;
322 if (nvs_open(VCARD_NAMESPACE, NVS_READONLY, &nvs) != ESP_OK) {
323 g_own_loaded = true;
324 return;
325 }
326
327 size_t len = sizeof(g_own_vcard);
328 if (nvs_get_str(nvs, VCARD_KEY_OWN, g_own_vcard, &len) == ESP_OK) {
329 g_own_present = true;
330 }
331 nvs_close(nvs);
332 g_own_loaded = true;
333}
334
343bool vcard_store_set_own(const char* vcard, size_t len, char* err, size_t err_len) {
344 if (!vcard_validate(vcard, len, err, err_len)) {
345 return false;
346 }
347
348 char tmp[VCARD_MAX_LEN + 1];
349 if (len > VCARD_MAX_LEN) {
350 set_err(err, err_len, "vCard too large");
351 return false;
352 }
353 memcpy(tmp, vcard, len);
354 tmp[len] = '\0';
355
356 len = vcard_filter_empty_fields(tmp, len);
357
358 nvs_handle_t nvs;
359 if (nvs_open(VCARD_NAMESPACE, NVS_READWRITE, &nvs) != ESP_OK) {
360 set_err(err, err_len, "NVS open failed");
361 return false;
362 }
363 esp_err_t ret = nvs_set_str(nvs, VCARD_KEY_OWN, tmp);
364 if (ret == ESP_OK) {
365 ret = nvs_commit(nvs);
366 }
367 nvs_close(nvs);
368 if (ret != ESP_OK) {
369 set_err(err, err_len, "NVS write failed");
370 return false;
371 }
372
373 strncpy(g_own_vcard, tmp, sizeof(g_own_vcard) - 1);
374 g_own_vcard[sizeof(g_own_vcard) - 1] = '\0';
375 g_own_loaded = true;
376 g_own_present = true;
377 LOG_I(TAG, "Own vCard stored (%d bytes)", (int)len);
378 return true;
379}
380
387size_t vcard_store_get_own(char* out, size_t max_len) {
388 if (!out || max_len == 0) return 0;
390 if (!g_own_present) return 0;
391 size_t len = strnlen(g_own_vcard, sizeof(g_own_vcard));
392 if (len >= max_len) len = max_len - 1;
393 memcpy(out, g_own_vcard, len);
394 out[len] = '\0';
395 return len;
396}
397
404 return g_own_present;
405}
406
413bool vcard_store_get_display_own(char* out, size_t max_len) {
414 if (!out || max_len == 0) return false;
416 if (!g_own_present) return false;
417 char last[32];
418 char display[64];
419 vcard_parse_names(g_own_vcard, last, sizeof(last), display, sizeof(display));
420 strncpy(out, display, max_len - 1);
421 out[max_len - 1] = '\0';
422 return true;
423}
424
430 nvs_handle_t nvs;
431 if (nvs_open(VCARD_NAMESPACE, NVS_READWRITE, &nvs) != ESP_OK) {
432 return false;
433 }
434 esp_err_t ret = nvs_erase_key(nvs, VCARD_KEY_OWN);
435 if (ret == ESP_OK) {
436 ret = nvs_commit(nvs);
437 }
438 nvs_close(nvs);
439 g_own_vcard[0] = '\0';
440 g_own_present = false;
441 g_own_loaded = true;
442 return ret == ESP_OK;
443}
444
449 if (g_cards_loaded) return;
450
451 memset(g_cards, 0, sizeof(g_cards));
452 g_card_count = 0;
453
454 nvs_handle_t nvs;
455 if (nvs_open(VCARD_NAMESPACE, NVS_READONLY, &nvs) != ESP_OK) {
456 g_cards_loaded = true;
457 return;
458 }
459
460 for (uint16_t slot = 0; slot < VCARD_MAX_CARDS; slot++) {
461 char key[8];
462 vcard_key_for_slot(key, sizeof(key), slot);
463 size_t len = 0;
464 if (nvs_get_str(nvs, key, nullptr, &len) != ESP_OK || len == 0 || len > VCARD_MAX_LEN) {
465 continue;
466 }
467 char tmp[VCARD_MAX_LEN + 1];
468 if (nvs_get_str(nvs, key, tmp, &len) == ESP_OK) {
469 tmp[VCARD_MAX_LEN] = '\0';
470 vcard_parse_names(tmp, g_cards[slot].last_name, sizeof(g_cards[slot].last_name),
471 g_cards[slot].display, sizeof(g_cards[slot].display));
472 g_cards[slot].used = true;
473 g_cards[slot].hash = fnv1a_hash(tmp, strnlen(tmp, VCARD_MAX_LEN));
474 g_card_count++;
475 }
476 }
477 nvs_close(nvs);
478 g_cards_loaded = true;
479}
480
485uint16_t vcard_store_count(void) {
487 return g_card_count;
488}
489
498static bool vcard_is_duplicate(nvs_handle_t nvs, const char* vcard, size_t len, uint32_t hash) {
499 for (uint16_t slot = 0; slot < VCARD_MAX_CARDS; slot++) {
500 char key[8];
501 vcard_key_for_slot(key, sizeof(key), slot);
502 size_t vlen = 0;
503 if (nvs_get_str(nvs, key, nullptr, &vlen) != ESP_OK || vlen == 0 || vlen > VCARD_MAX_LEN) {
504 continue;
505 }
506 char tmp[VCARD_MAX_LEN + 1];
507 if (nvs_get_str(nvs, key, tmp, &vlen) == ESP_OK) {
508 tmp[VCARD_MAX_LEN] = '\0';
509 if (strlen(tmp) == len && memcmp(tmp, vcard, len) == 0) {
510 return true;
511 }
512 }
513 (void)hash;
514 }
515 return false;
516}
517
518bool vcard_store_contains(const char* vcard, size_t len) {
519 if (!vcard || len == 0) return false;
521
522 nvs_handle_t nvs;
523 if (nvs_open(VCARD_NAMESPACE, NVS_READONLY, &nvs) != ESP_OK) {
524 return false;
525 }
526 bool found = vcard_is_duplicate(nvs, vcard, len, fnv1a_hash(vcard, len));
527 nvs_close(nvs);
528 return found;
529}
530
539bool vcard_store_add(const char* vcard, size_t len, char* err, size_t err_len) {
540 if (!vcard_validate(vcard, len, err, err_len)) {
541 return false;
542 }
545 set_err(err, err_len, "vCard list full");
546 return false;
547 }
548
549 nvs_handle_t nvs;
550 if (nvs_open(VCARD_NAMESPACE, NVS_READWRITE, &nvs) != ESP_OK) {
551 set_err(err, err_len, "NVS open failed");
552 return false;
553 }
554
555 uint32_t hash = fnv1a_hash(vcard, len);
556 if (vcard_is_duplicate(nvs, vcard, len, hash)) {
557 nvs_close(nvs);
558 set_err(err, err_len, "Duplicate vCard");
559 return false;
560 }
561
562 int free_slot = -1;
563 for (uint16_t i = 0; i < VCARD_MAX_CARDS; i++) {
564 if (!g_cards[i].used) {
565 free_slot = static_cast<int>(i);
566 break;
567 }
568 }
569
570 if (free_slot < 0) {
571 nvs_close(nvs);
572 set_err(err, err_len, "vCard list full");
573 return false;
574 }
575
576 char key[8];
577 vcard_key_for_slot(key, sizeof(key), static_cast<uint16_t>(free_slot));
578 char tmp[VCARD_MAX_LEN + 1];
579 memcpy(tmp, vcard, len);
580 tmp[len] = '\0';
581 len = vcard_filter_empty_fields(tmp, len);
582
583 esp_err_t ret = nvs_set_str(nvs, key, tmp);
584 if (ret == ESP_OK) {
585 ret = nvs_commit(nvs);
586 }
587 nvs_close(nvs);
588 if (ret != ESP_OK) {
589 set_err(err, err_len, "NVS write failed");
590 return false;
591 }
592
593 vcard_parse_names(tmp, g_cards[free_slot].last_name, sizeof(g_cards[free_slot].last_name),
594 g_cards[free_slot].display, sizeof(g_cards[free_slot].display));
595 g_cards[free_slot].used = true;
596 g_cards[free_slot].hash = hash;
597 g_card_count++;
598 return true;
599}
600
610bool vcard_store_update(uint16_t slot, const char* vcard, size_t len, char* err, size_t err_len) {
611 if (!vcard_validate(vcard, len, err, err_len)) {
612 return false;
613 }
615 if (slot >= VCARD_MAX_CARDS || !g_cards[slot].used) {
616 set_err(err, err_len, "No such vCard");
617 return false;
618 }
619
620 nvs_handle_t nvs;
621 if (nvs_open(VCARD_NAMESPACE, NVS_READWRITE, &nvs) != ESP_OK) {
622 set_err(err, err_len, "NVS open failed");
623 return false;
624 }
625
626 char key[8];
627 vcard_key_for_slot(key, sizeof(key), slot);
628 char tmp[VCARD_MAX_LEN + 1];
629 memcpy(tmp, vcard, len);
630 tmp[len] = '\0';
631 len = vcard_filter_empty_fields(tmp, len);
632
633 esp_err_t ret = nvs_set_str(nvs, key, tmp);
634 if (ret == ESP_OK) {
635 ret = nvs_commit(nvs);
636 }
637 nvs_close(nvs);
638 if (ret != ESP_OK) {
639 set_err(err, err_len, "NVS write failed");
640 return false;
641 }
642
643 vcard_parse_names(tmp, g_cards[slot].last_name, sizeof(g_cards[slot].last_name),
644 g_cards[slot].display, sizeof(g_cards[slot].display));
645 g_cards[slot].hash = fnv1a_hash(tmp, strnlen(tmp, VCARD_MAX_LEN));
646 return true;
647}
648
654bool vcard_store_delete(uint16_t slot) {
656 if (slot >= VCARD_MAX_CARDS || !g_cards[slot].used) {
657 return false;
658 }
659 nvs_handle_t nvs;
660 if (nvs_open(VCARD_NAMESPACE, NVS_READWRITE, &nvs) != ESP_OK) {
661 return false;
662 }
663 char key[8];
664 vcard_key_for_slot(key, sizeof(key), slot);
665 esp_err_t ret = nvs_erase_key(nvs, key);
666 if (ret == ESP_OK) {
667 ret = nvs_commit(nvs);
668 }
669 nvs_close(nvs);
670 if (ret != ESP_OK) return false;
671 g_cards[slot].used = false;
672 g_card_count--;
673 return true;
674}
675
683size_t vcard_store_get(uint16_t slot, char* out, size_t max_len) {
684 if (!out || max_len == 0) return 0;
686 if (slot >= VCARD_MAX_CARDS || !g_cards[slot].used) {
687 return 0;
688 }
689
690 nvs_handle_t nvs;
691 if (nvs_open(VCARD_NAMESPACE, NVS_READONLY, &nvs) != ESP_OK) {
692 return 0;
693 }
694 char key[8];
695 vcard_key_for_slot(key, sizeof(key), slot);
696 size_t len = max_len;
697 if (nvs_get_str(nvs, key, out, &len) != ESP_OK) {
698 nvs_close(nvs);
699 return 0;
700 }
701 nvs_close(nvs);
702 if (len > 0 && len <= max_len) {
703 out[len - 1] = '\0';
704 } else {
705 out[max_len - 1] = '\0';
706 }
707 return strnlen(out, max_len);
708}
709
717bool vcard_store_get_display(uint16_t slot, char* out, size_t max_len) {
718 if (!out || max_len == 0) return false;
720 if (slot >= VCARD_MAX_CARDS || !g_cards[slot].used) {
721 return false;
722 }
723 strncpy(out, g_cards[slot].display, max_len - 1);
724 out[max_len - 1] = '\0';
725 return true;
726}
727
731static void copy_field(char* dst, size_t dst_size, const char* src, size_t src_len) {
732 if (!dst || dst_size == 0) return;
733 if (src_len >= dst_size) src_len = dst_size - 1;
734 if (src && src_len > 0) memcpy(dst, src, src_len);
735 dst[src_len] = '\0';
736}
737
741static size_t trim_line_len(const char* line, size_t len) {
742 while (len > 0 && (line[len - 1] == '\r' || line[len - 1] == '\n' ||
743 line[len - 1] == ' ' || line[len - 1] == '\t')) {
744 len--;
745 }
746 return len;
747}
748
757static const char* line_value(const char* line, size_t line_len, size_t* out_len) {
758 const char* colon = static_cast<const char*>(memchr(line, ':', line_len));
759 if (!colon) return nullptr;
760 const char* val = colon + 1;
761 size_t val_len = line_len - static_cast<size_t>(val - line);
762 val_len = trim_line_len(val, val_len);
763 if (out_len) *out_len = val_len;
764 return val;
765}
766
777static bool match_property(const char* line, size_t line_len, const char* prop,
778 const char* type_value) {
779 size_t prop_len = strlen(prop);
780 if (line_len < prop_len) return false;
781 if (strncasecmp(line, prop, prop_len) != 0) return false;
782 if (line_len == prop_len) return false;
783
784 char next = line[prop_len];
785 if (type_value) {
786 if (next != ';') return false;
787 const char* params_end = static_cast<const char*>(memchr(line, ':', line_len));
788 if (!params_end) return false;
789 size_t params_len = static_cast<size_t>(params_end - (line + prop_len + 1));
790 const char* params = line + prop_len + 1;
791 const char* type_token = "TYPE=";
792 size_t token_len = strlen(type_token);
793 if (params_len < token_len) return false;
794 size_t value_len = strlen(type_value);
795 for (size_t i = 0; i + token_len <= params_len; i++) {
796 if (strncasecmp(params + i, type_token, token_len) == 0) {
797 const char* v = params + i + token_len;
798 size_t v_len = params_len - i - token_len;
799 if (v_len > value_len &&
800 (v[value_len] == ';' || v[value_len] == ',') &&
801 strncasecmp(v, type_value, value_len) == 0) {
802 return true;
803 }
804 if (v_len == value_len &&
805 strncasecmp(v, type_value, value_len) == 0) {
806 return true;
807 }
808 }
809 }
810 return false;
811 }
812 return next == ':' || next == ';';
813}
814
818static void parse_line_into_struct(const char* line, size_t line_len, vcard_data_t* out) {
819 line_len = trim_line_len(line, line_len);
820 if (line_len == 0) return;
821
822 size_t val_len = 0;
823 const char* val = nullptr;
824
825 if (match_property(line, line_len, "FN", nullptr)) {
826 val = line_value(line, line_len, &val_len);
827 if (val) copy_field(out->formatted_name, sizeof(out->formatted_name), val, val_len);
828 return;
829 }
830
831 if (match_property(line, line_len, "N", nullptr)) {
832 val = line_value(line, line_len, &val_len);
833 if (val) {
834 char tmp[256];
835 size_t copy = val_len < sizeof(tmp) - 1 ? val_len : sizeof(tmp) - 1;
836 memcpy(tmp, val, copy);
837 tmp[copy] = '\0';
838
839 char* family = tmp;
840 char* given = nullptr;
841 char* sep = strchr(tmp, ';');
842 if (sep) {
843 *sep = '\0';
844 given = sep + 1;
845 char* sep2 = strchr(given, ';');
846 if (sep2) *sep2 = '\0';
847 }
848 copy_field(out->family_name, sizeof(out->family_name),
849 family, family ? strlen(family) : 0);
850 if (given) {
851 copy_field(out->given_name, sizeof(out->given_name),
852 given, strlen(given));
853 }
854 }
855 return;
856 }
857
858 if (match_property(line, line_len, "ORG", nullptr)) {
859 val = line_value(line, line_len, &val_len);
860 if (val) copy_field(out->organization, sizeof(out->organization), val, val_len);
861 return;
862 }
863 if (match_property(line, line_len, "TITLE", nullptr)) {
864 val = line_value(line, line_len, &val_len);
865 if (val) copy_field(out->title, sizeof(out->title), val, val_len);
866 return;
867 }
868 if (match_property(line, line_len, "EMAIL", nullptr)) {
869 val = line_value(line, line_len, &val_len);
870 if (val) copy_field(out->email, sizeof(out->email), val, val_len);
871 return;
872 }
873 if (match_property(line, line_len, "URL", nullptr)) {
874 val = line_value(line, line_len, &val_len);
875 if (val) copy_field(out->url, sizeof(out->url), val, val_len);
876 return;
877 }
878 if (match_property(line, line_len, "NOTE", nullptr)) {
879 val = line_value(line, line_len, &val_len);
880 if (val) copy_field(out->note, sizeof(out->note), val, val_len);
881 return;
882 }
883 if (match_property(line, line_len, "X-SOCIALPROFILE", nullptr)) {
884 val = line_value(line, line_len, &val_len);
885 if (val) copy_field(out->social_profile, sizeof(out->social_profile), val, val_len);
886 return;
887 }
888
889 if (match_property(line, line_len, "TEL", "CELL")) {
890 val = line_value(line, line_len, &val_len);
891 if (val) copy_field(out->tel_cell, sizeof(out->tel_cell), val, val_len);
892 return;
893 }
894 if (match_property(line, line_len, "TEL", "HOME")) {
895 val = line_value(line, line_len, &val_len);
896 if (val) copy_field(out->tel_home, sizeof(out->tel_home), val, val_len);
897 return;
898 }
899 if (match_property(line, line_len, "TEL", "WORK")) {
900 val = line_value(line, line_len, &val_len);
901 if (val) copy_field(out->tel_work, sizeof(out->tel_work), val, val_len);
902 return;
903 }
904
905 if (line_len >= 5 && strncasecmp(line, "IMPP:", 5) == 0) {
906 const char* rest = line + 5;
907 size_t rest_len = line_len - 5;
908 const char* scheme_end = static_cast<const char*>(memchr(rest, ':', rest_len));
909 if (scheme_end) {
910 size_t scheme_len = static_cast<size_t>(scheme_end - rest);
911 const char* impp_val = scheme_end + 1;
912 size_t impp_val_len = rest_len - scheme_len - 1;
913 impp_val_len = trim_line_len(impp_val, impp_val_len);
914
915 if (scheme_len == 8 && strncasecmp(rest, "telegram", 8) == 0) {
916 copy_field(out->impp_telegram, sizeof(out->impp_telegram),
917 impp_val, impp_val_len);
918 } else if (scheme_len == 6 && strncasecmp(rest, "signal", 6) == 0) {
919 copy_field(out->impp_signal, sizeof(out->impp_signal),
920 impp_val, impp_val_len);
921 } else if (scheme_len == 6 && strncasecmp(rest, "matrix", 6) == 0) {
922 copy_field(out->impp_matrix, sizeof(out->impp_matrix),
923 impp_val, impp_val_len);
924 } else if (scheme_len == 7 && strncasecmp(rest, "threema", 7) == 0) {
925 copy_field(out->impp_threema, sizeof(out->impp_threema),
926 impp_val, impp_val_len);
927 }
928 }
929 }
930}
931
935bool vcard_parse_to_struct(const char* raw, vcard_data_t* out) {
936 if (!out) return false;
937 memset(out, 0, sizeof(*out));
938 if (!raw) return false;
939
940 bool saw_begin = false;
941 const char* p = raw;
942 while (*p) {
943 const char* line_start = p;
944 while (*p && *p != '\n') p++;
945 size_t line_len = static_cast<size_t>(p - line_start);
946
947 if (line_len >= 11 && strncasecmp(line_start, "BEGIN:VCARD", 11) == 0) {
948 saw_begin = true;
949 } else if (line_len >= 9 && strncasecmp(line_start, "END:VCARD", 9) == 0) {
950 break;
951 } else if (saw_begin) {
952 parse_line_into_struct(line_start, line_len, out);
953 }
954
955 if (*p == '\n') p++;
956 }
957 return saw_begin;
958}
959
963static bool append_field(char* buf, size_t buf_size, size_t* pos,
964 const char* prefix, const char* value) {
965 if (!value || value[0] == '\0') return true;
966 size_t prefix_len = strlen(prefix);
967 size_t value_len = strlen(value);
968 size_t need = prefix_len + value_len + 1;
969 if (*pos + need >= buf_size) return false;
970 memcpy(buf + *pos, prefix, prefix_len);
971 *pos += prefix_len;
972 memcpy(buf + *pos, value, value_len);
973 *pos += value_len;
974 buf[(*pos)++] = '\n';
975 return true;
976}
977
981size_t vcard_generate_from_struct(const vcard_data_t* data, char* out_buf, size_t buf_len) {
982 if (!data || !out_buf || buf_len < 32) return 0;
983 size_t pos = 0;
984
985 static const char* k_begin = "BEGIN:VCARD\n";
986 static const char* k_version = "VERSION:4.0\n";
987 static const char* k_end = "END:VCARD\n";
988
989 size_t begin_len = strlen(k_begin);
990 size_t version_len = strlen(k_version);
991 if (pos + begin_len >= buf_len) return 0;
992 memcpy(out_buf + pos, k_begin, begin_len); pos += begin_len;
993 if (pos + version_len >= buf_len) return 0;
994 memcpy(out_buf + pos, k_version, version_len); pos += version_len;
995
996 // N: family;given;;; -- emit only when at least one name part is set
997 if (data->family_name[0] || data->given_name[0]) {
998 char n_line[160];
999 snprintf(n_line, sizeof(n_line), "N:%s;%s;;;",
1000 data->family_name, data->given_name);
1001 if (!append_field(out_buf, buf_len, &pos, "", n_line)) return 0;
1002 }
1003
1004 // FN: fallback to "given family" when empty
1005 if (data->formatted_name[0]) {
1006 if (!append_field(out_buf, buf_len, &pos, "FN:", data->formatted_name)) return 0;
1007 } else if (data->given_name[0] || data->family_name[0]) {
1008 char fn_fallback[128];
1009 if (data->given_name[0] && data->family_name[0]) {
1010 snprintf(fn_fallback, sizeof(fn_fallback), "%s %s",
1011 data->given_name, data->family_name);
1012 } else if (data->given_name[0]) {
1013 snprintf(fn_fallback, sizeof(fn_fallback), "%s", data->given_name);
1014 } else {
1015 snprintf(fn_fallback, sizeof(fn_fallback), "%s", data->family_name);
1016 }
1017 if (!append_field(out_buf, buf_len, &pos, "FN:", fn_fallback)) return 0;
1018 }
1019
1020 if (!append_field(out_buf, buf_len, &pos, "ORG:", data->organization)) return 0;
1021 if (!append_field(out_buf, buf_len, &pos, "TITLE:", data->title)) return 0;
1022 if (!append_field(out_buf, buf_len, &pos, "EMAIL:", data->email)) return 0;
1023 if (!append_field(out_buf, buf_len, &pos, "TEL;TYPE=CELL:",data->tel_cell)) return 0;
1024 if (!append_field(out_buf, buf_len, &pos, "TEL;TYPE=HOME:",data->tel_home)) return 0;
1025 if (!append_field(out_buf, buf_len, &pos, "TEL;TYPE=WORK:",data->tel_work)) return 0;
1026 if (!append_field(out_buf, buf_len, &pos, "URL:", data->url)) return 0;
1027 if (!append_field(out_buf, buf_len, &pos, "IMPP:telegram:",data->impp_telegram)) return 0;
1028 if (!append_field(out_buf, buf_len, &pos, "IMPP:signal:", data->impp_signal)) return 0;
1029 if (!append_field(out_buf, buf_len, &pos, "IMPP:matrix:", data->impp_matrix)) return 0;
1030 if (!append_field(out_buf, buf_len, &pos, "IMPP:threema:", data->impp_threema)) return 0;
1031 if (!append_field(out_buf, buf_len, &pos, "X-SOCIALPROFILE:", data->social_profile)) return 0;
1032 if (!append_field(out_buf, buf_len, &pos, "NOTE:", data->note)) return 0;
1033
1034 size_t end_len = strlen(k_end);
1035 if (pos + end_len >= buf_len) return 0;
1036 memcpy(out_buf + pos, k_end, end_len); pos += end_len;
1037
1038 out_buf[pos] = '\0';
1039 return pos;
1040}
1041
1048uint16_t vcard_store_get_sorted(uint16_t* out_slots, uint16_t max_slots) {
1049 if (!out_slots || max_slots == 0) return 0;
1051
1052 uint16_t count = 0;
1053 for (uint16_t i = 0; i < VCARD_MAX_CARDS && count < max_slots; i++) {
1054 if (g_cards[i].used) {
1055 out_slots[count++] = i;
1056 }
1057 }
1058
1059 for (uint16_t i = 0; i < count; i++) {
1060 for (uint16_t j = i + 1; j < count; j++) {
1061 uint16_t a = out_slots[i];
1062 uint16_t b = out_slots[j];
1063 if (strcasecmp(g_cards[a].last_name, g_cards[b].last_name) > 0) {
1064 uint16_t tmp = out_slots[i];
1065 out_slots[i] = out_slots[j];
1066 out_slots[j] = tmp;
1067 }
1068 }
1069 }
1070 return count;
1071}
static const char * TAG
Centralized non-cryptographic hash utilities.
CDC Log: logging over TinyUSB CDC and UART.
#define LOG_I(tag, fmt,...)
Definition cdc_log.h:147
uint32_t fnv1a_32(const uint8_t *data, size_t len)
Computes FNV-1a 32-bit hash over a byte buffer.
Definition Hash.h:52
Structured representation of an own vCard for editor/wizard use.
Definition vcard_store.h:15
char tel_cell[32]
Definition vcard_store.h:23
char family_name[48]
Definition vcard_store.h:17
char tel_work[32]
Definition vcard_store.h:24
char formatted_name[96]
Definition vcard_store.h:18
char title[64]
Definition vcard_store.h:20
char social_profile[128]
Definition vcard_store.h:30
char tel_home[32]
Definition vcard_store.h:22
char note[128]
Definition vcard_store.h:31
char email[96]
Definition vcard_store.h:21
char organization[64]
Definition vcard_store.h:19
char given_name[48]
Definition vcard_store.h:16
char url[128]
Definition vcard_store.h:25
char impp_signal[64]
Definition vcard_store.h:27
char impp_threema[32]
Definition vcard_store.h:29
char impp_telegram[64]
Definition vcard_store.h:26
char impp_matrix[96]
Definition vcard_store.h:28
size_t vcard_store_get_own(char *out, size_t max_len)
Retrieves local own-vCard text.
static void vcard_trim_cr(char *line)
Trims trailing CR/LF characters from one line.
bool vcard_store_has_own(void)
Returns whether local own-vCard exists.
static void vcard_parse_names(const char *vcard, char *last, size_t last_len, char *display, size_t display_len)
Derives sortable last-name and display-name fields from vCard.
static const char * line_value(const char *line, size_t line_len, size_t *out_len)
Returns pointer to the value portion of a vCard line and its length. For a line like TEL;TYPE=HOME:12...
static void vcard_load_own(void)
Lazily loads local own-vCard from NVS cache.
bool vcard_store_set_own(const char *vcard, size_t len, char *err, size_t err_len)
Stores local own-vCard after validation and field filtering.
bool vcard_store_update(uint16_t slot, const char *vcard, size_t len, char *err, size_t err_len)
Overwrites the vCard stored at slot in place after validation.
static vcard_meta_t g_cards[100]
static bool vcard_extract_line(const char *vcard, const char *prefix, char *out, size_t out_len)
Extracts value from first vCard line matching prefix.
size_t vcard_store_get(uint16_t slot, char *out, size_t max_len)
Retrieves raw vCard text from slot.
static constexpr const char * VCARD_NAMESPACE
static bool g_own_present
bool vcard_store_clear_own(void)
Deletes local own-vCard from storage.
static bool vcard_validate(const char *vcard, size_t len, char *err, size_t err_len)
Validates basic vCard format constraints.
static bool vcard_line_has_content(const char *line, size_t len)
Checks whether a vCard line contains meaningful field content.
static bool match_property(const char *line, size_t line_len, const char *prop, const char *type_value)
Tests whether one vCard property prefix matches a line, optionally accepting a TYPE=....
bool vcard_parse_to_struct(const char *raw, vcard_data_t *out)
Parses raw vCard 4.0 text into a structured representation.
static void parse_line_into_struct(const char *line, size_t line_len, vcard_data_t *out)
Parses a single vCard line into the structured data, when recognized.
static void vcard_key_for_slot(char *out, size_t out_len, uint16_t slot)
Formats NVS key name for a card slot.
bool vcard_store_add(const char *vcard, size_t len, char *err, size_t err_len)
Adds peer vCard to first free slot after validation and duplicate check.
uint16_t vcard_store_get_sorted(uint16_t *out_slots, uint16_t max_slots)
Returns slot indices of stored cards sorted by last name.
size_t vcard_filter_empty_fields(char *vcard, size_t len)
Removes empty optional fields from vCard text in-place.
static constexpr const char * VCARD_KEY_OWN
uint16_t vcard_store_count(void)
Returns number of stored peer vCards.
bool vcard_store_contains(const char *vcard, size_t len)
Reports whether an exact-text vCard is already stored.
static bool vcard_is_duplicate(nvs_handle_t nvs, const char *vcard, size_t len, uint32_t hash)
Checks whether candidate vCard is already stored.
static bool g_own_loaded
bool vcard_store_get_display_own(char *out, size_t max_len)
Retrieves display name derived from local own-vCard.
bool vcard_store_get_display(uint16_t slot, char *out, size_t max_len)
Retrieves cached display label for slot.
static bool g_cards_loaded
static size_t trim_line_len(const char *line, size_t len)
Trims trailing CR and LF and whitespace characters from a length-bounded view.
static void set_err(char *err, size_t err_len, const char *msg)
Writes error text into bounded output buffer.
static void copy_field(char *dst, size_t dst_size, const char *src, size_t src_len)
Copies a bounded value into a fixed-size destination buffer.
size_t vcard_generate_from_struct(const vcard_data_t *data, char *out_buf, size_t buf_len)
Generates a vCard 4.0 text representation from the structured data.
void vcard_store_init(void)
Initializes metadata cache for stored peer vCards.
bool vcard_store_delete(uint16_t slot)
Deletes peer vCard at slot index.
static bool append_field(char *buf, size_t buf_size, size_t *pos, const char *prefix, const char *value)
Appends prefix:value\n to the output buffer if value is non-empty.
static uint16_t g_card_count
static char g_own_vcard[768+1]
static uint32_t fnv1a_hash(const char *data, size_t len)
Computes FNV-1a hash for vCard duplicate tracking.
#define VCARD_MAX_CARDS
Definition vcard_store.h:7
#define VCARD_MAX_LEN
Definition vcard_store.h:6