/* $Cambridge: hermes/src/prayer/accountd/string.c,v 1.4 2004/04/21 13:15:51 dpc22 Exp $ */
/************************************************
 *    Prayer - a Webmail Interface              *
 ************************************************/

/* Copyright (c) University of Cambridge 2000 - 2002 */
/* See the file NOTICE for conditions of use and distribution. */

#include "accountd.h"

/* Some simple string (i.e: char *) manipulation routines */

/* string_itoa() ********************************************************
 *
 * Convert number into string using named pool as target
 *   pool: scratch pool
 *  value: value to convert
 *
 * Returns: text representation of number
 ***********************************************************************/

char *string_itoa(struct pool *pool, unsigned long value)
{
    unsigned long tmp, weight, nodigits;
    char *result, *s;

    /* All numbers contain at least one digit.
     * Find weight of most significant digit. */
    weight = 1;
    nodigits = 1;
    tmp = value / 10;
    while (tmp > 0) {
        weight *= 10;
        tmp /= 10;
        nodigits++;
    }

    result = pool_alloc(pool, nodigits + 1);

    for (s = result; weight > 0; weight /= 10) {
        if (value >= weight) {  /* Strictly speaking redundant... */
            *s++ = '0' + (value / weight);      /* Digit other than zero */
            value -= weight * (value / weight); /* Calculate remainder */
        } else
            *s++ = '0';
    }
    *s = '\0';

    return (result);
}

/* string_itoa_tmp() ****************************************************
 *
 * Convert number into string using (static) scratch buffer
 *  value: value to convert
 *
 * Returns: text representation of number.
 ***********************************************************************/

char *string_itoa_tmp(unsigned long value)
{
    static char result[64];     /* Larger than 64-bit int */
    char *s;
    unsigned long weight, tmp;

    /* All numbers contain at least one digit.
     * Find weight of most significant digit. */

    weight = 1;
    tmp = value / 10;
    while (tmp > 0) {
        weight *= 10;
        tmp /= 10;
    }

    for (s = result; weight > 0; weight /= 10) {
        if (value >= weight) {  /* Strictly speaking redundant... */
            *s++ = '0' + (value / weight);      /* Digit other than zero */
            value -= weight * (value / weight); /* Calculate remainder */
        } else
            *s++ = '0';
    }
    *s = '\0';

    return (result);
}

/* string_isnumber() *****************************************************
 *
 * Check that string is non-negative integer
 *************************************************************************/

BOOL
string_isnumber(char *s)
{
    if (!Uisdigit(*s))
        return(NIL);

    while (*s) {
        if (!Uisdigit(*s))
            return(NIL);
        s++;
    }
    return(T);
}

/* ====================================================================== */

/* ishex() **************************************************************
 *
 * Check for valid hexidecimal digit
 *  value: Character to check.
 *
 * Returns: T if character was valid hex digit.
 ***********************************************************************/

BOOL ishex(char value)
{
    if (Uisdigit(value))
        return (T);

    if ((value >= 'A') && (value <= 'F'))
        return (T);

    if ((value >= 'a') && (value <= 'f'))
        return (T);

    return (NIL);
}

/* hex() ****************************************************************
 *
 * Convert single hex digit into number.
 *  value: Hex digit to convert
 *
 * Returns: Number between 0 and 15
 ***********************************************************************/

unsigned long hex(char value)
{
    if (Uisdigit(value))
        return (value - '0');

    if ((value >= 'A') && (value <= 'F'))
        return (10 + value - 'A');

    if ((value >= 'a') && (value <= 'f'))
        return (10 + value - 'a');

    log_fatal("hex() called with illegal value");
    return (0);                 /* NOTREACHED */
}

/* ====================================================================== */

/* RFC822 defines:
 *   specials    =  "(" / ")" / "<" / ">" / "@"  ; Must be in quoted-
 *                /  "," / ";" / ":" / "\" / <">  ;  string, to use
 *                /  "." / "[" / "]"              ;  within a word.
 */

/* string_atom_has_special() ********************************************
 *
 * Check if string contains significant RFC822 characters
 *   s: String to check
 *
 * Returns: T if strig contained significant characters.
 ***********************************************************************/

