1 /* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
2
3 #include "lib.h"
4 #include "hash.h"
5 #include "mail-index-modseq.h"
6 #include "mail-storage-private.h"
7 #include "dsync-mail.h"
8 #include "dsync-mailbox.h"
9 #include "dsync-transaction-log-scan.h"
10
11 struct dsync_transaction_log_scan {
12 pool_t pool;
13 HASH_TABLE_TYPE(dsync_uid_mail_change) changes;
14 HASH_TABLE_TYPE(dsync_attr_change) attr_changes;
15 struct mail_index_view *view;
16 uint32_t highest_wanted_uid;
17
18 uint32_t last_log_seq;
19 uoff_t last_log_offset;
20
21 bool returned_all_changes;
22 };
23
24 static bool ATTR_NOWARN_UNUSED_RESULT
export_change_get(struct dsync_transaction_log_scan * ctx,uint32_t uid,enum dsync_mail_change_type type,struct dsync_mail_change ** change_r)25 export_change_get(struct dsync_transaction_log_scan *ctx, uint32_t uid,
26 enum dsync_mail_change_type type,
27 struct dsync_mail_change **change_r)
28 {
29 struct dsync_mail_change *change;
30 const char *orig_guid;
31
32 i_assert(uid > 0);
33 i_assert(type != DSYNC_MAIL_CHANGE_TYPE_SAVE);
34
35 *change_r = NULL;
36
37 if (uid > ctx->highest_wanted_uid)
38 return FALSE;
39
40 change = hash_table_lookup(ctx->changes, POINTER_CAST(uid));
41 if (change == NULL) {
42 /* first change for this UID */
43 change = p_new(ctx->pool, struct dsync_mail_change, 1);
44 change->uid = uid;
45 change->type = type;
46 hash_table_insert(ctx->changes, POINTER_CAST(uid), change);
47 } else if (type == DSYNC_MAIL_CHANGE_TYPE_EXPUNGE) {
48 /* expunge overrides flag changes */
49 orig_guid = change->guid;
50 i_zero(change);
51 change->type = type;
52 change->uid = uid;
53 change->guid = orig_guid;
54 } else if (change->type == DSYNC_MAIL_CHANGE_TYPE_EXPUNGE) {
55 /* already expunged, this change doesn't matter */
56 return FALSE;
57 } else {
58 /* another flag update */
59 }
60 *change_r = change;
61 return TRUE;
62 }
63
64 static void
log_add_expunge(struct dsync_transaction_log_scan * ctx,const void * data,const struct mail_transaction_header * hdr)65 log_add_expunge(struct dsync_transaction_log_scan *ctx, const void *data,
66 const struct mail_transaction_header *hdr)
67 {
68 const struct mail_transaction_expunge *rec = data, *end;
69 struct dsync_mail_change *change;
70 uint32_t uid;
71
72 if ((hdr->type & MAIL_TRANSACTION_EXTERNAL) == 0) {
73 /* this is simply a request for expunge */
74 return;
75 }
76 end = CONST_PTR_OFFSET(data, hdr->size);
77 for (; rec != end; rec++) {
78 for (uid = rec->uid1; uid <= rec->uid2; uid++) {
79 export_change_get(ctx, uid,
80 DSYNC_MAIL_CHANGE_TYPE_EXPUNGE,
81 &change);
82 }
83 }
84 }
85
86 static bool
log_add_expunge_uid(struct dsync_transaction_log_scan * ctx,const void * data,const struct mail_transaction_header * hdr,uint32_t uid)87 log_add_expunge_uid(struct dsync_transaction_log_scan *ctx, const void *data,
88 const struct mail_transaction_header *hdr, uint32_t uid)
89 {
90 const struct mail_transaction_expunge *rec = data, *end;
91 struct dsync_mail_change *change;
92
93 if ((hdr->type & MAIL_TRANSACTION_EXTERNAL) == 0) {
94 /* this is simply a request for expunge */
95 return FALSE;
96 }
97 end = CONST_PTR_OFFSET(data, hdr->size);
98 for (; rec != end; rec++) {
99 if (uid >= rec->uid1 && uid <= rec->uid2) {
100 export_change_get(ctx, uid,
101 DSYNC_MAIL_CHANGE_TYPE_EXPUNGE,
102 &change);
103 return TRUE;
104 }
105 }
106 return FALSE;
107 }
108
109 static void
log_add_expunge_guid(struct dsync_transaction_log_scan * ctx,struct mail_index_view * view,const void * data,const struct mail_transaction_header * hdr)110 log_add_expunge_guid(struct dsync_transaction_log_scan *ctx,
111 struct mail_index_view *view, const void *data,
112 const struct mail_transaction_header *hdr)
113 {
114 const struct mail_transaction_expunge_guid *rec = data, *end;
115 struct dsync_mail_change *change;
116 uint32_t seq;
117 bool external;
118
119 external = (hdr->type & MAIL_TRANSACTION_EXTERNAL) != 0;
120
121 end = CONST_PTR_OFFSET(data, hdr->size);
122 for (; rec != end; rec++) {
123 if (!external && mail_index_lookup_seq(view, rec->uid, &seq)) {
124 /* expunge request that hasn't been actually done yet.
125 we check non-external ones because they might have
126 the GUID while external ones don't. */
127 continue;
128 }
129 if (export_change_get(ctx, rec->uid,
130 DSYNC_MAIL_CHANGE_TYPE_EXPUNGE,
131 &change) &&
132 !guid_128_is_empty(rec->guid_128)) T_BEGIN {
133 change->guid = p_strdup(ctx->pool,
134 guid_128_to_string(rec->guid_128));
135 } T_END;
136 }
137 }
138
139 static bool
log_add_expunge_guid_uid(struct dsync_transaction_log_scan * ctx,const void * data,const struct mail_transaction_header * hdr,uint32_t uid)140 log_add_expunge_guid_uid(struct dsync_transaction_log_scan *ctx, const void *data,
141 const struct mail_transaction_header *hdr, uint32_t uid)
142 {
143 const struct mail_transaction_expunge_guid *rec = data, *end;
144 struct dsync_mail_change *change;
145
146 /* we're assuming UID is already known to be expunged */
147 end = CONST_PTR_OFFSET(data, hdr->size);
148 for (; rec != end; rec++) {
149 if (rec->uid != uid)
150 continue;
151
152 if (!export_change_get(ctx, rec->uid,
153 DSYNC_MAIL_CHANGE_TYPE_EXPUNGE,
154 &change))
155 i_unreached();
156 if (!guid_128_is_empty(rec->guid_128)) T_BEGIN {
157 change->guid = p_strdup(ctx->pool,
158 guid_128_to_string(rec->guid_128));
159 } T_END;
160 return TRUE;
161 }
162 return FALSE;
163 }
164
165 static void
log_add_flag_update(struct dsync_transaction_log_scan * ctx,const void * data,const struct mail_transaction_header * hdr)166 log_add_flag_update(struct dsync_transaction_log_scan *ctx, const void *data,
167 const struct mail_transaction_header *hdr)
168 {
169 const struct mail_transaction_flag_update *rec = data, *end;
170 struct dsync_mail_change *change;
171 uint32_t uid;
172
173 end = CONST_PTR_OFFSET(data, hdr->size);
174 for (; rec != end; rec++) {
175 for (uid = rec->uid1; uid <= rec->uid2; uid++) {
176 if (export_change_get(ctx, uid,
177 DSYNC_MAIL_CHANGE_TYPE_FLAG_CHANGE,
178 &change)) {
179 change->add_flags |= rec->add_flags;
180 change->remove_flags &= ENUM_NEGATE(rec->add_flags);
181 change->remove_flags |= rec->remove_flags;
182 change->add_flags &= ENUM_NEGATE(rec->remove_flags);
183 }
184 }
185 }
186 }
187
188 static void
log_add_keyword_reset(struct dsync_transaction_log_scan * ctx,const void * data,const struct mail_transaction_header * hdr)189 log_add_keyword_reset(struct dsync_transaction_log_scan *ctx, const void *data,
190 const struct mail_transaction_header *hdr)
191 {
192 const struct mail_transaction_keyword_reset *rec = data, *end;
193 struct dsync_mail_change *change;
194 uint32_t uid;
195
196 end = CONST_PTR_OFFSET(data, hdr->size);
197 for (; rec != end; rec++) {
198 for (uid = rec->uid1; uid <= rec->uid2; uid++) {
199 if (!export_change_get(ctx, uid,
200 DSYNC_MAIL_CHANGE_TYPE_FLAG_CHANGE,
201 &change))
202 continue;
203
204 change->keywords_reset = TRUE;
205 if (array_is_created(&change->keyword_changes))
206 array_clear(&change->keyword_changes);
207 }
208 }
209 }
210
211 static void
keywords_change_remove(struct dsync_mail_change * change,const char * name)212 keywords_change_remove(struct dsync_mail_change *change, const char *name)
213 {
214 const char *const *changes;
215 unsigned int i, count;
216
217 changes = array_get(&change->keyword_changes, &count);
218 for (i = 0; i < count; i++) {
219 if (strcmp(changes[i]+1, name) == 0) {
220 array_delete(&change->keyword_changes, i, 1);
221 break;
222 }
223 }
224 }
225
226 static void
log_add_keyword_update(struct dsync_transaction_log_scan * ctx,const void * data,const struct mail_transaction_header * hdr)227 log_add_keyword_update(struct dsync_transaction_log_scan *ctx, const void *data,
228 const struct mail_transaction_header *hdr)
229 {
230 const struct mail_transaction_keyword_update *rec = data;
231 struct dsync_mail_change *change;
232 const char *kw_name, *change_str;
233 const uint32_t *uids, *end;
234 unsigned int uids_offset;
235 uint32_t uid;
236
237 uids_offset = sizeof(*rec) + rec->name_size;
238 if ((uids_offset % 4) != 0)
239 uids_offset += 4 - (uids_offset % 4);
240
241 kw_name = t_strndup((const void *)(rec+1), rec->name_size);
242 switch (rec->modify_type) {
243 case MODIFY_ADD:
244 change_str = p_strdup_printf(ctx->pool, "%c%s",
245 KEYWORD_CHANGE_ADD, kw_name);
246 break;
247 case MODIFY_REMOVE:
248 change_str = p_strdup_printf(ctx->pool, "%c%s",
249 KEYWORD_CHANGE_REMOVE, kw_name);
250 break;
251 default:
252 i_unreached();
253 }
254
255 uids = CONST_PTR_OFFSET(rec, uids_offset);
256 end = CONST_PTR_OFFSET(rec, hdr->size);
257
258 for (; uids < end; uids += 2) {
259 for (uid = uids[0]; uid <= uids[1]; uid++) {
260 if (!export_change_get(ctx, uid,
261 DSYNC_MAIL_CHANGE_TYPE_FLAG_CHANGE,
262 &change))
263 continue;
264 if (!array_is_created(&change->keyword_changes)) {
265 p_array_init(&change->keyword_changes,
266 ctx->pool, 4);
267 } else {
268 keywords_change_remove(change, kw_name);
269 }
270 array_push_back(&change->keyword_changes, &change_str);
271 }
272 }
273 }
274
275 static void
log_add_modseq_update(struct dsync_transaction_log_scan * ctx,const void * data,const struct mail_transaction_header * hdr,bool pvt_scan)276 log_add_modseq_update(struct dsync_transaction_log_scan *ctx, const void *data,
277 const struct mail_transaction_header *hdr, bool pvt_scan)
278 {
279 const struct mail_transaction_modseq_update *rec = data, *end;
280 struct dsync_mail_change *change;
281 uint64_t modseq;
282
283 /* update message's modseq, possibly by creating an empty flag change */
284 end = CONST_PTR_OFFSET(rec, hdr->size);
285 for (; rec != end; rec++) {
286 if (rec->uid == 0) {
287 /* highestmodseq update */
288 continue;
289 }
290
291 if (!export_change_get(ctx, rec->uid,
292 DSYNC_MAIL_CHANGE_TYPE_FLAG_CHANGE,
293 &change))
294 continue;
295
296 modseq = rec->modseq_low32 |
297 ((uint64_t)rec->modseq_high32 << 32);
298 if (!pvt_scan) {
299 if (change->modseq < modseq)
300 change->modseq = modseq;
301 } else {
302 if (change->pvt_modseq < modseq)
303 change->pvt_modseq = modseq;
304 }
305 }
306 }
307
308 static void
log_add_attribute_update_key(struct dsync_transaction_log_scan * ctx,const char * attr_change,uint64_t modseq)309 log_add_attribute_update_key(struct dsync_transaction_log_scan *ctx,
310 const char *attr_change, uint64_t modseq)
311 {
312 struct dsync_mailbox_attribute lookup_attr, *attr;
313
314 i_assert(strlen(attr_change) > 2); /* checked by lib-index */
315
316 lookup_attr.type = attr_change[1] == 'p' ?
317 MAIL_ATTRIBUTE_TYPE_PRIVATE : MAIL_ATTRIBUTE_TYPE_SHARED;
318 lookup_attr.key = attr_change+2;
319
320 attr = hash_table_lookup(ctx->attr_changes, &lookup_attr);
321 if (attr == NULL) {
322 attr = p_new(ctx->pool, struct dsync_mailbox_attribute, 1);
323 attr->type = lookup_attr.type;
324 attr->key = p_strdup(ctx->pool, lookup_attr.key);
325 hash_table_insert(ctx->attr_changes, attr, attr);
326 }
327 attr->deleted = attr_change[0] == '-';
328 attr->modseq = modseq;
329 }
330
331 static void
log_add_attribute_update(struct dsync_transaction_log_scan * ctx,const void * data,const struct mail_transaction_header * hdr,uint64_t modseq)332 log_add_attribute_update(struct dsync_transaction_log_scan *ctx,
333 const void *data,
334 const struct mail_transaction_header *hdr,
335 uint64_t modseq)
336 {
337 const char *attr_changes = data;
338 unsigned int i;
339
340 for (i = 0; i < hdr->size && attr_changes[i] != '\0'; ) {
341 log_add_attribute_update_key(ctx, attr_changes+i, modseq);
342 i += strlen(attr_changes+i) + 1;
343 }
344 }
345
346 static int
dsync_log_set(struct dsync_transaction_log_scan * ctx,struct mail_index_view * view,bool pvt_scan,struct mail_transaction_log_view * log_view,uint64_t modseq)347 dsync_log_set(struct dsync_transaction_log_scan *ctx,
348 struct mail_index_view *view, bool pvt_scan,
349 struct mail_transaction_log_view *log_view, uint64_t modseq)
350 {
351 uint32_t log_seq, end_seq;
352 uoff_t log_offset, end_offset;
353 const char *reason;
354 bool reset;
355 int ret;
356
357 end_seq = view->log_file_head_seq;
358 end_offset = view->log_file_head_offset;
359
360 if (modseq != 0 &&
361 mail_index_modseq_get_next_log_offset(view, modseq,
362 &log_seq, &log_offset)) {
363 /* scan the view only up to end of the current view.
364 if there are more changes, we don't care about them until
365 the next sync. */
366 ret = mail_transaction_log_view_set(log_view,
367 log_seq, log_offset,
368 end_seq, end_offset,
369 &reset, &reason);
370 if (ret != 0)
371 return ret;
372 }
373
374 /* return everything we've got (until the end of the view) */
375 if (!pvt_scan)
376 ctx->returned_all_changes = TRUE;
377 if (mail_transaction_log_view_set_all(log_view) < 0)
378 return -1;
379
380 mail_transaction_log_view_get_prev_pos(log_view, &log_seq, &log_offset);
381 if (log_seq > end_seq ||
382 (log_seq == end_seq && log_offset > end_offset)) {
383 end_seq = log_seq;
384 end_offset = log_offset;
385 }
386 ret = mail_transaction_log_view_set(log_view,
387 log_seq, log_offset,
388 end_seq, end_offset,
389 &reset, &reason);
390 if (ret == 0) {
391 /* we shouldn't get here. _view_set_all() already
392 reserved all the log files, the _view_set() only
393 removed unwanted ones. */
394 i_error("%s: Couldn't set transaction log view (seq %u..%u): %s",
395 view->index->filepath, log_seq, end_seq, reason);
396 ret = -1;
397 }
398 if (ret < 0)
399 return -1;
400 if (modseq != 0) {
401 /* we didn't see all the changes that we wanted to */
402 return 0;
403 }
404 return 1;
405 }
406
407 static int
dsync_log_scan(struct dsync_transaction_log_scan * ctx,struct mail_index_view * view,uint64_t modseq,bool pvt_scan)408 dsync_log_scan(struct dsync_transaction_log_scan *ctx,
409 struct mail_index_view *view, uint64_t modseq, bool pvt_scan)
410 {
411 struct mail_transaction_log_view *log_view;
412 const struct mail_transaction_header *hdr;
413 const void *data;
414 uint32_t file_seq, max_seq;
415 uoff_t file_offset, max_offset;
416 uint64_t cur_modseq;
417 int ret;
418
419 log_view = mail_transaction_log_view_open(view->index->log);
420 if ((ret = dsync_log_set(ctx, view, pvt_scan, log_view, modseq)) < 0) {
421 mail_transaction_log_view_close(&log_view);
422 return -1;
423 }
424
425 /* read the log only up to current position in view */
426 max_seq = view->log_file_expunge_seq;
427 max_offset = view->log_file_expunge_offset;
428
429 mail_transaction_log_view_get_prev_pos(log_view, &file_seq,
430 &file_offset);
431
432 while (mail_transaction_log_view_next(log_view, &hdr, &data) > 0) {
433 mail_transaction_log_view_get_prev_pos(log_view, &file_seq,
434 &file_offset);
435 if (file_offset >= max_offset && file_seq == max_seq)
436 break;
437
438 if ((hdr->type & MAIL_TRANSACTION_SYNC) != 0) {
439 /* ignore changes done by dsync, unless we can get
440 expunged message's GUID from it */
441 if ((hdr->type & MAIL_TRANSACTION_TYPE_MASK) !=
442 MAIL_TRANSACTION_EXPUNGE_GUID)
443 continue;
444 }
445
446 switch (hdr->type & MAIL_TRANSACTION_TYPE_MASK) {
447 case MAIL_TRANSACTION_EXPUNGE:
448 if (!pvt_scan)
449 log_add_expunge(ctx, data, hdr);
450 break;
451 case MAIL_TRANSACTION_EXPUNGE_GUID:
452 if (!pvt_scan)
453 log_add_expunge_guid(ctx, view, data, hdr);
454 break;
455 case MAIL_TRANSACTION_FLAG_UPDATE:
456 log_add_flag_update(ctx, data, hdr);
457 break;
458 case MAIL_TRANSACTION_KEYWORD_RESET:
459 log_add_keyword_reset(ctx, data, hdr);
460 break;
461 case MAIL_TRANSACTION_KEYWORD_UPDATE:
462 T_BEGIN {
463 log_add_keyword_update(ctx, data, hdr);
464 } T_END;
465 break;
466 case MAIL_TRANSACTION_MODSEQ_UPDATE:
467 log_add_modseq_update(ctx, data, hdr, pvt_scan);
468 break;
469 case MAIL_TRANSACTION_ATTRIBUTE_UPDATE:
470 cur_modseq = mail_transaction_log_view_get_prev_modseq(log_view);
471 log_add_attribute_update(ctx, data, hdr, cur_modseq);
472 break;
473 }
474 }
475
476 if (!pvt_scan) {
477 ctx->last_log_seq = file_seq;
478 ctx->last_log_offset = file_offset;
479 }
480 mail_transaction_log_view_close(&log_view);
481 return ret;
482 }
483
484 static int
dsync_mailbox_attribute_cmp(const struct dsync_mailbox_attribute * attr1,const struct dsync_mailbox_attribute * attr2)485 dsync_mailbox_attribute_cmp(const struct dsync_mailbox_attribute *attr1,
486 const struct dsync_mailbox_attribute *attr2)
487 {
488 if (attr1->type < attr2->type)
489 return -1;
490 if (attr1->type > attr2->type)
491 return 1;
492 return strcmp(attr1->key, attr2->key);
493 }
494
495 static unsigned int
dsync_mailbox_attribute_hash(const struct dsync_mailbox_attribute * attr)496 dsync_mailbox_attribute_hash(const struct dsync_mailbox_attribute *attr)
497 {
498 return str_hash(attr->key) ^ attr->type;
499 }
500
dsync_transaction_log_scan_init(struct mail_index_view * view,struct mail_index_view * pvt_view,uint32_t highest_wanted_uid,uint64_t modseq,uint64_t pvt_modseq,struct dsync_transaction_log_scan ** scan_r,bool * pvt_too_old_r)501 int dsync_transaction_log_scan_init(struct mail_index_view *view,
502 struct mail_index_view *pvt_view,
503 uint32_t highest_wanted_uid,
504 uint64_t modseq, uint64_t pvt_modseq,
505 struct dsync_transaction_log_scan **scan_r,
506 bool *pvt_too_old_r)
507 {
508 struct dsync_transaction_log_scan *ctx;
509 pool_t pool;
510 int ret, ret2;
511
512 *pvt_too_old_r = FALSE;
513
514 pool = pool_alloconly_create(MEMPOOL_GROWING"dsync transaction log scan",
515 10240);
516 ctx = p_new(pool, struct dsync_transaction_log_scan, 1);
517 ctx->pool = pool;
518 hash_table_create_direct(&ctx->changes, pool, 0);
519 hash_table_create(&ctx->attr_changes, pool, 0,
520 dsync_mailbox_attribute_hash,
521 dsync_mailbox_attribute_cmp);
522 ctx->view = view;
523 ctx->highest_wanted_uid = highest_wanted_uid;
524
525 if ((ret = dsync_log_scan(ctx, view, modseq, FALSE)) < 0)
526 return -1;
527 if (pvt_view != NULL) {
528 if ((ret2 = dsync_log_scan(ctx, pvt_view, pvt_modseq, TRUE)) < 0)
529 return -1;
530 if (ret2 == 0) {
531 ret = 0;
532 *pvt_too_old_r = TRUE;
533 }
534 }
535
536 *scan_r = ctx;
537 return ret;
538 }
539
540 HASH_TABLE_TYPE(dsync_uid_mail_change)
dsync_transaction_log_scan_get_hash(struct dsync_transaction_log_scan * scan)541 dsync_transaction_log_scan_get_hash(struct dsync_transaction_log_scan *scan)
542 {
543 return scan->changes;
544 }
545
546 HASH_TABLE_TYPE(dsync_attr_change)
dsync_transaction_log_scan_get_attr_hash(struct dsync_transaction_log_scan * scan)547 dsync_transaction_log_scan_get_attr_hash(struct dsync_transaction_log_scan *scan)
548 {
549 return scan->attr_changes;
550 }
551
552 bool
dsync_transaction_log_scan_has_all_changes(struct dsync_transaction_log_scan * scan)553 dsync_transaction_log_scan_has_all_changes(struct dsync_transaction_log_scan *scan)
554 {
555 return scan->returned_all_changes;
556 }
557
558 struct dsync_mail_change *
dsync_transaction_log_scan_find_new_expunge(struct dsync_transaction_log_scan * scan,uint32_t uid)559 dsync_transaction_log_scan_find_new_expunge(struct dsync_transaction_log_scan *scan,
560 uint32_t uid)
561 {
562 struct mail_transaction_log_view *log_view;
563 const struct mail_transaction_header *hdr;
564 const void *data;
565 const char *reason;
566 bool reset, found = FALSE;
567
568 i_assert(uid > 0);
569
570 if (scan->highest_wanted_uid < uid)
571 scan->highest_wanted_uid = uid;
572
573 log_view = mail_transaction_log_view_open(scan->view->index->log);
574 if (mail_transaction_log_view_set(log_view,
575 scan->last_log_seq,
576 scan->last_log_offset,
577 (uint32_t)-1, UOFF_T_MAX,
578 &reset, &reason) > 0) {
579 while (!found &&
580 mail_transaction_log_view_next(log_view, &hdr, &data) > 0) {
581 switch (hdr->type & MAIL_TRANSACTION_TYPE_MASK) {
582 case MAIL_TRANSACTION_EXPUNGE:
583 if (log_add_expunge_uid(scan, data, hdr, uid))
584 found = TRUE;
585 break;
586 case MAIL_TRANSACTION_EXPUNGE_GUID:
587 if (log_add_expunge_guid_uid(scan, data, hdr, uid))
588 found = TRUE;
589 break;
590 }
591 }
592 }
593 mail_transaction_log_view_close(&log_view);
594
595 return !found ? NULL :
596 hash_table_lookup(scan->changes, POINTER_CAST(uid));
597 }
598
dsync_transaction_log_scan_deinit(struct dsync_transaction_log_scan ** _scan)599 void dsync_transaction_log_scan_deinit(struct dsync_transaction_log_scan **_scan)
600 {
601 struct dsync_transaction_log_scan *scan = *_scan;
602
603 *_scan = NULL;
604
605 hash_table_destroy(&scan->changes);
606 hash_table_destroy(&scan->attr_changes);
607 pool_unref(&scan->pool);
608 }
609