Add JSON encoder and decoder
authorAdam Dickmeiss <adam@indexdata.dk>
Tue, 12 Jan 2010 21:37:31 +0000 (22:37 +0100)
committerAdam Dickmeiss <adam@indexdata.dk>
Tue, 12 Jan 2010 21:38:17 +0000 (22:38 +0100)
include/yaz/Makefile.am
include/yaz/json.h [new file with mode: 0644]
src/Makefile.am
src/json.c [new file with mode: 0644]
test/.gitignore
test/Makefile.am
test/tst_json.c [new file with mode: 0644]

index cf41239..91a4672 100644 (file)
@@ -19,7 +19,7 @@ pkginclude_HEADERS= backend.h ccl.h ccl_xml.h cql.h rpn2cql.h comstack.h \
  z-grs.h z-mterm2.h z-opac.h z-rrf1.h z-rrf2.h z-sum.h z-sutrs.h z-uifr1.h \
  z-univ.h z-oclcui.h zes-expi.h zes-exps.h zes-order.h zes-pquery.h \
  zes-psched.h zes-admin.h zes-pset.h zes-update.h zes-update0.h \
- zoom.h z-charneg.h charneg.h soap.h srw.h zgdu.h matchstr.h
+ zoom.h z-charneg.h charneg.h soap.h srw.h zgdu.h matchstr.h json.h
 
 EXTRA_DIST = yaz-version.h.in
 
diff --git a/include/yaz/json.h b/include/yaz/json.h
new file mode 100644 (file)
index 0000000..b56ee4c
--- /dev/null
@@ -0,0 +1,121 @@
+/* This file is part of the YAZ toolkit.
+ * Copyright (C) 1995-2009 Index Data.
+ * All rights reserved.
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *     * Redistributions of source code must retain the above copyright
+ *       notice, this list of conditions and the following disclaimer.
+ *     * 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.
+ *     * Neither the name of Index Data 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 REGENTS 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 REGENTS AND 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.
+ */
+
+/** \file json.h
+    \brief Header for JSON functions
+*/
+
+#ifndef YAZ_JSON_H
+#define YAZ_JSON_H
+#include <yaz/wrbuf.h>
+
+YAZ_BEGIN_CDECL
+
+/** \brief JSON node type for json_node */
+enum json_node_type {
+    json_node_object, /**< JSON object, u.link[0] is object content */
+    json_node_array,  /**< JSON array, u.link[0] is array content */
+    json_node_list,   /**< JSON elements or JSON members,
+                         u.link[0] is value, u.link[1] is next elemen in list */
+    json_node_pair,   /**< JSON pair, u.link[0] is name, u.link[1] is value */
+    json_node_string, /**< JSON string, u.string is content */
+    json_node_number, /**< JSON number (floating point), u.number is content */
+    json_node_true,   /**< JSON true */
+    json_node_false,  /**< JSON false */
+    json_node_null    /**< JSON null */
+};
+
+/** \brief JSON node */
+struct json_node {
+    enum json_node_type type;
+    union {
+        char *string; 
+        double number; 
+        struct json_node *link[2];
+    } u;
+};
+
+/** \brief JSON parser (opaque) */
+typedef struct json_parser_s *json_parser_t;
+
+/** \brief create JSON parser
+    \returns JSON parser handle
+*/
+YAZ_EXPORT
+json_parser_t json_parser_create(void);
+
+/** \brief destroys JSON parser
+    \param p JSON parser handle
+*/
+YAZ_EXPORT
+void json_parser_destroy(json_parser_t p);
+
+/** \brief parses JSON string
+    \param p JSON parser handle
+    \param json_str JSON string
+    \returns JSON tree or NULL if parse error occurred.
+
+    The resulting tree should be removed with a call to json_remove_node.
+*/
+YAZ_EXPORT
+struct json_node *json_parser_parse(json_parser_t p, const char *json_str);
+
+/** \brief returns parser error
+    \param p JSON parser handle
+    \returns parse error msg 
+
+    This function should be called if json_parser_parse returns NULL .
+*/
+YAZ_EXPORT
+const char *json_parser_get_errmsg(json_parser_t p);
+
+/** \brief destroys JSON tree node and its children
+    \param n JSON node
+*/
+YAZ_EXPORT
+void json_remove_node(struct json_node *n);
+
+/** \brief converts JSON tree to JSON string
+    \param node JSON tree
+    \param result resulting JSON string buffer
+*/
+YAZ_EXPORT
+void json_write_wrbuf(struct json_node *node, WRBUF result);
+
+YAZ_END_CDECL
+
+#endif
+
+/*
+ * Local variables:
+ * c-basic-offset: 4
+ * c-file-style: "Stroustrup"
+ * indent-tabs-mode: nil
+ * End:
+ * vim: shiftwidth=4 tabstop=8 expandtab
+ */
+
index ee24dba..207022f 100644 (file)
@@ -102,7 +102,8 @@ libyaz_la_SOURCES=version.c options.c log.c \
   record_conv.c retrieval.c elementset.c snprintf.c query-charset.c \
   copy_types.c match_glob.c poll.c daemon.c \
   iconv_encode_marc8.c iconv_encode_iso_8859_1.c iconv_encode_wchar.c \
