/*
 * Copyright (c) 2006 Mode2 inc.
 * Copyright (c) 2007 - 2008 BROADTECH INC.
 * Copyright (c) 2006 - 2008 Kazuaki ODA
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 3. Neither the name of the company nor the names of its contributors
 *    may be used to endorse or promote products derived from this software
 *    without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * $Id: ctr_browsers.c 540 2008-08-30 18:44:20Z oda $
 */

#include "ctr_browsers.h"
#include "http_log.h"
#include "ctr_util.h"
#include "ctr_xml.h"

#define CTR_BRWXML_STAT_LAST(_stats)                                    \
    (*(ctr_brwxmlstat_t*)CTR_ARRAY_LAST(_stats))

#define CTR_BRWXML_STAT_PUSH(_stats, _st) do {                          \
    ctr_brwxmlstat_t *_stat = apr_array_push(_stats);                   \
    *_stat  = (_st);                                                    \
} while (0)

#define CTR_BRWXML_STAT_POP(_stats) do {                                \
    apr_array_pop(_stats);                                              \
} while (0)

#define CTR_DOCOMO_PREFIX       "_docomo_"
#define CTR_KDDI_PREFIX         "_kddi_"
#define CTR_SOFTBANK_PREFIX     "_softbank_"
#define CTR_WILLCOM_PREFIX      "_willcom_"

typedef enum _ctr_brwxmlstat ctr_brwxmlstat_t;
typedef struct _ctr_brwxml_info ctr_brwxml_info_t;
typedef struct _ctr_symmap ctr_symmap_t;

enum _ctr_brwxmlstat {
    BX_NEUTRAL, BX_BROWSERS, BX_DOCOMO, BX_KDDI, BX_SOFTBANK, BX_VODAFONE,
    BX_WILLCOM, BX_BASE, BX_SCREEN, BX_CHARACTER, BX_PIXEL, BX_WALLPAPER,
    BX_COLORS, BX_PICTURES, BX_PICTURE, BX_CACHE
};

struct _ctr_brwxml_info {
    ctr_brwxml_data_t *brwdata;
    apr_array_header_t *stats;
    apr_array_header_t *unknowns;
    ctr_browser_t *curbrw;
    apr_pool_t *pool;
};

struct _ctr_symmap {
    const char *sym;
    int val;
};

static const char *ctr_brwxml_start_element(void *udata, const char *name,
                                            apr_table_t *attrs);
static const char *ctr_brwxml_end_element(void *udata, const char *name);
static ctr_browser_t *ctr_new_browser(apr_pool_t *pool);
static ctr_browser_t *ctr_browser_get(apr_hash_t *brwhash, const char *name,
                                      apr_pool_t *pool);
static char *ctr_brwxml_attr_get(apr_table_t *attrs, const char *attrname,
                                 const char *tagname, const char **errp,
                                 apr_pool_t *pool);
static char *ctr_brwxml_attr_get2(apr_table_t *attrs, const char *attrname,
                                  const char *tagname, const char *dflt,
                                  apr_pool_t *pool);
static int ctr_brwxml_attr_pintget(apr_table_t *attrs, const char *attrname,
                                   const char *tagname, const char **errp,
                                   apr_pool_t *pool);
static double ctr_brwxml_attr_pdblget(apr_table_t *attrs, const char *attrname,
                                      const char *tagname, const char **errp,
                                      apr_pool_t *pool);
static int ctr_brwxml_attr_symvalget(apr_table_t *attrs, const char *attrname,
                                     const ctr_symmap_t *syms,
                                     const char *tagname, const char **errp,
                                     apr_pool_t *pool);
static int ctr_brwxml_attr_symvalget2(apr_table_t *attrs, const char *attrname,
                                      const ctr_symmap_t *syms,
                                      const char *tagname, const char **errp,
                                      int dflt, apr_pool_t *pool);
static int ctr_brwxml_attr_flgget(apr_table_t *attrs, const char *attrname,
                                  const char *tagname, const char **errp,
                                  apr_pool_t *pool);
static int ctr_brwxml_attr_flgget2(apr_table_t *attrs, const char *attrname,
                                   const char *tagname, const char **errp,
                                   int dflt, apr_pool_t *pool);

const char *ctr_brwxml_read(ctr_brwxml_data_t *brwdata, apr_file_t *fd,
                            apr_pool_t *pool)
{
    ctr_brwxml_info_t *bxinfo;
    ctr_xml_handler_t handler;

    bxinfo = apr_palloc(pool, sizeof(*bxinfo));
    brwdata->brwhash = apr_hash_make(pool);
    brwdata->docomo_cnt = 0;
    brwdata->kddi_cnt = 0;
    brwdata->softbank_cnt = 0;
    brwdata->willcom_cnt = 0;
    bxinfo->brwdata = brwdata;
    bxinfo->stats = apr_array_make(pool, 1, sizeof(ctr_brwxmlstat_t));
    CTR_BRWXML_STAT_PUSH(bxinfo->stats, BX_NEUTRAL);
    CTR_XML_UNKNOWN_INIT(bxinfo->unknowns, pool);
    bxinfo->curbrw = NULL;
    bxinfo->pool = pool;

    handler.start_element = ctr_brwxml_start_element;
    handler.end_element = ctr_brwxml_end_element;
    handler.characters = NULL;

    return ctr_xml_file_parse(&handler, fd, bxinfo, pool);
}

