1 /* jmap_util.c -- Helper routines for JMAP
2 *
3 * Copyright (c) 1994-2018 Carnegie Mellon University. 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 *
9 * 1. Redistributions of source code must retain the above copyright
10 * notice, this list of conditions and the following disclaimer.
11 *
12 * 2. Redistributions in binary form must reproduce the above copyright
13 * notice, this list of conditions and the following disclaimer in
14 * the documentation and/or other materials provided with the
15 * distribution.
16 *
17 * 3. The name "Carnegie Mellon University" must not be used to
18 * endorse or promote products derived from this software without
19 * prior written permission. For permission or any legal
20 * details, please contact
21 * Carnegie Mellon University
22 * Center for Technology Transfer and Enterprise Creation
23 * 4615 Forbes Avenue
24 * Suite 302
25 * Pittsburgh, PA 15213
26 * (412) 268-7393, fax: (412) 268-7395
27 * innovation@andrew.cmu.edu
28 *
29 * 4. Redistributions of any form whatsoever must retain the following
30 * acknowledgment:
31 * "This product includes software developed by Computing Services
32 * at Carnegie Mellon University (http://www.cmu.edu/computing/)."
33 *
34 * CARNEGIE MELLON UNIVERSITY DISCLAIMS ALL WARRANTIES WITH REGARD TO
35 * THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
36 * AND FITNESS, IN NO EVENT SHALL CARNEGIE MELLON UNIVERSITY BE LIABLE
37 * FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
38 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
39 * AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
40 * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
41 *
42 */
43
44 #include <config.h>
45
46 #include <string.h>
47 #include <syslog.h>
48 #include <assert.h>
49
50 #include <sasl/saslutil.h>
51
52 #include "annotate.h"
53 #include "carddav_db.h"
54 #include "global.h"
55 #include "hash.h"
56 #include "index.h"
57 #include "jmap_util.h"
58 #include "json_support.h"
59 #include "search_query.h"
60 #include "times.h"
61 #include "xapian_wrap.h"
62
63 #ifdef HAVE_LIBCHARDET
64 #include <chardet/chardet.h>
65 #endif
66
67 /* generated headers are not necessarily in current directory */
68 #include "imap/imap_err.h"
69
jmap_readprop_full(json_t * root,const char * prefix,const char * name,int mandatory,json_t * invalid,const char * fmt,void * dst)70 EXPORTED int jmap_readprop_full(json_t *root, const char *prefix, const char *name,
71 int mandatory, json_t *invalid, const char *fmt,
72 void *dst)
73 {
74 int r = 0;
75 json_t *jval = json_object_get(root, name);
76 if (!jval && mandatory) {
77 r = -1;
78 } else if (jval) {
79 json_error_t err;
80 if (json_unpack_ex(jval, &err, 0, fmt, dst)) {
81 r = -2;
82 } else {
83 r = 1;
84 }
85 }
86 if (r < 0 && prefix) {
87 struct buf buf = BUF_INITIALIZER;
88 buf_printf(&buf, "%s.%s", prefix, name);
89 json_array_append_new(invalid, json_string(buf_cstring(&buf)));
90 buf_free(&buf);
91 } else if (r < 0) {
92 json_array_append_new(invalid, json_string(name));
93 }
94 return r;
95 }
96
jmap_pointer_needsencode(const char * src)97 EXPORTED int jmap_pointer_needsencode(const char *src)
98 {
99 return strchr(src, '/') || strchr(src, '~');
100 }
101
jmap_pointer_encode(const char * src)102 EXPORTED char* jmap_pointer_encode(const char *src)
103 {
104 struct buf buf = BUF_INITIALIZER;
105 const char *base, *top;
106 buf_ensure(&buf, strlen(src));
107
108 base = src;
109 top = base;
110 while (*base) {
111 for (top = base; *top && *top != '~' && *top != '/'; top++)
112 ;
113 if (!*top) break;
114
115 buf_appendmap(&buf, base, top-base);
116 if (*top == '~') {
117 buf_appendmap(&buf, "~0", 2);
118 top++;
119 } else if (*top == '/') {
120 buf_appendmap(&buf, "~1", 2);
121 top++;
122 }
123 base = top;
124 }
125 buf_appendmap(&buf, base, top-base);
126 return buf_release(&buf);
127 }
128
jmap_pointer_decode(const char * src,size_t len)129 EXPORTED char *jmap_pointer_decode(const char *src, size_t len)
130 {
131 struct buf buf = BUF_INITIALIZER;
132 const char *base, *top, *end;
133
134 buf_ensure(&buf, len);
135 end = src + len;
136
137 base = src;
138 while (base < end && (top = strchr(base, '~')) && top < end) {
139 buf_appendmap(&buf, base, top-base);
140
141 if (top < end-1 && *(top+1) == '0') {
142 buf_appendcstr(&buf, "~");
143 base = top + 2;
144 } else if (top < end-1 && *(top+1) == '1') {
145 buf_appendcstr(&buf, "/");
146 base = top + 2;
147 } else {
148 buf_appendcstr(&buf, "~");
149 base = top + 1;
150 }
151 }
152 if (base < end) {
153 buf_appendmap(&buf, base, end-base);
154 }
155
156 return buf_release(&buf);
157 }
158
jmap_patchobject_apply(json_t * val,json_t * patch,json_t * invalid)159 EXPORTED json_t* jmap_patchobject_apply(json_t *val, json_t *patch, json_t *invalid)
160 {
161 const char *path;
162 json_t *newval, *dst;
163
164 dst = json_deep_copy(val);
165 json_object_foreach(patch, path, newval) {
166 /* Start traversal at root object */
167 json_t *it = dst;
168 const char *base = path, *top;
169 /* Find path in object tree */
170 while ((top = strchr(base, '/'))) {
171 char *name = jmap_pointer_decode(base, top-base);
172 it = json_object_get(it, name);
173 free(name);
174 base = top + 1;
175 }
176 if (!it) {
177 /* No such path in 'val' */
178 if (invalid) {
179 json_array_append_new(invalid, json_string(path));
180 }
181 json_decref(dst);
182 return NULL;
183 }
184 /* Replace value at path */
185 char *name = jmap_pointer_decode(base, strlen(base));
186 if (newval == json_null()) {
187 json_object_del(it, name);
188 } else {
189 json_object_set(it, name, newval);
190 }
191 free(name);
192 }
193
194 return dst;
195 }
196
jmap_patchobject_set(json_t * diff,struct buf * path,const char * key,json_t * val)197 static void jmap_patchobject_set(json_t *diff, struct buf *path,
198 const char *key, json_t *val)
199 {
200 char *enckey = jmap_pointer_encode(key);
201 size_t len = buf_len(path);
202 if (len) buf_appendcstr(path, "/");
203 buf_appendcstr(path, enckey);
204 json_object_set(diff, buf_cstring(path), val);
205 buf_truncate(path, len);
206 free(enckey);
207 }
208
jmap_patchobject_diff(json_t * diff,struct buf * path,json_t * src,json_t * dst)209 static void jmap_patchobject_diff(json_t *diff, struct buf *path,
210 json_t *src, json_t *dst)
211 {
212 if (!json_is_object(src) || !json_is_object(dst))
213 return;
214
215 const char *key;
216 json_t *val;
217
218 // Add any properties that are set in dst but not in src
219 json_object_foreach(dst, key, val) {
220 if (json_object_get(src, key) == NULL) {
221 jmap_patchobject_set(diff, path, key, val);
222 }
223 }
224
225 // Remove any properties that are set in src but not in dst
226 json_object_foreach(src, key, val) {
227 if (json_object_get(dst, key) == NULL) {
228 jmap_patchobject_set(diff, path, key, json_null());
229 }
230 }
231
232 // Handle properties that exist in both src and dst
233 json_object_foreach(dst, key, val) {
234 json_t *srcval = json_object_get(src, key);
235 if (!srcval) {
236 continue;
237 }
238 if (json_typeof(val) != JSON_OBJECT) {
239 if (!json_equal(val, srcval)) {
240 jmap_patchobject_set(diff, path, key, val);
241 }
242 }
243 else if (json_typeof(srcval) != JSON_OBJECT) {
244 jmap_patchobject_set(diff, path, key, val);
245 }
246 else {
247 char *enckey = jmap_pointer_encode(key);
248 size_t len = buf_len(path);
249 if (len) buf_appendcstr(path, "/");
250 buf_appendcstr(path, enckey);
251 jmap_patchobject_diff(diff, path, srcval, val);
252 buf_truncate(path, len);
253 free(enckey);
254 }
255 }
256 }
257
jmap_patchobject_create(json_t * src,json_t * dst)258 EXPORTED json_t *jmap_patchobject_create(json_t *src, json_t *dst)
259 {
260 json_t *diff = json_object();
261 struct buf buf = BUF_INITIALIZER;
262
263 jmap_patchobject_diff(diff, &buf, src, dst);
264
265 buf_free(&buf);
266 return diff;
267 }
268
jmap_filterprops(json_t * jobj,hash_table * props)269 EXPORTED void jmap_filterprops(json_t *jobj, hash_table *props)
270 {
271 if (!props) return;
272
273 const char *key;
274 json_t *jval;
275 void *tmp;
276 json_object_foreach_safe(jobj, tmp, key, jval) {
277 if (!hash_lookup(key, props)) {
278 json_object_del(jobj, key);
279 }
280 }
281 }
282
address_to_smtp(smtp_addr_t * smtpaddr,json_t * addr)283 static void address_to_smtp(smtp_addr_t *smtpaddr, json_t *addr)
284 {
285 smtpaddr->addr = xstrdup(json_string_value(json_object_get(addr, "email")));
286
287 const char *key;
288 json_t *val;
289 struct buf xtext = BUF_INITIALIZER;
290 json_object_foreach(json_object_get(addr, "parameters"), key, val) {
291 /* We never take AUTH at face value */
292 if (!strcasecmp(key, "AUTH")) {
293 continue;
294 }
295 /* We handle FUTURERELEASE ourselves */
296 else if (!strcasecmp(key, "HOLDFOR") || !strcasecmp(key, "HOLDUNTIL")) {
297 continue;
298 }
299 /* Encode xtext value */
300 if (json_is_string(val)) {
301 const char *p;
302 for (p = json_string_value(val); *p; p++) {
303 if (('!' <= *p && *p <= '~') && *p != '=' && *p != '+') {
304 buf_putc(&xtext, *p);
305 }
306 else buf_printf(&xtext, "+%02X", *p);
307 }
308 }
309 /* Build parameter */
310 smtp_param_t *param = xzmalloc(sizeof(smtp_param_t));
311 param->key = xstrdup(key);
312 param->val = buf_len(&xtext) ? xstrdup(buf_cstring(&xtext)) : NULL;
313 ptrarray_append(&smtpaddr->params, param);
314 buf_reset(&xtext);
315 }
316 buf_free(&xtext);
317 }
318
jmap_emailsubmission_envelope_to_smtp(smtp_envelope_t * smtpenv,json_t * env)319 EXPORTED void jmap_emailsubmission_envelope_to_smtp(smtp_envelope_t *smtpenv,
320 json_t *env)
321 {
322 address_to_smtp(&smtpenv->from, json_object_get(env, "mailFrom"));
323 size_t i;
324 json_t *val;
325 json_array_foreach(json_object_get(env, "rcptTo"), i, val) {
326 smtp_addr_t *smtpaddr = xzmalloc(sizeof(smtp_addr_t));
327 address_to_smtp(smtpaddr, val);
328 ptrarray_append(&smtpenv->rcpts, smtpaddr);
329 }
330 }
331
jmap_fetch_snoozed(const char * mbox,uint32_t uid)332 EXPORTED json_t *jmap_fetch_snoozed(const char *mbox, uint32_t uid)
333 {
334 /* get the snoozed annotation */
335 const char *annot = IMAP_ANNOT_NS "snoozed";
336 struct buf value = BUF_INITIALIZER;
337 json_t *snooze = NULL;
338 int r;
339
340 r = annotatemore_msg_lookup(mbox, uid, annot, "", &value);
341
342 if (!r) {
343 if (!buf_len(&value)) {
344 /* get the legacy snoozed-until annotation */
345 annot = IMAP_ANNOT_NS "snoozed-until";
346
347 r = annotatemore_msg_lookup(mbox, uid, annot, "", &value);
348 if (!r && buf_len(&value)) {
349 /* build a SnoozeDetails object from the naked "until" value */
350 snooze = json_pack("{s:s}",
351 "until", json_string(buf_cstring(&value)));
352 }
353 }
354 else {
355 json_error_t jerr;
356
357 snooze = json_loadb(buf_base(&value), buf_len(&value), 0, &jerr);
358 }
359 }
360
361 buf_free(&value);
362
363 return snooze;
364 }
365
jmap_email_keyword_is_valid(const char * keyword)366 EXPORTED int jmap_email_keyword_is_valid(const char *keyword)
367 {
368 const char *p;
369
370 if (*keyword == '\0') {
371 return 0;
372 }
373 if (strlen(keyword) > 255) {
374 return 0;
375 }
376 for (p = keyword; *p; p++) {
377 if (*p < 0x21 || *p > 0x7e) {
378 return 0;
379 }
380 switch(*p) {
381 case '(':
382 case ')':
383 case '{':
384 case ']':
385 case '%':
386 case '*':
387 case '"':
388 case '\\':
389 return 0;
390 default:
391 ;
392 }
393 }
394 return 1;
395 }
396
jmap_keyword_to_imap(const char * keyword)397 EXPORTED const char *jmap_keyword_to_imap(const char *keyword)
398 {
399 if (!strcasecmp(keyword, "$Seen")) {
400 return "\\Seen";
401 }
402 else if (!strcasecmp(keyword, "$Flagged")) {
403 return "\\Flagged";
404 }
405 else if (!strcasecmp(keyword, "$Answered")) {
406 return "\\Answered";
407 }
408 else if (!strcasecmp(keyword, "$Draft")) {
409 return "\\Draft";
410 }
411 else if (jmap_email_keyword_is_valid(keyword)) {
412 return keyword;
413 }
414 return NULL;
415 }
416
jmap_parser_fini(struct jmap_parser * parser)417 HIDDEN void jmap_parser_fini(struct jmap_parser *parser)
418 {
419 strarray_fini(&parser->path);
420 json_decref(parser->invalid);
421 buf_free(&parser->buf);
422 }
423
jmap_parser_push(struct jmap_parser * parser,const char * prop)424 HIDDEN void jmap_parser_push(struct jmap_parser *parser, const char *prop)
425 {
426 strarray_push(&parser->path, prop);
427 }
428
jmap_parser_push_index(struct jmap_parser * parser,const char * prop,size_t index,const char * name)429 HIDDEN void jmap_parser_push_index(struct jmap_parser *parser, const char *prop,
430 size_t index, const char *name)
431 {
432 /* TODO make this more clever: won't need to printf most of the time */
433 buf_reset(&parser->buf);
434 if (name) buf_printf(&parser->buf, "%s[%zu:%s]", prop, index, name);
435 else buf_printf(&parser->buf, "%s[%zu]", prop, index);
436 strarray_push(&parser->path, buf_cstring(&parser->buf));
437 buf_reset(&parser->buf);
438 }
439
jmap_parser_push_name(struct jmap_parser * parser,const char * prop,const char * name)440 HIDDEN void jmap_parser_push_name(struct jmap_parser *parser,
441 const char *prop, const char *name)
442 {
443 /* TODO make this more clever: won't need to printf most of the time */
444 buf_reset(&parser->buf);
445 buf_printf(&parser->buf, "%s{%s}", prop, name);
446 strarray_push(&parser->path, buf_cstring(&parser->buf));
447 buf_reset(&parser->buf);
448 }
449
jmap_parser_pop(struct jmap_parser * parser)450 HIDDEN void jmap_parser_pop(struct jmap_parser *parser)
451 {
452 free(strarray_pop(&parser->path));
453 }
454
jmap_parser_path(struct jmap_parser * parser,struct buf * buf)455 HIDDEN const char* jmap_parser_path(struct jmap_parser *parser, struct buf *buf)
456 {
457 int i;
458 buf_reset(buf);
459
460 for (i = 0; i < parser->path.count; i++) {
461 const char *p = strarray_nth(&parser->path, i);
462 if (jmap_pointer_needsencode(p)) {
463 char *tmp = jmap_pointer_encode(p);
464 buf_appendcstr(buf, tmp);
465 free(tmp);
466 } else {
467 buf_appendcstr(buf, p);
468 }
469 if ((i + 1) < parser->path.count) {
470 buf_appendcstr(buf, "/");
471 }
472 }
473
474 return buf_cstring(buf);
475 }
476
jmap_parser_invalid(struct jmap_parser * parser,const char * prop)477 HIDDEN void jmap_parser_invalid(struct jmap_parser *parser, const char *prop)
478 {
479 if (prop)
480 jmap_parser_push(parser, prop);
481
482 json_array_append_new(parser->invalid,
483 json_string(jmap_parser_path(parser, &parser->buf)));
484
485 if (prop)
486 jmap_parser_pop(parser);
487 }
488
jmap_server_error(int r)489 HIDDEN json_t *jmap_server_error(int r)
490 {
491 switch (r) {
492 case IMAP_CONVERSATION_GUIDLIMIT:
493 return json_pack("{s:s}", "type", "tooManyMailboxes");
494 case IMAP_QUOTA_EXCEEDED:
495 return json_pack("{s:s}", "type", "overQuota");
496 default:
497 return json_pack("{s:s, s:s}",
498 "type", "serverFail",
499 "description", error_message(r));
500 }
501 }
502
jmap_encode_base64_nopad(const char * data,size_t len)503 HIDDEN char *jmap_encode_base64_nopad(const char *data, size_t len)
504 {
505 if (!len) return NULL;
506
507 /* Encode data */
508 size_t b64len = ((len + 2) / 3) << 2;
509 char *b64 = xzmalloc(b64len + 1);
510 if (sasl_encode64(data, len, b64, b64len + 1, NULL) != SASL_OK) {
511 free(b64);
512 return NULL;
513 }
514 /* Remove padding */
515 char *end = b64 + strlen(b64) - 1;
516 while (*end == '=') {
517 *end = '\0';
518 end--;
519 }
520
521 return b64;
522 }
523
jmap_decode_base64_nopad(const char * b64,size_t b64len)524 HIDDEN char *jmap_decode_base64_nopad(const char *b64, size_t b64len)
525 {
526 /* Pad base64 data. */
527 size_t myb64len = b64len;
528 switch (b64len % 4) {
529 case 3:
530 myb64len += 1;
531 break;
532 case 2:
533 myb64len += 2;
534 break;
535 case 1:
536 return NULL;
537 default:
538 ; // do nothing
539 }
540 char *myb64 = xzmalloc(myb64len+1);
541 memcpy(myb64, b64, b64len);
542 switch (myb64len - b64len) {
543 case 2:
544 myb64[b64len+1] = '=';
545 // fall through
546 case 1:
547 myb64[b64len] = '=';
548 break;
549 default:
550 ; // do nothing
551 }
552 /* Decode data. */
553 size_t datalen = ((4 * myb64len / 3) + 3) & ~3;
554 char *data = xzmalloc(datalen + 1);
555 if (sasl_decode64(myb64, myb64len, data, datalen, NULL) != SASL_OK) {
556 free(data);
557 free(myb64);
558 return NULL;
559 }
560
561 free(myb64);
562 return data;
563 }
564
jmap_decode_to_utf8(const char * charset,int encoding,const char * data,size_t datalen,float confidence,char ** val,int * is_encoding_problem)565 EXPORTED const char *jmap_decode_to_utf8(const char *charset, int encoding,
566 const char *data, size_t datalen,
567 float confidence,
568 char **val,
569 int *is_encoding_problem)
570 {
571 charset_t cs = charset_lookupname(charset);
572 char *text = NULL;
573 *val = NULL;
574 const char *charset_id = charset_canon_name(cs);
575 assert(confidence >= 0.0 && confidence <= 1.0);
576
577 /* Attempt fast path without allocation */
578 if (encoding == ENCODING_NONE && data[datalen] == '\0' &&
579 !strcasecmp(charset_id, "UTF-8")) {
580 struct char_counts counts = charset_count_validutf8(data, datalen);
581 if (!counts.invalid) {
582 charset_free(&cs);
583 return data;
584 }
585 }
586
587 /* Can't use fast path. Allocate and try to detect charset. */
588 if (cs == CHARSET_UNKNOWN_CHARSET || encoding == ENCODING_UNKNOWN) {
589 syslog(LOG_INFO, "decode_to_utf8 error (%s, %s)",
590 charset, encoding_name(encoding));
591 if (is_encoding_problem) *is_encoding_problem = 1;
592 goto done;
593 }
594 text = charset_to_utf8(data, datalen, cs, encoding);
595 if (!text) {
596 if (is_encoding_problem) *is_encoding_problem = 1;
597 goto done;
598 }
599
600 size_t textlen = strlen(text);
601 struct char_counts counts = charset_count_validutf8(text, textlen);
602 if (is_encoding_problem)
603 *is_encoding_problem = counts.invalid || counts.replacement;
604
605 if (!strncasecmp(charset_id, "UTF-32", 6)) {
606 /* Special-handle UTF-32. Some clients announce the wrong endianess. */
607 if (counts.invalid || counts.replacement) {
608 charset_t guess_cs = CHARSET_UNKNOWN_CHARSET;
609 if (!strcasecmp(charset_id, "UTF-32") || !strcasecmp(charset_id, "UTF-32BE"))
610 guess_cs = charset_lookupname("UTF-32LE");
611 else
612 guess_cs = charset_lookupname("UTF-32BE");
613 char *guess = charset_to_utf8(data, datalen, guess_cs, encoding);
614 if (guess) {
615 struct char_counts guess_counts = charset_count_validutf8(guess, strlen(guess));
616 if (guess_counts.valid > counts.valid) {
617 free(text);
618 text = guess;
619 counts = guess_counts;
620 textlen = strlen(text);
621 charset_id = charset_canon_name(guess_cs);
622 }
623 }
624 charset_free(&guess_cs);
625 }
626 }
627 else if (!charset_id || !strcasecmp("US-ASCII", charset_id)) {
628 int has_cntrl = 0;
629 size_t i;
630 for (i = 0; i < textlen; i++) {
631 if (iscntrl(text[i])) {
632 has_cntrl = 1;
633 break;
634 }
635 }
636 if (has_cntrl) {
637 /* Could be ISO-2022-JP */
638 charset_t guess_cs = charset_lookupname("ISO-2022-JP");
639 if (guess_cs != CHARSET_UNKNOWN_CHARSET) {
640 char *guess = charset_to_utf8(data, datalen, guess_cs, encoding);
641 if (guess) {
642 struct char_counts guess_counts = charset_count_validutf8(guess, strlen(guess));
643 if (!guess_counts.invalid && !guess_counts.replacement) {
644 free(text);
645 text = guess;
646 counts = guess_counts;
647 textlen = strlen(text);
648 charset_id = charset_canon_name(guess_cs);
649 }
650 else free(guess);
651 }
652 charset_free(&guess_cs);
653 }
654 }
655 }
656
657 #ifdef HAVE_LIBCHARDET
658 if (counts.invalid || counts.replacement) {
659 static Detect *d = NULL;
660 if (!d) d = detect_init();
661
662 DetectObj *obj = detect_obj_init();
663 if (!obj) goto done;
664 detect_reset(&d);
665
666 struct buf buf = BUF_INITIALIZER;
667 charset_decode(&buf, data, datalen, encoding);
668 buf_cstring(&buf);
669 if (detect_handledata_r(&d, buf_base(&buf), buf_len(&buf), &obj) == CHARDET_SUCCESS) {
670 charset_t guess_cs = charset_lookupname(obj->encoding);
671 if (guess_cs != CHARSET_UNKNOWN_CHARSET) {
672 char *guess = charset_to_utf8(data, datalen, guess_cs, encoding);
673 if (guess) {
674 struct char_counts guess_counts =
675 charset_count_validutf8(guess, strlen(guess));
676 if ((guess_counts.valid > counts.valid) &&
677 (obj->confidence >= confidence)) {
678 free(text);
679 text = guess;
680 counts = guess_counts;
681 }
682 else {
683 free(guess);
684 }
685 }
686 charset_free(&guess_cs);
687 }
688 }
689 detect_obj_free(&obj);
690 buf_free(&buf);
691 }
692 #endif
693
694 done:
695 charset_free(&cs);
696 *val = text;
697 return text;
698 }
699