This is an automated email from the ASF dual-hosted git repository.
lidavidm pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/arrow-adbc.git
The following commit(s) were added to refs/heads/main by this push:
new 8c833663b refactor(c/driver_manager): replace std::regex in profile
interpolation (#4780)
8c833663b is described below
commit 8c833663b7112fa926893fe65e25e5850ca1a3fa
Author: Kevin Ushey <[email protected]>
AuthorDate: Sun Sep 13 16:53:36 2026 -0700
refactor(c/driver_manager): replace std::regex in profile interpolation
(#4780)
Profile interpolation currently constructs a `std::regex` to expand `{{
env_var(NAME) }}` expressions. Replace it with a linear scanner that
preserves whitespace trimming, overlapping delimiters, literal malformed
braces, and existing error messages. This removes the regex construction
path implicated in #4638.
The interpolation helper follows the existing internal test-helper
export convention (`InternalAdbc` prefix and `ADBC_EXPORT`) so
shared-library tests link on Linux and Windows.
The CRAN GCC 16 sanitizer environment was not reproduced locally.
Closes #4638
---
.github/workflows/r-check.yml | 3 +
c/driver_manager/adbc_driver_manager.cc | 3 +-
c/driver_manager/adbc_driver_manager_internal.h | 6 +-
c/driver_manager/adbc_driver_manager_profiles.cc | 60 ++++++++++++-------
c/driver_manager/adbc_driver_manager_test.cc | 73 +++++++++++++++++++++++
go/adbc/drivermgr/adbc_driver_manager.cc | 3 +-
go/adbc/drivermgr/adbc_driver_manager_internal.h | 6 +-
go/adbc/drivermgr/adbc_driver_manager_profiles.cc | 60 ++++++++++++-------
8 files changed, 162 insertions(+), 52 deletions(-)
diff --git a/.github/workflows/r-check.yml b/.github/workflows/r-check.yml
index d98f739e2..6af752d99 100644
--- a/.github/workflows/r-check.yml
+++ b/.github/workflows/r-check.yml
@@ -85,6 +85,9 @@ jobs:
extra-packages: any::rcmdcheck, local::../adbcdrivermanager
needs: check
working-directory: r/${{ inputs.pkg }}
+ # These packages do not need Pandoc. Auto-detection ignores the local
+ # dependency override and tries to resolve adbcdrivermanager from
CRAN.
+ install-pandoc: false
- name: Start postgres test database
if: inputs.pkg == 'adbcpostgresql' && runner.os == 'Linux'
diff --git a/c/driver_manager/adbc_driver_manager.cc
b/c/driver_manager/adbc_driver_manager.cc
index 1a9fe7f36..5accc002b 100644
--- a/c/driver_manager/adbc_driver_manager.cc
+++ b/c/driver_manager/adbc_driver_manager.cc
@@ -63,7 +63,6 @@
#include <cstring>
#include <filesystem>
#include <functional>
-#include <regex>
#include <string>
#include <unordered_map>
#include <utility>
@@ -683,7 +682,7 @@ AdbcStatusCode InternalInitializeProfile(TempDatabase* args,
// use try_emplace so we only add the option if there isn't
// already an option with the same name
std::string processed;
- CHECK_STATUS(ProcessProfileValue(keys[i], values[i], processed, error));
+ CHECK_STATUS(InternalAdbcProcessProfileValue(keys[i], values[i],
processed, error));
args->options.try_emplace(keys[i], processed);
}
diff --git a/c/driver_manager/adbc_driver_manager_internal.h
b/c/driver_manager/adbc_driver_manager_internal.h
index 88b02e64c..6f0f6c832 100644
--- a/c/driver_manager/adbc_driver_manager_internal.h
+++ b/c/driver_manager/adbc_driver_manager_internal.h
@@ -189,8 +189,10 @@ AdbcStatusCode LoadDriverFromRegistry(HKEY root, const
std::wstring& driver_name
#endif
// Profile loading
-AdbcStatusCode ProcessProfileValue(std::string_view key, std::string_view
value,
- std::string& out, struct AdbcError* error);
+ADBC_EXPORT
+AdbcStatusCode InternalAdbcProcessProfileValue(std::string_view key,
+ std::string_view value,
std::string& out,
+ struct AdbcError* error);
// Initialization
/// Temporary state while the database is being configured.
diff --git a/c/driver_manager/adbc_driver_manager_profiles.cc
b/c/driver_manager/adbc_driver_manager_profiles.cc
index 2ad571676..b5597d0d8 100644
--- a/c/driver_manager/adbc_driver_manager_profiles.cc
+++ b/c/driver_manager/adbc_driver_manager_profiles.cc
@@ -23,7 +23,7 @@
#include "adbc_driver_manager_internal.h"
#include <filesystem>
-#include <regex>
+#include <locale>
#include <string>
#include <unordered_map>
#include <utility>
@@ -204,29 +204,47 @@ struct ProfileVisitor {
};
// Public implementations (non-static for use across translation units)
-AdbcStatusCode ProcessProfileValue(std::string_view key, std::string_view
value,
- std::string& out, struct AdbcError* error) {
+AdbcStatusCode InternalAdbcProcessProfileValue(std::string_view key,
+ std::string_view value,
std::string& out,
+ struct AdbcError* error) {
if (value.empty()) {
out = "";
return ADBC_STATUS_OK;
}
- static const std::regex pattern(R"(\{\{\s*([^{}]*?)\s*\}\})");
- auto end_of_last_match = value.begin();
- auto begin = std::regex_iterator(value.begin(), value.end(), pattern);
- auto end = decltype(begin){};
- std::match_results<std::string_view::iterator>::difference_type
pos_last_match = 0;
+ static const std::locale locale;
+ size_t end_of_last_match = 0;
+ size_t start_match = 0;
out.resize(0);
- for (auto itr = begin; itr != end; ++itr) {
- auto match = *itr;
- auto pos_match = match.position();
- auto diff = pos_match - pos_last_match;
- auto start_match = end_of_last_match;
- std::advance(start_match, diff);
- out.append(end_of_last_match, start_match);
-
- const auto content = match[1].str();
+ while ((start_match = value.find("{{", start_match)) !=
std::string_view::npos) {
+ const auto end_match = value.find_first_of("{}", start_match + 2);
+ if (end_match == std::string_view::npos) {
+ break;
+ }
+ if (value[end_match] == '{') {
+ // Braces cannot occur inside an interpolation. Allow overlapping opening
+ // delimiters so that "{{{ env_var(NAME) }}" still expands the inner
pair.
+ start_match = end_match - 1;
+ continue;
+ }
+ if (end_match + 1 == value.size() || value[end_match + 1] != '}') {
+ start_match = end_match + 1;
+ continue;
+ }
+ out.append(value.substr(end_of_last_match, start_match -
end_of_last_match));
+
+ auto content_start = start_match + 2;
+ auto content_end = end_match;
+ while (content_start < content_end && std::isspace(value[content_start],
locale)) {
+ ++content_start;
+ }
+ while (content_start < content_end && std::isspace(value[content_end - 1],
locale)) {
+ --content_end;
+ }
+
+ const auto content =
+ std::string(value.substr(content_start, content_end - content_start));
if (content.rfind("env_var(", 0) != 0) {
std::string message = "In profile: unsupported interpolation type in key
`" +
std::string(key) + "`: `" + content + "`";
@@ -270,13 +288,11 @@ AdbcStatusCode ProcessProfileValue(std::string_view key,
std::string_view value,
#endif
out.append(env_var_value);
- auto length_match = match.length();
- pos_last_match = pos_match + length_match;
- end_of_last_match = start_match;
- std::advance(end_of_last_match, length_match);
+ end_of_last_match = end_match + 2;
+ start_match = end_of_last_match;
}
- out.append(end_of_last_match, value.end());
+ out.append(value.substr(end_of_last_match));
return ADBC_STATUS_OK;
}
diff --git a/c/driver_manager/adbc_driver_manager_test.cc
b/c/driver_manager/adbc_driver_manager_test.cc
index cecfc4b10..caa0dddad 100644
--- a/c/driver_manager/adbc_driver_manager_test.cc
+++ b/c/driver_manager/adbc_driver_manager_test.cc
@@ -1759,6 +1759,79 @@ TEST_F(ConnectionProfiles, DuplicateQuotedKey) {
UnsetProfilePath();
}
+TEST_F(ConnectionProfiles, ProcessProfileValueDelimiters) {
+ const std::vector<std::pair<std::string, std::string>> cases = {
+ {"", ""},
+ {"literal text", "literal text"},
+ {"{{env_var(ADBC_PROFILE_PATH)}}", "profile-value"},
+ {"{{ \t\n\r\f\venv_var(ADBC_PROFILE_PATH)\v\f\r\n\t }}",
"profile-value"},
+ {"before{{env_var(ADBC_PROFILE_PATH)}}after",
"beforeprofile-valueafter"},
+ {"{{env_var(ADBC_PROFILE_PATH)}}{{env_var(ADBC_PROFILE_PATH)}}",
+ "profile-valueprofile-value"},
+ {"{env_var(ADBC_PROFILE_PATH)}", "{env_var(ADBC_PROFILE_PATH)}"},
+ {"{{env_var(ADBC_PROFILE_PATH)", "{{env_var(ADBC_PROFILE_PATH)"},
+ {"{{env_var(ADBC_PROFILE_PATH)}", "{{env_var(ADBC_PROFILE_PATH)}"},
+ {"{{env_var(ADBC_PROFILE_PATH)} }", "{{env_var(ADBC_PROFILE_PATH)} }"},
+ {"{{env_var(ADBC_{PROFILE_PATH)}}", "{{env_var(ADBC_{PROFILE_PATH)}}"},
+ {"{{env_var(ADBC_}PROFILE_PATH)}}", "{{env_var(ADBC_}PROFILE_PATH)}}"},
+ {"{{{env_var(ADBC_PROFILE_PATH)}}}", "{profile-value}"},
+ {"{{outer {{env_var(ADBC_PROFILE_PATH)}} }}", "{{outer profile-value
}}"},
+ {"{{broken} {{env_var(ADBC_PROFILE_PATH)}}", "{{broken} profile-value"},
+ {"}}{{env_var(ADBC_PROFILE_PATH)}}{{", "}}profile-value{{"},
+ {std::string("before\0", 7) + "{{env_var(ADBC_PROFILE_PATH)}}" +
+ std::string("\0after", 6),
+ std::string("before\0profile-value\0after", 26)},
+ {std::string(10000, '{') + "env_var(ADBC_PROFILE_PATH)}}",
+ std::string(9998, '{') + "profile-value"},
+ };
+ for (const auto& [input, expected] : cases) {
+ SCOPED_TRACE(input);
+ std::string out = "previous output";
+ SetProfilePath("profile-value");
+ const auto status = InternalAdbcProcessProfileValue("foo", input, out,
&error);
+ UnsetProfilePath();
+ ASSERT_THAT(status, IsOkStatus(&error));
+ EXPECT_EQ(out, expected);
+ }
+}
+
+TEST_F(ConnectionProfiles, ProcessProfileValueErrors) {
+ const std::vector<std::pair<std::string, std::string>> cases = {
+ {"{{}}", "unsupported interpolation type in key `foo`: ``"},
+ {"{{ \t\n\r\f\v }}", "unsupported interpolation type in key `foo`: ``"},
+ {"{{ unknown() }}", "unsupported interpolation type in key `foo`:
`unknown()`"},
+ {"{{ env_var (NAME) }}",
+ "unsupported interpolation type in key `foo`: `env_var (NAME)`"},
+ {"{{ env_var(NAME }}",
+ "malformed env_var() in key `foo`: missing closing parenthesis"},
+ {"{{ env_var( }}", "malformed env_var() in key `foo`: missing closing
parenthesis"},
+ {"{{ env_var() }}",
+ "malformed env_var() in key `foo`: missing environment variable name"},
+ };
+ for (const auto& [input, expected] : cases) {
+ SCOPED_TRACE(input);
+ std::string out;
+ ASSERT_THAT(
+ InternalAdbcProcessProfileValue("foo", "prefix" + input + "suffix",
out, &error),
+ IsStatus(ADBC_STATUS_INVALID_ARGUMENT, &error));
+ EXPECT_STREQ(error.message, ("[Driver Manager] In profile: " +
expected).c_str());
+ EXPECT_EQ(out, "prefix");
+ if (error.release) {
+ error.release(&error);
+ }
+ }
+}
+
+TEST_F(ConnectionProfiles, ProcessProfileValueDoesNotRecurse) {
+ std::string out;
+ SetProfilePath("{{ unsupported() }}");
+ const auto status = InternalAdbcProcessProfileValue(
+ "foo", "{{env_var(ADBC_PROFILE_PATH)}}", out, &error);
+ UnsetProfilePath();
+ ASSERT_THAT(status, IsOkStatus(&error));
+ EXPECT_EQ(out, "{{ unsupported() }}");
+}
+
TEST_F(ConnectionProfiles, UseEnvVar) {
auto filepath = temp_dir / "profile.toml";
toml::table profile = toml::parse(R"|(
diff --git a/go/adbc/drivermgr/adbc_driver_manager.cc
b/go/adbc/drivermgr/adbc_driver_manager.cc
index 1a9fe7f36..5accc002b 100644
--- a/go/adbc/drivermgr/adbc_driver_manager.cc
+++ b/go/adbc/drivermgr/adbc_driver_manager.cc
@@ -63,7 +63,6 @@
#include <cstring>
#include <filesystem>
#include <functional>
-#include <regex>
#include <string>
#include <unordered_map>
#include <utility>
@@ -683,7 +682,7 @@ AdbcStatusCode InternalInitializeProfile(TempDatabase* args,
// use try_emplace so we only add the option if there isn't
// already an option with the same name
std::string processed;
- CHECK_STATUS(ProcessProfileValue(keys[i], values[i], processed, error));
+ CHECK_STATUS(InternalAdbcProcessProfileValue(keys[i], values[i],
processed, error));
args->options.try_emplace(keys[i], processed);
}
diff --git a/go/adbc/drivermgr/adbc_driver_manager_internal.h
b/go/adbc/drivermgr/adbc_driver_manager_internal.h
index 88b02e64c..6f0f6c832 100644
--- a/go/adbc/drivermgr/adbc_driver_manager_internal.h
+++ b/go/adbc/drivermgr/adbc_driver_manager_internal.h
@@ -189,8 +189,10 @@ AdbcStatusCode LoadDriverFromRegistry(HKEY root, const
std::wstring& driver_name
#endif
// Profile loading
-AdbcStatusCode ProcessProfileValue(std::string_view key, std::string_view
value,
- std::string& out, struct AdbcError* error);
+ADBC_EXPORT
+AdbcStatusCode InternalAdbcProcessProfileValue(std::string_view key,
+ std::string_view value,
std::string& out,
+ struct AdbcError* error);
// Initialization
/// Temporary state while the database is being configured.
diff --git a/go/adbc/drivermgr/adbc_driver_manager_profiles.cc
b/go/adbc/drivermgr/adbc_driver_manager_profiles.cc
index 2ad571676..b5597d0d8 100644
--- a/go/adbc/drivermgr/adbc_driver_manager_profiles.cc
+++ b/go/adbc/drivermgr/adbc_driver_manager_profiles.cc
@@ -23,7 +23,7 @@
#include "adbc_driver_manager_internal.h"
#include <filesystem>
-#include <regex>
+#include <locale>
#include <string>
#include <unordered_map>
#include <utility>
@@ -204,29 +204,47 @@ struct ProfileVisitor {
};
// Public implementations (non-static for use across translation units)
-AdbcStatusCode ProcessProfileValue(std::string_view key, std::string_view
value,
- std::string& out, struct AdbcError* error) {
+AdbcStatusCode InternalAdbcProcessProfileValue(std::string_view key,
+ std::string_view value,
std::string& out,
+ struct AdbcError* error) {
if (value.empty()) {
out = "";
return ADBC_STATUS_OK;
}
- static const std::regex pattern(R"(\{\{\s*([^{}]*?)\s*\}\})");
- auto end_of_last_match = value.begin();
- auto begin = std::regex_iterator(value.begin(), value.end(), pattern);
- auto end = decltype(begin){};
- std::match_results<std::string_view::iterator>::difference_type
pos_last_match = 0;
+ static const std::locale locale;
+ size_t end_of_last_match = 0;
+ size_t start_match = 0;
out.resize(0);
- for (auto itr = begin; itr != end; ++itr) {
- auto match = *itr;
- auto pos_match = match.position();
- auto diff = pos_match - pos_last_match;
- auto start_match = end_of_last_match;
- std::advance(start_match, diff);
- out.append(end_of_last_match, start_match);
-
- const auto content = match[1].str();
+ while ((start_match = value.find("{{", start_match)) !=
std::string_view::npos) {
+ const auto end_match = value.find_first_of("{}", start_match + 2);
+ if (end_match == std::string_view::npos) {
+ break;
+ }
+ if (value[end_match] == '{') {
+ // Braces cannot occur inside an interpolation. Allow overlapping opening
+ // delimiters so that "{{{ env_var(NAME) }}" still expands the inner
pair.
+ start_match = end_match - 1;
+ continue;
+ }
+ if (end_match + 1 == value.size() || value[end_match + 1] != '}') {
+ start_match = end_match + 1;
+ continue;
+ }
+ out.append(value.substr(end_of_last_match, start_match -
end_of_last_match));
+
+ auto content_start = start_match + 2;
+ auto content_end = end_match;
+ while (content_start < content_end && std::isspace(value[content_start],
locale)) {
+ ++content_start;
+ }
+ while (content_start < content_end && std::isspace(value[content_end - 1],
locale)) {
+ --content_end;
+ }
+
+ const auto content =
+ std::string(value.substr(content_start, content_end - content_start));
if (content.rfind("env_var(", 0) != 0) {
std::string message = "In profile: unsupported interpolation type in key
`" +
std::string(key) + "`: `" + content + "`";
@@ -270,13 +288,11 @@ AdbcStatusCode ProcessProfileValue(std::string_view key,
std::string_view value,
#endif
out.append(env_var_value);
- auto length_match = match.length();
- pos_last_match = pos_match + length_match;
- end_of_last_match = start_match;
- std::advance(end_of_last_match, length_match);
+ end_of_last_match = end_match + 2;
+ start_match = end_of_last_match;
}
- out.append(end_of_last_match, value.end());
+ out.append(value.substr(end_of_last_match));
return ADBC_STATUS_OK;
}