ctr_browser_t *ctr_browser_lookup(apr_hash_t *brwhash, const char *uagent,
                                  const request_rec *r, int log_unknown)
{
    apr_pool_t *pool = r->pool;
    ctr_browser_t *brw;
    const char *msname;

    if (uagent == NULL)
        uagent = apr_table_get(r->headers_in, "User-Agent");

    if (uagent == NULL)
        return ctr_new_browser(pool);

    if (ctr_startwith(uagent, "DoCoMo")) {
        /* DoCoMo */
        int isfoma;
        const char *p, *q;
        const char *name;
        int cachesize;
        int charwidth, charheight;
        char *basever;
        char *hname;

        charwidth = charheight = -1;

        p = uagent + 6; /* 6 is length of "DoCoMo" */
        if (ctr_startwith(p, "/2.0")) {
            /* FOMA */
            isfoma = TRUE;

            p += 5;     /* 5 is length of "/2.0 " */
            if ((q = strchr(p, '(')) != NULL) {
                name = apr_pstrndup(pool, p, q - p);

                p = q + 1;
                if ((q = strchr(p, ';')) != NULL)
                    p = apr_pstrndup(pool, p, q - p);
                else if ((q = strchr(p, ')')) != NULL)
                    p = apr_pstrndup(pool, p, q - p);
                else
                    goto unknown;
                if (*p == 'c')
                    p++;
                if ((cachesize = (int)apr_atoi64(p)) <= 0)
                    cachesize = 5 * 1024;
                else
                    cachesize *= 1024;

                if (*q == ';') {
                    /*
                     * Get screen width and height from User-Agent.
                     */
                    for (;;) {
                        p = q + 1;
                        if ((q = strchr(p, ';')) == NULL &&
                            (q = strchr(p, ')')) == NULL)
                            break;
                        if (*p == 'W') {
                            p = apr_pstrndup(pool, p, q - p);
                            if (sscanf(p, "W%dH%d",
                                       &charwidth, &charheight) == 2)
                                break;
                            else {
                                charwidth = -1;
                                charheight = -1;
                            }
                        }
                    }
                }

                basever = "2.0";
            } else
                goto unknown;
        } else if (ctr_startwith(p, "/1.0")) {
            /* PDC */
            isfoma = FALSE;

            p += 5;     /* 5 is length of "/1.0/" */
            if ((q = strchr(p, '/')) != NULL) {
                name = apr_pstrndup(pool, p, q - p);

                p = q + 1;
                if ((q = strchr(p, '/')) != NULL)
                    p = apr_pstrndup(pool, p, q - p);
                if (*p == 'c')
                    p++;
                if ((cachesize = (int)apr_atoi64(p)) <= 0)
                    cachesize = 5 * 1024;
                else
                    cachesize *= 1024;

                if (q != NULL) {
                    /*
                     * Get screen width and height from User-Agent.
                     */
                    for (;;) {
                        p = q + 1;
                        q = strchr(p, '/');
                        if (*p == 'W') {
                            if (q != NULL)
                                p = apr_pstrndup(pool, p, q - p);
                            if (sscanf(p, "W%dH%d",
                                       &charwidth, &charheight) == 2)
                                break;
                            else {
                                charwidth = -1;
                                charheight = -1;
                            }
                        }
                        if (q == NULL)
                            break;
                    }
                }
            } else {
                name = p;
                cachesize = 5 * 1024;
            }

            basever = "1.0";
        } else
            goto unknown;

        hname = apr_pstrcat(pool, CTR_DOCOMO_PREFIX, name, NULL);
        brw = ctr_browser_get(brwhash, hname, pool);
        if (brw->carrier == CR_UNKNOWN) {
            if (log_unknown) {
                ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r,
                              "mod_cthree: unknown docomo browser: [%s]",
                              name);
            }

            brw->carrier = CR_DOCOMO;
            brw->name = name;
            brw->spec.docomo.htmlver = 7.0;
            if (isfoma)
                brw->spec.docomo.xhtmlver = 2.1;
            else
                brw->spec.docomo.xhtmlver = -1.0;
            brw->flashver = 1.1;
            brw->spec.docomo.isfoma = -1;
        }

        if (brw->spec.docomo.isfoma == -1)
            brw->spec.docomo.isfoma = isfoma;

        if (brw->charwidth == -1)
            brw->charwidth = charwidth;

        if (brw->charheight == -1)
            brw->charheight = charheight;

        if (brw->pixwidth == -1) {
            /*
             * We assume the browser can display QVGA sized image.
             */
            brw->pixwidth = 240;
            brw->pixheight = 320;
        }

        if (brw->wpixwidth == -1) {
            brw->wpixwidth = brw->pixwidth;
            brw->wpixheight = brw->pixheight;
        }

        if (brw->colors == -1)
            brw->colors = 65536;

        if (brw->pictypes == NULL) {
            /*
             * Probably the browser can display JPEG image.
             */
            brw->pictypes = apr_palloc(pool, sizeof(*brw->pictypes));
            brw->pictypes->type = PC_JPEG;
            brw->pictypes->next = NULL;
        }

        if (brw->cachesize == -1)
            brw->cachesize = cachesize;

        if (brw->iscacheincboth == -1)
            brw->iscacheincboth = TRUE;

        if (brw->basetype == BT_UNKNOWN)
            brw->basetype = BT_IMODE;

        if (brw->basever[0] == '\0')
            brw->basever = basever;
    } else if (ctr_startwith(uagent, "KDDI-")) {
        /* KDDI */
        const char *p, *q;
        char *name;
        int cachesize;
        const char *s;
        int v1, v2, i;
        ctr_brwtype_t basetype;
        char *basever;
        char *hname;

        p = uagent + 5;     /* 5 is length of "KDDI-" */
        if ((q = strchr(p, ' ')) != NULL)
            name = apr_pstrndup(pool, p, q - p);
        else
            goto unknown;

        if ((s = apr_table_get(r->headers_in,
                               "x-up-devcap-max-pdu")) != NULL &&
            (v1 = (int)apr_atoi64(s)) > 0)
            cachesize = v1;
        else
            cachesize = 9 * 1024;

        p = q + 1;
        if ((q = strchr(p, '/')) != NULL) {
            if (strncmp(p, "UP.Browser", q - p) == 0)
                basetype = BT_UPBROWSER;
            else
                basetype = BT_UNKNOWN;
        } else
            goto unknown;

        p = q + 1;
        if ((q = strchr(p, ' ')) != NULL)
            basever = apr_pstrndup(pool, p, q - p);
        else
            goto unknown;

        hname = apr_pstrcat(pool, CTR_KDDI_PREFIX, name, NULL);
        brw = ctr_browser_get(brwhash, hname, pool);
        if (brw->carrier == CR_UNKNOWN) {
            if (log_unknown) {
                ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r,
                              "mod_cthree: unknown kddi browser: [%s]", name);
            }

            brw->carrier = CR_KDDI;
            brw->name = name;
            brw->spec.kddi.realname = NULL;
            brw->flashver = 2.0;
            brw->spec.kddi.iswap20 = TRUE;
            brw->spec.kddi.istuka = FALSE;
        }

        if (brw->charwidth == -1 &&
            (s = apr_table_get(r->headers_in,
                               "x-up-devcap-screenchars")) != NULL) {
            ctr_char_split(s, &p, &q, ',', pool);
            if (p != NULL &&
                (v1 = (int)apr_atoi64(p)) > 0 &&
                (v2 = (int)apr_atoi64(q)) > 0) {
                brw->charwidth = v1;
                brw->charheight = v2;
            }
        }

        if (brw->pixwidth == -1) {
            if ((s = apr_table_get(r->headers_in,
                                   "x-up-devcap-screenpixels")) != NULL) {
                ctr_char_split(s, &p, &q, ',', pool);
                if (p != NULL &&
                    (v1 = (int)apr_atoi64(p)) > 0 &&
                    (v2 = (int)apr_atoi64(q)) > 0) {
                    brw->pixwidth = v1;
                    brw->pixheight = v2;
                }
            }
            if (brw->pixwidth == -1) {
                /*
                 * We assume the browser can display QVGA sized image.
                 */
                brw->pixwidth = 240;
                brw->pixheight = 320;
            }
        }

        if (brw->wpixwidth == -1) {
            brw->wpixwidth = brw->pixwidth;
            brw->wpixheight = brw->pixheight;
        }

        if (brw->colors == -1) {
            if ((s = apr_table_get(r->headers_in,
                                   "x-up-devcap-screendepth")) != NULL) {
                ctr_char_split(s, &p, &q, ',', pool);
                /* 1 is the least number (binary colors) */
                if (p != NULL && (v1 = (int)apr_atoi64(p)) > 1) {
                    brw->colors = 2;
                    for (i = 1; i < v1; i++)
                        brw->colors *= 2;
                }
            }
            if (brw->colors == -1)
                brw->colors = 65536;
        }

        if (brw->pictypes == NULL) {
            /*
             * Probably the browser can display JPEG image.
             */
            brw->pictypes = apr_palloc(pool, sizeof(*brw->pictypes));
            brw->pictypes->type = PC_JPEG;
            brw->pictypes->next = NULL;
        }

        if (brw->cachesize == -1)
            brw->cachesize = cachesize;

        if (brw->iscacheincboth == -1)
            brw->iscacheincboth = FALSE;

        if (brw->basetype == BT_UNKNOWN)
            brw->basetype = basetype;

        if (brw->basever[0] == '\0')
            brw->basever = basever;
    } else if ((msname = apr_table_get(r->headers_in,
                                       "x-jphone-msname")) != NULL) {
        /*
         * We regard a browser as SoftBank's if "x-jphone-msname" HTTP header
         * is received.  Since SoftBank frequently changes the rule of User-
         * Agent, this is probably the easist and safest way.
         */
        /* SoftBank */
        const char *s, *p, *q, *q2;
        ctr_brwtype_t basetype;
        char *basever;
        int v1, v2;
        char *hname;

        /*
         * It seems that some SoftBank's browsers have excessive string
         * like "_a", "_b" and so on.  So remove it if the browser has.
         */
        if ((p = strrchr(msname, '_')) != NULL)
            msname = apr_pstrndup(pool, msname, p - msname);

        if ((p = strchr(uagent, ' ')) != NULL &&
            (p = strchr(p + 1, '/')) != NULL &&
            (q = strchr(p + 1, '/')) != NULL &&
            (q2 = strchr(q + 1, ' ')) != NULL) {
            p++;
            if (strncmp(p, "UP.Browser", q - p) == 0)
                basetype = BT_UPBROWSER;
            else if (strncmp(p, "VF-Browser", q - p) == 0)
                basetype = BT_VFBROWSER;
            else if (strncmp(p, "VF-NetFront", q - p) == 0 ||
                     strncmp(p, "NetFront", q - p) == 0)
                basetype = BT_NETFRONT;
            else if (strncmp(p, "Teleca-Browser", q - p) == 0)
                basetype = BT_TELECA;
            else
                basetype = BT_UNKNOWN;

            q++;
            basever = apr_pstrndup(pool, q, q2 - q);
        } else {
            basetype = BT_UNKNOWN;
            basever = "";
        }

        hname = apr_pstrcat(pool, CTR_SOFTBANK_PREFIX, msname, NULL);
        brw = ctr_browser_get(brwhash, hname, pool);
        if (brw->carrier == CR_UNKNOWN) {
            if (log_unknown) {
                ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r,
                              "mod_cthree: unknown softbank browser: [%s]",
                              msname);
            }

            brw->carrier = CR_SOFTBANK;
            brw->name = msname;
            brw->flashver = 2.0;
            brw->spec.softbank.isgetonly = FALSE;
            brw->spec.softbank.ispacket = TRUE; /* XXX */
            brw->spec.softbank.isxhtml = TRUE;  /* XXX */
        }

        if (brw->pixwidth == -1) {
            if ((s = apr_table_get(r->headers_in,
                                   "x-jphone-display")) != NULL) {
                ctr_char_split(s, &p, &q, '*', pool);
                if (p != NULL &&
                    (v1 = (int)apr_atoi64(p)) > 0 &&
                    (v2 = (int)apr_atoi64(q)) > 0) {
                    brw->pixwidth = v1;
                    brw->pixheight = v2;
                }
            }
            if (brw->pixwidth == -1) {
                /*
                 * We assume the browser can display QVGA sized image.
                 */
                brw->pixwidth = 240;
                brw->pixheight = 320;
            }
        }

        if (brw->wpixwidth == -1) {
            brw->wpixwidth = brw->pixwidth;
            brw->wpixheight = brw->pixheight;
        }

        if (brw->colors == -1) {
            if ((s = apr_table_get(r->headers_in, "x-jphone-color")) != NULL) {
                s++;
                /* 4 is the least number (gray colors) */
                if ((v1 = (int)apr_atoi64(s)) >= 4)
                    brw->colors = v1;
            }
            if (brw->colors == -1)
                brw->colors = 65536;
        }

        if (brw->pictypes == NULL) {
            /*
             * Probably the browser can display JPEG image.
             */
            brw->pictypes = apr_palloc(pool, sizeof(*brw->pictypes));
            brw->pictypes->type = PC_JPEG;
            brw->pictypes->next = NULL;
        }

        if (brw->cachesize == -1) {
            if (brw->spec.softbank.ispacket == FALSE)
                brw->cachesize = 6 * 1024;
            else if (brw->spec.softbank.isxhtml)
                brw->cachesize = 300 * 1024;
            else
                brw->cachesize = 12 * 1024;
        }

        if (brw->iscacheincboth == -1)
            brw->iscacheincboth = TRUE;

        if (brw->basetype == BT_UNKNOWN)
            brw->basetype = basetype;

        if (brw->basever[0] == '\0')
            brw->basever = basever;
    } else if (ctr_startwith(uagent, "Mozilla/3.0(DDIPOCKET;") ||
               ctr_startwith(uagent, "Mozilla/3.0(WILLCOM;")) {
        /* WILLCOM */
        const char *p, *q;
        const char *name;
        int cachesize;
        ctr_brwtype_t basetype;
        char *basever;
        char *hname;

        p = strchr(uagent, ';');
        p++;
        if ((p = strchr(p, '/')) == NULL)
            goto unknown;
        p++;
        if ((q = strchr(p, '/')) == NULL)
            goto unknown;
        name = apr_pstrndup(pool, p, q - p);
        p = q + 1;

        if ((p = strchr(p, '/')) == NULL)
            goto unknown;
        p++;
        if ((p = strchr(p, '/')) == NULL)
            goto unknown;
        p++;
        if ((q = strchr(p, ')')) == NULL)
            goto unknown;
        p = apr_pstrndup(pool, p, q - p);
        if (*p == 'c' || *p == 'C')
            p++;
        if ((cachesize = (int)apr_atoi64(p)) <= 0)
            cachesize = 50 * 1024;
        else
            cachesize *= 1024;

        if ((p = strchr(uagent, ' ')) != NULL &&
            (q = strchr(p + 1, '/')) != NULL) {
            p++;
            if (strncmp(p, "NetFront", q - p) == 0)
                basetype = BT_NETFRONT;
            else if (strncmp(p, "Opera", q - p) == 0)
                basetype = BT_OPERA;
            else
                basetype = BT_UNKNOWN;

            q++;
            basever = apr_pstrdup(pool, q);
        } else {
            basetype = BT_UNKNOWN;
            basever = "";
        }

        hname = apr_pstrcat(pool, CTR_WILLCOM_PREFIX, name, NULL);
        brw = ctr_browser_get(brwhash, hname, pool);
        if (brw->carrier == CR_UNKNOWN) {
            if (log_unknown) {
                ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r,
                              "mod_cthree: unknown willcom browser: [%s]",
                              name);
            }

            brw->carrier = CR_WILLCOM;
            brw->name = name;
        }

        if (brw->pixwidth == -1) {
            /*
             * We assume the browser can display QVGA sized image.
             */
            brw->pixwidth = 240;
            brw->pixheight = 320;
        }

        if (brw->wpixwidth == -1) {
            brw->wpixwidth = brw->pixwidth;
            brw->wpixheight = brw->pixheight;
        }

        if (brw->colors == -1)
            brw->colors = 65536;

        if (brw->pictypes == NULL) {
            /*
             * Probably the browser can display GIF & JPEG image.
             */
            brw->pictypes = apr_palloc(pool, sizeof(*brw->pictypes));
            brw->pictypes->type = PC_GIF;
            brw->pictypes->next = apr_palloc(pool,
                                             sizeof(*brw->pictypes->next));
            brw->pictypes->next->type = PC_JPEG;
            brw->pictypes->next->next = NULL;
        }

        if (brw->cachesize == -1)
            brw->cachesize = cachesize;

        if (brw->iscacheincboth == -1)
            brw->iscacheincboth = TRUE; /* XXX: really? */

        if (brw->basetype == BT_UNKNOWN)
            brw->basetype = basetype;

        if (brw->basever[0] == '\0')
            brw->basever = basever;
    } else {
        /* others (not cellular phone) */
        brw = ctr_new_browser(pool);
    }

    return brw;

