1 /*
2 * Copyright 2004 John M Bell <jmb202@ecs.soton.ac.uk>
3 *
4 * This file is part of NetSurf, http://www.netsurf-browser.org/
5 *
6 * NetSurf is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; version 2 of the License.
9 *
10 * NetSurf is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
17 */
18
19 /**
20 * \file
21 * Implementation of HTML image maps
22 *
23 * \todo should this should use the general hashmap instead of its own
24 */
25
26 #include <assert.h>
27 #include <stdbool.h>
28 #include <string.h>
29 #include <strings.h>
30
31 #include <dom/dom.h>
32
33 #include "utils/log.h"
34 #include "utils/corestrings.h"
35 #include "content/content_protected.h"
36 #include "content/hlcache.h"
37
38 #include "html/box.h"
39 #include "html/box_construct.h"
40 #include "html/private.h"
41 #include "html/imagemap.h"
42
43 #define HASH_SIZE 31 /* fixed size hash table */
44
45 typedef enum {
46 IMAGEMAP_DEFAULT,
47 IMAGEMAP_RECT,
48 IMAGEMAP_CIRCLE,
49 IMAGEMAP_POLY
50 } imagemap_entry_type;
51
52 struct mapentry {
53 imagemap_entry_type type; /**< type of shape */
54 nsurl *url; /**< absolute url to go to */
55 char *target; /**< target frame (if any) */
56 union {
57 struct {
58 int x; /**< x coordinate of centre */
59 int y; /**< y coordinate of center */
60 int r; /**< radius of circle */
61 } circle;
62 struct {
63 int x0; /**< left hand edge */
64 int y0; /**< top edge */
65 int x1; /**< right hand edge */
66 int y1; /**< bottom edge */
67 } rect;
68 struct {
69 int num; /**< number of points */
70 float *xcoords; /**< x coordinates */
71 float *ycoords; /**< y coordinates */
72 } poly;
73 } bounds;
74 struct mapentry *next; /**< next entry in list */
75 };
76
77 struct imagemap {
78 char *key; /**< key for this entry */
79 struct mapentry *list; /**< pointer to linked list of entries */
80 struct imagemap *next; /**< next entry in this hash chain */
81 };
82
83 /**
84 * Create hashtable of imagemaps
85 *
86 * \param c The containing content
87 * \return true on success, false otherwise
88 */
imagemap_create(html_content * c)89 static bool imagemap_create(html_content *c)
90 {
91 assert(c != NULL);
92
93 if (c->imagemaps == NULL) {
94 c->imagemaps = calloc(HASH_SIZE, sizeof(struct imagemap *));
95 if (c->imagemaps == NULL) {
96 return false;
97 }
98 }
99
100 return true;
101 }
102
103 /**
104 * Hash function.
105 *
106 * \param key The key to hash.
107 * \return The hashed value.
108 */
imagemap_hash(const char * key)109 static unsigned int imagemap_hash(const char *key)
110 {
111 unsigned int z = 0;
112
113 if (key == 0) return 0;
114
115 for (; *key != 0; key++) {
116 z += *key & 0x1f;
117 }
118
119 return (z % (HASH_SIZE - 1)) + 1;
120 }
121
122 /**
123 * Add an imagemap to the hashtable, creating it if it doesn't exist
124 *
125 * \param c The containing content
126 * \param key The name of the imagemap
127 * \param list List of map regions
128 * \return true on succes, false otherwise
129 */
130 static bool
imagemap_add(html_content * c,dom_string * key,struct mapentry * list)131 imagemap_add(html_content *c, dom_string *key, struct mapentry *list)
132 {
133 struct imagemap *map;
134 unsigned int slot;
135
136 assert(c != NULL);
137 assert(key != NULL);
138 assert(list != NULL);
139
140 if (imagemap_create(c) == false)
141 return false;
142
143 map = calloc(1, sizeof(*map));
144 if (map == NULL)
145 return false;
146
147 /* \todo Stop relying on NULL termination of dom_string */
148 map->key = strdup(dom_string_data(key));
149 if (map->key == NULL) {
150 free(map);
151 return false;
152 }
153
154 map->list = list;
155
156 slot = imagemap_hash(map->key);
157
158 map->next = c->imagemaps[slot];
159 c->imagemaps[slot] = map;
160
161 return true;
162 }
163
164 /**
165 * Free list of imagemap entries
166 *
167 * \param list Pointer to head of list
168 */
imagemap_freelist(struct mapentry * list)169 static void imagemap_freelist(struct mapentry *list)
170 {
171 struct mapentry *entry, *prev;
172
173 assert(list != NULL);
174
175 entry = list;
176
177 while (entry != NULL) {
178 prev = entry;
179
180 nsurl_unref(entry->url);
181
182 if (entry->target)
183 free(entry->target);
184
185 if (entry->type == IMAGEMAP_POLY) {
186 free(entry->bounds.poly.xcoords);
187 free(entry->bounds.poly.ycoords);
188 }
189
190 entry = entry->next;
191 free(prev);
192 }
193 }
194
195 /**
196 * Destroy hashtable of imagemaps
197 *
198 * \param c The containing content
199 */
imagemap_destroy(html_content * c)200 void imagemap_destroy(html_content *c)
201 {
202 unsigned int i;
203
204 assert(c != NULL);
205
206 /* no imagemaps -> return */
207 if (c->imagemaps == NULL)
208 return;
209
210 for (i = 0; i != HASH_SIZE; i++) {
211 struct imagemap *map, *next;
212
213 map = c->imagemaps[i];
214 while (map != NULL) {
215 next = map->next;
216 imagemap_freelist(map->list);
217 free(map->key);
218 free(map);
219 map = next;
220 }
221 }
222
223 free(c->imagemaps);
224 }
225
226 /**
227 * Dump imagemap data to the log
228 *
229 * \param c The containing content
230 */
imagemap_dump(html_content * c)231 void imagemap_dump(html_content *c)
232 {
233 unsigned int i;
234
235 int j;
236
237 assert(c != NULL);
238
239 if (c->imagemaps == NULL)
240 return;
241
242 for (i = 0; i != HASH_SIZE; i++) {
243 struct imagemap *map;
244 struct mapentry *entry;
245
246 map = c->imagemaps[i];
247 while (map != NULL) {
248 NSLOG(netsurf, INFO, "Imagemap: %s", map->key);
249
250 for (entry = map->list; entry; entry = entry->next) {
251 switch (entry->type) {
252 case IMAGEMAP_DEFAULT:
253 NSLOG(netsurf, INFO, "\tDefault: %s",
254 nsurl_access(entry->url));
255 break;
256 case IMAGEMAP_RECT:
257 NSLOG(netsurf, INFO,
258 "\tRectangle: %s: [(%d,%d),(%d,%d)]",
259 nsurl_access(entry->url),
260 entry->bounds.rect.x0,
261 entry->bounds.rect.y0,
262 entry->bounds.rect.x1,
263 entry->bounds.rect.y1);
264 break;
265 case IMAGEMAP_CIRCLE:
266 NSLOG(netsurf, INFO,
267 "\tCircle: %s: [(%d,%d),%d]",
268 nsurl_access(entry->url),
269 entry->bounds.circle.x,
270 entry->bounds.circle.y,
271 entry->bounds.circle.r);
272 break;
273 case IMAGEMAP_POLY:
274 NSLOG(netsurf, INFO,
275 "\tPolygon: %s:",
276 nsurl_access(entry->url));
277 for (j = 0; j != entry->bounds.poly.num;
278 j++) {
279 fprintf(stderr, "(%d,%d) ",
280 (int)entry->bounds.poly.xcoords[j],
281 (int)entry->bounds.poly.ycoords[j]);
282 }
283 fprintf(stderr,"\n");
284 break;
285 }
286 }
287 map = map->next;
288 }
289 }
290 }
291
292 /**
293 * Adds an imagemap entry to the list
294 *
295 * \param c The html content that the imagemap belongs to
296 * \param n The xmlNode representing the entry to add
297 * \param base_url Base URL for resolving relative URLs
298 * \param entry Pointer to list of entries
299 * \param tagtype The type of tag
300 * \return false on memory exhaustion, true otherwise
301 */
302 static bool
imagemap_addtolist(const struct html_content * c,dom_node * n,nsurl * base_url,struct mapentry ** entry,dom_string * tagtype)303 imagemap_addtolist(const struct html_content *c,
304 dom_node *n,
305 nsurl *base_url,
306 struct mapentry **entry,
307 dom_string *tagtype)
308 {
309 dom_exception exc;
310 dom_string *href = NULL, *target = NULL, *shape = NULL;
311 dom_string *coords = NULL;
312 struct mapentry *new_map, *temp;
313 bool ret = true;
314
315 if (dom_string_caseless_isequal(tagtype, corestring_dom_area)) {
316 bool nohref = false;
317 exc = dom_element_has_attribute(n,
318 corestring_dom_nohref, &nohref);
319 if ((exc != DOM_NO_ERR) || nohref)
320 /* Skip <area nohref="anything" /> */
321 goto ok_out;
322 }
323
324 exc = dom_element_get_attribute(n, corestring_dom_href, &href);
325 if (exc != DOM_NO_ERR || href == NULL) {
326 /* No href="" attribute, skip this element */
327 goto ok_out;
328 }
329
330 exc = dom_element_get_attribute(n, corestring_dom_target, &target);
331 if (exc != DOM_NO_ERR) {
332 goto ok_out;
333 }
334
335 exc = dom_element_get_attribute(n, corestring_dom_shape, &shape);
336 if (exc != DOM_NO_ERR) {
337 goto ok_out;
338 }
339
340 /* If there's no shape, we default to rectangles */
341 if (shape == NULL)
342 shape = dom_string_ref(corestring_dom_rect);
343
344 if (!dom_string_caseless_lwc_isequal(shape, corestring_lwc_default)) {
345 /* If not 'default' and there's no 'coords' give up */
346 exc = dom_element_get_attribute(n, corestring_dom_coords,
347 &coords);
348 if (exc != DOM_NO_ERR || coords == NULL) {
349 goto ok_out;
350 }
351 }
352
353 new_map = calloc(1, sizeof(*new_map));
354 if (new_map == NULL) {
355 goto bad_out;
356 }
357
358 if (dom_string_caseless_lwc_isequal(shape, corestring_lwc_rect) ||
359 dom_string_caseless_lwc_isequal(shape, corestring_lwc_rectangle))
360 new_map->type = IMAGEMAP_RECT;
361 else if (dom_string_caseless_lwc_isequal(shape, corestring_lwc_circle))
362 new_map->type = IMAGEMAP_CIRCLE;
363 else if (dom_string_caseless_lwc_isequal(shape, corestring_lwc_poly) ||
364 dom_string_caseless_lwc_isequal(shape, corestring_lwc_polygon))
365 new_map->type = IMAGEMAP_POLY;
366 else if (dom_string_caseless_lwc_isequal(shape, corestring_lwc_default))
367 new_map->type = IMAGEMAP_DEFAULT;
368 else
369 goto bad_out;
370
371 if (box_extract_link(c, href, base_url, &new_map->url) == false)
372 goto bad_out;
373
374 if (new_map->url == NULL) {
375 /* non-fatal error -> ignore this */
376 goto ok_free_map_out;
377 }
378
379 if (target != NULL) {
380 /* Copy target dom string into the map data */
381 new_map->target = malloc(dom_string_byte_length(target) + 1);
382 if (new_map->target == NULL)
383 goto bad_out;
384
385 memcpy(new_map->target,
386 dom_string_data(target),
387 dom_string_byte_length(target));
388
389 new_map->target[dom_string_byte_length(target)] = 0;
390 }
391
392 if (new_map->type != IMAGEMAP_DEFAULT) {
393 int x, y;
394 float *xcoords, *ycoords;
395 /* coordinates are a comma-separated list of values */
396 char *val = strtok((char *)dom_string_data(coords), ",");
397 int num = 1;
398
399 switch (new_map->type) {
400 case IMAGEMAP_RECT:
401 /* (left, top, right, bottom) */
402 while (val != NULL && num <= 4) {
403 switch (num) {
404 case 1:
405 new_map->bounds.rect.x0 = atoi(val);
406 break;
407 case 2:
408 new_map->bounds.rect.y0 = atoi(val);
409 break;
410 case 3:
411 new_map->bounds.rect.x1 = atoi(val);
412 break;
413 case 4:
414 new_map->bounds.rect.y1 = atoi(val);
415 break;
416 }
417
418 num++;
419 val = strtok(NULL, ",");
420 }
421 break;
422 case IMAGEMAP_CIRCLE:
423 /* (x, y, radius ) */
424 while (val != NULL && num <= 3) {
425 switch (num) {
426 case 1:
427 new_map->bounds.circle.x = atoi(val);
428 break;
429 case 2:
430 new_map->bounds.circle.y = atoi(val);
431 break;
432 case 3:
433 new_map->bounds.circle.r = atoi(val);
434 break;
435 }
436
437 num++;
438 val = strtok(NULL, ",");
439 }
440 break;
441 case IMAGEMAP_POLY:
442 new_map->bounds.poly.xcoords = NULL;
443 new_map->bounds.poly.ycoords = NULL;
444
445 while (val != NULL) {
446 x = atoi(val);
447
448 val = strtok(NULL, ",");
449 if (val == NULL)
450 break;
451
452 y = atoi(val);
453
454 xcoords = realloc(new_map->bounds.poly.xcoords,
455 num * sizeof(float));
456 if (xcoords == NULL) {
457 goto bad_out;
458 }
459 new_map->bounds.poly.xcoords = xcoords;
460
461 ycoords = realloc(new_map->bounds.poly.ycoords,
462 num * sizeof(float));
463 if (ycoords == NULL) {
464 goto bad_out;
465 }
466 new_map->bounds.poly.ycoords = ycoords;
467
468 new_map->bounds.poly.xcoords[num - 1] = x;
469 new_map->bounds.poly.ycoords[num - 1] = y;
470
471 num++;
472 val = strtok(NULL, ",");
473 }
474
475 new_map->bounds.poly.num = num - 1;
476
477 break;
478 default:
479 break;
480 }
481 }
482
483 new_map->next = NULL;
484
485 if (*entry) {
486 /* add to END of list */
487 for (temp = (*entry); temp->next != NULL; temp = temp->next)
488 ;
489 temp->next = new_map;
490 } else {
491 (*entry) = new_map;
492 }
493
494 /* All good, linked in, let's clean up */
495 goto ok_out;
496
497 bad_out:
498 ret = false;
499 ok_free_map_out:
500 if (new_map != NULL) {
501 if (new_map->url != NULL)
502 nsurl_unref(new_map->url);
503 if (new_map->type == IMAGEMAP_POLY &&
504 new_map->bounds.poly.ycoords != NULL)
505 free(new_map->bounds.poly.ycoords);
506 if (new_map->type == IMAGEMAP_POLY &&
507 new_map->bounds.poly.xcoords != NULL)
508 free(new_map->bounds.poly.xcoords);
509 if (new_map->target != NULL)
510 free(new_map->target);
511
512 free(new_map);
513 }
514 ok_out:
515 if (href != NULL)
516 dom_string_unref(href);
517 if (target != NULL)
518 dom_string_unref(target);
519 if (shape != NULL)
520 dom_string_unref(shape);
521 if (coords != NULL)
522 dom_string_unref(coords);
523
524 return ret;
525 }
526
527 /**
528 * Extract an imagemap from html source
529 *
530 * \param node XML node containing map
531 * \param c Content containing document
532 * \param entry List of map entries
533 * \param tname The sub-tags to consider on this pass
534 * \return false on memory exhaustion, true otherwise
535 */
536 static bool
imagemap_extract_map_entries(dom_node * node,html_content * c,struct mapentry ** entry,dom_string * tname)537 imagemap_extract_map_entries(dom_node *node, html_content *c,
538 struct mapentry **entry, dom_string *tname)
539 {
540 dom_nodelist *nlist;
541 dom_exception exc;
542 unsigned long ent;
543 uint32_t tag_count;
544
545 exc = dom_element_get_elements_by_tag_name(node, tname, &nlist);
546 if (exc != DOM_NO_ERR) {
547 return false;
548 }
549
550 exc = dom_nodelist_get_length(nlist, &tag_count);
551 if (exc != DOM_NO_ERR) {
552 dom_nodelist_unref(nlist);
553 return false;
554 }
555
556 for (ent = 0; ent < tag_count; ++ent) {
557 dom_node *subnode;
558
559 exc = dom_nodelist_item(nlist, ent, &subnode);
560 if (exc != DOM_NO_ERR) {
561 dom_nodelist_unref(nlist);
562 return false;
563 }
564 if (imagemap_addtolist(c, subnode, c->base_url,
565 entry, tname) == false) {
566 dom_node_unref(subnode);
567 dom_nodelist_unref(nlist);
568 return false;
569 }
570 dom_node_unref(subnode);
571 }
572
573 dom_nodelist_unref(nlist);
574
575 return true;
576 }
577
578 /**
579 * Extract an imagemap from html source
580 *
581 * \param node XML node containing map
582 * \param c Content containing document
583 * \param entry List of map entries
584 * \return false on memory exhaustion, true otherwise
585 */
imagemap_extract_map(dom_node * node,html_content * c,struct mapentry ** entry)586 static bool imagemap_extract_map(dom_node *node, html_content *c,
587 struct mapentry **entry)
588 {
589 if (imagemap_extract_map_entries(node, c, entry,
590 corestring_dom_area) == false)
591 return false;
592 return imagemap_extract_map_entries(node, c, entry,
593 corestring_dom_a);
594 }
595
596 /**
597 * Extract all imagemaps from a document tree
598 *
599 * \param c The content to extract imagemaps from.
600 * \return false on memory exhaustion, true otherwise
601 */
602 nserror
imagemap_extract(html_content * c)603 imagemap_extract(html_content *c)
604 {
605 dom_nodelist *nlist;
606 dom_exception exc;
607 unsigned long mapnr;
608 uint32_t maybe_maps;
609 nserror ret = NSERROR_OK;
610
611 exc = dom_document_get_elements_by_tag_name(c->document,
612 corestring_dom_map,
613 &nlist);
614 if (exc != DOM_NO_ERR) {
615 return NSERROR_DOM;
616 }
617
618 exc = dom_nodelist_get_length(nlist, &maybe_maps);
619 if (exc != DOM_NO_ERR) {
620 ret = NSERROR_DOM;
621 goto out_nlist;
622 }
623
624 for (mapnr = 0; mapnr < maybe_maps; ++mapnr) {
625 dom_node *node;
626 dom_string *name;
627 exc = dom_nodelist_item(nlist, mapnr, &node);
628 if (exc != DOM_NO_ERR) {
629 ret = NSERROR_DOM;
630 goto out_nlist;
631 }
632
633 exc = dom_element_get_attribute(node, corestring_dom_id,
634 &name);
635 if (exc != DOM_NO_ERR) {
636 dom_node_unref(node);
637 ret = NSERROR_DOM;
638 goto out_nlist;
639 }
640
641 if (name == NULL) {
642 exc = dom_element_get_attribute(node,
643 corestring_dom_name,
644 &name);
645 if (exc != DOM_NO_ERR) {
646 dom_node_unref(node);
647 ret = NSERROR_DOM;
648 goto out_nlist;
649 }
650 }
651
652 if (name != NULL) {
653 struct mapentry *entry = NULL;
654 if (imagemap_extract_map(node, c, &entry) == false) {
655 if (entry != NULL) {
656 imagemap_freelist(entry);
657 }
658
659 dom_string_unref(name);
660 dom_node_unref(node);
661 ret = NSERROR_NOMEM; /** @todo check this */
662 goto out_nlist;
663 }
664
665 /* imagemap_extract_map may not extract anything,
666 * so entry can still be NULL here. This isn't an
667 * error as it just means that we've encountered
668 * an incorrectly defined <map>...</map> block
669 */
670 if ((entry != NULL) &&
671 (imagemap_add(c, name, entry) == false)) {
672 imagemap_freelist(entry);
673
674 dom_string_unref(name);
675 dom_node_unref(node);
676 ret = NSERROR_NOMEM; /** @todo check this */
677 goto out_nlist;
678 }
679 }
680
681 dom_string_unref(name);
682 dom_node_unref(node);
683 }
684
685 out_nlist:
686
687 dom_nodelist_unref(nlist);
688
689 return ret;
690 }
691
692 /**
693 * Test if a point lies within an arbitrary polygon
694 * Modified from comp.graphics.algorithms FAQ 2.03
695 *
696 * \param num Number of vertices
697 * \param xpt Array of x coordinates
698 * \param ypt Array of y coordinates
699 * \param x Left hand edge of containing box
700 * \param y Top edge of containing box
701 * \param click_x X coordinate of click
702 * \param click_y Y coordinate of click
703 * \return 1 if point is in polygon, 0 if outside. 0 or 1 if on boundary
704 */
705 static int
imagemap_point_in_poly(int num,float * xpt,float * ypt,unsigned long x,unsigned long y,unsigned long click_x,unsigned long click_y)706 imagemap_point_in_poly(int num, float *xpt, float *ypt, unsigned long x,
707 unsigned long y, unsigned long click_x, unsigned long click_y)
708 {
709 int i, j, c = 0;
710
711 assert(xpt != NULL);
712 assert(ypt != NULL);
713
714 for (i = 0, j = num - 1; i < num; j = i++) {
715 if ((((ypt[i] + y <= click_y) && (click_y < ypt[j] + y)) ||
716 ((ypt[j] + y <= click_y) && (click_y < ypt[i] + y))) &&
717 (click_x < (xpt[j] - xpt[i]) *
718 (click_y - (ypt[i] + y)) / (ypt[j] - ypt[i]) + xpt[i] + x))
719 c = !c;
720 }
721
722 return c;
723 }
724
725 /**
726 * Retrieve url associated with imagemap entry
727 *
728 * \param c The containing content
729 * \param key The map name to search for
730 * \param x The left edge of the containing box
731 * \param y The top edge of the containing box
732 * \param click_x The horizontal location of the click
733 * \param click_y The vertical location of the click
734 * \param target Pointer to location to receive target pointer (if any)
735 * \return The url associated with this area, or NULL if not found
736 */
imagemap_get(struct html_content * c,const char * key,unsigned long x,unsigned long y,unsigned long click_x,unsigned long click_y,const char ** target)737 nsurl *imagemap_get(struct html_content *c, const char *key,
738 unsigned long x, unsigned long y,
739 unsigned long click_x, unsigned long click_y,
740 const char **target)
741 {
742 unsigned int slot = 0;
743 struct imagemap *map;
744 struct mapentry *entry;
745 unsigned long cx, cy;
746
747 assert(c != NULL);
748
749 if (key == NULL)
750 return NULL;
751
752 if (c->imagemaps == NULL)
753 return NULL;
754
755 slot = imagemap_hash(key);
756
757 for (map = c->imagemaps[slot]; map != NULL; map = map->next) {
758 if (map->key != NULL && strcasecmp(map->key, key) == 0)
759 break;
760 }
761
762 if (map == NULL || map->list == NULL)
763 return NULL;
764
765 for (entry = map->list; entry; entry = entry->next) {
766 switch (entry->type) {
767 case IMAGEMAP_DEFAULT:
768 /* just return the URL. no checks required */
769 if (target)
770 *target = entry->target;
771 return entry->url;
772 break;
773 case IMAGEMAP_RECT:
774 if (click_x >= x + entry->bounds.rect.x0 &&
775 click_x <= x + entry->bounds.rect.x1 &&
776 click_y >= y + entry->bounds.rect.y0 &&
777 click_y <= y + entry->bounds.rect.y1) {
778 if (target)
779 *target = entry->target;
780 return entry->url;
781 }
782 break;
783 case IMAGEMAP_CIRCLE:
784 cx = x + entry->bounds.circle.x - click_x;
785 cy = y + entry->bounds.circle.y - click_y;
786 if ((cx * cx + cy * cy) <=
787 (unsigned long) (entry->bounds.circle.r *
788 entry->bounds.circle.r)) {
789 if (target)
790 *target = entry->target;
791 return entry->url;
792 }
793 break;
794 case IMAGEMAP_POLY:
795 if (imagemap_point_in_poly(entry->bounds.poly.num,
796 entry->bounds.poly.xcoords,
797 entry->bounds.poly.ycoords, x, y,
798 click_x, click_y)) {
799 if (target)
800 *target = entry->target;
801 return entry->url;
802 }
803 break;
804 }
805 }
806
807 if (target)
808 *target = NULL;
809
810 return NULL;
811 }
812