/*
  Copyright (C) 1993-2025 Free Software Foundation, Inc.

  This file is part of GNU Inetutils.

  GNU Inetutils is free software: you can redistribute it and/or modify
  it under the terms of the GNU General Public License as published by
  the Free Software Foundation, either version 3 of the License, or (at
  your option) any later version.

  GNU Inetutils is distributed in the hope that it will be useful, but
  WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
  General Public License for more details.

  You should have received a copy of the GNU General Public License
  along with this program.  If not, see `http://www.gnu.org/licenses/'. */

#include <config.h>

#define TELOPTS
#define TELCMDS
#define SLC_NAMES
#include "telnetd.h"
#include <stdarg.h>
#ifdef HAVE_TERMIO_H
# include <termio.h>
#endif
#include <time.h>

#if defined AUTHENTICATION || defined ENCRYPTION
# include <libtelnet/misc.h>
# define NET_ENCRYPT net_encrypt
#else
# define NET_ENCRYPT()
#endif

#ifdef HAVE_TERMCAP_TGETENT
# include <termcap.h>
#elif defined HAVE_CURSES_TGETENT
# include <curses.h>
# ifndef _XOPEN_CURSES
#  include <term.h>
# endif
#endif

#if defined HAVE_STREAMSPTY && defined HAVE_GETMSG	\
  && defined HAVE_STROPTS_H
# include <stropts.h>
#endif

static char netobuf[BUFSIZ + NETSLOP], *nfrontp, *nbackp;
static char *neturg;		/* one past last byte of urgent data */
#ifdef  ENCRYPTION
static char *nclearto;
#endif

static char ptyobuf[BUFSIZ + NETSLOP], *pfrontp, *pbackp;

static char netibuf[BUFSIZ], *netip;
static int ncc;

static char ptyibuf[BUFSIZ], *ptyip;
static int pcc;

extern int not42;

static int
readstream (int p, char *ibuf, int bufsize)
{
#ifndef HAVE_STREAMSPTY
  return read (p, ibuf, bufsize);
#else
  int flags = 0;
  int ret = 0;
  struct termios *tsp;
# ifdef HAVE_TERMIO_H
  struct termio *tp;
# endif
  struct iocblk *ip;
  char vstop, vstart;
  int ixon;
  int newflow;
  struct strbuf strbufc, strbufd;
  unsigned char ctlbuf[BUFSIZ];
  static int flowstate = -1;

  strbufc.maxlen = BUFSIZ;
  strbufc.buf = (char *) ctlbuf;
  strbufd.maxlen = bufsize - 1;
  strbufd.len = 0;
  strbufd.buf = ibuf + 1;
  ibuf[0] = 0;

  ret = getmsg (p, &strbufc, &strbufd, &flags);
  if (ret < 0)			/* error of some sort -- probably EAGAIN */
    return -1;

  if (strbufc.len <= 0 || ctlbuf[0] == M_DATA)
    {
      /* data message */
      if (strbufd.len > 0)	/* real data */
	return strbufd.len + 1;	/* count header char */
      else
	{
	  /* nothing there */
	  errno = EAGAIN;
	  return -1;
	}
    }

  /*
   * It's a control message.  Return 1, to look at the flag we set
   */

  switch (ctlbuf[0])
    {
    case M_FLUSH:
      if (ibuf[1] & FLUSHW)
	ibuf[0] = TIOCPKT_FLUSHWRITE;
      return 1;

    case M_IOCTL:
      ip = (struct iocblk *) (ibuf + 1);

      switch (ip->ioc_cmd)
	{
	case TCSETS:
	case TCSETSW:
	case TCSETSF:
	  tsp = (struct termios *) (ibuf + 1 + sizeof (struct iocblk));
	  vstop = tsp->c_cc[VSTOP];
	  vstart = tsp->c_cc[VSTART];
	  ixon = tsp->c_iflag & IXON;
	  break;

# ifdef HAVE_TERMIO_H
	case TCSETA:
	case TCSETAW:
	case TCSETAF:
	  tp = (struct termio *) (ibuf + 1 + sizeof (struct iocblk));
	  vstop = tp->c_cc[VSTOP];
	  vstart = tp->c_cc[VSTART];
	  ixon = tp->c_iflag & IXON;
	  break;
# endif/* HAVE_TERMIO_H */

	default:
	  errno = EAGAIN;
	  return -1;
	}

      newflow = (ixon && (vstart == 021) && (vstop == 023)) ? 1 : 0;
      if (newflow != flowstate)	/* it's a change */
	{
	  flowstate = newflow;
	  ibuf[0] = newflow ? TIOCPKT_DOSTOP : TIOCPKT_NOSTOP;
	  return 1;
	}
    }

  /* nothing worth doing anything about */
  errno = EAGAIN;
  return -1;
#endif /* HAVE_STREAMSPTY */
}

/* ************************************************************************* */
/* Net and PTY I/O functions */

void
io_setup (void)
{
  pfrontp = pbackp = ptyobuf;
  nfrontp = nbackp = netobuf;
#ifdef  ENCRYPTION
  nclearto = 0;
#endif
  netip = netibuf;
  ptyip = ptyibuf;
}

void
set_neturg (void)
{
  neturg = nfrontp - 1;
}

/* net-buffers */

void
net_output_byte (int c)
{
  *nfrontp++ = c;
}

int
net_output_data (const char *format, ...)
{
  va_list args;
  size_t remaining, ret;

  va_start (args, format);
  remaining = BUFSIZ - (nfrontp - netobuf);
  /* try a netflush() if the room is too low */
  if (strlen (format) > remaining || BUFSIZ / 4 > remaining)
    {
      netflush ();
      remaining = BUFSIZ - (nfrontp - netobuf);
    }
  ret = vsnprintf (nfrontp, remaining, format, args);
  nfrontp += ((ret < remaining - 1) ? ret : remaining - 1);
  va_end (args);
  return ret;
}

int
net_output_datalen (const void *buf, size_t l)
{
  size_t remaining;

  remaining = BUFSIZ - (nfrontp - netobuf);
  if (remaining < l)
    {
      netflush ();
      remaining = BUFSIZ - (nfrontp - netobuf);
    }
  if (remaining < l)
    return -1;
  memmove (nfrontp, buf, l);
  nfrontp += l;
  return (int) l;
}

int
net_input_level (void)
{
  return ncc;
}

int
net_output_level (void)
{
  return nfrontp - nbackp;
}

int
net_buffer_is_full (void)
{
  return (&netobuf[BUFSIZ] - nfrontp) < 2;
}

