]>
Commit | Line | Data |
---|---|---|
d7837182 TL |
1 | /* hash.c |
2 | ||
3 | Routines for manipulating hash tables... */ | |
4 | ||
5 | /* | |
1943bbf8 SR |
6 | * Copyright (c) 2009-2010 by Internet Systems Consortium, Inc. ("ISC") |
7 | * Copyright (c) 2004-2007 by Internet Systems Consortium, Inc. ("ISC") | |
98311e4b | 8 | * Copyright (c) 1995-2003 by Internet Software Consortium |
d7837182 | 9 | * |
98311e4b DH |
10 | * Permission to use, copy, modify, and distribute this software for any |
11 | * purpose with or without fee is hereby granted, provided that the above | |
12 | * copyright notice and this permission notice appear in all copies. | |
d7837182 | 13 | * |
98311e4b DH |
14 | * THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES |
15 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF | |
16 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR | |
17 | * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES | |
18 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN | |
19 | * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT | |
20 | * OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. | |
d7837182 | 21 | * |
98311e4b DH |
22 | * Internet Systems Consortium, Inc. |
23 | * 950 Charter Street | |
24 | * Redwood City, CA 94063 | |
25 | * <info@isc.org> | |
2c85ac9b | 26 | * https://www.isc.org/ |
49733f31 | 27 | * |
98311e4b | 28 | * This software has been written for Internet Systems Consortium |
49733f31 | 29 | * by Ted Lemon in cooperation with Vixie Enterprises and Nominum, Inc. |
98311e4b | 30 | * To learn more about Internet Systems Consortium, see |
2c85ac9b | 31 | * ``https://www.isc.org/''. To learn more about Vixie Enterprises, |
49733f31 TL |
32 | * see ``http://www.vix.com''. To learn more about Nominum, Inc., see |
33 | * ``http://www.nominum.com''. | |
d7837182 TL |
34 | */ |
35 | ||
fe5b0fdd DH |
36 | #include "dhcpd.h" |
37 | ||
c62871ba | 38 | #include <omapip/omapip_p.h> |
fe5b0fdd | 39 | #include <limits.h> |
7d9784f6 | 40 | #include <ctype.h> |
d7837182 | 41 | |
fe5b0fdd | 42 | static unsigned |
f7fdb216 DH |
43 | find_length(const void *key, |
44 | unsigned (*do_hash)(const void *, unsigned, unsigned)) | |
45 | { | |
46 | if (do_hash == do_case_hash || do_hash == do_string_hash) | |
47 | return strlen((const char *)key); | |
48 | if (do_hash == do_number_hash) | |
49 | return sizeof(unsigned); | |
50 | if (do_hash == do_ip4_hash) | |
51 | return 4; | |
52 | ||
1943bbf8 SR |
53 | log_debug("Unexpected hash function at %s:%d.", MDL); |
54 | /* | |
55 | * If we get a hash function we don't specifically expect | |
56 | * return a length of 0, this covers the case where a client | |
57 | * id has a length of 0. | |
58 | */ | |
59 | return 0; | |
f7fdb216 | 60 | } |
c62871ba | 61 | |
98311e4b DH |
62 | int new_hash_table (tp, count, file, line) |
63 | struct hash_table **tp; | |
f7fdb216 | 64 | unsigned count; |
c62871ba DN |
65 | const char *file; |
66 | int line; | |
67 | { | |
98311e4b | 68 | struct hash_table *rval; |
272ef1bc | 69 | unsigned extra; |
98311e4b DH |
70 | |
71 | if (!tp) { | |
72 | log_error ("%s(%d): new_hash_table called with null pointer.", | |
73 | file, line); | |
74 | #if defined (POINTER_DEBUG) | |
75 | abort (); | |
76 | #endif | |
77 | return 0; | |
78 | } | |
79 | if (*tp) { | |
80 | log_error ("%s(%d): non-null target for new_hash_table.", | |
81 | file, line); | |
82 | #if defined (POINTER_DEBUG) | |
83 | abort (); | |
84 | #endif | |
85 | } | |
272ef1bc SK |
86 | |
87 | /* There is one hash bucket in the structure. Allocate extra | |
88 | * memory beyond the end of the structure to fulfill the requested | |
89 | * count ("count - 1"). Do not let there be less than one. | |
90 | */ | |
91 | if (count <= 1) | |
92 | extra = 0; | |
93 | else | |
94 | extra = count - 1; | |
95 | ||
96 | rval = dmalloc(sizeof(struct hash_table) + | |
97 | (extra * sizeof(struct hash_bucket *)), file, line); | |
98311e4b DH |
98 | if (!rval) |
99 | return 0; | |
c62871ba | 100 | rval -> hash_count = count; |
98311e4b DH |
101 | *tp = rval; |
102 | return 1; | |
c62871ba DN |
103 | } |
104 | ||
98311e4b DH |
105 | void free_hash_table (tp, file, line) |
106 | struct hash_table **tp; | |
c62871ba DN |
107 | const char *file; |
108 | int line; | |
109 | { | |
98311e4b | 110 | struct hash_table *ptr = *tp; |
d758ad8c TL |
111 | |
112 | #if defined (DEBUG_MEMORY_LEAKAGE) || \ | |
113 | defined (DEBUG_MEMORY_LEAKAGE_ON_EXIT) | |
28868515 SK |
114 | int i; |
115 | struct hash_bucket *hbc, *hbn = (struct hash_bucket *)0; | |
116 | ||
d758ad8c TL |
117 | for (i = 0; i < ptr -> hash_count; i++) { |
118 | for (hbc = ptr -> buckets [i]; hbc; hbc = hbn) { | |
119 | hbn = hbc -> next; | |
120 | if (ptr -> dereferencer && hbc -> value) | |
121 | (*ptr -> dereferencer) (&hbc -> value, MDL); | |
122 | } | |
123 | for (hbc = ptr -> buckets [i]; hbc; hbc = hbn) { | |
124 | hbn = hbc -> next; | |
125 | free_hash_bucket (hbc, MDL); | |
126 | } | |
127 | ptr -> buckets [i] = (struct hash_bucket *)0; | |
128 | } | |
129 | #endif | |
130 | ||
fe5b0fdd | 131 | dfree((void *)ptr, MDL); |
98311e4b | 132 | *tp = (struct hash_table *)0; |
c62871ba DN |
133 | } |
134 | ||
135 | struct hash_bucket *free_hash_buckets; | |
136 | ||
d758ad8c TL |
137 | #if defined (DEBUG_MEMORY_LEAKAGE_ON_EXIT) |
138 | struct hash_bucket *hash_bucket_hunks; | |
139 | ||
140 | void relinquish_hash_bucket_hunks () | |
141 | { | |
142 | struct hash_bucket *c, *n, **p; | |
143 | ||
144 | /* Account for all the hash buckets on the free list. */ | |
145 | p = &free_hash_buckets; | |
146 | for (c = free_hash_buckets; c; c = c -> next) { | |
147 | for (n = hash_bucket_hunks; n; n = n -> next) { | |
148 | if (c > n && c < n + 127) { | |
149 | *p = c -> next; | |
150 | n -> len++; | |
151 | break; | |
152 | } | |
153 | } | |
154 | /* If we didn't delete the hash bucket from the free list, | |
155 | advance the pointer. */ | |
156 | if (!n) | |
157 | p = &c -> next; | |
158 | } | |
159 | ||
160 | for (c = hash_bucket_hunks; c; c = n) { | |
161 | n = c -> next; | |
162 | if (c -> len != 126) { | |
163 | log_info ("hashbucket %lx hash_buckets %d free %u", | |
164 | (unsigned long)c, 127, c -> len); | |
165 | } | |
166 | dfree (c, MDL); | |
167 | } | |
168 | } | |
169 | #endif | |
170 | ||
c62871ba DN |
171 | struct hash_bucket *new_hash_bucket (file, line) |
172 | const char *file; | |
173 | int line; | |
174 | { | |
175 | struct hash_bucket *rval; | |
d758ad8c | 176 | int i = 0; |
c62871ba DN |
177 | if (!free_hash_buckets) { |
178 | rval = dmalloc (127 * sizeof (struct hash_bucket), | |
179 | file, line); | |
180 | if (!rval) | |
181 | return rval; | |
d758ad8c TL |
182 | # if defined (DEBUG_MEMORY_LEAKAGE_ON_EXIT) |
183 | rval -> next = hash_bucket_hunks; | |
184 | hash_bucket_hunks = rval; | |
185 | hash_bucket_hunks -> len = 0; | |
186 | i++; | |
187 | rval++; | |
188 | #endif | |
189 | for (; i < 127; i++) { | |
c62871ba DN |
190 | rval -> next = free_hash_buckets; |
191 | free_hash_buckets = rval; | |
192 | rval++; | |
193 | } | |
194 | } | |
195 | rval = free_hash_buckets; | |
196 | free_hash_buckets = rval -> next; | |
197 | return rval; | |
198 | } | |
199 | ||
200 | void free_hash_bucket (ptr, file, line) | |
201 | struct hash_bucket *ptr; | |
202 | const char *file; | |
203 | int line; | |
204 | { | |
d758ad8c | 205 | #if defined (DEBUG_MALLOC_POOL) |
28868515 SK |
206 | struct hash_bucket *hp; |
207 | ||
d758ad8c TL |
208 | for (hp = free_hash_buckets; hp; hp = hp -> next) { |
209 | if (hp == ptr) { | |
210 | log_error ("hash bucket freed twice!"); | |
211 | abort (); | |
212 | } | |
213 | } | |
214 | #endif | |
c62871ba DN |
215 | ptr -> next = free_hash_buckets; |
216 | free_hash_buckets = ptr; | |
217 | } | |
089fb364 | 218 | |
f7fdb216 DH |
219 | int new_hash(struct hash_table **rp, |
220 | hash_reference referencer, | |
221 | hash_dereference dereferencer, | |
222 | unsigned hsize, | |
223 | unsigned (*hasher)(const void *, unsigned, unsigned), | |
224 | const char *file, int line) | |
d7837182 | 225 | { |
f7fdb216 DH |
226 | if (hsize == 0) |
227 | hsize = DEFAULT_HASH_SIZE; | |
228 | ||
229 | if (!new_hash_table (rp, hsize, file, line)) | |
98311e4b | 230 | return 0; |
f7fdb216 DH |
231 | |
232 | memset ((*rp)->buckets, 0, hsize * sizeof(struct hash_bucket *)); | |
233 | ||
234 | (*rp)->referencer = referencer; | |
235 | (*rp)->dereferencer = dereferencer; | |
236 | (*rp)->do_hash = hasher; | |
237 | ||
238 | if (hasher == do_case_hash) | |
239 | (*rp)->cmp = casecmp; | |
240 | else | |
241 | (*rp)->cmp = memcmp; | |
242 | ||
98311e4b | 243 | return 1; |
d7837182 TL |
244 | } |
245 | ||
f7fdb216 DH |
246 | unsigned |
247 | do_case_hash(const void *name, unsigned len, unsigned size) | |
d7837182 | 248 | { |
f7fdb216 DH |
249 | register unsigned accum = 0; |
250 | register const unsigned char *s = name; | |
d7837182 | 251 | int i = len; |
7d9784f6 TL |
252 | register unsigned c; |
253 | ||
254 | while (i--) { | |
255 | /* Make the hash case-insensitive. */ | |
256 | c = *s++; | |
f7fdb216 DH |
257 | if (isascii(c)) |
258 | c = tolower(c); | |
7d9784f6 TL |
259 | |
260 | /* Add the character in... */ | |
d5ef2946 TL |
261 | accum = (accum << 1) + c; |
262 | ||
7d9784f6 | 263 | /* Add carry back in... */ |
d5ef2946 TL |
264 | while (accum > 65535) { |
265 | accum = (accum & 65535) + (accum >> 16); | |
7d9784f6 | 266 | } |
f7fdb216 | 267 | |
7d9784f6 TL |
268 | } |
269 | return accum % size; | |
270 | } | |
271 | ||
f7fdb216 DH |
272 | unsigned |
273 | do_string_hash(const void *name, unsigned len, unsigned size) | |
7d9784f6 | 274 | { |
f7fdb216 | 275 | register unsigned accum = 0; |
7d9784f6 TL |
276 | register const unsigned char *s = (const unsigned char *)name; |
277 | int i = len; | |
278 | ||
34bef96b TL |
279 | while (i--) { |
280 | /* Add the character in... */ | |
d5ef2946 TL |
281 | accum = (accum << 1) + *s++; |
282 | ||
34bef96b | 283 | /* Add carry back in... */ |
d5ef2946 TL |
284 | while (accum > 65535) { |
285 | accum = (accum & 65535) + (accum >> 16); | |
d7837182 TL |
286 | } |
287 | } | |
288 | return accum % size; | |
289 | } | |
290 | ||
6708d944 DH |
291 | /* Client identifiers are generally 32-bits of ordinary |
292 | * non-randomness followed by 24-bits of unordinary randomness. | |
293 | * So, end-align in 24-bit chunks, and xor any preceding data | |
294 | * just to mix it up a little. | |
295 | */ | |
296 | unsigned | |
297 | do_id_hash(const void *name, unsigned len, unsigned size) | |
298 | { | |
299 | register unsigned accum = 0; | |
300 | register const unsigned char *s = (const unsigned char *)name; | |
301 | const unsigned char *end = s + len; | |
302 | ||
303 | if (len == 0) | |
304 | return 0; | |
305 | ||
a3528574 SR |
306 | /* |
307 | * The switch handles our starting conditions, then we hash the | |
308 | * remaining bytes in groups of 3 | |
6708d944 | 309 | */ |
a3528574 | 310 | |
6708d944 | 311 | switch (len % 3) { |
a3528574 SR |
312 | case 0: |
313 | break; | |
314 | case 2: | |
315 | accum ^= *s++ << 8; | |
316 | case 1: | |
317 | accum ^= *s++; | |
6708d944 DH |
318 | break; |
319 | } | |
320 | ||
a3528574 SR |
321 | while (s < end) { |
322 | accum ^= *s++ << 16; | |
323 | accum ^= *s++ << 8; | |
324 | accum ^= *s++; | |
325 | } | |
326 | ||
6708d944 DH |
327 | return accum % size; |
328 | } | |
329 | ||
f7fdb216 DH |
330 | unsigned |
331 | do_number_hash(const void *key, unsigned len, unsigned size) | |
332 | { | |
333 | register unsigned number = *((const unsigned *)key); | |
334 | ||
335 | return number % size; | |
336 | } | |
337 | ||
338 | unsigned | |
339 | do_ip4_hash(const void *key, unsigned len, unsigned size) | |
340 | { | |
341 | u_int32_t number; | |
342 | ||
343 | memcpy(&number, key, 4); | |
344 | ||
345 | number = ntohl(number); | |
346 | ||
347 | return number % size; | |
348 | } | |
349 | ||
6708d944 DH |
350 | unsigned char * |
351 | hash_report(struct hash_table *table) | |
352 | { | |
353 | static unsigned char retbuf[sizeof("Contents/Size (%): " | |
354 | "2147483647/2147483647 " | |
355 | "(2147483647%). " | |
356 | "Min/max: 2147483647/2147483647")]; | |
357 | unsigned curlen, pct, contents=0, minlen=UINT_MAX, maxlen=0; | |
358 | unsigned i; | |
359 | struct hash_bucket *bp; | |
360 | ||
8b1cb226 DH |
361 | if (table == NULL) |
362 | return (unsigned char *) "No table."; | |
363 | ||
6708d944 | 364 | if (table->hash_count == 0) |
28868515 | 365 | return (unsigned char *) "Invalid hash table."; |
6708d944 DH |
366 | |
367 | for (i = 0 ; i < table->hash_count ; i++) { | |
368 | curlen = 0; | |
369 | ||
370 | bp = table->buckets[i]; | |
371 | while (bp != NULL) { | |
372 | curlen++; | |
373 | bp = bp->next; | |
374 | } | |
375 | ||
376 | if (curlen < minlen) | |
377 | minlen = curlen; | |
378 | if (curlen > maxlen) | |
379 | maxlen = curlen; | |
380 | ||
381 | contents += curlen; | |
382 | } | |
383 | ||
384 | if (contents >= (UINT_MAX / 100)) | |
385 | pct = contents / ((table->hash_count / 100) + 1); | |
386 | else | |
387 | pct = (contents * 100) / table->hash_count; | |
388 | ||
389 | if (contents > 2147483647 || | |
390 | table->hash_count > 2147483647 || | |
391 | pct > 2147483647 || | |
392 | minlen > 2147483647 || | |
393 | maxlen > 2147483647) | |
28868515 | 394 | return (unsigned char *) "Report out of range for display."; |
6708d944 | 395 | |
28868515 SK |
396 | sprintf((char *)retbuf, |
397 | "Contents/Size (%%): %u/%u (%u%%). Min/max: %u/%u", | |
6708d944 DH |
398 | contents, table->hash_count, pct, minlen, maxlen); |
399 | ||
400 | return retbuf; | |
401 | } | |
402 | ||
f7fdb216 | 403 | void add_hash (table, key, len, pointer, file, line) |
d7837182 | 404 | struct hash_table *table; |
b1b7b521 | 405 | unsigned len; |
f7fdb216 | 406 | const void *key; |
20916cae TL |
407 | hashed_object_t *pointer; |
408 | const char *file; | |
409 | int line; | |
d7837182 | 410 | { |
a0330332 TL |
411 | int hashno; |
412 | struct hash_bucket *bp; | |
0cae26a7 | 413 | void *foo; |
a0330332 TL |
414 | |
415 | if (!table) | |
416 | return; | |
417 | ||
48036271 | 418 | if (!len) |
f7fdb216 | 419 | len = find_length(key, table->do_hash); |
48036271 | 420 | |
f7fdb216 | 421 | hashno = (*table->do_hash)(key, len, table->hash_count); |
20916cae | 422 | bp = new_hash_bucket (file, line); |
a0330332 | 423 | |
d7837182 | 424 | if (!bp) { |
f7fdb216 | 425 | log_error ("Can't add entry to hash table: no memory."); |
d7837182 TL |
426 | return; |
427 | } | |
f7fdb216 | 428 | bp -> name = key; |
0cae26a7 TL |
429 | if (table -> referencer) { |
430 | foo = &bp -> value; | |
20916cae | 431 | (*(table -> referencer)) (foo, pointer, file, line); |
0cae26a7 TL |
432 | } else |
433 | bp -> value = pointer; | |
d7837182 | 434 | bp -> next = table -> buckets [hashno]; |
97ca1699 | 435 | bp -> len = len; |
d7837182 TL |
436 | table -> buckets [hashno] = bp; |
437 | } | |
438 | ||
f7fdb216 | 439 | void delete_hash_entry (table, key, len, file, line) |
d7837182 | 440 | struct hash_table *table; |
b1b7b521 | 441 | unsigned len; |
f7fdb216 | 442 | const void *key; |
20916cae TL |
443 | const char *file; |
444 | int line; | |
d7837182 | 445 | { |
a0330332 | 446 | int hashno; |
d7837182 | 447 | struct hash_bucket *bp, *pbp = (struct hash_bucket *)0; |
0cae26a7 | 448 | void *foo; |
d7837182 | 449 | |
a0330332 TL |
450 | if (!table) |
451 | return; | |
452 | ||
48036271 | 453 | if (!len) |
f7fdb216 | 454 | len = find_length(key, table->do_hash); |
48036271 | 455 | |
f7fdb216 | 456 | hashno = (*table->do_hash)(key, len, table->hash_count); |
a0330332 | 457 | |
d7837182 TL |
458 | /* Go through the list looking for an entry that matches; |
459 | if we find it, delete it. */ | |
460 | for (bp = table -> buckets [hashno]; bp; bp = bp -> next) { | |
69858972 | 461 | if ((!bp -> len && |
f7fdb216 | 462 | !strcmp ((const char *)bp->name, key)) || |
d7837182 | 463 | (bp -> len == len && |
f7fdb216 | 464 | !(table -> cmp)(bp->name, key, len))) { |
d7837182 TL |
465 | if (pbp) { |
466 | pbp -> next = bp -> next; | |
467 | } else { | |
468 | table -> buckets [hashno] = bp -> next; | |
469 | } | |
d758ad8c | 470 | if (bp -> value && table -> dereferencer) { |
0cae26a7 | 471 | foo = &bp -> value; |
20916cae | 472 | (*(table -> dereferencer)) (foo, file, line); |
0cae26a7 | 473 | } |
20916cae | 474 | free_hash_bucket (bp, file, line); |
d7837182 TL |
475 | break; |
476 | } | |
4c4c7907 | 477 | pbp = bp; /* jwg, 9/6/96 - nice catch! */ |
d7837182 TL |
478 | } |
479 | } | |
480 | ||
f7fdb216 | 481 | int hash_lookup (vp, table, key, len, file, line) |
20916cae | 482 | hashed_object_t **vp; |
d7837182 | 483 | struct hash_table *table; |
f7fdb216 | 484 | const void *key; |
b1b7b521 | 485 | unsigned len; |
20916cae TL |
486 | const char *file; |
487 | int line; | |
d7837182 | 488 | { |
a0330332 | 489 | int hashno; |
d7837182 TL |
490 | struct hash_bucket *bp; |
491 | ||
a0330332 | 492 | if (!table) |
20916cae | 493 | return 0; |
48036271 | 494 | if (!len) |
f7fdb216 | 495 | len = find_length(key, table->do_hash); |
48036271 | 496 | |
272ef1bc SK |
497 | if (*vp != NULL) { |
498 | log_fatal("Internal inconsistency: storage value has not been " | |
499 | "initialized to zero (from %s:%d).", file, line); | |
500 | } | |
501 | ||
f7fdb216 | 502 | hashno = (*table->do_hash)(key, len, table->hash_count); |
a0330332 | 503 | |
48036271 TL |
504 | for (bp = table -> buckets [hashno]; bp; bp = bp -> next) { |
505 | if (len == bp -> len | |
f7fdb216 | 506 | && !(*table->cmp)(bp->name, key, len)) { |
20916cae TL |
507 | if (table -> referencer) |
508 | (*table -> referencer) (vp, bp -> value, | |
509 | file, line); | |
510 | else | |
511 | *vp = bp -> value; | |
512 | return 1; | |
513 | } | |
d7837182 | 514 | } |
20916cae | 515 | return 0; |
d7837182 TL |
516 | } |
517 | ||
d0411fbd TL |
518 | int hash_foreach (struct hash_table *table, hash_foreach_func func) |
519 | { | |
520 | int i; | |
521 | struct hash_bucket *bp, *next; | |
522 | int count = 0; | |
523 | ||
524 | if (!table) | |
525 | return 0; | |
526 | ||
527 | for (i = 0; i < table -> hash_count; i++) { | |
528 | bp = table -> buckets [i]; | |
529 | while (bp) { | |
530 | next = bp -> next; | |
06e77c34 DH |
531 | if ((*func)(bp->name, bp->len, bp->value) |
532 | != ISC_R_SUCCESS) | |
533 | return count; | |
d0411fbd TL |
534 | bp = next; |
535 | count++; | |
536 | } | |
537 | } | |
538 | return count; | |
539 | } | |
540 | ||
d19e2cf7 | 541 | int casecmp (const void *v1, const void *v2, size_t len) |
7d9784f6 | 542 | { |
d19e2cf7 | 543 | size_t i; |
804401cc EH |
544 | const unsigned char *s = v1; |
545 | const unsigned char *t = v2; | |
7d9784f6 TL |
546 | |
547 | for (i = 0; i < len; i++) | |
548 | { | |
549 | int c1, c2; | |
d19e2cf7 DH |
550 | if (isascii(s[i])) |
551 | c1 = tolower(s[i]); | |
7d9784f6 | 552 | else |
d19e2cf7 DH |
553 | c1 = s[i]; |
554 | ||
555 | if (isascii(t[i])) | |
556 | c2 = tolower(t[i]); | |
7d9784f6 | 557 | else |
d19e2cf7 DH |
558 | c2 = t[i]; |
559 | ||
7d9784f6 TL |
560 | if (c1 < c2) |
561 | return -1; | |
562 | if (c1 > c2) | |
563 | return 1; | |
564 | } | |
565 | return 0; | |
566 | } |