unknown:
    if (log_unknown) {
        ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r,
                      "mod_cthree: unknown user agent: %s", uagent);
    }
    return ctr_new_browser(pool);
}

static const char *ctr_brwxml_start_element(void *udata, const char *name,
                                            apr_table_t *attrs)
{
#define CTR_BRWXML_ATTR_GET(_attrname, _val) do {                       \
    if (((_val) = ctr_brwxml_attr_get(attrs, (_attrname), name,         \
                                      &errmsg, pool)) == NULL)          \
        return errmsg;                                                  \
} while (0)
#define CTR_BRWXML_ATTR_GET2(_attrname, _val, _dflt) do {               \
    (_val) = ctr_brwxml_attr_get2(attrs, (_attrname), name,             \
                                  (_dflt), pool);                       \
} while (0)
#define CTR_BRWXML_ATTR_PINTGET(_attrname, _val) do {                   \
    if (((_val) = ctr_brwxml_attr_pintget(attrs, (_attrname), name,     \
                                          &errmsg, pool)) == -1)        \
        return errmsg;                                                  \
} while (0)
#define CTR_BRWXML_ATTR_PDBLGET(_attrname, _val) do {                   \
    if (((_val) = ctr_brwxml_attr_pdblget(attrs, (_attrname), name,     \
                                          &errmsg, pool)) == -1)        \
        return errmsg;                                                  \
} while (0)
#define CTR_BRWXML_ATTR_SYMVALGET(_attrname, _syms, _val) do {          \
    if (((_val) = ctr_brwxml_attr_symvalget(attrs, (_attrname), (_syms),\
                                            name, &errmsg,              \
                                            pool)) == -1)               \
        return errmsg;                                                  \
} while (0)
#define CTR_BRWXML_ATTR_SYMVALGET2(_attrname, _syms, _val, _dflt) do {  \
    if (((_val) = ctr_brwxml_attr_symvalget2(attrs, (_attrname),        \
                                             (_syms), name, &errmsg,    \
                                             (_dflt), pool)) == -1 &&   \
        errmsg != NULL)                                                 \
        return errmsg;                                                  \
} while (0)
#define CTR_BRWXML_ATTR_FLGGET(_attrname, _val) do {                    \
    if (((_val) = ctr_brwxml_attr_flgget(attrs, (_attrname), name,      \
                                         &errmsg, pool)) == -1)         \
        return errmsg;                                                  \
} while (0)
#define CTR_BRWXML_ATTR_FLGGET2(_attrname, _val, _dflt) do {            \
    if (((_val) = ctr_brwxml_attr_flgget2(attrs, (_attrname), name,     \
                                          &errmsg, (_dflt),             \
                                          pool)) == -1 &&               \
        errmsg != NULL)                                                 \
        return errmsg;                                                  \
} while (0)

    ctr_brwxml_info_t *bxinfo = udata;
    apr_pool_t *pool = bxinfo->pool;
    ctr_browser_t *brw;
    const char *errmsg;

    /*
     * If we are now in unknown mode, push the tag name into a unknown list,
     * and return here.
     */
    if (!CTR_XML_UNKNOWN_EMPTY(bxinfo->unknowns)) {
        CTR_XML_UNKNOWN_PUSH(bxinfo->unknowns, name);
        return NULL;
    }

    if (strcmp(name, "browsers") == 0) {
        if (CTR_BRWXML_STAT_LAST(bxinfo->stats) == BX_NEUTRAL)
            CTR_BRWXML_STAT_PUSH(bxinfo->stats, BX_BROWSERS);
        else
            goto errout;
    } else if (strcmp(name, "docomo") == 0) {
        char *bname;
        char *group;
        double htmlver;
        char *sxhtmlver;
        double xhtmlver;
        char *sflashver;
        double flashver;
        int isfoma;
        char *hname;

        if (CTR_BRWXML_STAT_LAST(bxinfo->stats) != BX_BROWSERS)
            goto errout;

        CTR_BRWXML_ATTR_GET("name", bname);
        CTR_BRWXML_ATTR_GET("group", group);
        CTR_BRWXML_ATTR_PDBLGET("htmlver", htmlver);
        CTR_BRWXML_ATTR_GET2("xhtmlver", sxhtmlver, "none");
        if (strcmp(sxhtmlver, "none") == 0)
            xhtmlver = -1.0;
        else
            CTR_BRWXML_ATTR_PDBLGET("xhtmlver", xhtmlver);
        CTR_BRWXML_ATTR_GET2("flashver", sflashver, "none");
        if (strcmp(sflashver, "none") == 0)
            flashver = -1.0;
        else
            CTR_BRWXML_ATTR_PDBLGET("flashver", flashver);
        CTR_BRWXML_ATTR_FLGGET2("isfoma", isfoma, -1);

        brw = ctr_new_browser(pool);
        brw->carrier = CR_DOCOMO;
        brw->name = bname;
        brw->group = group;
        brw->spec.docomo.htmlver = htmlver;
        brw->spec.docomo.xhtmlver = xhtmlver;
        brw->flashver = flashver;
        brw->spec.docomo.isfoma = isfoma;
        hname = apr_pstrcat(pool, CTR_DOCOMO_PREFIX, bname, NULL);
        CTR_HASH_SET(bxinfo->brwdata->brwhash, hname, brw);
        bxinfo->brwdata->docomo_cnt++;

        bxinfo->curbrw = brw;
        CTR_BRWXML_STAT_PUSH(bxinfo->stats, BX_DOCOMO);
    } else if (strcmp(name, "kddi") == 0) {
        char *bname;
        char *group;
        char *realname;
        char *sflashver;
        double flashver;
        int iswap20;
        int istuka;
        char *hname;

        if (CTR_BRWXML_STAT_LAST(bxinfo->stats) != BX_BROWSERS)
            goto errout;

        CTR_BRWXML_ATTR_GET("name", bname);
        CTR_BRWXML_ATTR_GET("group", group);
        CTR_BRWXML_ATTR_GET("realname", realname);
        CTR_BRWXML_ATTR_GET2("flashver", sflashver, "none");
        if (strcmp(sflashver, "none") == 0)
            flashver = -1.0;
        else
            CTR_BRWXML_ATTR_PDBLGET("flashver", flashver);
        CTR_BRWXML_ATTR_FLGGET2("iswap20", iswap20, TRUE);
        CTR_BRWXML_ATTR_FLGGET2("istuka", istuka, FALSE);

        brw = ctr_new_browser(pool);
        brw->carrier = CR_KDDI;
        brw->name = bname;
        brw->group = group;
        brw->spec.kddi.realname = realname;
        brw->flashver = flashver;
        brw->spec.kddi.iswap20 = iswap20;
        brw->spec.kddi.istuka = istuka;
        hname = apr_pstrcat(pool, CTR_KDDI_PREFIX, bname, NULL);
        CTR_HASH_SET(bxinfo->brwdata->brwhash, hname, brw);
        bxinfo->brwdata->kddi_cnt++;

        bxinfo->curbrw = brw;
        CTR_BRWXML_STAT_PUSH(bxinfo->stats, BX_KDDI);
    } else if (strcmp(name, "softbank") == 0 ||
               strcmp(name, "vodafone") == 0) {
        char *bname;
        char *group;
        char *sflashver;
        double flashver;
        int isgetonly;
        int ispacket;
        int isxhtml;
        char *hname;

        if (CTR_BRWXML_STAT_LAST(bxinfo->stats) != BX_BROWSERS)
            goto errout;

        CTR_BRWXML_ATTR_GET("name", bname);
        CTR_BRWXML_ATTR_GET("group", group);
        CTR_BRWXML_ATTR_GET2("flashver", sflashver, "none");
        if (strcmp(sflashver, "none") == 0)
            flashver = -1.0;
        else
            CTR_BRWXML_ATTR_PDBLGET("flashver", flashver);
        CTR_BRWXML_ATTR_FLGGET2("isgetonly", isgetonly, FALSE);
        CTR_BRWXML_ATTR_FLGGET2("ispacket", ispacket, TRUE);
        CTR_BRWXML_ATTR_FLGGET2("isxhtml", isxhtml, TRUE);

        brw = ctr_new_browser(pool);
        brw->carrier = CR_SOFTBANK;
        brw->name = bname;
        brw->group = group;
        brw->flashver = flashver;
        brw->spec.softbank.isgetonly = isgetonly;
        brw->spec.softbank.ispacket = ispacket;
        brw->spec.softbank.isxhtml = isxhtml;
        hname = apr_pstrcat(pool, CTR_SOFTBANK_PREFIX, bname, NULL);
        CTR_HASH_SET(bxinfo->brwdata->brwhash, hname, brw);
        bxinfo->brwdata->softbank_cnt++;

        bxinfo->curbrw = brw;
        if (strcmp(name, "softbank") == 0)
            CTR_BRWXML_STAT_PUSH(bxinfo->stats, BX_SOFTBANK);
        else
            CTR_BRWXML_STAT_PUSH(bxinfo->stats, BX_VODAFONE);
    } else if (strcmp(name, "willcom") == 0) {
        char *bname;
        char *group;
        char *sflashver;
        double flashver;
        char *hname;

        if (CTR_BRWXML_STAT_LAST(bxinfo->stats) != BX_BROWSERS)
            goto errout;

        CTR_BRWXML_ATTR_GET("name", bname);
        CTR_BRWXML_ATTR_GET("group", group);
        CTR_BRWXML_ATTR_GET2("flashver", sflashver, "none");
        if (strcmp(sflashver, "none") == 0)
            flashver = -1.0;
        else
            CTR_BRWXML_ATTR_PDBLGET("flashver", flashver);

        brw = ctr_new_browser(pool);
        brw->carrier = CR_WILLCOM;
        brw->name = bname;
        brw->group = group;
        brw->flashver = flashver;
        hname = apr_pstrcat(pool, CTR_WILLCOM_PREFIX, bname, NULL);
        CTR_HASH_SET(bxinfo->brwdata->brwhash, hname, brw);
        bxinfo->brwdata->willcom_cnt++;

        bxinfo->curbrw = brw;
        CTR_BRWXML_STAT_PUSH(bxinfo->stats, BX_WILLCOM);
    } else if (strcmp(name, "base") == 0) {
        static const ctr_symmap_t base_syms[] = {
            { "imode", BT_IMODE }, { "upbrowser", BT_UPBROWSER },
            { "vfbrowser", BT_VFBROWSER }, { "netfront", BT_NETFRONT },
            { "opera", BT_OPERA }, { NULL, -1 }
        };
        int stat;

        stat = CTR_BRWXML_STAT_LAST(bxinfo->stats);
        if (stat != BX_DOCOMO && stat != BX_KDDI && stat != BX_SOFTBANK &&
            stat != BX_VODAFONE && stat != BX_WILLCOM)
            goto errout;

        CTR_BRWXML_ATTR_SYMVALGET2("type", base_syms,
                                   bxinfo->curbrw->basetype, BT_UNKNOWN);
        CTR_BRWXML_ATTR_GET2("version", bxinfo->curbrw->basever, "");

        CTR_BRWXML_STAT_PUSH(bxinfo->stats, BX_BASE);
    } else if (strcmp(name, "screen") == 0) {
        int stat;

        stat = CTR_BRWXML_STAT_LAST(bxinfo->stats);
        if (stat != BX_DOCOMO && stat != BX_KDDI && stat != BX_SOFTBANK &&
            stat != BX_VODAFONE && stat != BX_WILLCOM)
            goto errout;

        CTR_BRWXML_STAT_PUSH(bxinfo->stats, BX_SCREEN);
    } else if (strcmp(name, "character") == 0) {
        if (CTR_BRWXML_STAT_LAST(bxinfo->stats) != BX_SCREEN)
            goto errout;

        CTR_BRWXML_ATTR_PINTGET("width", bxinfo->curbrw->charwidth);
        CTR_BRWXML_ATTR_PINTGET("height", bxinfo->curbrw->charheight);

        CTR_BRWXML_STAT_PUSH(bxinfo->stats, BX_CHARACTER);
    } else if (strcmp(name, "pixel") == 0) {
        if (CTR_BRWXML_STAT_LAST(bxinfo->stats) != BX_SCREEN)
            goto errout;

        CTR_BRWXML_ATTR_PINTGET("width", bxinfo->curbrw->pixwidth);
        CTR_BRWXML_ATTR_PINTGET("height", bxinfo->curbrw->pixheight);

        CTR_BRWXML_STAT_PUSH(bxinfo->stats, BX_PIXEL);
    } else if (strcmp(name, "wallpaper") == 0) {
        if (CTR_BRWXML_STAT_LAST(bxinfo->stats) != BX_SCREEN)
            goto errout;

        CTR_BRWXML_ATTR_PINTGET("width", bxinfo->curbrw->wpixwidth);
        CTR_BRWXML_ATTR_PINTGET("height", bxinfo->curbrw->wpixheight);

        CTR_BRWXML_STAT_PUSH(bxinfo->stats, BX_WALLPAPER);
    } else if (strcmp(name, "colors") == 0) {
        if (CTR_BRWXML_STAT_LAST(bxinfo->stats) != BX_SCREEN)
            goto errout;

        CTR_BRWXML_ATTR_PINTGET("value", bxinfo->curbrw->colors);

        CTR_BRWXML_STAT_PUSH(bxinfo->stats, BX_COLORS);
    } else if (strcmp(name, "pictures") == 0) {
        if (CTR_BRWXML_STAT_LAST(bxinfo->stats) != BX_SCREEN)
            goto errout;

        CTR_BRWXML_STAT_PUSH(bxinfo->stats, BX_PICTURES);
    } else if (strcmp(name, "picture") == 0) {
        static const ctr_symmap_t pic_syms[] = {
            { "gif", PC_GIF }, { "jpeg", PC_JPEG }, { "png256", PC_PNG256 },
            { "bmp4", PC_BMP4 }, { "bmp2", PC_BMP2 }, { NULL, -1 }
        };
        int type;
        ctr_pictype_t *pictype;

        if (CTR_BRWXML_STAT_LAST(bxinfo->stats) != BX_PICTURES)
            goto errout;

        CTR_BRWXML_ATTR_SYMVALGET("type", pic_syms, type);
        pictype = apr_palloc(pool, sizeof(*pictype));
        pictype->type = type;
        pictype->next = bxinfo->curbrw->pictypes;
        bxinfo->curbrw->pictypes = pictype;

        CTR_BRWXML_STAT_PUSH(bxinfo->stats, BX_PICTURE);
    } else if (strcmp(name, "cache") == 0) {
        int stat;

        stat = CTR_BRWXML_STAT_LAST(bxinfo->stats);
        if (stat != BX_DOCOMO && stat != BX_KDDI && stat != BX_SOFTBANK &&
            stat != BX_VODAFONE && stat != BX_WILLCOM)
            goto errout;

        CTR_BRWXML_ATTR_PINTGET("size", bxinfo->curbrw->cachesize);
        CTR_BRWXML_ATTR_FLGGET("isincboth", bxinfo->curbrw->iscacheincboth);

        CTR_BRWXML_STAT_PUSH(bxinfo->stats, BX_CACHE);
    } else {
        /*
         * This tag may be a new one that is unknown for us.  Push the tag name
         * into a unknown list, and go into unknown mode.
         */
        CTR_XML_UNKNOWN_PUSH(bxinfo->unknowns, name);
    }

    return NULL;

