Files
wget2/libwget/xml.c
Tim Rühsen b0e1def6fe Add docs for xml parsing functions.
* libwget/xml.c: Rename XML_CONTEXT to _xml_context,
  document wget_* functions
* docs/Makefile.am: Sort order of man page files,
  add building of libwget-xml.3
2017-08-04 11:26:24 +02:00

618 lines
17 KiB
C

/*
* Copyright(c) 2012 Tim Ruehsen
* Copyright(c) 2015-2016 Free Software Foundation, Inc.
*
* This file is part of libwget.
*
* Libwget is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Libwget 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with libwget. If not, see <https://www.gnu.org/licenses/>.
*
*
* xml parsing routines
*
* Changelog
* 22.06.2012 Tim Ruehsen created, but needs definitely a rewrite
*
* This derives from an old source code that I wrote in 2001.
* It is short, fast and has a low memory print, BUT it is a hack.
* It has to be replaced by e.g. libxml2 or something better.
*
* HTML parsing is (very) different from XML parsing, see here:
* https://html.spec.whatwg.org/multipage/syntax.html
* It is a PITA and should be handled by a specialized, external library !
*
*/
#include <config.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <fcntl.h>
#include <sys/stat.h>
#ifdef HAVE_MMAP
#include <sys/mman.h>
#endif
#include <wget.h>
#include "private.h"
typedef struct {
const char
*buf, // pointer to original start of buffer (0-terminated)
*p, // pointer next char in buffer
*token; // token buffer
int
hints; // XML_HINT...
size_t
token_size, // size of token buffer
token_len; // used bytes of token buffer (not counting terminating 0 byte)
void
*user_ctx; // user context (not needed if we were using nested functions)
wget_xml_callback_t
callback;
} _xml_context;
/* \cond _hide_internal_symbols */
#define ascii_isspace(c) (c == ' ' || (c >= 9 && c <= 13))
// working only for consecutive alphabets, e.g. EBCDIC would not work
#define ascii_isalpha(c) ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'))
/* \endcond */
// append a char to token buffer
static const char *getToken(_xml_context *context)
{
int c;
const char *p;
// skip leading whitespace
while ((c = *context->p) && ascii_isspace(c))
context->p++;
if (!c) return NULL;
context->token = context->p++;
// info_printf("a c=%c\n", c);
if (ascii_isalpha(c) || c == '_') {
while ((c = *context->p) && !ascii_isspace(c) && c != '>' && c != '=')
context->p++;
if (!c) return NULL;
context->token_len = context->p - context->token;
return context->token;
}
if (c == '/') {
if (!(c = *context->p)) return NULL;
context->p++;
if (c == '>') {
context->token_len = 2;
return context->token;
} else return NULL; // syntax error
}
if (c == '\"' || c == '\'') { // read in quoted value
int quote = c;
context->token = context->p;
if (!(p = strchr(context->p, quote)))
return NULL;
context->p = p + 1;
context->token_len = context->p - context->token - 1;
return context->token;
}
if (c == '<') { // fetch specials, e.g. start of comments '<!--'
if (!(c = *context->p)) return NULL;
context->p++;
if (c == '?' || c == '/') {
context->token_len = 2;
return context->token;
}
if (c == '!') {
// left: <!--, <![CDATA[ and <!WHATEVER
if (!(c = *context->p)) return NULL;
if (c == '-') {
context->p++;
if (!(c = *context->p)) return NULL;
context->p++;
if (c == '-') {
context->token_len = 4;
return context->token;
} else {
context->p -= 2;
context->token_len = 2;
return context->token;
}
} else {
context->token_len = 2;
return context->token;
}
} else {
context->p--;
context->token_len = 1;
return context->token;
}
}
if (c == '>' || c == '=') {
context->token_len = 1;
return context->token;
}
if (c == '-') { // fetch specials, e.g. end of comments '-->'
if (!(c = *context->p)) return NULL;
if (c != '-') {
c = '-';
} else {
context->p++;
if (!(c = *context->p)) return NULL;
context->p++;
if (c != '>') {
context->p -= 2;
c = '-';
} else {
context->token_len = 3;
return context->token;
}
}
}
if (c == '?') { // fetch specials, e.g. '?>'
if (!(c = *context->p)) return NULL;
if (c != '>') {
// c = '?';
} else {
context->p++;
context->token_len = 2;
return context->token;
}
}
while ((c = *context->p) && !ascii_isspace(c))
context->p++;
if (c) {
context->token_len = context->p - context->token;
return context->token;
}
return NULL;
}
static int getValue(_xml_context *context)
{
int c;
context->token_len = 0;
context->token = context->p;
// remove leading spaces
while ((c = *context->p) && ascii_isspace(c))
context->p++;
if (!c) return EOF;
if (c == '=') {
context->p++;
if (!getToken(context))
return EOF;
else
return 1; // token valid
}
// attribute without value
context->token = context->p;
return 1;
}
// special HTML <script> content parsing
// see https://html.spec.whatwg.org/multipage/scripting.html#the-script-element
// 4.3.1.2 Restrictions for contents of script elements
static const char *getScriptContent(_xml_context *context)
{
int comment = 0, length_valid = 0;
const char *p;
for (p = context->token = context->p; *p; p++) {
if (comment) {
if (*p == '-' && !strncmp(p, "-->", 3)) {
p += 3 - 1;
comment = 0;
}
} else {
if (*p == '<' && !strncmp(p, "<!--", 4)) {
p += 4 - 1;
comment = 1;
} else if (*p == '<' && !wget_strncasecmp_ascii(p, "</script", 8)) {
context->token_len = p - context->token;
length_valid = 1;
for (p += 8; ascii_isspace(*p); p++);
if (*p == '>') {
p++;
break; // found end of <script>
} else if (!*p)
break; // end of input
}
}
}
context->p = p;
if (!length_valid)
context->token_len = p - context->token;
if (!*p && !context->token_len)
return NULL;
if (context->callback)
context->callback(context->user_ctx, XML_FLG_CONTENT | XML_FLG_END, "script", NULL, context->token, context->token_len, context->token - context->buf);
return context->token;
}
static const char *getUnparsed(_xml_context *context, int flags, const char *end, size_t len, const char *directory)
{
int c;
if (len == 1) {
for (context->token = context->p; (c = *context->p) && c != *end; context->p++);
} else {
for (context->token = context->p; (c = *context->p); context->p++) {
if (c == *end && context->p[1] == end[1] && (len == 2 || context->p[2] == end[2])) {
break;
}
}
}
context->token_len = context->p - context->token;
if (c) context->p += len;
if (!c && !context->token_len)
return NULL;
/*
if (context->token && context->token_len && context->hints & XML_HINT_REMOVE_EMPTY_CONTENT) {
int notempty = 0;
char *p;
for (p = context->token; *p; p++) {
if (!ascii_isspace(*p)) {
notempty = 1;
break;
}
}
if (notempty) {
if (context->callback)
context->callback(context->user_ctx, flags, directory, NULL, context->token, context->token_len, context->token - context->buf);
} else {
// ignore empty content
context->token_len = 0;
context->token[0] = 0;
}
} else {
*/
if (context->callback)
context->callback(context->user_ctx, flags, directory, NULL, context->token, context->token_len, context->token - context->buf);
// }
return context->token;
}
static const char *getComment(_xml_context *context)
{
return getUnparsed(context, XML_FLG_COMMENT, "-->", 3, NULL);
}
static const char *getProcessing(_xml_context *context)
{
return getUnparsed(context, XML_FLG_PROCESSING, "?>", 2, NULL);
}
static const char *getSpecial(_xml_context *context)
{
return getUnparsed(context, XML_FLG_SPECIAL, ">", 1, NULL);
}
static const char *getContent(_xml_context *context, const char *directory)
{
int c;
for (context->token = context->p; (c = *context->p) && c != '<'; context->p++);
context->token_len = context->p - context->token;
if (!c && !context->token_len)
return NULL;
// debug_printf("content=%.*s\n", (int)context->token_len, context->token);
if (context->callback && context->token_len)
context->callback(context->user_ctx, XML_FLG_CONTENT, directory, NULL, context->token, context->token_len, context->token - context->buf);
return context->token;
}
static void parseXML(const char *dir, _xml_context *context)
{
const char *tok;
char directory[256] = "";
size_t pos = 0;
if (!(context->hints & XML_HINT_HTML)) {
pos = wget_strlcpy(directory, dir, sizeof(directory));
if (pos >= sizeof(directory)) pos = sizeof(directory) - 1;
}
do {
getContent(context, directory);
if (context->token_len)
debug_printf("%s='%.*s'\n", directory, (int)context->token_len, context->token);
if (!(tok = getToken(context))) return;
// debug_printf("A Token '%.*s' len=%zu tok='%s'\n", (int)context->token_len, context->token, context->token_len, tok);
if (context->token_len == 1 && *tok == '<') {
// get element name and add it to directory
int flags = XML_FLG_BEGIN;
if (!(tok = getToken(context))) return;
// debug_printf("A2 Token '%.*s'\n", (int)context->token_len, context->token);
if (!(context->hints & XML_HINT_HTML)) {
if (!pos || directory[pos - 1] != '/')
snprintf(&directory[pos], sizeof(directory) - pos, "/%.*s", (int)context->token_len, tok);
else
snprintf(&directory[pos], sizeof(directory) - pos, "%.*s", (int)context->token_len, tok);
} else {
// snprintf(directory, sizeof(directory), "%.*s", (int)context->token_len, tok);
if (context->token_len < sizeof(directory)) {
memcpy(directory, tok, context->token_len);
directory[context->token_len] = 0;
} else {
memcpy(directory, tok, sizeof(directory) - 1);
directory[sizeof(directory) - 1] = 0;
}
}
while ((tok = getToken(context))) {
// debug_printf("C Token %.*s %zu %p %p dir=%s tok=%s\n", (int)context->token_len, context->token, context->token_len, context->token, context->p, directory, tok);
if (context->token_len == 2 && !strncmp(tok, "/>", 2)) {
if (context->callback)
context->callback(context->user_ctx, flags | XML_FLG_END, directory, NULL, NULL, 0, 0);
break; // stay in this level
} else if (context->token_len == 1 && *tok == '>') {
if (context->callback)
context->callback(context->user_ctx, flags | XML_FLG_CLOSE, directory, NULL, NULL, 0, 0);
if (context->hints & XML_HINT_HTML) {
if (!wget_strcasecmp_ascii(directory, "script")) {
// special HTML <script> content parsing
// see https://html.spec.whatwg.org/multipage/scripting.html#the-script-element
// 4.3.1.2 Restrictions for contents of script elements
debug_printf("*** need special <script> handling\n");
getScriptContent(context);
if (context->token_len)
debug_printf("%s=%.*s\n", directory, (int)context->token_len, context->token);
}
else if (!wget_strcasecmp_ascii(directory, "style")) {
getContent(context, "style");
if (context->token_len)
debug_printf("%s=%.*s\n", directory, (int)context->token_len, context->token);
}
} else
parseXML(directory, context); // descend one level
break;
} else {
// snprintf(attribute, sizeof(attribute), "%.*s", (int)context->token_len, tok);
char attribute[context->token_len + 1];
memcpy(attribute, tok, context->token_len);
attribute[context->token_len] = 0;
if (getValue(context) == 0) return;
if (context->token_len) {
debug_printf("%s/@%s=%.*s\n", directory, attribute, (int)context->token_len, context->token);
if (context->callback)
context->callback(context->user_ctx, flags | XML_FLG_ATTRIBUTE, directory, attribute, context->token, context->token_len, context->token - context->buf);
} else {
debug_printf("%s/@%s\n", directory, attribute);
if (context->callback)
context->callback(context->user_ctx, flags | XML_FLG_ATTRIBUTE, directory, attribute, NULL, 0, 0);
}
flags = 0;
}
}
directory[pos] = 0;
} else if (context->token_len == 2) {
if (!strncmp(tok, "</", 2)) {
// ascend one level
// cleanup - get name and '>'
if (!(tok = getToken(context))) return;
// debug_printf("X Token %s\n",tok);
if (context->callback) {
if (!(context->hints & XML_HINT_HTML))
context->callback(context->user_ctx, XML_FLG_END, directory, NULL, NULL, 0, 0);
else {
char tag[context->token_len + 1]; // we need to \0 terminate tok
memcpy(tag, tok, context->token_len);
tag[context->token_len] = 0;
context->callback(context->user_ctx, XML_FLG_END, tag, NULL, NULL, 0, 0);
}
}
if (!(tok = getToken(context))) return;
// debug_printf("Y Token %s\n",tok);
if (!(context->hints & XML_HINT_HTML))
return;
else
continue;
} else if (!strncmp(tok, "<?", 2)) { // special info - ignore
getProcessing(context);
debug_printf("%s=<?%.*s?>\n", directory, (int)context->token_len, context->token);
continue;
} else if (!strncmp(tok, "<!", 2)) {
getSpecial(context);
debug_printf("%s=<!%.*s>\n", directory, (int)context->token_len, context->token);
}
} else if (context->token_len == 4 && !strncmp(tok, "<!--", 4)) { // comment - ignore
getComment(context);
debug_printf("%s=<!--%.*s-->\n", directory, (int)context->token_len, context->token);
continue;
}
} while (tok);
}
/**
* \file
* \brief XML parsing functions
* \defgroup libwget-xml XML parsing functions
* @{
*/
/**
* \param[in] buf Zero-terminated XML or HTML input data
* \param[in] callback Function called for each token scan result
* \param[in] user_ctx User-defined context variable, handed to \p callback
* \param[in] hints Flags to influence parsing
*
* This function scans the XML input from \p buf and calls \p callback for each token
* found. \p user_ctx is a user-defined context variable and given to each call of \p callback.
*
* \p hints may be 0 or any combination of %XML_HINT_REMOVE_EMPTY_CONTENT and %XML_HINT_HTML.
*
* %XML_HINT_REMOVE_EMPTY_CONTENT reduces the number of calls to \p callback by ignoring
* empty content and superfluous spaces.
*
* %XML_HINT_HTML turns on HTML scanning.
*/
void wget_xml_parse_buffer(
const char *buf,
wget_xml_callback_t callback,
void *user_ctx,
int hints)
{
_xml_context context;
context.token = NULL;
context.token_size = 0;
context.token_len = 0;
context.buf = buf;
context.p = buf;
context.user_ctx = user_ctx;
context.callback = callback;
context.hints = hints;
parseXML("/", &context);
}
/**
* \param[in] buf Zero-terminated HTML input data
* \param[in] callback Function called for each token scan result
* \param[in] user_ctx User-defined context variable, handed to \p callback
* \param[in] hints Flags to influence parsing
*
* Convenience function that calls wget_xml_parse_buffer() with HTML parsing turned on.
*/
void wget_html_parse_buffer(
const char *buf,
wget_xml_callback_t callback,
void *user_ctx,
int hints)
{
wget_xml_parse_buffer(buf, callback, user_ctx, hints | XML_HINT_HTML);
}
/**
* \param[in] fname Name of XML or HTML input file
* \param[in] callback Function called for each token scan result
* \param[in] user_ctx User-defined context variable, handed to \p callback
* \param[in] hints Flags to influence parsing
*
* Convenience function that calls wget_xml_parse_buffer() with the file content.
*
* If \p fname is `-`, the data is read from stdin.
*/
void wget_xml_parse_file(
const char *fname,
wget_xml_callback_t callback,
void *user_ctx,
int hints)
{
int fd;
if (strcmp(fname,"-")) {
if ((fd = open(fname, O_RDONLY|O_BINARY)) != -1) {
struct stat st;
if (fstat(fd, &st) == 0) {
#ifdef HAVE_MMAP
size_t nread = st.st_size;
char *buf = mmap(NULL, nread + 1, PROT_READ|PROT_WRITE, MAP_PRIVATE, fd, 0);
#else
char *buf=xmalloc(st.st_size + 1);
size_t nread=read(fd, buf, st.st_size);
#endif
if (nread > 0) {
buf[nread] = 0; // PROT_WRITE allows this write, MAP_PRIVATE prevents changes in underlying file system
wget_xml_parse_buffer(buf, callback, user_ctx, hints);
}
#ifdef HAVE_MMAP
munmap(buf, nread);
#else
xfree(buf);
#endif
}
close(fd);
} else
error_printf(_("Failed to open %s\n"), fname);
} else {
// read data from STDIN.
// maybe should use yy_scan_bytes instead of buffering into memory.
char tmp[4096];
ssize_t nbytes;
wget_buffer_t *buf = wget_buffer_alloc(4096);
while ((nbytes = read(STDIN_FILENO, tmp, sizeof(tmp))) > 0) {
wget_buffer_memcat(buf, tmp, nbytes);
}
if (buf->length)
wget_xml_parse_buffer(buf->data, callback, user_ctx, hints);
wget_buffer_free(&buf);
}
}
/**
* \param[in] fname Name of XML or HTML input file
* \param[in] callback Function called for each token scan result
* \param[in] user_ctx User-defined context variable, handed to \p callback
* \param[in] hints Flags to influence parsing
*
* Convenience function that calls wget_xml_parse_file() with HTML parsing turned on.
*
* If \p fname is `-`, the data is read from stdin.
*/
void wget_html_parse_file(
const char *fname,
wget_xml_callback_t callback,
void *user_ctx,
int hints)
{
wget_xml_parse_file(fname, callback, user_ctx, hints | XML_HINT_HTML);
}
/** @} */