1
0
Fork 0
xdg-user-dirs/xdg-user-dirs-update.c
Daniel Baumann 8628cbbff3
Adding upstream version 0.18.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
2025-06-22 22:55:16 +02:00

1152 lines
21 KiB
C

#include <config.h>
#define _GNU_SOURCE
#include <sys/types.h>
#include <sys/stat.h>
#include <libintl.h>
#include <locale.h>
#include <pwd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <stdarg.h>
#include <errno.h>
#include <iconv.h>
#include <langinfo.h>
typedef struct {
char *name;
char *path;
} Directory;
Directory backwards_compat_dirs[] = {
{ "DESKTOP", "Desktop" },
{ "TEMPLATES", "Templates" },
{ "PUBLICSHARE", "Public" },
{ NULL}
};
static Directory *default_dirs = NULL;
static Directory *user_dirs = NULL;
static int user_dirs_changed = 0;
/* Config: */
static int enabled = 1;
static char *filename_encoding = NULL; /* NULL => utf8 */
/* Args */
static char *dummy_file = NULL;
static iconv_t filename_converter = (iconv_t)(-1);
static char *
concat_strings (const char *first, ...)
{
char *res;
const char *next;
va_list va;
int len;
res = strdup (first);
len = strlen (res) + 1;
va_start (va, first);
while ( (next = va_arg(va, const char *)) != NULL)
{
res = realloc (res, len + strlen (next) + 1);
strcat (res, next);
}
va_end(va);
return res;
}
static char *
strdup_end (const char *start, const char *end)
{
char *res;
res = malloc (end - start + 1);
memcpy (res, start, end - start);
res[end-start] = 0;
return res;
}
static int
has_prefix (const char *str, const char *prefix)
{
return strncmp (str, prefix, strlen (prefix)) == 0;
}
static char
ascii_toupper (char c)
{
if (c >= 'a' && c <= 'z')
c = (c - 'a') + 'A';
return c;
}
static void
ascii_str_toupper (char *c)
{
while (*c)
{
*c = ascii_toupper (*c);
c++;
}
}
static int
is_space (char c)
{
return c == ' ' || c == '\t';
}
static void
remove_trailing_whitespace (char *s)
{
int len;
len = strlen (s);
while (len > 0 && is_space (s[len-1]))
{
s[len-1] = 0;
len--;
}
}
static int
is_regular_file (char *path)
{
struct stat statbuf;
if (stat (path, &statbuf) == -1)
return 0;
return S_ISREG(statbuf.st_mode);
}
static int
is_directory (char *path)
{
struct stat statbuf;
if (stat (path, &statbuf) == -1)
return 0;
return S_ISDIR(statbuf.st_mode);
}
static int
mkdir_all (char *path)
{
char *p;
int revert;
int result;
path = strdup (path);
result = 1;
p = path;
while (*p)
{
/* skip initial slashes */
while (*p == '/')
p++;
while (*p && *p != '/')
p++;
revert = 0;
if (*p == '/')
{
*p = 0;
revert = 1;
}
if ((mkdir (path, 0755) == -1) &&
(errno != EEXIST))
{
result = 0;
break;
}
if (revert)
*p = '/';
}
free (path);
return result;
}
static char *
shell_unescape (char *escaped)
{
char *unescaped;
char *d;
unescaped = malloc (strlen (escaped) + 1);
d = unescaped;
while (*escaped)
{
if (*escaped == '\\' && *(escaped + 1) != 0)
escaped++;
*d++ = *escaped++;
}
*d = 0;
return unescaped;
}
static char *
shell_escape (char *unescaped)
{
char *escaped;
char *d;
escaped = malloc (strlen (unescaped) * 2 + 1);
d = escaped;
while (*unescaped)
{
if (*unescaped == '$' ||
*unescaped == '`' ||
*unescaped == '\\')
*d++ = '\\';
*d++ = *unescaped++;
}
*d = 0;
return escaped;
}
static char *
filename_from_utf8 (const char *utf8_path)
{
size_t res, len;
const char *in;
char *out, *outp;
size_t in_left, out_left, outbuf_size;
int done;
if (filename_converter == (iconv_t)(-1))
return strdup (utf8_path);
len = strlen (utf8_path);
outbuf_size = len + 1;
done = 0;
do
{
in = utf8_path;
in_left = len;
out = malloc (outbuf_size);
out_left = outbuf_size - 1;
outp = out;
res = iconv (filename_converter,
(ICONV_CONST char **)&in, &in_left,
&outp, &out_left);
if (res == (size_t)(-1) && errno == E2BIG)
{
free (out);
outbuf_size *= 2;
}
else
done = 1;
}
while (!done);
if (res == (size_t)(-1))
{
free (out);
return NULL;
}
/* zero terminate */
*outp = 0;
return out;
}
static char *
get_home_dir (void)
{
struct passwd *pw;
static char *home_dir = NULL;
const char *home_env;
if (home_dir != NULL)
return home_dir;
home_env = getenv ("HOME");
if (home_env && *home_env)
{
home_dir = strdup (home_env);
}
else
{
setpwent ();
pw = getpwuid (getuid ());
endpwent ();
if (pw && pw->pw_dir)
home_dir = strdup (pw->pw_dir);
}
return home_dir;
}
static char *
get_user_config_file (const char *filename)
{
char *config_home, *file;
int free_config_home;
config_home = getenv ("XDG_CONFIG_HOME");
free_config_home = 0;
if (config_home == NULL || config_home[0] == 0)
{
config_home = concat_strings (get_home_dir (), "/.config", NULL);
free_config_home = 1;
}
file = concat_strings (config_home, "/", filename, NULL);
if (free_config_home)
free (config_home);
return file;
}
static void
freev (char **strs)
{
int i;
if (strs)
{
for (i = 0; strs[i] != NULL; i++)
free (strs[i]);
free (strs);
}
}
static char **
parse_colon_separated_dirs (const char *dirs)
{
int numfiles;
char **paths;
const char *p;
numfiles = 0;
paths = malloc (sizeof (char *));
paths[0] = NULL;
p = dirs;
while (p != NULL && *p != 0)
{
int len;
const char *path;
char *colon;
path = p;
colon = strchr (path, ':');
if (colon)
{
len = colon - p;
p = colon + 1;
}
else
{
len = strlen (p);
p = NULL;
}
paths = realloc (paths, sizeof (char *) * (numfiles + 2));
paths[numfiles++] = strndup (path, len);
paths[numfiles] = NULL;
}
return paths;
}
static char **
get_config_files (char *filename)
{
int i;
int numfiles;
char *config_dirs;
char *file;
char **paths, **config_paths;
numfiles = 0;
paths = malloc (sizeof (char *));
paths[0] = NULL;
file = get_user_config_file (filename);
if (file)
{
if (is_regular_file (file))
{
paths = realloc (paths, sizeof (char *) * (numfiles + 2));
paths[numfiles++] = file;
paths[numfiles] = NULL;
}
else
free (file);
}
config_dirs = getenv ("XDG_CONFIG_DIRS");
if (config_dirs)
config_paths = parse_colon_separated_dirs (config_dirs);
else
config_paths = parse_colon_separated_dirs (XDGCONFDIR);
for (i = 0; config_paths[i] != NULL; i++)
{
file = concat_strings (config_paths[i], "/", filename, NULL);
if (is_regular_file (file))
{
paths = realloc (paths, sizeof (char *) * (numfiles + 2));
paths[numfiles++] = file;
paths[numfiles] = NULL;
}
else
free (file);
free (config_paths[i]);
}
free (config_paths);
return paths;
}
static Directory *
add_directory (Directory *dirs, Directory *dir)
{
Directory *new_dirs;
int i;
if (dirs == NULL)
{
new_dirs = malloc (sizeof (Directory) * 2);
new_dirs[0] = *dir;
new_dirs[1].name = NULL;
}
else
{
for (i = 0; dirs[i].name != NULL; i++)
;
new_dirs = realloc (dirs, (i + 2) * sizeof (Directory));
new_dirs[i] = *dir;
new_dirs[i+1].name = NULL;
}
return new_dirs;
}
static int
is_true (const char *str)
{
while (is_space (*str))
str++;
if (*str == '1' ||
has_prefix (str, "True") ||
has_prefix (str, "true"))
return 1;
return 0;
}
static void
load_config (char *path)
{
FILE *file;
char buffer[512];
char *p;
int len;
file = fopen (path, "r");
if (file == NULL)
return;
while (fgets (buffer, sizeof (buffer), file))
{
/* Remove newline at end */
len = strlen (buffer);
if (len > 0 && buffer[len-1] == '\n')
buffer[len-1] = 0;
p = buffer;
/* Skip whitespace */
while (is_space (*p))
p++;
if (*p == '#')
continue;
remove_trailing_whitespace (p);
if (has_prefix (p, "enabled="))
{
p += strlen ("enabled=");
enabled = is_true (p);
}
if (has_prefix (p, "filename_encoding="))
{
p += strlen ("filename_encoding=");
while (is_space (*p))
p++;
ascii_str_toupper (p);
remove_trailing_whitespace (p);
if (filename_encoding)
free (filename_encoding);
if (strcmp (p, "UTF8") == 0 ||
strcmp (p, "UTF-8") == 0)
filename_encoding = NULL;
else if (strcmp (p, "LOCALE") == 0)
filename_encoding = strdup (nl_langinfo (CODESET));
else
filename_encoding = strdup (p);
}
}
fclose (file);
}
static void
load_all_configs (void)
{
char **paths;
int i;
paths = get_config_files ("user-dirs.conf");
/* Load config files in reverse */
for (i = 0; paths[i] != NULL; i++)
;
while (--i >= 0)
load_config (paths[i]);
freev (paths);
}
static void
load_default_dirs (void)
{
FILE *file;
char buffer[512];
char *p;
char *key, *key_end, *value;
int len;
Directory dir;
char **paths;
paths = get_config_files ("user-dirs.defaults");
if (paths[0] == NULL)
{
fprintf (stderr, "No default user directories\n");
exit (1);
}
file = fopen (paths[0], "r");
if (file == NULL)
{
fprintf (stderr, "Can't open %s\n", paths[0]);
exit (1);
}
while (fgets (buffer, sizeof (buffer), file))
{
/* Remove newline at end */
len = strlen (buffer);
if (len > 0 && buffer[len-1] == '\n')
buffer[len-1] = 0;
p = buffer;
/* Skip whitespace */
while (is_space (*p))
p++;
if (*p == '#')
continue;
key = p;
while (*p && !is_space (*p) && * p != '=')
p++;
key_end = p;
while (is_space (*p))
p++;
if (*p == '=')
p++;
while (is_space (*p))
p++;
value = p;
*key_end = 0;
if (*key == 0 || *value == 0)
continue;
dir.name = strdup (key);
dir.path = strdup (value);
default_dirs = add_directory (default_dirs, &dir);
}
fclose (file);
freev (paths);
}
static void
load_user_dirs (void)
{
FILE *file;
char buffer[512];
char *p;
char *key, *key_end, *value, *value_end;
int len;
Directory dir;
char *user_config_file;
user_config_file = get_user_config_file ("user-dirs.dirs");
file = fopen (user_config_file, "r");
free (user_config_file);
if (file == NULL)
return;
while (fgets (buffer, sizeof (buffer), file))
{
/* Remove newline at end */
len = strlen (buffer);
if (len > 0 && buffer[len-1] == '\n')
buffer[len-1] = 0;
p = buffer;
/* Skip whitespace */
while (is_space (*p))
p++;
/* Skip comment lines */
if (*p == '#')
continue;
if (!has_prefix(p, "XDG_"))
continue;
p += 4;
key = p;
while (*p && !is_space (*p) && * p != '=')
p++;
if (*p == 0)
continue;
key_end = p - 4;
if (key_end <= key ||
!has_prefix (key_end, "_DIR"))
continue;
if (*p == '=')
p++;
while (is_space (*p))
p++;
if (*p++ != '"')
continue;
if (has_prefix (p, "$HOME"))
{
p += 5;
if (*p == '/')
p++;
else if (*p != '"' && *p != 0)
continue; /* Not ending after $HOME, nor followed by slash. Ignore */
}
else if (*p != '/')
continue;
value = p;
while (*p)
{
if (*p == '"')
break;
if (*p == '\\' && *(p+1) != 0)
p++;
p++;
}
value_end = p;
*key_end = 0;
*value_end = 0;
if (*key == 0)
continue;
dir.name = strdup (key);
dir.path = shell_unescape (value);
user_dirs = add_directory (user_dirs, &dir);
}
fclose (file);
}
static void
save_locale (void)
{
FILE *file;
char *user_locale_file;
char *locale, *dot;
user_locale_file = get_user_config_file ("user-dirs.locale");
file = fopen (user_locale_file, "w");
free (user_locale_file);
if (file == NULL)
{
fprintf (stderr, "Can't save user-dirs.locale\n");
return;
}
locale = strdup (setlocale (LC_MESSAGES, NULL));
/* Skip encoding part */
dot = strchr (locale, '.');
if (dot)
*dot = 0;
fprintf (file, "%s", locale);
free (locale);
fclose (file);
}
static int
save_user_dirs (void)
{
FILE *file;
char *user_config_file;
char *tmp_file;
int i;
char *escaped;
int tmp_fd;
int res;
char *dir, *slash;
struct stat stat_buf;
res = 1;
tmp_file = NULL;
if (dummy_file)
user_config_file = strdup (dummy_file);
else
user_config_file = get_user_config_file ("user-dirs.dirs");
dir = strdup (user_config_file);
slash = strrchr (dir, '/');
if (slash)
*slash = 0;
if (stat (dir, &stat_buf) == -1 && errno == ENOENT)
{
if (mkdir (dir, 0700) == -1)
{
free (dir);
fprintf (stderr, "Can't save user-dirs.dirs, failed to create directory\n");
res = 0;
goto out;
}
}
free (dir);
tmp_file = malloc (strlen (user_config_file) + 6 + 1);
strcpy (tmp_file, user_config_file);
strcat (tmp_file, "XXXXXX");
tmp_fd = mkstemp (tmp_file);
if (tmp_fd == -1)
{
fprintf (stderr, "Can't save user-dirs.dirs\n");
res = 0;
goto out;
}
file = fdopen (tmp_fd, "w");
if (file == NULL)
{
unlink (tmp_file);
fprintf (stderr, "Can't save user-dirs.dirs\n");
res = 0;
goto out;
}
fprintf (file, "# This file is written by xdg-user-dirs-update\n");
fprintf (file, "# If you want to change or add directories, just edit the line you're\n");
fprintf (file, "# interested in. All local changes will be retained on the next run.\n");
fprintf (file, "# Format is XDG_xxx_DIR=\"$HOME/yyy\", where yyy is a shell-escaped\n");
fprintf (file, "# homedir-relative path, or XDG_xxx_DIR=\"/yyy\", where /yyy is an\n");
fprintf (file, "# absolute path. No other format is supported.\n");
fprintf (file, "# \n");
if (user_dirs)
{
for (i = 0; user_dirs[i].name != NULL; i++)
{
escaped = shell_escape (user_dirs[i].path);
fprintf (file, "XDG_%s_DIR=\"%s%s\"\n",
user_dirs[i].name,
(*escaped == '/')?"":"$HOME/",
escaped);
free (escaped);
}
}
fclose (file);
if (rename (tmp_file, user_config_file) == -1)
{
unlink (tmp_file);
fprintf (stderr, "Can't save user-dirs.dirs\n");
res = 0;
}
out:
if (tmp_file)
free (tmp_file);
free (user_config_file);
return res;
}
static char *
localize_path_name (const char *path)
{
char *res;
const char *element, *element_end;
char *element_copy;
char *translated;
int has_slash;
res = strdup ("");
while (*path)
{
has_slash = 0;
while (*path == '/')
{
path++;
has_slash = 1;
}
element = path;
while (*path && *path != '/')
path++;
element_end = path;
element_copy = strdup_end (element, element_end);
translated = gettext (element_copy);
res = realloc (res, strlen (res) + 1 + strlen (translated) + 1);
if (has_slash)
strcat (res, "/");
strcat (res, translated);
free (element_copy);
}
return res;
}
static Directory *
lookup_backwards_compat (Directory *dir)
{
int i;
for (i = 0; backwards_compat_dirs[i].name != NULL; i++)
{
if (strcmp (dir->name, backwards_compat_dirs[i].name) == 0)
return &backwards_compat_dirs[i];
}
return NULL;
}
static Directory *
find_dir (Directory *dirs, const char *name)
{
int i;
if (dirs == NULL)
return NULL;
for (i = 0; dirs[i].name != NULL; i++)
{
if (strcmp (dirs[i].name, name) == 0)
return &dirs[i];
}
return NULL;
}
static void
create_dirs (int force)
{
int i;
Directory dir;
Directory *user_dir, *default_dir, *compat_dir;
char *path_name, *relative_path_name, *translated_name;
if (default_dirs == NULL)
return;
for (i = 0; default_dirs[i].name != NULL; i++)
{
default_dir = &default_dirs[i];
user_dir = NULL;
user_dir = find_dir (user_dirs, default_dir->name);
if (user_dir && !force)
{
if (user_dir->path[0] == '/')
path_name = strdup (user_dir->path);
else
path_name = concat_strings (get_home_dir (), "/", user_dir->path, NULL);
if (!is_directory (path_name))
{
fprintf (stderr, "%s was removed, reassigning %s to homedir\n",
path_name, user_dir->name);
free (user_dir->path);
user_dir->path = strdup ("");
user_dirs_changed = 1;
}
free (path_name);
continue;
}
path_name = NULL;
relative_path_name = NULL;
if (user_dir == NULL && !force)
{
/* New default dir. Check if its an old named dir. We want to
reuse that if it exists. */
compat_dir = lookup_backwards_compat (default_dir);
if (compat_dir)
{
path_name = concat_strings (get_home_dir (), "/", compat_dir->path, NULL);
if (!is_directory (path_name))
{
free (path_name);
path_name = NULL;
}
else
relative_path_name = strdup (compat_dir->path);
}
}
if (path_name == NULL)
{
translated_name = localize_path_name (default_dir->path);
relative_path_name = filename_from_utf8 (translated_name);
if (relative_path_name == NULL)
relative_path_name = strdup (translated_name);
free (translated_name);
if (relative_path_name[0] == '/')
path_name = strdup (relative_path_name); /* default path was absolute, not homedir relative */
else
path_name = concat_strings (get_home_dir (), "/", relative_path_name, NULL);
}
if (user_dir == NULL || strcmp (relative_path_name, user_dir->path) != 0)
{
/* Don't make the directories if we're writing a dummy output file */
if (dummy_file == NULL &&
!mkdir_all (path_name))
{
fprintf (stderr, "Can't create dir %s\n", path_name);
}
else
{
user_dirs_changed = 1;
if (user_dir == NULL)
{
dir.name = strdup (default_dir->name);
dir.path = strdup (relative_path_name);
user_dirs = add_directory (user_dirs, &dir);
}
else
{
/* We forced an update */
fprintf (stdout, "Moving %s directory from %s to %s\n",
default_dir->name, user_dir->path, relative_path_name);
free (user_dir->path);
user_dir->path = strdup (relative_path_name);
}
}
}
free (relative_path_name);
free (path_name);
}
}
int
main (int argc, char *argv[])
{
int force;
int i;
int was_empty;
char *set_dir = NULL;
char *set_value = NULL;
char *locale_dir = NULL;
setlocale (LC_ALL, "");
if (is_directory (LOCALEDIR))
locale_dir = strdup (LOCALEDIR);
else
{
/* In case LOCALEDIR does not exist, e.g. xdg-user-dirs is installed in
* a different location than the one determined at compile time, look
* through the XDG_DATA_DIRS environment variable for alternate locations
* of the locale files */
char *data_dirs = getenv ("XDG_DATA_DIRS");
if (data_dirs)
{
char **data_paths;
data_paths = parse_colon_separated_dirs (data_dirs);
for (i = 0; data_paths[i] != NULL; i++)
{
if (!locale_dir)
{
char *dir = NULL;
dir = concat_strings (data_paths[i], "/", "locale", NULL);
if (is_directory (dir))
locale_dir = dir;
else
free (dir);
}
free (data_paths[i]);
}
free (data_paths);
}
}
bindtextdomain (GETTEXT_PACKAGE, locale_dir);
free (locale_dir);
bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8");
textdomain (GETTEXT_PACKAGE);
force = 0;
for (i = 1; i < argc; i++)
{
if (strcmp (argv[i], "--help") == 0)
{
printf ("Usage: xdg-user-dirs-update [--force] [--dummy-output <path>] [--set DIR path]\n");
exit (0);
}
else if (strcmp (argv[i], "--force") == 0)
force = 1;
else if (strcmp (argv[i], "--dummy-output") == 0 && i + 1 < argc)
dummy_file = argv[++i];
else if (strcmp (argv[i], "--set") == 0 && i + 2 < argc)
{
set_dir = argv[++i];
set_value = argv[++i];
if (*set_value != '/')
{
printf ("directory value must be absolute path (was %s)\n", set_value);
exit (1);
}
}
else
{
printf ("Invalid argument %s\n", argv[i]);
exit (1);
}
}
load_all_configs ();
if (filename_encoding)
{
filename_converter = iconv_open (filename_encoding, "UTF-8");
if (filename_converter == (iconv_t)(-1))
{
fprintf (stderr, "Can't convert from UTF-8 to %s\n", filename_encoding);
return 1;
}
}
if (set_dir != NULL)
{
Directory *dir;
char *path, *home;
/* Set a key */
load_user_dirs ();
home = get_home_dir ();
path = set_value;
if (has_prefix (path, home))
{
path += strlen (home);
while (*path == '/')
path++;
}
dir = find_dir (user_dirs, set_dir);
if (dir != NULL)
{
free (dir->path);
dir->path = strdup (path);
}
else
{
Directory new_dir;
new_dir.name = strdup (set_dir);
new_dir.path = strdup (path);
user_dirs = add_directory (user_dirs, &new_dir);
}
if (!save_user_dirs ())
return 1;
}
else
{
/* default: update */
if (!enabled)
return 0;
load_default_dirs ();
load_user_dirs ();
was_empty = (user_dirs == NULL) || (user_dirs->name == NULL);
create_dirs (force);
if (user_dirs_changed)
{
if (!save_user_dirs ())
return 1;
if ((force || was_empty) && dummy_file == NULL)
save_locale ();
}
}
return 0;
}