errout:
    return apr_psprintf(pool, "found <%s> at invalid place", name);
}

static const char *ctr_brwxml_end_element(void *udata, const char *name)
{
    ctr_brwxml_info_t *bxinfo = udata;
    apr_pool_t *pool = bxinfo->pool;
    int tagmatch;

    /*
     * If we are now in unknown mode, pop the tag name from a unknown list, and
     * return here.
     */
    if (!CTR_XML_UNKNOWN_EMPTY(bxinfo->unknowns)) {
        if (strcmp(CTR_XML_UNKNOWN_LAST(bxinfo->unknowns), name) == 0)
            CTR_XML_UNKNOWN_POP(bxinfo->unknowns);
        return NULL;
    }

    tagmatch = FALSE;
    if ((strcmp(name, "browsers") == 0 && (tagmatch = TRUE) &&
         CTR_BRWXML_STAT_LAST(bxinfo->stats) == BX_BROWSERS) ||
        (strcmp(name, "docomo") == 0 && (tagmatch = TRUE) &&
         CTR_BRWXML_STAT_LAST(bxinfo->stats) == BX_DOCOMO) ||
        (strcmp(name, "kddi") == 0 && (tagmatch = TRUE) &&
         CTR_BRWXML_STAT_LAST(bxinfo->stats) == BX_KDDI) ||
        (strcmp(name, "softbank") == 0 && (tagmatch = TRUE) &&
         CTR_BRWXML_STAT_LAST(bxinfo->stats) == BX_SOFTBANK) ||
        (strcmp(name, "vodafone") == 0 && (tagmatch = TRUE) &&
         CTR_BRWXML_STAT_LAST(bxinfo->stats) == BX_VODAFONE) ||
        (strcmp(name, "willcom") == 0 && (tagmatch = TRUE) &&
         CTR_BRWXML_STAT_LAST(bxinfo->stats) == BX_WILLCOM) ||
        (strcmp(name, "base") == 0 && (tagmatch = TRUE) &&
         CTR_BRWXML_STAT_LAST(bxinfo->stats) == BX_BASE) ||
        (strcmp(name, "screen") == 0 && (tagmatch = TRUE) &&
         CTR_BRWXML_STAT_LAST(bxinfo->stats) == BX_SCREEN) ||
        (strcmp(name, "character") == 0 && (tagmatch = TRUE) &&
         CTR_BRWXML_STAT_LAST(bxinfo->stats) == BX_CHARACTER) ||
        (strcmp(name, "pixel") == 0 && (tagmatch = TRUE) &&
         CTR_BRWXML_STAT_LAST(bxinfo->stats) == BX_PIXEL) ||
        (strcmp(name, "wallpaper") == 0 && (tagmatch = TRUE) &&
         CTR_BRWXML_STAT_LAST(bxinfo->stats) == BX_WALLPAPER) ||
        (strcmp(name, "colors") == 0 && (tagmatch = TRUE) &&
         CTR_BRWXML_STAT_LAST(bxinfo->stats) == BX_COLORS) ||
        (strcmp(name, "pictures") == 0 && (tagmatch = TRUE) &&
         CTR_BRWXML_STAT_LAST(bxinfo->stats) == BX_PICTURES) ||
        (strcmp(name, "picture") == 0 && (tagmatch = TRUE) &&
         CTR_BRWXML_STAT_LAST(bxinfo->stats) == BX_PICTURE) ||
        (strcmp(name, "cache") == 0 && (tagmatch = TRUE) &&
         CTR_BRWXML_STAT_LAST(bxinfo->stats) == BX_CACHE)) {
            CTR_BRWXML_STAT_POP(bxinfo->stats);
            return NULL;
    }

    if (tagmatch)
        return apr_psprintf(pool, "found </%s> at invalid place", name);
    else
        return NULL;
}

