Destroy already created resources if create fails
[apr-util.git] / dbd / apr_dbd_freetds.c
blob4455512ced1e2a71e3cdba9eeb19471a78998ff6
1 /* Licensed to the Apache Software Foundation (ASF) under one or more
2 * contributor license agreements. See the NOTICE file distributed with
3 * this work for additional information regarding copyright ownership.
4 * The ASF licenses this file to You under the Apache License, Version 2.0
5 * (the "License"); you may not use this file except in compliance with
6 * the License. You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
17 #include "apu.h"
18 #include "apu_config.h"
20 /* COMPILE_STUBS: compile stubs for unimplemented functions.
22 * This is required to compile in /trunk/, but can be
23 * undefined to compile a driver for httpd-2.2 and other
24 * APR-1.2 applications
26 #define COMPILE_STUBS
28 #if APU_HAVE_FREETDS
30 #include <ctype.h>
31 #include <stdlib.h>
33 #include "apr_strings.h"
34 #include "apr_lib.h"
36 #include "apr_pools.h"
37 #include "apr_dbd_internal.h"
39 #include <sybdb.h>
40 #include <stdio.h>
41 #include <sys/types.h>
42 #include <regex.h>
44 /* This probably needs to change for different applications */
45 #define MAX_COL_LEN 256
47 typedef struct freetds_cell_t {
48 int type;
49 DBINT len;
50 BYTE *data;
51 } freetds_cell_t;
53 struct apr_dbd_transaction_t {
54 int mode;
55 int errnum;
56 apr_dbd_t *handle;
59 struct apr_dbd_t {
60 DBPROCESS *proc;
61 apr_dbd_transaction_t *trans;
62 apr_pool_t *pool;
63 const char *params;
64 RETCODE err;
67 struct apr_dbd_results_t {
68 int random;
69 size_t ntuples;
70 size_t sz;
71 apr_pool_t *pool;
72 DBPROCESS *proc;
75 struct apr_dbd_row_t {
76 apr_dbd_results_t *res;
77 BYTE buf[MAX_COL_LEN];
80 struct apr_dbd_prepared_t {
81 int nargs;
82 regex_t **taint;
83 int *sz;
84 char *fmt;
87 #define dbd_freetds_is_success(x) (x == SUCCEED)
89 static int labelnum = 0; /* FIXME */
90 static regex_t dbd_freetds_find_arg;
92 /* execute a query that doesn't return a result set, mop up,
93 * and return and APR-flavoured status
95 static RETCODE freetds_exec(DBPROCESS *proc, const char *query,
96 int want_results, int *nrows)
98 /* TBD */
99 RETCODE rv = dbcmd(proc, query);
100 if (rv != SUCCEED) {
101 return rv;
103 rv = dbsqlexec(proc);
104 if (rv != SUCCEED) {
105 return rv;
107 if (!want_results) {
108 while (dbresults(proc) != NO_MORE_RESULTS) {
109 ++*nrows;
112 return SUCCEED;
114 static apr_status_t clear_result(void *data)
116 /* clear cursor */
117 return (dbcanquery((DBPROCESS*)data) == SUCCEED)
118 ? APR_SUCCESS
119 : APR_EGENERAL;
122 static int dbd_freetds_select(apr_pool_t *pool, apr_dbd_t *sql,
123 apr_dbd_results_t **results,
124 const char *query, int seek)
126 apr_dbd_results_t *res;
127 if (sql->trans && (sql->trans->errnum != SUCCEED)) {
128 return 1;
130 /* the core of this is
131 * dbcmd(proc, query);
132 * dbsqlexec(proc);
133 * while (dbnextrow(dbproc) != NO_MORE_ROWS) {
134 * do things
137 * Ignore seek
140 sql->err = freetds_exec(sql->proc, query, 1, NULL);
141 if (!dbd_freetds_is_success(sql->err)) {
142 if (sql->trans) {
143 sql->trans->errnum = sql->err;
145 return 1;
148 sql->err = dbresults(sql->proc);
149 if (sql->err != SUCCEED) {
150 if (sql->trans) {
151 sql->trans->errnum = sql->err;
153 return 1;
156 if (!*results) {
157 *results = apr_pcalloc(pool, sizeof(apr_dbd_results_t));
159 res = *results;
160 res->proc = sql->proc;
161 res->random = seek;
162 res->pool = pool;
163 res->ntuples = dblastrow(sql->proc);
164 res->sz = dbnumcols(sql->proc);
165 apr_pool_cleanup_register(pool, sql->proc, clear_result,
166 apr_pool_cleanup_null);
168 #if 0
169 /* Now we have a result set. We need to bind to its vars */
170 res->vars = apr_palloc(pool, res->sz * sizeof(freetds_cell_t*));
171 for (i=1; i <= res->sz; ++i) {
172 freetds_cell_t *cell = &res->vars[i-1];
173 cell->type = dbcoltype(sql->proc, i);
174 cell->len = dbcollen(sql->proc, i);
175 cell->data = apr_palloc(pool, cell->len);
176 sql->err = dbbind(sql->proc, i, /*cell->type */ STRINGBIND, cell->len, cell->data);
177 if (sql->err != SUCCEED) {
178 fprintf(stderr, "dbbind error: %d, %d, %d", i, cell->type, cell->len);
180 if ((sql->err != SUCCEED) && (sql->trans != NULL)) {
181 sql->trans->errnum = sql->err;
184 #endif
185 return (sql->err == SUCCEED) ? 0 : 1;
187 static const char *dbd_untaint(apr_pool_t *pool, regex_t *rx, const char *val)
189 regmatch_t match[1];
190 if (rx == NULL) {
191 /* no untaint expression */
192 return val;
194 if (regexec(rx, val, 1, match, 0) == 0) {
195 return apr_pstrndup(pool, val+match[0].rm_so,
196 match[0].rm_eo - match[0].rm_so);
198 return "";
200 static const char *dbd_statement(apr_pool_t *pool,
201 apr_dbd_prepared_t *stmt,
202 int nargs, const char **args)
204 int i;
205 int len;
206 const char *var;
207 char *ret;
208 const char *p_in;
209 char *p_out;
210 char *q;
212 /* compute upper bound on length (since untaint shrinks) */
213 len = strlen(stmt->fmt) +1;
214 for (i=0; i<nargs; ++i) {
215 len += strlen(args[i]) - 2;
217 i = 0;
218 p_in = stmt->fmt;
219 p_out = ret = apr_palloc(pool, len);
220 /* FIXME silly bug - this'll catch %%s */
221 while (q = strstr(p_in, "%s"), q != NULL) {
222 len = q-p_in;
223 strncpy(p_out, p_in, len);
224 p_in += len;
225 p_out += len;
226 var = dbd_untaint(pool, stmt->taint[i], args[i]);
227 len = strlen(var);
228 strncpy(p_out, var, len);
229 p_in += 2;
230 p_out += len;
231 ++i;
233 strcpy(p_out, p_in);
234 return ret;
236 static int dbd_freetds_pselect(apr_pool_t *pool, apr_dbd_t *sql,
237 apr_dbd_results_t **results,
238 apr_dbd_prepared_t *statement,
239 int seek, const char **values)
241 const char *query = dbd_statement(pool, statement,
242 statement->nargs, values);
243 return dbd_freetds_select(pool, sql, results, query, seek);
245 static int dbd_freetds_pvselect(apr_pool_t *pool, apr_dbd_t *sql,
246 apr_dbd_results_t **results,
247 apr_dbd_prepared_t *statement,
248 int seek, va_list args)
250 const char **values;
251 int i;
253 if (sql->trans && sql->trans->errnum) {
254 return sql->trans->errnum;
257 values = apr_palloc(pool, sizeof(*values) * statement->nargs);
259 for (i = 0; i < statement->nargs; i++) {
260 values[i] = va_arg(args, const char*);
263 return dbd_freetds_pselect(pool, sql, results, statement, seek, values);
265 static int dbd_freetds_query(apr_dbd_t *sql, int *nrows, const char *query);
266 static int dbd_freetds_pquery(apr_pool_t *pool, apr_dbd_t *sql,
267 int *nrows, apr_dbd_prepared_t *statement,
268 const char **values)
270 const char *query = dbd_statement(pool, statement,
271 statement->nargs, values);
272 return dbd_freetds_query(sql, nrows, query);
274 static int dbd_freetds_pvquery(apr_pool_t *pool, apr_dbd_t *sql, int *nrows,
275 apr_dbd_prepared_t *statement, va_list args)
277 const char **values;
278 int i;
280 if (sql->trans && sql->trans->errnum) {
281 return sql->trans->errnum;
284 values = apr_palloc(pool, sizeof(*values) * statement->nargs);
286 for (i = 0; i < statement->nargs; i++) {
287 values[i] = va_arg(args, const char*);
289 return dbd_freetds_pquery(pool, sql, nrows, statement, values);
292 static int dbd_freetds_get_row(apr_pool_t *pool, apr_dbd_results_t *res,
293 apr_dbd_row_t **rowp, int rownum)
295 RETCODE rv = 0;
296 apr_dbd_row_t *row = *rowp;
297 int sequential = ((rownum >= 0) && res->random) ? 0 : 1;
299 if (row == NULL) {
300 row = apr_palloc(pool, sizeof(apr_dbd_row_t));
301 *rowp = row;
302 row->res = res;
305 else {
306 if ( sequential ) {
307 ++row->n;
309 else {
310 row->n = rownum;
314 if (sequential) {
315 rv = dbnextrow(res->proc);
317 else {
318 rv = (rownum >= 0) ? dbgetrow(res->proc, rownum) : NO_MORE_ROWS;
320 switch (rv) {
321 case SUCCEED: return 0;
322 case REG_ROW: return 0;
323 case NO_MORE_ROWS:
324 apr_pool_cleanup_run(pool, res->proc, clear_result);
325 *rowp = NULL;
326 return -1;
327 case FAIL: return 1;
328 case BUF_FULL: return 2; /* FIXME */
329 default: return 3;
332 return 0;
335 static const char *dbd_freetds_get_entry(const apr_dbd_row_t *row, int n)
337 /* FIXME: support different data types */
338 /* this fails - bind gets some vars but not others
339 return (const char*)row->res->vars[n].data;
341 DBPROCESS* proc = row->res->proc;
342 BYTE *ptr = dbdata(proc, n+1);
343 int t = dbcoltype(proc, n+1);
344 int l = dbcollen(proc, n+1);
345 if (dbwillconvert(t, SYBCHAR)) {
346 dbconvert(proc, t, ptr, l, SYBCHAR, (BYTE *)row->buf, -1);
347 return (const char*)row->buf;
349 return (char*)ptr;
352 static const char *dbd_freetds_error(apr_dbd_t *sql, int n)
354 /* XXX this doesn't seem to exist in the API ??? */
355 return apr_psprintf(sql->pool, "Error %d", sql->err);
358 static int dbd_freetds_query(apr_dbd_t *sql, int *nrows, const char *query)
360 if (sql->trans && sql->trans->errnum) {
361 return sql->trans->errnum;
363 *nrows = 0;
364 sql->err = freetds_exec(sql->proc, query, 0, nrows);
366 if (sql->err != SUCCEED) {
367 if (sql->trans) {
368 sql->trans->errnum = sql->err;
370 return 1;
372 return 0;
375 static const char *dbd_freetds_escape(apr_pool_t *pool, const char *arg,
376 apr_dbd_t *sql)
378 return arg;
381 static apr_status_t freetds_regfree(void *rx)
383 regfree((regex_t*)rx);
384 return APR_SUCCESS;
386 static int recurse_args(apr_pool_t *pool, int n, const char *query,
387 apr_dbd_prepared_t *stmt, int offs)
390 /* we only support %s arguments for now */
391 int ret;
392 char arg[256];
393 regmatch_t matches[3];
394 if (regexec(&dbd_freetds_find_arg, query, 3, matches, 0) != 0) {
395 /* No more args */
396 stmt->nargs = n;
397 stmt->taint = apr_palloc(pool, n*sizeof(regex_t*));
398 stmt->sz = apr_palloc(pool, n*sizeof(int));
399 ret = 0;
401 else {
402 int i;
403 int sz = 0;
404 int len = matches[1].rm_eo - matches[1].rm_so - 2;
405 if (len > 255) {
406 return 9999;
409 ret = recurse_args(pool, n+1, query+matches[0].rm_eo,
410 stmt, offs+matches[0].rm_eo);
412 memmove(stmt->fmt + offs + matches[1].rm_so,
413 stmt->fmt + offs + matches[0].rm_eo-1,
414 strlen(stmt->fmt+offs+matches[0].rm_eo)+2);
416 /* compile untaint to a regex if found */
417 if (matches[1].rm_so == -1) {
418 stmt->taint[n] = NULL;
420 else {
421 strncpy(arg, query+matches[1].rm_so+1,
422 matches[1].rm_eo - matches[1].rm_so - 2);
423 arg[matches[1].rm_eo - matches[1].rm_so - 2] = '\0';
424 stmt->taint[n] = apr_palloc(pool, sizeof(regex_t));
425 if (regcomp(stmt->taint[n], arg, REG_ICASE|REG_EXTENDED) != 0) {
426 ++ret;
428 else {
429 apr_pool_cleanup_register(pool, stmt->taint[n], freetds_regfree,
430 apr_pool_cleanup_null);
434 /* record length if specified */
435 for (i=matches[2].rm_so; i<matches[2].rm_eo; ++i) {
436 sz = 10*sz + (query[i]-'\0');
439 return ret;
442 static int dbd_freetds_prepare(apr_pool_t *pool, apr_dbd_t *sql,
443 const char *query, const char *label,
444 int nargs, int nvals, apr_dbd_type_e *types,
445 apr_dbd_prepared_t **statement)
447 apr_dbd_prepared_t *stmt;
449 if (label == NULL) {
450 label = apr_psprintf(pool, "%d", labelnum++);
453 if (!*statement) {
454 *statement = apr_palloc(pool, sizeof(apr_dbd_prepared_t));
456 stmt = *statement;
458 #if 0
459 /* count args */
460 stmt->fmt = apr_pstrdup(pool, query);
461 stmt->fmt = recurse_args(pool, 0, query, stmt, stmt->fmt);
463 /* overestimate by a byte or two to simplify */
464 len = strlen("CREATE PROC apr.")
465 + strlen(label)
466 + stmt->nargs * strlen(" @arg1 varchar(len1),")
467 + strlen(" AS begin ")
468 + strlen(stmt->fmt)
469 + strlen(" end "); /* extra byte for terminator */
471 pquery = apr_pcalloc(pool, len);
472 sprintf(pquery, "CREATE PROC apr.%s", label);
473 for (i=0; i<stmt->nargs; ++i) {
474 sprintf(pquery+strlen(pquery), " @arg%d varchar(%d)", i, stmt->sz[i]);
475 if (i < stmt->nargs-1) {
476 pquery[strlen(pquery)] = ',';
479 strcat(pquery, " AS BEGIN ");
480 strcat(pquery, stmt->fmt);
481 strcat(pquery, " END");
483 return (freetds_exec(sql->proc, pquery, 0, &i) == SUCCEED) ? 0 : 1;
484 #else
485 stmt->fmt = apr_pstrdup(pool, query);
486 return recurse_args(pool, 0, query, stmt, 0);
487 #endif
491 static int dbd_freetds_start_transaction(apr_pool_t *pool, apr_dbd_t *handle,
492 apr_dbd_transaction_t **trans)
494 int dummy;
496 /* XXX handle recursive transactions here */
498 handle->err = freetds_exec(handle->proc, "BEGIN TRANSACTION", 0, &dummy);
500 if (dbd_freetds_is_success(handle->err)) {
501 if (!*trans) {
502 *trans = apr_pcalloc(pool, sizeof(apr_dbd_transaction_t));
504 (*trans)->handle = handle;
505 handle->trans = *trans;
506 return 0;
509 return 1;
512 static int dbd_freetds_end_transaction(apr_dbd_transaction_t *trans)
514 int dummy;
515 if (trans) {
516 /* rollback on error or explicit rollback request */
517 if (trans->errnum) {
518 trans->errnum = 0;
519 trans->handle->err = freetds_exec(trans->handle->proc,
520 "ROLLBACK", 0, &dummy);
522 else {
523 trans->handle->err = freetds_exec(trans->handle->proc,
524 "COMMIT", 0, &dummy);
526 trans->handle->trans = NULL;
528 return (trans->handle->err == SUCCEED) ? 0 : 1;
531 static DBPROCESS *freetds_open(apr_pool_t *pool, const char *params,
532 const char **error)
534 char *server = NULL;
535 DBPROCESS *process;
536 LOGINREC *login;
537 static const char *delims = " \r\n\t;|,";
538 char *ptr;
539 char *key;
540 char *value;
541 int vlen;
542 int klen;
543 char *buf;
544 char *databaseName = NULL;
546 /* FIXME - this uses malloc */
547 /* FIXME - pass error message back to the caller in case of failure */
548 login = dblogin();
549 if (login == NULL) {
550 return NULL;
552 /* now set login properties */
553 for (ptr = strchr(params, '='); ptr; ptr = strchr(ptr, '=')) {
554 /* don't dereference memory that may not belong to us */
555 if (ptr == params) {
556 ++ptr;
557 continue;
559 for (key = ptr-1; apr_isspace(*key); --key);
560 klen = 0;
561 while (apr_isalpha(*key)) {
562 --key;
563 ++klen;
565 ++key;
566 for (value = ptr+1; apr_isspace(*value); ++value);
568 vlen = strcspn(value, delims);
569 buf = apr_pstrndup(pool, value, vlen); /* NULL-terminated copy */
571 if (!strncasecmp(key, "username", klen)) {
572 DBSETLUSER(login, buf);
574 else if (!strncasecmp(key, "password", klen)) {
575 DBSETLPWD(login, buf);
577 else if (!strncasecmp(key, "appname", klen)) {
578 DBSETLAPP(login, buf);
580 else if (!strncasecmp(key, "dbname", klen)) {
581 databaseName = buf;
583 else if (!strncasecmp(key, "host", klen)) {
584 DBSETLHOST(login, buf);
586 else if (!strncasecmp(key, "charset", klen)) {
587 DBSETLCHARSET(login, buf);
589 else if (!strncasecmp(key, "lang", klen)) {
590 DBSETLNATLANG(login, buf);
592 else if (!strncasecmp(key, "server", klen)) {
593 server = buf;
595 else {
596 /* unknown param */
598 ptr = value+vlen;
601 process = dbopen(login, server);
603 fprintf(stderr, "databaseName [%s]\n", databaseName);
605 if (databaseName != NULL)
607 dbuse(process, databaseName);
610 dbloginfree(login);
611 if (process == NULL) {
612 return NULL;
615 return process;
617 static apr_dbd_t *dbd_freetds_open(apr_pool_t *pool, const char *params,
618 const char **error)
620 apr_dbd_t *sql;
621 /* FIXME - pass error message back to the caller in case of failure */
622 DBPROCESS *process = freetds_open(pool, params, error);
623 if (process == NULL) {
624 return NULL;
626 sql = apr_palloc (pool, sizeof (apr_dbd_t));
627 sql->pool = pool;
628 sql->proc = process;
629 sql->params = params;
630 return sql;
633 static apr_status_t dbd_freetds_close(apr_dbd_t *handle)
635 dbclose(handle->proc);
636 return APR_SUCCESS;
639 static apr_status_t dbd_freetds_check_conn(apr_pool_t *pool,
640 apr_dbd_t *handle)
642 if (dbdead(handle->proc)) {
643 /* try again */
644 dbclose(handle->proc);
645 handle->proc = freetds_open(handle->pool, handle->params, NULL);
646 if (!handle->proc || dbdead(handle->proc)) {
647 return APR_EGENERAL;
650 /* clear it, in case this is called in error handling */
651 dbcancel(handle->proc);
652 return APR_SUCCESS;
655 static int dbd_freetds_select_db(apr_pool_t *pool, apr_dbd_t *handle,
656 const char *name)
658 /* ouch, it's declared int. But we can use APR 0/nonzero */
659 return (dbuse(handle->proc, (char*)name) == SUCCEED) ? APR_SUCCESS : APR_EGENERAL;
662 static void *dbd_freetds_native(apr_dbd_t *handle)
664 return handle->proc;
667 static int dbd_freetds_num_cols(apr_dbd_results_t* res)
669 return res->sz;
672 static int dbd_freetds_num_tuples(apr_dbd_results_t* res)
674 if (res->random) {
675 return res->ntuples;
677 else {
678 return -1;
682 static apr_status_t freetds_term(void *dummy)
684 dbexit();
685 regfree(&dbd_freetds_find_arg);
686 return APR_SUCCESS;
688 static void dbd_freetds_init(apr_pool_t *pool)
690 int rv = regcomp(&dbd_freetds_find_arg,
691 "%(\\{[^}]*\\})?([0-9]*)[A-Za-z]", REG_EXTENDED);
692 if (rv != 0) {
693 char errmsg[256];
694 regerror(rv, &dbd_freetds_find_arg, errmsg, 256);
695 fprintf(stderr, "regcomp failed: %s\n", errmsg);
697 dbinit();
698 apr_pool_cleanup_register(pool, NULL, freetds_term, apr_pool_cleanup_null);
701 #ifdef COMPILE_STUBS
702 /* get_name is the only one of these that is implemented */
703 static const char *dbd_freetds_get_name(const apr_dbd_results_t *res, int n)
705 return (const char*) dbcolname(res->proc, n+1); /* numbering starts at 1 */
708 /* These are stubs: transaction modes not implemented here */
709 #define DBD_NOTIMPL APR_ENOTIMPL;
710 static int dbd_freetds_transaction_mode_get(apr_dbd_transaction_t *trans)
712 return trans ? trans->mode : APR_DBD_TRANSACTION_COMMIT;
715 static int dbd_freetds_transaction_mode_set(apr_dbd_transaction_t *trans,
716 int mode)
718 if (trans) {
719 trans->mode = mode & TXN_MODE_BITS;
720 return trans->mode;
722 return APR_DBD_TRANSACTION_COMMIT;
724 static int dbd_freetds_pvbquery(apr_pool_t *pool, apr_dbd_t *sql, int *nrows,
725 apr_dbd_prepared_t *statement, va_list args)
727 return DBD_NOTIMPL;
729 static int dbd_freetds_pbquery(apr_pool_t *pool, apr_dbd_t *sql, int *nrows,
730 apr_dbd_prepared_t * statement,
731 const void **values)
733 return DBD_NOTIMPL;
736 static int dbd_freetds_pvbselect(apr_pool_t *pool, apr_dbd_t *sql,
737 apr_dbd_results_t **results,
738 apr_dbd_prepared_t *statement,
739 int seek, va_list args)
741 return DBD_NOTIMPL;
743 static int dbd_freetds_pbselect(apr_pool_t *pool, apr_dbd_t *sql,
744 apr_dbd_results_t **results,
745 apr_dbd_prepared_t *statement,
746 int seek, const void **values)
748 return DBD_NOTIMPL;
750 static apr_status_t dbd_freetds_datum_get(const apr_dbd_row_t *row, int n,
751 apr_dbd_type_e type, void *data)
753 return APR_ENOTIMPL;
755 #endif
757 APU_MODULE_DECLARE_DATA const apr_dbd_driver_t apr_dbd_freetds_driver = {
758 "freetds",
759 dbd_freetds_init,
760 dbd_freetds_native,
761 dbd_freetds_open,
762 dbd_freetds_check_conn,
763 dbd_freetds_close,
764 dbd_freetds_select_db,
765 dbd_freetds_start_transaction,
766 dbd_freetds_end_transaction,
767 dbd_freetds_query,
768 dbd_freetds_select,
769 dbd_freetds_num_cols,
770 dbd_freetds_num_tuples,
771 dbd_freetds_get_row,
772 dbd_freetds_get_entry,
773 dbd_freetds_error,
774 dbd_freetds_escape,
775 dbd_freetds_prepare,
776 dbd_freetds_pvquery,
777 dbd_freetds_pvselect,
778 dbd_freetds_pquery,
779 dbd_freetds_pselect,
780 /* this is only implemented to support httpd/2.2 standard usage,
781 * as in the original DBD implementation. Everything else is NOTIMPL.
783 #ifdef COMPILE_STUBS
784 dbd_freetds_get_name,
785 dbd_freetds_transaction_mode_get,
786 dbd_freetds_transaction_mode_set,
788 dbd_freetds_pvbquery,
789 dbd_freetds_pvbselect,
790 dbd_freetds_pbquery,
791 dbd_freetds_pbselect,
792 dbd_freetds_datum_get
793 #endif
795 #endif