int
net_get_char (int peek)
{
  if (peek)
    return *netip;
  else if (ncc > 0)
    {
      ncc--;
      return *netip++ & 0377;
    }

  return 0;
}

int
net_read (void)
{
  ncc = read (net, netibuf, sizeof (netibuf));
  if (ncc < 0 && errno == EWOULDBLOCK)
    ncc = 0;
  else if (ncc == 0)
    {
      syslog (LOG_INFO, "telnetd:  peer died");
      cleanup (0);
      /* NOT REACHED */
    }
  else if (ncc > 0)
    {
      netip = netibuf;
      DEBUG (debug_report, 1,
	     debug_output_data ("td: netread %d chars\r\n", ncc));
      DEBUG (debug_net_data, 1, printdata ("nd", netip, ncc));
    }
  return ncc;
}

/* PTY buffer functions */

int
pty_buffer_is_full (void)
{
  return (&ptyobuf[BUFSIZ] - pfrontp) < 2;
}

void
pty_output_byte (int c)
{
  *pfrontp++ = c;
}

void
pty_output_datalen (const void *data, size_t len)
{
  if ((size_t) (&ptyobuf[BUFSIZ] - pfrontp) > len)
    ptyflush ();
  memcpy (pfrontp, data, len);
  pfrontp += len;
}

int
pty_input_level (void)
{
  return pcc;
}

int
pty_output_level (void)
{
  return pfrontp - pbackp;
}

void
ptyflush (void)
{
  int n;

  if ((n = pfrontp - pbackp) > 0)
    {
      DEBUG (debug_report, 1,
	     debug_output_data ("td: ptyflush %d chars\r\n", n));
      DEBUG (debug_pty_data, 1, printdata ("pd", pbackp, n));
      n = write (pty, pbackp, n);
    }

  if (n < 0)
    {
      if (errno == EWOULDBLOCK || errno == EINTR)
	return;
      cleanup (0);
      /* NOT REACHED */
    }

  pbackp += n;
  if (pbackp == pfrontp)
    pbackp = pfrontp = ptyobuf;
}

int
pty_get_char (int peek)
{
  if (peek)
    return *ptyip;
  else if (pcc > 0)
    {
      pcc--;
      return *ptyip++ & 0377;
    }

  return 0;
}

int
pty_input_putback (const char *str, size_t len)
{
  if (len > (size_t) (&ptyibuf[BUFSIZ] - ptyip))
    len = &ptyibuf[BUFSIZ] - ptyip;
  strncpy (ptyip, str, len);
  pcc += len;

  return 0;
}

/* pty_read()
 *
 * Read errors EWOULDBLOCK, EAGAIN, and EIO are
 * tweaked into reporting zero bytes input.
 * In particular, EIO is known to appear when
 * reading off the master side, before having
 * an active slave side.
 */
int
pty_read (void)
{
  pcc = readstream (pty, ptyibuf, BUFSIZ);
  if (pcc < 0 && (errno == EWOULDBLOCK
#ifdef	EAGAIN
		  || errno == EAGAIN
#endif
		  || errno == EIO))
    pcc = 0;
  ptyip = ptyibuf;

  DEBUG (debug_report, 1,
	 debug_output_data ("td: ptyread %d chars\r\n", pcc));
  DEBUG (debug_pty_data, 1, printdata ("pd", ptyip, pcc));
  return pcc;
}

/* ************************************************************************* */


/* io_drain ()
 *
 *
 *	A small subroutine to flush the network output buffer, get some data
 * from the network, and pass it through the telnet state machine.  We
 * also flush the pty input buffer (by dropping its data) if it becomes
 * too full.
 */

void
io_drain (void)
{
  fd_set rfds;

  DEBUG (debug_report, 1, debug_output_data ("td: ttloop\r\n"));
  if (nfrontp - nbackp > 0)
    netflush ();

  FD_ZERO (&rfds);
  FD_SET (net, &rfds);
  if (1 != select (net + 1, &rfds, NULL, NULL, NULL))
    {
      syslog (LOG_INFO, "ttloop:  select: %m\n");
      exit (EXIT_FAILURE);
    }

  ncc = read (net, netibuf, sizeof netibuf);
  if (ncc < 0)
    {
      syslog (LOG_INFO, "ttloop:  read: %m\n");
      exit (EXIT_FAILURE);
    }
  else if (ncc == 0)
    {
      syslog (LOG_INFO, "ttloop:  peer died: %m\n");
      exit (EXIT_FAILURE);
    }
  DEBUG (debug_report, 1,
	 debug_output_data ("td: ttloop read %d chars\r\n", ncc));
  netip = netibuf;
  telrcv ();			/* state machine */
  if (ncc > 0)
    {
      pfrontp = pbackp = ptyobuf;
      telrcv ();
    }
}				/* end of ttloop */

/*
 * Check a descriptor to see if out of band data exists on it.
 */
/* int	s; socket number */
int
stilloob (int s)
{
  static struct timeval timeout = { 0, 0 };
  fd_set excepts;
  int value;

  do
    {
      FD_ZERO (&excepts);
      FD_SET (s, &excepts);
      value = select (s + 1, (fd_set *) 0, (fd_set *) 0, &excepts, &timeout);
    }
  while (value == -1 && errno == EINTR);

  if (value < 0)
    fatalperror (pty, "select");

  return FD_ISSET (s, &excepts);
}

/*
 * nextitem()
 *
 *	Return the address of the next "item" in the TELNET data
 * stream.  This will be the address of the next character if
 * the current address is a user data character, or it will
 * be the address of the character following the TELNET command
 * if the current address is a TELNET IAC ("I Am a Command")
 * character.
 */
char *
nextitem (char *current, const char *endp)
{
  if (current >= endp)
    return NULL;
  if ((*current & 0xff) != IAC)
    return current + 1;
  if (current + 1 >= endp)
    return NULL;

  switch (*(current + 1) & 0xff)
    {
    case DO:
    case DONT:
    case WILL:
    case WONT:
      return current + 3 <= endp ? current + 3 : NULL;

    case SB:			/* loop forever looking for the SE */
      {
	char *look = current + 2;

	while (look < endp)
	  if ((*look++ & 0xff) == IAC && look < endp
	      && (*look++ & 0xff) == SE)
	    return look;

	return NULL;
      }
    default:
      return current + 2 <= endp ? current + 2 : NULL;
    }
}				/* end of nextitem */