static ctr_browser_t *ctr_new_browser(apr_pool_t *pool)
{
    ctr_browser_t *brw;

    brw = apr_palloc(pool, sizeof(*brw));
    brw->carrier = CR_UNKNOWN;
    brw->name = "";
    brw->group = "";
    brw->flashver = -1.0;
    brw->charwidth = -1;
    brw->charheight = -1;
    brw->pixwidth = -1;
    brw->pixheight = -1;
    brw->wpixwidth = -1;
    brw->wpixheight = -1;
    brw->colors = -1;
    brw->pictypes = NULL;
    brw->cachesize = -1;
    brw->iscacheincboth = -1;
    brw->basetype = BT_UNKNOWN;
    brw->basever = "";
    memset(&brw->spec, 0, sizeof(brw->spec));

    return brw;
}

static ctr_browser_t *ctr_browser_get(apr_hash_t *brwhash, const char *name,
                                      apr_pool_t *pool)
{
    ctr_browser_t *brw, *hbrw;

    brw = ctr_new_browser(pool);

    if (brwhash != NULL && (hbrw = CTR_HASH_GET(brwhash, name)) != NULL)
        *brw = *hbrw;

    return brw;
}

static char *ctr_brwxml_attr_get(apr_table_t *attrs, const char *attrname,
                                 const char *tagname, const char **errp,
                                 apr_pool_t *pool)
{
    const char *s;

    if ((s = apr_table_get(attrs, attrname)) == NULL) {
        if (errp != NULL)
            *errp = apr_psprintf(pool, "no %s in <%s>", attrname, tagname);
        return NULL;
    }
    return ctr_trim(apr_pstrdup(pool, s));
}

