1 /* $NetBSD: dict_open.c,v 1.3 2020/03/18 19:05:21 christos Exp $ */ 2 3 /*++ 4 /* NAME 5 /* dict_open 3 6 /* SUMMARY 7 /* low-level dictionary interface 8 /* SYNOPSIS 9 /* #include <dict.h> 10 /* 11 /* DICT *dict_open(dict_spec, open_flags, dict_flags) 12 /* const char *dict_spec; 13 /* int open_flags; 14 /* int dict_flags; 15 /* 16 /* DICT *dict_open3(dict_type, dict_name, open_flags, dict_flags) 17 /* const char *dict_type; 18 /* const char *dict_name; 19 /* int open_flags; 20 /* int dict_flags; 21 /* 22 /* int dict_put(dict, key, value) 23 /* DICT *dict; 24 /* const char *key; 25 /* const char *value; 26 /* 27 /* const char *dict_get(dict, key) 28 /* DICT *dict; 29 /* const char *key; 30 /* 31 /* int dict_del(dict, key) 32 /* DICT *dict; 33 /* const char *key; 34 /* 35 /* int dict_seq(dict, func, key, value) 36 /* DICT *dict; 37 /* int func; 38 /* const char **key; 39 /* const char **value; 40 /* 41 /* void dict_close(dict) 42 /* DICT *dict; 43 /* 44 /* typedef DICT *(*DICT_OPEN_FN) (const char *, int, int); 45 /* 46 /* dict_open_register(type, open) 47 /* const char *type; 48 /* DICT_OPEN_FN open; 49 /* 50 /* typedef DICT_OPEN_FN (*DICT_OPEN_EXTEND_FN)(const char *type); 51 /* 52 /* DICT_OPEN_EXTEND_FN dict_open_extend(call_back) 53 /* DICT_OPEN_EXTEND_FN call_back; 54 /* 55 /* ARGV *dict_mapnames() 56 /* 57 /* typedef ARGV *(*DICT_MAPNAMES_EXTEND_FN)(ARGV *names); 58 /* 59 /* DICT_MAPNAMES_EXTEND_FN dict_mapnames_extend(call_back) 60 /* DICT_MAPNAMES_EXTEND_FN call_back; 61 /* 62 /* int dict_isjmp(dict) 63 /* DICT *dict; 64 /* 65 /* int dict_setjmp(dict) 66 /* DICT *dict; 67 /* 68 /* int dict_longjmp(dict, val) 69 /* DICT *dict; 70 /* int val; 71 /* 72 /* void dict_type_override(dict, type) 73 /* DICT *dict; 74 /* const char *type; 75 /* DESCRIPTION 76 /* This module implements a low-level interface to multiple 77 /* physical dictionary types. 78 /* 79 /* dict_open() takes a type:name pair that specifies a dictionary type 80 /* and dictionary name, opens the dictionary, and returns a dictionary 81 /* handle. The \fIopen_flags\fR arguments are as in open(2). The 82 /* \fIdict_flags\fR are the bit-wise OR of zero or more of the following: 83 /* .IP DICT_FLAG_DUP_WARN 84 /* Warn about duplicate keys, if the underlying database does not 85 /* support duplicate keys. The default is to terminate with a fatal 86 /* error. 87 /* .IP DICT_FLAG_DUP_IGNORE 88 /* Ignore duplicate keys if the underlying database does not 89 /* support duplicate keys. The default is to terminate with a fatal 90 /* error. 91 /* .IP DICT_FLAG_DUP_REPLACE 92 /* Replace duplicate keys if the underlying database supports such 93 /* an operation. The default is to terminate with a fatal error. 94 /* .IP DICT_FLAG_TRY0NULL 95 /* With maps where this is appropriate, append no null byte to 96 /* keys and values. 97 /* When neither DICT_FLAG_TRY0NULL nor DICT_FLAG_TRY1NULL are 98 /* specified, the software guesses what format to use for reading; 99 /* and in the absence of definite information, a system-dependent 100 /* default is chosen for writing. 101 /* .IP DICT_FLAG_TRY1NULL 102 /* With maps where this is appropriate, append one null byte to 103 /* keys and values. 104 /* When neither DICT_FLAG_TRY0NULL nor DICT_FLAG_TRY1NULL are 105 /* specified, the software guesses what format to use for reading; 106 /* and in the absence of definite information, a system-dependent 107 /* default is chosen for writing. 108 /* .IP DICT_FLAG_LOCK 109 /* With maps where this is appropriate, acquire an exclusive lock 110 /* before writing, and acquire a shared lock before reading. 111 /* Release the lock when the operation completes. 112 /* .IP DICT_FLAG_OPEN_LOCK 113 /* The behavior of this flag depends on whether a database 114 /* sets the DICT_FLAG_MULTI_WRITER flag to indicate that it 115 /* is multi-writer safe. 116 /* 117 /* With databases that are not multi-writer safe, dict_open() 118 /* acquires a persistent exclusive lock, or it terminates with 119 /* a fatal run-time error. 120 /* 121 /* With databases that are multi-writer safe, dict_open() 122 /* downgrades the DICT_FLAG_OPEN_LOCK flag (persistent lock) 123 /* to DICT_FLAG_LOCK (temporary lock). 124 /* .IP DICT_FLAG_FOLD_FIX 125 /* With databases whose lookup fields are fixed-case strings, 126 /* fold the search string to lower case before accessing the 127 /* database. This includes hash:, cdb:, dbm:. nis:, ldap:, 128 /* *sql. WARNING: case folding is supported only for ASCII or 129 /* valid UTF-8. 130 /* .IP DICT_FLAG_FOLD_MUL 131 /* With databases where one lookup field can match both upper 132 /* and lower case, fold the search key to lower case before 133 /* accessing the database. This includes regexp: and pcre:. 134 /* WARNING: case folding is supported only for ASCII or valid 135 /* UTF-8. 136 /* .IP DICT_FLAG_FOLD_ANY 137 /* Short-hand for (DICT_FLAG_FOLD_FIX | DICT_FLAG_FOLD_MUL). 138 /* .IP DICT_FLAG_SYNC_UPDATE 139 /* With file-based maps, flush I/O buffers to file after each update. 140 /* Thus feature is not supported with some file-based dictionaries. 141 /* .IP DICT_FLAG_NO_REGSUB 142 /* Disallow regular expression substitution from the lookup string 143 /* into the lookup result, to block data injection attacks. 144 /* .IP DICT_FLAG_NO_PROXY 145 /* Disallow access through the unprivileged \fBproxymap\fR 146 /* service, to block privilege escalation attacks. 147 /* .IP DICT_FLAG_NO_UNAUTH 148 /* Disallow lookup mechanisms that lack any form of authentication, 149 /* to block privilege escalation attacks (example: tcp_table; 150 /* even NIS can be secured to some extent by requiring that 151 /* the server binds to a privileged port). 152 /* .IP DICT_FLAG_PARANOID 153 /* A combination of all the paranoia flags: DICT_FLAG_NO_REGSUB, 154 /* DICT_FLAG_NO_PROXY and DICT_FLAG_NO_UNAUTH. 155 /* .IP DICT_FLAG_BULK_UPDATE 156 /* Enable preliminary code for bulk-mode database updates. 157 /* The caller must create an exception handler with dict_jmp_alloc() 158 /* and must trap exceptions from the database client with dict_setjmp(). 159 /* .IP DICT_FLAG_DEBUG 160 /* Enable additional logging. 161 /* .IP DICT_FLAG_UTF8_REQUEST 162 /* With util_utf8_enable != 0, require that lookup/update/delete 163 /* keys and values are valid UTF-8. Skip a lookup/update/delete 164 /* request with a non-UTF-8 key, skip an update request with 165 /* a non-UTF-8 value, and fail a lookup request with a non-UTF-8 166 /* value. 167 /* .IP DICT_FLAG_SRC_RHS_IS_FILE 168 /* With dictionaries that are created from source text, each 169 /* value in the source of a dictionary specifies a list of 170 /* file names separated by comma and/or whitespace. The file 171 /* contents are concatenated with a newline inserted between 172 /* files, and the base64-encoded result is stored under the 173 /* key. 174 /* .sp 175 /* NOTE 1: it is up to the application to decode lookup results 176 /* with dict_file_lookup() or equivalent (this requires that 177 /* the dictionary is opened with DICT_FLAG_SRC_RHS_IS_FILE). 178 /* Decoding is not built into the normal dictionary lookup 179 /* method, because that would complicate dictionary nesting, 180 /* pipelining, and proxying. 181 /* .sp 182 /* NOTE 2: it is up to the application to convert file names 183 /* into base64-encoded file content before calling the dictionary 184 /* update method (see dict_file(3) for support). Automatic 185 /* file content encoding is available only when a dictionary 186 /* is created from source text. 187 /* .PP 188 /* Specify DICT_FLAG_NONE for no special processing. 189 /* 190 /* The dictionary types are as follows: 191 /* .IP environ 192 /* The process environment array. The \fIdict_name\fR argument is ignored. 193 /* .IP dbm 194 /* DBM file. 195 /* .IP hash 196 /* Berkeley DB file in hash format. 197 /* .IP btree 198 /* Berkeley DB file in btree format. 199 /* .IP nis 200 /* NIS map. Only read access is supported. 201 /* .IP nisplus 202 /* NIS+ map. Only read access is supported. 203 /* .IP netinfo 204 /* NetInfo table. Only read access is supported. 205 /* .IP ldap 206 /* LDAP ("light-weight" directory access protocol) database access. 207 /* .IP pcre 208 /* PERL-compatible regular expressions. 209 /* .IP regexp 210 /* POSIX-compatible regular expressions. 211 /* .IP texthash 212 /* Flat text in postmap(1) input format. 213 /* .PP 214 /* dict_open3() takes separate arguments for dictionary type and 215 /* name, but otherwise performs the same functions as dict_open(). 216 /* 217 /* The dict_get(), dict_put(), dict_del(), and dict_seq() 218 /* macros evaluate their first argument multiple times. 219 /* These names should have been in uppercase. 220 /* 221 /* dict_get() retrieves the value stored in the named dictionary 222 /* under the given key. A null pointer means the value was not found. 223 /* As with dict_lookup(), the result is owned by the lookup table 224 /* implementation. Make a copy if the result is to be modified, 225 /* or if the result is to survive multiple table lookups. 226 /* 227 /* dict_put() stores the specified key and value into the named 228 /* dictionary. A zero (DICT_STAT_SUCCESS) result means the 229 /* update was made. 230 /* 231 /* dict_del() removes a dictionary entry, and returns 232 /* DICT_STAT_SUCCESS in case of success. 233 /* 234 /* dict_seq() iterates over all members in the named dictionary. 235 /* func is define DICT_SEQ_FUN_FIRST (select first member) or 236 /* DICT_SEQ_FUN_NEXT (select next member). A zero (DICT_STAT_SUCCESS) 237 /* result means that an entry was found. 238 /* 239 /* dict_close() closes the specified dictionary and cleans up the 240 /* associated data structures. 241 /* 242 /* dict_open_register() adds support for a new dictionary type. 243 /* 244 /* dict_open_extend() registers a call-back function that looks 245 /* up the dictionary open() function for a type that is not 246 /* registered, or null in case of error. The result value is 247 /* the last previously-registered call-back or null. 248 /* 249 /* dict_mapnames() returns a sorted list with the names of all available 250 /* dictionary types. 251 /* 252 /* dict_mapnames_extend() registers a call-back function that 253 /* enumerates additional dictionary type names. The result 254 /* will be sorted by dict_mapnames(). The result value 255 /* is the last previously-registered call-back or null. 256 /* 257 /* dict_setjmp() saves processing context and makes that context 258 /* available for use with dict_longjmp(). Normally, dict_setjmp() 259 /* returns zero. A non-zero result means that dict_setjmp() 260 /* returned through a dict_longjmp() call; the result is the 261 /* \fIval\fR argument given to dict_longjmp(). dict_isjmp() 262 /* returns non-zero when dict_setjmp() and dict_longjmp() 263 /* are enabled for a given dictionary. 264 /* 265 /* NB: non-local jumps such as dict_longjmp() are not safe for 266 /* jumping out of any routine that manipulates DICT data. 267 /* longjmp() like calls are best avoided in signal handlers. 268 /* 269 /* dict_type_override() changes the symbolic dictionary type. 270 /* This is used by dictionaries whose internals are based on 271 /* some other dictionary type. 272 /* DIAGNOSTICS 273 /* Fatal error: open error, unsupported dictionary type, attempt to 274 /* update non-writable dictionary. 275 /* 276 /* The lookup routine returns non-null when the request is 277 /* satisfied. The update, delete and sequence routines return 278 /* zero (DICT_STAT_SUCCESS) when the request is satisfied. 279 /* The dict->errno value is non-zero only when the last operation 280 /* was not satisfied due to a dictionary access error. This 281 /* can have the following values: 282 /* .IP DICT_ERR_NONE(zero) 283 /* There was no dictionary access error. For example, the 284 /* request was satisfied, the requested information did not 285 /* exist in the dictionary, or the information already existed 286 /* when it should not exist (collision). 287 /* .IP DICT_ERR_RETRY(<0) 288 /* The dictionary was temporarily unavailable. This can happen 289 /* with network-based services. 290 /* .IP DICT_ERR_CONFIG(<0) 291 /* The dictionary was unavailable due to a configuration error. 292 /* .PP 293 /* Generally, a program is expected to test the function result 294 /* value for "success" first. If the operation was not successful, 295 /* a program is expected to test for a non-zero dict->error 296 /* status to distinguish between a data notfound/collision 297 /* condition or a dictionary access error. 298 /* LICENSE 299 /* .ad 300 /* .fi 301 /* The Secure Mailer license must be distributed with this software. 302 /* AUTHOR(S) 303 /* Wietse Venema 304 /* IBM T.J. Watson Research 305 /* P.O. Box 704 306 /* Yorktown Heights, NY 10598, USA 307 /* 308 /* Wietse Venema 309 /* Google, Inc. 310 /* 111 8th Avenue 311 /* New York, NY 10011, USA 312 /*--*/ 313 314 /* System library. */ 315 316 #include <sys_defs.h> 317 #include <string.h> 318 #include <stdlib.h> 319 320 /* Utility library. */ 321 322 #include <argv.h> 323 #include <mymalloc.h> 324 #include <msg.h> 325 #include <dict.h> 326 #include <dict_cdb.h> 327 #include <dict_env.h> 328 #include <dict_unix.h> 329 #include <dict_tcp.h> 330 #include <dict_sdbm.h> 331 #include <dict_dbm.h> 332 #include <dict_db.h> 333 #include <dict_lmdb.h> 334 #include <dict_nis.h> 335 #include <dict_nisplus.h> 336 #include <dict_ni.h> 337 #include <dict_pcre.h> 338 #include <dict_regexp.h> 339 #include <dict_static.h> 340 #include <dict_cidr.h> 341 #include <dict_ht.h> 342 #include <dict_thash.h> 343 #include <dict_sockmap.h> 344 #include <dict_fail.h> 345 #include <dict_pipe.h> 346 #include <dict_random.h> 347 #include <dict_union.h> 348 #include <dict_inline.h> 349 #include <stringops.h> 350 #include <split_at.h> 351 #include <htable.h> 352 #include <myflock.h> 353 354 /* 355 * lookup table for available map types. 356 */ 357 typedef struct { 358 char *type; 359 DICT_OPEN_FN open; 360 } DICT_OPEN_INFO; 361 362 static const DICT_OPEN_INFO dict_open_info[] = { 363 DICT_TYPE_ENVIRON, dict_env_open, 364 DICT_TYPE_HT, dict_ht_open, 365 DICT_TYPE_UNIX, dict_unix_open, 366 DICT_TYPE_TCP, dict_tcp_open, 367 #ifdef HAS_DBM 368 DICT_TYPE_DBM, dict_dbm_open, 369 #endif 370 #ifdef HAS_DB 371 DICT_TYPE_HASH, dict_hash_open, 372 DICT_TYPE_BTREE, dict_btree_open, 373 #endif 374 #ifdef HAS_NIS 375 DICT_TYPE_NIS, dict_nis_open, 376 #endif 377 #ifdef HAS_NISPLUS 378 DICT_TYPE_NISPLUS, dict_nisplus_open, 379 #endif 380 #ifdef HAS_NETINFO 381 DICT_TYPE_NETINFO, dict_ni_open, 382 #endif 383 #ifdef HAS_POSIX_REGEXP 384 DICT_TYPE_REGEXP, dict_regexp_open, 385 #endif 386 DICT_TYPE_STATIC, dict_static_open, 387 DICT_TYPE_CIDR, dict_cidr_open, 388 DICT_TYPE_THASH, dict_thash_open, 389 DICT_TYPE_SOCKMAP, dict_sockmap_open, 390 DICT_TYPE_FAIL, dict_fail_open, 391 DICT_TYPE_PIPE, dict_pipe_open, 392 DICT_TYPE_RANDOM, dict_random_open, 393 DICT_TYPE_UNION, dict_union_open, 394 DICT_TYPE_INLINE, dict_inline_open, 395 #ifndef USE_DYNAMIC_MAPS 396 #ifdef HAS_PCRE 397 DICT_TYPE_PCRE, dict_pcre_open, 398 #endif 399 #ifdef HAS_CDB 400 DICT_TYPE_CDB, dict_cdb_open, 401 #endif 402 #ifdef HAS_SDBM 403 DICT_TYPE_SDBM, dict_sdbm_open, 404 #endif 405 #ifdef HAS_LMDB 406 DICT_TYPE_LMDB, dict_lmdb_open, 407 #endif 408 #endif /* !USE_DYNAMIC_MAPS */ 409 0, 410 }; 411 412 static HTABLE *dict_open_hash; 413 414 /* 415 * Extension hooks. 416 */ 417 static DICT_OPEN_EXTEND_FN dict_open_extend_hook; 418 static DICT_MAPNAMES_EXTEND_FN dict_mapnames_extend_hook; 419 420 /* 421 * Workaround. 422 */ 423 DEFINE_DICT_LMDB_MAP_SIZE; 424 DEFINE_DICT_DB_CACHE_SIZE; 425 426 /* dict_open_init - one-off initialization */ 427 428 static void dict_open_init(void) 429 { 430 const char *myname = "dict_open_init"; 431 const DICT_OPEN_INFO *dp; 432 433 if (dict_open_hash != 0) 434 msg_panic("%s: multiple initialization", myname); 435 dict_open_hash = htable_create(10); 436 437 for (dp = dict_open_info; dp->type; dp++) 438 htable_enter(dict_open_hash, dp->type, (void *) dp); 439 } 440 441 /* dict_open - open dictionary */ 442 443 DICT *dict_open(const char *dict_spec, int open_flags, int dict_flags) 444 { 445 char *saved_dict_spec = mystrdup(dict_spec); 446 char *dict_name; 447 DICT *dict; 448 449 if ((dict_name = split_at(saved_dict_spec, ':')) == 0) 450 msg_fatal("open dictionary: expecting \"type:name\" form instead of \"%s\"", 451 dict_spec); 452 453 dict = dict_open3(saved_dict_spec, dict_name, open_flags, dict_flags); 454 myfree(saved_dict_spec); 455 return (dict); 456 } 457 458 459 /* dict_open3 - open dictionary */ 460 461 DICT *dict_open3(const char *dict_type, const char *dict_name, 462 int open_flags, int dict_flags) 463 { 464 const char *myname = "dict_open"; 465 DICT_OPEN_INFO *dp; 466 DICT_OPEN_FN open_fn; 467 DICT *dict; 468 469 if (*dict_type == 0 || *dict_name == 0) 470 msg_fatal("open dictionary: expecting \"type:name\" form instead of \"%s:%s\"", 471 dict_type, dict_name); 472 if (dict_open_hash == 0) 473 dict_open_init(); 474 if ((dp = (DICT_OPEN_INFO *) htable_find(dict_open_hash, dict_type)) == 0) { 475 if (dict_open_extend_hook != 0 476 && (open_fn = dict_open_extend_hook(dict_type)) != 0) { 477 dict_open_register(dict_type, open_fn); 478 dp = (DICT_OPEN_INFO *) htable_find(dict_open_hash, dict_type); 479 } 480 if (dp == 0) 481 return (dict_surrogate(dict_type, dict_name, open_flags, dict_flags, 482 "unsupported dictionary type: %s", dict_type)); 483 } 484 if ((dict = dp->open(dict_name, open_flags, dict_flags)) == 0) 485 return (dict_surrogate(dict_type, dict_name, open_flags, dict_flags, 486 "cannot open %s:%s: %m", dict_type, dict_name)); 487 if (msg_verbose) 488 msg_info("%s: %s:%s", myname, dict_type, dict_name); 489 /* XXX The choice between wait-for-lock or no-wait is hard-coded. */ 490 if (dict->flags & DICT_FLAG_OPEN_LOCK) { 491 if (dict->flags & DICT_FLAG_LOCK) 492 msg_panic("%s: attempt to open %s:%s with both \"open\" lock and \"access\" lock", 493 myname, dict_type, dict_name); 494 /* Multi-writer safe map: downgrade persistent lock to temporary. */ 495 if (dict->flags & DICT_FLAG_MULTI_WRITER) { 496 dict->flags &= ~DICT_FLAG_OPEN_LOCK; 497 dict->flags |= DICT_FLAG_LOCK; 498 } 499 /* Multi-writer unsafe map: acquire exclusive lock or bust. */ 500 else if (dict->lock(dict, MYFLOCK_OP_EXCLUSIVE | MYFLOCK_OP_NOWAIT) < 0) 501 msg_fatal("%s:%s: unable to get exclusive lock: %m", 502 dict_type, dict_name); 503 } 504 /* Last step: insert proxy for UTF-8 syntax checks and casefolding. */ 505 if ((dict->flags & DICT_FLAG_UTF8_ACTIVE) == 0 506 && DICT_NEED_UTF8_ACTIVATION(util_utf8_enable, dict_flags)) 507 dict = dict_utf8_activate(dict); 508 return (dict); 509 } 510 511 /* dict_open_register - register dictionary type */ 512 513 void dict_open_register(const char *type, DICT_OPEN_FN open) 514 { 515 const char *myname = "dict_open_register"; 516 DICT_OPEN_INFO *dp; 517 HTABLE_INFO *ht; 518 519 if (dict_open_hash == 0) 520 dict_open_init(); 521 if (htable_find(dict_open_hash, type)) 522 msg_panic("%s: dictionary type exists: %s", myname, type); 523 dp = (DICT_OPEN_INFO *) mymalloc(sizeof(*dp)); 524 dp->open = open; 525 ht = htable_enter(dict_open_hash, type, (void *) dp); 526 dp->type = ht->key; 527 } 528 529 /* dict_open_extend - register alternate dictionary search routine */ 530 531 DICT_OPEN_EXTEND_FN dict_open_extend(DICT_OPEN_EXTEND_FN new_cb) 532 { 533 DICT_OPEN_EXTEND_FN old_cb; 534 535 old_cb = dict_open_extend_hook; 536 dict_open_extend_hook = new_cb; 537 return (old_cb); 538 } 539 540 /* dict_sort_alpha_cpp - qsort() callback */ 541 542 static int dict_sort_alpha_cpp(const void *a, const void *b) 543 { 544 return (strcmp(((char **) a)[0], ((char **) b)[0])); 545 } 546 547 /* dict_mapnames - return an ARGV of available map_names */ 548 549 ARGV *dict_mapnames() 550 { 551 HTABLE_INFO **ht_info; 552 HTABLE_INFO **ht; 553 DICT_OPEN_INFO *dp; 554 ARGV *mapnames; 555 556 if (dict_open_hash == 0) 557 dict_open_init(); 558 mapnames = argv_alloc(dict_open_hash->used + 1); 559 for (ht_info = ht = htable_list(dict_open_hash); *ht; ht++) { 560 dp = (DICT_OPEN_INFO *) ht[0]->value; 561 argv_add(mapnames, dp->type, ARGV_END); 562 } 563 if (dict_mapnames_extend_hook != 0) 564 (void) dict_mapnames_extend_hook(mapnames); 565 qsort((void *) mapnames->argv, mapnames->argc, sizeof(mapnames->argv[0]), 566 dict_sort_alpha_cpp); 567 myfree((void *) ht_info); 568 argv_terminate(mapnames); 569 return mapnames; 570 } 571 572 /* dict_mapnames_extend - register alternate dictionary type list routine */ 573 574 DICT_MAPNAMES_EXTEND_FN dict_mapnames_extend(DICT_MAPNAMES_EXTEND_FN new_cb) 575 { 576 DICT_MAPNAMES_EXTEND_FN old_cb; 577 578 old_cb = dict_mapnames_extend_hook; 579 dict_mapnames_extend_hook = new_cb; 580 return (old_cb); 581 } 582 583 /* dict_type_override - disguise a dictionary type */ 584 585 void dict_type_override(DICT *dict, const char *type) 586 { 587 myfree(dict->type); 588 dict->type = mystrdup(type); 589 } 590 591 #ifdef TEST 592 593 /* 594 * Proof-of-concept test program. 595 */ 596 int main(int argc, char **argv) 597 { 598 dict_test(argc, argv); 599 return (0); 600 } 601 602 #endif 603