/*
 * netclear()
 *
 *	We are about to do a TELNET SYNCH operation.  Clear
 * the path to the network.
 *
 *	Things are a bit tricky since we may have sent the first
 * byte or so of a previous TELNET command into the network.
 * So, we have to scan the network buffer from the beginning
 * until we are up to where we want to be.
 *
 *	A side effect of what we do, just to keep things
 * simple, is to clear the urgent data pointer.  The principal
 * caller should be setting the urgent data pointer AFTER calling
 * us in any case.
 */
#define wewant(p)					\
  ((nfrontp > p) && ((*p & 0xff) == IAC) &&		\
   (nfrontp > p + 1 && (((*(p + 1) & 0xff) != EC) &&	\
                        ((*(p + 1) & 0xff) != EL))))


void
netclear (void)
{
  char *thisitem, *next;
  char *good;

#ifdef	ENCRYPTION
  thisitem = nclearto > netobuf ? nclearto : netobuf;
#else /* ENCRYPTION */
  thisitem = netobuf;
#endif /* ENCRYPTION */

  while ((next = nextitem (thisitem, nbackp)) != NULL && next <= nbackp)
    thisitem = next;

  /* Now, thisitem is first before/at boundary. */

#ifdef	ENCRYPTION
  good = nclearto > netobuf ? nclearto : netobuf;
#else /* ENCRYPTION */
  good = netobuf;		/* where the good bytes go */
#endif /* ENCRYPTION */

  while (thisitem != NULL && nfrontp > thisitem)
    {
      if (wewant (thisitem))
	{
	  int length;

	  for (next = thisitem;
	       next != NULL && wewant (next) && nfrontp > next;
	       next = nextitem (next, nfrontp))
	    ;
	  if (next == NULL)
	    next = nfrontp;

	  length = next - thisitem;
	  memmove (good, thisitem, length);
	  good += length;
	  thisitem = next;
	}
      else
	{
	  thisitem = nextitem (thisitem, nfrontp);
	}
    }

  nbackp = netobuf;
  nfrontp = good;		/* next byte to be sent */
  neturg = 0;
}				/* end of netclear */

/*
 *  netflush
 *		Send as much data as possible to the network,
 *	handling requests for urgent data.
 */
void
netflush (void)
{
  int n;

  if ((n = nfrontp - nbackp) > 0)
    {
      NET_ENCRYPT ();

      /*
       * if no urgent data, or if the other side appears to be an
       * old 4.2 client (and thus unable to survive TCP urgent data),
       * write the entire buffer in non-OOB mode.
       */
      if (!neturg || !not42)
	n = write (net, nbackp, n);	/* normal write */
      else
	{
	  n = neturg - nbackp;
	  /*
	   * In 4.2 (and 4.3) systems, there is some question about
	   * what byte in a sendOOB operation is the "OOB" data.
	   * To make ourselves compatible, we only send ONE byte
	   * out of band, the one WE THINK should be OOB (though
	   * we really have more the TCP philosophy of urgent data
	   * rather than the Unix philosophy of OOB data).
	   */
	  if (n > 1)
	    n = send (net, nbackp, n - 1, 0);	/* send URGENT all by itself */
	  else
	    n = send (net, nbackp, n, MSG_OOB);	/* URGENT data */
	}
    }
  if (n < 0)
    {
      if (errno == EWOULDBLOCK || errno == EINTR)
	return;
      cleanup (0);
      /* NOT REACHED */
    }

  nbackp += n;
#ifdef	ENCRYPTION
  if (nbackp > nclearto)
    nclearto = 0;
#endif /* ENCRYPTION */
  if (nbackp >= neturg)
    neturg = 0;

  if (nbackp == nfrontp)
    {
      nbackp = nfrontp = netobuf;
#ifdef	ENCRYPTION
      nclearto = 0;
#endif /* ENCRYPTION */
    }
  DEBUG (debug_report, 1, debug_output_data ("td: netflush %d chars\r\n", n));

}				/* end of netflush */

/*
 * miscellaneous functions doing a variety of little jobs follow ...
 */

void
fatal (int f, char *msg)
{
  char buf[BUFSIZ];

  snprintf (buf, sizeof buf, "telnetd: %s.\r\n", msg);
#ifdef	ENCRYPTION
  if (encrypt_output)
    {
      /*
       * Better turn off encryption first....
       * Hope it flushes...
       */
      encrypt_send_end ();
      netflush ();
    }
#endif /* ENCRYPTION */
  write (f, buf, (int) strlen (buf));
  sleep (1);
   /*FIXME*/ exit (EXIT_FAILURE);
}

void
fatalperror (int f, char *msg)
{
  char buf[BUFSIZ];

  snprintf (buf, sizeof buf, "%s: %s", msg, strerror (errno));
  fatal (f, buf);
}

/* ************************************************************************* */
/* Terminal determination */

static unsigned char ttytype_sbbuf[] = {
  IAC, SB, TELOPT_TTYPE, TELQUAL_SEND, IAC, SE
};

static void
_gettermname (void)
{
  if (his_state_is_wont (TELOPT_TTYPE))
    return;
  settimer (baseline);
  net_output_datalen (ttytype_sbbuf, sizeof ttytype_sbbuf);
  ttloop (sequenceIs (ttypesubopt, baseline));
}

/*
 * Changes terminaltype.
 */
