modules/rp/rp_search.c

/* [<][>]
[^][v][top][bottom][index][help] */

FUNCTIONS

This source file includes following functions.
  1. rp_exclude_datlink
  2. rp_preflist_search
  3. rp_find_smallest_span
  4. rp_leaf_occ_inc
  5. rp_exclude_exact_match
  6. rp_find_longest_prefix
  7. rp_asc_process_datlist
  8. rp_asc_append_datref
  9. rp_srch_copyresults
  10. rp_begend_preselection
  11. RP_asc_search

   1 /***************************************
   2   $Revision: 1.15 $
   3 
   4   Radix payload (rp) - user level functions for storing data in radix trees
   5 
   6   rp_search = search the loaded radix trees using an ascii key
   7 
   8               Motto: "And all that for inetnums..."
   9 
  10   Status: NOT REVIEWED, TESTED
  11   
  12   Design and implementation by: Marek Bukowy
  13   
  14   ******************/ /******************
  15   Copyright (c) 1999                              RIPE NCC
  16  
  17   All Rights Reserved
  18   
  19   Permission to use, copy, modify, and distribute this software and its
  20   documentation for any purpose and without fee is hereby granted,
  21   provided that the above copyright notice appear in all copies and that
  22   both that copyright notice and this permission notice appear in
  23   supporting documentation, and that the name of the author not be
  24   used in advertising or publicity pertaining to distribution of the
  25   software without specific, written prior permission.
  26   
  27   THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING
  28   ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS; IN NO EVENT SHALL
  29   AUTHOR BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY
  30   DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
  31   AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
  32   OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  33   ***************************************/
  34 
  35 
  36 #include <rp.h>
  37 
  38 static
  39 void
  40 rp_exclude_datlink(GList    **datlist, GList    *element)
     /* [<][>][^][v][top][bottom][index][help] */
  41 {
  42   /* remove element from list(becomes a self-consistent list) */
  43   *datlist = g_list_remove_link(*datlist, element);
  44   
  45   /* free it and the payload */
  46   wr_clear_list( &element );
  47 }
  48 
  49 
  50 /**************************************************************************/
  51 /*+++++++++++
  52    helper: 
  53    this routine goes through the list of prefixes and performs a bin_search
  54    on each of them; attaches the results to datlist.
  55 +++++++++++*/
  56 static
  57 er_ret_t
  58 rp_preflist_search (
     /* [<][>][^][v][top][bottom][index][help] */
  59                     rx_srch_mt search_mode, 
  60                     int par_a,
  61                     int par_b,
  62                     rx_tree_t  *mytree,
  63                     GList    **preflist,
  64                     GList    **datlist
  65                     )
  66 
  67 { 
  68   char   prefstr[IP_PREFSTR_MAX];
  69   GList   *qitem;
  70   ip_prefix_t *querypref;
  71   er_ret_t err;
  72   
  73   for( qitem = g_list_first(*preflist);
  74        qitem != NULL;
  75        qitem = g_list_next(qitem)) {
  76     
  77     querypref = qitem->data;
  78     
  79     if( IP_pref_b2a( querypref, prefstr, IP_PREFSTR_MAX) != IP_OK ) {
  80       die;
  81     }
  82     ER_dbg_va(FAC_RP, ASP_RP_SRCH_DET,
  83               "rx_preflist_search: mode %d (%s) (par %d) for %s", 
  84               search_mode, RX_text_srch_mode(search_mode), par_a, prefstr);
  85     
  86     if (mytree->num_nodes > 0) {
  87       err = RX_bin_search( search_mode, par_a, par_b, mytree, querypref, 
  88                    datlist, RX_ANS_ALL);
  89       if( err != RX_OK ) {
  90         return err;
  91       }
  92     }
  93   }
  94   
  95   return RX_OK;
  96 }
  97 
  98 /*++++
  99   this is a helper: goes through a datlist and returns the smallest
 100   size of a range
 101 
 102   works for IPv4 only
 103   +++*/
 104 static
 105 ip_rangesize_t
 106 rp_find_smallest_span( GList *datlist ) {
     /* [<][>][^][v][top][bottom][index][help] */
 107   ip_rangesize_t  min_span, span;
 108   GList *ditem;
 109 
 110   min_span = 0xffffffff; /* IPv4 only!!!!*/
 111 
 112     /* go through the list and find the shortest range.    */
 113     for(ditem = g_list_first(datlist);
 114         ditem != NULL;
 115         ditem = g_list_next(ditem)) {
 116       rx_datref_t *refptr = (rx_datref_t *) (ditem->data);
 117       
 118       span = IP_rang_span( & refptr->leafptr->iprange);
 119       
 120       if( span < min_span ) {
 121         min_span = span;
 122       }
 123     }
 124     ER_dbg_va(FAC_RP, ASP_RP_SRCH_DET,
 125               "rp_find_smallest_span: minimal span is %d", min_span);
 126 
 127     return min_span;
 128 }
 129 
 130 
 131 
 132 /* helper for the inetnum/exless search - for this one a hash of pairs
 133 (leafptr,occurences) must be maintained.
 134 
 135 This routine increments the counter for a leafptr, creating a new
 136 pair if this leafptr was not referenced before.
 137 
 138 */
 139 static
 140 int rp_leaf_occ_inc(GHashTable *hash, rx_dataleaf_t *leafptr)
     /* [<][>][^][v][top][bottom][index][help] */
 141 {
 142   /* one little trick: store the number of occurences 
 143      as cast (void *) */
 144   int val;
 145   
 146   val = (int) g_hash_table_lookup(hash, leafptr);
 147   /* 0 if it's not known yet. anyway: put it in the hash (value==key) */
 148   
 149   g_hash_table_insert(hash, leafptr, (void *) ++val); 
 150   
 151   return val;
 152 }
 153 
 154 /* exclude exact match - not to be merged with preselction :-( */
 155 static void
 156 rp_exclude_exact_match( GList **datlist, ip_range_t *testrang) 
     /* [<][>][^][v][top][bottom][index][help] */
 157 {
 158   GList *ditem, *newitem;
 159   
 160   ditem = g_list_first(*datlist);
 161 
 162   while( ditem != NULL ) {
 163     rx_datref_t *refptr = (rx_datref_t *) (ditem->data);
 164 
 165     newitem = g_list_next(ditem);
 166     
 167     if( memcmp( & refptr->leafptr->iprange, 
 168                 testrang, sizeof(ip_range_t)) == 0 ) {  
 169       rp_exclude_datlink(datlist, ditem);
 170       ER_dbg_va(FAC_RP, ASP_RP_SRCH_DET,
 171                 "process_datlist: discarded an exact match");
 172     }
 173     ditem = newitem;
 174   } /* while */
 175 }
 176 
 177 static int
 178 rp_find_longest_prefix(GList **datlist)
     /* [<][>][^][v][top][bottom][index][help] */
 179 {
 180   GList *ditem;
 181   int max_pref=0;
 182 
 183   for(ditem = g_list_first(*datlist);
 184       ditem != NULL;
 185       ditem = g_list_next(ditem)) {
 186     rx_datref_t *refptr = (rx_datref_t *) (ditem->data);
 187     
 188     if( refptr->leafptr->preflen > max_pref ) {
 189       max_pref = refptr->leafptr->preflen;
 190     }
 191   }
 192   
 193   return max_pref;
 194 }
 195 
 196 
 197 /*+ rp_asc_process_datlist() - helper for RP_asc_search()
 198   
 199   fetches the copies of objects from the radix tree into datlist
 200 
 201      ASSUMES LOCKED TREE
 202 
 203      the behaviour for a default inetnum (range) query is: 
 204        do an exact match; 
 205        if it fails, do an exless match on the encompassing prefix
 206      for routes(prefixes):
 207        do an exless match
 208      
 209      So if it's the default search mode on an inetnum tree,
 210      and the key is a range, 
 211      then an exact search is performed on one of the composing prefixes.
 212 
 213      Then the resulting data leaves are checked for exact matching with 
 214      the range queried for.
 215      Any dataleaves that do not match are discarded, and if none are left,
 216      the procedure falls back to searching for the encompassing prefix.
 217      (calculated in the smart_conv routine). 
 218      Add the dataleaf copies to the list of answers, 
 219      taking span into account 
 220 +*/
 221 static
 222 er_ret_t
 223 rp_asc_process_datlist(
     /* [<][>][^][v][top][bottom][index][help] */
 224                        rx_srch_mt search_mode,
 225                        int        par_a,
 226                        rx_fam_t   fam_id,
 227                        int        prefnumber,
 228                        GList    **datlist,
 229                        ip_range_t *testrang,
 230                        int       *hits
 231                        )
 232 {
 233   ip_rangesize_t  min_span=0, span;
 234   int use_span = 0;
 235   int max_pref = -1;
 236   GList    *ditem, *newitem;
 237   GHashTable *lohash = g_hash_table_new(NULL, NULL);
 238  
 239   /* in MORE and LESS(1) search exact match must not be displayed */
 240   if ( search_mode == RX_SRCH_MORE 
 241        || ( search_mode == RX_SRCH_LESS && par_a == 1 ) ) {
 242     rp_exclude_exact_match(datlist, testrang);
 243   }
 244   
 245   /* Preselection moved to processing, only span calculation done here *
 246    * 
 247     
 248    EXLESS and LESS(1) search: the smallest span must be found,
 249    but if the less spec node is not the same for all composing prefixes,
 250    it means it's not really this one.
 251    
 252    we check that by the number of references to this node is less than
 253    the number of composing prefixes
 254    
 255    We do the same for the less specific search - a node must be less 
 256    specific to all prefixes.
 257    
 258    if the number of references is  not enough, then return no hits,
 259    another try will be made, this time with one, encompassing prefix.
 260   */
 261   
 262   if ( (search_mode == RX_SRCH_EXLESS )   
 263        || ( search_mode == RX_SRCH_LESS && par_a == 1 ) )  {
 264     /* span works only for IP_V4. We use it only for inetnums,
 265        although RT/v4 would work too */
 266     if( testrang->begin.space == IP_V4 &&
 267         fam_id == RX_FAM_IN ) {
 268       min_span = rp_find_smallest_span(*datlist);
 269       use_span = 1;
 270     }
 271     else {
 272       /* in IPv6 and RT trees in general,  we can obtain the same
 273          result by selecting the longest prefix */
 274       max_pref = rp_find_longest_prefix(datlist);
 275     }
 276   }
 277   
 278   /* Process the dataleaf copies and add to the list of answers. */  
 279   ditem = g_list_first(*datlist);
 280   while(ditem != NULL) {
 281     rx_datref_t *refptr = (rx_datref_t *) (ditem->data);
 282     int exclude = 0;
 283     
 284     if(search_mode == RX_SRCH_EXLESS || search_mode == RX_SRCH_LESS ) {
 285       
 286       /* min_span defined <=> EXLESS or LESS(1) search of INETNUMS: 
 287          the smallest span must be returned */
 288       if( !exclude && use_span 
 289           && (span = IP_rang_span( &refptr->leafptr->iprange))!=min_span) {
 290         ER_dbg_va(FAC_RP, ASP_RP_SRCH_DET,
 291                   "process_datlist: (EX)LESS: discarded object with span %d", span);
 292         exclude = 1;
 293       }
 294       /* max_pref defined <=> EXLESS search of INETNUMS or LESS(1) of RT:
 295        */
 296       if( !exclude && max_pref >= 0
 297           && refptr->leafptr->preflen < max_pref ) {
 298         ER_dbg_va(FAC_RP, ASP_RP_SRCH_DET,
 299                   "process_datlist: (EX)LESS: discarded object with preflen %d", 
 300                   refptr->leafptr->preflen);
 301         exclude = 1;
 302       }
 303 
 304       /* number of occurences */
 305       /* XXX this will go when the old algorithm goes */
 306       if( !exclude 
 307           && prefnumber > 1 ) { /* do not check if all will be approved */
 308         
 309         if( rp_leaf_occ_inc(lohash, refptr->leafptr) < prefnumber ) {
 310           ER_dbg_va(FAC_RP, ASP_RP_SRCH_DET,
 311                     "process_datlist: (EX)LESS: leafptr %x not enough",refptr->leafptr);
 312           exclude = 1;
 313         } 
 314         else {
 315           ER_dbg_va(FAC_RP, ASP_RP_SRCH_DET,
 316                     "process_datlist: (EX)LESS: leafptr %x GOOD enough",refptr->leafptr);
 317         }
 318       }
 319     } 
 320     else if( search_mode ==  RX_SRCH_EXACT ) {
 321       /* EXACT search - discard if the range does not match */
 322       if( memcmp( & refptr->leafptr->iprange, 
 323                   testrang, sizeof(ip_range_t)) != 0) {
 324         
 325         ER_dbg_va(FAC_RP, ASP_RP_SRCH_DET,
 326                   "process_datlist: EXACT; discarded a mismatch");
 327         exclude = 1;
 328       } /*  EXACT match */
 329     } 
 330     else if( search_mode ==  RX_SRCH_MORE ) {
 331       /* MORE: exclude if not fully contained in the search term */
 332       if( ! (IP_addr_in_rang(&refptr->leafptr->iprange.begin, testrang )
 333           && IP_addr_in_rang(&refptr->leafptr->iprange.end, testrang ))) {
 334         ER_dbg_va(FAC_RP, ASP_RP_SRCH_DET,
 335                   "process_datlist: MORE; discarded a not-fully contained one");
 336         exclude = 1;
 337       }
 338     }
 339     
 340     
 341     /* get next item now, before the current gets deleted */
 342     newitem = g_list_next(ditem);
 343     if( exclude ) {
 344       /* get rid of it */
 345       rp_exclude_datlink(datlist, ditem);
 346     } 
 347     else {
 348       /* OK, so we ACCEPT these results*/
 349       /* uniqueness ensured in copy_results */
 350       (*hits)++;
 351     }
 352     ditem = newitem;
 353   } /* while ditem */ 
 354   
 355   /* wr_clear_list(&lolist); */
 356   g_hash_table_destroy(lohash);
 357   return RX_OK;
 358 }      
 359 
 360 /**************************************************************************/
 361 
 362 /*+ appends the element pointed to by datref to finallist +*/
 363 static
 364 er_ret_t
 365 rp_asc_append_datref(rx_datref_t *refptr, GList **finallist)
     /* [<][>][^][v][top][bottom][index][help] */
 366 {
 367   er_ret_t err;
 368   rx_datcpy_t *datcpy;
 369   void *dataptr;
 370 
 371     /* OK, so we ACCEPT this result. Copy it.*/
 372 
 373     if( (err=wr_calloc( (void **)& datcpy, 1, sizeof(rx_datcpy_t))) != UT_OK) {
 374       return err; /*    die;*/
 375     }
 376     
 377     datcpy->leafcpy = *(refptr->leafptr);
 378     
 379     /* copy the immediate data too. Set the ptr.*/
 380     
 381     if( (err=wr_calloc( (void **) & dataptr, 1, refptr->leafptr->data_len)) 
 382         != UT_OK) {
 383       return err; /*    die;*/
 384     }
 385     memcpy(dataptr, refptr->leafptr->data_ptr, refptr->leafptr->data_len);
 386     
 387     datcpy->leafcpy.data_ptr = dataptr;
 388 
 389     *finallist = g_list_prepend(*finallist, datcpy);
 390 
 391     /* XXX this wouldn't work in access_control */
 392     ER_dbg_va(FAC_RP, ASP_RP_SRCH_DATA,
 393               "rp_asc_append 'ed: %s", dataptr);
 394 
 395     return RX_OK;
 396 }
 397 
 398 /*+ goes through datlist (list of references "datref") and add copies of 
 399 leaves referenced to the finallist 
 400 
 401 maintains its own uniqhash which holds pointers to copied dataleaves.
 402 
 403 modifies: finallist
 404 
 405 returns: error from wr_malloc
 406 
 407 +*/
 408 static
 409 er_ret_t
 410 rp_srch_copyresults(GList *datlist,
     /* [<][>][^][v][top][bottom][index][help] */
 411                     GList **finallist,
 412                     int maxcount)
 413 {
 414   er_ret_t err;
 415   GList    *ditem;
 416   GHashTable *uniqhash = g_hash_table_new(NULL, NULL); /* defaults */
 417   int count = 0;
 418 
 419   ER_dbg_va(FAC_RP, ASP_RP_SRCH_DET, "srch_copyresults");
 420 
 421   /*  copy dataleaves pointed to by entries from the datlist
 422       only once (check uniqueness in the hash table) */
 423   for(ditem = g_list_first(datlist);
 424       ditem != NULL;
 425       ditem = g_list_next(ditem)) {
 426     rx_datref_t   *refptr = (rx_datref_t *) (ditem->data);
 427     rx_dataleaf_t *ansptr = refptr->leafptr;
 428 
 429     /* search for every ansptr (dataleaf pointer) in uniqhash */
 430     if( g_hash_table_lookup(uniqhash, ansptr) == NULL ) {
 431       
 432       /* it's not known yet. OK: put it in the hash (value==key) */
 433       g_hash_table_insert(uniqhash, ansptr, ansptr); 
 434       
 435       /* and copy the dataleaf */
 436       if( !NOERR(err = rp_asc_append_datref(refptr, finallist)) ) {
 437         return err;
 438       }
 439     }
 440 
 441     /* check the limit on number of objects if defined ( >0)  */
 442     count++;
 443     if( maxcount > 0 && count > maxcount ) {
 444       break;
 445     }
 446 
 447   } /*  foreach (datlist) */
 448     
 449   g_hash_table_destroy(uniqhash); /* elements are still linked to through datlist */
 450 
 451   return RP_OK;
 452 }
 453 
 454 static 
 455 void
 456 rp_begend_preselection(GList **datlist, rx_fam_t fam_id, ip_range_t *testrang) 
     /* [<][>][^][v][top][bottom][index][help] */
 457 {
 458   GList *ditem, *newitem; 
 459 
 460   ditem = g_list_first(*datlist);
 461 
 462   while( ditem != NULL ) {
 463     rx_datref_t *refptr = (rx_datref_t *) (ditem->data);
 464     newitem = g_list_next(ditem);
 465 
 466     /* the test is indentical for route & inetnum trees */
 467     if( IP_addr_in_rang(&testrang->end, &refptr->leafptr->iprange) == 0 ) {
 468       
 469       ER_dbg_va(FAC_RP, ASP_RP_SRCH_DET,
 470                 "process_datlist: discarded an uncovering leafptr %x",
 471                 refptr->leafptr);
 472       rp_exclude_datlink(datlist, ditem);
 473     }
 474     ditem = newitem;
 475   } /* while */
 476 }
 477 
 478 /*+++++++++++++++
 479   search.
 480 
 481   2 approaches: 
 482 
 483   1. (most modes): look up all less specifics of beginning and end of range,
 484   compare/select/etc.
 485 
 486   2. More spec mode: break up the query range into prefixes, [erform a search
 487   for each of them. Add all results together.
 488 
 489   translates a query into a binary prefix (or prefixes, if range).
 490   for registry+space (or if they are zero, for all
 491   registries/spaces)
 492   finds tree 
 493   calls RX_bin_search (returning node copies).
 494   will not put duplicate entries (composed inetnums).
 495   returns some sort of error code :-) 
 496   
 497   Cuts the number of answers from RX_bin_search down to max_count,
 498   but since some of the answers may have been "normalized" in the
 499   underlying functions (multiple occurences removed), 
 500   the result is _at_most_ max_count.
 501   
 502   appends to a given list of data blocks (not nodes!)
 503 
 504   The EXLESS search on inetnum tree should return the shortest range 
 505   that was found, by means of comparing span (size) of the range.
 506   If there are more of size equal to the smallest one, they are also
 507   returned.
 508 
 509   returns RX_OK or a code from an underlying function
 510 ++++++++++++*/
 511 er_ret_t
 512 RP_asc_search ( 
     /* [<][>][^][v][top][bottom][index][help] */
 513                rx_srch_mt search_mode, 
 514                int par_a,
 515                int par_b,
 516                char *key,     /*+ search term: (string) prefix/range/IP +*/
 517                rp_regid_t  reg_id,
 518                rp_attr_t  attr,    /*+ extra tree id (within the same reg/spc/fam +*/
 519                GList **finallist,    /*+ answers go here, please +*/
 520                int    max_count    /*+ max # of answers. RX_ALLANS == unlimited +*/
 521                )
 522 { 
 523   GList    *preflist = NULL;
 524   GList    *datlist = NULL;
 525   er_ret_t   err; 
 526   ip_range_t  testrang;
 527   int        locked = 0;
 528   ip_keytype_t key_type;
 529   ip_space_t   spc_id;
 530   rx_fam_t   fam_id = RP_attr2fam( attr );
 531   rx_tree_t   *mytree;
 532   int hits=0;
 533   ip_prefix_t beginpref;
 534   
 535 
 536   /*  abort on error (but unlock the tree) */  
 537   ER_dbg_va(FAC_RP, ASP_RP_SRCH_GEN,
 538             "RP_NEW_asc_search:  query %s : mode %d (%s) (par %d) for %s",
 539             DF_get_attribute_name(attr),
 540             search_mode, RX_text_srch_mode(search_mode), par_a, key);
 541 
 542   
 543   /* parse the key into a prefix list */
 544   if( ( err = IP_smart_conv(key, 0, 0,
 545                             &preflist, IP_EXPN, &key_type)) != IP_OK ) {
 546     /* operational trouble (UT_*) or invalid key (IP_INVARG)*/
 547     return err; 
 548   }
 549 
 550   /* set the test values */
 551   IP_smart_range(key, &testrang, IP_EXPN, &key_type);
 552   
 553   /* find the tree */
 554   if( NOERR(err) ) {
 555     spc_id = IP_pref_b2_space( g_list_first(preflist)->data );
 556     if( ! NOERR(err = RP_tree_get( &mytree, reg_id, spc_id, attr ))) {
 557       return err;
 558     }
 559   }
 560   /* the point of no return: now we lock the tree. From here, even if errors
 561      occur, we still go through all procedure to unlock the tree at the end */
 562   
 563   /* lock the tree */
 564   TH_acquire_read_lockw( &(mytree->rwlock) );
 565   locked = 1;
 566 
 567   /* Collection: this procedure is used for some search_modes only */
 568   if(    search_mode == RX_SRCH_EXLESS 
 569       || search_mode == RX_SRCH_LESS 
 570       || search_mode == RX_SRCH_EXACT )  {
 571 
 572     /* 1. compose a /32(/128) prefix for beginning of range */
 573     beginpref.ip = testrang.begin;
 574     beginpref.bits = IP_sizebits(spc_id);
 575     
 576     /* 2. dataleaves collection: look up the beginning prefix in LESS(255) mode */
 577     if( NOERR(err) ) {
 578       err = RX_bin_search( RX_SRCH_LESS, 255, 0, mytree, &beginpref, 
 579                            &datlist, RX_ANS_ALL);
 580     }
 581     
 582     /* 3. preselection: exclude those that do not include end of range 
 583      */
 584     if( NOERR(err) ) {
 585       rp_begend_preselection(&datlist, fam_id, &testrang);
 586     }
 587 
 588   } /* if exless|less|exact */
 589   else {
 590     /* MORE */
 591 
 592     /* standard collection using the traditional method: 
 593        repeat the search for all prefixes and join results */
 594 
 595     if( NOERR(err) ) {
 596       err = rp_preflist_search ( search_mode, par_a, par_b, 
 597                                  mytree, &preflist, &datlist);
 598     }
 599   } /* collection */
 600 
 601   ER_dbg_va(FAC_RP, ASP_RP_SRCH_GEN,
 602             "RP_NEW_asc_search: collected %d references ",
 603             g_list_length(datlist));
 604 
 605 
 606   /* 5. processing - using the same processing function */
 607   if( NOERR(err) ) {
 608     err = rp_asc_process_datlist( search_mode, par_a, fam_id, 
 609                                   1, /* one occurence is enough */
 610                                   &datlist,  
 611                                   &testrang,  &hits );
 612   }
 613   
 614   /* 6. copy results */
 615   if( NOERR(err) ) {
 616     err = rp_srch_copyresults(datlist, finallist, max_count); /* and uniq */
 617   }
 618 
 619   if( locked ) {
 620     /* 100. unlock the tree */
 621     TH_release_read_lockw( &(mytree->rwlock) );
 622   }
 623 
 624   /* clean up */
 625   wr_clear_list( &preflist ); 
 626   wr_clear_list( &datlist );  
 627 
 628   /* NOTE if error occured, finallist may be partly filled in. */
 629   return err;
 630 }
 631   

/* [<][>][^][v][top][bottom][index][help] */