BOOL string_atom_has_special(char *s)
{
    char c;

    while ((c = *s++)) {
        switch (c) {
        case '(':
        case ')':
        case '<':
        case '>':
        case '@':
        case ',':
        case ';':
        case ':':
        case '\\':
        case '"':              /* NB: must use \" quoting if '"' used */
        case '.':
        case '[':
        case ']':
            return (T);
        }
    }
    return (NIL);
}

/* string_atom_quote() **************************************************
 *
 * Convert string to RFC822 quoted form.
 *  pool: Target pool.
 *     s: String to convert
 *
 * Returns: Quoted string
 ***********************************************************************/

char *string_atom_quote(struct pool *pool, char *s)
{
    char *result, *t;
    unsigned long count, len;

    /* Scan string for '"' chars, calculate length at the same time */
    count = 0;
    len = 0;
    for (t = s; *t; t++) {
        if (*t == '"')
            count++;
        len++;
    }

    if (count == 0)
        return (s);

    result = pool_alloc(pool, count + len + 1);

    t = result;
    while (*s) {
        if (*s == '"')
            *t++ = '\\';
        *t++ = *s++;
    }
    *t = '\0';

    return (result);
}

/* ====================================================================== */

/* string_contains_dotdot() *********************************************
 *
 * Look for "/.." sequence in filenames
 *     s: String to check
 *
 * Returns: T if string contained invalid sequence
 ***********************************************************************/

BOOL string_contains_dotdot(char *s)
{
    char c;

    /* Look for .. at start of path */
    if ((s[0] == '.') && (s[1] == '.'))
        return (T);

    /* Check for /.. sequence anywhere else in decoded version */
    while ((c = *s++)) {
        if ((c == '/') && (s[0] == '.') && (s[1] == '.'))
            return (T);
    }

    return (NIL);
}

/* ====================================================================== */

/* string_prune() ********************************************************
 *
 * Generate pruned version of string if too long (replaces last three
 * characters with "...").
 *   pool: Target pool
 *      s: String to prune
 * maxlen: Maximum length of string before pruning applies.
 *
 * Returns: T if string contained invalid sequence
 ***********************************************************************/

char *string_prune(struct pool *pool, char *s, unsigned long maxlen)
{
    char *result;

    if (maxlen < (strlen("...") + 1))
        return (s);

    if (maxlen == 0)
        return (s);

    if (strlen(s) < maxlen)
        return (s);

    /* Make temporary copy of first maxlen chars of string */
    result = pool_alloc(pool, maxlen + 1);
    memcpy(result, s, maxlen);

    /* Replace last three characters with "..." */
    strcpy(result + maxlen - strlen("..."), "...");

    return (result);
}


/* ====================================================================== */

/* string_trim_whitespace() *********************************************
 *
 * Remove leading and trailing whitespace from a string
 *  string: String to prune
 *
 * Returns: Trimmed string.
 ***********************************************************************/

char *string_trim_whitespace(char *string)
{
    unsigned long len;

    /* Remove leading whitespace */
    while ((string[0] == ' ') ||
           (string[0] == '\t') ||
           (string[0] == '\015') || (string[0] == '\012'))
        string++;

    /* Remove traiing whitespace */
    len = strlen(string);
    while ((len > 0) &&
           ((string[len - 1] == ' ') ||
            (string[len - 1] == '\t') ||
            (string[len - 1] == '\015') || (string[len - 1] == '\012')))
        len--;

    /* Tie off the string */
    string[len] = '\0';

    return (string);
}

/* ====================================================================== */

/* String tokenisation routines */

/* string_isspace() *****************************************************
 *
 * Check if character is space character.
 ***********************************************************************/

BOOL string_isspace(char c)
{
    return ((c == ' ') || (c == '\t'));
}

/* string_iseol() *******************************************************
 *
 * Check if character is end of line
 ***********************************************************************/

BOOL string_iseol(char c)
{
    return ((c == '\0') || (c == '\015') || (c == '\012'));
}

/* string_next_token() **************************************************
 *
 * Find next token in string
 *    sp: Ptr to current string location (updated)
 *
 * Returns: Ptr to next token, NIL if none.
 ***********************************************************************/

char *string_next_token(char **sp)
{
    char *s = *sp;

    if (!s)
        return (NIL);

    while ((*s == ' ') || (*s == '\t')) /* Skip leading whitespace */
        s++;

    *sp = s;

    return ((*s) ? s : NIL);
}