int
getterminaltype (char *uname, size_t len)
{
  int retval = -1;

  settimer (baseline);
#if defined AUTHENTICATION
  /*
   * Handle the Authentication option before we do anything else.
   * Distinguish the available modes by level:
   *
   *   off:                     Authentication is forbidden.
   *   none:                    Voluntary authentication.
   *   user, valid, other:      Mandatory authentication only.
   */
  if (auth_level < 0)
    send_wont (TELOPT_AUTHENTICATION, 1);
  else
    {
      if (auth_level > 0)
	send_do (TELOPT_AUTHENTICATION, 1);
      else
	send_will (TELOPT_AUTHENTICATION, 1);

      ttloop (his_will_wont_is_changing (TELOPT_AUTHENTICATION));

      if (his_state_is_will (TELOPT_AUTHENTICATION))
	retval = auth_wait (uname, len);
    }
#else /* !AUTHENTICATION */
  (void) uname;			/* Silence warning.  */
  (void) len;			/* Silence warning.  */
#endif

#ifdef	ENCRYPTION
  send_will (TELOPT_ENCRYPT, 1);
#endif /* ENCRYPTION */
  send_do (TELOPT_TTYPE, 1);
  send_do (TELOPT_TSPEED, 1);
  send_do (TELOPT_XDISPLOC, 1);
  send_do (TELOPT_NEW_ENVIRON, 1);
  send_do (TELOPT_OLD_ENVIRON, 1);

#ifdef ENCRYPTION
  ttloop (his_do_dont_is_changing (TELOPT_ENCRYPT)
	  || his_will_wont_is_changing (TELOPT_TTYPE)
	  || his_will_wont_is_changing (TELOPT_TSPEED)
	  || his_will_wont_is_changing (TELOPT_XDISPLOC)
	  || his_will_wont_is_changing (TELOPT_NEW_ENVIRON)
	  || his_will_wont_is_changing (TELOPT_OLD_ENVIRON));
#else
  ttloop (his_will_wont_is_changing (TELOPT_TTYPE)
	  || his_will_wont_is_changing (TELOPT_TSPEED)
	  || his_will_wont_is_changing (TELOPT_XDISPLOC)
	  || his_will_wont_is_changing (TELOPT_NEW_ENVIRON)
	  || his_will_wont_is_changing (TELOPT_OLD_ENVIRON));
#endif

#ifdef  ENCRYPTION
  if (his_state_is_will (TELOPT_ENCRYPT))
    encrypt_wait ();
#endif

  if (his_state_is_will (TELOPT_TSPEED))
    {
      static unsigned char sb[] =
	{ IAC, SB, TELOPT_TSPEED, TELQUAL_SEND, IAC, SE };

      net_output_datalen (sb, sizeof sb);
    }
  if (his_state_is_will (TELOPT_XDISPLOC))
    {
      static unsigned char sb[] =
	{ IAC, SB, TELOPT_XDISPLOC, TELQUAL_SEND, IAC, SE };

      net_output_datalen (sb, sizeof sb);
    }
  if (his_state_is_will (TELOPT_NEW_ENVIRON))
    {
      static unsigned char sb[] =
	{ IAC, SB, TELOPT_NEW_ENVIRON, TELQUAL_SEND, IAC, SE };

      net_output_datalen (sb, sizeof sb);
    }
  else if (his_state_is_will (TELOPT_OLD_ENVIRON))
    {
      static unsigned char sb[] =
	{ IAC, SB, TELOPT_OLD_ENVIRON, TELQUAL_SEND, IAC, SE };

      net_output_datalen (sb, sizeof sb);
    }
  if (his_state_is_will (TELOPT_TTYPE))
    net_output_datalen (ttytype_sbbuf, sizeof ttytype_sbbuf);

  if (his_state_is_will (TELOPT_TSPEED))
    ttloop (sequenceIs (tspeedsubopt, baseline));
  if (his_state_is_will (TELOPT_XDISPLOC))
    ttloop (sequenceIs (xdisplocsubopt, baseline));
  if (his_state_is_will (TELOPT_NEW_ENVIRON))
    ttloop (sequenceIs (environsubopt, baseline));
  if (his_state_is_will (TELOPT_OLD_ENVIRON))
    ttloop (sequenceIs (oenvironsubopt, baseline));

  if (his_state_is_will (TELOPT_TTYPE))
    {
      char *first = NULL, *last = NULL;

      ttloop (sequenceIs (ttypesubopt, baseline));
      /*
       * If the other side has already disabled the option, then
       * we have to just go with what we (might) have already gotten.
       */
      if (his_state_is_will (TELOPT_TTYPE) && !terminaltypeok (terminaltype))
	{
	  free (first);
	  first = xstrdup (terminaltype);
	  for (;;)
	    {
	      /* Save the unknown name, and request the next name. */
	      free (last);
	      last = xstrdup (terminaltype);
	      _gettermname ();
	      if (terminaltypeok (terminaltype))
		break;
	      if ((strcmp (last, terminaltype) == 0)
		  || his_state_is_wont (TELOPT_TTYPE))
		{
		  /*
		   * We've hit the end.  If this is the same as
		   * the first name, just go with it.
		   */
		  if (strcmp (first, terminaltype) == 0)
		    break;
		  /*
		   * Get the terminal name one more time, so that
		   * RFC1091 compliant telnets will cycle back to
		   * the start of the list.
		   */
		  _gettermname ();
		  if (strcmp (first, terminaltype) != 0)
		    {
		      free (terminaltype);
		      terminaltype = xstrdup (first);
		    }
		  break;
		}
	    }
	}
      free (first);
      free (last);
    }
  return retval;
}

/*
 * Exit status:
 *
 *  1   Accepted terminal type, or inconclusive,
 *  0   Explicitly unsupported type.
 */
int
terminaltypeok (char *s)
{
#ifdef HAVE_TGETENT
  char buf[2048];

  if (terminaltype == NULL)
    return 1;

  if (tgetent (buf, s) == 0)
    return 0;
#endif /* HAVE_TGETENT */

  return 1;
}


/* ************************************************************************* */
/* Debugging support */

static FILE *debug_fp = NULL;

static int
debug_open (void)
{
  int um = umask (077);
  if (!debug_fp)
    debug_fp = fopen ("/tmp/telnet.debug", "a");
  umask (um);
  return debug_fp == NULL;
}

static int
debug_close (void)
{
  if (debug_fp)
    fclose (debug_fp);
  debug_fp = NULL;

  return 0;
}

void
debug_output_datalen (const char *data, size_t len)
{
  if (debug_open ())
    return;

  fwrite (data, 1, len, debug_fp);
  debug_close ();
}

void
debug_output_data (const char *fmt, ...)
{
  va_list ap;
  if (debug_open ())
    return;
  va_start (ap, fmt);
  vfprintf (debug_fp, fmt, ap);
  va_end (ap);
  debug_close ();
}

/*
 * Print telnet options and commands in plain text, if possible.
 */
void
printoption (char *fmt, int option)
{
  if (TELOPT_OK (option))
    debug_output_data ("%s %s\r\n", fmt, TELOPT (option));
  else if (TELCMD_OK (option))
    debug_output_data ("%s %s\r\n", fmt, TELCMD (option));
  else
    debug_output_data ("%s %d\r\n", fmt, option);
}

