@@ -2030,7 +2030,7 @@ inline size_t get_header_value_u64(const Headers &headers,
20302030inline size_t get_header_value_u64 (const Headers &headers,
20312031 const std::string &key, size_t def,
20322032 size_t id) {
2033- bool dummy = false ;
2033+ auto dummy = false ;
20342034 return get_header_value_u64 (headers, key, def, id, dummy);
20352035}
20362036
@@ -2301,15 +2301,19 @@ std::string hosted_at(const std::string &hostname);
23012301
23022302void hosted_at (const std::string &hostname, std::vector<std::string> &addrs);
23032303
2304+ // JavaScript-style URL encoding/decoding functions
23042305std::string encode_uri_component (const std::string &value);
2305-
23062306std::string encode_uri (const std::string &value);
2307-
23082307std::string decode_uri_component (const std::string &value);
2309-
23102308std::string decode_uri (const std::string &value);
23112309
2312- std::string encode_query_param (const std::string &value);
2310+ // RFC 3986 compliant URL component encoding/decoding functions
2311+ std::string encode_path_component (const std::string &component);
2312+ std::string decode_path_component (const std::string &component);
2313+ std::string encode_query_component (const std::string &component,
2314+ bool space_as_plus = true );
2315+ std::string decode_query_component (const std::string &component,
2316+ bool plus_as_space = true );
23132317
23142318std::string append_query_params (const std::string &path, const Params ¶ms);
23152319
@@ -2352,8 +2356,6 @@ struct FileStat {
23522356 int ret_ = -1 ;
23532357};
23542358
2355- std::string decode_path (const std::string &s, bool convert_plus_to_space);
2356-
23572359std::string trim_copy (const std::string &s);
23582360
23592361void divide (
@@ -2854,43 +2856,6 @@ inline std::string encode_path(const std::string &s) {
28542856 return result;
28552857}
28562858
2857- inline std::string decode_path (const std::string &s,
2858- bool convert_plus_to_space) {
2859- std::string result;
2860-
2861- for (size_t i = 0 ; i < s.size (); i++) {
2862- if (s[i] == ' %' && i + 1 < s.size ()) {
2863- if (s[i + 1 ] == ' u' ) {
2864- auto val = 0 ;
2865- if (from_hex_to_i (s, i + 2 , 4 , val)) {
2866- // 4 digits Unicode codes
2867- char buff[4 ];
2868- size_t len = to_utf8 (val, buff);
2869- if (len > 0 ) { result.append (buff, len); }
2870- i += 5 ; // 'u0000'
2871- } else {
2872- result += s[i];
2873- }
2874- } else {
2875- auto val = 0 ;
2876- if (from_hex_to_i (s, i + 1 , 2 , val)) {
2877- // 2 digits hex codes
2878- result += static_cast <char >(val);
2879- i += 2 ; // '00'
2880- } else {
2881- result += s[i];
2882- }
2883- }
2884- } else if (convert_plus_to_space && s[i] == ' +' ) {
2885- result += ' ' ;
2886- } else {
2887- result += s[i];
2888- }
2889- }
2890-
2891- return result;
2892- }
2893-
28942859inline std::string file_extension (const std::string &path) {
28952860 std::smatch m;
28962861 thread_local auto re = std::regex (" \\ .([a-zA-Z0-9]+)$" );
@@ -4615,7 +4580,7 @@ inline bool parse_header(const char *beg, const char *end, T fn) {
46154580 case_ignore::equal (key, " Referer" )) {
46164581 fn (key, val);
46174582 } else {
4618- fn (key, decode_path (val, false ));
4583+ fn (key, decode_path_component (val));
46194584 }
46204585
46214586 return true ;
@@ -5263,9 +5228,9 @@ inline std::string params_to_query_str(const Params ¶ms) {
52635228
52645229 for (auto it = params.begin (); it != params.end (); ++it) {
52655230 if (it != params.begin ()) { query += " &" ; }
5266- query += it->first ;
5231+ query += encode_query_component ( it->first ) ;
52675232 query += " =" ;
5268- query += httplib::encode_uri_component (it->second );
5233+ query += encode_query_component (it->second );
52695234 }
52705235 return query;
52715236}
@@ -5288,7 +5253,7 @@ inline void parse_query_text(const char *data, std::size_t size,
52885253 });
52895254
52905255 if (!key.empty ()) {
5291- params.emplace (decode_path (key, true ), decode_path (val, true ));
5256+ params.emplace (decode_query_component (key), decode_query_component (val));
52925257 }
52935258 });
52945259}
@@ -5611,7 +5576,7 @@ class FormDataParser {
56115576
56125577 std::smatch m2;
56135578 if (std::regex_match (it->second , m2, re_rfc5987_encoding)) {
5614- file_.filename = decode_path (m2[1 ], false ); // override...
5579+ file_.filename = decode_path_component (m2[1 ]); // override...
56155580 } else {
56165581 is_valid_ = false ;
56175582 return false ;
@@ -6517,9 +6482,154 @@ inline std::string decode_uri(const std::string &value) {
65176482 return result;
65186483}
65196484
6520- [[deprecated (" Use encode_uri_component instead" )]]
6521- inline std::string encode_query_param (const std::string &value) {
6522- return encode_uri_component (value);
6485+ inline std::string encode_path_component (const std::string &component) {
6486+ std::string result;
6487+ result.reserve (component.size () * 3 );
6488+
6489+ for (size_t i = 0 ; i < component.size (); i++) {
6490+ auto c = static_cast <unsigned char >(component[i]);
6491+
6492+ // Unreserved characters per RFC 3986: ALPHA / DIGIT / "-" / "." / "_" / "~"
6493+ if (std::isalnum (c) || c == ' -' || c == ' .' || c == ' _' || c == ' ~' ) {
6494+ result += static_cast <char >(c);
6495+ }
6496+ // Path-safe sub-delimiters: "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" /
6497+ // "," / ";" / "="
6498+ else if (c == ' !' || c == ' $' || c == ' &' || c == ' \' ' || c == ' (' ||
6499+ c == ' )' || c == ' *' || c == ' +' || c == ' ,' || c == ' ;' ||
6500+ c == ' =' ) {
6501+ result += static_cast <char >(c);
6502+ }
6503+ // Colon is allowed in path segments except first segment
6504+ else if (c == ' :' ) {
6505+ result += static_cast <char >(c);
6506+ }
6507+ // @ is allowed in path
6508+ else if (c == ' @' ) {
6509+ result += static_cast <char >(c);
6510+ } else {
6511+ result += ' %' ;
6512+ char hex[3 ];
6513+ snprintf (hex, sizeof (hex), " %02X" , c);
6514+ result.append (hex, 2 );
6515+ }
6516+ }
6517+ return result;
6518+ }
6519+
6520+ inline std::string decode_path_component (const std::string &component) {
6521+ std::string result;
6522+ result.reserve (component.size ());
6523+
6524+ for (size_t i = 0 ; i < component.size (); i++) {
6525+ if (component[i] == ' %' && i + 1 < component.size ()) {
6526+ if (component[i + 1 ] == ' u' ) {
6527+ // Unicode %uXXXX encoding
6528+ auto val = 0 ;
6529+ if (detail::from_hex_to_i (component, i + 2 , 4 , val)) {
6530+ // 4 digits Unicode codes
6531+ char buff[4 ];
6532+ size_t len = detail::to_utf8 (val, buff);
6533+ if (len > 0 ) { result.append (buff, len); }
6534+ i += 5 ; // 'u0000'
6535+ } else {
6536+ result += component[i];
6537+ }
6538+ } else {
6539+ // Standard %XX encoding
6540+ auto val = 0 ;
6541+ if (detail::from_hex_to_i (component, i + 1 , 2 , val)) {
6542+ // 2 digits hex codes
6543+ result += static_cast <char >(val);
6544+ i += 2 ; // 'XX'
6545+ } else {
6546+ result += component[i];
6547+ }
6548+ }
6549+ } else {
6550+ result += component[i];
6551+ }
6552+ }
6553+ return result;
6554+ }
6555+
6556+ inline std::string encode_query_component (const std::string &component,
6557+ bool space_as_plus) {
6558+ std::string result;
6559+ result.reserve (component.size () * 3 );
6560+
6561+ for (size_t i = 0 ; i < component.size (); i++) {
6562+ auto c = static_cast <unsigned char >(component[i]);
6563+
6564+ // Unreserved characters per RFC 3986
6565+ if (std::isalnum (c) || c == ' -' || c == ' .' || c == ' _' || c == ' ~' ) {
6566+ result += static_cast <char >(c);
6567+ }
6568+ // Space handling
6569+ else if (c == ' ' ) {
6570+ if (space_as_plus) {
6571+ result += ' +' ;
6572+ } else {
6573+ result += " %20" ;
6574+ }
6575+ }
6576+ // Plus sign handling
6577+ else if (c == ' +' ) {
6578+ if (space_as_plus) {
6579+ result += " %2B" ;
6580+ } else {
6581+ result += static_cast <char >(c);
6582+ }
6583+ }
6584+ // Query-safe sub-delimiters (excluding & and = which are query delimiters)
6585+ else if (c == ' !' || c == ' $' || c == ' \' ' || c == ' (' || c == ' )' ||
6586+ c == ' *' || c == ' ,' || c == ' ;' ) {
6587+ result += static_cast <char >(c);
6588+ }
6589+ // Colon and @ are allowed in query
6590+ else if (c == ' :' || c == ' @' ) {
6591+ result += static_cast <char >(c);
6592+ }
6593+ // Forward slash is allowed in query values
6594+ else if (c == ' /' ) {
6595+ result += static_cast <char >(c);
6596+ }
6597+ // Question mark is allowed in query values (after first ?)
6598+ else if (c == ' ?' ) {
6599+ result += static_cast <char >(c);
6600+ } else {
6601+ result += ' %' ;
6602+ char hex[3 ];
6603+ snprintf (hex, sizeof (hex), " %02X" , c);
6604+ result.append (hex, 2 );
6605+ }
6606+ }
6607+ return result;
6608+ }
6609+
6610+ inline std::string decode_query_component (const std::string &component,
6611+ bool plus_as_space) {
6612+ std::string result;
6613+ result.reserve (component.size ());
6614+
6615+ for (size_t i = 0 ; i < component.size (); i++) {
6616+ if (component[i] == ' %' && i + 2 < component.size ()) {
6617+ std::string hex = component.substr (i + 1 , 2 );
6618+ char *end;
6619+ unsigned long value = std::strtoul (hex.c_str (), &end, 16 );
6620+ if (end == hex.c_str () + 2 ) {
6621+ result += static_cast <char >(value);
6622+ i += 2 ;
6623+ } else {
6624+ result += component[i];
6625+ }
6626+ } else if (component[i] == ' +' && plus_as_space) {
6627+ result += ' ' ; // + becomes space in form-urlencoded
6628+ } else {
6629+ result += component[i];
6630+ }
6631+ }
6632+ return result;
65236633}
65246634
65256635inline std::string append_query_params (const std::string &path,
@@ -7404,8 +7514,8 @@ inline bool Server::parse_request_line(const char *s, Request &req) const {
74047514 detail::divide (req.target , ' ?' ,
74057515 [&](const char *lhs_data, std::size_t lhs_size,
74067516 const char *rhs_data, std::size_t rhs_size) {
7407- req.path = detail::decode_path (
7408- std::string (lhs_data, lhs_size), false );
7517+ req.path =
7518+ decode_path_component ( std::string (lhs_data, lhs_size));
74097519 detail::parse_query_text (rhs_data, rhs_size, req.params );
74107520 });
74117521 }
@@ -8678,7 +8788,7 @@ inline bool ClientImpl::redirect(Request &req, Response &res, Error &error) {
86788788 if (next_host.empty ()) { next_host = host_; }
86798789 if (next_path.empty ()) { next_path = " /" ; }
86808790
8681- auto path = detail::decode_path (next_path, true ) + next_query;
8791+ auto path = decode_query_component (next_path, true ) + next_query;
86828792
86838793 // Same host redirect - use current client
86848794 if (next_scheme == scheme && next_host == host_ && next_port == port_) {
@@ -8966,15 +9076,28 @@ inline bool ClientImpl::write_request(Stream &strm, Request &req,
89669076 {
89679077 detail::BufferStream bstrm;
89689078
8969- const auto &path_with_query =
8970- req.params .empty () ? req.path
8971- : append_query_params (req.path , req.params );
9079+ // Extract path and query from req.path
9080+ std::string path_part, query_part;
9081+ auto query_pos = req.path .find (' ?' );
9082+ if (query_pos != std::string::npos) {
9083+ path_part = req.path .substr (0 , query_pos);
9084+ query_part = req.path .substr (query_pos + 1 );
9085+ } else {
9086+ path_part = req.path ;
9087+ query_part = " " ;
9088+ }
89729089
8973- const auto &path =
8974- path_encode_ ? detail::encode_path (path_with_query) : path_with_query;
9090+ // Encode path and query
9091+ auto path_with_query =
9092+ path_encode_ ? detail::encode_path (path_part) : path_part;
89759093
8976- detail::write_request_line (bstrm, req.method , path);
9094+ detail::parse_query_text (query_part, req.params );
9095+ if (!req.params .empty ()) {
9096+ path_with_query = append_query_params (path_with_query, req.params );
9097+ }
89779098
9099+ // Write request line and headers
9100+ detail::write_request_line (bstrm, req.method , path_with_query);
89789101 header_writer_ (bstrm, req.headers );
89799102
89809103 // Flush buffer
0 commit comments