/* string_get_token() ****************************************************
 *
 * Isolate next token in string
 *    sp: Ptr to current string location
 *        (updated to point to following token)
 *
 * Returns: Ptr to next token, NIL if none.
 ***********************************************************************/

char *string_get_token(char **sp)
{
    char *s = *sp, *result;

    if (!(s = string_next_token(sp)))
        return (NIL);

    /* Record position of this token */
    result = s;

    /* Find next whitespace character or end of string */
    while ((*s) && (*s != ' ') && (*s != '\t'))
        s++;

    /* Tie off the string unless \0 already reached */
    if (*s) {
        *s++ = '\0';

        while ((*s == ' ') || (*s == '\t'))
            s++;
    }

    /* Record position of first non-whitespace character for next caller */
    *sp = s;

    return (result);
}

/* string_skip_token() **************************************************
 *
 * Skip next token
 *    sp: Ptr to current string location
 *        (updated to point to following token)
 *
 * Returns: Ptr to next token, NIL if none.
 ***********************************************************************/

BOOL string_skip_token(char **sp)
{
    char *s;

    if (!(s = string_next_token(sp)))
        return (NIL);

    /* Find next whitespace character or end of string */
    while ((*s) && !string_isspace(*s))
        s++;

    /* Find next non-whitespace character */
    while ((*s == ' ') || (*s == '\t'))
        s++;

    /* Record position of first non-whitespace character for next caller */
    *sp = s;

    return (T);
}

/* ====================================================================== */

/* string_next_line() ***************************************************
 *
 * Skip to next line
 *    sp: Ptr to current string location
 *        (updated to point to following line)
 *
 * Returns: Ptr to next line
 ***********************************************************************/

char *string_next_line(char **sp)
{
    char *s = *sp;
    char c;

    if (!(s && s[0]))
        return (NIL);

    while ((c = *s)) {
        if (c == '\015') {      /* CR */
            s++;
            if (*s == '\012')   /* CRLF */
                s++;
            break;
        } else if (c == '\012') {       /* LF */
            s++;
            break;
        }
        s++;
    }

    *sp = s;

    return (s);
}

/* string_get_line() *****************************************************
 *
 * Get a line
 *    sp: Ptr to current string location
 *        (updated to point to following line)
 *
 * Returns: Ptr to this line
 ***********************************************************************/

char *string_get_line(char **sp)
{
    char *s, c, *result;

    result = s = *sp;           /* Record position of this line */

    if (!(s && s[0]))
        return (NIL);

    while ((c = *s)) {
        if (c == '\015') {      /* CR */
            *s++ = '\0';
            if (*s == '\012')   /* CRLF */
                s++;
            break;
        } else if (c == '\012') {       /* LF */
            *s++ = '\0';
            break;
        }
        s++;
    }

    /* Record position of next line */
    *sp = s;

    return (result);
}

/* ====================================================================== */

/* string_get_lws_line() ************************************************
 *
 * Get a linear whitespace line (e.g: folded RFC822 or HTTP header line)
 *       sp: Ptr to current string location
 *           (updated to point to following line)
 *  squeeze: Remove superfluous spaces from result.
 *
 * Returns: Ptr to this line
 ***********************************************************************/

char *string_get_lws_line(char **sp, BOOL squeeze)
{
    char *s, *t, *result;
    BOOL quoted = NIL;

    s = *sp;

    if (!(s && s[0]))
        return (NIL);

    /* Skip leading whitespace */
    while ((*s == ' ') || (*s == '\t'))
        s++;

    /* Empty string before data proper starts? */
    if ((s[0] == '\0')) {
        *sp = s;
        return (s);
    }

    /* CR, LF or CRLF before data proper starts? */
    if ((s[0] == '\015') || (s[0] == '\012')) {
        result = s;
        if ((s[0] == '\015') && (s[1] == '\012')) {     /* CRLF */
            *s = '\0';
            s += 2;
        } else
            *s++ = '\0';        /* CR or LF */

        *sp = s;
        return (result);
    }

    result = t = s;             /* Record position of non-LWS */

    while (*s) {
        if ((*s == '\015') || (*s == '\012')) { /* CR, LF or CRLF */
            s += ((s[0] == '\015') && (s[1] == '\012')) ? 2 : 1;

            if ((*s != ' ') && (*s != '\t'))
                break;
            /* Is a continuation line */
            quoted = NIL;       /* Is "LWS" allowed? */
            if ((t > result) && (t[-1] != ' ')) {       /* Replace LWS with single SP */
                *t++ = ' ';
            }
        } else {
            if (*s == '"')
                quoted = (quoted) ? NIL : T;    /* Toggle quoted bit */

            if ((!quoted) && (squeeze) && ((*s == ' ') || (*s == '\t'))) {
                if ((t > result) && (t[-1] != ' '))
                    *t++ = ' ';
                s++;
            } else if (t < s)   /* Copy faithfully */
                *t++ = *s++;
            else {
                t++;
                s++;            /* Data hasn't moved yet */
            }
        }
    }

    /* Remove trailing whitespace. Easier to do this at the end */
    if (squeeze) {
        while ((t > result) && ((t[-1] == ' ') || (t[-1] == '\t')))
            t--;
    }
    *t = '\0';                  /* Tie off result string */

    *sp = s;                    /* Set up for next caller */

    return (result);
}

