/* cyrusdb_quotalegacy: cyrusdb backend for accessing legacy quota files
*
* Copyright (c) 1994-2008 Carnegie Mellon University. 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. The name "Carnegie Mellon University" must not be used to
* endorse or promote products derived from this software without
* prior written permission. For permission or any legal
* details, please contact
* Carnegie Mellon University
* Center for Technology Transfer and Enterprise Creation
* 4615 Forbes Avenue
* Suite 302
* Pittsburgh, PA 15213
* (412) 268-7393, fax: (412) 268-7395
* innovation@andrew.cmu.edu
*
* 4. Redistributions of any form whatsoever must retain the following
* acknowledgment:
* "This product includes software developed by Computing Services
* at Carnegie Mellon University (http://www.cmu.edu/computing/)."
*
* CARNEGIE MELLON UNIVERSITY DISCLAIMS ALL WARRANTIES WITH REGARD TO
* THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS, IN NO EVENT SHALL CARNEGIE MELLON UNIVERSITY BE LIABLE
* FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
* AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
* OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
/* FILE FORMAT:
* used\nlimit\n
* where used is a 64 bit unsigned value in bytes
* limit is a 32 bit signed value in kilobytes (1024 bytes)
* special value: -1 means unlimited quota
* then there's some extra rubbish for multiple quotas
* and finally there's the dlist file format, which just plain sucks less
*/
#include <config.h>
#ifdef HAVE_UNISTD_H
#include <unistd.h>
#endif
#include <stdio.h>
#include <stdlib.h>
#include <syslog.h>
#include <string.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/uio.h>
#include <fcntl.h>
#if HAVE_DIRENT_H
# include <dirent.h>
# define NAMLEN(dirent) strlen((dirent)->d_name)
#else
# define dirent direct
# define NAMLEN(dirent) (dirent)->d_namlen
# if HAVE_SYS_NDIR_H
# include <sys/ndir.h>
# endif
# if HAVE_SYS_DIR_H
# include <sys/dir.h>
# endif
# if HAVE_NDIR_H
# include <ndir.h>
# endif
#endif
#include "assert.h"
#include "bsearch.h"
#include "cyrusdb.h"
#include "exitcodes.h"
#include "hash.h"
#include "map.h"
#include "libcyr_cfg.h"
#include "cyr_lock.h"
#include "util.h"
#include "xmalloc.h"
#include "xstrlcpy.h"
#include "xstrlcat.h"
#include "strarray.h"
#define FNAME_QUOTADIR "/quota/"
#define MAX_QUOTA_PATH 4096
/* we have the file locked iff we have an outstanding transaction */
struct subtxn {
int fd;
char *fnamenew;
int fdnew;
int delete;
};
struct txn {
hash_table table; /* hash table of sub-transactions */
int (*proc)(const char *, struct subtxn *); /* commit/abort procedure */
int result; /* final result of the commit/abort */
};
struct dbengine {
char *path;
char *data; /* allocated buffer for fetched data */
struct txn txn; /* transaction associated with this db handle */
/* sorting function */
int (*compar) (const void *s1, const void *s2);
};
static int abort_txn(struct dbengine *db __attribute__((unused)), struct txn *tid);
static int compar_qr(const void *v1, const void *v2);
static int compar_qr_mbox(const void *v1, const void *v2);
/* hash the prefix - either with or without 'user.' part */
static char name_to_hashchar(const char *name, int isprefix)
{
int config_fulldirhash = libcyrus_config_getswitch(CYRUSOPT_FULLDIRHASH);
const char *idx;
if (!*name) return '\0';
/* you can't actually prefix with a fulldirhash character! (Bug 3735) */
if (config_fulldirhash && isprefix) return '\0';
idx = strchr(name, '.'); /* skip past user. */
if (idx == NULL) {
idx = name;
} else {
idx++;
}
return (char) dir_hash_c(idx, config_fulldirhash);
}
/* simple hash so it's easy to find these things in the filesystem;
our human time is worth more than efficiency */
static void hash_quota(char *buf, size_t size, const char *qr, char *path)
{
int config_virtdomains = libcyrus_config_getswitch(CYRUSOPT_VIRTDOMAINS);
int config_fulldirhash = libcyrus_config_getswitch(CYRUSOPT_FULLDIRHASH);
char c, *p;
unsigned len;
if ((len = snprintf(buf, size, "%s", path)) >= size) {
fatal("insufficient buffer size in hash_quota", EC_TEMPFAIL);
}
buf += len;
size -= len;
if (config_virtdomains && (p = strchr(qr, '!'))) {
*p = '\0'; /* split domain!qr */
c = (char) dir_hash_c(qr, config_fulldirhash);
if ((len = snprintf(buf, size, "%s%c/%s",
FNAME_DOMAINDIR, c, qr)) >= size) {
fatal("insufficient buffer size in hash_quota", EC_TEMPFAIL);
}
*p++ = '!'; /* reassemble domain!qr */
qr = p;
buf += len;
size -= len;
if (!*qr) {
/* quota for entire domain */
if (snprintf(buf, size, "%sroot", FNAME_QUOTADIR) >= (int) size) {
fatal("insufficient buffer size in hash_quota",
EC_TEMPFAIL);
}
return;
}
}
c = name_to_hashchar(qr, 0);
if (snprintf(buf, size, "%s%c/%s", FNAME_QUOTADIR, c, qr) >= (int) size) {
fatal("insufficient buffer size in hash_quota", EC_TEMPFAIL);
}
}
/* other routines call this one when they fail */
static int abort_subtxn(const char *fname, struct subtxn *tid)
{
int r = CYRUSDB_OK;
assert(fname && tid);
/* cleanup done while lock is held */
if (tid->fnamenew) {
unlink(tid->fnamenew);
free(tid->fnamenew);
}
if (tid->fdnew != -1) {
r = close(tid->fdnew);
}
if (tid->fd != -1) {
/* release lock */
r = lock_unlock(tid->fd, fname);
if (r == -1) {
syslog(LOG_ERR, "IOERROR: unlocking %s: %m", fname);
r = CYRUSDB_IOERROR;
}
/* close */
r = close(tid->fd);
if (r == -1) {
syslog(LOG_ERR, "IOERROR: closing %s: %m", fname);
r = CYRUSDB_IOERROR;
}
}
free(tid);
return r;
}
static int commit_subtxn(const char *fname, struct subtxn *tid)
{
int writefd;
int r = 0;
struct stat sbuf;
assert(fname && tid);
if ((writefd = tid->fdnew) != -1) {
/* we wrote something */
if (fsync(writefd) ||
fstat(writefd, &sbuf) == -1 ||
rename(tid->fnamenew, fname) == -1 ||
lock_unlock(writefd, fname) == -1) {
syslog(LOG_ERR, "IOERROR: writing %s: %m", tid->fnamenew);
r = CYRUSDB_IOERROR;
}
close(writefd);
free(tid->fnamenew);
} else if (tid->delete) {
/* delete file */
r = unlink(fname);
if (r == -1) {
syslog(LOG_ERR, "IOERROR: unlinking %s: %m", fname);
r = CYRUSDB_IOERROR;
}
} else {
/* read-only txn */
}
/* release lock */
if (tid->fd != -1) {
r = lock_unlock(tid->fd, fname);
if (r == -1) {
syslog(LOG_ERR, "IOERROR: unlocking %s: %m", fname);
r = CYRUSDB_IOERROR;
}
r = close(tid->fd);
if (r == -1) {
syslog(LOG_ERR, "IOERROR: closing %s: %m", fname);
r = CYRUSDB_IOERROR;
}
}
free(tid);
return r;
}
static void free_db(struct dbengine *db)
{
if (db) {
if (db->path) free(db->path);
if (db->data) free(db->data);
free_hash_table(&db->txn.table, NULL);
free(db);
}
}
static struct subtxn *new_subtxn(const char *fname __attribute__((unused)),
int fd)
{
struct subtxn *ret = (struct subtxn *) xmalloc(sizeof(struct subtxn));
ret->fd = fd;
ret->fnamenew = NULL;
ret->fdnew = -1;
ret->delete = 0;
return ret;
}
static int myopen(const char *fname, int flags, struct dbengine **ret, struct txn **mytid)
{
struct dbengine *db = (struct dbengine *) xzmalloc(sizeof(struct dbengine));
struct stat sbuf;
char *p;
int r;
assert(fname && ret);
db->path = xstrdup(fname);
construct_hash_table(&db->txn.table, 200, 0);
/* strip any filename from the path */
if ((p = strrchr(db->path, '/'))) *p = '\0';
r = stat(db->path, &sbuf);
if (r == -1 && errno == ENOENT && (flags & CYRUSDB_CREATE)) {
if (cyrus_mkdir(fname, 0755) != -1) {
r = stat(db->path, &sbuf);
}
}
if (r == -1) {
int level = (flags & CYRUSDB_CREATE) ? LOG_ERR : LOG_DEBUG;
syslog(level, "IOERROR: stating quota %s: %m", db->path);
free_db(db);
return CYRUSDB_IOERROR;
}
db->compar = (flags & CYRUSDB_MBOXSORT) ? compar_qr_mbox : compar_qr;
*ret = db;
if (mytid) {
*mytid = &db->txn;
}
return 0;
}
static int myclose(struct dbengine *db)
{
assert(db);
free_db(db);
return 0;
}
static int myfetch(struct dbengine *db, char *quota_path,
const char **data, size_t *datalen,
struct txn **tid)
{
struct subtxn *mytid = NULL;
int quota_fd;
const char *quota_base = 0;
size_t quota_len = 0;
int r = 0;
assert(db);
if (data) *data = NULL;
if (datalen) *datalen = 0;
if (!data || !datalen) {
/* just check if the key exists */
struct stat sbuf;
if (stat(quota_path, &sbuf) == -1)
return CYRUSDB_NOTFOUND;
return 0;
}
if (tid) {
if (!*tid)
*tid = &db->txn;
else
mytid = (struct subtxn *) hash_lookup(quota_path, &db->txn.table);
}
/* open and lock file, if needed */
if (!mytid) {
quota_fd = open(quota_path, O_RDWR, 0);
if (quota_fd == -1) {
if (errno == ENOENT) {
/* key doesn't exist */
return CYRUSDB_NOTFOUND;
}
syslog(LOG_ERR, "IOERROR: opening quota file %s: %m", quota_path);
return CYRUSDB_IOERROR;
}
if (tid) {
int r;
struct stat sbuf;
const char *lockfailaction;
r = lock_reopen(quota_fd, quota_path, &sbuf, &lockfailaction);
if (r == -1) {
syslog(LOG_ERR, "IOERROR: %s quota %s: %m", lockfailaction,
quota_path);
xclose(quota_fd);
return CYRUSDB_IOERROR;
}
mytid = new_subtxn(quota_path, quota_fd);
hash_insert(quota_path, mytid, &db->txn.table);
}
}
else
quota_fd = mytid->fd;
free(db->data);
db->data = NULL;
map_refresh(quota_fd, 1, "a_base, "a_len,
MAP_UNKNOWN_LEN, quota_path, 0);
if (!quota_len) {
*data = db->data = xstrdup("");
*datalen = 0;
}
else if (quota_base[quota_len-1] != '\n') {
r = CYRUSDB_IOERROR;
}
else {
*data = db->data = xstrndup(quota_base, quota_len);
*datalen = quota_len - 1;
db->data[*datalen] = '\0';
}
map_free("a_base, "a_len);
if (!tid) close(quota_fd);
if (r) return r;
/* magic old-format conversion */
if (db->data[0] != '%') {
char *eol = strchr(db->data, '\n');
/* convert the separating \n to SP */
if (eol) *eol = ' ';
}
return 0;
}
static int fetch(struct dbengine *db,
const char *key, size_t keylen,
const char **data, size_t *datalen,
struct txn **tid)
{
char quota_path[MAX_QUOTA_PATH+1], *tmpkey = NULL;
/* if we need to truncate the key, do so */
if (key[keylen] != '\0') {
tmpkey = xmalloc(keylen + 1);
memcpy(tmpkey, key, keylen);
tmpkey[keylen] = '\0';
key = tmpkey;
}
hash_quota(quota_path, sizeof(quota_path), key, db->path);
if (tmpkey) free(tmpkey);
return myfetch(db, quota_path, data, datalen, tid);
}
static const char *path_to_qr(const char *path, char *buf)
{
const char *qr;
char *p;
qr = strrchr(path, '/') + 1;
if ((p = strstr(path, FNAME_DOMAINDIR))) {
/* use the quota_path as a buffer to construct virtdomain qr */
p += strlen(FNAME_DOMAINDIR) + 2; /* +2 for hashdir */
sprintf(buf, "%.*s!%s", (int) strcspn(p, "/"), p,
strcmp(qr, "root") ? qr : "");
qr = buf;
}
return qr;
}
static int compar_qr(const void *v1, const void *v2)
{
const char *qr1, *qr2;
char qrbuf1[MAX_QUOTA_PATH+1], qrbuf2[MAX_QUOTA_PATH+1];
qr1 = path_to_qr(*((const char **) v1), qrbuf1);
qr2 = path_to_qr(*((const char **) v2), qrbuf2);
return strcmp(qr1, qr2);
}
static int compar_qr_mbox(const void *v1, const void *v2)
{
const char *qr1, *qr2;
char qrbuf1[MAX_QUOTA_PATH+1], qrbuf2[MAX_QUOTA_PATH+1];
qr1 = path_to_qr(*((const char **) v1), qrbuf1);
qr2 = path_to_qr(*((const char **) v2), qrbuf2);
return bsearch_compare_mbox(qr1, qr2);
}
static void scan_qr_dir(char *quota_path, const char *prefix,
strarray_t *pathbuf)
{
int config_fulldirhash = libcyrus_config_getswitch(CYRUSOPT_FULLDIRHASH);
int config_virtdomains = libcyrus_config_getswitch(CYRUSOPT_VIRTDOMAINS);
char *endp;
char onlyc = '\0';
int c, i;
DIR *qrdir;
struct dirent *next = NULL;
/* strip off the qr specific path */
endp = strstr(quota_path, FNAME_QUOTADIR) + strlen(FNAME_QUOTADIR);
strcpy(endp, "?/");
/* check for path restriction - if there's a prefix we only
* need to scan a single directory */
onlyc = name_to_hashchar(prefix, 1);
c = config_fulldirhash ? 'A' : 'a';
for (i = 0; i < 26; i++, c++) {
if (onlyc && c != onlyc) continue;
*endp = c;
qrdir = opendir(quota_path);
if (qrdir) {
while ((next = readdir(qrdir)) != NULL) {
if (!strcmp(next->d_name, ".")
|| !strcmp(next->d_name, "..")) continue;
if (!strncmp(next->d_name, prefix, strlen(prefix)))
strarray_appendm(pathbuf,
strconcat(quota_path, next->d_name, (char *)NULL));
}
closedir(qrdir);
}
}
if (config_virtdomains && !strlen(prefix) &&
strstr(quota_path, FNAME_DOMAINDIR)) {
/* search for a domain quota */
struct stat buf;
strcpy(endp, "root");
if (!stat(quota_path, &buf))
strarray_append(pathbuf, quota_path);
}
}
static int foreach(struct dbengine *db,
const char *prefix, size_t prefixlen,
foreach_p *goodp,
foreach_cb *cb, void *rock,
struct txn **tid)
{
int r = CYRUSDB_OK;
int config_fulldirhash = libcyrus_config_getswitch(CYRUSOPT_FULLDIRHASH);
int config_virtdomains = libcyrus_config_getswitch(CYRUSOPT_VIRTDOMAINS);
char quota_path[MAX_QUOTA_PATH+1];
strarray_t pathbuf = STRARRAY_INITIALIZER;
int i;
char *tmpprefix = NULL, *p = NULL;
/* if we need to truncate the prefix, do so */
if (prefix[prefixlen] != '\0') {
tmpprefix = xmalloc(prefixlen + 1);
memcpy(tmpprefix, prefix, prefixlen);
tmpprefix[prefixlen] = '\0';
prefix = tmpprefix;
}
hash_quota(quota_path, sizeof(quota_path), prefix, db->path);
if (config_virtdomains && (p = strchr(prefix, '!')))
prefix = p + 1;
/* search for the quotaroots */
scan_qr_dir(quota_path, prefix, &pathbuf);
if (config_virtdomains && !prefixlen) {
/* search for all virtdomain quotaroots */
char *endp;
int c, i, n;
DIR *qrdir;
struct dirent *next = NULL;
n = snprintf(quota_path, sizeof(quota_path)-3, "%s%s",
db->path, FNAME_DOMAINDIR);
endp = quota_path + n;
c = config_fulldirhash ? 'A' : 'a';
for (i = 0; i < 26; i++, c++) {
endp[0] = c;
endp[1] = '/';
endp[2] = '\0';
qrdir = opendir(quota_path);
if (qrdir) {
while ((next = readdir(qrdir)) != NULL) {
if (!strcmp(next->d_name, ".")
|| !strcmp(next->d_name, "..")) continue;
snprintf(endp+2, sizeof(quota_path) - (n+2),
"%s%s", next->d_name, FNAME_QUOTADIR);
scan_qr_dir(quota_path, "", &pathbuf);
}
closedir(qrdir);
}
}
}
if (tmpprefix) free(tmpprefix);
if (tid && !*tid) *tid = &db->txn;
/* sort the quotaroots (ignoring paths) */
if (pathbuf.data)
qsort(pathbuf.data, pathbuf.count, sizeof(char *), db->compar);
for (i = 0; i < pathbuf.count; i++) {
const char *data, *key;
size_t keylen, datalen;
r = myfetch(db, pathbuf.data[i], &data, &datalen, tid);
if (r) break;
key = path_to_qr(pathbuf.data[i], quota_path);
keylen = strlen(key);
if (!goodp || goodp(rock, key, keylen, data, datalen)) {
/* make callback */
r = cb(rock, key, keylen, data, datalen);
if (r) break;
}
}
strarray_fini(&pathbuf);
return r;
}
static int mystore(struct dbengine *db,
const char *key, size_t keylen,
const char *data, size_t datalen,
struct txn **tid, int overwrite)
{
char quota_path[MAX_QUOTA_PATH+1], *tmpkey = NULL;
struct subtxn *mytid = NULL;
int r = 0;
/* if we need to truncate the key, do so */
tmpkey = xmalloc(keylen + 1);
memcpy(tmpkey, key, keylen);
tmpkey[keylen] = '\0';
hash_quota(quota_path, sizeof(quota_path), tmpkey, db->path);
if (tmpkey) free(tmpkey);
if (tid) {
if (!*tid)
*tid = &db->txn;
else
mytid = (struct subtxn *) hash_lookup(quota_path, &db->txn.table);
}
/* open and lock file, if needed */
if (!mytid) {
int fd;
struct stat sbuf;
const char *lockfailaction;
fd = open(quota_path, O_RDWR, 0644);
if (fd == -1 && errno == ENOENT && data) {
if (cyrus_mkdir(quota_path, 0755) != -1) {
fd = open(quota_path, O_RDWR | O_CREAT, 0644);
}
}
if (fd == -1 && (errno != ENOENT || data)) {
syslog(LOG_ERR, "IOERROR: opening quota file %s: %m", quota_path);
return CYRUSDB_IOERROR;
}
if (fd != -1) {
r = lock_reopen(fd, quota_path, &sbuf, &lockfailaction);
if (r == -1) {
syslog(LOG_ERR, "IOERROR: %s quota %s: %m", lockfailaction,
quota_path);
xclose(fd);
return CYRUSDB_IOERROR;
}
}
mytid = new_subtxn(quota_path, fd);
if (tid) hash_insert(quota_path, mytid, &db->txn.table);
}
if (!data) {
mytid->delete = 1;
}
else {
char new_quota_path[MAX_QUOTA_PATH+1], *buf, *p;
int newfd = -1, r1 = 0;
ssize_t n;
if (mytid->fd != -1 && !overwrite) {
if (tid)
abort_txn(db, *tid);
else
abort_subtxn(quota_path, mytid);
return CYRUSDB_EXISTS;
}
if (mytid->fdnew == -1) {
strlcpy(new_quota_path, quota_path, sizeof(new_quota_path));
strlcat(new_quota_path, ".NEW", sizeof(new_quota_path));
unlink(new_quota_path);
newfd = open(new_quota_path, O_CREAT | O_TRUNC | O_RDWR, 0666);
if (newfd == -1 && errno == ENOENT) {
if (cyrus_mkdir(new_quota_path, 0755) != -1)
newfd = open(new_quota_path, O_CREAT | O_TRUNC | O_RDWR, 0666);
}
if (newfd == -1) {
syslog(LOG_ERR, "IOERROR: creating quota file %s: %m",
new_quota_path);
if (tid)
abort_txn(db, *tid);
else
abort_subtxn(quota_path, mytid);
return CYRUSDB_IOERROR;
}
mytid->fdnew = newfd;
r = lock_blocking(newfd, new_quota_path);
if (r) {
syslog(LOG_ERR, "IOERROR: locking quota file %s: %m",
new_quota_path);
if (tid)
abort_txn(db, *tid);
else
abort_subtxn(quota_path, mytid);
return CYRUSDB_IOERROR;
}
}
buf = xmalloc(datalen+1);
memcpy(buf, data, datalen);
if (buf[0] != '%') {
/* convert separating SP to \n */
p = memchr(buf, ' ', datalen);
if (p) *p = '\n';
}
/* add a terminating \n */
buf[datalen] = '\n';
lseek(mytid->fdnew, 0, SEEK_SET);
n = write(mytid->fdnew, buf, datalen+1);
if (n == (ssize_t)datalen+1) r1 = ftruncate(mytid->fdnew, datalen+1);
free(buf);
if (n != (ssize_t)datalen+1 || r1 == -1) {
if (n == -1 || r1 == -1)
syslog(LOG_ERR, "IOERROR: writing quota file %s: %m",
new_quota_path);
else
syslog(LOG_ERR,
"IOERROR: writing quota file %s: failed to write %d bytes",
new_quota_path, (int)datalen+1);
if (tid)
abort_txn(db, *tid);
else
abort_subtxn(quota_path, mytid);
return CYRUSDB_IOERROR;
}
if (!mytid->fnamenew)
mytid->fnamenew = xstrdup(new_quota_path);
}
if (!tid) {
/* commit immediately */
r = commit_subtxn(quota_path, mytid);
}
return r;
}
static int create(struct dbengine *db,
const char *key, size_t keylen,
const char *data, size_t datalen,
struct txn **tid)
{
return mystore(db, key, keylen, data, datalen, tid, 0);
}
static int store(struct dbengine *db,
const char *key, size_t keylen,
const char *data, size_t datalen,
struct txn **tid)
{
return mystore(db, key, keylen, data, datalen, tid, 1);
}
static int delete(struct dbengine *db,
const char *key, size_t keylen,
struct txn **mytid, int force __attribute__((unused)))
{
return mystore(db, key, keylen, NULL, 0, mytid, 1);
}
static void txn_proc(const char *fname, void *data, void *rock)
{
struct txn *tid = (struct txn *) rock;
int r;
r = tid->proc(fname, (struct subtxn *) data);
hash_del(fname, &tid->table);
if (r && !tid->result) tid->result = r;
}
static int commit_txn(struct dbengine *db __attribute__((unused)), struct txn *tid)
{
tid->proc = commit_subtxn;
tid->result = 0;
hash_enumerate(&tid->table, txn_proc, tid);
return tid->result;
}
static int abort_txn(struct dbengine *db __attribute__((unused)), struct txn *tid)
{
tid->proc = abort_subtxn;
tid->result = 0;
hash_enumerate(&tid->table, txn_proc, tid);
return tid->result;
}
/* quotalegacy gets compar set at startup, but it's not the same */
static int mycompar(struct dbengine *db, const char *a, int alen,
const char*b, int blen)
{
if (db->compar == compar_qr_mbox)
return bsearch_ncompare_mbox(a, alen, b, blen);
else
return bsearch_ncompare_raw(a, alen, b, blen);
}
HIDDEN struct cyrusdb_backend cyrusdb_quotalegacy =
{
"quotalegacy", /* name */
&cyrusdb_generic_init,
&cyrusdb_generic_done,
&cyrusdb_generic_sync,
&cyrusdb_generic_noarchive,
&cyrusdb_generic_unlink,
&myopen,
&myclose,
&fetch,
&fetch,
NULL,
&foreach,
&create,
&store,
&delete,
&commit_txn,
&abort_txn,
NULL,
NULL,
NULL,
&mycompar
};