blob: 9ca6fdbd515b7d39d8400ea0bd3c1c537b0209d0 [file] [log] [blame]
/* Reading Desktop Entry files.
Copyright (C) 1995-1998, 2000-2003, 2005-2006, 2008-2009, 2014-2019 Free
Software Foundation, Inc.
This file was written by Daiki Ueno <ueno@gnu.org>.
This program 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.
This program 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 <https://www.gnu.org/licenses/>. */
#ifdef HAVE_CONFIG_H
# include <config.h>
#endif
/* Specification. */
#include "read-desktop.h"
#include "xalloc.h"
#include <assert.h>
#include <errno.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "error.h"
#include "error-progname.h"
#include "xalloc.h"
#include "xvasprintf.h"
#include "c-ctype.h"
#include "po-lex.h"
#include "po-xerror.h"
#include "gettext.h"
#define _(str) gettext (str)
#define SIZEOF(a) (sizeof(a) / sizeof(a[0]))
/* The syntax of a Desktop Entry file is defined at
https://standards.freedesktop.org/desktop-entry-spec/latest/index.html. */
desktop_reader_ty *
desktop_reader_alloc (desktop_reader_class_ty *method_table)
{
desktop_reader_ty *reader;
reader = (desktop_reader_ty *) xmalloc (method_table->size);
reader->methods = method_table;
if (method_table->constructor)
method_table->constructor (reader);
return reader;
}
void
desktop_reader_free (desktop_reader_ty *reader)
{
if (reader->methods->destructor)
reader->methods->destructor (reader);
free (reader);
}
void
desktop_reader_handle_group (desktop_reader_ty *reader, const char *group)
{
if (reader->methods->handle_group)
reader->methods->handle_group (reader, group);
}
void
desktop_reader_handle_pair (desktop_reader_ty *reader,
lex_pos_ty *key_pos,
const char *key,
const char *locale,
const char *value)
{
if (reader->methods->handle_pair)
reader->methods->handle_pair (reader, key_pos, key, locale, value);
}
void
desktop_reader_handle_comment (desktop_reader_ty *reader, const char *s)
{
if (reader->methods->handle_comment)
reader->methods->handle_comment (reader, s);
}
void
desktop_reader_handle_blank (desktop_reader_ty *reader, const char *s)
{
if (reader->methods->handle_blank)
reader->methods->handle_blank (reader, s);
}
/* Real filename, used in error messages about the input file. */
static const char *real_file_name;
/* File name and line number. */
extern lex_pos_ty gram_pos;
/* The input file stream. */
static FILE *fp;
static int
phase1_getc ()
{
int c;
c = getc (fp);
if (c == EOF)
{
if (ferror (fp))
{
const char *errno_description = strerror (errno);
po_xerror (PO_SEVERITY_FATAL_ERROR, NULL, NULL, 0, 0, false,
xasprintf ("%s: %s",
xasprintf (_("error while reading \"%s\""),
real_file_name),
errno_description));
}
return EOF;
}
return c;
}
static inline void
phase1_ungetc (int c)
{
if (c != EOF)
ungetc (c, fp);
}
static unsigned char phase2_pushback[2];
static int phase2_pushback_length;
static int
phase2_getc ()
{
int c;
if (phase2_pushback_length)
c = phase2_pushback[--phase2_pushback_length];
else
{
c = phase1_getc ();
if (c == '\r')
{
int c2 = phase1_getc ();
if (c2 == '\n')
c = c2;
else
phase1_ungetc (c2);
}
}
if (c == '\n')
gram_pos.line_number++;
return c;
}
static void
phase2_ungetc (int c)
{
if (c == '\n')
--gram_pos.line_number;
if (c != EOF)
phase2_pushback[phase2_pushback_length++] = c;
}
enum token_type_ty
{
token_type_eof,
token_type_group,
token_type_pair,
/* Unlike other scanners, preserve comments and blank lines for
merging translations back into a desktop file, with msgfmt. */
token_type_comment,
token_type_blank,
token_type_other
};
typedef enum token_type_ty token_type_ty;
typedef struct token_ty token_ty;
struct token_ty
{
token_type_ty type;
char *string;
const char *value;
const char *locale;
};
/* Free the memory pointed to by a 'struct token_ty'. */
static inline void
free_token (token_ty *tp)
{
if (tp->type == token_type_group || tp->type == token_type_pair
|| tp->type == token_type_comment || tp->type == token_type_blank)
free (tp->string);
}
static void
desktop_lex (token_ty *tp)
{
static char *buffer;
static size_t bufmax;
size_t bufpos;
#undef APPEND
#define APPEND(c) \
do \
{ \
if (bufpos >= bufmax) \
{ \
bufmax += 100; \
buffer = xrealloc (buffer, bufmax); \
} \
buffer[bufpos++] = c; \
} \
while (0)
bufpos = 0;
for (;;)
{
int c;
c = phase2_getc ();
switch (c)
{
case EOF:
tp->type = token_type_eof;
return;
case '[':
{
bool non_blank = false;
for (;;)
{
c = phase2_getc ();
if (c == EOF || c == ']')
break;
if (c == '\n')
{
po_xerror (PO_SEVERITY_WARNING, NULL,
real_file_name, gram_pos.line_number, 0, false,
_("unterminated group name"));
break;
}
/* Group names may contain all ASCII characters
except for '[' and ']' and control characters. */
if (!(c_isascii (c) && c != '[' && !c_iscntrl (c)))
break;
APPEND (c);
}
/* Skip until newline. */
while (c != '\n' && c != EOF)
{
c = phase2_getc ();
if (c == EOF)
break;
if (!c_isspace (c))
non_blank = true;
}
if (non_blank)
po_xerror (PO_SEVERITY_WARNING, NULL,
real_file_name, gram_pos.line_number, 0, false,
_("invalid non-blank character"));
APPEND (0);
tp->type = token_type_group;
tp->string = xstrdup (buffer);
return;
}
case '#':
{
/* Read until newline. */
for (;;)
{
c = phase2_getc ();
if (c == EOF || c == '\n')
break;
APPEND (c);
}
APPEND (0);
tp->type = token_type_comment;
tp->string = xstrdup (buffer);
return;
}
case 'A': case 'B': case 'C': case 'D': case 'E': case 'F':
case 'G': case 'H': case 'I': case 'J': 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 '-':
case 'a': case 'b': case 'c': case 'd': case 'e': case 'f':
case 'g': case 'h': case 'i': case 'j': 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 '0': case '1': case '2': case '3': case '4':
case '5': case '6': case '7': case '8': case '9':
{
size_t locale_start;
bool found_locale = false;
size_t value_start;
for (;;)
{
APPEND (c);
c = phase2_getc ();
switch (c)
{
case 'A': case 'B': case 'C': case 'D': case 'E': case 'F':
case 'G': case 'H': case 'I': case 'J': 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 '-':
case 'a': case 'b': case 'c': case 'd': case 'e': case 'f':
case 'g': case 'h': case 'i': case 'j': 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 '0': case '1': case '2': case '3': case '4':
case '5': case '6': case '7': case '8': case '9':
continue;
case '[':
/* Finish the key part and start the locale part. */
APPEND (0);
found_locale = true;
locale_start = bufpos;
for (;;)
{
int c2 = phase2_getc ();
if (c2 == EOF || c2 == ']')
break;
APPEND (c2);
}
break;
default:
phase2_ungetc (c);
break;
}
break;
}
APPEND (0);
/* Skip any space before '='. */
for (;;)
{
c = phase2_getc ();
switch (c)
{
case ' ':
continue;
default:
phase2_ungetc (c);
break;
case EOF: case '\n':
break;
}
break;
}
c = phase2_getc ();
if (c != '=')
{
po_xerror (PO_SEVERITY_WARNING, NULL,
real_file_name, gram_pos.line_number, 0, false,
xasprintf (_("missing '=' after \"%s\""), buffer));
for (;;)
{
c = phase2_getc ();
if (c == EOF || c == '\n')
break;
}
tp->type = token_type_other;
return;
}
/* Skip any space after '='. */
for (;;)
{
c = phase2_getc ();
switch (c)
{
case ' ':
continue;
default:
phase2_ungetc (c);
break;
case EOF:
break;
}
break;
}
value_start = bufpos;
for (;;)
{
c = phase2_getc ();
if (c == EOF || c == '\n')
break;
APPEND (c);
}
APPEND (0);
tp->type = token_type_pair;
tp->string = xmemdup (buffer, bufpos);
tp->locale = found_locale ? &buffer[locale_start] : NULL;
tp->value = &buffer[value_start];
return;
}
default:
{
bool non_blank = false;
for (;;)
{
if (c == '\n' || c == EOF)
break;
if (!c_isspace (c))
non_blank = true;
else
APPEND (c);
c = phase2_getc ();
}
if (non_blank)
{
po_xerror (PO_SEVERITY_WARNING, NULL,
real_file_name, gram_pos.line_number, 0, false,
_("invalid non-blank line"));
tp->type = token_type_other;
return;
}
APPEND (0);
tp->type = token_type_blank;
tp->string = xstrdup (buffer);
return;
}
}
}
#undef APPEND
}
void
desktop_parse (desktop_reader_ty *reader, FILE *file,
const char *real_filename, const char *logical_filename)
{
fp = file;
real_file_name = real_filename;
gram_pos.file_name = xstrdup (logical_filename);
gram_pos.line_number = 1;
for (;;)
{
struct token_ty token;
desktop_lex (&token);
switch (token.type)
{
case token_type_eof:
goto out;
case token_type_group:
desktop_reader_handle_group (reader, token.string);
break;
case token_type_comment:
desktop_reader_handle_comment (reader, token.string);
break;
case token_type_pair:
desktop_reader_handle_pair (reader, &gram_pos,
token.string, token.locale, token.value);
break;
case token_type_blank:
desktop_reader_handle_blank (reader, token.string);
break;
case token_type_other:
break;
}
free_token (&token);
}
out:
fp = NULL;
real_file_name = NULL;
gram_pos.line_number = 0;
}
char *
desktop_escape_string (const char *s, bool is_list)
{
char *buffer, *p;
p = buffer = XNMALLOC (strlen (s) * 2 + 1, char);
/* The first character must not be a whitespace. */
if (*s == ' ')
{
p = stpcpy (p, "\\s");
s++;
}
else if (*s == '\t')
{
p = stpcpy (p, "\\t");
s++;
}
for (;; s++)
{
if (*s == '\0')
{
*p = '\0';
break;
}
switch (*s)
{
case '\n':
p = stpcpy (p, "\\n");
break;
case '\r':
p = stpcpy (p, "\\r");
break;
case '\\':
if (is_list && *(s + 1) == ';')
{
p = stpcpy (p, "\\;");
s++;
}
else
p = stpcpy (p, "\\\\");
break;
default:
*p++ = *s;
break;
}
}
return buffer;
}
char *
desktop_unescape_string (const char *s, bool is_list)
{
char *buffer, *p;
p = buffer = XNMALLOC (strlen (s) + 1, char);
for (;; s++)
{
if (*s == '\0')
{
*p = '\0';
break;
}
if (*s == '\\')
{
s++;
if (*s == '\0')
{
*p = '\0';
break;
}
switch (*s)
{
case 's':
*p++ = ' ';
break;
case 'n':
*p++ = '\n';
break;
case 't':
*p++ = '\t';
break;
case 'r':
*p++ = '\r';
break;
case ';':
p = stpcpy (p, "\\;");
break;
default:
*p++ = *s;
break;
}
}
else
*p++ = *s;
}
return buffer;
}
void
desktop_add_keyword (hash_table *keywords, const char *name, bool is_list)
{
hash_insert_entry (keywords, name, strlen (name), (void *) is_list);
}
void
desktop_add_default_keywords (hash_table *keywords)
{
/* When adding new keywords here, also update the documentation in
xgettext.texi! */
desktop_add_keyword (keywords, "Name", false);
desktop_add_keyword (keywords, "GenericName", false);
desktop_add_keyword (keywords, "Comment", false);
#if 0 /* Icon values are localizable, but not supported by xgettext. */
desktop_add_keyword (keywords, "Icon", false);
#endif
desktop_add_keyword (keywords, "Keywords", true);
}