/* char	direction; '<' or '>' */
/* unsigned char *pointer;  where suboption data sits */
/* int	length;	 length of suboption data */
void
printsub (int direction, unsigned char *pointer, int length)
{
  int i = 0;

#if defined AUTHENTICATION || defined ENCRYPTION
  char buf[512];
#endif

  /* Silence unwanted debugging to '/tmp/telnet.debug'.
   *
   * XXX: Better location?
   */
  if ((pointer[0] == TELOPT_AUTHENTICATION && debug_level[debug_auth] < 1)
      || (pointer[0] == TELOPT_ENCRYPT && debug_level[debug_encr] < 1))
    return;

  if (direction)
    {
      debug_output_data ("td: %s suboption ",
			 direction == '<' ? "recv" : "send");
      if (length >= 3)
	{
	  int j;

	  i = pointer[length - 2];
	  j = pointer[length - 1];

	  if (i != IAC || j != SE)
	    {
	      debug_output_data ("(terminated by ");
	      if (TELOPT_OK (i))
		debug_output_data ("%s ", TELOPT (i));
	      else if (TELCMD_OK (i))
		debug_output_data ("%s ", TELCMD (i));
	      else
		debug_output_data ("%d ", i);
	      if (TELOPT_OK (j))
		debug_output_data ("%s", TELOPT (j));
	      else if (TELCMD_OK (j))
		debug_output_data ("%s", TELCMD (j));
	      else
		debug_output_data ("%d", j);
	      debug_output_data (", not IAC SE!) ");
	    }
	}
      length -= 2;
    }
  if (length < 1)
    {
      debug_output_data ("(Empty suboption??\?)");
      return;
    }

  switch (pointer[0])
    {
    case TELOPT_TTYPE:
      debug_output_data ("TERMINAL-TYPE ");
      switch (pointer[1])
	{
	case TELQUAL_IS:
	  debug_output_data ("IS \"%.*s\"", length - 2, (char *) pointer + 2);
	  break;

	case TELQUAL_SEND:
	  debug_output_data ("SEND");
	  break;

	default:
	  debug_output_data ("- unknown qualifier %d (0x%x).",
			     pointer[1], pointer[1]);
	}
      break;

    case TELOPT_TSPEED:
      debug_output_data ("TERMINAL-SPEED");
      if (length < 2)
	{
	  debug_output_data (" (empty suboption??\?)");
	  break;
	}

      switch (pointer[1])
	{
	case TELQUAL_IS:
	  debug_output_data (" IS %.*s", length - 2, (char *) pointer + 2);
	  break;

	default:
	  if (pointer[1] == 1)
	    debug_output_data (" SEND");
	  else
	    debug_output_data (" %d (unknown)", pointer[1]);
	  for (i = 2; i < length; i++)
	    {
	      debug_output_data (" ?%d?", pointer[i]);
	    }
	  break;
	}
      break;

    case TELOPT_LFLOW:
      debug_output_data ("TOGGLE-FLOW-CONTROL");
      if (length < 2)
	{
	  debug_output_data (" (empty suboption??\?)");
	  break;
	}

      switch (pointer[1])
	{
	case LFLOW_OFF:
	  debug_output_data (" OFF");
	  break;

	case LFLOW_ON:
	  debug_output_data (" ON");
	  break;

	case LFLOW_RESTART_ANY:
	  debug_output_data (" RESTART-ANY");
	  break;

	case LFLOW_RESTART_XON:
	  debug_output_data (" RESTART-XON");
	  break;

	default:
	  debug_output_data (" %d (unknown)", pointer[1]);
	}

      for (i = 2; i < length; i++)
	debug_output_data (" ?%d?", pointer[i]);
      break;

    case TELOPT_NAWS:
      debug_output_data ("NAWS");
      if (length < 2)
	{
	  debug_output_data (" (empty suboption??\?)");
	  break;
	}
      if (length == 2)
	{
	  debug_output_data (" ?%d?", pointer[1]);
	  break;
	}

      debug_output_data (" %d %d (%d)",
			 pointer[1], pointer[2],
			 (int) ((((unsigned int) pointer[1]) << 8) |
				((unsigned int) pointer[2])));
      if (length == 4)
	{
	  debug_output_data (" ?%d?", pointer[3]);
	  break;
	}

      debug_output_data (" %d %d (%d)",
			 pointer[3], pointer[4],
			 (int) ((((unsigned int) pointer[3]) << 8) |
				((unsigned int) pointer[4])));
      for (i = 5; i < length; i++)
	debug_output_data (" ?%d?", pointer[i]);
      break;

    case TELOPT_LINEMODE:
      debug_output_data ("LINEMODE ");
      if (length < 2)
	{
	  debug_output_data (" (empty suboption??\?)");
	  break;
	}

      switch (pointer[1])
	{
	case WILL:
	  debug_output_data ("WILL ");
	  goto common;

	case WONT:
	  debug_output_data ("WONT ");
	  goto common;

	case DO:
	  debug_output_data ("DO ");
	  goto common;

	case DONT:
	  debug_output_data ("DONT ");

	common:
	  if (length < 3)
	    {
	      debug_output_data ("(no option??\?)");
	      break;
	    }
	  switch (pointer[2])
	    {
	    case LM_FORWARDMASK:
	      debug_output_data ("Forward Mask");
	      for (i = 3; i < length; i++)
		debug_output_data (" %x", pointer[i]);
	      break;

	    default:
	      debug_output_data ("%d (unknown)", pointer[2]);
	      for (i = 3; i < length; i++)
		debug_output_data (" %d", pointer[i]);
	      break;
	    }
	  break;

	case LM_SLC:
	  debug_output_data ("SLC");
	  for (i = 2; i < length - 2; i += 3)
	    {
	      if (SLC_NAME_OK (pointer[i + SLC_FUNC]))
		debug_output_data (" %s", SLC_NAME (pointer[i + SLC_FUNC]));
	      else
		debug_output_data (" %d", pointer[i + SLC_FUNC]);
	      switch (pointer[i + SLC_FLAGS] & SLC_LEVELBITS)
		{
		case SLC_NOSUPPORT:
		  debug_output_data (" NOSUPPORT");
		  break;

		case SLC_CANTCHANGE:
		  debug_output_data (" CANTCHANGE");
		  break;

		case SLC_VARIABLE:
		  debug_output_data (" VARIABLE");
		  break;

		case SLC_DEFAULT:
		  debug_output_data (" DEFAULT");
		  break;
		}

	      debug_output_data ("%s%s%s",
				 pointer[i +
					 SLC_FLAGS] & SLC_ACK ? "|ACK" : "",
				 pointer[i +
					 SLC_FLAGS] & SLC_FLUSHIN ? "|FLUSHIN"
				 : "",
				 pointer[i +
					 SLC_FLAGS] & SLC_FLUSHOUT ?
				 "|FLUSHOUT" : "");
	      if (pointer[i + SLC_FLAGS] &
		  ~(SLC_ACK | SLC_FLUSHIN | SLC_FLUSHOUT | SLC_LEVELBITS))
		debug_output_data ("(0x%x)", pointer[i + SLC_FLAGS]);
	      debug_output_data (" %d;", pointer[i + SLC_VALUE]);

	      /* Protocol enforced duplication of IAC depends on pre
	       * and post modification of data, hence differs between
	       * in, out, and no direction, i.e., recursive mode.
	       * Some systems assign _POSIX_VDISABLE and IAC the same
	       * value!  Heuristic experiments led to the following.
	       * Recursive mode needs both steps as written here.
	       */
	      if ((pointer[i + SLC_VALUE] == IAC) &&
		  (pointer[i + SLC_VALUE + 1] == IAC) && (direction != '<'))
		i++;
	      if ((pointer[i + SLC_VALUE] == IAC) &&
		  (pointer[i + SLC_VALUE + 1] == IAC) && !direction)
		i += 2;
	    }

	  for (; i < length; i++)
	    debug_output_data (" ?%d?", pointer[i]);
	  break;

	case LM_MODE:
	  debug_output_data ("MODE ");
	  if (length < 3)
	    {
	      debug_output_data ("(no mode??\?)");
	      break;
	    }
	  {
	    char tbuf[sizeof ("|EDIT|TRAPSIG|SOFT_TAB|LIT_ECHO|ACK")];

	    snprintf (tbuf, sizeof (tbuf), "%s%s%s%s%s",
		      pointer[2] & MODE_EDIT ? "|EDIT" : "",
		      pointer[2] & MODE_TRAPSIG ? "|TRAPSIG" : "",
		      pointer[2] & MODE_SOFT_TAB ? "|SOFT_TAB" : "",
		      pointer[2] & MODE_LIT_ECHO ? "|LIT_ECHO" : "",
		      pointer[2] & MODE_ACK ? "|ACK" : "");
	    debug_output_data ("%s", tbuf[0] ? &tbuf[1] : "0");
	  }

	  if (pointer[2] & ~(MODE_EDIT | MODE_TRAPSIG | MODE_ACK))
	    debug_output_data (" (0x%x)", pointer[2]);

	  for (i = 3; i < length; i++)
	    debug_output_data (" ?0x%x?", pointer[i]);
	  break;

	default:
	  debug_output_data ("%d (unknown)", pointer[1]);
	  for (i = 2; i < length; i++)
	    debug_output_data (" %d", pointer[i]);
	}
      break;

    case TELOPT_STATUS:
      {
	char *cp;
	int j, k;

	debug_output_data ("STATUS");

	switch (pointer[1])
	  {
	  default:
	    if (pointer[1] == TELQUAL_SEND)
	      debug_output_data (" SEND");
	    else
	      debug_output_data (" %d (unknown)", pointer[1]);
	    for (i = 2; i < length; i++)
	      debug_output_data (" ?%d?", pointer[i]);
	    break;

	  case TELQUAL_IS:
	    debug_output_data (" IS\r\n");

	    for (i = 2; i < length; i++)
	      {
		switch (pointer[i])
		  {
		  case DO:
		    cp = "DO";
		    goto common2;

		  case DONT:
		    cp = "DONT";
		    goto common2;

		  case WILL:
		    cp = "WILL";
		    goto common2;

		  case WONT:
		    cp = "WONT";
		    goto common2;

		  common2:
		    i++;
		    if (TELOPT_OK (pointer[i]))
		      debug_output_data (" %s %s\r\n", cp,
					 TELOPT (pointer[i]));
		    else
		      debug_output_data (" %s %d\r\n", cp, pointer[i]);
		    break;

		  case SB:
		    debug_output_data (" SB ");
		    i++;
		    j = k = i;
		    while (j < length)
		      {
			if (pointer[j] == SE)
			  {
			    if (j + 1 == length)
			      break;
			    if (pointer[j + 1] == SE)
			      j++;
			    else
			      break;
			  }
			pointer[k++] = pointer[j++];
		      }
		    printsub (0, &pointer[i], k - i);
		    if (i < length)
		      {
			debug_output_data (" SE");
			i = j;
		      }
		    else
		      i = j - 1;

		    debug_output_data ("\r\n");
		    break;

		  default:
		    debug_output_data (" %d", pointer[i]);
		    break;
		  }
	      }
	    break;
	  }
	break;
      }

    case TELOPT_XDISPLOC:
      debug_output_data ("X-DISPLAY-LOCATION ");
      switch (pointer[1])
	{
	case TELQUAL_IS:
	  debug_output_data ("IS \"%.*s\"", length - 2, (char *) pointer + 2);
	  break;
	case TELQUAL_SEND:
	  debug_output_data ("SEND");
	  break;
	default:
	  debug_output_data ("- unknown qualifier %d (0x%x).",
			     pointer[1], pointer[1]);
	}
      break;

    case TELOPT_NEW_ENVIRON:
      debug_output_data ("NEW-ENVIRON ");
      goto env_common1;

    case TELOPT_OLD_ENVIRON:
      debug_output_data ("OLD-ENVIRON");
    env_common1:
      switch (pointer[1])
	{
	case TELQUAL_IS:
	  debug_output_data ("IS ");
	  goto env_common;

	case TELQUAL_SEND:
	  debug_output_data ("SEND ");
	  goto env_common;

	case TELQUAL_INFO:
	  debug_output_data ("INFO ");

	env_common:
	  {
	    char *quote = "";
	    for (i = 2; i < length; i++)
	      {
		switch (pointer[i])
		  {
		  case NEW_ENV_VAR:
		    debug_output_data ("%sVAR ", quote);
		    quote = "";
		    break;

		  case NEW_ENV_VALUE:
		    debug_output_data ("%sVALUE ", quote);
		    quote = "";
		    break;

		  case ENV_ESC:
		    debug_output_data ("%sESC ", quote);
		    quote = "";
		    break;

		  case ENV_USERVAR:
		    debug_output_data ("%sUSERVAR ", quote);
		    quote = "";
		    break;

		  default:
		    if (isprint (pointer[i]) && pointer[i] != '"')
		      {
			if (strcmp (quote, "") == 0)
			  {
			    debug_output_data ("\"");
			    quote = "\" ";
			  }
			debug_output_datalen ((char *) &pointer[i], 1);
		      }
		    else
		      {
			debug_output_data ("%s%03o ", quote, pointer[i]);
			quote = "";
		      }
		    break;
		  }
	      }
	    if (strcmp (quote, "\" ") == 0)
	      debug_output_data ("\"");
	    break;
	  }
	}
      break;

#if defined AUTHENTICATION
    case TELOPT_AUTHENTICATION:
      debug_output_data ("AUTHENTICATION");

      if (length < 2)
	{
	  debug_output_data (" (empty suboption??\?)");
	  break;
	}
      switch (pointer[1])
	{
	case TELQUAL_REPLY:
	case TELQUAL_IS:
	  debug_output_data (" %s ", (pointer[1] == TELQUAL_IS) ?
			     "IS" : "REPLY");
	  if (AUTHTYPE_NAME_OK (pointer[2]) && AUTHTYPE_NAME (pointer[2]))
	    debug_output_data ("%s ", AUTHTYPE_NAME (pointer[2]));
	  else
	    debug_output_data ("%d ", pointer[2]);
	  if (length < 3)
	    {
	      debug_output_data ("(partial suboption??\?)");
	      break;
	    }
	  debug_output_data ("%s|%s",
			     ((pointer[3] & AUTH_WHO_MASK) ==
			      AUTH_WHO_CLIENT) ? "CLIENT" : "SERVER",
			     ((pointer[3] & AUTH_HOW_MASK) ==
			      AUTH_HOW_MUTUAL) ? "MUTUAL" : "ONE-WAY");

	  auth_printsub (&pointer[1], length - 1, buf, sizeof (buf));
	  debug_output_data ("%s", buf);
	  break;

	case TELQUAL_SEND:
	  i = 2;
	  debug_output_data (" SEND ");
	  while (i < length)
	    {
	      if (AUTHTYPE_NAME_OK (pointer[i]) && AUTHTYPE_NAME (pointer[i]))
		debug_output_data ("%s ", AUTHTYPE_NAME (pointer[i]));
	      else
		debug_output_data ("%d ", pointer[i]);
	      if (++i >= length)
		{
		  debug_output_data ("(partial suboption??\?)");
		  break;
		}
	      debug_output_data ("%s|%s ",
				 ((pointer[i] & AUTH_WHO_MASK) ==
				  AUTH_WHO_CLIENT) ? "CLIENT" : "SERVER",
				 ((pointer[i] & AUTH_HOW_MASK) ==
				  AUTH_HOW_MUTUAL) ? "MUTUAL" : "ONE-WAY");
	      ++i;
	    }
	  break;

	case TELQUAL_NAME:
	  i = 2;
	  debug_output_data (" NAME \"");
	  debug_output_datalen ((char *) &pointer[i], length);
	  i += length;
	  debug_output_data ("\"");
	  break;

	default:
	  for (i = 2; i < length; i++)
	    debug_output_data (" ?%d?", pointer[i]);
	  break;
	}
      break;
#endif

#ifdef	ENCRYPTION
    case TELOPT_ENCRYPT:
      debug_output_data ("ENCRYPT");
      if (length < 2)
	{
	  debug_output_data (" (empty suboption??\?)");
	  break;
	}
      switch (pointer[1])
	{
	case ENCRYPT_START:
	  debug_output_data (" START");
	  break;

	case ENCRYPT_END:
	  debug_output_data (" END");
	  break;

	case ENCRYPT_REQSTART:
	  debug_output_data (" REQUEST-START");
	  break;

	case ENCRYPT_REQEND:
	  debug_output_data (" REQUEST-END");
	  break;

	case ENCRYPT_IS:
	case ENCRYPT_REPLY:
	  debug_output_data (" %s ", (pointer[1] == ENCRYPT_IS) ?
			     "IS" : "REPLY");
	  if (length < 3)
	    {
	      debug_output_data (" (partial suboption??\?)");
	      break;
	    }
	  if (ENCTYPE_NAME_OK (pointer[2]) && ENCTYPE_NAME (pointer[2]))
	    debug_output_data ("%s ", ENCTYPE_NAME (pointer[2]));
	  else
	    debug_output_data (" %d (unknown)", pointer[2]);

	  encrypt_printsub (&pointer[1], length - 1, buf, sizeof (buf));
	  debug_output_data ("%s", buf);
	  break;

	case ENCRYPT_SUPPORT:
	  i = 2;
	  debug_output_data (" SUPPORT ");
	  while (i < length)
	    {
	      if (ENCTYPE_NAME_OK (pointer[i]) && ENCTYPE_NAME (pointer[i]))
		debug_output_data ("%s ", ENCTYPE_NAME (pointer[i]));
	      else
		debug_output_data ("%d ", pointer[i]);
	      i++;
	    }
	  break;

	case ENCRYPT_ENC_KEYID:
	  debug_output_data (" ENC_KEYID", pointer[1]);
	  goto encommon;

	case ENCRYPT_DEC_KEYID:
	  debug_output_data (" DEC_KEYID", pointer[1]);
	  goto encommon;

	default:
	  debug_output_data (" %d (unknown)", pointer[1]);
	encommon:
	  for (i = 2; i < length; i++)
	    debug_output_data (" %d", pointer[i]);
	  break;
	}
      break;
#endif /* ENCRYPTION */

    default:
      if (TELOPT_OK (pointer[0]))
	debug_output_data ("%s (unknown)", TELOPT (pointer[0]));
      else
	debug_output_data ("%d (unknown)", pointer[0]);
      for (i = 1; i < length; i++)
	debug_output_data (" %d", pointer[i]);
      break;
    }

  /* Without direction, we are doing a recursive suboption printing.
   * Suppress NL, which would otherwise misplace the SE marker.
   */
  if (direction)
    debug_output_data ("\r\n");
}

