/*
* Copyright 2021-2023 Patrick Gaskin
*
* 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 2 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 .
*/
#include "pl_env.h"
#include "options.h"
#include "xmalloc.h"
#include
#include
#include
#include
#include
extern char **environ;
static char **pl_env_cache = NULL;
static bool pl_env_contains_delimiter(const char *str)
{
return !!strchr(str, PL_ENV_DELIMITER);
}
/**
* pl_env_norm gets an env var and puts it into the required format for
* pl_env_reduce and pl_env_expand, which does exact string matching/replacement
* against the file paths. It converts backslashes to slashes on Windows,
* removes consecutive slashes, removes './' path segments, simplifies '../'
* path segments (and returns NULL if it would result in going above the
* uppermost directory), and removes the trailing slash. In addition, it trims
* the variable name before looking it up.
*/
static char *pl_env_norm(char *path)
{
#ifdef _WIN32
/* convert backslashes to slashes */
/* note: cmus uses forward slashes internally, but Windows accepts both */
/* even though they will get normalized on windows, we need paths to match exactly */
for (char *p = path; *p; p++)
*p = *p == '\\' ? '/' : *p;
#endif
/* canonicalize the path in-place */
size_t r = 1, w = 1;
while (path[r]) {
/* handle the start of a segment */
if (w == 0 || path[w-1] == '/') {
/* handle empty segments */
if (path[r] == '/') {
/* skip the duplicate slashes */
while (path[r] == '/') r++;
continue;
}
/* handle '.' segments */
if (path[r] == '.' && (path[r+1] == '/' || !path[r+1])) {
/* skip them */
if (path[r += 1]) r++;
continue;
}
/* handle '..' segments */
if (path[r] == '.' && path[r+1] == '.' && (path[r+2] == '/' || !path[r+2])) {
/* if there aren't any parent directories left to skip, return NULL */
if (!w) return NULL;
/* remove the previous segment up to the '/' */
for (w--; w && path[w-1] != '/'; ) w--;
/* skip the '..' */
if (path[r += 2]) r++;
continue;
}
}
/* write the next character */
path[w++] = path[r++];
}
/* remove the trailing slash if the path isn't / */
if (w >= 2 && path[w-1] == '/') {
w--;
}
/* terminate the path */
path[w] = '\0';
return path;
}
/**
* pl_env_get is like getenv, but it allows using non-null-terminated variable
* names, trims the variable name, ensures the environment variable doesn't
* contain the marker used by pl_env, and normalizes the paths in environment
* variables with pl_env_norm.
*/
static const char *pl_env_get(const char *var, int var_len)
{
if (!var)
return NULL;
size_t vl = var_len == -1
? strlen(var)
: var_len;
const char *vs = var;
const char *ve = var + vl;
while (vs < ve && isspace(*vs)) vs++;
while (ve > vs && isspace(*(ve-1))) ve--;
vl = ve-vs;
if (!vl)
return NULL;
for (const char *c = vs; c < ve; c++)
if (*c == PL_ENV_DELIMITER || *c == '=')
return NULL;
for (char **x = pl_env_cache; x && *x; x++)
if (strncmp(*x, vs, vl) == 0 && (*x)[vl] == '=')
return *x + vl + 1;
return NULL;
}
void pl_env_init(void)
{
for (char **x = pl_env_cache; x && *x; x++)
free(*x);
free(pl_env_cache);
size_t n = 0;
for (char **x = environ; *x; x++)
n++;
char **new = pl_env_cache = xnew(char*, n+1);
for (char **x = environ; *x; x++)
if (!pl_env_contains_delimiter(*x))
if (!pl_env_norm(strchr((*new++ = xstrdup(*x)), '=') + 1))
free(*--new);
*new = NULL;
}
char *pl_env_reduce(const char *path)
{
if (!pl_env_vars || !*pl_env_vars || pl_env_var(path, NULL))
return xstrdup(path);
for (char **var = pl_env_vars; *var && **var; var++) {
const char *val = pl_env_get(*var, -1);
if (!val)
continue;
size_t val_len = strlen(val);
#ifdef _WIN32
if (strncasecmp(path, val, val_len) != 0)
continue;
#else
if (strncmp(path, val, val_len) != 0)
continue;
#endif
const char *rem = path + val_len;
/* always keep the slash at the beginning of the path, and only
use the env var if it replaces an entire path component (i.e.
it is a directory) */
if (*rem != '/')
continue;
size_t var_len = strlen(*var);
size_t rem_len = strlen(rem);
char *new, *ptr;
new = ptr = xmalloc(1+var_len+1+rem_len+1);
*ptr++ = PL_ENV_DELIMITER;
memcpy(ptr, *var, var_len);
ptr += var_len;
*ptr++ = PL_ENV_DELIMITER;
strcpy(ptr, rem);
ptr += rem_len;
*ptr = '\0';
return new;
}
return xstrdup(path);
}
char *pl_env_expand(const char *path)
{
if (!path)
return NULL;
int len;
const char *var;
if (!(var = pl_env_var(path, &len)))
return xstrdup(path);
const char *val = pl_env_get(var, len);
if (!val)
return xstrdup(path);
const char *rem = pl_env_var_remainder(path, len);
size_t val_len = strlen(val);
size_t rem_len = strlen(rem);
char *new, *ptr;
new = ptr = xmalloc(val_len+rem_len+1);
strcpy(ptr, val);
ptr += val_len;
strcpy(ptr, rem);
ptr += rem_len;
*ptr = '\0';
return new;
}
const char *pl_env_var(const char *path, int *out_length)
{
const char *end;
if (!path || *path++ != PL_ENV_DELIMITER)
return NULL;
if (!(end = strrchr(path, PL_ENV_DELIMITER)) || path == end)
return NULL;
if (out_length)
*out_length = (int) (end-path);
return path;
}
const char *pl_env_var_remainder(const char *path, int length)
{
return path+length+2;
}
int pl_env_var_len(const char *path)
{
int len;
return pl_env_var(path, &len) ? len : 0;
}