From 0b53f0df9532534690cc6766f3b793ddcaa31801 Mon Sep 17 00:00:00 2001 From: Victor Benarbia Date: Thu, 12 Feb 2026 22:09:24 -0600 Subject: [PATCH 1/4] Fix release task: use 'v' prefix for tags and inject PKG_CONFIG_PATH for dist-check. --- Rakefile | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/Rakefile b/Rakefile index 6f45669..29bcbe2 100644 --- a/Rakefile +++ b/Rakefile @@ -79,9 +79,8 @@ end desc('release current library') task release: [:default] do # Extract version from meson.build - meson_build = File.read('meson.build') - version = meson_build.match(/version\s*:\s*'([^']+)'/)[1] - tag = "#{version}" + version = File.read('meson.build').match(/version\s*:\s*'([^']+)'/)[1] + tag = "v#{version}" puts "Releasing #{tag}..." @@ -90,9 +89,8 @@ task release: [:default] do puts "Tag #{tag} already exists. Skipping git tag." else # Ensure working directory is clean - if !`git status --porcelain`.strip.empty? - puts "Working directory is not clean. Please commit or stash changes." - exit 1 + unless `git status --porcelain`.strip.empty? + abort "Working directory is not clean. Please commit or stash changes." end sh "git tag -a #{tag} -m 'Release #{tag}'" @@ -100,7 +98,8 @@ task release: [:default] do end # Create distribution package - sh "meson dist -C build" + # meson dist performs a build and test in a temporary directory, so it needs the env + sh "#{pkg_config_env} meson dist -C build" puts "\nRelease #{tag} completed successfully!" puts "Next steps:" From 8aeed24e3d0060fd9ac61a19e7d89b08fd578457 Mon Sep 17 00:00:00 2001 From: Victor Benarbia Date: Mon, 8 Jun 2026 21:54:48 -0500 Subject: [PATCH 2/4] Improve error handling and security in release task. - Add nil-check for version regex match with informative error message - Use Shellwords.escape() to safely handle tag names in shell commands - Fix PKG_CONFIG_PATH handling to properly escape paths with special characters - Improves robustness against malformed configuration or injection attacks Co-Authored-By: Claude Haiku 4.5 --- Rakefile | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/Rakefile b/Rakefile index 29bcbe2..7d9ea6a 100644 --- a/Rakefile +++ b/Rakefile @@ -21,7 +21,8 @@ def pkg_config_env existing_path = ENV['PKG_CONFIG_PATH'] pkg_paths << existing_path if existing_path && !existing_path.empty? - "PKG_CONFIG_PATH='#{pkg_paths.join(':')}'" + require 'shellwords' + "PKG_CONFIG_PATH=#{Shellwords.escape(pkg_paths.join(':'))}" end desc('initialize meson build') @@ -78,14 +79,19 @@ end desc('release current library') task release: [:default] do + require 'shellwords' + # Extract version from meson.build - version = File.read('meson.build').match(/version\s*:\s*'([^']+)'/)[1] + version_match = File.read('meson.build').match(/version\s*:\s*'([^']+)'/) + abort "Unable to extract version from meson.build. Check version format." unless version_match + + version = version_match[1] tag = "v#{version}" puts "Releasing #{tag}..." # Check if tag already exists - if `git tag -l #{tag}`.strip == tag + if `git tag -l #{Shellwords.escape(tag)}`.strip == tag puts "Tag #{tag} already exists. Skipping git tag." else # Ensure working directory is clean @@ -93,14 +99,14 @@ task release: [:default] do abort "Working directory is not clean. Please commit or stash changes." end - sh "git tag -a #{tag} -m 'Release #{tag}'" + sh "git tag -a #{Shellwords.escape(tag)} -m #{Shellwords.escape("Release #{tag}")}" puts "Created tag #{tag}." end # Create distribution package # meson dist performs a build and test in a temporary directory, so it needs the env sh "#{pkg_config_env} meson dist -C build" - + puts "\nRelease #{tag} completed successfully!" puts "Next steps:" puts "1. git push origin #{tag}" From dc8ae3de0d4939a460ed96d6ee3ac85442fb2f23 Mon Sep 17 00:00:00 2001 From: Victor Benarbia Date: Mon, 8 Jun 2026 22:06:10 -0500 Subject: [PATCH 3/4] Fix critical error handling and resource management in C++ code. **Critical Fixes:** - Add null pointer checks in callback for input validation - Check for integer overflow in callback size calculation - Add CURL handle validation after curl_easy_init() - Use std::call_once for thread-safe CURL global initialization - Check curl_easy_perform() return value and report errors - Validate JSON parse errors with HasParseError() - Add null checks for curl_easy_escape() allocations - Remove unused includes and 'using namespace' directives - Add public setTimeout() method for configurable timeouts **Improvements:** - Use std::ostringstream for efficient URL parameter encoding - Replace static bool with std::once_flag for thread safety - Return meaningful error messages on failures - Add exception safety in callback function - Proper cleanup of CURL resources on all error paths Co-Authored-By: Claude Haiku 4.5 --- src/callback.cpp | 24 ++++++----- src/serpapi.cpp | 109 ++++++++++++++++++++++++++--------------------- src/serpapi.hpp | 2 + 3 files changed, 75 insertions(+), 60 deletions(-) diff --git a/src/callback.cpp b/src/callback.cpp index 36c7403..4763280 100644 --- a/src/callback.cpp +++ b/src/callback.cpp @@ -1,16 +1,18 @@ #include -#include -#include +#include #include -using namespace std; +namespace serpapi { +size_t callback(const char* in, size_t size, size_t num, std::string* out) { + if (!out || !in) return 0; + if (size == 0 || num == 0) return 0; + if (size > std::numeric_limits::max() / num) return 0; -namespace serpapi -{ - size_t callback(const char* in, size_t size, size_t num, string* out) - { - const size_t totalBytes(size*num); - out->append(in, totalBytes); - return totalBytes; - } + const size_t totalBytes = size * num; + try { + out->append(in, totalBytes); + return totalBytes; + } catch (const std::exception&) { + return 0; + } } diff --git a/src/serpapi.cpp b/src/serpapi.cpp index 281dee5..fd74b5a 100644 --- a/src/serpapi.cpp +++ b/src/serpapi.cpp @@ -4,6 +4,8 @@ #include "callback.hpp" #include +#include +#include namespace serpapi { @@ -11,94 +13,96 @@ const static std::string HOST = "https://serpapi.com"; const static std::string NAME = "serpapi-cpp"; const static std::string VERSION = "0.3.0"; -using namespace rapidjson; -using namespace std; +static std::once_flag curl_init_flag; -Client::Client(const map ¶meter) { +Client::Client(const std::map ¶meter) { this->parameter = parameter; } Client::~Client() {} -/*** - * Get HTML search results - */ -string Client::html(const map ¶meter) { +std::string Client::html(const std::map ¶meter) { GetResponse gr = Client::get("/search", "html", parameter); return gr.payload; } -Document Client::search(const map ¶meter) { +rapidjson::Document Client::search(const std::map ¶meter) { return Client::json("/search", parameter); } -Document Client::search_archive(const string &id) { - return Client::json("/searches/" + id + ".json", map()); +rapidjson::Document Client::search_archive(const std::string &id) { + return Client::json("/searches/" + id + ".json", std::map()); } -Document Client::account(const map ¶meter) { +rapidjson::Document Client::account(const std::map ¶meter) { return Client::json("/account.json", parameter); } -Document Client::location(const map ¶meter) { +rapidjson::Document Client::location(const std::map ¶meter) { return Client::json("/locations.json", parameter); } -Document Client::json(const string &uri, const map ¶meter) { +rapidjson::Document Client::json(const std::string &uri, const std::map ¶meter) { GetResponse gr = get(uri, "json", parameter); - const char *json_payload = gr.payload.c_str(); - Document d; - d.Parse(json_payload); + rapidjson::Document d; + d.Parse(gr.payload.c_str()); + if (d.HasParseError()) { + d.SetObject(); + d.AddMember("error", "JSON parse error", d.GetAllocator()); + } return d; } -string encodeUrl(CURL *curl, const map ¶meter) { - string s = ""; - map::const_iterator it; - for (it = parameter.begin(); it != parameter.end(); ++it) { - if (s != "") { - s += "&"; - } +std::string encodeUrl(CURL *curl, const std::map ¶meter) { + std::ostringstream oss; + bool first = true; + + for (const auto& entry : parameter) { + char *escaped_key = curl_easy_escape(curl, entry.first.c_str(), entry.first.length()); + char *escaped_value = curl_easy_escape(curl, entry.second.c_str(), entry.second.length()); - char *escaped_key = - curl_easy_escape(curl, it->first.c_str(), it->first.length()); - char *escaped_value = - curl_easy_escape(curl, it->second.c_str(), it->second.length()); + if (!escaped_key || !escaped_value) { + if (escaped_key) curl_free(escaped_key); + if (escaped_value) curl_free(escaped_value); + return ""; + } - s += string(escaped_key) + "=" + string(escaped_value); + if (!first) oss << "&"; + oss << escaped_key << "=" << escaped_value; + first = false; curl_free(escaped_key); curl_free(escaped_value); } - return s; + return oss.str(); } -string Client::url(CURL *curl, const string &output, - const map ¶meter) { - // encode parameter - string url_str = encodeUrl(curl, parameter); +std::string Client::url(CURL *curl, const std::string &output, + const std::map ¶meter) { + std::string url_str = encodeUrl(curl, parameter); if (this->parameter.size() > 0) { - if (!url_str.empty()) - url_str += "&"; + if (!url_str.empty()) url_str += "&"; url_str += encodeUrl(curl, this->parameter); } - // append output format url_str += "&output=" + output; - // append source language url_str += "&source=" + NAME + ":" + VERSION; return url_str; } -GetResponse Client::get(const string &uri, const string &output, - const map ¶meter) { - static bool initialized = false; - if (!initialized) { - curl_global_init(CURL_GLOBAL_DEFAULT); - initialized = true; - } +GetResponse Client::get(const std::string &uri, const std::string &output, + const std::map ¶meter) { + std::call_once(curl_init_flag, []() { curl_global_init(CURL_GLOBAL_DEFAULT); }); + CURL *curl = curl_easy_init(); - const string url_params = this->url(curl, output, parameter); - const string full_url = HOST + uri + "?" + url_params; + if (!curl) { + GetResponse gr; + gr.httpCode = 0; + gr.payload = "CURL initialization failed"; + return gr; + } + + const std::string url_params = this->url(curl, output, parameter); + const std::string full_url = HOST + uri + "?" + url_params; curl_easy_setopt(curl, CURLOPT_URL, full_url.c_str()); curl_easy_setopt(curl, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4); @@ -108,12 +112,19 @@ GetResponse Client::get(const string &uri, const string &output, curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L); long httpCode(0); - string httpData; + std::string httpData; curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, callback); curl_easy_setopt(curl, CURLOPT_WRITEDATA, &httpData); - // execute search - curl_easy_perform(curl); + CURLcode res = curl_easy_perform(curl); + if (res != CURLE_OK) { + curl_easy_cleanup(curl); + GetResponse gr; + gr.httpCode = 0; + gr.payload = std::string("CURL error: ") + curl_easy_strerror(res); + return gr; + } + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &httpCode); curl_easy_cleanup(curl); diff --git a/src/serpapi.hpp b/src/serpapi.hpp index 0ce9594..dcf1356 100644 --- a/src/serpapi.hpp +++ b/src/serpapi.hpp @@ -25,6 +25,8 @@ class Client { explicit Client(const std::map ¶meter); ~Client(); + void setTimeout(int seconds) { timeout = seconds; } + rapidjson::Document search(const std::map ¶meter = {}); From 4458e925625a509df9bb399ad52265771d8cfd9a Mon Sep 17 00:00:00 2001 From: Victor Benarbia Date: Mon, 8 Jun 2026 22:12:01 -0500 Subject: [PATCH 4/4] Fix missing closing namespace brace in callback.cpp Co-Authored-By: Claude Sonnet 4.6 --- src/callback.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/callback.cpp b/src/callback.cpp index 4763280..a7028cf 100644 --- a/src/callback.cpp +++ b/src/callback.cpp @@ -16,3 +16,4 @@ size_t callback(const char* in, size_t size, size_t num, std::string* out) { return 0; } } +} // namespace serpapi