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

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