1 /*	$OpenBSD: rrdp_notification.c,v 1.21 2024/04/12 11:50:29 job Exp $ */
2 /*
3  * Copyright (c) 2020 Nils Fisher <nils_fisher@hotmail.com>
4  * Copyright (c) 2021 Claudio Jeker <claudio@openbsd.org>
5  *
6  * Permission to use, copy, modify, and distribute this software for any
7  * purpose with or without fee is hereby granted, provided that the above
8  * copyright notice and this permission notice appear in all copies.
9  *
10  * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
11  * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
12  * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
13  * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
14  * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
15  * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
16  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
17  */
18 
19 #include <sys/stat.h>
20 
21 #include <assert.h>
22 #include <err.h>
23 #include <errno.h>
24 #include <limits.h>
25 #include <fcntl.h>
26 #include <string.h>
27 #include <unistd.h>
28 
29 #include <expat.h>
30 #include <openssl/sha.h>
31 
32 #include "extern.h"
33 #include "rrdp.h"
34 
35 enum notification_scope {
36 	NOTIFICATION_SCOPE_START,
37 	NOTIFICATION_SCOPE_NOTIFICATION,
38 	NOTIFICATION_SCOPE_SNAPSHOT,
39 	NOTIFICATION_SCOPE_NOTIFICATION_POST_SNAPSHOT,
40 	NOTIFICATION_SCOPE_DELTA,
41 	NOTIFICATION_SCOPE_END
42 };
43 
44 struct delta_item {
45 	char			*uri;
46 	char			 hash[SHA256_DIGEST_LENGTH];
47 	long long		 serial;
48 	TAILQ_ENTRY(delta_item)	 q;
49 };
50 
51 TAILQ_HEAD(delta_q, delta_item);
52 
53 struct notification_xml {
54 	XML_Parser		 parser;
55 	struct rrdp_session	*repository;
56 	struct rrdp_session	*current;
57 	const char		*notifyuri;
58 	char			*session_id;
59 	char			*snapshot_uri;
60 	char			 snapshot_hash[SHA256_DIGEST_LENGTH];
61 	struct delta_q		 delta_q;
62 	long long		 serial;
63 	long long		 min_serial;
64 	int			 version;
65 	enum notification_scope	 scope;
66 };
67 
68 static void	free_delta(struct delta_item *);
69 
70 static int
add_delta(struct notification_xml * nxml,const char * uri,const char hash[SHA256_DIGEST_LENGTH],long long serial)71 add_delta(struct notification_xml *nxml, const char *uri,
72     const char hash[SHA256_DIGEST_LENGTH], long long serial)
73 {
74 	struct delta_item *d, *n;
75 
76 	if ((d = calloc(1, sizeof(struct delta_item))) == NULL)
77 		err(1, "%s - calloc", __func__);
78 
79 	d->serial = serial;
80 	d->uri = xstrdup(uri);
81 	memcpy(d->hash, hash, sizeof(d->hash));
82 
83 	/* optimise for a sorted input */
84 	n = TAILQ_LAST(&nxml->delta_q, delta_q);
85 	if (n == NULL)
86 		TAILQ_INSERT_HEAD(&nxml->delta_q, d, q);
87 	else if (n->serial < serial)
88 		TAILQ_INSERT_TAIL(&nxml->delta_q, d, q);
89 	else
90 		TAILQ_FOREACH(n, &nxml->delta_q, q) {
91 			if (n->serial == serial) {
92 				warnx("duplicate delta serial %lld ", serial);
93 				free_delta(d);
94 				return 0;
95 			}
96 			if (n->serial > serial) {
97 				TAILQ_INSERT_BEFORE(n, d, q);
98 				break;
99 			}
100 		}
101 
102 	return 1;
103 }
104 
105 /* check that there are no holes in the list */
106 static int
check_delta(struct notification_xml * nxml)107 check_delta(struct notification_xml *nxml)
108 {
109 	struct delta_item *d;
110 	long long serial = 0;
111 
112 	TAILQ_FOREACH(d, &nxml->delta_q, q) {
113 		if (serial != 0 && serial + 1 != d->serial)
114 			return 0;
115 		serial = d->serial;
116 	}
117 	return 1;
118 }
119 
120 static void
free_delta(struct delta_item * d)121 free_delta(struct delta_item *d)
122 {
123 	free(d->uri);
124 	free(d);
125 }
126 
127 /*
128  * Parse a delta serial and hash line at idx from the rrdp session state.
129  * Return the serial or 0 on error. If hash is non-NULL, it is set to the
130  * start of the hash string on success.
131  */
132 static long long
delta_parse(struct rrdp_session * s,size_t idx,char ** hash)133 delta_parse(struct rrdp_session *s, size_t idx, char **hash)
134 {
135 	long long serial;
136 	char *line, *ep;
137 
138 	if (hash != NULL)
139 		*hash = NULL;
140 	if (idx < 0 || idx >= sizeof(s->deltas) / sizeof(s->deltas[0]))
141 		return 0;
142 	if ((line = s->deltas[idx]) == NULL)
143 		return 0;
144 
145 	errno = 0;
146 	serial = strtoll(line, &ep, 10);
147 	if (line[0] == '\0' || *ep != ' ')
148 		return 0;
149 	if (serial <= 0 || (errno == ERANGE && serial == LLONG_MAX))
150 		return 0;
151 
152 	if (hash != NULL)
153 		*hash = ep + 1;
154 	return serial;
155 }
156 
157 static void
start_notification_elem(struct notification_xml * nxml,const char ** attr)158 start_notification_elem(struct notification_xml *nxml, const char **attr)
159 {
160 	XML_Parser p = nxml->parser;
161 	int has_xmlns = 0;
162 	size_t i;
163 
164 	if (nxml->scope != NOTIFICATION_SCOPE_START)
165 		PARSE_FAIL(p,
166 		    "parse failed - entered notification elem unexpectedely");
167 	for (i = 0; attr[i]; i += 2) {
168 		const char *errstr;
169 		if (strcmp("xmlns", attr[i]) == 0 &&
170 		    strcmp(RRDP_XMLNS, attr[i + 1]) == 0) {
171 			has_xmlns = 1;
172 			continue;
173 		}
174 		if (strcmp("session_id", attr[i]) == 0 &&
175 		    valid_uuid(attr[i + 1])) {
176 			nxml->session_id = xstrdup(attr[i + 1]);
177 			continue;
178 		}
179 		if (strcmp("version", attr[i]) == 0) {
180 			nxml->version = strtonum(attr[i + 1],
181 			    1, MAX_VERSION, &errstr);
182 			if (errstr == NULL)
183 				continue;
184 		}
185 		if (strcmp("serial", attr[i]) == 0) {
186 			nxml->serial = strtonum(attr[i + 1],
187 			    1, LLONG_MAX, &errstr);
188 			if (errstr == NULL)
189 				continue;
190 		}
191 		PARSE_FAIL(p, "parse failed - non conforming "
192 		    "attribute '%s' found in notification elem", attr[i]);
193 	}
194 	if (!(has_xmlns && nxml->version && nxml->session_id && nxml->serial))
195 		PARSE_FAIL(p, "parse failed - incomplete "
196 		    "notification attributes");
197 
198 	/* Limit deltas to the ones which matter for us. */
199 	if (nxml->min_serial == 0 && nxml->serial > MAX_RRDP_DELTAS)
200 		nxml->min_serial = nxml->serial - MAX_RRDP_DELTAS;
201 
202 	nxml->scope = NOTIFICATION_SCOPE_NOTIFICATION;
203 }
204 
205 static void
end_notification_elem(struct notification_xml * nxml)206 end_notification_elem(struct notification_xml *nxml)
207 {
208 	XML_Parser p = nxml->parser;
209 
210 	if (nxml->scope != NOTIFICATION_SCOPE_NOTIFICATION_POST_SNAPSHOT)
211 		PARSE_FAIL(p, "parse failed - exited notification "
212 		    "elem unexpectedely");
213 	nxml->scope = NOTIFICATION_SCOPE_END;
214 
215 	if (!check_delta(nxml))
216 		PARSE_FAIL(p, "parse failed - delta list has holes");
217 }
218 
219 static void
start_snapshot_elem(struct notification_xml * nxml,const char ** attr)220 start_snapshot_elem(struct notification_xml *nxml, const char **attr)
221 {
222 	XML_Parser p = nxml->parser;
223 	int i, hasUri = 0, hasHash = 0;
224 
225 	if (nxml->scope != NOTIFICATION_SCOPE_NOTIFICATION)
226 		PARSE_FAIL(p,
227 		    "parse failed - entered snapshot elem unexpectedely");
228 	for (i = 0; attr[i]; i += 2) {
229 		if (strcmp("uri", attr[i]) == 0 && hasUri++ == 0) {
230 			if (valid_uri(attr[i + 1], strlen(attr[i + 1]),
231 			    HTTPS_PROTO) &&
232 			    valid_origin(attr[i + 1], nxml->notifyuri)) {
233 				nxml->snapshot_uri = xstrdup(attr[i + 1]);
234 				continue;
235 			}
236 		}
237 		if (strcmp("hash", attr[i]) == 0 && hasHash++ == 0) {
238 			if (hex_decode(attr[i + 1], nxml->snapshot_hash,
239 			    sizeof(nxml->snapshot_hash)) == 0)
240 				continue;
241 		}
242 		PARSE_FAIL(p, "parse failed - non conforming "
243 		    "attribute '%s' found in snapshot elem", attr[i]);
244 	}
245 	if (hasUri != 1 || hasHash != 1)
246 		PARSE_FAIL(p, "parse failed - incomplete snapshot attributes");
247 
248 	nxml->scope = NOTIFICATION_SCOPE_SNAPSHOT;
249 }
250 
251 static void
end_snapshot_elem(struct notification_xml * nxml)252 end_snapshot_elem(struct notification_xml *nxml)
253 {
254 	XML_Parser p = nxml->parser;
255 
256 	if (nxml->scope != NOTIFICATION_SCOPE_SNAPSHOT)
257 		PARSE_FAIL(p, "parse failed - exited snapshot "
258 		    "elem unexpectedely");
259 	nxml->scope = NOTIFICATION_SCOPE_NOTIFICATION_POST_SNAPSHOT;
260 }
261 
262 static void
start_delta_elem(struct notification_xml * nxml,const char ** attr)263 start_delta_elem(struct notification_xml *nxml, const char **attr)
264 {
265 	XML_Parser p = nxml->parser;
266 	int i, hasUri = 0, hasHash = 0;
267 	const char *delta_uri = NULL;
268 	char delta_hash[SHA256_DIGEST_LENGTH];
269 	long long delta_serial = 0;
270 
271 	if (nxml->scope != NOTIFICATION_SCOPE_NOTIFICATION_POST_SNAPSHOT)
272 		PARSE_FAIL(p, "parse failed - entered delta "
273 		    "elem unexpectedely");
274 	for (i = 0; attr[i]; i += 2) {
275 		if (strcmp("uri", attr[i]) == 0 && hasUri++ == 0) {
276 			if (valid_uri(attr[i + 1], strlen(attr[i + 1]),
277 			    HTTPS_PROTO) &&
278 			    valid_origin(attr[i + 1], nxml->notifyuri)) {
279 				delta_uri = attr[i + 1];
280 				continue;
281 			}
282 		}
283 		if (strcmp("hash", attr[i]) == 0 && hasHash++ == 0) {
284 			if (hex_decode(attr[i + 1], delta_hash,
285 			    sizeof(delta_hash)) == 0)
286 				continue;
287 		}
288 		if (strcmp("serial", attr[i]) == 0 && delta_serial == 0) {
289 			const char *errstr;
290 
291 			delta_serial = strtonum(attr[i + 1],
292 			    1, LLONG_MAX, &errstr);
293 			if (errstr == NULL)
294 				continue;
295 		}
296 		PARSE_FAIL(p, "parse failed - non conforming "
297 		    "attribute '%s' found in delta elem", attr[i]);
298 	}
299 	/* Only add to the list if we are relevant */
300 	if (hasUri != 1 || hasHash != 1 || delta_serial == 0)
301 		PARSE_FAIL(p, "parse failed - incomplete delta attributes");
302 
303 	/* Delta serial must be smaller or equal to the notification serial */
304 	if (nxml->serial < delta_serial)
305 		PARSE_FAIL(p, "parse failed - bad delta serial");
306 
307 	/* optimisation, add only deltas that could be interesting */
308 	if (nxml->min_serial < delta_serial) {
309 		if (add_delta(nxml, delta_uri, delta_hash, delta_serial) == 0)
310 			PARSE_FAIL(p, "parse failed - adding delta failed");
311 	}
312 
313 	nxml->scope = NOTIFICATION_SCOPE_DELTA;
314 }
315 
316 static void
end_delta_elem(struct notification_xml * nxml)317 end_delta_elem(struct notification_xml *nxml)
318 {
319 	XML_Parser p = nxml->parser;
320 
321 	if (nxml->scope != NOTIFICATION_SCOPE_DELTA)
322 		PARSE_FAIL(p, "parse failed - exited delta elem unexpectedely");
323 	nxml->scope = NOTIFICATION_SCOPE_NOTIFICATION_POST_SNAPSHOT;
324 }
325 
326 static void
notification_xml_elem_start(void * data,const char * el,const char ** attr)327 notification_xml_elem_start(void *data, const char *el, const char **attr)
328 {
329 	struct notification_xml *nxml = data;
330 	XML_Parser p = nxml->parser;
331 
332 	/*
333 	 * Can only enter here once as we should have no ways to get back to
334 	 * START scope
335 	 */
336 	if (strcmp("notification", el) == 0)
337 		start_notification_elem(nxml, attr);
338 	/*
339 	 * Will enter here multiple times, BUT never nested. will start
340 	 * collecting character data in that handler
341 	 * mem is cleared in end block, (TODO or on parse failure)
342 	 */
343 	else if (strcmp("snapshot", el) == 0)
344 		start_snapshot_elem(nxml, attr);
345 	else if (strcmp("delta", el) == 0)
346 		start_delta_elem(nxml, attr);
347 	else
348 		PARSE_FAIL(p, "parse failed - unexpected elem exit found");
349 }
350 
351 static void
notification_xml_elem_end(void * data,const char * el)352 notification_xml_elem_end(void *data, const char *el)
353 {
354 	struct notification_xml *nxml = data;
355 	XML_Parser p = nxml->parser;
356 
357 	if (strcmp("notification", el) == 0)
358 		end_notification_elem(nxml);
359 	else if (strcmp("snapshot", el) == 0)
360 		end_snapshot_elem(nxml);
361 	else if (strcmp("delta", el) == 0)
362 		end_delta_elem(nxml);
363 	else
364 		PARSE_FAIL(p, "parse failed - unexpected elem exit found");
365 }
366 
367 static void
notification_doctype_handler(void * data,const char * doctypeName,const char * sysid,const char * pubid,int subset)368 notification_doctype_handler(void *data, const char *doctypeName,
369     const char *sysid, const char *pubid, int subset)
370 {
371 	struct notification_xml *nxml = data;
372 	XML_Parser p = nxml->parser;
373 
374 	PARSE_FAIL(p, "parse failed - DOCTYPE not allowed");
375 }
376 
377 struct notification_xml *
new_notification_xml(XML_Parser p,struct rrdp_session * repository,struct rrdp_session * current,const char * notifyuri)378 new_notification_xml(XML_Parser p, struct rrdp_session *repository,
379     struct rrdp_session *current, const char *notifyuri)
380 {
381 	struct notification_xml *nxml;
382 
383 	if ((nxml = calloc(1, sizeof(*nxml))) == NULL)
384 		err(1, "%s", __func__);
385 	TAILQ_INIT(&(nxml->delta_q));
386 	nxml->parser = p;
387 	nxml->repository = repository;
388 	nxml->current = current;
389 	nxml->notifyuri = notifyuri;
390 	nxml->min_serial = delta_parse(repository, 0, NULL);
391 
392 	XML_SetElementHandler(nxml->parser, notification_xml_elem_start,
393 	    notification_xml_elem_end);
394 	XML_SetUserData(nxml->parser, nxml);
395 	XML_SetDoctypeDeclHandler(nxml->parser, notification_doctype_handler,
396 	    NULL);
397 
398 	return nxml;
399 }
400 
401 static void
free_delta_queue(struct notification_xml * nxml)402 free_delta_queue(struct notification_xml *nxml)
403 {
404 	while (!TAILQ_EMPTY(&nxml->delta_q)) {
405 		struct delta_item *d = TAILQ_FIRST(&nxml->delta_q);
406 		TAILQ_REMOVE(&nxml->delta_q, d, q);
407 		free_delta(d);
408 	}
409 }
410 
411 void
free_notification_xml(struct notification_xml * nxml)412 free_notification_xml(struct notification_xml *nxml)
413 {
414 	if (nxml == NULL)
415 		return;
416 
417 	free(nxml->session_id);
418 	free(nxml->snapshot_uri);
419 	free_delta_queue(nxml);
420 	free(nxml);
421 }
422 
423 /*
424  * Collect a list of deltas to store in the repository state.
425  */
426 static void
notification_collect_deltas(struct notification_xml * nxml)427 notification_collect_deltas(struct notification_xml *nxml)
428 {
429 	struct delta_item *d;
430 	long long keep_serial = 0;
431 	size_t cur_idx = 0, max_deltas;
432 	char *hash;
433 
434 	max_deltas =
435 	    sizeof(nxml->current->deltas) / sizeof(nxml->current->deltas[0]);
436 
437 	if (nxml->serial > (long long)max_deltas)
438 		keep_serial = nxml->serial - max_deltas + 1;
439 
440 	TAILQ_FOREACH(d, &nxml->delta_q, q) {
441 		if (d->serial >= keep_serial) {
442 			assert(cur_idx < max_deltas);
443 			hash = hex_encode(d->hash, sizeof(d->hash));
444 			if (asprintf(&nxml->current->deltas[cur_idx++],
445 			    "%lld %s", d->serial, hash) == -1)
446 				err(1, NULL);
447 			free(hash);
448 		}
449 	}
450 }
451 
452 /*
453  * Validate the delta list with the information from the repository state.
454  * Remove all obsolete deltas so that the list starts with the delta with
455  * serial nxml->repository->serial + 1.
456  * Returns 1 if all deltas were valid and 0 on failure.
457  */
458 static int
notification_check_deltas(struct notification_xml * nxml)459 notification_check_deltas(struct notification_xml *nxml)
460 {
461 	struct delta_item *d, *nextd;
462 	char *hash, *exp_hash;
463 	long long exp_serial, new_serial;
464 	size_t exp_idx = 0;
465 
466 	exp_serial = delta_parse(nxml->repository, exp_idx++, &exp_hash);
467 	new_serial = nxml->repository->serial + 1;
468 
469 	/* compare hash of delta against repository state info */
470 	TAILQ_FOREACH_SAFE(d, &nxml->delta_q, q, nextd) {
471 		while (exp_serial != 0  && exp_serial < d->serial) {
472 			exp_serial = delta_parse(nxml->repository,
473 			    exp_idx++, &exp_hash);
474 		}
475 
476 		if (d->serial == exp_serial) {
477 			hash = hex_encode(d->hash, sizeof(d->hash));
478 			if (strcmp(hash, exp_hash) != 0) {
479 				warnx("%s: %s#%lld unexpected delta "
480 				    "mutation (expected %s, got %s)",
481 				    nxml->notifyuri, nxml->session_id,
482 				    exp_serial, hash, exp_hash);
483 				free(hash);
484 				return 0;
485 			}
486 			free(hash);
487 			exp_serial = delta_parse(nxml->repository,
488 			    exp_idx++, &exp_hash);
489 		}
490 
491 		/* is this delta needed? */
492 		if (d->serial < new_serial) {
493 			TAILQ_REMOVE(&nxml->delta_q, d, q);
494 			free_delta(d);
495 		}
496 	}
497 
498 	return 1;
499 }
500 
501 /*
502  * Finalize notification step, decide if a delta update is possible
503  * if either the session_id changed or the delta files fail to cover
504  * all the steps up to the new serial fall back to a snapshot.
505  * Return SNAPSHOT or DELTA for snapshot or delta processing.
506  * Return NOTIFICATION if repository is up to date.
507  */
508 enum rrdp_task
notification_done(struct notification_xml * nxml,char * last_mod)509 notification_done(struct notification_xml *nxml, char *last_mod)
510 {
511 	nxml->current->last_mod = last_mod;
512 	nxml->current->session_id = xstrdup(nxml->session_id);
513 	notification_collect_deltas(nxml);
514 
515 	/* check the that the session_id was valid and still the same */
516 	if (nxml->repository->session_id == NULL ||
517 	    strcmp(nxml->session_id, nxml->repository->session_id) != 0)
518 		goto snapshot;
519 
520 	/* if repository serial is 0 fall back to snapshot */
521 	if (nxml->repository->serial == 0)
522 		goto snapshot;
523 
524 	/* check that all needed deltas are available and valid */
525 	if (!notification_check_deltas(nxml))
526 		goto snapshot;
527 
528 	if (nxml->repository->serial > nxml->serial)
529 		warnx("%s: serial number decreased from %lld to %lld",
530 		    nxml->notifyuri, nxml->repository->serial, nxml->serial);
531 
532 	/* if our serial is equal or plus 2, the repo is up to date */
533 	if (nxml->repository->serial >= nxml->serial &&
534 	    nxml->repository->serial - nxml->serial <= 2) {
535 		nxml->current->serial = nxml->repository->serial;
536 		return NOTIFICATION;
537 	}
538 
539 	/* it makes no sense to process too many deltas */
540 	if (nxml->serial - nxml->repository->serial > MAX_RRDP_DELTAS)
541 		goto snapshot;
542 
543 	/* no deltas queued */
544 	if (TAILQ_EMPTY(&nxml->delta_q))
545 		goto snapshot;
546 
547 	/* first possible delta is no match */
548 	if (nxml->repository->serial + 1 != TAILQ_FIRST(&nxml->delta_q)->serial)
549 		goto snapshot;
550 
551 	/* update via delta possible */
552 	nxml->current->serial = nxml->repository->serial;
553 	nxml->repository->serial = nxml->serial;
554 	return DELTA;
555 
556 snapshot:
557 	/* update via snapshot download */
558 	free_delta_queue(nxml);
559 	nxml->current->serial = nxml->serial;
560 	return SNAPSHOT;
561 }
562 
563 const char *
notification_get_next(struct notification_xml * nxml,char * hash,size_t hlen,enum rrdp_task task)564 notification_get_next(struct notification_xml *nxml, char *hash, size_t hlen,
565     enum rrdp_task task)
566 {
567 	struct delta_item *d;
568 
569 	switch (task) {
570 	case SNAPSHOT:
571 		assert(hlen == sizeof(nxml->snapshot_hash));
572 		memcpy(hash, nxml->snapshot_hash, hlen);
573 		/*
574 		 * Ensure that the serial is correct in case a previous
575 		 * delta request failed.
576 		 */
577 		nxml->current->serial = nxml->serial;
578 		return nxml->snapshot_uri;
579 	case DELTA:
580 		/* first bump serial, then use first delta */
581 		nxml->current->serial += 1;
582 		d = TAILQ_FIRST(&nxml->delta_q);
583 		assert(d->serial == nxml->current->serial);
584 		assert(hlen == sizeof(d->hash));
585 		memcpy(hash, d->hash, hlen);
586 		return d->uri;
587 	default:
588 		errx(1, "%s: bad task", __func__);
589 	}
590 }
591 
592 /*
593  * Pop first element from the delta queue. Return non-0 if this was the last
594  * delta to fetch.
595  */
596 int
notification_delta_done(struct notification_xml * nxml)597 notification_delta_done(struct notification_xml *nxml)
598 {
599 	struct delta_item *d;
600 
601 	d = TAILQ_FIRST(&nxml->delta_q);
602 	assert(d->serial == nxml->current->serial);
603 	TAILQ_REMOVE(&nxml->delta_q, d, q);
604 	free_delta(d);
605 
606 	assert(!TAILQ_EMPTY(&nxml->delta_q) ||
607 	    nxml->serial == nxml->current->serial);
608 	return TAILQ_EMPTY(&nxml->delta_q);
609 }
610 
611 /* Used in regress. */
612 void
log_notification_xml(struct notification_xml * nxml)613 log_notification_xml(struct notification_xml *nxml)
614 {
615 	struct delta_item *d;
616 	char *hash;
617 
618 	logx("session_id: %s, serial: %lld", nxml->session_id, nxml->serial);
619 	logx("snapshot_uri: %s", nxml->snapshot_uri);
620 	hash = hex_encode(nxml->snapshot_hash, sizeof(nxml->snapshot_hash));
621 	logx("snapshot hash: %s", hash);
622 	free(hash);
623 
624 	TAILQ_FOREACH(d, &nxml->delta_q, q) {
625 		logx("delta serial %lld uri: %s", d->serial, d->uri);
626 		hash = hex_encode(d->hash, sizeof(d->hash));
627 		logx("delta hash: %s", hash);
628 		free(hash);
629 	}
630 }
631