/*
 * Dump a data buffer in hex and ascii to the output data stream.
 */
void
printdata (char *tag, char *ptr, int cnt)
{
  int i;
  char xbuf[30];

  while (cnt)
    {
      /* add a line of output */
      debug_output_data ("%s: ", tag);
      for (i = 0; i < 20 && cnt; i++)
	{
	  debug_output_data ("%02x", (unsigned char) *ptr);
	  xbuf[i] = isprint ((int) *ptr) ? *ptr : '.';

	  if (i % 2)
	    debug_output_data (" ");
	  cnt--;
	  ptr++;
	}

      xbuf[i] = '\0';
      debug_output_data (" %s\r\n", xbuf);
    }
}

#if defined AUTHENTICATION || defined ENCRYPTION

int
net_write (unsigned char *str, int len)
{
  return net_output_datalen (str, len);
}

void
net_encrypt (void)
{
# ifdef	ENCRYPTION
  char *s = (nclearto > nbackp) ? nclearto : nbackp;
  if (s < nfrontp && encrypt_output)
    (*encrypt_output) ((unsigned char *) s, nfrontp - s);
  nclearto = nfrontp;
# endif/* ENCRYPTION */
}

int
telnet_spin (void)
{
  io_drain ();
  return 0;
}


#endif

/* ************************************************************************* */
/* String expansion functions */

