diff options
-rw-r--r-- | .gitlab-ci.yml | 2 | ||||
-rw-r--r-- | NEWS | 5 | ||||
-rw-r--r-- | doc/Doxyfile.in | 2 | ||||
-rw-r--r-- | include/zix/environment.h | 39 | ||||
-rw-r--r-- | include/zix/zix.h | 10 | ||||
-rw-r--r-- | meson.build | 5 | ||||
-rw-r--r-- | src/posix/environment_posix.c | 134 | ||||
-rw-r--r-- | src/win32/environment_win32.c | 22 | ||||
-rw-r--r-- | test/.clang-tidy | 1 | ||||
-rw-r--r-- | test/meson.build | 1 | ||||
-rw-r--r-- | test/test_environment.c | 140 |
11 files changed, 356 insertions, 5 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index ebfdd1a..081c7b6 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -124,7 +124,7 @@ freebsd: stage: build tags: [freebsd,meson] script: - - meson setup build -Dbuildtype=debug -Dwarning_level=everything -Dwerror=true -Ddocs=disabled + - meson setup build -Dbuildtype=debug -Dwarning_level=everything -Dwerror=true -Ddocs=disabled -Db_lundef=false - ninja -C build test - meson configure -Dbuildtype=release build - ninja -C build test @@ -1,6 +1,7 @@ -zix (0.5.0) unstable; urgency=medium +zix (0.5.1) unstable; urgency=medium * Add ZIX_NODISCARD attribute + * Add zix_expand_environment_strings() * Add zix_string_view_equals() * Avoid fdatasync() on Darwin * Fix build on POSIX systems without PATH_MAX defined @@ -9,7 +10,7 @@ zix (0.5.0) unstable; urgency=medium * Fix ring unit test when mlock() is unavailable * Improve documentation - -- David Robillard <d@drobilla.net> Fri, 15 Nov 2024 19:18:43 +0000 + -- David Robillard <d@drobilla.net> Sun, 24 Nov 2024 15:48:27 +0000 zix (0.4.2) stable; urgency=medium diff --git a/doc/Doxyfile.in b/doc/Doxyfile.in index 741fe1a..0c60cab 100644 --- a/doc/Doxyfile.in +++ b/doc/Doxyfile.in @@ -68,5 +68,7 @@ INPUT = @ZIX_SRCDIR@/include/zix/zix.h \ \ @ZIX_SRCDIR@/include/zix/filesystem.h \ @ZIX_SRCDIR@/include/zix/path.h \ + \ + @ZIX_SRCDIR@/include/zix/environment.h \ OUTPUT_DIRECTORY = @DOX_OUTPUT@ diff --git a/include/zix/environment.h b/include/zix/environment.h new file mode 100644 index 0000000..fbbcd0d --- /dev/null +++ b/include/zix/environment.h @@ -0,0 +1,39 @@ +// Copyright 2024 David Robillard <d@drobilla.net> +// SPDX-License-Identifier: ISC + +#ifndef ZIX_ENVIRONMENT_H +#define ZIX_ENVIRONMENT_H + +#include <zix/allocator.h> +#include <zix/attributes.h> + +ZIX_BEGIN_DECLS + +/** + @defgroup zix_expand Variable Expansion + @ingroup zix_environment + @{ +*/ + +/** + Expand shell-style variables in a string. + + On Windows, this expands environment variable references like + "%USERPROFILE%". On POSIX systems, it expands environment variable + references like "$HOME", and the special path component "~". + + @param allocator Allocator used for returned string. + @param string Input string to expand. + @return A newly allocated copy of `string` with variables expanded, or null. +*/ +ZIX_API char* ZIX_ALLOCATED +zix_expand_environment_strings(ZixAllocator* ZIX_NULLABLE allocator, + const char* ZIX_NONNULL string); + +/** + @} +*/ + +ZIX_END_DECLS + +#endif /* ZIX_ENVIRONMENT_H */ diff --git a/include/zix/zix.h b/include/zix/zix.h index 4fcf238..ceaadc0 100644 --- a/include/zix/zix.h +++ b/include/zix/zix.h @@ -1,4 +1,4 @@ -// Copyright 2016-2022 David Robillard <d@drobilla.net> +// Copyright 2016-2024 David Robillard <d@drobilla.net> // SPDX-License-Identifier: ISC #ifndef ZIX_ZIX_H @@ -68,6 +68,14 @@ /** @} + @defgroup zix_environment Environment + @{ +*/ + +#include <zix/environment.h> + +/** + @} @} */ diff --git a/meson.build b/meson.build index 55ed750..ad31a87 100644 --- a/meson.build +++ b/meson.build @@ -12,7 +12,7 @@ project( ], license: 'ISC', meson_version: '>= 0.56.0', - version: '0.5.0', + version: '0.5.1', ) zix_src_root = meson.current_source_dir() @@ -305,6 +305,7 @@ c_headers = files( 'include/zix/btree.h', 'include/zix/bump_allocator.h', 'include/zix/digest.h', + 'include/zix/environment.h', 'include/zix/filesystem.h', 'include/zix/hash.h', 'include/zix/path.h', @@ -335,11 +336,13 @@ sources = files( if host_machine.system() == 'windows' sources += files( + 'src/win32/environment_win32.c', 'src/win32/filesystem_win32.c', 'src/win32/system_win32.c', ) else sources += files( + 'src/posix/environment_posix.c', 'src/posix/filesystem_posix.c', 'src/posix/system_posix.c', ) diff --git a/src/posix/environment_posix.c b/src/posix/environment_posix.c new file mode 100644 index 0000000..58b8fd2 --- /dev/null +++ b/src/posix/environment_posix.c @@ -0,0 +1,134 @@ +// Copyright 2012-2024 David Robillard <d@drobilla.net> +// SPDX-License-Identifier: ISC + +#include <zix/environment.h> + +#include <zix/allocator.h> +#include <zix/string_view.h> + +#include <stdbool.h> +#include <string.h> + +// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) +extern char** environ; + +static bool +is_path_delim(const char c) +{ + return c == '/' || c == ':' || c == '\0'; +} + +static bool +is_var_name_char(const char c) +{ + return (c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') || (c == '_'); +} + +static const char* +find_env(const ZixStringView name) +{ + if (environ) { + for (size_t i = 0U; environ[i]; ++i) { + // Find the first character in this entry that doesn't match the name + const char* const entry = environ[i]; + size_t j = 0U; + while (j < name.length && entry[j] == name.data[j]) { + ++j; + } + + if (j == name.length && entry[j] == '=') { + return entry + j + 1U; + } + } + } + + return NULL; +} + +// Append suffix to dst, update dst_len, and return the realloc'd result +static char* +append_str(ZixAllocator* const allocator, + size_t* const dst_len, + char* const dst, + const size_t suffix_len, + const char* const suffix) +{ + const size_t out_len = *dst_len + suffix_len; + char* const out = (char*)zix_realloc(allocator, dst, out_len + 1U); + if (!out) { + zix_free(allocator, dst); + return NULL; + } + + memcpy(out + *dst_len, suffix, suffix_len); + out[(*dst_len += suffix_len)] = '\0'; + return out; +} + +// Append the value of the environment variable var to dst if one is set +static char* +append_var(ZixAllocator* const allocator, + size_t* const dst_len, + char* const dst, + const size_t ref_len, + const char* const ref) +{ + // Get value from environment + const ZixStringView var = zix_substring(ref + 1U, ref_len - 1U); + const char* val = find_env(var); + if (val) { + return append_str(allocator, dst_len, dst, strlen(val), val); + } + + // No value found, append variable reference as-is + return append_str(allocator, dst_len, dst, ref_len, ref); +} + +char* +zix_expand_environment_strings(ZixAllocator* const allocator, + const char* const string) +{ + char* out = NULL; + size_t len = 0U; + + size_t start = 0U; // Start of current chunk to copy + for (size_t s = 0U; string[s];) { + const char c = string[s]; + if (c == '$' && is_var_name_char(string[s + 1U])) { + // Hit $ (variable reference like $VAR_NAME) + for (size_t t = 1U;; ++t) { + if (!is_var_name_char(string[s + t])) { + const char* const prefix = string + start; + const size_t prefix_len = s - start; + if ((prefix_len && + !(out = append_str(allocator, &len, out, prefix_len, prefix))) || + !(out = append_var(allocator, &len, out, t, string + s))) { + return NULL; + } + start = s = t; + break; + } + } + } else if (c == '~' && is_path_delim(string[s + 1U])) { + // Hit ~ before delimiter or end of string (home directory reference) + const char* const prefix = string + start; + const size_t prefix_len = s - start; + if ((prefix_len && + !(out = append_str(allocator, &len, out, prefix_len, prefix))) || + !(out = append_var(allocator, &len, out, 5U, "$HOME"))) { + return NULL; + } + start = ++s; + } else { + ++s; + } + } + + if (string[start]) { + const char* const tail = string + start; + const size_t tail_len = strlen(tail); + out = append_str(allocator, &len, out, tail_len, tail); + } + + return out; +} diff --git a/src/win32/environment_win32.c b/src/win32/environment_win32.c new file mode 100644 index 0000000..e7a0ee3 --- /dev/null +++ b/src/win32/environment_win32.c @@ -0,0 +1,22 @@ +// Copyright 2012-2024 David Robillard <d@drobilla.net> +// SPDX-License-Identifier: ISC + +#include <zix/environment.h> + +#include <windows.h> + +char* +zix_expand_environment_strings(ZixAllocator* const allocator, + const char* const string) +{ + const DWORD size = ExpandEnvironmentStrings(string, NULL, 0U); + if (!size) { + return NULL; + } + + char* const out = (char*)zix_calloc(allocator, 1U, size + 1U); + if (out) { + ExpandEnvironmentStrings(string, out, size + 1U); + } + return out; +} diff --git a/test/.clang-tidy b/test/.clang-tidy index f869db0..401b1b0 100644 --- a/test/.clang-tidy +++ b/test/.clang-tidy @@ -9,6 +9,7 @@ Checks: > -bugprone-multi-level-implicit-pointer-conversion, -cert-err33-c, -clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling, + -concurrency-mt-unsafe, -cppcoreguidelines-avoid-non-const-global-variables, -google-readability-casting, -llvm-header-guard, diff --git a/test/meson.build b/test/meson.build index 6aa4e95..309551f 100644 --- a/test/meson.build +++ b/test/meson.build @@ -73,6 +73,7 @@ sequential_tests = { }, 'filesystem': {'': files('../README.md')}, 'digest': {'': []}, + 'environment': {'': []}, 'hash': {'': []}, 'path': {'': []}, 'status': {'': []}, diff --git a/test/test_environment.c b/test/test_environment.c new file mode 100644 index 0000000..dba7b90 --- /dev/null +++ b/test/test_environment.c @@ -0,0 +1,140 @@ +// Copyright 2012-2024 David Robillard <d@drobilla.net> +// SPDX-License-Identifier: ISC + +#undef NDEBUG + +#include "failing_allocator.h" + +#include <zix/allocator.h> +#include <zix/environment.h> +#include <zix/path.h> + +#include <assert.h> +#include <stdlib.h> +#include <string.h> + +#ifdef _WIN32 + +# define HOME_NAME "USERPROFILE" +# define HOME_VAR "%USERPROFILE%" + +#else + +# define HOME_NAME "HOME" +# define HOME_VAR "$HOME" + +static char* +concat(ZixAllocator* const allocator, + const char* const prefix, + const char* const suffix) +{ + const size_t prefix_len = strlen(prefix); + const size_t suffix_len = strlen(suffix); + const size_t result_len = prefix_len + suffix_len; + + char* const result = (char*)zix_calloc(allocator, 1U, result_len + 1U); + assert(result); + memcpy(result, prefix, prefix_len + 1U); + memcpy(result + prefix_len, suffix, suffix_len + 1U); + return result; +} + +#endif + +static void +check_expansion(const char* const path, const char* const expected) +{ + char* const expanded = zix_expand_environment_strings(NULL, path); + assert(expanded); + assert(!strcmp(expanded, expected)); + zix_free(NULL, expanded); +} + +static void +test_expansion(void) +{ + // Check non-expansion of hopefully unset variables + check_expansion("$ZIX_UNSET0", "$ZIX_UNSET0"); + check_expansion("$ZIX_unset0", "$ZIX_unset0"); + check_expansion("%ZIX_UNSET0%", "%ZIX_UNSET0%"); + check_expansion("%ZIX_unset0%", "%ZIX_unset0%"); + + // Check non-expansion of invalid variable names + check_expansion("$%INVALID", "$%INVALID"); + check_expansion("$<INVALID>", "$<INVALID>"); + check_expansion("$[INVALID]", "$[INVALID]"); + check_expansion("$invalid", "$invalid"); + check_expansion("${INVALID}", "${INVALID}"); + + const char* const home = getenv(HOME_NAME); + if (home) { + char* const var_foo = zix_path_join(NULL, HOME_VAR, "foo"); + char* const home_foo = zix_path_join(NULL, home, "foo"); + + check_expansion(var_foo, home_foo); + +#ifndef _WIN32 + char* const tilde_foo = zix_path_join(NULL, "~", "foo"); + char* const home_and_other = concat(NULL, home, ":/other"); + char* const other_and_home = concat(NULL, "/other:", home); + check_expansion("~other", "~other"); + check_expansion("~", home); + check_expansion("~/foo", home_foo); + check_expansion("~:/other", home_and_other); + check_expansion("/other:~", other_and_home); + check_expansion("$HO", "$HO"); + check_expansion("$HOMEZIX", "$HOMEZIX"); + zix_free(NULL, other_and_home); + zix_free(NULL, home_and_other); + zix_free(NULL, tilde_foo); +#endif + + zix_free(NULL, home_foo); + zix_free(NULL, var_foo); + } +} + +static void +test_failed_alloc(void) +{ + ZixFailingAllocator allocator = zix_failing_allocator(); + + zix_failing_allocator_reset(&allocator, 0U); + assert(!zix_expand_environment_strings(&allocator.base, "/one:~")); + assert(!zix_expand_environment_strings(&allocator.base, "/only")); + assert(!zix_expand_environment_strings(&allocator.base, "~")); + +#ifndef _WIN32 + zix_failing_allocator_reset(&allocator, 1U); + assert(!zix_expand_environment_strings(&allocator.base, "/one:~")); + assert(!zix_expand_environment_strings(&allocator.base, "/one:$HOME/two")); + + zix_failing_allocator_reset(&allocator, 2U); + assert(!zix_expand_environment_strings(&allocator.base, "/one:$HOME/two")); +#endif +} + +#ifndef _WIN32 + +// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) +extern char** environ; + +static void +test_null_environ(void) +{ + environ = NULL; + check_expansion(HOME_VAR, HOME_VAR); +} + +#endif + +int +main(void) +{ + test_expansion(); + test_failed_alloc(); +#ifndef _WIN32 + test_null_environ(); +#endif + return 0; +} |