/* ====================================================================== */

/* Support routines for canon encoding */

static void canon_encode_char(unsigned char c, char **tp)
{
    unsigned char *t = (unsigned char *) *tp;
    static char hex[] = "0123456789abcdef";

    if (Uisalnum(c)) {
        *t++ = c;
    } else
        switch (c) {
        case '_':
        case '.':
        case '!':
        case '\'':
        case '(':
        case ')':
        case '-':
            *t++ = c;
            break;
        default:
            *t++ = '*';
            *t++ = hex[c >> 4];
            *t++ = hex[c & 15];
            break;
        }

    *tp = (char *) t;
}

static int canon_count_specials(char *s)
{
    unsigned long special_count = 0;
    char c;

    while ((c = *s++)) {
        if (!Uisalnum(c))
            switch (c) {
            case '_':
            case '.':
            case '!':
            case '\'':
            case '(':
            case ')':
            case '-':
                break;
            default:
                special_count++;
                break;
            }
    }
    return (special_count);
}

/* ====================================================================== */

/* string_canon_encode() ************************************************
 *
 * Encode string using canonical encoding for special characters
 *   pool: Target string
 *    arg: String to encode
 *
 * Returns: Encoded string
 ***********************************************************************/

char *string_canon_encode(struct pool *pool, char *arg)
{
    char *s, *result, *t;
    unsigned long specials;

    s = pool_strdup(pool, arg);

    /* Count special characters in string */
    if ((specials = canon_count_specials(s)) == 0)
        return (s);

    /* Need another copy, with space for encoded specials */
    result = pool_alloc(pool, strlen(s) + (2 * specials) + 1);

    /* Copy s to result, encoding specials */
    t = result;
    while (*s) {
        canon_encode_char((unsigned char) (*s), &t);
        s++;
    }
    *t = '\0';

    return (result);
}

/* string_canon_path_encode() *******************************************
 *
 * Encode (directory, file) combination using canonical encoding for
 * special characters.
 *   pool: Target string
 *    dir: Directory
 *   file: Filename
 *
 * Returns: Encoded string
 ***********************************************************************/

char *string_canon_path_encode(struct pool *pool, char *dir, char *file)
{
    char *s, *result, *t;
    unsigned long specials;

    if (file[0]) {
        if (dir && dir[0]) {
            s = pool_alloc(pool, strlen(dir) + 1 + strlen(file) + 1);
            strcpy(s, dir);
            t = s + strlen(s);
            *t++ = '/';
            strcpy(t, file);
        } else
            s = pool_strdup(pool, file);
    } else
        s = pool_strdup(pool, dir);

    /* Count special characters in string */
    if ((specials = canon_count_specials(s)) == 0)
        return (s);

    /* Need another copy, with space for encoded specials */
    result = pool_alloc(pool, strlen(s) + (2 * specials) + 1);

    /* Copy s to result, encoding specials */
    t = result;
    while (*s) {
        canon_encode_char(*s, &t);
        s++;
    }
    *t = '\0';

    return (result);
}

/* ====================================================================== */

/* string_canon_decode() ************************************************
 *
 * Decode string from canonical encoding (*XY hex-encoding for specials)
 * '*' choosen as a URL safe character: hopefully no browser will try
 * and URL enode this.
 *   string: Input string
 *
 * Returns: Decoded string, updating in place.
 ***********************************************************************/