static char *ctr_brwxml_attr_get2(apr_table_t *attrs, const char *attrname,
                                  const char *tagname, const char *dflt,
                                  apr_pool_t *pool)
{
    const char *s;

    if ((s = apr_table_get(attrs, attrname)) == NULL)
        s = dflt;
    return ctr_trim(apr_pstrdup(pool, s));
}

static int ctr_brwxml_attr_pintget(apr_table_t *attrs, const char *attrname,
                                   const char *tagname, const char **errp,
                                   apr_pool_t *pool)
{
    const char *s;
    int v;

    if ((s = ctr_brwxml_attr_get(attrs, attrname, tagname,
                                 errp, pool)) == NULL)
        return -1;
    if ((v = (int)apr_atoi64(s)) <= 0) {
        if (errp != NULL) {
            *errp = apr_psprintf(pool, "invalid %s in <%s>",
                                 attrname, tagname);
        }
        return -1;
    }
    return v;
}

static double ctr_brwxml_attr_pdblget(apr_table_t *attrs, const char *attrname,
                                      const char *tagname, const char **errp,
                                      apr_pool_t *pool)
{
    const char *s;
    double v;

    if ((s = ctr_brwxml_attr_get(attrs, attrname, tagname,
                                 errp, pool)) == NULL)
        return -1.0;
    if ((v = atof(s)) < 1.0) {
        if (errp != NULL) {
            *errp = apr_psprintf(pool, "invalid %s in <%s>",
                                 attrname, tagname);
        }
        return -1.0;
    }
    return v;
}

