/* $Id: interface.c 7277 2005-06-07 04:40:16Z eagle $
**
** Storage Manager interface
*/
#include "config.h"
#include "clibrary.h"
#include <ctype.h>
#include <errno.h>
#include <syslog.h>
#include <time.h>
#include "conffile.h"
#include "inn/innconf.h"
#include "inn/wire.h"
#include "interface.h"
#include "libinn.h"
#include "methods.h"
#include "paths.h"
typedef enum {INIT_NO, INIT_DONE, INIT_FAIL} INITTYPE;
typedef struct {
INITTYPE initialized;
bool configured;
bool selfexpire;
bool expensivestat;
} METHOD_DATA;
METHOD_DATA method_data[NUM_STORAGE_METHODS];
static STORAGE_SUB *subscriptions = NULL;
static unsigned int typetoindex[256];
int SMerrno;
char *SMerrorstr = NULL;
static bool ErrorAlloc = false;
static bool Initialized = false;
bool SMopenmode = false;
bool SMpreopen = false;
/*
** Checks to see if the token is valid
*/
bool IsToken(const char *text) {
const char *p;
if (!text)
return false;
if (strlen(text) != (sizeof(TOKEN) * 2) + 2)
return false;
if (text[0] != '@')
return false;
if (text[(sizeof(TOKEN) * 2) + 1] != '@')
return false;
for (p = text + 1; *p != '@'; p++)
if (!isxdigit((int)*p))
return false;
return true;
}
/*
** Converts a token to a textual representation for error messages
** and the like.
*/
char *
TokenToText(const TOKEN token)
{
static const char hex[] = "0123456789ABCDEF";
static char result[(sizeof(TOKEN) * 2) + 3];
const char *p;
char *q;
size_t i;
result[0] = '@';
for (q = result + 1, p = (const char *) &token, i = 0; i < sizeof(TOKEN);
i++, p++) {
*q++ = hex[(*p & 0xF0) >> 4];
*q++ = hex[*p & 0x0F];
}
*q++ = '@';
*q++ = '\0';
return result;
}
/*
** Converts a hex digit and converts it to a int
*/
static int hextodec(const int c) {
return isdigit(c) ? (c - '0') : ((c - 'A') + 10);
}
/*
** Converts a textual representation of a token back to a native
** representation
*/
TOKEN TextToToken(const char *text) {
const char *p;
char *q;
int i;
TOKEN token;
if (text[0] == '@')
p = &text[1];
else
p = text;
for (q = (char *)&token, i = 0; i != sizeof(TOKEN); i++) {
q[i] = (hextodec(*p) << 4) + hextodec(*(p + 1));
p += 2;
}
return token;
}
/*
** Given an article and length in non-wire format, return a malloced region
** containing the article in wire format. Set *newlen to the length of the
** new article.
*/
char *
ToWireFmt(const char *article, size_t len, size_t *newlen)
{
size_t bytes;
char *newart;
const char *p;
char *dest;
bool atstartofline=true;
/* First go thru article and count number of bytes we need. */
for (bytes = 0, p=article ; p < &article[len] ; ++p) {
if (*p == '.' && atstartofline) ++bytes; /* 1 byte for escaping . */
++bytes;
if (*p == '\n') {
++bytes; /* need another byte for CR */
atstartofline = true; /* next char starts new line */
} else {
atstartofline = false;
}
}
bytes += 3; /* for .\r\n */
newart = xmalloc(bytes + 1);
*newlen = bytes;
/* now copy the article, making changes */
atstartofline = true;
for (p=article, dest=newart ; p < &article[len] ; ++p) {
if (*p == '\n') {
*dest++ = '\r';
*dest++ = '\n';
atstartofline = true;
} else {
if (atstartofline && *p == '.') *dest++ = '.'; /* add extra . */
*dest++ = *p;
atstartofline = false;
}
}
*dest++ = '.';
*dest++ = '\r';
*dest++ = '\n';
*dest = '\0';
return newart;
}
char *
FromWireFmt(const char *article, size_t len, size_t *newlen)
{
size_t bytes;
char *newart;
const char *p;
char *dest;
bool atstartofline = true;
/* First go thru article and count number of bytes we need */
for (bytes = 0, p=article ; p < &article[len] ; ) {
/* check for terminating .\r\n and if so break */
if (p == &article[len-3] && *p == '.' && p[1] == '\r' && p[2] == '\n')
break;
/* check for .. at start-of-line */
if (atstartofline && p < &article[len-1] && *p == '.' && p[1] == '.') {
bytes++; /* only output 1 byte */
p+=2;
atstartofline = false;
} else if (p < &article[len-1] && *p == '\r' && p[1] == '\n') {
bytes++; /* \r\n counts as only one byte in output */
p += 2;
atstartofline = true;
} else {
bytes++;
p++;
atstartofline = false;
}
}
newart = xmalloc(bytes + 1);
*newlen = bytes;
for (p = article, dest = newart ; p < &article[len]; ) {
/* check for terminating .\r\n and if so break */
if (p == &article[len-3] && *p == '.' && p[1] == '\r' && p[2] == '\n')
break;
if (atstartofline && p < &article[len-1] && *p == '.' && p[1] == '.') {
*dest++ = '.';
p += 2;
atstartofline = false;
} else if (p < &article[len-1] && *p == '\r' && p[1] == '\n') {
*dest++ = '\n';
p += 2;
atstartofline = true;
} else {
*dest++ = *p++;
atstartofline = false;
}
}
*dest = '\0';
return newart;
}
/*
** get Xref header without pathhost
*/
static char *
GetXref(ARTHANDLE *art)
{
const char *p, *p1;
const char *q;
char *buff;
bool Nocr = false;
p = wire_findheader(art->data, art->len, "xref");
if (p == NULL)
return NULL;
q = p;
for (p1 = NULL; p < art->data + art->len; p++) {
if (p1 != (char *)NULL && *p1 == '\r' && *p == '\n') {
Nocr = false;
break;
}
if (*p == '\n') {
Nocr = true;
break;
}
p1 = p;
}
if (p >= art->data + art->len)
return NULL;
if (!Nocr)
p = p1;
/* skip pathhost */
for (; (*q == ' ') && (q < p); q++);
if (q == p)
return NULL;
if ((q = memchr(q, ' ', p - q)) == NULL)
return NULL;
for (q++; (*q == ' ') && (q < p); q++);
if (q == p)
return NULL;
buff = xmalloc(p - q + 1);
memcpy(buff, q, p - q);
buff[p - q] = '\0';
return buff;
}
/*
** Split newsgroup and returns artnum
** or 0 if there are no newsgroup.
*/
static ARTNUM GetGroups(char *Xref) {
char *p;
if ((p = strchr(Xref, ':')) == NULL)
return 0;
*p++ = '\0';
return ((ARTNUM)atoi(p));
}
STORAGE_SUB *SMGetConfig(STORAGETYPE type, STORAGE_SUB *sub) {
if (sub == (STORAGE_SUB *)NULL)
sub = subscriptions;
else
sub = sub->next;
for (;sub != NULL; sub = sub->next) {
if (sub->type == type) {
return sub;
}
}
return (STORAGE_SUB *)NULL;
}
static time_t ParseTime(char *tmbuf)
{
char *startnum;
time_t ret;
int tmp;
ret = 0;
startnum = tmbuf;
while (*tmbuf) {
if (!isdigit((int)*tmbuf)) {
tmp = atol(startnum);
switch (*tmbuf) {
case 'M':
ret += tmp*60*60*24*31;
break;
case 'd':
ret += tmp*60*60*24;
break;
case 'h':
ret += tmp*60*60;
break;
case 'm':
ret += tmp*60;
break;
case 's':
ret += tmp;
break;
default:
return(0);
}
startnum = tmbuf+1;
}
tmbuf++;
}
return(ret);
}
#define SMlbrace 1
#define SMrbrace 2
#define SMmethod 10
#define SMgroups 11
#define SMsize 12
#define SMclass 13
#define SMexpire 14
#define SMoptions 15
#define SMexactmatch 16
static CONFTOKEN smtoks[] = {
{ SMlbrace, "{" },
{ SMrbrace, "}" },
{ SMmethod, "method" },
{ SMgroups, "newsgroups:" },
{ SMsize, "size:" },
{ SMclass, "class:" },
{ SMexpire, "expires:" },
{ SMoptions, "options:" },
{ SMexactmatch, "exactmatch:" },
{ 0, 0 }
};
/* Open the config file and parse it, generating the policy data */
static bool
SMreadconfig(void)
{
CONFFILE *f;
CONFTOKEN *tok;
int type;
int i;
char *p;
char *q;
char *path;
char *method = NULL;
char *pattern = NULL;
size_t minsize = 0;
size_t maxsize = 0;
time_t minexpire = 0;
time_t maxexpire = 0;
int class = 0;
STORAGE_SUB *sub = NULL;
STORAGE_SUB *prev = NULL;
char *options = 0;
int inbrace;
bool exactmatch = false;
/* if innconf isn't already read in, do so. */
if (innconf == NULL) {
if (!innconf_read(NULL)) {
SMseterror(SMERR_INTERNAL, "ReadInnConf() failed");
return false;
}
}
for (i = 0; i < NUM_STORAGE_METHODS; i++) {
method_data[i].initialized = INIT_NO;
method_data[i].configured = false;
}
path = concatpath(innconf->pathetc, _PATH_STORAGECTL);
f = CONFfopen(path);
if (f == NULL) {
SMseterror(SMERR_UNDEFINED, NULL);
syslog(L_ERROR, "SM Could not open %s: %m", path);
free(path);
return false;
}
free(path);
inbrace = 0;
while ((tok = CONFgettoken(smtoks, f)) != NULL) {
if (!inbrace) {
if (tok->type != SMmethod) {
SMseterror(SMERR_CONFIG, "Expected 'method' keyword");
syslog(L_ERROR, "SM expected 'method' keyword, line %d", f->lineno);
return false;
}
if ((tok = CONFgettoken(0, f)) == NULL) {
SMseterror(SMERR_CONFIG, "Expected method name");
syslog(L_ERROR, "SM expected method name, line %d", f->lineno);
return false;
}
method = xstrdup(tok->name);
if ((tok = CONFgettoken(smtoks, f)) == NULL || tok->type != SMlbrace) {
SMseterror(SMERR_CONFIG, "Expected '{'");
syslog(L_ERROR, "SM Expected '{', line %d", f->lineno);
return false;
}
inbrace = 1;
/* initialize various params to defaults. */
minsize = 0;
maxsize = 0; /* zero means no limit */
class = 0;
pattern = NULL;
options = NULL;
minexpire = 0;
maxexpire = 0;
exactmatch = false;
} else {
type = tok->type;
if (type == SMrbrace)
inbrace = 0;
else {
if ((tok = CONFgettoken(0, f)) == NULL) {
SMseterror(SMERR_CONFIG, "Keyword with no value");
syslog(L_ERROR, "SM keyword with no value, line %d", f->lineno);
return false;
}
p = tok->name;
switch(type) {
case SMgroups:
if (pattern)
free(pattern);
pattern = xstrdup(tok->name);
break;
case SMsize:
minsize = strtoul(p, NULL, 10);
if ((p = strchr(p, ',')) != NULL) {
p++;
maxsize = strtoul(p, NULL, 10);
}
break;
case SMclass:
class = atoi(p);
if (class > NUM_STORAGE_CLASSES) {
SMseterror(SMERR_CONFIG, "Storage class too large");
warn("SM: storage class larger than %d, line %d",
NUM_STORAGE_CLASSES, f->lineno);
return false;
}
break;
case SMexpire:
q = strchr(p, ',');
if (q)
*q++ = 0;
minexpire = ParseTime(p);
if (q)
maxexpire = ParseTime(q);
break;
case SMoptions:
if (options)
free(options);
options = xstrdup(p);
break;
case SMexactmatch:
if (strcasecmp(p, "true") == 0
|| strcasecmp(p, "yes") == 0
|| strcasecmp(p, "on") == 0)
exactmatch = true;
break;
default:
SMseterror(SMERR_CONFIG, "Unknown keyword in method declaration");
syslog(L_ERROR, "SM Unknown keyword in method declaration, line %d: %s", f->lineno, tok->name);
free(method);
return false;
break;
}
}
}
if (!inbrace) {
/* just finished a declaration */
sub = xmalloc(sizeof(STORAGE_SUB));
sub->type = TOKEN_EMPTY;
for (i = 0; i < NUM_STORAGE_METHODS; i++) {
if (!strcasecmp(method, storage_methods[i].name)) {
sub->type = storage_methods[i].type;
method_data[i].configured = true;
break;
}
}
if (sub->type == TOKEN_EMPTY) {
SMseterror(SMERR_CONFIG, "Invalid storage method name");
syslog(L_ERROR, "SM no configured storage methods are named '%s'", method);
free(options);
free(sub);
return false;
}
if (!pattern) {
SMseterror(SMERR_CONFIG, "pattern not defined");
syslog(L_ERROR, "SM no pattern defined");
free(options);
free(sub);
return false;
}
sub->pattern = pattern;
sub->minsize = minsize;
sub->maxsize = maxsize;
sub->class = class;
sub->options = options;
sub->minexpire = minexpire;
sub->maxexpire = maxexpire;
sub->exactmatch = exactmatch;
free(method);
method = 0;
if (!prev)
subscriptions = sub;
if (prev)
prev->next = sub;
prev = sub;
sub->next = NULL;
}
}
CONFfclose(f);
return true;
}
/*
** setup storage api environment (open mode etc.)
*/
bool SMsetup(SMSETUP type, void *value) {
if (Initialized)
return false;
switch (type) {
case SM_RDWR:
SMopenmode = *(bool *)value;
break;
case SM_PREOPEN:
SMpreopen = *(bool *)value;
break;
default:
return false;
}
return true;
}
/*
** Calls the setup function for all of the configured methods and returns
** true if they all initialize ok, false if they don't
*/
bool SMinit(void) {
int i;
bool allok = true;
static bool once = false;
SMATTRIBUTE smattr;
if (Initialized)
return true;
Initialized = true;
if (!SMreadconfig()) {
SMshutdown();
Initialized = false;
return false;
}
for (i = 0; i < NUM_STORAGE_METHODS; i++) {
if (method_data[i].configured) {
if (method_data[i].configured && storage_methods[i].init(&smattr)) {
method_data[i].initialized = INIT_DONE;
method_data[i].selfexpire = smattr.selfexpire;
method_data[i].expensivestat = smattr.expensivestat;
} else {
method_data[i].initialized = INIT_FAIL;
method_data[i].selfexpire = false;
method_data[i].expensivestat = true;
syslog(L_ERROR, "SM storage method '%s' failed initialization", storage_methods[i].name);
allok = false;
}
}
typetoindex[storage_methods[i].type] = i;
}
if (!allok) {
SMshutdown();
Initialized = false;
SMseterror(SMERR_UNDEFINED, "one or more storage methods failed initialization");
syslog(L_ERROR, "SM one or more storage methods failed initialization");
return false;
}
if (!once && atexit(SMshutdown) < 0) {
SMshutdown();
Initialized = false;
SMseterror(SMERR_UNDEFINED, NULL);
return false;
}
once = true;
return true;
}
static bool InitMethod(STORAGETYPE method) {
SMATTRIBUTE smattr;
if (!Initialized)
if (!SMreadconfig()) {
Initialized = false;
return false;
}
Initialized = true;
if (method_data[method].initialized == INIT_DONE)
return true;
if (method_data[method].initialized == INIT_FAIL)
return false;
if (!method_data[method].configured) {
method_data[method].initialized = INIT_FAIL;
SMseterror(SMERR_UNDEFINED, "storage method is not configured.");
return false;
}
if (!storage_methods[method].init(&smattr)) {
method_data[method].initialized = INIT_FAIL;
method_data[method].selfexpire = false;
method_data[method].expensivestat = true;
SMseterror(SMERR_UNDEFINED, "Could not initialize storage method late.");
return false;
}
method_data[method].initialized = INIT_DONE;
method_data[method].selfexpire = smattr.selfexpire;
method_data[method].expensivestat = smattr.expensivestat;
return true;
}
static bool
MatchGroups(const char *g, int len, const char *pattern, bool exactmatch)
{
char *group, *groups, *q;
int i, lastwhite;
enum uwildmat matched;
bool wanted = false;
q = groups = xmalloc(len + 1);
for (lastwhite = -1, i = 0 ; i < len ; i++) {
/* trim white chars */
if (g[i] == '\r' || g[i] == '\n' || g[i] == ' ' || g[i] == '\t') {
if (lastwhite + 1 != i)
*q++ = ' ';
lastwhite = i;
} else
*q++ = g[i];
}
*q = '\0';
group = strtok(groups, " ,");
while (group != NULL) {
q = strchr(group, ':');
if (q != NULL)
*q = '\0';
matched = uwildmat_poison(group, pattern);
if (matched == UWILDMAT_POISON || (exactmatch && !matched)) {
free(groups);
return false;
}
if (matched == UWILDMAT_MATCH)
wanted = true;
group = strtok(NULL, " ,");
}
free(groups);
return wanted;
}
STORAGE_SUB *SMgetsub(const ARTHANDLE article) {
STORAGE_SUB *sub;
if (article.len == 0) {
SMseterror(SMERR_BADHANDLE, NULL);
return NULL;
}
if (article.groups == NULL)
return NULL;
for (sub = subscriptions; sub != NULL; sub = sub->next) {
if (!(method_data[typetoindex[sub->type]].initialized == INIT_FAIL) &&
(article.len >= sub->minsize) &&
(!sub->maxsize || (article.len <= sub->maxsize)) &&
(!sub->minexpire || article.expires >= sub->minexpire) &&
(!sub->maxexpire || (article.expires <= sub->maxexpire)) &&
MatchGroups(article.groups, article.groupslen, sub->pattern,
sub->exactmatch)) {
if (InitMethod(typetoindex[sub->type]))
return sub;
}
}
errno = 0;
SMseterror(SMERR_NOMATCH, "no matching entry in storage.conf");
return NULL;
}
TOKEN SMstore(const ARTHANDLE article) {
STORAGE_SUB *sub;
TOKEN result;
if (!SMopenmode) {
result.type = TOKEN_EMPTY;
SMseterror(SMERR_INTERNAL, "read only storage api");
return result;
}
result.type = TOKEN_EMPTY;
if ((sub = SMgetsub(article)) == NULL) {
return result;
}
return storage_methods[typetoindex[sub->type]].store(article, sub->class);
}
ARTHANDLE *SMretrieve(const TOKEN token, const RETRTYPE amount) {
ARTHANDLE *art;
if (method_data[typetoindex[token.type]].initialized == INIT_FAIL) {
SMseterror(SMERR_UNINIT, NULL);
return NULL;
}
if (method_data[typetoindex[token.type]].initialized == INIT_NO && !InitMethod(typetoindex[token.type])) {
syslog(L_ERROR, "SM could not find token type or method was not initialized (%d)",
token.type);
SMseterror(SMERR_UNINIT, NULL);
return NULL;
}
art = storage_methods[typetoindex[token.type]].retrieve(token, amount);
if (art)
art->nextmethod = 0;
return art;
}
ARTHANDLE *SMnext(const ARTHANDLE *article, const RETRTYPE amount) {
unsigned char i;
int start;
ARTHANDLE *newart;
if (article == NULL)
start = 0;
else
start= article->nextmethod;
if (method_data[start].initialized == INIT_FAIL) {
SMseterror(SMERR_UNINIT, NULL);
return NULL;
}
if (method_data[start].initialized == INIT_NO && method_data[start].configured
&& !InitMethod(start)) {
SMseterror(SMERR_UNINIT, NULL);
return NULL;
}
for (i = start, newart = NULL; i < NUM_STORAGE_METHODS; i++) {
if (method_data[i].configured && (newart = storage_methods[i].next(article, amount)) != (ARTHANDLE *)NULL) {
newart->nextmethod = i;
break;
} else
article = NULL;
}
return newart;
}
void SMfreearticle(ARTHANDLE *article) {
if (method_data[typetoindex[article->type]].initialized == INIT_FAIL) {
return;
}
if (method_data[typetoindex[article->type]].initialized == INIT_NO && !InitMethod(typetoindex[article->type])) {
syslog(L_ERROR, "SM can't free article with uninitialized method");
return;
}
storage_methods[typetoindex[article->type]].freearticle(article);
}
bool SMcancel(TOKEN token) {
if (!SMopenmode) {
SMseterror(SMERR_INTERNAL, "read only storage api");
return false;
}
if (method_data[typetoindex[token.type]].initialized == INIT_FAIL) {
SMseterror(SMERR_UNINIT, NULL);
return false;
}
if (method_data[typetoindex[token.type]].initialized == INIT_NO && !InitMethod(typetoindex[token.type])) {
SMseterror(SMERR_UNINIT, NULL);
syslog(L_ERROR, "SM can't cancel article with uninitialized method");
return false;
}
return storage_methods[typetoindex[token.type]].cancel(token);
}
bool SMprobe(PROBETYPE type, TOKEN *token, void *value) {
struct artngnum *ann;
ARTHANDLE *art;
switch (type) {
case SELFEXPIRE:
return (method_data[typetoindex[token->type]].selfexpire);
case SMARTNGNUM:
if (method_data[typetoindex[token->type]].initialized == INIT_FAIL) {
SMseterror(SMERR_UNINIT, NULL);
return false;
}
if (method_data[typetoindex[token->type]].initialized == INIT_NO && !InitMethod(typetoindex[token->type])) {
SMseterror(SMERR_UNINIT, NULL);
syslog(L_ERROR, "SM can't cancel article with uninitialized method");
return false;
}
if ((ann = (struct artngnum *)value) == NULL)
return false;
ann->groupname = NULL;
if (storage_methods[typetoindex[token->type]].ctl(type, token, value)) {
if (ann->artnum != 0) {
/* set by storage method */
return true;
} else {
art = storage_methods[typetoindex[token->type]].retrieve(*token, RETR_HEAD);
if (art == NULL) {
if (ann->groupname != NULL)
free(ann->groupname);
storage_methods[typetoindex[token->type]].freearticle(art);
return false;
}
if ((ann->groupname = GetXref(art)) == NULL) {
if (ann->groupname != NULL)
free(ann->groupname);
storage_methods[typetoindex[token->type]].freearticle(art);
return false;
}
storage_methods[typetoindex[token->type]].freearticle(art);
if ((ann->artnum = GetGroups(ann->groupname)) == 0) {
if (ann->groupname != NULL)
free(ann->groupname);
return false;
}
return true;
}
} else {
return false;
}
case EXPENSIVESTAT:
return (method_data[typetoindex[token->type]].expensivestat);
default:
return false;
}
}
bool SMflushcacheddata(FLUSHTYPE type) {
int i;
for (i = 0; i < NUM_STORAGE_METHODS; i++) {
if (method_data[i].initialized == INIT_DONE &&
!storage_methods[i].flushcacheddata(type))
syslog(L_ERROR, "SM can't flush cached data method '%s'", storage_methods[i].name);
}
return true;
}
void SMprintfiles(FILE *file, TOKEN token, char **xref, int ngroups) {
if (method_data[typetoindex[token.type]].initialized == INIT_FAIL)
return;
if (method_data[typetoindex[token.type]].initialized == INIT_NO
&& !InitMethod(typetoindex[token.type])) {
SMseterror(SMERR_UNINIT, NULL);
syslog(L_ERROR, "SM can't print files for article with uninitialized method");
return;
}
storage_methods[typetoindex[token.type]].printfiles(file, token, xref, ngroups);
}
void SMshutdown(void) {
int i;
STORAGE_SUB *old;
if (!Initialized)
return;
for (i = 0; i < NUM_STORAGE_METHODS; i++)
if (method_data[i].initialized == INIT_DONE) {
storage_methods[i].shutdown();
method_data[i].initialized = INIT_NO;
method_data[i].configured = false;
}
while (subscriptions) {
old = subscriptions;
subscriptions = subscriptions->next;
free(old->pattern);
free(old->options);
free(old);
}
Initialized = false;
}
void SMseterror(int errornum, char *error) {
if (ErrorAlloc)
free(SMerrorstr);
ErrorAlloc = false;
if ((errornum == SMERR_UNDEFINED) && (errno == ENOENT))
errornum = SMERR_NOENT;
SMerrno = errornum;
if (error == NULL) {
switch (SMerrno) {
case SMERR_UNDEFINED:
SMerrorstr = xstrdup(strerror(errno));
ErrorAlloc = true;
break;
case SMERR_INTERNAL:
SMerrorstr = "Internal error";
break;
case SMERR_NOENT:
SMerrorstr = "Token not found";
break;
case SMERR_TOKENSHORT:
SMerrorstr = "Configured token size too small";
break;
case SMERR_NOBODY:
SMerrorstr = "No article body found";
break;
case SMERR_UNINIT:
SMerrorstr = "Storage manager is not initialized";
break;
case SMERR_CONFIG:
SMerrorstr = "Error reading config file";
break;
case SMERR_BADHANDLE:
SMerrorstr = "Bad article handle";
break;
case SMERR_BADTOKEN:
SMerrorstr = "Bad token";
break;
case SMERR_NOMATCH:
SMerrorstr = "No matching entry in storage.conf";
break;
default:
SMerrorstr = "Undefined error";
}
} else {
SMerrorstr = xstrdup(error);
ErrorAlloc = true;
}
}
syntax highlighted by Code2HTML, v. 0.9.1