char *string_canon_decode(char *string)
{
    char *s, *t;

    if (!string)
        return (NIL);

    /* Set all pointers to start of string */
    s = t = string;

    while (*t) {
        switch (*t) {
        case '*':
            if (ishex(t[1]) && ishex(t[2])) {   /* Better way to do this? */
                *s++ = (16 * hex(t[1])) + hex(t[2]);
                t += 3;
                continue;
            }
            /* Otherwise fall through to default behaviour */
        }

        if (s < t) {
            *s++ = *t++;        /* Copy string to new location */
        } else {
            s++;
            t++;                /* Just keep going */
        }
    }
    *s = '\0';                  /* Tie off string */

    return (string);
}

/* ====================================================================== */

/* Support routines for URL encoding */

static void url_encode_char(unsigned char c, char **tp)
{
    unsigned char *t = (unsigned char *) *tp;
    static char hex[] = "0123456789abcdef";

    if (!Uisalnum(c)) {
        *t++ = '%';
        *t++ = hex[c >> 4];
        *t++ = hex[c & 15];
    } else
        *t++ = c;

    *tp = (char *) t;
}

static int url_count_specials(char *s)
{
    unsigned long special_count = 0;
    char c;

    while ((c = *s++)) {
        if (!Uisalnum(c))
            special_count++;
    }
    return (special_count);
}

/* ====================================================================== */

/* string_url_encode() **************************************************
 *
 * Encode string using canonical encoding for special characters
 *   pool: Target string
 *    arg: String to encode
 *
 * Returns: Encoded string
 ***********************************************************************/

char *string_url_encode(struct pool *pool, char *arg)
{
    char *result, *t;
    unsigned long specials;

    /* Count special characters in string */
    if ((specials = url_count_specials(arg)) == 0)
        return (arg);

    /* Need another copy, with space for encoded specials */
    result = pool_alloc(pool, strlen(arg) + (2 * specials) + 1);

    /* Copy s to result, encoding specials */
    t = result;
    while (*arg) {
        url_encode_char(*arg, &t);
        arg++;
    }
    *t = '\0';

    return (result);
}

/* ====================================================================== */

/* string_url_decode() **************************************************
 *
 * Decode string from URL encoding (%XY hex-encoding for specials)
 *   string: Input string
 *
 * Returns: Decoded string, updating in place.
 ***********************************************************************/

char *string_url_decode(char *string)
{
    char *s, *t;

    if (!string)
        return (NIL);

    /* Set all pointers to start of string */
    s = t = string;

    while (*t) {
        switch (*t) {
        case '%':
            if (ishex(t[1]) && ishex(t[2])) {   /* Better way to do this? */
                *s++ = (16 * hex(t[1])) + hex(t[2]);
                t += 3;
                continue;
            }
            /* Otherwise fall through to default behaviour */
        }

        if (s < t) {
            *s++ = *t++;        /* Copy string to new location */
        } else {
            s++;
            t++;                /* Just keep going */
        }
    }
    *s = '\0';                  /* Tie off string */

    return (string);
}


/* ====================================================================== */

/* string_url_decode_component() ****************************************
 *
 * Decode single component of string from URL encoding (%XY hex-encoding
 * for specials)
 *      tp: Ptr to string (will be updated to point to next component)
 *     sep: Separator character (typically '/')
 * Returns: Decoded string, updating in place.
 ***********************************************************************/

char *string_url_decode_component(char **tp, char sep)
{
    char *s, *t, *result;

    /* Set all pointers to start of string */
    result = s = t = *tp;

    while ((*t) && (*t != sep)) {
        switch (*t) {
        case '+':              /* Is use of '+' documented anywhere? */
            *s++ = ' ';
            t++;
            continue;
        case '%':
            if (ishex(t[1]) && ishex(t[2])) {
                *s++ = (16 * hex(t[1])) + hex(t[2]);
                t += 3;
                continue;
            }
            /* Otherwise fall through to default behaviour */
        }

        if (s < t) {
            *s++ = *t++;        /* Copy string to new location */
        } else {
            s++;
            t++;                /* Just keep going */
        }
    }

    if (*t)                     /* Nuke '&' separator if it exists */
        *t++ = '\0';

    *s = '\0';                  /* Tie off string */

    *tp = t;                    /* Record location for next caller */

    return (result);
}

/* ====================================================================== */

/* string_filename_encode() *********************************************
 *
 * Mangle filename to URL friend form by replacing " " with "_". Only
 * needed because browsers appear to ignore filename parameters.
 *
 ***********************************************************************/

