[BACK]Return to mandoc.c CVS log [TXT][DIR] Up to [cvsweb.bsd.lv] / mandoc

File: [cvsweb.bsd.lv] / mandoc / mandoc.c (download)

Revision 1.45, Sat Apr 9 15:29:40 2011 UTC (13 years ago) by kristaps
Branch: MAIN
Changes since 1.44: +306 -142 lines

Remove a2roffdeco() and mandoc_special() functions and replace them with
a public (mandoc.h) function mandoc_escape(), which merges the
functionality of both prior functions.

Reason: code duplication.  The a2roffdeco() and mandoc_special()
functions were pretty much the same thing and both quite complex.  This
allows one function to receive improvements in (e.g.) subexpression
handling and performance, instead of having to replicate functionality.

As such, the mandoc_escape() function already handles a superset of the
escapes handled in previous versions and has improvements in performance
(using strcspn(), for example) and reliable handling of subexpressions.

This code Works For Me, but may need work to catch any regressions.
Since the benefits are great (leaner code, simpler API), I'd rather have
it in-tree than floating as a patch.

/*	$Id: mandoc.c,v 1.45 2011/04/09 15:29:40 kristaps Exp $ */
/*
 * Copyright (c) 2008, 2009, 2010 Kristaps Dzonsons <kristaps@bsd.lv>
 * Copyright (c) 2011 Ingo Schwarze <schwarze@openbsd.org>
 *
 * Permission to use, copy, modify, and distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHORS DISCLAIM ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include <sys/types.h>

#include <assert.h>
#include <ctype.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <time.h>

#include "mandoc.h"
#include "libmandoc.h"

#define DATESIZE 32

static	int	 a2time(time_t *, const char *, const char *);
static	char	*time2a(time_t);
static	int	 numescape(const char *);

/*
 * Pass over recursive numerical expressions.  This context of this
 * function is important: it's only called within character-terminating
 * escapes (e.g., \s[xxxyyy]), so all we need to do is handle initial
 * recursion: we don't care about what's in these blocks. 
 * This returns the number of characters skipped or -1 if an error
 * occurs (the caller should bail).
 */
static int
numescape(const char *start)
{
	int		 i;
	size_t		 sz;
	const char	*cp;

	i = 0;

	/* The expression consists of a subexpression. */

	if ('\\' == start[i]) {
		cp = &start[++i];
		/*
		 * Read past the end of the subexpression.
		 * Bail immediately on errors.
		 */
		if (ESCAPE_ERROR == mandoc_escape(&cp, NULL, NULL))
			return(-1);
		return(i + cp - &start[i]);
	} 

	if ('(' != start[i++])
		return(0);

	/*
	 * A parenthesised subexpression.  Read until the closing
	 * parenthesis, making sure to handle any nested subexpressions
	 * that might ruin our parse.
	 */

	while (')' != start[i]) {
		sz = strcspn(&start[i], ")\\");
		i += (int)sz;

		if ('\0' == start[i])
			return(-1);
		else if ('\\' != start[i])
			continue;

		cp = &start[++i];
		if (ESCAPE_ERROR == mandoc_escape(&cp, NULL, NULL))
			return(-1);
		i += cp - &start[i];
	}

	/* Read past the terminating ')'. */
	return(++i);
}

/*
 * Handle an escaped sequeence.  This should be called with any
 * string subsequent a `\'.  Pass a pointer to this substring as "end";
 * it will be set to the supremum of the parsed escape sequence.  If
 * this returns ESCAPE_ERROR, the string is bogus and should be thrown
 * away.  If not ESCAPE_ERROR or ESCAPE_IGNORE, "start" is set to the
 * first relevant character of the substring (font, glyph, whatever) of
 * length sz.  Both "start" and "sz" may be NULL.
 */