-  iconv_decode_marc8.c iconv_decode_iso5426.c iconv_decode_danmarc.c sc.c
+  iconv_decode_marc8.c iconv_decode_iso5426.c iconv_decode_danmarc.c sc.c \
+  json.c
 
 libyaz_la_LDFLAGS=-version-info $(YAZ_VERSION_INFO)
 
diff --git a/src/json.c b/src/json.c
new file mode 100644 (file)
index 0000000..41b99eb
--- /dev/null
@@ -0,0 +1,451 @@
+/* This file is part of the YAZ toolkit.
+ * Copyright (C) 1995-2009 Index Data
+ * See the file LICENSE for details.
+ */
+
+/**
+ * \file json.c
+ * \brief JSON encoding/decoding
+ */
+
+#include <yaz/json.h>
+
+#include <stdlib.h>
+#include <errno.h>
+#include <string.h>
+#include <assert.h>
+#include <stdio.h>
+
+#include <yaz/xmalloc.h>
+
+struct json_parser_s {
+    const char *buf;
+    const char *cp;
+    const char *err_msg;
+};
+
+json_parser_t json_parser_create(void)
+{
+    json_parser_t p = (json_parser_t) xmalloc(sizeof(*p));
+    
+    p->buf = 0;
+    p->cp = 0;
+    p->err_msg = 0;
+    return p;
+}
+
+void json_parser_destroy(json_parser_t p)
+{
+    xfree(p);
+}
+
+static int look_ch(json_parser_t p)
+{
+    while (*p->cp && strchr(" \t\r\n\f", *p->cp))
+        (p->cp)++;
+    return *p->cp;
+}
+
+static void move_ch(json_parser_t p)
+{
+    if (*p->cp)
+        (p->cp)++;
+}
+
+static struct json_node *json_new_node(json_parser_t p, enum json_node_type type)
+{
+    struct json_node *n = (struct json_node *) xmalloc(sizeof(*n));
+    n->type = type;
+    n->u.link[0] = n->u.link[1] = 0;
+    return n;
+}
+
+void json_remove_node(struct json_node *n)
+{
+    if (!n)
+        return;
+    switch (n->type)
+    {
+    case json_node_object:
+    case json_node_array:
+    case json_node_list:
+    case json_node_pair:
+        json_remove_node(n->u.link[0]);
+        json_remove_node(n->u.link[1]);
+        break;
+    case json_node_string:
+        xfree(n->u.string);
+        break;
+    case json_node_number:
+    case json_node_true:
+    case json_node_false:
+    case json_node_null:
+        break;
+    }
+    xfree(n);
+}
+
+static struct json_node *json_parse_object(json_parser_t p);
+static struct json_node *json_parse_array(json_parser_t p);
+
+static int json_one_char(const char **p, char *out)
+{
+    if (**p == '\\' && p[0][1])
+    {
+        (*p)++;
+        switch(**p)
+        {
+        case '"':
+            *out = '"'; break;
+        case '\\':
+            *out = '\\'; break;
+        case '/':
+            *out = '/'; break;
+        case 'b':
+            *out = '\b'; break;
+        case 'f':
+            *out = '\b'; break;
+        case 'n':
+            *out = '\n'; break;
+        case 'r':
+            *out = '\r'; break;
+        case 't':
+            *out = '\t'; break;
+        case 'u':
+            if (p[0][1])
+            {
+                unsigned code;
+                char *outp = out;
+                int error;
+                size_t outbytesleft = 6;
+                sscanf(*p + 1, "%4x", &code);
+                if (!yaz_write_UTF8_char(code, &outp, &outbytesleft, &error))
+                {
+                    *p += 5;
+                    return outp - out;
+                }
+            }
+        default:
+            *out = '_'; break;
+            break;
+        }
+        (*p)++;
+        return 1;
+    }
+    else
+    {
+        *out = **p;
+        (*p)++;
+        return 1;
+    }
+}
+
+static struct json_node *json_parse_string(json_parser_t p)
+{
+    struct json_node *n;
+    const char *cp;
+    char *dst;
+    int l = 0;
+    if (look_ch(p) != '\"')
+    {
+        p->err_msg = "string expected";
+        return 0;
+    }
+    move_ch(p);
+
+    cp = p->cp;
+    while (*cp && *cp != '"')
+    {
+        char out[6];
+        l += json_one_char(&cp, out);
+    }
+    if (!*cp)
+    {
+        p->err_msg = "missing \"";
+        return 0;
+    }
+    n = json_new_node(p, json_node_string);
+    dst = n->u.string = (char *) xmalloc(l + 1);
+    
+    cp = p->cp;
+    while (*cp && *cp != '"')
+    {
+        char out[6];
+
+        l = json_one_char(&cp, out);
+        memcpy(dst, out, l);
+        dst += l;
+    }
+    *dst = '\0';
+    p->cp = cp+1;
+    return n;
+}
+
+static struct json_node *json_parse_number(json_parser_t p)
+{
+    struct json_node *n;
+    char *endptr;
+    double v;
+
+    look_ch(p); // skip spaces
+    v = strtod(p->cp, &endptr);
+
+    if (endptr == p->cp)
+    {
+        p->err_msg = "bad number";
+        return 0;
+    }
+    p->cp = endptr;
+    n = json_new_node(p, json_node_number);
+    n->u.number = v;
+    return n;
+}
+
+static struct json_node *json_parse_value(json_parser_t p)
+{
+    int c = look_ch(p);
+    if (c == '\"')
+        return json_parse_string(p);
+    else if (strchr("0123456789-+", c))
+        return json_parse_number(p);
+    else if (c == '{')
+        return json_parse_object(p);
+    else if (c == '[')
+        return json_parse_array(p);
+    else
+    {
+        char tok[8];
+        int i = 0;
+        while (c >= 'a' && c <= 'z' && i < 7)
+        {
+            tok[i++] = c;
+            p->cp++;
+            c = *p->cp;
+        }
+        tok[i] = 0;
+        if (!strcmp(tok, "true"))
+            return json_new_node(p, json_node_true);
+        else if (!strcmp(tok, "false"))
+            return json_new_node(p, json_node_false);
+        else if (!strcmp(tok, "null"))
+            return json_new_node(p, json_node_null);
+        else
+        {
+            p->err_msg = "bad value";
+            return 0;
+        }
+    }
+}
+
+static struct json_node *json_parse_elements(json_parser_t p)
+{
+    struct json_node *n1 = json_parse_value(p);
+    struct json_node *m0, *m1;
+    if (!n1)
+        return 0;
+    m0 = m1 = json_new_node(p, json_node_list);
+    m1->u.link[0] = n1;
+    while (look_ch(p) == ',')
+    {
+        struct json_node *n2, *m2;
+        move_ch(p);
+        n2 = json_parse_value(p);
+        if (!n2)
+        {
+            json_remove_node(m0);
+            return 0;
+        }
+        m2 = json_new_node(p, json_node_list);
+        m2->u.link[0] = n2;
+        
+        m1->u.link[1] = m2;
+        m1 = m2;
+    }
+    return m0;
+}
+
+static struct json_node *json_parse_array(json_parser_t p)
+{
+    struct json_node *n;
+    if (look_ch(p) != '[')
+    {
+        p->err_msg = "expecting [";
+        return 0;
+    }
+    move_ch(p);
+    n = json_new_node(p, json_node_array);
+    if (look_ch(p) != ']')
+        n->u.link[0] = json_parse_elements(p);
+
+    if (look_ch(p) != ']')
+    {
+        p->err_msg = "expecting ]";
+        json_remove_node(n);
+        return 0;
+    }
+    move_ch(p);
+    return n;
+}
+
+static struct json_node *json_parse_pair(json_parser_t p)
+{
+    struct json_node *s = json_parse_string(p);
+    struct json_node *v, *n;
+    if (!s)
+        return 0;
+    if (look_ch(p) != ':')
+    {
+        json_remove_node(s);
+        return 0;
+    }
+    move_ch(p);
+    v = json_parse_value(p);
+    if (!v)
+    {
+        json_remove_node(s);
+        return 0;
+    }
+    n = json_new_node(p, json_node_pair);
+    n->u.link[0] = s;
+    n->u.link[1] = v;
+    return n;
+}
+
+static struct json_node *json_parse_members(json_parser_t p)
+{
+    struct json_node *n1 = json_parse_pair(p);
+    struct json_node *m0, *m1;
+    if (!n1)
+        return 0;
+    m0 = m1 = json_new_node(p, json_node_list);
+    m1->u.link[0] = n1;
+    while (look_ch(p) == ',')
+    {
+        struct json_node *n2, *m2;
+        move_ch(p);
+        n2 = json_parse_pair(p);
+        if (!n2)
+        {
+            json_remove_node(m0);
+            return 0;
+        }
+        m2 = json_new_node(p, json_node_list);
+        m2->u.link[0] = n2;
+        
+        m1->u.link[1] = m2;
+        m1 = m2;
+    }
+    return m0;
+}
+
+static struct json_node *json_parse_object(json_parser_t p)
+{
+    struct json_node *n;
+    if (look_ch(p) != '{')
+    {
+        p->err_msg = "{ expected";
+        return 0;
+    }
+    move_ch(p);
+
+    n = json_new_node(p, json_node_object);
+    if (look_ch(p) != '}')
+    {
+        struct json_node *m = json_parse_members(p);
+        if (!m)
+        {
+            json_remove_node(n);
+            return 0;
+        }
+        n->u.link[0] = m;
+    }
+    if (look_ch(p) != '}')
+    {
+        p->err_msg = "Missing }";
+        json_remove_node(n);
+        return 0;
+    }
+    move_ch(p);
+    return n;
+}
+
+struct json_node *json_parser_parse(json_parser_t p, const char *json_str)
+{
+    int c;
+    struct json_node *n;
+    p->buf = json_str;
+    p->cp = p->buf;
+
+    n = json_parse_object(p);
+    c = look_ch(p);
+    if (c != 0)
+    {
+        p->err_msg = "extra characters";
+        json_remove_node(n);
+        return 0;
+    }
+    return n;
+}
+
+void json_write_wrbuf(struct json_node *node, WRBUF result)
+{
+    switch (node->type)
+    {
+    case json_node_object:
+        wrbuf_puts(result, "{");
+        if (node->u.link[0])
+            json_write_wrbuf(node->u.link[0], result);
+        wrbuf_puts(result, "}");
+        break;
+    case json_node_array:
+        wrbuf_puts(result, "[");
+        if (node->u.link[0])
+            json_write_wrbuf(node->u.link[0], result);
+        wrbuf_puts(result, "]");
+        break;
+    case json_node_list:
+        json_write_wrbuf(node->u.link[0], result);
+        if (node->u.link[1])
+        {
+            wrbuf_puts(result, ",");
+            json_write_wrbuf(node->u.link[1], result);
+        }
+        break;
+    case json_node_pair:
+        json_write_wrbuf(node->u.link[0], result);
+        wrbuf_puts(result, ":");
+        json_write_wrbuf(node->u.link[1], result);
+        break;
+    case json_node_string:
+        wrbuf_puts(result, "\"");
+        wrbuf_puts(result, node->u.string);
+        wrbuf_puts(result, "\"");
+        break;
+    case json_node_number:
+        wrbuf_printf(result, "%lg", node->u.number);
+        break;
+    case json_node_true:
+        wrbuf_puts(result, "true");
+        break;
+    case json_node_false:
+        wrbuf_puts(result, "false");
+        break;
+    case json_node_null:
+        wrbuf_puts(result, "null");
+        break;
+    }
+}
+
+const char *json_parser_get_errmsg(json_parser_t p)
+{
+    return p->err_msg;
+}
+
+/*
+ * Local variables:
+ * c-basic-offset: 4
+ * c-file-style: "Stroustrup"
+ * indent-tabs-mode: nil
+ * End:
+ * vim: shiftwidth=4 tabstop=8 expandtab
+ */
index d310adc..dabacdc 100644 (file)
@@ -28,6 +28,7 @@ tst_query_charset
 tst_icu_I18N
 tst_match_glob
 tst_rpn2cql
