1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29 #include <stdio.h>
30 #include <stdlib.h>
31 #include <string.h>
32 #include <errno.h>
33
34 #include "db.h"
35
36 #include <cx/utils.h>
37
38 #include <libidav/utils.h>
39
40 #include <libxml/encoding.h>
41 #include <libxml/xmlwriter.h>
42
43
44 #define xstreq(a,b) xmlStrEqual(
BAD_CAST a,
BAD_CAST b)
45
46 #ifdef _WIN32
47 #define ENV_HOME getenv(
"USERPROFILE")
48 #else
49 #define ENV_HOME getenv(
"HOME")
50 #endif
51
52 SyncDatabase* load_db(
char *name) {
53 char *dav_dir = util_concat_path(
ENV_HOME,
".dav");
54 char *db_file = util_concat_path(dav_dir, name);
55 free(dav_dir);
56
57 SyncDatabase *db = malloc(
sizeof(SyncDatabase));
58 db->resources = cxHashMapCreate(cxDefaultAllocator,
CX_STORE_POINTERS,
2048);
59 db->conflict = cxHashMapCreate(cxDefaultAllocator,
CX_STORE_POINTERS,
16);
60
61 db->resources->destructor_data = (cx_destructor_func)local_resource_free;
62 db->conflict->destructor_data = (cx_destructor_func)local_resource_free;
63
64 xmlTextReaderPtr reader = xmlReaderForFile(db_file,
NULL,
0);
65 if(!reader) {
66 xmlDoc *doc = doc = xmlNewDoc(
BAD_CAST "1.0");
67 xmlNode *root = xmlNewNode(
NULL,
BAD_CAST "directory");
68 xmlDocSetRootElement(doc, root);
69 if(xmlSaveFormatFileEnc(db_file, doc,
"UTF-8",
1) == -
1) {
70 destroy_db(db);
71 db =
NULL;
72 }
73 xmlFreeDoc(doc);
74 free(db_file);
75 return db;
76 }
77 free(db_file);
78
79 int error =
0;
80 while(xmlTextReaderRead(reader)) {
81 int type = xmlTextReaderNodeType(reader);
82 const xmlChar *name = xmlTextReaderConstName(reader);
83
84 if(type ==
XML_READER_TYPE_ELEMENT) {
85 if(xstreq(name,
"resource")) {
86 LocalResource *res = process_resource(reader);
87 if(res) {
88 cxMapPut(db->resources, cx_hash_key_str(res->path), res);
89 }
else {
90 error =
1;
91 break;
92 }
93 }
else if(xstreq(name,
"conflict")) {
94 LocalResource *res = process_conflict(reader);
95 if(res) {
96 cxMapPut(db->conflict, cx_hash_key_str(res->path), res);
97 }
else {
98 error =
1;
99 break;
100 }
101 }
102 }
103 }
104
105 xmlFreeTextReader(reader);
106 if(error) {
107 destroy_db(db);
108 return NULL;
109 }
else {
110 return db;
111 }
112 }
113
114 void process_parts(xmlTextReaderPtr reader, LocalResource *res) {
115
116
117 CxList *parts = cxLinkedListCreateSimple(
CX_STORE_POINTERS);
118 parts->destructor_data = (cx_destructor_func)filepart_free;
119
120 FilePart *current_part =
NULL;
121
122 size_t count =
0;
123 int field = -
1;
124 int err =
0;
125 while(xmlTextReaderRead(reader)) {
126 int type = xmlTextReaderNodeType(reader);
127 const xmlChar *name = xmlTextReaderConstName(reader);
128 int depth = xmlTextReaderDepth(reader);
129
130 if(type ==
XML_READER_TYPE_ELEMENT) {
131 if(depth ==
3 && xstreq(name,
"part")) {
132 current_part = calloc(
1,
sizeof(FilePart));
133 current_part->block = count;
134 cxListAdd(parts, current_part);
135 count++;
136 }
else if(depth ==
4) {
137 if(xstreq(name,
"hash")) {
138 field =
0;
139 }
else if(xstreq(name,
"etag")) {
140 field =
1;
141 }
142 }
143 }
else if(type ==
XML_READER_TYPE_END_ELEMENT) {
144 if(depth ==
2) {
145
146 break;
147 }
else if(depth ==
3) {
148 if(current_part) {
149 if(!current_part->hash || !current_part->etag) {
150 err =
1;
151 }
152 }
153
154 current_part =
NULL;
155 }
156 field = -
1;
157 }
else if(type ==
XML_READER_TYPE_TEXT && depth ==
5 && current_part) {
158 const char *text = (
const char*)xmlTextReaderConstValue(reader);
159 if(field ==
0) {
160 current_part->hash = strdup(text);
161 }
else if(field ==
1) {
162 current_part->etag = strdup(text);
163 }
164 }
165 }
166
167 if(!err) {
168 FilePart *file_parts = calloc(count,
sizeof(FilePart));
169 size_t i =
0;
170 CxIterator iter = cxListIterator(parts);
171 cx_foreach(FilePart*, p, iter) {
172 file_parts[i] = *p;
173 free(p);
174 i++;
175 }
176
177 res->parts = file_parts;
178 res->numparts = count;
179 }
180
181 cxListDestroy(parts);
182 }
183
184 LocalResource* process_resource(xmlTextReaderPtr reader) {
185 LocalResource *res = calloc(
1,
sizeof(LocalResource));
186
187 int field = -
1;
188 while(xmlTextReaderRead(reader)) {
189 int type = xmlTextReaderNodeType(reader);
190 const xmlChar *name = xmlTextReaderConstName(reader);
191
192 if(type ==
XML_READER_TYPE_ELEMENT) {
193 if(xstreq(name,
"path")) {
194 field =
0;
195 }
else if(xstreq(name,
"etag")) {
196 field =
1;
197 }
else if(xstreq(name,
"lastmodified")) {
198 field =
2;
199 }
else if(xstreq(name,
"size")) {
200 field =
3;
201 }
else if(xstreq(name,
"tags-hash")) {
202 field =
4;
203 }
else if(xstreq(name,
"mode")) {
204 field =
5;
205 }
else if(xstreq(name,
"uid")) {
206 field =
6;
207 }
else if(xstreq(name,
"gid")) {
208 field =
7;
209 }
else if(xstreq(name,
"xattr-hash")) {
210 field =
8;
211 }
else if(xstreq(name,
"remote-tags-hash")) {
212 field =
9;
213 }
else if(xstreq(name,
"blocksize")) {
214 field =
10;
215 }
else if(xstreq(name,
"hash")) {
216 field =
11;
217 }
else if(xstreq(name,
"link")) {
218 field =
12;
219 }
else if(xstreq(name,
"localpath")) {
220 field =
13;
221 }
else if(xstreq(name,
"versioncontrol")) {
222 field =
14;
223 }
else if(xstreq(name,
"skipped")) {
224 res->skipped =
TRUE;
225 }
else if(xstreq(name,
"tags-updated")) {
226 res->tags_updated =
TRUE;
227 }
else if(xstreq(name,
"parts")) {
228 process_parts(reader, res);
229 }
else if(xstreq(name,
"isdirectory")) {
230 res->isdirectory =
1;
231 }
232 }
else if(type ==
XML_READER_TYPE_TEXT) {
233 const xmlChar *value = xmlTextReaderConstValue(reader);
234
235 switch(field) {
236 case 0: {
237 res->path = strdup((
char*)value);
238 break;
239 }
240 case 1: {
241 res->etag = strdup((
char*)value);
242 break;
243 }
244 case 2: {
245
246
247 char *endptr = (
char*)value;
248 time_t t = strtoll((
char*)value, &endptr,
10);
249 if(endptr == (
char*)value) {
250 fprintf(
251 stderr,
252 "lastmodified does not contain a number: %s\n", value);
253 }
else {
254 res->last_modified = t;
255 }
256 break;
257 }
258 case 3: {
259 res->size =
0;
260 int64_t filelen =
0;
261 if(util_strtoint((
char*)value, &filelen)) {
262 if(filelen >
0) {
263 res->size = (
size_t)filelen;
264 }
265 }
266 break;
267 }
268 case 4: {
269 res->tags_hash = strdup((
char*)value);
270 break;
271 }
272 case 5: {
273 char *end;
274 errno =
0;
275 long int mode = strtol((
char*)value, &end,
8);
276 if(errno ==
0) {
277 res->mode = (
mode_t)mode;
278 }
279 break;
280 }
281 case 6: {
282 uint64_t uid =
0;
283 if(util_strtouint((
char*)value, &uid)) {
284 res->uid = (
uid_t)uid;
285 }
286 break;
287 }
288 case 7: {
289 uint64_t gid =
0;
290 if(util_strtouint((
char*)value, &gid)) {
291 res->gid = (
gid_t)gid;
292 }
293 break;
294 }
295 case 8: {
296 res->xattr_hash = strdup((
char*)value);
297 break;
298 }
299 case 9: {
300 res->remote_tags_hash = strdup((
char*)value);
301 break;
302 }
303 case 10: {
304 int64_t blsz =
0;
305 if(util_strtoint((
char*)value, &blsz)) {
306 if(blsz < -
1) {
307 blsz = -
1;
308 }
309 if(blsz >
0 && blsz <
16) {
310 blsz =
0;
311 }
312 res->blocksize = blsz;
313 }
314 break;
315 }
316 case 11: {
317 res->hash = strdup((
char*)value);
318 break;
319 }
320 case 12: {
321 res->link_target = strdup((
char*)value);
322 break;
323 }
324 case 13: {
325 res->local_path = strdup((
char*)value);
326 }
327 case 14: {
328 res->versioncontrol = util_getboolean((
char*)value);
329 }
330 }
331 }
else if(
XML_READER_TYPE_END_ELEMENT) {
332 if(xstreq(name,
"resource")) {
333 break;
334 }
else {
335 field = -
1;
336 }
337 }
338 }
339
340 if(!res->path) {
341
342 return NULL;
343 }
else {
344 return res;
345 }
346 }
347
348 LocalResource* process_conflict(xmlTextReaderPtr reader) {
349 LocalResource *res = calloc(
1,
sizeof(LocalResource));
350
351 int field =
0;
352 while(xmlTextReaderRead(reader)) {
353 int type = xmlTextReaderNodeType(reader);
354 const xmlChar *name = xmlTextReaderConstName(reader);
355
356 if(type ==
XML_READER_TYPE_ELEMENT) {
357 if(xstreq(name,
"path")) {
358 field =
1;
359 }
else if(xstreq(name,
"source")) {
360 field =
2;
361 }
362 }
else if(type ==
XML_READER_TYPE_TEXT) {
363 const xmlChar *value = xmlTextReaderConstValue(reader);
364 switch(field) {
365 case 1: {
366 res->path = strdup((
const char*)value);
367 break;
368 }
369 case 2: {
370 res->conflict_source = strdup((
const char*)value);
371 break;
372 }
373 }
374 }
else if(
XML_READER_TYPE_END_ELEMENT) {
375 if(xstreq(name,
"conflict")) {
376 break;
377 }
else {
378 field =
0;
379 }
380 }
381 }
382
383 if(!res->path) {
384
385 return NULL;
386 }
else {
387 return res;
388 }
389 }
390
391 int store_db(SyncDatabase *db,
char *name,
uint32_t settings) {
392
393 char *dav_dir = util_concat_path(
ENV_HOME,
".dav");
394 char *db_file = util_concat_path(dav_dir, name);
395 free(dav_dir);
396 xmlTextWriterPtr writer = xmlNewTextWriterFilename(db_file,
0);
397 if(!writer) {
398 fprintf(stderr,
"Cannot write db file: %s\n", db_file);
399 free(db_file);
400 return -
1;
401 }
402 free(db_file);
403
404
405 int r =
0;
406 r = xmlTextWriterStartDocument(writer,
NULL,
"UTF-8",
NULL);
407 if(r <
0) {
408 xmlFreeTextWriter(writer);
409 return -
1;
410 }
411 xmlTextWriterStartElement(writer,
BAD_CAST "directory");
412
413
414 CxIterator i = cxMapIteratorValues(db->resources);
415 LocalResource *res;
416 cx_foreach(LocalResource*, res, i) {
417
418 xmlTextWriterStartElement(writer,
BAD_CAST "resource");
419
420 r = xmlTextWriterWriteElement(
421 writer,
422 BAD_CAST "path",
423 BAD_CAST res->path);
424 if(r <
0) {
425 fprintf(stderr,
"Cannot write path: %s\n", res->path);
426 xmlFreeTextWriter(writer);
427 return -
1;
428 }
429
430 if(res->isdirectory) {
431 r = xmlTextWriterStartElement(writer,
BAD_CAST "isdirectory");
432 r += xmlTextWriterEndElement(writer);
433 if(r <
0) {
434 fprintf(stderr,
"Cannot write isdirectory\n");
435 xmlFreeTextWriter(writer);
436 return -
1;
437 }
438 }
439
440 if(res->etag) {
441 r = xmlTextWriterWriteElement(
442 writer,
443 BAD_CAST "etag",
444 BAD_CAST res->etag);
445 if(r <
0) {
446 fprintf(stderr,
"Cannot write etag: %s\n", res->etag);
447 xmlFreeTextWriter(writer);
448 return -
1;
449 }
450 }
451
452 if(res->hash) {
453 r = xmlTextWriterWriteElement(
454 writer,
455 BAD_CAST "hash",
456 BAD_CAST res->hash);
457 if(r <
0) {
458 fprintf(stderr,
"Cannot write hash: %s\n", res->hash);
459 xmlFreeTextWriter(writer);
460 return -
1;
461 }
462 }
463
464 r = xmlTextWriterWriteFormatElement(
465 writer,
466 BAD_CAST "lastmodified",
467 "%" PRId64,
468 (
int64_t)res->last_modified);
469 if(r <
0) {
470 fprintf(stderr,
"Cannot write lastmodified\n");
471 xmlFreeTextWriter(writer);
472 return -
1;
473 }
474
475 if(res->blocksize !=
0) {
476 r = xmlTextWriterWriteFormatElement(
477 writer,
478 BAD_CAST "mode",
479 "%" PRId64,
480 res->blocksize);
481 if(r <
0) {
482 fprintf(stderr,
"Cannot write blocksize\n");
483 xmlFreeTextWriter(writer);
484 return -
1;
485 }
486 }
487
488 if((settings &
DB_STORE_MODE) ==
DB_STORE_MODE) {
489 r = xmlTextWriterWriteFormatElement(
490 writer,
491 BAD_CAST "mode",
492 "%o",
493 (
int)res->mode);
494 if(r <
0) {
495 fprintf(stderr,
"Cannot write mode\n");
496 xmlFreeTextWriter(writer);
497 return -
1;
498 }
499 }
500
501 if((settings &
DB_STORE_OWNER) ==
DB_STORE_OWNER) {
502 r = xmlTextWriterWriteFormatElement(
503 writer,
504 BAD_CAST "uid",
505 "%u",
506 (
unsigned int)res->uid);
507 if(r <
0) {
508 fprintf(stderr,
"Cannot write uid\n");
509 xmlFreeTextWriter(writer);
510 return -
1;
511 }
512 r = xmlTextWriterWriteFormatElement(
513 writer,
514 BAD_CAST "gid",
515 "%u",
516 (
unsigned int)res->gid);
517 if(r <
0) {
518 fprintf(stderr,
"Cannot write gid\n");
519 xmlFreeTextWriter(writer);
520 return -
1;
521 }
522 }
523
524 r = xmlTextWriterWriteFormatElement(
525 writer,
526 BAD_CAST "size",
527 "%" PRId64,
528 (
int64_t)res->size);
529 if(r <
0) {
530 fprintf(stderr,
"Cannot write size\n");
531 xmlFreeTextWriter(writer);
532 return -
1;
533 }
534
535 if(res->tags_hash) {
536 r = xmlTextWriterWriteElement(
537 writer,
538 BAD_CAST "tags-hash",
539 BAD_CAST res->tags_hash);
540 if(r <
0) {
541 fprintf(stderr,
"Cannot write tags-hash: %s\n", res->tags_hash);
542 xmlFreeTextWriter(writer);
543 return -
1;
544 }
545 }
546
547 if(res->remote_tags_hash) {
548 r = xmlTextWriterWriteElement(
549 writer,
550 BAD_CAST "remote-tags-hash",
551 BAD_CAST res->remote_tags_hash);
552 if(r <
0) {
553 fprintf(stderr,
"Cannot write remote-tags-hash: %s\n", res->remote_tags_hash);
554 xmlFreeTextWriter(writer);
555 return -
1;
556 }
557 }
558
559 if(res->link_target) {
560 r = xmlTextWriterWriteElement(
561 writer,
562 BAD_CAST "link",
563 BAD_CAST res->link_target);
564 if(r <
0) {
565 fprintf(stderr,
"Cannot write link: %s\n", res->link_target);
566 xmlFreeTextWriter(writer);
567 return -
1;
568 }
569 }
570
571 if(res->local_path) {
572 r = xmlTextWriterWriteElement(
573 writer,
574 BAD_CAST "localpath",
575 BAD_CAST res->local_path);
576 if(r <
0) {
577 fprintf(stderr,
"Cannot write localpath: %s\n", res->local_path);
578 xmlFreeTextWriter(writer);
579 return -
1;
580 }
581 }
582
583
584 if(res->xattr_hash) {
585 r = xmlTextWriterWriteElement(
586 writer,
587 BAD_CAST "xattr-hash",
588 BAD_CAST res->xattr_hash);
589 if(r <
0) {
590 fprintf(stderr,
"Cannot write xattr-hash: %s\n", res->xattr_hash);
591 xmlFreeTextWriter(writer);
592 return -
1;
593 }
594 }
595
596 if(res->skipped) {
597 r = xmlTextWriterStartElement(writer,
BAD_CAST "skipped");
598 r += xmlTextWriterEndElement(writer);
599 if(r <
0) {
600 fprintf(stderr,
"Cannot write skipped\n");
601 xmlFreeTextWriter(writer);
602 return -
1;
603 }
604 }
605
606 if(res->tags_updated) {
607 r = xmlTextWriterStartElement(writer,
BAD_CAST "tags-updated");
608 r += xmlTextWriterEndElement(writer);
609 if(r <
0) {
610 fprintf(stderr,
"Cannot write tags-updated\n");
611 xmlFreeTextWriter(writer);
612 return -
1;
613 }
614 }
615
616 if(res->numparts >
0) {
617 r = xmlTextWriterStartElement(writer,
BAD_CAST "parts");
618 if(r <
0) {
619 xmlFreeTextWriter(writer);
620 return -
1;
621 }
622 for(
size_t i=
0;i<res->numparts;i++) {
623 FilePart p = res->parts[i];
624 r = xmlTextWriterStartElement(writer,
BAD_CAST "part");
625 if(r <
0) {
626 xmlFreeTextWriter(writer);
627 return -
1;
628 }
629
630 if(p.hash) {
631 r = xmlTextWriterWriteElement(writer,
BAD_CAST "hash",
BAD_CAST p.hash);
632 if(r <
0) {
633 xmlFreeTextWriter(writer);
634 return -
1;
635 }
636 }
637 if(p.etag) {
638 r = xmlTextWriterWriteElement(writer,
BAD_CAST "etag",
BAD_CAST p.etag);
639 if(r <
0) {
640 xmlFreeTextWriter(writer);
641 return -
1;
642 }
643 }
644 r = xmlTextWriterEndElement(writer);
645 if(r <
0) {
646 xmlFreeTextWriter(writer);
647 return -
1;
648 }
649 }
650 r = xmlTextWriterEndElement(writer);
651 if(r <
0) {
652 xmlFreeTextWriter(writer);
653 return -
1;
654 }
655 }
656
657 if(res->versioncontrol) {
658 r = xmlTextWriterWriteElement(
659 writer,
660 BAD_CAST "versioncontrol",
661 BAD_CAST "true");
662 if(r <
0) {
663 fprintf(stderr,
"Cannot write versioncontrol\n");
664 xmlFreeTextWriter(writer);
665 return -
1;
666 }
667 }
668
669
670 xmlTextWriterEndElement(writer);
671 }
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691 i = cxMapIteratorValues(db->conflict);
692 cx_foreach(LocalResource*, res, i) {
693
694 xmlTextWriterStartElement(writer,
BAD_CAST "conflict");
695
696 xmlTextWriterWriteElement(
697 writer,
698 BAD_CAST "path",
699 BAD_CAST res->path);
700
701 if(res->conflict_source) {
702 xmlTextWriterWriteElement(
703 writer,
704 BAD_CAST "source",
705 BAD_CAST res->conflict_source);
706 }
707
708
709 xmlTextWriterEndElement(writer);
710 }
711
712
713 xmlTextWriterEndElement(writer);
714 r = xmlTextWriterEndDocument(writer);
715 if(r <
0) {
716 xmlFreeTextWriter(writer);
717 return -
1;
718 }
719 xmlFreeTextWriter(writer);
720 return 0;
721 }
722
723 void destroy_db(SyncDatabase *db) {
724 cxMapDestroy(db->resources);
725 cxMapDestroy(db->conflict);
726 free(db);
727 }
728
729 void local_resource_free(LocalResource *res) {
730 if(!res) {
731 return;
732 }
733 if(res->name) {
734 free(res->name);
735 }
736 if(res->path) {
737 free(res->path);
738 }
739 if(res->etag) {
740 free(res->etag);
741 }
742 if(res->cached_tags) {
743 cxBufferFree(res->cached_tags);
744 }
745 if(res->tags_hash) {
746 free(res->tags_hash);
747 }
748 if(res->prev_hash) {
749 free(res->prev_hash);
750 }
751 free(res);
752 }
753
754 static char* nullstrdup(
const char *s) {
755 return s ? strdup(s) :
NULL;
756 }
757
758 void local_resource_copy_parts(LocalResource *from, LocalResource *to) {
759 if(from->parts) {
760 to->numparts = from->numparts;
761 to->parts = calloc(from->numparts,
sizeof(FilePart));
762 for(
int i=
0;i<to->numparts;i++) {
763 FilePart s = from->parts[i];
764 FilePart p;
765 p.block = s.block;
766 p.hash = nullstrdup(s.hash);
767 p.etag = nullstrdup(s.etag);
768 to->parts[i] = p;
769 }
770 }
771 }
772
773 LocalResource* local_resource_copy(LocalResource *src,
const char *new_path) {
774 LocalResource *newres = calloc(
1,
sizeof(LocalResource));
775 newres->path = strdup(new_path);
776 newres->etag = nullstrdup(src->etag);
777 newres->hash = nullstrdup(src->hash);
778 newres->last_modified = src->last_modified;
779 newres->mode = src->mode;
780 newres->uid = src->uid;
781 newres->gid = src->gid;
782 newres->size = src->size;
783 newres->isdirectory = src->isdirectory;
784 newres->skipped = src->skipped;
785 newres->versioncontrol = src->versioncontrol;
786
787 if(src->xattr) {
788 XAttributes *xattr = calloc(
1,
sizeof(XAttributes));
789 xattr->hash = nullstrdup(src->xattr->hash);
790 xattr->nattr = src->xattr->nattr;
791 xattr->names = calloc(xattr->nattr,
sizeof(
char*));
792 xattr->values = calloc(xattr->nattr,
sizeof(cxmutstr));
793 for(
int i=
0;i<xattr->nattr;i++) {
794 xattr->names[i] = strdup(src->xattr->names[i]);
795 xattr->values[i] = cx_strdup(cx_strcast(src->xattr->values[i]));
796 }
797 newres->xattr = xattr;
798 }
799
800 newres->tags_hash = nullstrdup(src->tags_hash);
801 newres->xattr_hash = nullstrdup(src->xattr_hash);
802 newres->remote_tags_hash = nullstrdup(src->remote_tags_hash);
803
804 local_resource_copy_parts(src, newres);
805
806 newres->blocksize = src->blocksize;
807
808 newres->tags_updated = src->tags_updated;
809 newres->finfo_updated = src->finfo_updated;
810 newres->xattr_updated = src->xattr_updated;
811 newres->metadata_updated = src->metadata_updated;
812
813 return newres;
814
815 }
816
817 void filepart_free(FilePart *part) {
818 if(part->etag) {
819 free(part->etag);
820 }
821 if(part->hash) {
822 free(part->hash);
823 }
824 free(part);
825 }
826
827 CxMap* create_hash_index(SyncDatabase *db) {
828 CxMap *hmap = cxHashMapCreate(cxDefaultAllocator,
CX_STORE_POINTERS, db->resources->size +
64);
829
830 CxIterator i = cxMapIteratorValues(db->resources);
831 LocalResource *res;
832 cx_foreach(LocalResource*, res, i) {
833 if(res->hash) {
834 cxMapPut(hmap, cx_hash_key_str(res->hash), res);
835 }
836 }
837
838 return hmap;
839 }
840