enum mandoc_esc
mandoc_escape(const char **end, const char **start, int *sz)
{
	char		 c, term, numeric;
	int		 i, lim, ssz, rlim;
	const char	*cp, *rstart;
	enum mandoc_esc	 gly; 

	cp = *end;
	rstart = cp;
	if (start)
		*start = rstart;
	i = 0;
	gly = ESCAPE_ERROR;
	term = '\0';
	numeric = 0;

	switch ((c = cp[i++])) {
	/*
	 * First the glyphs.  There are several different forms of
	 * these, but each eventually returns a substring of the glyph
	 * name.
	 */
	case ('('):
		gly = ESCAPE_SPECIAL;
		lim = 2;
		break;
	case ('['):
		gly = ESCAPE_SPECIAL;
		term = ']';
		break;
	case ('C'):
		if ('\'' != cp[i])
			return(ESCAPE_ERROR);
		gly = ESCAPE_SPECIAL;
		term = '\'';
		break;

	/*
	 * Handle all triggers matching \X(xy, \Xx, and \X[xxxx], where
	 * 'X' is the trigger.  These have opaque sub-strings.
	 */
	case ('F'):
		/* FALLTHROUGH */
	case ('g'):
		/* FALLTHROUGH */
	case ('k'):
		/* FALLTHROUGH */
	case ('M'):
		/* FALLTHROUGH */
	case ('m'):
		/* FALLTHROUGH */
	case ('n'):
		/* FALLTHROUGH */
	case ('V'):
		/* FALLTHROUGH */
	case ('Y'):
		if (ESCAPE_ERROR == gly)
			gly = ESCAPE_IGNORE;
		/* FALLTHROUGH */
	case ('*'):
		if (ESCAPE_ERROR == gly)
			gly = ESCAPE_PREDEF;
		/* FALLTHROUGH */
	case ('f'):
		if (ESCAPE_ERROR == gly)
			gly = ESCAPE_FONT;

		rstart= &cp[i];
		if (start) 
			*start = rstart;

		switch (cp[i++]) {
		case ('('):
			lim = 2;
			break;
		case ('['):
			term = ']';
			break;
		default:
			lim = 1;
			i--;
			break;
		}
		break;

	/*
	 * These escapes are of the form \X'Y', where 'X' is the trigger
	 * and 'Y' is any string.  These have opaque sub-strings.
	 */
	case ('A'):
		/* FALLTHROUGH */
	case ('b'):
		/* FALLTHROUGH */
	case ('D'):
		/* FALLTHROUGH */
	case ('o'):
		/* FALLTHROUGH */
	case ('R'):
		/* FALLTHROUGH */
	case ('X'):
		/* FALLTHROUGH */
	case ('Z'):
		if ('\'' != cp[i++])
			return(ESCAPE_ERROR);
		gly = ESCAPE_IGNORE;
		term = '\'';
		break;

	/*
	 * These escapes are of the form \X'N', where 'X' is the trigger
	 * and 'N' resolves to a numerical expression.
	 */
	case ('B'):
		/* FALLTHROUGH */
	case ('h'):
		/* FALLTHROUGH */
	case ('H'):
		/* FALLTHROUGH */
	case ('L'):
		/* FALLTHROUGH */
	case ('l'):
		/* FALLTHROUGH */
	case ('N'):
		if (ESCAPE_ERROR == gly)
			gly = ESCAPE_NUMBERED;
		/* FALLTHROUGH */
	case ('S'):
		/* FALLTHROUGH */
	case ('v'):
		/* FALLTHROUGH */
	case ('w'):
		/* FALLTHROUGH */
	case ('x'):
		if (ESCAPE_ERROR == gly)
			gly = ESCAPE_IGNORE;
		if ('\'' != cp[i++])
			return(ESCAPE_ERROR);
		term = numeric = '\'';
		break;

	/* 
	 * Sizes get a special category of their own.
	 */
	case ('s'):
		gly = ESCAPE_IGNORE;

		rstart = &cp[i];
		if (start) 
			*start = rstart;

		/* See +/- counts as a sign. */
		c = cp[i];
		if ('+' == c || '-' == c || ASCII_HYPH == c)
			++i;

		switch (cp[i++]) {
		case ('('):
			lim = 2;
			break;
		case ('['):
			term = numeric = ']';
			break;
		case ('\''):
			term = numeric = '\'';
			break;
		default:
			lim = 1;
			i--;
			break;
		}

		/* See +/- counts as a sign. */
		c = cp[i];
		if ('+' == c || '-' == c || ASCII_HYPH == c)
			++i;

		break;

	/*
	 * Anything else is assumed to be a glyph.
	 */
	default:
		gly = ESCAPE_SPECIAL;
		lim = 1;
		i--;
		break;
	}

	assert(ESCAPE_ERROR != gly);

	rstart = &cp[i];
	if (start)
		*start = rstart;

	/*
	 * If a terminating block has been specified, we need to
	 * handle the case of recursion, which could have their
	 * own terminating blocks that mess up our parse.  This, by the
	 * way, means that the "start" and "size" values will be
	 * effectively meaningless.
	 */

	ssz = 0;
	if (numeric && -1 == (ssz = numescape(&cp[i])))
		return(ESCAPE_ERROR);

	i += ssz;
	rlim = -1;

	/*
	 * We have a character terminator.  Try to read up to that
	 * character.  If we can't (i.e., we hit the nil), then return
	 * an error; if we can, calculate our length, read past the
	 * terminating character, and exit.
	 */

	if ('\0' != term) {
		*end = strchr(&cp[i], term);
		if ('\0' == *end)
			return(ESCAPE_ERROR);

		rlim = *end - &cp[i];
		if (sz)
			*sz = rlim;
		(*end)++;
		goto out;
	}

	assert(lim > 0);

	/*
	 * We have a numeric limit.  If the string is shorter than that,
	 * stop and return an error.  Else adjust our endpoint, length,
	 * and return the current glyph.
	 */

	if ((size_t)lim > strlen(&cp[i]))
		return(ESCAPE_ERROR);

	rlim = lim;
	if (sz)
		*sz = rlim;

	*end = &cp[i] + lim;

out:
	assert(rlim >= 0 && rstart);

	/* Run post-processors. */

	switch (gly) {
	case (ESCAPE_FONT):
		if (1 != rlim)
			break;
		switch (*rstart) {
		case ('3'):
			/* FALLTHROUGH */
		case ('B'):
			gly = ESCAPE_FONTBOLD;
			break;
		case ('2'):
			/* FALLTHROUGH */
		case ('I'):
			gly = ESCAPE_FONTITALIC;
			break;
		case ('P'):
			gly = ESCAPE_FONTPREV;
			break;
		case ('1'):
			/* FALLTHROUGH */
		case ('R'):
			gly = ESCAPE_FONTROMAN;
			break;
		}
	case (ESCAPE_SPECIAL):
		if (1 != rlim)
			break;
		if ('c' == *rstart)
			gly = ESCAPE_NOSPACE;
		break;
	default:
		break;
	}

	return(gly);
}