+tst_json
 nfatest1
 nfaxmltest1
 tst_oid
index da862d4..d69ac07 100644 (file)
@@ -5,7 +5,9 @@ check_PROGRAMS = tstxmalloc tsticonv tstnmem tstmatchstr tstwrbuf tstodr \
  tstccl tstlog \
  tstsoap1 tstsoap2 tstodrstack tstlogthread tstxmlquery tstpquery \
  tst_comstack tst_filepath tst_record_conv tst_retrieval tst_tpath \
- tst_timing tst_query_charset tst_oid tst_icu_I18N tst_match_glob tst_rpn2cql
+ tst_timing tst_query_charset tst_oid tst_icu_I18N tst_match_glob \
+ tst_rpn2cql tst_json
+
 check_SCRIPTS = tstmarc.sh tstmarccol.sh tstcql2xcql.sh tstcql2pqf.sh tsticu.sh
 
 TESTS = $(check_PROGRAMS) $(check_SCRIPTS)
@@ -76,3 +78,4 @@ tst_query_charset_SOURCES = tst_query_charset.c
 tst_icu_I18N_SOURCES = tst_icu_I18N.c
 tst_match_glob_SOURCES = tst_match_glob.c
 tst_rpn2cql_SOURCES = tst_rpn2cql.c
