]>
Commit | Line | Data |
---|---|---|
718e3744 | 1 | /* BGP Extended Communities Attribute |
2 | Copyright (C) 2000 Kunihiro Ishiguro <kunihiro@zebra.org> | |
3 | ||
4 | This file is part of GNU Zebra. | |
5 | ||
6 | GNU Zebra is free software; you can redistribute it and/or modify it | |
7 | under the terms of the GNU General Public License as published by the | |
8 | Free Software Foundation; either version 2, or (at your option) any | |
9 | later version. | |
10 | ||
11 | GNU Zebra is distributed in the hope that it will be useful, but | |
12 | WITHOUT ANY WARRANTY; without even the implied warranty of | |
13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |
14 | General Public License for more details. | |
15 | ||
16 | You should have received a copy of the GNU General Public License | |
17 | along with GNU Zebra; see the file COPYING. If not, write to the Free | |
18 | Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA | |
19 | 02111-1307, USA. */ | |
20 | ||
21 | #include <zebra.h> | |
22 | ||
23 | #include "hash.h" | |
24 | #include "memory.h" | |
25 | #include "prefix.h" | |
26 | #include "command.h" | |
27 | ||
28 | #include "bgpd/bgpd.h" | |
29 | #include "bgpd/bgp_ecommunity.h" | |
30 | ||
31 | /* Hash of community attribute. */ | |
32 | struct hash *ecomhash; | |
33 | \f | |
34 | /* Allocate a new ecommunities. */ | |
35 | struct ecommunity * | |
36 | ecommunity_new () | |
37 | { | |
38 | return (struct ecommunity *) XCALLOC (MTYPE_ECOMMUNITY, | |
39 | sizeof (struct ecommunity)); | |
40 | } | |
41 | ||
42 | /* Allocate ecommunities. */ | |
43 | void | |
44 | ecommunity_free (struct ecommunity *ecom) | |
45 | { | |
46 | if (ecom->val) | |
47 | XFREE (MTYPE_ECOMMUNITY_VAL, ecom->val); | |
48 | if (ecom->str) | |
49 | XFREE (MTYPE_ECOMMUNITY_STR, ecom->str); | |
50 | XFREE (MTYPE_ECOMMUNITY, ecom); | |
51 | } | |
52 | ||
53 | /* Add a new Extended Communities value to Extended Communities | |
54 | Attribute structure. When the value is already exists in the | |
55 | structure, we don't add the value. Newly added value is sorted by | |
56 | numerical order. When the value is added to the structure return 1 | |
57 | else return 0. */ | |
58 | static int | |
59 | ecommunity_add_val (struct ecommunity *ecom, struct ecommunity_val *eval) | |
60 | { | |
61 | u_char *p; | |
62 | int ret; | |
63 | int c; | |
64 | ||
65 | /* When this is fist value, just add it. */ | |
66 | if (ecom->val == NULL) | |
67 | { | |
68 | ecom->size++; | |
69 | ecom->val = XMALLOC (MTYPE_ECOMMUNITY_VAL, ecom_length (ecom)); | |
70 | memcpy (ecom->val, eval->val, ECOMMUNITY_SIZE); | |
71 | return 1; | |
72 | } | |
73 | ||
74 | /* If the value already exists in the structure return 0. */ | |
75 | c = 0; | |
76 | for (p = ecom->val; c < ecom->size; p += ECOMMUNITY_SIZE, c++) | |
77 | { | |
78 | ret = memcmp (p, eval->val, ECOMMUNITY_SIZE); | |
79 | if (ret == 0) | |
80 | return 0; | |
81 | if (ret > 0) | |
82 | break; | |
83 | } | |
84 | ||
85 | /* Add the value to the structure with numerical sorting. */ | |
86 | ecom->size++; | |
87 | ecom->val = XREALLOC (MTYPE_ECOMMUNITY_VAL, ecom->val, ecom_length (ecom)); | |
88 | ||
89 | memmove (ecom->val + (c + 1) * ECOMMUNITY_SIZE, | |
90 | ecom->val + c * ECOMMUNITY_SIZE, | |
91 | (ecom->size - 1 - c) * ECOMMUNITY_SIZE); | |
92 | memcpy (ecom->val + c * ECOMMUNITY_SIZE, eval->val, ECOMMUNITY_SIZE); | |
93 | ||
94 | return 1; | |
95 | } | |
96 | ||
97 | /* This function takes pointer to Extended Communites strucutre then | |
98 | create a new Extended Communities structure by uniq and sort each | |
99 | Exteneded Communities value. */ | |
100 | struct ecommunity * | |
101 | ecommunity_uniq_sort (struct ecommunity *ecom) | |
102 | { | |
103 | int i; | |
104 | struct ecommunity *new; | |
105 | struct ecommunity_val *eval; | |
106 | ||
107 | if (! ecom) | |
108 | return NULL; | |
109 | ||
110 | new = ecommunity_new ();; | |
111 | ||
112 | for (i = 0; i < ecom->size; i++) | |
113 | { | |
114 | eval = (struct ecommunity_val *) (ecom->val + (i * ECOMMUNITY_SIZE)); | |
115 | ecommunity_add_val (new, eval); | |
116 | } | |
117 | return new; | |
118 | } | |
119 | ||
120 | /* Parse Extended Communites Attribute in BGP packet. */ | |
121 | struct ecommunity * | |
122 | ecommunity_parse (char *pnt, u_short length) | |
123 | { | |
124 | struct ecommunity tmp; | |
125 | struct ecommunity *new; | |
126 | ||
127 | /* Length check. */ | |
128 | if (length % ECOMMUNITY_SIZE) | |
129 | return NULL; | |
130 | ||
131 | /* Prepare tmporary structure for making a new Extended Communities | |
132 | Attribute. */ | |
133 | tmp.size = length / ECOMMUNITY_SIZE; | |
134 | tmp.val = pnt; | |
135 | ||
136 | /* Create a new Extended Communities Attribute by uniq and sort each | |
137 | Extended Communities value */ | |
138 | new = ecommunity_uniq_sort (&tmp); | |
139 | ||
140 | return ecommunity_intern (new); | |
141 | } | |
142 | ||
143 | /* Duplicate the Extended Communities Attribute structure. */ | |
144 | struct ecommunity * | |
145 | ecommunity_dup (struct ecommunity *ecom) | |
146 | { | |
147 | struct ecommunity *new; | |
148 | ||
149 | new = XCALLOC (MTYPE_ECOMMUNITY, sizeof (struct ecommunity)); | |
150 | new->size = ecom->size; | |
151 | if (new->size) | |
152 | { | |
153 | new->val = XMALLOC (MTYPE_ECOMMUNITY_VAL, ecom->size * ECOMMUNITY_SIZE); | |
154 | memcpy (new->val, ecom->val, ecom->size * ECOMMUNITY_SIZE); | |
155 | } | |
156 | else | |
157 | new->val = NULL; | |
158 | return new; | |
159 | } | |
160 | ||
161 | /* Merge two Extended Communities Attribute structure. */ | |
162 | struct ecommunity * | |
163 | ecommunity_merge (struct ecommunity *ecom1, struct ecommunity *ecom2) | |
164 | { | |
165 | if (ecom1->val) | |
166 | ecom1->val = XREALLOC (MTYPE_ECOMMUNITY_VAL, ecom1->val, | |
167 | (ecom1->size + ecom2->size) * ECOMMUNITY_SIZE); | |
168 | else | |
169 | ecom1->val = XMALLOC (MTYPE_ECOMMUNITY_VAL, | |
170 | (ecom1->size + ecom2->size) * ECOMMUNITY_SIZE); | |
171 | ||
172 | memcpy (ecom1->val + (ecom1->size * ECOMMUNITY_SIZE), | |
173 | ecom2->val, ecom2->size * ECOMMUNITY_SIZE); | |
174 | ecom1->size += ecom2->size; | |
175 | ||
176 | return ecom1; | |
177 | } | |
178 | ||
179 | /* Intern Extended Communities Attribute. */ | |
180 | struct ecommunity * | |
181 | ecommunity_intern (struct ecommunity *ecom) | |
182 | { | |
183 | struct ecommunity *find; | |
184 | ||
185 | assert (ecom->refcnt == 0); | |
186 | ||
187 | find = (struct ecommunity *) hash_get (ecomhash, ecom, hash_alloc_intern); | |
188 | ||
189 | if (find != ecom) | |
190 | ecommunity_free (ecom); | |
191 | ||
192 | find->refcnt++; | |
193 | ||
194 | if (! find->str) | |
195 | find->str = ecommunity_ecom2str (find, ECOMMUNITY_FORMAT_DISPLAY); | |
196 | ||
197 | return find; | |
198 | } | |
199 | ||
200 | /* Unintern Extended Communities Attribute. */ | |
201 | void | |
202 | ecommunity_unintern (struct ecommunity *ecom) | |
203 | { | |
204 | struct ecommunity *ret; | |
205 | ||
206 | if (ecom->refcnt) | |
207 | ecom->refcnt--; | |
208 | ||
209 | /* Pull off from hash. */ | |
210 | if (ecom->refcnt == 0) | |
211 | { | |
212 | /* Extended community must be in the hash. */ | |
213 | ret = (struct ecommunity *) hash_release (ecomhash, ecom); | |
214 | assert (ret != NULL); | |
215 | ||
216 | ecommunity_free (ecom); | |
217 | } | |
218 | } | |
219 | ||
220 | /* Utinity function to make hash key. */ | |
221 | unsigned int | |
222 | ecommunity_hash_make (struct ecommunity *ecom) | |
223 | { | |
224 | int c; | |
225 | unsigned int key; | |
226 | unsigned char *pnt; | |
227 | ||
228 | key = 0; | |
229 | pnt = ecom->val; | |
230 | ||
231 | for (c = 0; c < ecom->size * ECOMMUNITY_SIZE; c++) | |
232 | key += pnt[c]; | |
233 | ||
234 | return key; | |
235 | } | |
236 | ||
237 | /* Compare two Extended Communities Attribute structure. */ | |
238 | int | |
239 | ecommunity_cmp (struct ecommunity *ecom1, struct ecommunity *ecom2) | |
240 | { | |
241 | if (ecom1->size == ecom2->size | |
242 | && memcmp (ecom1->val, ecom2->val, ecom1->size * ECOMMUNITY_SIZE) == 0) | |
243 | return 1; | |
244 | return 0; | |
245 | } | |
246 | ||
247 | /* Initialize Extended Comminities related hash. */ | |
248 | void | |
249 | ecommunity_init () | |
250 | { | |
251 | ecomhash = hash_create (ecommunity_hash_make, ecommunity_cmp); | |
252 | } | |
253 | \f | |
254 | /* Extended Communities token enum. */ | |
255 | enum ecommunity_token | |
256 | { | |
257 | ecommunity_token_rt, | |
258 | ecommunity_token_soo, | |
259 | ecommunity_token_val, | |
260 | ecommunity_token_unknown | |
261 | }; | |
262 | ||
263 | /* Get next Extended Communities token from the string. */ | |
264 | char * | |
265 | ecommunity_gettoken (char *str, struct ecommunity_val *eval, | |
266 | enum ecommunity_token *token) | |
267 | { | |
268 | int ret; | |
269 | int dot = 0; | |
270 | int digit = 0; | |
271 | int separator = 0; | |
272 | u_int32_t val_low = 0; | |
273 | u_int32_t val_high = 0; | |
274 | char *p = str; | |
275 | struct in_addr ip; | |
276 | char ipstr[INET_ADDRSTRLEN + 1]; | |
277 | ||
278 | /* Skip white space. */ | |
279 | while (isspace ((int) *p)) | |
280 | { | |
281 | p++; | |
282 | str++; | |
283 | } | |
284 | ||
285 | /* Check the end of the line. */ | |
286 | if (*p == '\0') | |
287 | return NULL; | |
288 | ||
289 | /* "rt" and "soo" keyword parse. */ | |
290 | if (! isdigit ((int) *p)) | |
291 | { | |
292 | /* "rt" match check. */ | |
293 | if (tolower ((int) *p) == 'r') | |
294 | { | |
295 | p++; | |
296 | if (tolower ((int) *p) == 't') | |
297 | { | |
298 | p++; | |
299 | *token = ecommunity_token_rt; | |
300 | return p; | |
301 | } | |
302 | if (isspace ((int) *p) || *p == '\0') | |
303 | { | |
304 | *token = ecommunity_token_rt; | |
305 | return p; | |
306 | } | |
307 | goto error; | |
308 | } | |
309 | /* "soo" match check. */ | |
310 | else if (tolower ((int) *p) == 's') | |
311 | { | |
312 | p++; | |
313 | if (tolower ((int) *p) == 'o') | |
314 | { | |
315 | p++; | |
316 | if (tolower ((int) *p) == 'o') | |
317 | { | |
318 | p++; | |
319 | *token = ecommunity_token_soo; | |
320 | return p; | |
321 | } | |
322 | if (isspace ((int) *p) || *p == '\0') | |
323 | { | |
324 | *token = ecommunity_token_soo; | |
325 | return p; | |
326 | } | |
327 | goto error; | |
328 | } | |
329 | if (isspace ((int) *p) || *p == '\0') | |
330 | { | |
331 | *token = ecommunity_token_soo; | |
332 | return p; | |
333 | } | |
334 | goto error; | |
335 | } | |
336 | goto error; | |
337 | } | |
338 | ||
339 | while (isdigit ((int) *p) || *p == ':' || *p == '.') | |
340 | { | |
341 | if (*p == ':') | |
342 | { | |
343 | if (separator) | |
344 | goto error; | |
345 | ||
346 | separator = 1; | |
347 | digit = 0; | |
348 | ||
349 | if (dot) | |
350 | { | |
351 | if ((p - str) > INET_ADDRSTRLEN) | |
352 | goto error; | |
353 | ||
354 | memset (ipstr, 0, INET_ADDRSTRLEN + 1); | |
355 | memcpy (ipstr, str, p - str); | |
356 | ||
357 | ret = inet_aton (ipstr, &ip); | |
358 | if (ret == 0) | |
359 | goto error; | |
360 | } | |
361 | else | |
362 | val_high = val_low; | |
363 | ||
364 | val_low = 0; | |
365 | } | |
366 | else if (*p == '.') | |
367 | { | |
368 | if (separator) | |
369 | goto error; | |
370 | dot++; | |
371 | if (dot > 4) | |
372 | goto error; | |
373 | } | |
374 | else | |
375 | { | |
376 | digit = 1; | |
377 | val_low *= 10; | |
378 | val_low += (*p - '0'); | |
379 | } | |
380 | p++; | |
381 | } | |
382 | ||
383 | /* Low digit part must be there. */ | |
384 | if (! digit || ! separator) | |
385 | goto error; | |
386 | ||
387 | /* Encode result into routing distinguisher. */ | |
388 | if (dot) | |
389 | { | |
390 | eval->val[0] = ECOMMUNITY_ENCODE_IP; | |
391 | eval->val[1] = 0; | |
392 | memcpy (&eval->val[2], &ip, sizeof (struct in_addr)); | |
393 | eval->val[6] = (val_low >> 8) & 0xff; | |
394 | eval->val[7] = val_low & 0xff; | |
395 | } | |
396 | else | |
397 | { | |
398 | eval->val[0] = ECOMMUNITY_ENCODE_AS; | |
399 | eval->val[1] = 0; | |
400 | eval->val[2] = (val_high >>8) & 0xff; | |
401 | eval->val[3] = val_high & 0xff; | |
402 | eval->val[4] = (val_low >>24) & 0xff; | |
403 | eval->val[5] = (val_low >>16) & 0xff; | |
404 | eval->val[6] = (val_low >>8) & 0xff; | |
405 | eval->val[7] = val_low & 0xff; | |
406 | } | |
407 | *token = ecommunity_token_val; | |
408 | return p; | |
409 | ||
410 | error: | |
411 | *token = ecommunity_token_unknown; | |
412 | return p; | |
413 | } | |
414 | ||
415 | /* Convert string to extended community attribute. | |
416 | ||
417 | When type is already known, please specify both str and type. str | |
418 | should not include keyword such as "rt" and "soo". Type is | |
419 | ECOMMUNITY_ROUTE_TARGET or ECOMMUNITY_SITE_ORIGIN. | |
420 | keyword_included should be zero. | |
421 | ||
422 | For example route-map's "set extcommunity" command case: | |
423 | ||
424 | "rt 100:1 100:2 100:3" -> str = "100:1 100:2 100:3" | |
425 | type = ECOMMUNITY_ROUTE_TARGET | |
426 | keyword_included = 0 | |
427 | ||
428 | "soo 100:1" -> str = "100:1" | |
429 | type = ECOMMUNITY_SITE_ORIGIN | |
430 | keyword_included = 0 | |
431 | ||
432 | When string includes keyword for each extended community value. | |
433 | Please specify keyword_included as non-zero value. | |
434 | ||
435 | For example standard extcommunity-list case: | |
436 | ||
437 | "rt 100:1 rt 100:2 soo 100:1" -> str = "rt 100:1 rt 100:2 soo 100:1" | |
438 | type = 0 | |
439 | keyword_include = 1 | |
440 | */ | |
441 | struct ecommunity * | |
442 | ecommunity_str2com (char *str, int type, int keyword_included) | |
443 | { | |
444 | struct ecommunity *ecom = NULL; | |
445 | enum ecommunity_token token; | |
446 | struct ecommunity_val eval; | |
447 | int keyword = 0; | |
448 | ||
449 | while ((str = ecommunity_gettoken (str, &eval, &token))) | |
450 | { | |
451 | switch (token) | |
452 | { | |
453 | case ecommunity_token_rt: | |
454 | case ecommunity_token_soo: | |
455 | if (! keyword_included || keyword) | |
456 | { | |
457 | if (ecom) | |
458 | ecommunity_free (ecom); | |
459 | return NULL; | |
460 | } | |
461 | keyword = 1; | |
462 | ||
463 | if (token == ecommunity_token_rt) | |
464 | { | |
465 | type = ECOMMUNITY_ROUTE_TARGET; | |
466 | } | |
467 | if (token == ecommunity_token_soo) | |
468 | { | |
469 | type = ECOMMUNITY_SITE_ORIGIN; | |
470 | } | |
471 | break; | |
472 | case ecommunity_token_val: | |
473 | if (keyword_included) | |
474 | { | |
475 | if (! keyword) | |
476 | { | |
477 | if (ecom) | |
478 | ecommunity_free (ecom); | |
479 | return NULL; | |
480 | } | |
481 | keyword = 0; | |
482 | } | |
483 | if (ecom == NULL) | |
484 | ecom = ecommunity_new (); | |
485 | eval.val[1] = type; | |
486 | ecommunity_add_val (ecom, &eval); | |
487 | break; | |
488 | case ecommunity_token_unknown: | |
489 | default: | |
490 | if (ecom) | |
491 | ecommunity_free (ecom); | |
492 | return NULL; | |
493 | break; | |
494 | } | |
495 | } | |
496 | return ecom; | |
497 | } | |
498 | ||
499 | /* Convert extended community attribute to string. | |
500 | ||
501 | Due to historical reason of industry standard implementation, there | |
502 | are three types of format. | |
503 | ||
504 | route-map set extcommunity format | |
505 | "rt 100:1 100:2" | |
506 | "soo 100:3" | |
507 | ||
508 | extcommunity-list | |
509 | "rt 100:1 rt 100:2 soo 100:3" | |
510 | ||
511 | "show ip bgp" and extcommunity-list regular expression matching | |
512 | "RT:100:1 RT:100:2 SoO:100:3" | |
513 | ||
514 | For each formath please use below definition for format: | |
515 | ||
516 | ECOMMUNITY_FORMAT_ROUTE_MAP | |
517 | ECOMMUNITY_FORMAT_COMMUNITY_LIST | |
518 | ECOMMUNITY_FORMAT_DISPLAY | |
519 | */ | |
520 | char * | |
521 | ecommunity_ecom2str (struct ecommunity *ecom, int format) | |
522 | { | |
523 | int i; | |
524 | u_char *pnt; | |
525 | int encode = 0; | |
526 | int type = 0; | |
527 | #define ECOMMUNITY_STR_DEFAULT_LEN 26 | |
528 | int str_size; | |
529 | int str_pnt; | |
530 | u_char *str_buf; | |
531 | char *prefix; | |
532 | int len = 0; | |
533 | int first = 1; | |
534 | ||
535 | /* For parse Extended Community attribute tupple. */ | |
536 | struct ecommunity_as | |
537 | { | |
538 | as_t as; | |
539 | u_int32_t val; | |
540 | } eas; | |
541 | ||
542 | struct ecommunity_ip | |
543 | { | |
544 | struct in_addr ip; | |
545 | u_int16_t val; | |
546 | } eip; | |
547 | ||
548 | if (ecom->size == 0) | |
549 | { | |
550 | str_buf = XMALLOC (MTYPE_ECOMMUNITY_STR, 1); | |
551 | str_buf[0] = '\0'; | |
552 | return str_buf; | |
553 | } | |
554 | ||
555 | /* Prepare buffer. */ | |
556 | str_buf = XMALLOC (MTYPE_ECOMMUNITY_STR, ECOMMUNITY_STR_DEFAULT_LEN + 1); | |
557 | str_size = ECOMMUNITY_STR_DEFAULT_LEN + 1; | |
558 | str_pnt = 0; | |
559 | ||
560 | for (i = 0; i < ecom->size; i++) | |
561 | { | |
562 | pnt = ecom->val + (i * 8); | |
563 | ||
564 | /* High-order octet of type. */ | |
565 | encode = *pnt++; | |
566 | if (encode != ECOMMUNITY_ENCODE_AS && encode != ECOMMUNITY_ENCODE_IP) | |
567 | { | |
568 | if (str_buf) | |
569 | XFREE (MTYPE_ECOMMUNITY_STR, str_buf); | |
570 | return "Unknown"; | |
571 | } | |
572 | ||
573 | /* Low-order octet of type. */ | |
574 | type = *pnt++; | |
575 | if (type != ECOMMUNITY_ROUTE_TARGET && type != ECOMMUNITY_SITE_ORIGIN) | |
576 | { | |
577 | if (str_buf) | |
578 | XFREE (MTYPE_ECOMMUNITY_STR, str_buf); | |
579 | return "Unknown"; | |
580 | } | |
581 | ||
582 | switch (format) | |
583 | { | |
584 | case ECOMMUNITY_FORMAT_COMMUNITY_LIST: | |
585 | prefix = (type == ECOMMUNITY_ROUTE_TARGET ? "rt " : "soo "); | |
586 | break; | |
587 | case ECOMMUNITY_FORMAT_DISPLAY: | |
588 | prefix = (type == ECOMMUNITY_ROUTE_TARGET ? "RT:" : "SoO:"); | |
589 | break; | |
590 | case ECOMMUNITY_FORMAT_ROUTE_MAP: | |
591 | prefix = ""; | |
592 | break; | |
593 | default: | |
594 | if (str_buf) | |
595 | XFREE (MTYPE_ECOMMUNITY_STR, str_buf); | |
596 | return "Unknown"; | |
597 | break; | |
598 | } | |
599 | ||
600 | /* Make it sure size is enough. */ | |
601 | while (str_pnt + ECOMMUNITY_STR_DEFAULT_LEN >= str_size) | |
602 | { | |
603 | str_size *= 2; | |
604 | str_buf = XREALLOC (MTYPE_ECOMMUNITY_STR, str_buf, str_size); | |
605 | } | |
606 | ||
607 | /* Space between each value. */ | |
608 | if (! first) | |
609 | str_buf[str_pnt++] = ' '; | |
610 | ||
611 | /* Put string into buffer. */ | |
612 | if (encode == ECOMMUNITY_ENCODE_AS) | |
613 | { | |
614 | eas.as = (*pnt++ << 8); | |
615 | eas.as |= (*pnt++); | |
616 | ||
617 | eas.val = (*pnt++ << 24); | |
618 | eas.val |= (*pnt++ << 16); | |
619 | eas.val |= (*pnt++ << 8); | |
620 | eas.val |= (*pnt++); | |
621 | ||
622 | len = sprintf (str_buf + str_pnt, "%s%d:%d", prefix, | |
623 | eas.as, eas.val); | |
624 | str_pnt += len; | |
625 | first = 0; | |
626 | } | |
627 | else if (encode == ECOMMUNITY_ENCODE_IP) | |
628 | { | |
629 | memcpy (&eip.ip, pnt, 4); | |
630 | pnt += 4; | |
631 | eip.val = (*pnt++ << 8); | |
632 | eip.val |= (*pnt++); | |
633 | ||
634 | len = sprintf (str_buf + str_pnt, "%s%s:%d", prefix, | |
635 | inet_ntoa (eip.ip), eip.val); | |
636 | str_pnt += len; | |
637 | first = 0; | |
638 | } | |
639 | } | |
640 | return str_buf; | |
641 | } |