void *
mandoc_calloc(size_t num, size_t size)
{
	void		*ptr;

	ptr = calloc(num, size);
	if (NULL == ptr) {
		perror(NULL);
		exit((int)MANDOCLEVEL_SYSERR);
	}

	return(ptr);
}


void *
mandoc_malloc(size_t size)
{
	void		*ptr;

	ptr = malloc(size);
	if (NULL == ptr) {
		perror(NULL);
		exit((int)MANDOCLEVEL_SYSERR);
	}

	return(ptr);
}


void *
mandoc_realloc(void *ptr, size_t size)
{

	ptr = realloc(ptr, size);
	if (NULL == ptr) {
		perror(NULL);
		exit((int)MANDOCLEVEL_SYSERR);
	}

	return(ptr);
}


char *
mandoc_strdup(const char *ptr)
{
	char		*p;

	p = strdup(ptr);
	if (NULL == p) {
		perror(NULL);
		exit((int)MANDOCLEVEL_SYSERR);
	}

	return(p);
}

/*
 * Parse a quoted or unquoted roff-style request or macro argument.
 * Return a pointer to the parsed argument, which is either the original
 * pointer or advanced by one byte in case the argument is quoted.
 * Null-terminate the argument in place.
 * Collapse pairs of quotes inside quoted arguments.
 * Advance the argument pointer to the next argument,
 * or to the null byte terminating the argument line.
 */
char *
mandoc_getarg(struct mparse *parse, char **cpp, int ln, int *pos)
{
	char	 *start, *cp;
	int	  quoted, pairs, white;

	/* Quoting can only start with a new word. */
	start = *cpp;
	if ('"' == *start) {
		quoted = 1;
		start++;
	} else
		quoted = 0;

	pairs = 0;
	white = 0;
	for (cp = start; '\0' != *cp; cp++) {
		/* Move left after quoted quotes and escaped backslashes. */
		if (pairs)
			cp[-pairs] = cp[0];
		if ('\\' == cp[0]) {
			if ('\\' == cp[1]) {
				/* Poor man's copy mode. */
				pairs++;
				cp++;
			} else if (0 == quoted && ' ' == cp[1])
				/* Skip escaped blanks. */
				cp++;
		} else if (0 == quoted) {
			if (' ' == cp[0]) {
				/* Unescaped blanks end unquoted args. */
				white = 1;
				break;
			}
		} else if ('"' == cp[0]) {
			if ('"' == cp[1]) {
				/* Quoted quotes collapse. */
				pairs++;
				cp++;
			} else {
				/* Unquoted quotes end quoted args. */
				quoted = 2;
				break;
			}
		}
	}

	/* Quoted argument without a closing quote. */
	if (1 == quoted)
		mandoc_msg(MANDOCERR_BADQUOTE, parse, ln, *pos, NULL);

	/* Null-terminate this argument and move to the next one. */
	if (pairs)
		cp[-pairs] = '\0';
	if ('\0' != *cp) {
		*cp++ = '\0';
		while (' ' == *cp)
			cp++;
	}
	*pos += (int)(cp - start) + (quoted ? 1 : 0);
	*cpp = cp;

	if ('\0' == *cp && (white || ' ' == cp[-1]))
		mandoc_msg(MANDOCERR_EOLNSPACE, parse, ln, *pos, NULL);

	return(start);
}

