From 734cd01da88bb1408351aa91cc1a722efab6f9b1 Mon Sep 17 00:00:00 2001 From: yyc12345 Date: Thu, 14 Aug 2025 20:17:02 +0800 Subject: [PATCH] feat: add Rust env namespace --- src/CMakeLists.txt | 2 + src/yycc/rust/env.cpp | 159 ++++++++++++++++++++++++++++++++++++ src/yycc/rust/env.hpp | 62 ++++++++++++++ testbench/CMakeLists.txt | 1 + testbench/yycc/rust/env.cpp | 45 ++++++++++ 5 files changed, 269 insertions(+) create mode 100644 src/yycc/rust/env.cpp create mode 100644 src/yycc/rust/env.hpp create mode 100644 testbench/yycc/rust/env.cpp diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 34eb5c7..e7e169d 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -15,6 +15,7 @@ PRIVATE yycc/string/op.cpp yycc/patch/fopen.cpp yycc/rust/panic.cpp + yycc/rust/env.cpp yycc/windows/com.cpp #yycc/windows/dialog.cpp yycc/windows/winfct.cpp @@ -53,6 +54,7 @@ FILES yycc/rust/panic.hpp yycc/rust/option.hpp yycc/rust/result.hpp + yycc/rust/env.hpp yycc/windows/import_guard_head.hpp yycc/windows/import_guard_tail.hpp yycc/windows/com.hpp diff --git a/src/yycc/rust/env.cpp b/src/yycc/rust/env.cpp new file mode 100644 index 0000000..d25247f --- /dev/null +++ b/src/yycc/rust/env.cpp @@ -0,0 +1,159 @@ +#include "env.hpp" +#include "../macro/os_detector.hpp" + +#if defined(YYCC_OS_WINDOWS) +#include "../encoding/windows.hpp" +#include "../num/safe_op.hpp" +#include "../num/safe_cast.hpp" +#include +#include +#else +#include "../string/reinterpret.hpp" +#include +#include +#include +#endif + +#define SAFECAST ::yycc::num::safe_cast +#define SAFEOP ::yycc::num::safe_op +#define ENC ::yycc::encoding::windows +#define REINTERPRET ::yycc::string::reinterpret + +namespace yycc::rust::env { + + EnvResult get_var(const std::u8string_view &name) { +#if defined(YYCC_OS_WINDOWS) + // Reference: https://learn.microsoft.com/en-us/windows/win32/api/processenv/nf-processenv-getenvironmentvariablew + // Convert to wchar + auto wname = ENC::to_wchar(name); + if (!wname.has_value()) { + return std::unexpected(EnvError::BadEncoding); + } + + // Prepare a variable with proper init size for holding value. + std::wstring wvalue; + wvalue.resize(256); + + // Start to fetch value + while (true) { + // YYC MARK: + // Due to the shitty design of GetEnvironmentVariableW, + // the size passed to this function must include NULL terminal. + // So we forcely use checked add and sub for this bad behavior. + auto fct_size = SAFEOP::checked_add(wvalue.size(), 1); + if (!fct_size.has_value()) return std::unexpected(EnvError::BadArithmetic); + auto rv = ::GetEnvironmentVariableW(wname.value().c_str(), wvalue.data(), fct_size.value()); + + // Check the return value + if (rv == 0) { + // Function failed. Extract error reason. + auto ec = GetLastError(); + if (ec == ERROR_ENVVAR_NOT_FOUND) return std::unexpected(EnvError::NoSuchName); + else return std::unexpected(EnvError::BadCall); + } else { + // Function okey. Check the size. + // Fetch function expected size. + auto rv_size = SAFECAST::try_to(rv); + if (!rv_size.has_value()) return std::unexpected(EnvError::BadArithmetic); + auto exp_size = SAFEOP::checked_sub(rv_size.value(), 1); + if (!exp_size.has_value()) return std::unexpected(EnvError::BadArithmetic); + + // YYC MARK: + // According to Microsoft, the return value of this function is just a bullshit. + // If "wvalue" is big enough, the range of return value is [0, wvalue.size()], + // indicating the size of final string, excluding NULL terminal. + // otherwise, the range of return value is [wvalue.size() + 1, +inf), + // indicating the required size of buffer including NULL terminal. + // So we must treat this return value carefully. + if (exp_size.value() <= wvalue.size()) { + // Okey, shrink to the real size of value and break. + wvalue.resize(rv_size.value()); + break; + } else { + // We need resize it and try again. + wvalue.resize(exp_size.value()); + continue; + } + } + } + + // Convert back to UTF8 string and return. + return ENC::to_utf8(wvalue).transform_error([](auto err) { return EnvError::BadEncoding; }); +#else + // String view is not NULL-terminal-guaranted, + // so we solve this when casting its type. + auto ordinary_name = REINTERPRET::as_ordinary(name); + + // Fetch variable + auto finder = std::getenv(ordinary_name.c_str()); + if (finder == nullptr) return std::unexpected(EnvError::NoSuchName); + else return REINTERPRET::as_utf8(finder); +#endif + } + + EnvResult set_var(const std::u8string_view &name, const std::u8string_view &value) { +#if defined(YYCC_OS_WINDOWS) + // Reference: https://learn.microsoft.com/en-us/windows/win32/api/processenv/nf-processenv-setenvironmentvariablew + + // Convert to wchar + auto wname = ENC::to_wchar(name); + auto wvalue = ENC::to_wchar(value); + if (!(wname.has_value() && wvalue.has_value())) { + return std::unexpected(EnvError::BadEncoding); + } + + // Delete variable and check result. + auto rv = ::SetEnvironmentVariableW(wname.value().c_str(), wvalue.value().c_str()); + if (!rv) { + return std::unexpected(EnvError::BadCall); + } + + return {}; +#else + // Reference: https://pubs.opengroup.org/onlinepubs/9699919799/functions/setenv.html + + // Set variable + auto ordinary_name = REINTERPRET::as_ordinary(name); + auto ordinary_value = REINTERPRET::as_ordinary(value); + auto rv = setenv(ordinary_name.c_str(), ordinary_value.c_str(), 1); + if (rv == 0) return {}; + + // Check error type + if (errno == EINVAL) return std::unexpected(EnvError::BadName); + else if (errno == ENOMEM) return std::unexpected(EnvError::NoMemory); + else throw std::runtime_error("impossible errno"); +#endif + } + + EnvResult del_var(const std::u8string_view &name) { +#if defined(YYCC_OS_WINDOWS) + // Reference: https://learn.microsoft.com/en-us/windows/win32/api/processenv/nf-processenv-setenvironmentvariablew + + // Convert to wchar + auto wname = ENC::to_wchar(name); + if (!wname.has_value()) { + return std::unexpected(EnvError::BadEncoding); + } + + // Delete variable and check result. + auto rv = ::SetEnvironmentVariableW(wname.value().c_str(), NULL); + if (!rv) { + return std::unexpected(EnvError::BadCall); + } + + return {}; +#else + // Reference: https://pubs.opengroup.org/onlinepubs/9699919799/functions/unsetenv.html + + // Delete variable + auto ordinary_name = REINTERPRET::as_ordinary(name); + auto rv = unsetenv(ordinary_name.c_str()); + if (rv == 0) return {}; + + // Check error type + if (errno == EINVAL) return std::unexpected(EnvError::BadName); + else throw std::runtime_error("impossible errno"); +#endif + } + +} // namespace yycc::rust::env diff --git a/src/yycc/rust/env.hpp b/src/yycc/rust/env.hpp new file mode 100644 index 0000000..a04e494 --- /dev/null +++ b/src/yycc/rust/env.hpp @@ -0,0 +1,62 @@ +#pragma once +#include +#include +#include + +/** + * @brief The namespace providing environment variable operations. + * @details + * When I programming with Rust, I was astonished that + * Rust standard library have so much robust environment variable operations. + * Oppositly, C++ STL still lake in this even in today. + * + * The functions manipulating environment variable is different in different OS. + * I create this namespace inspired from Rust standard library + * to glue all these things up and make a uniform interface. + */ +namespace yycc::rust::env { + + /// @brief The error occurs in this module. + enum class EnvError { + NoSuchName, ///< The variable with given name is not presented. + BadEncoding, ///< Error when performing encoding convertion. + BadArithmetic, ///< Error when performing arithmetic operations. + BadCall, ///< Error occurs when calling backend functions. + BadName, ///< Given name is ill-formated (empty string or has "=" character). + NoMemory, ///< No enough memory to finish this operation. + }; + + /// @brief The result type in this module. + template + using EnvResult = std::expected; + + /** + * @brief Get the value of given environment variable name. + * @param[in] name The name of environment variable + * @return Gotten value, or error occurs. + */ + EnvResult get_var(const std::u8string_view& name); + + /** + * @brief Set the value of given environment variable name. + * @details + * If there is no such name variable presented in environment, + * a new variable will be created, + * otherwise, new value will overwrite old value. + * @param[in] name The name of environment variable + * @param[in] value The value to be written into. + * @return Nothing or error occurs. + */ + EnvResult set_var(const std::u8string_view& name, const std::u8string_view& value); + + /** + * @brief Delete environment variable with given name. + * @details + * If given variable is not presented in environment, + * this function will NOT return error. + * @param[in] name The name of environment variable + * @return Nothing, or error occurs. + */ + EnvResult del_var(const std::u8string_view& name); + +} // namespace yycc::rust::env diff --git a/testbench/CMakeLists.txt b/testbench/CMakeLists.txt index 376bd89..f4cc139 100644 --- a/testbench/CMakeLists.txt +++ b/testbench/CMakeLists.txt @@ -12,6 +12,7 @@ PRIVATE yycc/constraint/builder.cpp yycc/patch/ptr_pad.cpp yycc/patch/fopen.cpp + yycc/rust/env.cpp yycc/string/op.cpp yycc/string/reinterpret.cpp yycc/num/parse.cpp diff --git a/testbench/yycc/rust/env.cpp b/testbench/yycc/rust/env.cpp new file mode 100644 index 0000000..38b111f --- /dev/null +++ b/testbench/yycc/rust/env.cpp @@ -0,0 +1,45 @@ +#include +#include +#include + +#define ENV ::yycc::rust::env + +namespace yycctest::rust::env { + + constexpr char8_t VAR_NAME[] = u8"HOMER"; + constexpr char8_t VAR_VALUE[] = u8"doh"; + + TEST(RustEnv, All) { + // Write a new variable should okey + { + auto rv = ENV::set_var(VAR_NAME, VAR_VALUE); + ASSERT_TRUE(rv.has_value()); + } + + // After writing, we can fetch it and check its value. + { + auto rv = ENV::get_var(VAR_NAME); + ASSERT_TRUE(rv.has_value()); + EXPECT_EQ(rv.value(), VAR_VALUE); + } + + // The we can delete it. + { + auto rv = ENV::del_var(VAR_NAME); + ASSERT_TRUE(rv.has_value()); + } + + // Delete inexisting variable also should be okey + { + auto rv = ENV::del_var(VAR_NAME); + ASSERT_TRUE(rv.has_value()); + } + + // After deleting, we can not fetch it anymore. + { + auto rv = ENV::get_var(VAR_NAME); + ASSERT_FALSE(rv.has_value()); + } + } + +}