+tst_json_SOURCES = tst_json.c
diff --git a/test/tst_json.c b/test/tst_json.c
new file mode 100644 (file)
index 0000000..d20beba
--- /dev/null
@@ -0,0 +1,136 @@
+/**
+ * \file 
+ * \brief JSON test
+ */
+#include <yaz/test.h>
+#include <yaz/json.h>
+#include <string.h>
+#include <yaz/log.h>
+
+static int expect(json_parser_t p, const char *input, 
+                  const char *output)
+{
+    int ret = 0;
+    struct json_node *n;
+
+    n = json_parser_parse(p, input);
+    if (n == 0 && output == 0)
+        ret = 1;
+    else if (n && output)
+    {
+        WRBUF result = wrbuf_alloc();
+
+        json_write_wrbuf(n, result);
+        if (strcmp(wrbuf_cstr(result), output) == 0)
+            ret = 1;
+        else
+        {
+            yaz_log(YLOG_WARN, "expected '%s' but got '%s'",
+                    output, wrbuf_cstr(result));
+        }
+        wrbuf_destroy(result);
+    }
+    else if (!n)
+    {
+        yaz_log(YLOG_WARN, "expected '%s' but got error '%s'",
+                output, json_parser_get_errmsg(p));
+    }
+    json_remove_node(n);
+    return ret;
+}
+
+static void tst1(void)
+{
+    json_parser_t p = json_parser_create();
+
+    YAZ_CHECK(p);
+    if (!p)
+        return;
+
+    YAZ_CHECK(expect(p, "", 0));
+
+    YAZ_CHECK(expect(p, "1234", 0));
+
+    YAZ_CHECK(expect(p, "[ 1234 ]", 0));
+
+    YAZ_CHECK(expect(p, "{\"k\":tru}", 0));
+
+    YAZ_CHECK(expect(p, "{\"k\":null", 0));
+
+    YAZ_CHECK(expect(p, "{\"k\":nullx}", 0));
+
+    YAZ_CHECK(expect(p, "{\"k\":-", 0));
+
+    YAZ_CHECK(expect(p, "{\"k\":+", 0));
+
+    YAZ_CHECK(expect(p, "{\"k\":\"a}", 0));
+
+    YAZ_CHECK(expect(p, "{\"k\":\"a", 0));
+
+    YAZ_CHECK(expect(p, "{\"k\":\"", 0));
+
+    YAZ_CHECK(expect(p, "{", 0));
+
+    YAZ_CHECK(expect(p, "{}", "{}"));
+
+    YAZ_CHECK(expect(p, "{}  extra", 0));
+
+    YAZ_CHECK(expect(p, "{\"a\":[1,2,3}", 0));
+    
+    YAZ_CHECK(expect(p, "{\"a\":[1,2,", 0));
+
+    YAZ_CHECK(expect(p, "{\"k\":\"wa\"}", "{\"k\":\"wa\"}"));
+
+    YAZ_CHECK(expect(p, "{\"k\":null}", "{\"k\":null}"));
+
+    YAZ_CHECK(expect(p, "{\"k\":false}", "{\"k\":false}"));
+
+    YAZ_CHECK(expect(p, "{\"k\":true}", "{\"k\":true}"));
+
+    YAZ_CHECK(expect(p, "{\"k\":12}", "{\"k\":12}"));
+
+    YAZ_CHECK(expect(p, "{\"k\":-12}", "{\"k\":-12}"));
+
+    YAZ_CHECK(expect(p, "{\"k\":1.2e6}", "{\"k\":1.2e+06}"));
+
+    YAZ_CHECK(expect(p, "{\"k\":1e3}", "{\"k\":1000}"));
+
+    YAZ_CHECK(expect(p, "{\"k\":\"\"}", "{\"k\":\"\"}"));
+
+    YAZ_CHECK(expect(p, "{\"a\":1,\"b\":2}", "{\"a\":1,\"b\":2}"));
+
+    YAZ_CHECK(expect(p, "{\"a\":1,\"b\":2,\"c\":3}",
+                     "{\"a\":1,\"b\":2,\"c\":3}"));
+
+    YAZ_CHECK(expect(p, "{\"a\":[]}", "{\"a\":[]}"));
+
+    YAZ_CHECK(expect(p, "{\"a\":[1]}", "{\"a\":[1]}"));
+
+    YAZ_CHECK(expect(p, "{\"a\":[1,2]}", "{\"a\":[1,2]}"));
+
+    YAZ_CHECK(expect(p, "{\"a\":[1,2,3]}", "{\"a\":[1,2,3]}"));
+
+    YAZ_CHECK(expect(p, "{\"k\":\"\\t\"}", "{\"k\":\"\x09\"}"));
+
+    YAZ_CHECK(expect(p, "{\"k\":\"\\u0009\"}", "{\"k\":\"\x09\"}"));
+
+    json_parser_destroy(p);
+}
+
+int main (int argc, char **argv)
+{
+    YAZ_CHECK_INIT(argc, argv);
+    tst1();
+    YAZ_CHECK_TERM;
+}
+
+/*
+ * Local variables:
+ * c-basic-offset: 4
+ * c-file-style: "Stroustrup"
+ * indent-tabs-mode: nil
+ * End:
+ * vim: shiftwidth=4 tabstop=8 expandtab
+ */
+
+