#define EXP_STATE_CONTINUE 0
#define EXP_STATE_SUCCESS  1
#define EXP_STATE_ERROR    2

struct line_expander
{
  int state;			/* Current state */
  int level;			/* The nesting level */
  char *source;			/* The source string */
  char *cp;			/* Current position in the source */
  struct obstack stk;		/* Obstack for expanded version */
};

static char *_var_short_name (struct line_expander *exp);
static char *_var_long_name (struct line_expander *exp,
			     char *start, int length);
static char *_expand_var (struct line_expander *exp);
static void _expand_cond (struct line_expander *exp);
static void _skip_block (struct line_expander *exp);
static void _expand_block (struct line_expander *exp);

/* Expand a variable referenced by its short one-symbol name.
   Input: exp->cp points to the variable name.
   FIXME: not implemented */
char *
_var_short_name (struct line_expander *exp)
{
  char *q;
  char timebuf[64];
  time_t t;

  switch (*exp->cp++)
    {
    case 'a':
#ifdef AUTHENTICATION
      if (auth_level >= 0 && autologin == AUTH_VALID)
	return xstrdup ("ok");
#endif
      return NULL;

    case 'd':
      time (&t);
      strftime (timebuf, sizeof (timebuf),
		"%l:%M%p on %A, %d %B %Y", localtime (&t));
      return xstrdup (timebuf);

    case 'h':
      return xstrdup (remote_hostname);

    case 'l':
      return xstrdup (local_hostname);

    case 'L':
      return xstrdup (line);

    case 't':
      q = strchr (line + 1, '/');
      if (q)
	q++;
      else
	q = line;
      return xstrdup (q);

    case 'T':
      return terminaltype ? xstrdup (terminaltype) : NULL;

    case 'u':
      return user_name ? xstrdup (user_name) : NULL;

    case 'U':
      return getenv ("USER") ? xstrdup (getenv ("USER")) : xstrdup ("");

    default:
      exp->state = EXP_STATE_ERROR;
      return NULL;
    }
}

