#include <sys/cdefs.h>
#ifndef __lint__
__RCSID("$NetBSD: format.c,v 1.20 2025/07/09 16:59:54 rillig Exp $");
#endif
#include <time.h>
#include <stdio.h>
#include <util.h>
#include "def.h"
#include "extern.h"
#include "format.h"
#include "glob.h"
#include "thread.h"
#undef DEBUG
#ifdef DEBUG
#define DPRINTF(a) printf a
#else
#define DPRINTF(a)
#endif
static void
check_bufsize(char **buf, size_t *bufsize, char **p, size_t cnt)
{
size_t offset = (size_t)(*p - *buf);
if (cnt < *bufsize - offset)
return;
while (cnt >= *bufsize - offset) {
if (*bufsize > SIZE_MAX/2)
errx(1, "out of memory");
*bufsize *= 2;
}
*buf = erealloc(*buf, *bufsize);
*p = *buf + offset;
}
static const char *
sfmtoff(const char **fmtbeg, const char *fmtch, off_t off)
{
char *newfmt;
size_t len;
char *p;
len = fmtch - *fmtbeg + sizeof(PRId64);
newfmt = salloc(len);
(void)strlcpy(newfmt, *fmtbeg, len - sizeof(PRId64) + 1);
(void)strlcat(newfmt, PRId64, len);
*fmtbeg = fmtch + 1;
(void)sasprintf(&p, newfmt, off);
return p;
}
static const char *
sfmtint(const char **fmtbeg, const char *fmtch, int num)
{
char *newfmt;
size_t len;
char *p;
len = fmtch - *fmtbeg + 2;
newfmt = salloc(len);
(void)strlcpy(newfmt, *fmtbeg, len);
newfmt[len-2] = 'd';
*fmtbeg = fmtch + 1;
(void)sasprintf(&p, newfmt, num);
return p;
}
static const char *
sfmtstr(const char **fmtbeg, const char *fmtch, const char *str)
{
char *newfmt;
size_t len;
char *p;
len = fmtch - *fmtbeg + 2;
newfmt = salloc(len);
(void)strlcpy(newfmt, *fmtbeg, len);
newfmt[len-2] = 's';
*fmtbeg = fmtch + 1;
(void)sasprintf(&p, newfmt, str ? str : "");
return p;
}
#ifdef THREAD_SUPPORT
static char*
sfmtdepth(char *str, int depth)
{
char *p;
if (*str == '\0') {
(void)sasprintf(&p, "%d", depth);
return p;
}
p = __UNCONST("");
for (; depth > 0; depth--)
(void)sasprintf(&p, "%s%s", p, str);
return p;
}
#endif
static const char *
sfmtfield(const char **fmtbeg, const char *fmtch, struct message *mp)
{
const char *q;
q = strchr(fmtch + 1, '?');
if (q) {
size_t len;
char *p;
const char *str;
int skin_it;
#ifdef THREAD_SUPPORT
int depth;
#endif
if (mp == NULL) {
*fmtbeg = q + 1;
return NULL;
}
#ifdef THREAD_SUPPORT
depth = mp->m_depth;
#endif
skin_it = 0;
switch (fmtch[1]) {
#ifdef THREAD_SUPPORT
case '&':
depth -= thread_depth();
case '*':
len = q - fmtch - 1;
p = salloc(len);
(void)strlcpy(p, fmtch + 2, len);
p = sfmtdepth(p, depth);
break;
#endif
case '-':
skin_it = 1;
default:
len = q - fmtch - skin_it;
p = salloc(len);
(void)strlcpy(p, fmtch + skin_it + 1, len);
p = hfield(p, mp);
if (skin_it)
p = skin(p);
break;
}
str = sfmtstr(fmtbeg, fmtch, p);
*fmtbeg = q + 1;
return str;
}
return NULL;
}
struct flags_s {
int f_and;
int f_or;
int f_new;
int f_unread;
};
static void
get_and_or_flags(struct message *mp, struct flags_s *flags)
{
for (; mp; mp = mp->m_flink) {
flags->f_and &= mp->m_flag;
flags->f_or |= mp->m_flag;
flags->f_new |= (mp->m_flag & (MREAD|MNEW)) == MNEW;
flags->f_unread |= (mp->m_flag & (MREAD|MNEW)) == 0;
get_and_or_flags(mp->m_clink, flags);
}
}
static const char *
sfmtflag(const char **fmtbeg, const char *fmtch, struct message *mp)
{
char disp[2];
struct flags_s flags;
int is_thread;
if (mp == NULL)
return NULL;
is_thread = mp->m_clink != NULL;
disp[0] = is_thread ? '+' : ' ';
disp[1] = '\0';
flags.f_and = mp->m_flag;
flags.f_or = mp->m_flag;
flags.f_new = 0;
flags.f_unread = 0;
#ifdef THREAD_SUPPORT
if (thread_hidden())
get_and_or_flags(mp->m_clink, &flags);
#endif
if (flags.f_or & MTAGGED)
disp[0] = 't';
if (flags.f_and & MTAGGED)
disp[0] = 'T';
if (flags.f_or & MMODIFY)
disp[0] = 'e';
if (flags.f_and & MMODIFY)
disp[0] = 'E';
if (flags.f_or & MSAVED)
disp[0] = '&';
if (flags.f_and & MSAVED)
disp[0] = '*';
if (flags.f_or & MPRESERVE)
disp[0] = 'p';
if (flags.f_and & MPRESERVE)
disp[0] = 'P';
if (flags.f_unread)
disp[0] = 'u';
if ((flags.f_or & (MREAD|MNEW)) == 0)
disp[0] = 'U';
if (flags.f_new)
disp[0] = 'n';
if ((flags.f_and & (MREAD|MNEW)) == MNEW)
disp[0] = 'N';
if (flags.f_or & MBOX)
disp[0] = 'm';
if (flags.f_and & MBOX)
disp[0] = 'M';
return sfmtstr(fmtbeg, fmtch, disp);
}
static const char *
login_name(const char *addr)
{
const char *p;
p = strchr(addr, '@');
if (p) {
char *q;
size_t len;
len = p - addr + 1;
q = salloc(len);
(void)strlcpy(q, addr, len);
return q;
}
return addr;
}
static const char *
subformat(const char **src, struct message *mp, const char *addr,
const char *user, const char *subj, int tm_isdst)
{
#define MP(a) mp ? a : (*src = (p + 1), NULL)
const char *p;
p = *src;
if (p[1] == '%') {
*src += 2;
return "%%";
}
for (p = *src; *p && !isalpha((unsigned char)*p) && *p != '?'; p++)
continue;
switch (*p) {
case '?':
return sfmtfield(src, p, mp);
case 'J':
return MP(sfmtint(src, p, (int)(mp->m_lines - mp->m_blines)));
case 'K':
return MP(sfmtint(src, p, (int)mp->m_blines));
case 'L':
return MP(sfmtint(src, p, (int)mp->m_lines));
case 'N':
return sfmtstr(src, p, user);
case 'O':
return MP(sfmtoff(src, p, mp->m_size));
case 'P':
return MP(sfmtstr(src, p, mp == dot ? ">" : " "));
case 'Q':
return MP(sfmtflag(src, p, mp));
case 'f':
return sfmtstr(src, p, addr);
case 'i':
return sfmtint(src, p, get_msgnum(mp));
case 'n':
return sfmtstr(src, p, login_name(addr));
case 'q':
return sfmtstr(src, p, subj);
case 't':
return sfmtint(src, p, get_msgCount());
case 'Z':
if (tm_isdst < 0) {
*src = p + 1;
return "???";
}
return NULL;
case 'z':
if (tm_isdst < 0) {
*src = p + 1;
return "-0000";
}
return NULL;
default:
return NULL;
}
#undef MP
}
static const char *
snarf_comment(char **buf, char *bufend, const char *string)
{
const char *p;
char *q;
char *qend;
int clevel;
q = buf ? *buf : NULL;
qend = buf ? bufend : NULL;
clevel = 1;
for (p = string + 1; *p != '\0'; p++) {
DPRINTF(("snarf_comment: %s\n", p));
if (*p == '(') {
clevel++;
continue;
}
if (*p == ')') {
if (--clevel == 0)
break;
continue;
}
if (*p == '\\' && p[1] != 0)
p++;
if (q < qend)
*q++ = *p;
}
if (buf) {
*q = '\0';
DPRINTF(("snarf_comment: terminating: %s\n", *buf));
*buf = q;
}
if (*p == '\0')
p--;
return p;
}
static const char *
snarf_quote(char **buf, char *bufend, const char *string)
{
const char *p;
char *q;
char *qend;
q = buf ? *buf : NULL;
qend = buf ? bufend : NULL;
for (p = string + 1; *p != '\0' && *p != '"'; p++) {
DPRINTF(("snarf_quote: %s\n", p));
if (*p == '\\' && p[1] != '\0')
p++;
if (q < qend)
*q++ = *p;
}
if (buf) {
*q = '\0';
DPRINTF(("snarf_quote: terminating: %s\n", *buf));
*buf = q;
}
if (*p == '\0')
p--;
return p;
}
static char *
get_comments(char *name)
{
char nbuf[LINESIZE];
const char *p;
char *qend;
char *q;
char *lastq;
if (name == NULL)
return NULL;
p = name;
q = nbuf;
lastq = nbuf;
qend = nbuf + sizeof(nbuf) - 1;
for (p = skip_WSP(name); *p != '\0'; p++) {
DPRINTF(("get_comments: %s\n", p));
switch (*p) {
case '"':
p = snarf_quote(NULL, NULL, p);
break;
case '(':
p = snarf_comment(&q, qend, p);
lastq = q;
if (q < qend)
*q++ = ' ';
break;
default:
break;
}
}
*lastq = '\0';
return savestr(nbuf);
}
static const char *
convert_obs_zone(const char *obs_zone)
{
static const struct obs_zone_tbl_s {
const char *zone;
const char *gmtoff;
} obs_zone_tbl[] = {
{"UT", "+0000"},
{"GMT", "+0000"},
{"EST", "-0500"},
{"EDT", "-0400"},
{"CST", "-0600"},
{"CDT", "-0500"},
{"MST", "-0700"},
{"MDT", "-0600"},
{"PST", "-0800"},
{"PDT", "-0700"},
{NULL, NULL},
};
const struct obs_zone_tbl_s *zp;
if (obs_zone[0] == '+' || obs_zone[0] == '-')
return obs_zone;
if (obs_zone[1] == 0) {
switch((unsigned char)obs_zone[0]) {
case 'A': case 'B': case 'C': case 'D': case 'E':
case 'F': case 'G': case 'H': case 'I':
case 'K': case 'L': case 'M': case 'N': case 'O':
case 'P': case 'Q': case 'R': case 'S': case 'T':
case 'U': case 'V': case 'W': case 'X': case 'Y':
case 'Z':
case 'a': case 'b': case 'c': case 'd': case 'e':
case 'f': case 'g': case 'h': case 'i':
case 'k': case 'l': case 'm': case 'n': case 'o':
case 'p': case 'q': case 'r': case 's': case 't':
case 'u': case 'v': case 'w': case 'x': case 'y':
case 'z':
return "-0000";
default:
return obs_zone;
}
}
for (zp = obs_zone_tbl; zp->zone; zp++) {
if (strcmp(obs_zone, zp->zone) == 0)
return zp->gmtoff;
}
return obs_zone;
}
static const char *
date_to_tm(char *date, struct tm *tm)
{
char *tail;
char *p;
struct tm tmp_tm;
if ((tail = strptime(date, " %a,", &tmp_tm)) == NULL)
tail = date;
else
tm->tm_wday = tmp_tm.tm_wday;
if ((tail = strptime(tail, " %d %b", &tmp_tm)) == NULL)
return NULL;
tm->tm_mday = tmp_tm.tm_mday;
tm->tm_mon = tmp_tm.tm_mon;
if ((p = strptime(tail, " %y", &tmp_tm)) != NULL && is_WSP(*p))
tail = p;
else if ((tail = strptime(tail, " %Y", &tmp_tm)) == NULL)
return NULL;
tm->tm_year = tmp_tm.tm_year;
if ((tail = strptime(tail, " %H:%M", &tmp_tm)) == NULL)
return NULL;
tm->tm_hour = tmp_tm.tm_hour;
tm->tm_min = tmp_tm.tm_min;
if ((p = strptime(tail, ":%S", &tmp_tm)) != NULL) {
tail = p;
tm->tm_sec = tmp_tm.tm_sec;
}
tail = skip_WSP(tail);
if ((p = strchr(tail, '(')) != NULL)
tm->tm_zone = get_comments(p);
else
tm->tm_zone = NULL;
tail = skin(tail);
return convert_obs_zone(tail);
}
static char *
hl_date_to_tm(const char *buf, struct tm *tm)
{
char *tail;
char *p;
char zone[4];
struct tm tmp_tm;
int len;
zone[0] = '\0';
if ((tail = strptime(buf, " %a %b %d %H:%M", &tmp_tm)) == NULL)
return NULL;
tm->tm_wday = tmp_tm.tm_wday;
tm->tm_mday = tmp_tm.tm_mday;
tm->tm_mon = tmp_tm.tm_mon;
tm->tm_hour = tmp_tm.tm_hour;
tm->tm_min = tmp_tm.tm_min;
if ((p = strptime(tail, ":%S", &tmp_tm)) != NULL) {
tail = p;
tm->tm_sec = tmp_tm.tm_sec;
}
if (sscanf(tail, " %3[A-Z] %n", zone, &len) == 1) {
if (zone[0])
tm->tm_zone = savestr(zone);
tail += len;
}
tail = strptime(tail, " %Y ", &tmp_tm);
tm->tm_year = tmp_tm.tm_year;
return tail;
}
PUBLIC void
dateof(struct tm *tm, struct message *mp, int use_hl_date)
{
static int tzinit = 0;
char *date = NULL;
const char *gmtoff;
(void)memset(tm, 0, sizeof(*tm));
if (!tzinit) {
tzinit = 1;
tzset();
}
if (mp == NULL) {
time_t now;
(void)time(&now);
(void)localtime_r(&now, tm);
return;
}
if (use_hl_date == 0 &&
(date = hfield("date", mp)) != NULL &&
(gmtoff = date_to_tm(date, tm)) != NULL) {
int hour;
int min;
char sign[2];
struct tm save_tm;
tm->tm_isdst = -1;
save_tm = *tm;
if (strcmp(gmtoff, "-0000") != 0 &&
sscanf(gmtoff, " %1[+-]%2d%2d ", sign, &hour, &min) == 3) {
time_t otime;
if (sign[0] == '-') {
tm->tm_hour += hour;
tm->tm_min += min;
}
else {
tm->tm_hour -= hour;
tm->tm_min -= min;
}
if ((otime = timegm(tm)) == (time_t)-1 ||
localtime_r(&otime, tm) == NULL) {
if (debug)
warnx("cannot convert date: \"%s\"", date);
*tm = save_tm;
}
}
else {
*tm = save_tm;
tm->tm_gmtoff = 0;
tm->tm_zone = NULL;
}
}
else {
struct headline hl;
char headline[LINESIZE];
char pbuf[LINESIZE];
if (debug && use_hl_date == 0)
warnx("invalid date: \"%s\"", date ? date : "<null>");
tm->tm_isdst = -1;
headline[0] = '\0';
(void)readline(setinput(mp), headline, (int)sizeof(headline), 0);
parse(headline, &hl, pbuf);
if (hl.l_date == NULL)
warnx("invalid headline: `%s'", headline);
else if (hl_date_to_tm(hl.l_date, tm) == NULL ||
mktime(tm) == -1)
warnx("invalid headline date: `%s'", hl.l_date);
}
}
static const char *
addrof(struct message *mp)
{
if (mp == NULL)
return NULL;
return nameof(mp, 0);
}
static char *
get_display_name(char *name)
{
char nbuf[LINESIZE];
const char *p;
char *q;
char *qend;
char *lastq;
int quoted;
if (name == NULL)
return NULL;
q = nbuf;
lastq = nbuf;
qend = nbuf + sizeof(nbuf) - 1;
quoted = 0;
for (p = skip_WSP(name); *p != '\0'; p++) {
DPRINTF(("get_display_name: %s\n", p));
switch (*p) {
case '"':
q = nbuf;
p = snarf_quote(&q, qend, p);
if (!quoted)
lastq = q;
quoted = 1;
break;
case ':':
case '<':
if (lastq == nbuf)
return NULL;
*lastq = '\0';
return savestr(nbuf);
case '(':
p = snarf_comment(NULL, NULL, p);
break;
default:
if (!quoted && q < qend) {
*q++ = *p;
if (!is_WSP(*p)
)
lastq = q;
}
break;
}
}
return NULL;
}
static const char *
userof(struct message *mp)
{
char *sender;
char *dispname;
if (mp == NULL)
return NULL;
if ((sender = hfield("from", mp)) != NULL ||
(sender = hfield("sender", mp)) != NULL)
if ((dispname = get_display_name(sender)) != NULL ||
(dispname = get_comments(sender)) != NULL)
return dispname;
return NULL;
}
static const char *
subjof(struct message *mp)
{
const char *subj;
if (mp == NULL)
return NULL;
if ((subj = hfield("subject", mp)) == NULL)
subj = hfield("subj", mp);
return subj;
}
static const char*
protect(const char *str)
{
char *p, *q;
size_t size;
if (str == NULL || (size = strlen(str)) == 0)
return str;
p = salloc(2 * size + 1);
for (q = p; *str; str++) {
*q = *str;
if (*q++ == '%')
*q++ = '%';
}
*q = '\0';
return p;
}
static char *
preformat(struct tm *tm, const char *oldfmt, struct message *mp, int use_hl_date)
{
const char *subj;
const char *addr;
const char *user;
const char *p;
char *q;
char *newfmt;
size_t fmtsize;
if (mp != NULL && (mp->m_flag & MDELETED) != 0)
mp = NULL;
subj = protect(subjof(mp));
addr = protect(addrof(mp));
user = protect(userof(mp));
dateof(tm, mp, use_hl_date);
fmtsize = LINESIZE;
newfmt = ecalloc(1, fmtsize);
q = newfmt;
p = oldfmt;
while (*p) {
if (*p == '%') {
const char *fp;
fp = subformat(&p, mp, addr, user, subj, tm->tm_isdst);
if (fp) {
size_t len;
len = strlen(fp);
check_bufsize(&newfmt, &fmtsize, &q, len);
(void)strcpy(q, fp);
q += len;
continue;
}
}
check_bufsize(&newfmt, &fmtsize, &q, 1);
*q++ = *p++;
}
*q = '\0';
return newfmt;
}
#define USE_HL_DATE "%??"
PUBLIC char *
smsgprintf(const char *fmtstr, struct message *mp)
{
struct tm tm;
int use_hl_date;
char *newfmt;
char *buf;
size_t bufsize;
if (strncmp(fmtstr, USE_HL_DATE, sizeof(USE_HL_DATE) - 1) != 0)
use_hl_date = 0;
else {
use_hl_date = 1;
fmtstr += sizeof(USE_HL_DATE) - 1;
}
bufsize = LINESIZE;
buf = salloc(bufsize);
newfmt = preformat(&tm, fmtstr, mp, use_hl_date);
(void)strftime(buf, bufsize, newfmt, &tm);
free(newfmt);
return buf;
}
PUBLIC void
fmsgprintf(FILE *fo, const char *fmtstr, struct message *mp)
{
char *buf;
buf = smsgprintf(fmtstr, mp);
(void)fprintf(fo, "%s\n", buf);
}