char *string_filename_encode(struct pool *pool, char *arg)
{
    char *result = pool_strdup(pool, arg);
    char *s = result;

    for (s = result; *s; s++) {
        if (*s == ' ')
            *s = '_';
    }

    return (result);
}

/* ====================================================================== */

/* string_date_to_string_short() *****************************************
 *
 * Convert (month, day) tuple into printable form.
 *   month: month in range 1 to 12
 *     day: Day in range 1 to 31
 *
 * Returns: Date in printable form
 ***********************************************************************/

char *string_date_to_string_short(unsigned long month, unsigned long day)
{
    static char *date_month[12] = {
        "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep",
        "Oct", "Nov", "Dec"
    };
    static char result[64];

    if ((month < 1) || (month > 12))
        return ("");

    sprintf(result, "%s %lu", date_month[month - 1], day);

    return (result);
}

/* string_date_to_string_long() *****************************************
 *
 * Convert (month, day, year) tuple into printable form.
 *   month: month in range 1 to 12
 *     day: Day in range 1 to 31
 *    year: Four digit date.
 *
 * Returns: Date in printable form
 ***********************************************************************/

char *string_date_to_string_long(unsigned long month,
                                 unsigned long day, unsigned long year)
{
    static char *date_month[12] = {
        "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep",
        "Oct", "Nov", "Dec"
    };
    static char result[64];

    if ((month < 1) || (month > 12))
        return ("");

    sprintf(result, "%s %lu %lu", date_month[month - 1], day, year);

    return (result);
}

/* ====================================================================== */

/* Small malloc interface library */

/* string_malloc() ******************************************************
 *
 * Malloc space for string updating ptr argument
 *     ptr: Ptr to (void *) that will point to result.
 *    size: Number of bytes to allocated.
 ***********************************************************************/

void string_malloc(void **ptr, unsigned long size)
{
    if (*ptr)
        free(*ptr);

    if ((*ptr = malloc(size)))
        return;

    log_fatal("Out of memory!");
    /* NOTREACHED */
    exit(1);
}

/* string_strdup() ******************************************************
 *
 * Strdup string:
 *     ptr: Ptr to (void *) that will point to result.
 *  string: String to strdup
 ***********************************************************************/

void string_strdup(char **ptr, char *string)
{
    if (*ptr)
        free(*ptr);

    if (string == NIL) {
        *ptr = NIL;
        return;
    }

    if ((*ptr = strdup(string)))
        return;

    log_fatal("Out of memory!");
    /* NOTREACHED */
    exit(1);
}

/* string_free() ********************************************************
 *
 * Free string, clearing ptr in process.
 *     ptr: Ptr to (void *) that will be cleared after free
 ***********************************************************************/

void string_free(void **ptr)
{
    free(*ptr);
    *ptr = NIL;
}

/* ====================================================================== */

/* string_expand() *******************************************************
 *
 * Expand $name and ${name} references in string using hash for lookups
 ************************************************************************/

char *string_expand(struct pool *pool, struct assoc *h, char *s)
{
    struct buffer *b = buffer_create(pool, 48); /* Typically short */
    char *t, *value;

    while (*s) {
        if (*s == '$') {
            if (s[1] == '{') {
                /* Find end of ${name} expansion */
                if (!(t = strchr(s, '}')))
                    return (NIL);

                s += 2;
                *t++ = '\0';
            } else {
                /* Find end of $name expansion (w/s ends token) */
                t = ++s;
                while (Uisalpha(*t))
                    t++;

                if (*t)
                    *t++ = '\0';
            }

            /* Attempt to expand value. Name not found => expansion fails */
            if (!(value = assoc_lookup(h, s)))
                return (NIL);
            bputs(b, value);
            s = t;
        } else if (*s == '\\') {
            /* Quoted character */
            s++;
            if (*s == '\0')
                return (NIL);
            switch (*s) {
            case 'n':
                bputc(b, '\n');
                break;
            case 'r':
                bputc(b, '\r');
                break;
            case 't':
                bputc(b, '\t');
                break;
            default:
                bputc(b, *s);
                break;
            }
            s++;
        } else {
            /* Simple character */
            bputc(b, *s++);
        }
    }
    return (buffer_fetch(b, 0, buffer_size(b), NIL));
}


syntax highlighted by Code2HTML, v. 0.9.1