/* Expand a variable referenced by its long name.
   Input: exp->cp points to initial '('
   FIXME: not implemented */
char *
_var_long_name (struct line_expander *exp, char *start, int length)
{
  (void) start;			/* Silence warnings until implemented.  */
  (void) length;
  exp->state = EXP_STATE_ERROR;
  return NULL;
}

/* Expand a variable to its value.
   Input: exp->cp points one character _past_ % (or ?) */
char *
_expand_var (struct line_expander *exp)
{
  char *p;
  switch (*exp->cp)
    {
    case '{':
      /* Collect variable name */
      for (p = ++exp->cp; *exp->cp && *exp->cp != '}'; exp->cp++)
	;
      if (*exp->cp == 0)
	{
	  exp->cp = p;
	  exp->state = EXP_STATE_ERROR;
	  break;
	}
      p = _var_long_name (exp, p, exp->cp - p);
      exp->cp++;
      break;

    default:
      p = _var_short_name (exp);
      break;
    }
  return p;
}

/* Expand a conditional block. A conditional block is:
       %?<var>{true-stmt}[{false-stmt}]
   <var> may be either a one-symbol variable name or (string). The latter
   is not handled yet.
   On input exp->cp points to % character */
void
_expand_cond (struct line_expander *exp)
{
  char *p;

  if (*++exp->cp == '?')
    {
      /* condition */
      exp->cp++;
      p = _expand_var (exp);
      if (p)
	{
	  _expand_block (exp);
	  _skip_block (exp);
	}
      else
	{
	  _skip_block (exp);
	  _expand_block (exp);
	}
      free (p);
    }
  else
    {
      p = _expand_var (exp);
      if (p)
	obstack_grow (&exp->stk, p, strlen (p));
      free (p);
    }
}

/* Skip the block. If the exp->cp does not point to the beginning of a
   block ({ character), the function does nothing */
void
_skip_block (struct line_expander *exp)
{
  int level = exp->level;
  if (*exp->cp != '{')
    return;
  for (; *exp->cp; exp->cp++)
    {
      switch (*exp->cp)
	{
	case '{':
	  exp->level++;
	  break;

	case '}':
	  exp->level--;
	  if (exp->level == level)
	    {
	      exp->cp++;
	      return;
	    }
	}
    }
}

/* Expand a block within the formatted line. Stops either when end of source
   line was reached or the nesting reaches the initial value */
void
_expand_block (struct line_expander *exp)
{
  int level = exp->level;
  if (*exp->cp == '{')
    {
      exp->level++;
      exp->cp++;		/*FIXME? */
    }
  while (exp->state == EXP_STATE_CONTINUE)
    {
      for (; *exp->cp && *exp->cp != '%'; exp->cp++)
	{
	  switch (*exp->cp)
	    {
	    case '{':
	      exp->level++;
	      break;

	    case '}':
	      exp->level--;
	      if (exp->level == level)
		{
		  exp->cp++;
		  return;
		}
	      break;

	    case '\\':
	      exp->cp++;
	      break;
	    }
	  obstack_1grow (&exp->stk, *exp->cp);
	}

      if (*exp->cp == 0)
	{
	  obstack_1grow (&exp->stk, 0);
	  exp->state = EXP_STATE_SUCCESS;
	  break;
	}
      else if (*exp->cp == '%' && exp->cp[1] == '%')
	{
	  obstack_1grow (&exp->stk, *exp->cp);
	  exp->cp += 2;
	  continue;
	}

      _expand_cond (exp);
    }
}

/* Expand a format line */
char *
expand_line (const char *line)
{
  char *p = NULL;
  struct line_expander exp;

  exp.state = EXP_STATE_CONTINUE;
  exp.level = 0;
  exp.source = (char *) line;
  exp.cp = (char *) line;
  obstack_init (&exp.stk);
  _expand_block (&exp);
  if (exp.state == EXP_STATE_SUCCESS)
    p = xstrdup (obstack_finish (&exp.stk));
  else
    {
      syslog (LOG_ERR, "can't expand line: %s", line);
      syslog (LOG_ERR, "stopped near %s", exp.cp ? exp.cp : "(END)");
    }
  obstack_free (&exp.stk, NULL);
  return p;
}
