1 /*- 2 * Copyright (c) 2008 Sam Leffler, Errno Consulting 3 * All rights reserved. 4 * 5 * Redistribution and use in source and binary forms, with or without 6 * modification, are permitted provided that the following conditions 7 * are met: 8 * 1. Redistributions of source code must retain the above copyright 9 * notice, this list of conditions and the following disclaimer. 10 * 2. Redistributions in binary form must reproduce the above copyright 11 * notice, this list of conditions and the following disclaimer in the 12 * documentation and/or other materials provided with the distribution. 13 * 14 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR 15 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 16 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 17 * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, 18 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 19 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 20 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 21 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 22 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 23 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 * 25 * $FreeBSD: head/sbin/ifconfig/regdomain.c 200587 2009-12-15 20:44:12Z gavin $ 26 */ 27 28 #include <sys/param.h> 29 #include <sys/types.h> 30 #include <sys/errno.h> 31 #include <sys/mman.h> 32 #include <sys/sbuf.h> 33 #include <sys/stat.h> 34 35 #include <stdio.h> 36 #include <string.h> 37 #include <ctype.h> 38 #include <fcntl.h> 39 #include <err.h> 40 #include <unistd.h> 41 42 #include <bsdxml.h> 43 44 #include "regdomain.h" 45 46 #include <netproto/802_11/_ieee80211.h> 47 48 #define MAXLEVEL 20 49 50 struct mystate { 51 XML_Parser parser; 52 struct regdata *rdp; 53 struct regdomain *rd; /* current domain */ 54 struct netband *netband; /* current netband */ 55 struct freqband *freqband; /* current freqband */ 56 struct country *country; /* current country */ 57 netband_head *curband; /* current netband list */ 58 int level; 59 struct sbuf *sbuf[MAXLEVEL]; 60 int nident; 61 }; 62 63 static int 64 iseq(const char *a, const char *b) 65 { 66 return (strcasecmp(a, b) == 0); 67 } 68 69 static void 70 start_element(void *data, const char *name, const char **attr) 71 { 72 struct mystate *mt; 73 const void *id, *ref, *mode; 74 int i; 75 76 mt = data; 77 if (++mt->level == MAXLEVEL) { 78 /* XXX force parser to abort */ 79 return; 80 } 81 mt->sbuf[mt->level] = sbuf_new_auto(); 82 id = ref = mode = NULL; 83 for (i = 0; attr[i] != NULL; i += 2) { 84 if (iseq(attr[i], "id")) { 85 id = attr[i+1]; 86 } else if (iseq(attr[i], "ref")) { 87 ref = attr[i+1]; 88 } else if (iseq(attr[i], "mode")) { 89 mode = attr[i+1]; 90 } else 91 printf("%*.*s[%s = %s]\n", mt->level + 1, 92 mt->level + 1, "", attr[i], attr[i+1]); 93 } 94 if (iseq(name, "rd") && mt->rd == NULL) { 95 if (mt->country == NULL) { 96 mt->rd = calloc(1, sizeof(struct regdomain)); 97 mt->rd->name = strdup(id); 98 mt->nident++; 99 LIST_INSERT_HEAD(&mt->rdp->domains, mt->rd, next); 100 } else 101 mt->country->rd = (void *)strdup(ref); 102 return; 103 } 104 if (iseq(name, "defcc") && mt->rd != NULL) { 105 mt->rd->cc = (void *)strdup(ref); 106 return; 107 } 108 if (iseq(name, "netband") && mt->curband == NULL && mt->rd != NULL) { 109 if (mode == NULL) { 110 warnx("no mode for netband at line %ld", 111 XML_GetCurrentLineNumber(mt->parser)); 112 return; 113 } 114 if (iseq(mode, "11b")) 115 mt->curband = &mt->rd->bands_11b; 116 else if (iseq(mode, "11g")) 117 mt->curband = &mt->rd->bands_11g; 118 else if (iseq(mode, "11a")) 119 mt->curband = &mt->rd->bands_11a; 120 else if (iseq(mode, "11ng")) 121 mt->curband = &mt->rd->bands_11ng; 122 else if (iseq(mode, "11na")) 123 mt->curband = &mt->rd->bands_11na; 124 else 125 warnx("unknown mode \"%s\" at line %ld", 126 __DECONST(char *, mode), 127 XML_GetCurrentLineNumber(mt->parser)); 128 return; 129 } 130 if (iseq(name, "band") && mt->netband == NULL) { 131 if (mt->curband == NULL) { 132 warnx("band without enclosing netband at line %ld", 133 XML_GetCurrentLineNumber(mt->parser)); 134 return; 135 } 136 mt->netband = calloc(1, sizeof(struct netband)); 137 LIST_INSERT_HEAD(mt->curband, mt->netband, next); 138 return; 139 } 140 if (iseq(name, "freqband") && mt->freqband == NULL && mt->netband != NULL) { 141 /* XXX handle inlines and merge into table? */ 142 if (mt->netband->band != NULL) { 143 warnx("duplicate freqband at line %ld ignored", 144 XML_GetCurrentLineNumber(mt->parser)); 145 /* XXX complain */ 146 } else 147 mt->netband->band = (void *)strdup(ref); 148 return; 149 } 150 151 if (iseq(name, "country") && mt->country == NULL) { 152 mt->country = calloc(1, sizeof(struct country)); 153 mt->country->isoname = strdup(id); 154 mt->country->code = NO_COUNTRY; 155 mt->nident++; 156 LIST_INSERT_HEAD(&mt->rdp->countries, mt->country, next); 157 return; 158 } 159 160 if (iseq(name, "freqband") && mt->freqband == NULL) { 161 mt->freqband = calloc(1, sizeof(struct freqband)); 162 mt->freqband->id = strdup(id); 163 mt->nident++; 164 LIST_INSERT_HEAD(&mt->rdp->freqbands, mt->freqband, next); 165 return; 166 } 167 } 168 169 static int 170 decode_flag(struct mystate *mt, const char *p, int len) 171 { 172 static const struct { 173 const char *name; 174 int len; 175 uint32_t value; 176 } flags[] = { 177 #define FLAG(x) { #x, sizeof(#x)-1, x } 178 FLAG(IEEE80211_CHAN_A), 179 FLAG(IEEE80211_CHAN_B), 180 FLAG(IEEE80211_CHAN_G), 181 FLAG(IEEE80211_CHAN_HT20), 182 FLAG(IEEE80211_CHAN_HT40), 183 FLAG(IEEE80211_CHAN_ST), 184 FLAG(IEEE80211_CHAN_TURBO), 185 FLAG(IEEE80211_CHAN_PASSIVE), 186 FLAG(IEEE80211_CHAN_DFS), 187 FLAG(IEEE80211_CHAN_CCK), 188 FLAG(IEEE80211_CHAN_OFDM), 189 FLAG(IEEE80211_CHAN_2GHZ), 190 FLAG(IEEE80211_CHAN_5GHZ), 191 FLAG(IEEE80211_CHAN_DYN), 192 FLAG(IEEE80211_CHAN_GFSK), 193 FLAG(IEEE80211_CHAN_GSM), 194 FLAG(IEEE80211_CHAN_STURBO), 195 FLAG(IEEE80211_CHAN_HALF), 196 FLAG(IEEE80211_CHAN_QUARTER), 197 FLAG(IEEE80211_CHAN_HT40U), 198 FLAG(IEEE80211_CHAN_HT40D), 199 FLAG(IEEE80211_CHAN_4MSXMIT), 200 FLAG(IEEE80211_CHAN_NOADHOC), 201 FLAG(IEEE80211_CHAN_NOHOSTAP), 202 FLAG(IEEE80211_CHAN_11D), 203 FLAG(IEEE80211_CHAN_FHSS), 204 FLAG(IEEE80211_CHAN_PUREG), 205 FLAG(IEEE80211_CHAN_108A), 206 FLAG(IEEE80211_CHAN_108G), 207 #undef FLAG 208 { "ECM", 3, REQ_ECM }, 209 { "INDOOR", 6, REQ_INDOOR }, 210 { "OUTDOOR", 7, REQ_OUTDOOR }, 211 }; 212 size_t i; 213 214 for (i = 0; i < nitems(flags); i++) 215 if (len == flags[i].len && iseq(p, flags[i].name)) 216 return flags[i].value; 217 warnx("unknown flag \"%.*s\" at line %ld ignored", 218 len, p, XML_GetCurrentLineNumber(mt->parser)); 219 return 0; 220 } 221 222 static void 223 end_element(void *data, const char *name) 224 { 225 struct mystate *mt; 226 int len; 227 char *p; 228 229 mt = data; 230 sbuf_finish(mt->sbuf[mt->level]); 231 p = sbuf_data(mt->sbuf[mt->level]); 232 len = sbuf_len(mt->sbuf[mt->level]); 233 234 /* <freqband>...</freqband> */ 235 if (iseq(name, "freqstart") && mt->freqband != NULL) { 236 mt->freqband->freqStart = strtoul(p, NULL, 0); 237 goto done; 238 } 239 if (iseq(name, "freqend") && mt->freqband != NULL) { 240 mt->freqband->freqEnd = strtoul(p, NULL, 0); 241 goto done; 242 } 243 if (iseq(name, "chanwidth") && mt->freqband != NULL) { 244 mt->freqband->chanWidth = strtoul(p, NULL, 0); 245 goto done; 246 } 247 if (iseq(name, "chansep") && mt->freqband != NULL) { 248 mt->freqband->chanSep = strtoul(p, NULL, 0); 249 goto done; 250 } 251 if (iseq(name, "flags")) { 252 if (mt->freqband != NULL) 253 mt->freqband->flags |= decode_flag(mt, p, len); 254 else if (mt->netband != NULL) 255 mt->netband->flags |= decode_flag(mt, p, len); 256 else { 257 warnx("flags without freqband or netband at line %ld ignored", 258 XML_GetCurrentLineNumber(mt->parser)); 259 } 260 goto done; 261 } 262 263 /* <rd> ... </rd> */ 264 if (iseq(name, "name") && mt->rd != NULL) { 265 mt->rd->name = strdup(p); 266 goto done; 267 } 268 if (iseq(name, "sku") && mt->rd != NULL) { 269 mt->rd->sku = strtoul(p, NULL, 0); 270 goto done; 271 } 272 if (iseq(name, "netband") && mt->rd != NULL) { 273 mt->curband = NULL; 274 goto done; 275 } 276 277 /* <band> ... </band> */ 278 if (iseq(name, "freqband") && mt->netband != NULL) { 279 /* XXX handle inline freqbands */ 280 goto done; 281 } 282 if (iseq(name, "maxpower") && mt->netband != NULL) { 283 mt->netband->maxPower = strtoul(p, NULL, 0); 284 goto done; 285 } 286 if (iseq(name, "maxpowerdfs") && mt->netband != NULL) { 287 mt->netband->maxPowerDFS = strtoul(p, NULL, 0); 288 goto done; 289 } 290 if (iseq(name, "maxantgain") && mt->netband != NULL) { 291 mt->netband->maxAntGain = strtoul(p, NULL, 0); 292 goto done; 293 } 294 295 /* <country>...</country> */ 296 if (iseq(name, "isocc") && mt->country != NULL) { 297 mt->country->code = strtoul(p, NULL, 0); 298 goto done; 299 } 300 if (iseq(name, "name") && mt->country != NULL) { 301 mt->country->name = strdup(p); 302 goto done; 303 } 304 305 if (len != 0) { 306 warnx("unexpected XML token \"%s\" data \"%s\" at line %ld", 307 name, p, XML_GetCurrentLineNumber(mt->parser)); 308 /* XXX goto done? */ 309 } 310 /* </freqband> */ 311 if (iseq(name, "freqband") && mt->freqband != NULL) { 312 /* XXX must have start/end frequencies */ 313 /* XXX must have channel width/sep */ 314 mt->freqband = NULL; 315 goto done; 316 } 317 /* </rd> */ 318 if (iseq(name, "rd") && mt->rd != NULL) { 319 mt->rd = NULL; 320 goto done; 321 } 322 /* </band> */ 323 if (iseq(name, "band") && mt->netband != NULL) { 324 if (mt->netband->band == NULL) { 325 warnx("no freqbands for band at line %ld", 326 XML_GetCurrentLineNumber(mt->parser)); 327 } 328 if (mt->netband->maxPower == 0) { 329 warnx("no maxpower for band at line %ld", 330 XML_GetCurrentLineNumber(mt->parser)); 331 } 332 /* default max power w/ DFS to max power */ 333 if (mt->netband->maxPowerDFS == 0) 334 mt->netband->maxPowerDFS = mt->netband->maxPower; 335 mt->netband = NULL; 336 goto done; 337 } 338 /* </netband> */ 339 if (iseq(name, "netband") && mt->netband != NULL) { 340 mt->curband = NULL; 341 goto done; 342 } 343 /* </country> */ 344 if (iseq(name, "country") && mt->country != NULL) { 345 if (mt->country->code == NO_COUNTRY) { 346 warnx("no ISO cc for country at line %ld", 347 XML_GetCurrentLineNumber(mt->parser)); 348 } 349 if (mt->country->name == NULL) { 350 warnx("no name for country at line %ld", 351 XML_GetCurrentLineNumber(mt->parser)); 352 } 353 if (mt->country->rd == NULL) { 354 warnx("no regdomain reference for country at line %ld", 355 XML_GetCurrentLineNumber(mt->parser)); 356 } 357 mt->country = NULL; 358 goto done; 359 } 360 done: 361 sbuf_delete(mt->sbuf[mt->level]); 362 mt->sbuf[mt->level--] = NULL; 363 } 364 365 static void 366 char_data(void *data, const XML_Char *s, int len) 367 { 368 struct mystate *mt; 369 const char *b, *e; 370 371 mt = data; 372 373 b = s; 374 e = s + len-1; 375 for (; isspace(*b) && b < e; b++) 376 ; 377 for (; isspace(*e) && e > b; e++) 378 ; 379 if (e != b || (*b != '\0' && !isspace(*b))) 380 sbuf_bcat(mt->sbuf[mt->level], b, e-b+1); 381 } 382 383 static void * 384 findid(struct regdata *rdp, const void *id, enum IdentType type) 385 { 386 struct ident *ip; 387 388 for (ip = rdp->ident; ip->id != NULL; ip++) 389 if (ip->type == type && iseq(ip->id, id)) 390 return ip->p; 391 return NULL; 392 } 393 394 /* 395 * Parse an regdomain XML configuration and build the internal representation. 396 */ 397 int 398 lib80211_regdomain_readconfig(struct regdata *rdp, const void *p, size_t len) 399 { 400 struct mystate *mt; 401 struct regdomain *dp; 402 struct country *cp; 403 struct freqband *fp; 404 struct netband *nb; 405 const void *id; 406 int i, errors; 407 408 memset(rdp, 0, sizeof(struct regdata)); 409 mt = calloc(1, sizeof(struct mystate)); 410 if (mt == NULL) 411 return ENOMEM; 412 /* parse the XML input */ 413 mt->rdp = rdp; 414 mt->parser = XML_ParserCreate(NULL); 415 XML_SetUserData(mt->parser, mt); 416 XML_SetElementHandler(mt->parser, start_element, end_element); 417 XML_SetCharacterDataHandler(mt->parser, char_data); 418 if (XML_Parse(mt->parser, p, len, 1) != XML_STATUS_OK) { 419 warnx("%s: %s at line %ld", __func__, 420 XML_ErrorString(XML_GetErrorCode(mt->parser)), 421 XML_GetCurrentLineNumber(mt->parser)); 422 return -1; 423 } 424 XML_ParserFree(mt->parser); 425 426 /* setup the identifer table */ 427 rdp->ident = calloc(sizeof(struct ident), mt->nident + 1); 428 if (rdp->ident == NULL) 429 return ENOMEM; 430 free(mt); 431 432 errors = 0; 433 i = 0; 434 LIST_FOREACH(dp, &rdp->domains, next) { 435 rdp->ident[i].id = dp->name; 436 rdp->ident[i].p = dp; 437 rdp->ident[i].type = DOMAIN; 438 i++; 439 } 440 LIST_FOREACH(fp, &rdp->freqbands, next) { 441 rdp->ident[i].id = fp->id; 442 rdp->ident[i].p = fp; 443 rdp->ident[i].type = FREQBAND; 444 i++; 445 } 446 LIST_FOREACH(cp, &rdp->countries, next) { 447 rdp->ident[i].id = cp->isoname; 448 rdp->ident[i].p = cp; 449 rdp->ident[i].type = COUNTRY; 450 i++; 451 } 452 453 /* patch references */ 454 LIST_FOREACH(dp, &rdp->domains, next) { 455 if (dp->cc != NULL) { 456 id = dp->cc; 457 dp->cc = findid(rdp, id, COUNTRY); 458 if (dp->cc == NULL) { 459 warnx("undefined country \"%s\"", 460 __DECONST(char *, id)); 461 errors++; 462 } 463 free(__DECONST(char *, id)); 464 } 465 LIST_FOREACH(nb, &dp->bands_11b, next) { 466 id = findid(rdp, nb->band, FREQBAND); 467 if (id == NULL) { 468 warnx("undefined 11b band \"%s\"", 469 __DECONST(char *, nb->band)); 470 errors++; 471 } 472 nb->band = id; 473 } 474 LIST_FOREACH(nb, &dp->bands_11g, next) { 475 id = findid(rdp, nb->band, FREQBAND); 476 if (id == NULL) { 477 warnx("undefined 11g band \"%s\"", 478 __DECONST(char *, nb->band)); 479 errors++; 480 } 481 nb->band = id; 482 } 483 LIST_FOREACH(nb, &dp->bands_11a, next) { 484 id = findid(rdp, nb->band, FREQBAND); 485 if (id == NULL) { 486 warnx("undefined 11a band \"%s\"", 487 __DECONST(char *, nb->band)); 488 errors++; 489 } 490 nb->band = id; 491 } 492 LIST_FOREACH(nb, &dp->bands_11ng, next) { 493 id = findid(rdp, nb->band, FREQBAND); 494 if (id == NULL) { 495 warnx("undefined 11ng band \"%s\"", 496 __DECONST(char *, nb->band)); 497 errors++; 498 } 499 nb->band = id; 500 } 501 LIST_FOREACH(nb, &dp->bands_11na, next) { 502 id = findid(rdp, nb->band, FREQBAND); 503 if (id == NULL) { 504 warnx("undefined 11na band \"%s\"", 505 __DECONST(char *, nb->band)); 506 errors++; 507 } 508 nb->band = id; 509 } 510 } 511 LIST_FOREACH(cp, &rdp->countries, next) { 512 id = cp->rd; 513 cp->rd = findid(rdp, id, DOMAIN); 514 if (cp->rd == NULL) { 515 warnx("undefined country \"%s\"", 516 __DECONST(char *, id)); 517 errors++; 518 } 519 free(__DECONST(char *, id)); 520 } 521 522 return errors ? EINVAL : 0; 523 } 524 525 static void 526 cleanup_bands(netband_head *head) 527 { 528 struct netband *nb; 529 530 for (;;) { 531 nb = LIST_FIRST(head); 532 if (nb == NULL) 533 break; 534 free(nb); 535 } 536 } 537 538 /* 539 * Cleanup state/resources for a previously parsed regdomain database. 540 */ 541 void 542 lib80211_regdomain_cleanup(struct regdata *rdp) 543 { 544 545 free(rdp->ident); 546 rdp->ident = NULL; 547 for (;;) { 548 struct regdomain *dp = LIST_FIRST(&rdp->domains); 549 if (dp == NULL) 550 break; 551 LIST_REMOVE(dp, next); 552 cleanup_bands(&dp->bands_11b); 553 cleanup_bands(&dp->bands_11g); 554 cleanup_bands(&dp->bands_11a); 555 cleanup_bands(&dp->bands_11ng); 556 cleanup_bands(&dp->bands_11na); 557 if (dp->name != NULL) 558 free(__DECONST(char *, dp->name)); 559 } 560 for (;;) { 561 struct country *cp = LIST_FIRST(&rdp->countries); 562 if (cp == NULL) 563 break; 564 LIST_REMOVE(cp, next); 565 if (cp->name != NULL) 566 free(__DECONST(char *, cp->name)); 567 free(cp); 568 } 569 for (;;) { 570 struct freqband *fp = LIST_FIRST(&rdp->freqbands); 571 if (fp == NULL) 572 break; 573 LIST_REMOVE(fp, next); 574 free(fp); 575 } 576 } 577 578 struct regdata * 579 lib80211_alloc_regdata(void) 580 { 581 struct regdata *rdp; 582 struct stat sb; 583 void *xml; 584 int fd; 585 586 rdp = calloc(1, sizeof(struct regdata)); 587 588 fd = open(_PATH_REGDOMAIN, O_RDONLY); 589 if (fd < 0) { 590 #ifdef DEBUG 591 warn("%s: open(%s)", __func__, _PATH_REGDOMAIN); 592 #endif 593 free(rdp); 594 return NULL; 595 } 596 if (fstat(fd, &sb) < 0) { 597 #ifdef DEBUG 598 warn("%s: fstat(%s)", __func__, _PATH_REGDOMAIN); 599 #endif 600 close(fd); 601 free(rdp); 602 return NULL; 603 } 604 xml = mmap(NULL, sb.st_size, PROT_READ, MAP_PRIVATE, fd, 0); 605 if (xml == MAP_FAILED) { 606 #ifdef DEBUG 607 warn("%s: mmap", __func__); 608 #endif 609 close(fd); 610 free(rdp); 611 return NULL; 612 } 613 if (lib80211_regdomain_readconfig(rdp, xml, sb.st_size) != 0) { 614 #ifdef DEBUG 615 warn("%s: error reading regulatory database", __func__); 616 #endif 617 munmap(xml, sb.st_size); 618 close(fd); 619 free(rdp); 620 return NULL; 621 } 622 munmap(xml, sb.st_size); 623 close(fd); 624 625 return rdp; 626 } 627 628 void 629 lib80211_free_regdata(struct regdata *rdp) 630 { 631 lib80211_regdomain_cleanup(rdp); 632 free(rdp); 633 } 634 635 /* 636 * Lookup a regdomain by SKU. 637 */ 638 const struct regdomain * 639 lib80211_regdomain_findbysku(const struct regdata *rdp, enum RegdomainCode sku) 640 { 641 const struct regdomain *dp; 642 643 LIST_FOREACH(dp, &rdp->domains, next) { 644 if (dp->sku == sku) 645 return dp; 646 } 647 return NULL; 648 } 649 650 /* 651 * Lookup a regdomain by name. 652 */ 653 const struct regdomain * 654 lib80211_regdomain_findbyname(const struct regdata *rdp, const char *name) 655 { 656 const struct regdomain *dp; 657 658 LIST_FOREACH(dp, &rdp->domains, next) { 659 if (iseq(dp->name, name)) 660 return dp; 661 } 662 return NULL; 663 } 664 665 /* 666 * Lookup a country by ISO country code. 667 */ 668 const struct country * 669 lib80211_country_findbycc(const struct regdata *rdp, enum ISOCountryCode cc) 670 { 671 const struct country *cp; 672 673 LIST_FOREACH(cp, &rdp->countries, next) { 674 if (cp->code == cc) 675 return cp; 676 } 677 return NULL; 678 } 679 680 /* 681 * Lookup a country by ISO/long name. 682 */ 683 const struct country * 684 lib80211_country_findbyname(const struct regdata *rdp, const char *name) 685 { 686 const struct country *cp; 687 int len; 688 689 LIST_FOREACH(cp, &rdp->countries, next) { 690 if (iseq(cp->isoname, name)) 691 return cp; 692 } 693 len = strlen(name); 694 LIST_FOREACH(cp, &rdp->countries, next) { 695 if (strncasecmp(cp->name, name, len) == 0) 696 return cp; 697 } 698 return NULL; 699 } 700