static int
a2time(time_t *t, const char *fmt, const char *p)
{
	struct tm	 tm;
	char		*pp;

	memset(&tm, 0, sizeof(struct tm));

	pp = strptime(p, fmt, &tm);
	if (NULL != pp && '\0' == *pp) {
		*t = mktime(&tm);
		return(1);
	}

	return(0);
}

static char *
time2a(time_t t)
{
	struct tm	 tm;
	char		*buf, *p;
	size_t		 ssz;
	int		 isz;

	localtime_r(&t, &tm);

	/*
	 * Reserve space:
	 * up to 9 characters for the month (September) + blank
	 * up to 2 characters for the day + comma + blank
	 * 4 characters for the year and a terminating '\0'
	 */
	p = buf = mandoc_malloc(10 + 4 + 4 + 1);

	if (0 == (ssz = strftime(p, 10 + 1, "%B ", &tm)))
		goto fail;
	p += (int)ssz;

	if (-1 == (isz = snprintf(p, 4 + 1, "%d, ", tm.tm_mday)))
		goto fail;
	p += isz;

	if (0 == strftime(p, 4 + 1, "%Y", &tm))
		goto fail;
	return(buf);

fail:
	free(buf);
	return(NULL);
}

char *
mandoc_normdate(struct mparse *parse, char *in, int ln, int pos)
{
	char		*out;
	time_t		 t;

	if (NULL == in || '\0' == *in ||
	    0 == strcmp(in, "$" "Mdocdate$")) {
		mandoc_msg(MANDOCERR_NODATE, parse, ln, pos, NULL);
		time(&t);
	}
	else if (!a2time(&t, "$" "Mdocdate: %b %d %Y $", in) &&
	    !a2time(&t, "%b %d, %Y", in) &&
	    !a2time(&t, "%Y-%m-%d", in)) {
		mandoc_msg(MANDOCERR_BADDATE, parse, ln, pos, NULL);
		t = 0;
	}
	out = t ? time2a(t) : NULL;
	return(out ? out : mandoc_strdup(in));
}

int
mandoc_eos(const char *p, size_t sz, int enclosed)
{
	const char *q;
	int found;

	if (0 == sz)
		return(0);

	/*
	 * End-of-sentence recognition must include situations where
	 * some symbols, such as `)', allow prior EOS punctuation to
	 * propogate outward.
	 */

	found = 0;
	for (q = p + (int)sz - 1; q >= p; q--) {
		switch (*q) {
		case ('\"'):
			/* FALLTHROUGH */
		case ('\''):
			/* FALLTHROUGH */
		case (']'):
			/* FALLTHROUGH */
		case (')'):
			if (0 == found)
				enclosed = 1;
			break;
		case ('.'):
			/* FALLTHROUGH */
		case ('!'):
			/* FALLTHROUGH */
		case ('?'):
			found = 1;
			break;
		default:
			return(found && (!enclosed || isalnum((unsigned char)*q)));
		}
	}

	return(found && !enclosed);
}

int
mandoc_hyph(const char *start, const char *c)
{

	/*
	 * Choose whether to break at a hyphenated character.  We only
	 * do this if it's free-standing within a word.
	 */

	/* Skip first/last character of buffer. */
	if (c == start || '\0' == *(c + 1))
		return(0);
	/* Skip first/last character of word. */
	if ('\t' == *(c + 1) || '\t' == *(c - 1))
		return(0);
	if (' ' == *(c + 1) || ' ' == *(c - 1))
		return(0);
	/* Skip double invocations. */
	if ('-' == *(c + 1) || '-' == *(c - 1))
		return(0);
	/* Skip escapes. */
	if ('\\' == *(c - 1))
		return(0);

	return(1);
}

/*
 * Find out whether a line is a macro line or not.  If it is, adjust the
 * current position and return one; if it isn't, return zero and don't
 * change the current position.
 */
int
mandoc_getcontrol(const char *cp, int *ppos)
{
	int		pos;

	pos = *ppos;

	if ('\\' == cp[pos] && '.' == cp[pos + 1])
		pos += 2;
	else if ('.' == cp[pos] || '\'' == cp[pos])
		pos++;
	else
		return(0);

	while (' ' == cp[pos] || '\t' == cp[pos])
		pos++;

	*ppos = pos;
	return(1);
}