https://github.com/dchest/pyblake2
Tip revision: 646d0a55da24196c5312b35573598cd18f8137b1 authored by Dmitry Chestnykh on 14 September 2020, 14:32:33 UTC
EOL
EOL
Tip revision: 646d0a5
pyblake2module.c
/*
* Written in 2013 by Dmitry Chestnykh <dmitry@codingrobots.com>
*
* To the extent possible under law, the author have dedicated all
* copyright and related and neighboring rights to this software to
* the public domain worldwide. This software is distributed without
* any warranty. http://creativecommons.org/publicdomain/zero/1.0/
*/
#include <Python.h>
#include "pyblake2_impl_common.h"
#include "impl/blake2.h"
#include "impl/blake2-impl.h" /* for secure_zero_memory() and store48() */
PyDoc_STRVAR(pyblake2__doc__,
"pyblake2 is an extension module implementing BLAKE2 hash function\n"
"with hashlib-compatible interface.\n"
"\n"
"Examples:\n"
"\n"
" >>> from pyblake2 import blake2s\n"
" >>> blake2s(b'cats').hexdigest()\n"
" 'c473a8d190c3867bdaf6529e8d8531925e824cff07f17d489233fde665979f0c'\n"
"\n"
" >>> from pyblake2 import blake2b\n"
" >>> h = blake2b(digest_size = 20)\n"
" >>> h.update(b'Take that, Keccak')\n"
" >>> h.digest()\n"
" '\\x98\\xb37\\xfa\\xf5\\xbc\\tY\\xca\\x12\\x19\\x12\\xbd\\xfa"
"\\xd8\\x13z\\xf9\\xffl'\n"
);
/*
* Python 2-3 compatibility macros.
*/
#if PY_MAJOR_VERSION >= 3
# define COMPAT_PYINT_AS_LONG PyLong_AsLong
# define COMPAT_PYINT_FROM_LONG PyLong_FromLong
# define COMPAT_PYSTRING_FROM_STRING PyUnicode_FromString
# define COMPAT_PYSTRING_FROM_STRING_AND_SIZE PyUnicode_FromStringAndSize
# define COMPAT_PYBYTES_FROM_STRING_AND_SIZE PyBytes_FromStringAndSize
# define BYTES_FMT "y"
#else
# define COMPAT_PYINT_AS_LONG PyInt_AsLong
# define COMPAT_PYINT_FROM_LONG PyInt_FromLong
# define COMPAT_PYSTRING_FROM_STRING PyString_FromString
# define COMPAT_PYSTRING_FROM_STRING_AND_SIZE PyString_FromStringAndSize
# define COMPAT_PYBYTES_FROM_STRING_AND_SIZE PyString_FromStringAndSize
# define BYTES_FMT "s"
#endif
/*
* Minimum size of buffer when updating hash
* object for GIL to be released.
*/
#define GIL_MINSIZE 2048
#ifdef WITH_THREAD
# include "pythread.h"
# define OBJECT_LOCK_FIELD PyThread_type_lock lock;
# define ACQUIRE_LOCK(obj) \
if ((obj)->lock) { \
if (!PyThread_acquire_lock((obj)->lock, 0)) { \
Py_BEGIN_ALLOW_THREADS \
PyThread_acquire_lock((obj)->lock, 1); \
Py_END_ALLOW_THREADS \
} \
}
# define RELEASE_LOCK(obj) \
if ((obj)->lock) { \
PyThread_release_lock((obj)->lock); \
}
# define INIT_LOCK(obj) do { \
(obj)->lock = NULL; \
} while (0)
# define FREE_LOCK(obj) \
if ((obj)->lock) { \
PyThread_free_lock((obj)->lock); \
(obj)->lock = NULL; \
}
#else
# define OBJECT_LOCK_FIELD
# define ACQUIRE_LOCK(obj)
# define RELEASE_LOCK(obj)
# define INIT_LOCK(obj)
# define FREE_LOCK(obj)
#endif /* !WITH_THREAD */
static inline int
getbuffer(PyObject *obj, Py_buffer *bp) {
if (PyUnicode_Check(obj)) {
PyErr_SetString(PyExc_TypeError,
"Unicode-objects must be encoded before hashing");
return 0;
}
if (!PyObject_CheckBuffer(obj)) {
PyErr_SetString(PyExc_TypeError,
"object supporting the buffer API required");
return 0;
}
if (PyObject_GetBuffer(obj, bp, PyBUF_SIMPLE) == -1)
return 0;
if (bp->ndim > 1) {
PyErr_SetString(PyExc_BufferError, "Buffer must be single dimension");
PyBuffer_Release(bp);
return 0;
}
return 1;
}
static void
tohex(char *dst, uint8_t *src, size_t srclen)
{
static char hexdigits[] = "0123456789abcdef";
size_t i;
for (i = 0; i < srclen; i++) {
dst[i*2 + 0] = hexdigits[(src[i] >> 4) & 0x0f];
dst[i*2 + 1] = hexdigits[src[i] & 0x0f];
}
}
/*
* Helpers for setting node offset.
*/
static inline int
blake2b_set_node_offset(blake2b_param *param, uint64_t offset)
{
store64(¶m->node_offset, offset);
return 1;
}
static inline int
blake2s_set_node_offset(blake2s_param *param, uint64_t offset)
{
if (offset > 0xFFFFFFFFFFFFULL) /* maximum 2**48 - 1 */
return 0;
store48(¶m->node_offset, offset);
return 1;
}
/*
* Unleash the macros!
*/
#define DECL_BLAKE2_STRUCT(name) \
static PyTypeObject name##Type; \
\
typedef struct { \
PyObject_HEAD \
name##_param param; \
name##_state state; \
OBJECT_LOCK_FIELD \
} name##Object;
#define DECL_NEW_BLAKE2_OBJECT(name) \
static name##Object * \
new_##name##Object(void) \
{ \
name##Object *obj; \
obj = (name##Object *)PyObject_New(name##Object, &name##Type); \
if (obj != NULL) INIT_LOCK(obj); \
return obj; \
}
static char *kwlist[] = {
"data", "digest_size", "key", "salt", "person",
"fanout", "depth", "leaf_size", "node_offset",
"node_depth", "inner_size", "last_node", NULL
};
#define DECL_INIT_BLAKE2_OBJECT(name, bigname) \
static int \
init_##name##Object(name##Object *self, PyObject *args, PyObject *kw) \
{ \
Py_buffer buf, key, salt, person; \
PyObject *data = NULL, *last_node_obj = NULL, *fanout_obj = NULL, \
*depth_obj = NULL; \
unsigned int leaf_size = 0; \
unsigned PY_LONG_LONG node_offset = 0; \
int node_depth = 0, inner_size = 0, digest_size = bigname##_OUTBYTES; \
long fanout = 1, depth = 1; \
\
/* Initialize buffers. */ \
key.buf = salt.buf = person.buf = NULL; \
\
/* Parse arguments. */ \
if (!PyArg_ParseTupleAndKeywords(args, kw, \
"|" /* following arguments are optional */ \
"O" /* `data` as PyObject */ \
"i" /* `digest_size` as int */ \
BYTES_FMT"*" /* `key` as Py_buffer */ \
BYTES_FMT"*" /* `salt` as Py_buffer */ \
BYTES_FMT"*" /* `person` as Py_buffer */ \
"O" /* `fanout` as PyObject */ \
"O" /* `depth` as PyObject */ \
"I" /* `leaf_size` as unsigned int */ \
"K" /* `node_offset` as unsigned PY_LONG_LONG*/ \
"i" /* `node_depth` as int */ \
"i" /* `inner_size` as int */ \
"O" /* `last_node` as PyObject */ \
":"#name"", /* function name for errors */ \
kwlist, &data, &digest_size, &key, &salt, &person, \
&fanout_obj, &depth_obj, &leaf_size, &node_offset, \
&node_depth, &inner_size, &last_node_obj)) \
goto err0; \
\
/* Zero parameter block. */ \
memset(&self->param, 0, sizeof(self->param)); \
\
/* Set digest size. */ \
if (digest_size <= 0 || digest_size > bigname##_OUTBYTES) { \
PyErr_Format(PyExc_ValueError, \
"digest_size must be between 1 and %d bytes", \
bigname##_OUTBYTES); \
goto err0; \
} \
self->param.digest_length = digest_size; \
\
/* Set salt parameter. */ \
if (salt.buf != NULL) { \
if (salt.len > bigname##_SALTBYTES) { \
PyErr_Format(PyExc_ValueError, \
"maximum salt length is %d bytes", \
bigname##_SALTBYTES); \
goto err0; \
} \
memcpy(self->param.salt, salt.buf, salt.len); \
} \
\
/* Set personalization parameter. */ \
if (person.buf != NULL) { \
if (person.len > bigname##_PERSONALBYTES) { \
PyErr_Format(PyExc_ValueError, \
"maximum person length is %d bytes", \
bigname##_PERSONALBYTES); \
goto err0; \
} \
memcpy(self->param.personal, person.buf, person.len); \
} \
\
/* Set tree parameters. */ \
if (fanout_obj != NULL) { \
fanout = COMPAT_PYINT_AS_LONG(fanout_obj); \
if (fanout == -1 && PyErr_Occurred()) \
goto err0; \
} \
if (fanout < 0 || fanout > 255) { \
PyErr_SetString(PyExc_ValueError, \
"fanout must be between 0 and 255"); \
goto err0; \
} \
self->param.fanout = (uint8_t)fanout; \
\
if (depth_obj != NULL) { \
depth = COMPAT_PYINT_AS_LONG(depth_obj); \
if (depth == -1 && PyErr_Occurred()) \
goto err0; \
} \
if (depth <= 0 || depth > 255) { \
PyErr_SetString(PyExc_ValueError, \
"depth must be between 1 and 255"); \
goto err0; \
} \
self->param.depth = (uint8_t)depth; \
\
store32(&self->param.leaf_length, leaf_size); \
\
if (!name##_set_node_offset(&self->param, node_offset)) { \
PyErr_SetString(PyExc_ValueError, "node_offset is too large"); \
goto err0; \
} \
\
if (node_depth < 0 || node_depth > 255) { \
PyErr_SetString(PyExc_ValueError, \
"node_depth must be between 0 and 255"); \
goto err0; \
} \
self->param.node_depth = node_depth; \
\
if (inner_size < 0 || inner_size > bigname##_OUTBYTES) { \
PyErr_Format(PyExc_ValueError, \
"inner_size must be between 0 and is %d", \
bigname##_OUTBYTES); \
goto err0; \
} \
self->param.inner_length = inner_size; \
\
/* Set key length. */ \
if (key.buf != NULL && key.len > 0) { \
if (key.len > bigname##_KEYBYTES) { \
PyErr_Format(PyExc_ValueError, \
"maximum key length is %d bytes", \
bigname##_KEYBYTES); \
goto err0; \
} \
self->param.key_length = key.len; \
} \
\
/* Initialize hash state. */ \
if (name##_init_param(&self->state, &self->param) < 0) { \
PyErr_SetString(PyExc_RuntimeError, \
"error initializing hash state"); \
goto err0; \
} \
\
/* Set last node flag (must come after initialization). */ \
self->state.last_node = (last_node_obj != NULL && \
PyObject_IsTrue(last_node_obj)); \
\
/* Process key block if any. */ \
if (key.buf != NULL && key.len > 0) { \
uint8_t block[bigname##_BLOCKBYTES]; \
memset(block, 0, sizeof(block)); \
memcpy(block, key.buf, key.len); \
name##_update(&self->state, block, sizeof(block)); \
secure_zero_memory(block, sizeof(block)); \
} \
\
/* Process initial data if any. */ \
if (data != NULL) { \
if (!getbuffer(data, &buf)) \
goto err0; \
\
if (buf.len >= GIL_MINSIZE) { \
Py_BEGIN_ALLOW_THREADS \
name##_update(&self->state, buf.buf, buf.len); \
Py_END_ALLOW_THREADS \
} else { \
name##_update(&self->state, buf.buf, buf.len); \
} \
PyBuffer_Release(&buf); \
} \
\
/* Release buffers. */ \
if (key.buf != NULL) \
PyBuffer_Release(&key); \
if (salt.buf != NULL) \
PyBuffer_Release(&salt); \
if (person.buf != NULL) \
PyBuffer_Release(&person); \
\
return 1; \
\
err0: \
/* Error: release buffers. */ \
if (key.buf != NULL) \
PyBuffer_Release(&key); \
if (salt.buf != NULL) \
PyBuffer_Release(&salt); \
if (person.buf != NULL) \
PyBuffer_Release(&person); \
\
return 0; \
}
/*
* Methods.
*/
#define DECL_PY_BLAKE2_COPY(name) \
PyDoc_STRVAR(py_##name##_copy__doc__, \
"Return a copy of the hash object."); \
\
static PyObject * \
py_##name##_copy(name##Object *self, PyObject *unused) \
{ \
name##Object *cpy; \
\
if ((cpy = new_##name##Object()) == NULL) \
return NULL; \
\
ACQUIRE_LOCK(self); \
cpy->param = self->param; \
cpy->state = self->state; \
RELEASE_LOCK(self); \
return (PyObject *)cpy; \
}
/*
* Macro used inside DECL_PY_BLAKE2_UPDATE:
*/
#ifdef WITH_THREAD
/* With threads:
* Update hash object with buffer, releasing GIL if length of buffer
* is greater than or equal to GIL_MINSIZE.
*/
# define INNER_UPDATE(name) do { \
if (self->lock == NULL && buf.len >= GIL_MINSIZE) \
self->lock = PyThread_allocate_lock(); \
\
if (self->lock != NULL) { \
Py_BEGIN_ALLOW_THREADS \
PyThread_acquire_lock(self->lock, 1); \
name##_update(&self->state, buf.buf, buf.len); \
PyThread_release_lock(self->lock); \
Py_END_ALLOW_THREADS \
} else { \
name##_update(&self->state, buf.buf, buf.len); \
} \
} while (0)
#else
/* Without threads:
* just update hash object with buffer.
*/
# define INNER_UPDATE(name) do { \
name##_update(&self->state, buf.buf, buf.len); \
} while (0)
#endif /* !WITH_THREAD */
#define DECL_PY_BLAKE2_UPDATE(name) \
PyDoc_STRVAR(py_##name##_update__doc__, \
"Update the hash object with the object, which must be interpretable " \
"as buffer of bytes."); \
\
static PyObject * \
py_##name##_update(name##Object *self, PyObject *args) \
{ \
PyObject *obj; \
Py_buffer buf; \
\
if (!PyArg_ParseTuple(args, "O:update", &obj)) \
return NULL; \
\
if (!getbuffer(obj, &buf)) \
return NULL; \
\
INNER_UPDATE(name); \
PyBuffer_Release(&buf); \
\
Py_INCREF(Py_None); \
return Py_None; \
}
#define DECL_PY_BLAKE2_DIGEST(name, bigname) \
PyDoc_STRVAR(py_##name##_digest__doc__, \
"Return the digest of the data so far."); \
\
static PyObject * \
py_##name##_digest(name##Object *self, PyObject *unused) \
{ \
uint8_t digest[bigname##_OUTBYTES]; \
name##_state state_cpy; \
\
ACQUIRE_LOCK(self); \
state_cpy = self->state; \
name##_final(&state_cpy, digest, self->param.digest_length); \
RELEASE_LOCK(self); \
return COMPAT_PYBYTES_FROM_STRING_AND_SIZE((const char *)digest, \
self->param.digest_length); \
}
#define DECL_PY_BLAKE2_HEXDIGEST(name, bigname) \
PyDoc_STRVAR(py_##name##_hexdigest__doc__, \
"Like digest() except the digest is returned as a string of double " \
"length, containing only hexadecimal digits."); \
\
static PyObject * \
py_##name##_hexdigest(name##Object *self, PyObject *unused) \
{ \
uint8_t digest[bigname##_OUTBYTES]; \
char hexdigest[sizeof(digest) * 2]; \
name##_state state_cpy; \
\
ACQUIRE_LOCK(self); \
state_cpy = self->state; \
name##_final(&state_cpy, digest, self->param.digest_length); \
tohex(hexdigest, digest, self->param.digest_length); \
RELEASE_LOCK(self); \
return COMPAT_PYSTRING_FROM_STRING_AND_SIZE((const char *)hexdigest,\
self->param.digest_length * 2); \
}
#define DECL_PY_BLAKE2_METHODS(name) \
static PyMethodDef name##_methods[] = { \
{"copy", (PyCFunction)py_##name##_copy, METH_NOARGS, \
py_##name##_copy__doc__}, \
{"digest", (PyCFunction)py_##name##_digest, METH_NOARGS, \
py_##name##_digest__doc__}, \
{"hexdigest", (PyCFunction)py_##name##_hexdigest, METH_NOARGS, \
py_##name##_hexdigest__doc__}, \
{"update", (PyCFunction)py_##name##_update, METH_VARARGS, \
py_##name##_update__doc__}, \
{NULL, NULL} \
};
/*
* Getters.
*/
#define DECL_PY_BLAKE2_GET_NAME(name) \
static PyObject * \
py_##name##_get_name(name##Object *self, void *closure) \
{ \
return COMPAT_PYSTRING_FROM_STRING("" #name ""); \
}
#define DECL_PY_BLAKE2_GET_BLOCK_SIZE(name, bigname) \
static PyObject * \
py_##name##_get_block_size(name##Object *self, void *closure) \
{ \
return COMPAT_PYINT_FROM_LONG(bigname##_BLOCKBYTES); \
}
#define DECL_PY_BLAKE2_GET_DIGEST_SIZE(name) \
static PyObject * \
py_##name##_get_digest_size(name##Object *self, void *closure) \
{ \
return COMPAT_PYINT_FROM_LONG(self->param.digest_length); \
}
#define DECL_PY_BLAKE2_GETSETTERS(name) \
static PyGetSetDef name##_getsetters[] = { \
{"name", (getter)py_##name##_get_name, \
NULL, NULL, NULL}, \
{"block_size", (getter)py_##name##_get_block_size, \
NULL, NULL, NULL}, \
{"digest_size", (getter)py_##name##_get_digest_size, \
NULL, NULL, NULL}, \
{NULL} \
};
#define DECL_PY_BLAKE2_DEALLOC(name) \
static void \
py_##name##_dealloc(PyObject *self) \
{ \
name##Object *obj = (name##Object *)self; \
\
/* Try not to leave state in memory. */ \
secure_zero_memory(&obj->param, sizeof(obj->param)); \
secure_zero_memory(&obj->state, sizeof(obj->state)); \
FREE_LOCK(obj); \
PyObject_Del(self); \
}
#define DECL_PY_BLAKE2_TYPE_OBJECT(name) \
static PyTypeObject name##Type = { \
PyVarObject_HEAD_INIT(NULL, 0) \
"pyblake2." #name, /* tp_name */ \
sizeof(name##Object), /* tp_size */ \
0, /* tp_itemsize */ \
py_##name##_dealloc, /* tp_dealloc */ \
0, /* tp_print */ \
0, /* tp_getattr */ \
0, /* tp_setattr */ \
0, /* tp_compare */ \
0, /* tp_repr */ \
0, /* tp_as_number */ \
0, /* tp_as_sequence */ \
0, /* tp_as_mapping */ \
0, /* tp_hash */ \
0, /* tp_call */ \
0, /* tp_str */ \
0, /* tp_getattro */ \
0, /* tp_setattro */ \
0, /* tp_as_buffer */ \
Py_TPFLAGS_DEFAULT, /* tp_flags */ \
0, /* tp_doc */ \
0, /* tp_traverse */ \
0, /* tp_clear */ \
0, /* tp_richcompare */ \
0, /* tp_weaklistoffset */ \
0, /* tp_iter */ \
0, /* tp_iternext */ \
name##_methods, /* tp_methods */ \
0, /* tp_members */ \
name##_getsetters, /* tp_getset */ \
};
#define DECL_PY_BLAKE2_NEW(name) \
static PyObject * \
py_##name##_new(PyObject *self, PyObject *args, PyObject *kwdict) \
{ \
name##Object *obj; \
\
if ((obj = new_##name##Object()) == NULL) \
goto err0; \
\
if (PyErr_Occurred()) \
goto err1; \
\
if (!init_##name##Object(obj, args, kwdict)) \
goto err1; \
\
return (PyObject *)obj; \
\
err1: \
Py_DECREF(obj); \
err0: \
return NULL; \
}
#define DECL_BLAKE2_WRAPPER(name, bigname) \
DECL_BLAKE2_STRUCT(name) \
DECL_NEW_BLAKE2_OBJECT(name) \
DECL_INIT_BLAKE2_OBJECT(name, bigname) \
DECL_PY_BLAKE2_COPY(name) \
DECL_PY_BLAKE2_UPDATE(name) \
DECL_PY_BLAKE2_DIGEST(name, bigname) \
DECL_PY_BLAKE2_HEXDIGEST(name, bigname) \
DECL_PY_BLAKE2_METHODS(name) \
DECL_PY_BLAKE2_GET_NAME(name) \
DECL_PY_BLAKE2_GET_BLOCK_SIZE(name, bigname) \
DECL_PY_BLAKE2_GET_DIGEST_SIZE(name) \
DECL_PY_BLAKE2_GETSETTERS(name) \
DECL_PY_BLAKE2_DEALLOC(name) \
DECL_PY_BLAKE2_TYPE_OBJECT(name) \
DECL_PY_BLAKE2_NEW(name)
/*
* Declare objects.
* Note: don't forget to update module init function and constants.
*/
PyDoc_STRVAR(py_blake2b_new__doc__,
"blake2b(data=b'', digest_size=64, key=b'', salt=b'', person=b'', "
"fanout=1, depth=1, leaf_size=0, node_offset=0, node_depth=0, "
"inner_size=0, last_node=False) -> blake2b object\n"
"\n"
"Return a new BLAKE2b hash object.");
DECL_BLAKE2_WRAPPER(blake2b, BLAKE2B)
PyDoc_STRVAR(py_blake2s_new__doc__,
"blake2s(data=b'', digest_size=32, key=b'', salt=b'', person=b'', "
"fanout=1, depth=1, leaf_size=0, node_offset=0, node_depth=0, "
"inner_size=0, last_node=False) -> blake2s object\n"
"\n"
"Return a new BLAKE2s hash object.");
DECL_BLAKE2_WRAPPER(blake2s, BLAKE2S)
/*
* Module.
*/
static struct PyMethodDef pyblake2_functions[] = {
{"blake2b", (PyCFunction)py_blake2b_new, METH_VARARGS|METH_KEYWORDS,
py_blake2b_new__doc__},
{"blake2s", (PyCFunction)py_blake2s_new, METH_VARARGS|METH_KEYWORDS,
py_blake2s_new__doc__},
{NULL, NULL}
};
#if PY_MAJOR_VERSION >= 3
static struct PyModuleDef pyblake2_module = {
PyModuleDef_HEAD_INIT,
"pyblake2",
pyblake2__doc__,
-1,
pyblake2_functions,
NULL,
NULL,
NULL,
NULL
};
# define INIT_FUNC_NAME PyInit_pyblake2
# define INIT_ERROR return NULL
#else
# define INIT_FUNC_NAME initpyblake2
# define INIT_ERROR return
#endif
PyMODINIT_FUNC
INIT_FUNC_NAME(void)
{
PyObject *m;
Py_TYPE(&blake2bType) = &PyType_Type;
if (PyType_Ready(&blake2bType) < 0)
INIT_ERROR;
Py_TYPE(&blake2sType) = &PyType_Type;
if (PyType_Ready(&blake2sType) < 0)
INIT_ERROR;
/* TODO: do runtime self-check */
#if PY_MAJOR_VERSION >= 3
m = PyModule_Create(&pyblake2_module);
#else
m = Py_InitModule3("pyblake2", pyblake2_functions, pyblake2__doc__);
#endif
/* Add module constants. */
PyModule_AddIntConstant(m, "BLAKE2B_SALT_SIZE", BLAKE2B_SALTBYTES);
PyModule_AddIntConstant(m, "BLAKE2B_PERSON_SIZE", BLAKE2B_PERSONALBYTES);
PyModule_AddIntConstant(m, "BLAKE2B_MAX_KEY_SIZE", BLAKE2B_KEYBYTES);
PyModule_AddIntConstant(m, "BLAKE2B_MAX_DIGEST_SIZE", BLAKE2B_OUTBYTES);
PyModule_AddIntConstant(m, "BLAKE2S_SALT_SIZE", BLAKE2S_SALTBYTES);
PyModule_AddIntConstant(m, "BLAKE2S_PERSON_SIZE", BLAKE2S_PERSONALBYTES);
PyModule_AddIntConstant(m, "BLAKE2S_MAX_KEY_SIZE", BLAKE2S_KEYBYTES);
PyModule_AddIntConstant(m, "BLAKE2S_MAX_DIGEST_SIZE", BLAKE2S_OUTBYTES);
#if PY_MAJOR_VERSION >= 3
return m;
#endif
}
/* vim: set expandtab ts=4 sw=4: */