1 /* 2 * CDDL HEADER START 3 * 4 * The contents of this file are subject to the terms of the 5 * Common Development and Distribution License, Version 1.0 only 6 * (the "License"). You may not use this file except in compliance 7 * with the License. 8 * 9 * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE 10 * or http://www.opensolaris.org/os/licensing. 11 * See the License for the specific language governing permissions 12 * and limitations under the License. 13 * 14 * When distributing Covered Code, include this CDDL HEADER in each 15 * file and include the License file at usr/src/OPENSOLARIS.LICENSE. 16 * If applicable, add the following below this CDDL HEADER, with the 17 * fields enclosed by brackets "[]" replaced with your own identifying 18 * information: Portions Copyright [yyyy] [name of copyright owner] 19 * 20 * CDDL HEADER END 21 */ 22 /* 23 * Copyright 2004 Sun Microsystems, Inc. All rights reserved. 24 * Use is subject to license terms. 25 */ 26 27 #pragma ident "%Z%%M% %I% %E% SMI" 28 29 /* 30 * Public utilities and convenience calls (from the API spec): 31 * SLPFindScopes (queries for all known scopes) 32 * SLPEscape / Unescape 33 * SLPFree 34 * SLPSet/GetProperty 35 */ 36 37 #include <stdio.h> 38 #include <stdlib.h> 39 #include <string.h> 40 #include <syslog.h> 41 #include <netdb.h> 42 #include <unistd.h> 43 #include <libintl.h> 44 #include <slp-internal.h> 45 46 struct scopes_tree { 47 void *scopes; 48 int len; 49 }; 50 51 typedef SLPBoolean SLPScopeCallback(SLPHandle, const char *, SLPError, void *); 52 53 static SLPSrvURLCallback collate_scopes; 54 static void collect_scopes(void *, VISIT, int, void *); 55 static SLPBoolean unpackSAAdvert_scope(slp_handle_impl_t *, char *, 56 SLPScopeCallback, void *, 57 void **, int *); 58 static SLPError SAAdvert_for_scopes(SLPHandle, void **); 59 static SLPError slp_unescape(const char *, char **, SLPBoolean, const char); 60 61 /* 62 * Finds scopes according the the user administrative model. 63 */ 64 SLPError SLPFindScopes(SLPHandle hSLP, char **ppcScopes) { 65 SLPError err; 66 char *reply, *unesc_reply; 67 void *stree = NULL; 68 void *collator = NULL; 69 70 if (!hSLP || !ppcScopes) { 71 return (SLP_PARAMETER_BAD); 72 } 73 74 /* first try administratively configured scopes */ 75 if ((err = slp_administrative_scopes(ppcScopes, SLP_FALSE)) 76 != SLP_OK) { 77 return (err); 78 } 79 80 if (*ppcScopes) { 81 /* got scopes */ 82 return (SLP_OK); 83 } 84 85 /* DAs from active and passive discovery */ 86 if ((err = slp_find_das("", &reply)) != SLP_OK && 87 err != SLP_NETWORK_ERROR) 88 return (err); 89 90 /* Unpack the reply */ 91 if (reply) { 92 int numResults = 0; /* placeholder; not actually used */ 93 94 /* tag call as internal */ 95 ((slp_handle_impl_t *)hSLP)->internal_call = SLP_TRUE; 96 97 (void) slp_unpackSrvReply( 98 hSLP, reply, collate_scopes, 99 &stree, &collator, &numResults); 100 /* invoke last call */ 101 (void) slp_unpackSrvReply( 102 hSLP, NULL, collate_scopes, 103 &stree, &collator, &numResults); 104 free(reply); 105 106 /* revert internal call tag */ 107 ((slp_handle_impl_t *)hSLP)->internal_call = SLP_FALSE; 108 } 109 110 /* Finally, if no results yet, try SA discovery */ 111 if (!stree) { 112 (void) SAAdvert_for_scopes(hSLP, &stree); 113 } 114 115 if (!stree) { 116 /* found none, so just return "default" */ 117 if (!(*ppcScopes = strdup("default"))) { 118 slp_err(LOG_CRIT, 0, "SLPFindScopes", "out of memory"); 119 return (SLP_MEMORY_ALLOC_FAILED); 120 } 121 return (SLP_OK); 122 } 123 124 /* we now have a btree, each leaf of which is a unique scope */ 125 slp_twalk(stree, collect_scopes, 0, (void *) ppcScopes); 126 127 /* unescape scopes list */ 128 if ((err = slp_unescape(*ppcScopes, &unesc_reply, SLP_FALSE, '%')) 129 == SLP_OK) { 130 free(*ppcScopes); 131 *ppcScopes = unesc_reply; 132 } else { 133 free(unesc_reply); 134 } 135 136 return (err); 137 } 138 139 /* 140 * Finds scopes according to the adminstrative scoping model. A 141 * comma-seperated list of scopes is returned in *ppcScopes; the 142 * caller must free *ppcScopes. 143 * If the return_default parameter is true, and no scopes are found, 144 * *ppcScopes will be set to 'default', otherwise, *ppcScopes will 145 * be NULL. This helps simplify internal memory management. 146 */ 147 SLPError slp_administrative_scopes(char **ppcScopes, 148 SLPBoolean return_default) { 149 const char *useScopes; 150 151 *ppcScopes = NULL; 152 153 /* @@@ first try DHCP */ 154 /* next try the useScopes property */ 155 useScopes = SLPGetProperty(SLP_CONFIG_USESCOPES); 156 157 if (useScopes && *useScopes) { 158 if (!(*ppcScopes = strdup(useScopes))) { 159 slp_err(LOG_CRIT, 0, "SLPFindScopes", "out of memory"); 160 return (SLP_MEMORY_ALLOC_FAILED); 161 } 162 return (SLP_OK); 163 } 164 165 /* found none, so just return "default" */ 166 if (return_default && !(*ppcScopes = strdup("default"))) { 167 slp_err(LOG_CRIT, 0, "SLPFindScopes", "out of memory"); 168 return (SLP_MEMORY_ALLOC_FAILED); 169 } 170 return (SLP_OK); 171 } 172 173 /* 174 * This function operates on the same btree as the collate_scopes(). 175 * The difference is that this one is called for each 176 * SAAdvert recieved. 177 */ 178 /* ARGSUSED */ 179 static SLPBoolean saadvert_callback(SLPHandle hp, char *scopes, 180 SLPError err, void **stree) { 181 char *s, *tstate; 182 183 if (err != SLP_OK) { 184 return (SLP_TRUE); 185 } 186 187 for ( 188 s = strtok_r((char *)scopes, ",", &tstate); 189 s; 190 s = strtok_r(NULL, ",", &tstate)) { 191 192 char *ascope, **srch; 193 194 if (!(ascope = strdup(s))) { /* no memory! */ 195 slp_err(LOG_CRIT, 0, "collate_scopes", 196 "out of memory"); 197 return (SLP_TRUE); 198 } 199 200 srch = slp_tsearch( 201 (void *) ascope, stree, 202 (int (*)(const void *, const void *)) slp_strcasecmp); 203 if (*srch != ascope) 204 /* scope is already in there, so just free ascope */ 205 free(ascope); 206 } 207 208 return (SLP_TRUE); 209 } 210 211 /* 212 * Generates an SAAdvert solicitation, and returns any scopes found 213 * from all recieved SAAdverts in stree. stree must be a btree 214 * structure. 215 */ 216 static SLPError SAAdvert_for_scopes(SLPHandle hSLP, void **stree) { 217 SLPError err; 218 SLPBoolean sync_state; 219 slp_handle_impl_t *hp = (slp_handle_impl_t *)hSLP; 220 char *predicate; 221 const char *type_hint; 222 223 /* get type hint, if set */ 224 if ((type_hint = SLPGetProperty(SLP_CONFIG_TYPEHINT)) != NULL && 225 *type_hint != 0) { 226 227 size_t hintlen = strlen(type_hint); 228 size_t predlen = strlen("(service-type=)"); 229 230 /* check bounds */ 231 if (hintlen > (SLP_MAX_STRINGLEN - predlen)) { 232 return (SLP_PARAMETER_BAD); 233 } 234 if (!(predicate = malloc(hintlen + predlen + 1))) { 235 slp_err(LOG_CRIT, 0, "SAAdvert_for_scopes", 236 "out of memory"); 237 return (SLP_MEMORY_ALLOC_FAILED); 238 } 239 (void) strcpy(predicate, "(service-type="); 240 (void) strcat(predicate, type_hint); 241 (void) strcat(predicate, ")"); 242 } else { 243 predicate = ""; 244 type_hint = NULL; 245 } 246 247 /* No callback for SLPFindScopes, so force synchronous mode only */ 248 sync_state = hp->async; 249 hp->async = SLP_FALSE; 250 251 if ((err = slp_start_call(hp)) != SLP_OK) 252 return (err); 253 254 err = slp_packSrvRqst("service:service-agent", predicate, hp); 255 256 if (err == SLP_OK) 257 err = slp_ua_common(hSLP, "", 258 (SLPGenericAppCB *)saadvert_callback, 259 stree, 260 (SLPMsgReplyCB *)unpackSAAdvert_scope); 261 262 if (type_hint) { 263 free(predicate); 264 } 265 266 if (err != SLP_OK) 267 slp_end_call(hp); 268 269 /* restore sync state */ 270 hp->async = sync_state; 271 272 return (err); 273 } 274 275 /* 276 * Unpack an SAAdvert and pass each set of scopes into cb. 277 */ 278 /* ARGSUSED */ 279 static SLPBoolean unpackSAAdvert_scope(slp_handle_impl_t *hSLP, char *reply, 280 SLPScopeCallback cb, void *cookie, 281 void **collator, int *numResults) { 282 char *surl, *scopes, *attrs; 283 SLPBoolean cont; 284 285 if (!reply) { 286 cb(hSLP, NULL, SLP_LAST_CALL, cookie); 287 return (SLP_FALSE); 288 } 289 290 /* tag call as internal; gets all scopes, regardless of maxResults */ 291 hSLP->internal_call = SLP_TRUE; 292 293 if (slp_unpackSAAdvert(reply, &surl, &scopes, &attrs) != SLP_OK) { 294 return (SLP_TRUE); 295 } 296 297 cont = cb(hSLP, scopes, SLP_OK, cookie); 298 299 /* revert internal_call tag */ 300 hSLP->internal_call = SLP_FALSE; 301 302 free(surl); 303 free(scopes); 304 free(attrs); 305 306 return (cont); 307 } 308 309 /* 310 * Creates a service request for finding DAs or SAs (based on 'filter'), 311 * and sends it to slpd, returning the reply in 'reply'. 312 */ 313 SLPError slp_find_das(const char *filter, char **reply) { 314 SLPError err; 315 char *msg, hostname[MAXHOSTNAMELEN]; 316 317 /* Try the cache first */ 318 if (*reply = slp_find_das_cached(filter)) { 319 return (SLP_OK); 320 } 321 322 /* 323 * create a find scopes message: 324 * this is a SrvRqst for the type directory-agent.sun. 325 */ 326 /* use the local host's name for the scope */ 327 (void) gethostname(hostname, MAXHOSTNAMELEN); 328 329 err = slp_packSrvRqst_single( 330 SLP_SUN_DA_TYPE, hostname, filter, &msg, "en"); 331 332 if (err == SLP_OK) { 333 err = slp_send2slpd(msg, reply); 334 free(msg); 335 } 336 337 /* Add the reply to the cache */ 338 if (err == SLP_OK) { 339 slp_put_das_cached(filter, *reply, slp_get_length(*reply)); 340 } 341 342 return (err); 343 } 344 345 /* 346 * This is called for each URL entry in the DA service reply (sun private). 347 * Contained within the cookie is a btree, to which it adds new 348 * scopes from the URL entry. The scopes are retrieved from the btree 349 * by traversing the tree in SLPFindScopes(). 350 * SLPHandle h is NULL, so don't touch it! 351 */ 352 /*ARGSUSED*/ 353 static SLPBoolean collate_scopes(SLPHandle h, const char *u, 354 unsigned short lifetime, 355 SLPError errCode, void *cookie) { 356 SLPSrvURL *surl; 357 char *s, *tstate, *p, *url; 358 void **collator = cookie; 359 360 if (errCode != SLP_OK) 361 return (SLP_TRUE); 362 363 /* dup url so as not to corrupt da cache */ 364 if (!(url = strdup(u))) { 365 slp_err(LOG_CRIT, 0, "collate_scopes", "out of memory"); 366 return (SLP_FALSE); 367 } 368 369 /* parse url into a SLPSrvURL struct */ 370 if (SLPParseSrvURL(url, &surl) != SLP_OK) 371 return (SLP_TRUE); /* bad URL; skip it */ 372 373 /* collate the scopes using the btree stree->scopes: */ 374 /* skip the 'scopes=' part */ 375 if (!(p = strchr(surl->s_pcSrvPart, '='))) { 376 free(surl); 377 return (SLP_TRUE); /* bad URL; skip it */ 378 } 379 p++; 380 381 for ( 382 s = strtok_r(p, ",", &tstate); 383 s; 384 s = strtok_r(NULL, ",", &tstate)) { 385 386 char *ascope, **srch; 387 388 if (!(ascope = strdup(s))) { /* no memory! */ 389 slp_err(LOG_CRIT, 0, "collate_scopes", 390 "out of memory"); 391 free(surl); 392 return (SLP_TRUE); 393 } 394 395 srch = slp_tsearch( 396 (void *) ascope, collator, 397 (int (*)(const void *, const void *)) slp_strcasecmp); 398 if (*srch != ascope) 399 /* scope is already in there, so just free ascope */ 400 free(ascope); 401 } 402 403 free(url); 404 free(surl); 405 406 return (SLP_TRUE); 407 } 408 409 /* 410 * Each time we visit a node for the last time, copy that scope into 411 * the scope collection and free the scope string and the node. 412 */ 413 /*ARGSUSED*/ 414 static void collect_scopes(void *node, VISIT order, int level, void *cookie) { 415 char **scopes = (char **)cookie; 416 417 if (order == endorder || order == leaf) { 418 char *s = *(char **)node; 419 slp_add2list(s, scopes, SLP_FALSE); 420 free(s); 421 free(node); 422 } 423 } 424 425 void SLPFree(void *pvMem) { 426 if (pvMem) 427 free(pvMem); 428 } 429 430 /* 431 * Escape / Unescape 432 */ 433 434 #define isBadTagChar(c) ((c) == '*' || (c) == '_' || \ 435 (c) == '\n' || (c) == '\t' || (c) == '\r') 436 437 #define isReserved(c) ((c) <= 31 || (c) == '(' || (c) == ')' || \ 438 (c) == ',' || (c) == '\\' || (c) == '!' || \ 439 (c) == '<' || (c) == '=' || (c) == '>' || \ 440 (c) == '~') 441 442 SLPError SLPEscape(const char *pcInbuf, char **ppcOutBuf, SLPBoolean isTag) { 443 char *buf, *pin, *pout; 444 445 if (!pcInbuf || !ppcOutBuf) 446 return (SLP_PARAMETER_BAD); 447 448 if (!(buf = malloc(strlen(pcInbuf) * 3 + 1))) { 449 slp_err(LOG_CRIT, 0, "SLPEscape", "out of memory"); 450 return (SLP_MEMORY_ALLOC_FAILED); 451 } 452 *ppcOutBuf = buf; 453 454 for (pin = (char *)pcInbuf, pout = buf; *pin; ) { 455 int len; 456 457 /* If char is start of multibyte char, just copy it in */ 458 if ((len = mblen(pin, MB_CUR_MAX)) > 1) { 459 int i; 460 for (i = 0; i < len && *pin; i++) 461 *pout++ = *pin++; 462 continue; 463 } 464 465 /* check for bad tag */ 466 if (isTag && isBadTagChar(*pin)) 467 return (SLP_PARSE_ERROR); 468 469 if (isReserved(*pin)) { 470 if (isTag) 471 return (SLP_PARSE_ERROR); 472 (void) sprintf(pout, "\\%.2x", *pin); 473 pout += 3; 474 pin++; 475 } else { 476 *pout++ = *pin++; 477 } 478 } 479 *pout = 0; 480 481 return (SLP_OK); 482 } 483 484 SLPError SLPUnescape(const char *pcInbuf, char **ppcOutBuf, SLPBoolean isTag) { 485 if (!pcInbuf || !ppcOutBuf) 486 return (SLP_PARAMETER_BAD); 487 488 return (slp_unescape(pcInbuf, ppcOutBuf, isTag, '\\')); 489 } 490 491 492 /* 493 * The actual unescaping routine; allows for different escape chars. 494 */ 495 static SLPError slp_unescape(const char *pcInbuf, char **ppcOutBuf, 496 SLPBoolean isTag, const char esc_char) { 497 char *buf, *pin, *pout, conv[3]; 498 499 if (!(buf = malloc(strlen(pcInbuf) * 3 + 1))) { 500 slp_err(LOG_CRIT, 0, "SLPEscape", "out of memory"); 501 return (SLP_MEMORY_ALLOC_FAILED); 502 } 503 *ppcOutBuf = buf; 504 505 conv[2] = 0; 506 for (pin = (char *)pcInbuf, pout = buf; *pin; ) { 507 int len; 508 509 /* If char is start of multibyte char, just copy it in */ 510 if ((len = mblen(pin, MB_CUR_MAX)) > 1) { 511 int i; 512 for (i = 0; i < len && *pin; i++) 513 *pout++ = *pin++; 514 continue; 515 } 516 517 if (*pin == esc_char) { 518 if (!pin[1] || !pin[2]) 519 return (SLP_PARSE_ERROR); 520 pin++; 521 conv[0] = *pin++; 522 conv[1] = *pin++; 523 *pout++ = (char)strtol(conv, NULL, 16); 524 if (isTag && isBadTagChar(*pout)) 525 return (SLP_PARSE_ERROR); 526 } else { 527 *pout++ = *pin++; 528 } 529 } 530 *pout = 0; 531 532 return (SLP_OK); 533 } 534 535 /* 536 * Properties 537 * 538 * All properties are stored in a global tree (prop_table). This 539 * tree is created and accessed by slp_tsearch and slp_tfind. 540 */ 541 struct prop_entry { 542 const char *key, *val; 543 }; 544 typedef struct prop_entry slp_prop_entry_t; 545 546 /* Global properties table */ 547 static void *slp_props = NULL; 548 static mutex_t prop_table_lock = DEFAULTMUTEX; 549 550 static void setDefaults(); 551 552 static int compare_props(const void *a, const void *b) { 553 return (strcmp( 554 ((slp_prop_entry_t *)a)->key, 555 ((slp_prop_entry_t *)b)->key)); 556 } 557 558 void SLPSetProperty(const char *pcName, const char *pcValue) { 559 slp_prop_entry_t *pe, **pe2; 560 561 if (!slp_props) setDefaults(); 562 563 if (!pcName || !pcValue) { 564 return; 565 } 566 567 if (!(pe = malloc(sizeof (*pe)))) { 568 slp_err(LOG_CRIT, 0, "SLPSetProperty", "out of memory"); 569 return; 570 } 571 572 /* place the strings under library ownership */ 573 if (!(pe->key = strdup(pcName))) { 574 free(pe); 575 slp_err(LOG_CRIT, 0, "SLPSetProperty", "out of memory"); 576 return; 577 } 578 579 if (!(pe->val = strdup(pcValue))) { 580 free((void *) pe->key); 581 free(pe); 582 slp_err(LOG_CRIT, 0, "SLPSetProperty", "out of memory"); 583 return; 584 } 585 586 /* is pcName already set? */ 587 (void) mutex_lock(&prop_table_lock); 588 pe2 = slp_tsearch((void *) pe, &slp_props, compare_props); 589 if (pe != *pe2) { 590 /* this prop is already set; overwrite the old value */ 591 free((void *) (*pe2)->val); 592 (*pe2)->val = pe->val; 593 free((void *) pe->key); 594 free(pe); 595 } 596 (void) mutex_unlock(&prop_table_lock); 597 } 598 599 const char *SLPGetProperty(const char *pcName) { 600 slp_prop_entry_t pe[1], **ans; 601 602 if (!slp_props) setDefaults(); 603 604 if (!pcName) { 605 return (NULL); 606 } 607 608 pe->key = pcName; 609 610 (void) mutex_lock(&prop_table_lock); 611 ans = slp_tfind(pe, &slp_props, compare_props); 612 (void) mutex_unlock(&prop_table_lock); 613 if (ans) 614 return ((*ans)->val); 615 return (NULL); 616 } 617 618 static void setDefaults() { 619 slp_prop_entry_t *pe; 620 static mutex_t lock = DEFAULTMUTEX; 621 622 (void) mutex_lock(&lock); 623 if (slp_props) { 624 (void) mutex_unlock(&lock); 625 return; 626 } 627 628 pe = malloc(sizeof (*pe)); 629 pe->key = strdup(SLP_CONFIG_ISBROADCASTONLY); 630 pe->val = strdup("false"); 631 (void) slp_tsearch((void *) pe, &slp_props, compare_props); 632 633 pe = malloc(sizeof (*pe)); 634 pe->key = strdup(SLP_CONFIG_MULTICASTTTL); 635 pe->val = strdup("255"); 636 (void) slp_tsearch((void *) pe, &slp_props, compare_props); 637 638 pe = malloc(sizeof (*pe)); 639 pe->key = strdup(SLP_CONFIG_MULTICASTMAXWAIT); 640 pe->val = strdup("15000"); 641 (void) slp_tsearch((void *) pe, &slp_props, compare_props); 642 643 pe = malloc(sizeof (*pe)); 644 pe->key = strdup(SLP_CONFIG_DATAGRAMTIMEOUTS); 645 pe->val = strdup("2000,2000,2000"); 646 (void) slp_tsearch((void *) pe, &slp_props, compare_props); 647 648 pe = malloc(sizeof (*pe)); 649 pe->key = strdup(SLP_CONFIG_MULTICASTTIMEOUTS); 650 pe->val = strdup("1000,3000,3000,3000,3000"); 651 (void) slp_tsearch((void *) pe, &slp_props, compare_props); 652 653 pe = malloc(sizeof (*pe)); 654 pe->key = SLP_CONFIG_MTU; pe->val = "1400"; 655 (void) slp_tsearch((void *) pe, &slp_props, compare_props); 656 657 pe = malloc(sizeof (*pe)); 658 pe->key = strdup(SLP_CONFIG_MAXRESULTS); 659 pe->val = strdup("-1"); 660 (void) slp_tsearch((void *) pe, &slp_props, compare_props); 661 662 pe = malloc(sizeof (*pe)); 663 pe->key = strdup(SLP_CONFIG_SECURITY_ON); 664 pe->val = strdup("false"); 665 (void) slp_tsearch((void *) pe, &slp_props, compare_props); 666 667 pe = malloc(sizeof (*pe)); 668 pe->key = strdup(SLP_CONFIG_BYPASS_AUTH); 669 pe->val = strdup("false"); 670 (void) slp_tsearch((void *) pe, &slp_props, compare_props); 671 672 slp_readConfig(); 673 674 (void) mutex_unlock(&lock); 675 } 676 677 static const char *error_strings[] = { 678 "OK", /* 0 */ 679 "Language not supported", /* -1 */ 680 "Parse error", /* -2 */ 681 "Invalid registration", /* -3 */ 682 "Scope not supported", /* -4 */ 683 "Invalid error number", /* -5 */ 684 "Authentication absent", /* -6 */ 685 "Authentication failed", /* -7 */ 686 "Invalid error number", /* -8 */ 687 "Invalid error number", /* -9 */ 688 "Invalid error number", /* -10 */ 689 "Invalid error number", /* -11 */ 690 "Invalid error number", /* -12 */ 691 "Invalid update", /* -13 */ 692 "Invalid error number", /* -14 */ 693 "Invalid error number", /* -15 */ 694 "Invalid error number", /* -16 */ 695 "Not implemented", /* -17 */ 696 "Buffer overflow", /* -18 */ 697 "Network timed out", /* -19 */ 698 "Network init failed", /* -20 */ 699 "Memory alloc failed", /* -21 */ 700 "Parameter bad", /* -22 */ 701 "Network error", /* -23 */ 702 "Internal system error", /* -24 */ 703 "Handle in use", /* -25 */ 704 "Type error" /* -26 */ 705 }; 706 707 #define SLP_MAX_ERR_CNT 26 708 709 const char *slp_strerror(SLPError err) { 710 int abserr; 711 const char *str; 712 713 if (err == SLP_LAST_CALL) { 714 str = "Last call"; 715 } else if (err == SLP_SECURITY_UNAVAILABLE) { 716 str = "Security Implementation Unavailable"; 717 } else { 718 abserr = abs(err); 719 if (abserr > SLP_MAX_ERR_CNT) { 720 str = "Invalid error number"; 721 } else { 722 str = error_strings[abserr]; 723 } 724 } 725 726 return (dgettext("libslp", str)); 727 } 728