static int ctr_brwxml_attr_symvalget(apr_table_t *attrs, const char *attrname,
                                     const ctr_symmap_t *syms,
                                     const char *tagname, const char **errp,
                                     apr_pool_t *pool)
{
    const char *s;

    if ((s = ctr_brwxml_attr_get(attrs, attrname, tagname,
                                 errp, pool)) == NULL)
        return -1;
    while (syms->sym != NULL) {
        if (strcasecmp(syms->sym, s) == 0)
            return syms->val;
        syms++;
    }
    if (errp != NULL)
        *errp = apr_psprintf(pool, "invalid %s in <%s>", attrname, tagname);
    return -1;
}

static int ctr_brwxml_attr_symvalget2(apr_table_t *attrs, const char *attrname,
                                      const ctr_symmap_t *syms,
                                      const char *tagname, const char **errp,
                                      int dflt, apr_pool_t *pool)
{
    const char *s;

    if (errp != NULL)
        *errp = NULL;

    if ((s = ctr_brwxml_attr_get(attrs, attrname, tagname,
                                 NULL, pool)) == NULL)
        return dflt;
    while (syms->sym != NULL) {
        if (strcasecmp(syms->sym, s) == 0)
            return syms->val;
        syms++;
    }
    if (errp != NULL)
        *errp = apr_psprintf(pool, "invalid %s in <%s>", attrname, tagname);
    return -1;
}

static int ctr_brwxml_attr_flgget(apr_table_t *attrs, const char *attrname,
                                  const char *tagname, const char **errp,
                                  apr_pool_t *pool)
{
    static const ctr_symmap_t syms[] = {
        { "true", TRUE }, { "false", FALSE }, { NULL, -1 }
    };

    return ctr_brwxml_attr_symvalget(attrs, attrname, syms,
                                     tagname, errp, pool);
}

static int ctr_brwxml_attr_flgget2(apr_table_t *attrs, const char *attrname,
                                   const char *tagname, const char **errp,
                                   int dflt, apr_pool_t *pool)
{
    static const ctr_symmap_t syms[] = {
        { "true", TRUE }, { "false", FALSE }, { NULL, -1 }
    };

    return ctr_brwxml_attr_symvalget2(attrs, attrname, syms,
                                      tagname, errp, dflt, pool);
}
