@@ -2030,7 +2030,7 @@ inline size_t get_header_value_u64(const Headers &headers,
2030
2030
inline size_t get_header_value_u64 (const Headers &headers,
2031
2031
const std::string &key, size_t def,
2032
2032
size_t id) {
2033
- bool dummy = false ;
2033
+ auto dummy = false ;
2034
2034
return get_header_value_u64 (headers, key, def, id, dummy);
2035
2035
}
2036
2036
@@ -2301,15 +2301,19 @@ std::string hosted_at(const std::string &hostname);
2301
2301
2302
2302
void hosted_at (const std::string &hostname, std::vector<std::string> &addrs);
2303
2303
2304
+ // JavaScript-style URL encoding/decoding functions
2304
2305
std::string encode_uri_component (const std::string &value);
2305
-
2306
2306
std::string encode_uri (const std::string &value);
2307
-
2308
2307
std::string decode_uri_component (const std::string &value);
2309
-
2310
2308
std::string decode_uri (const std::string &value);
2311
2309
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 );
2313
2317
2314
2318
std::string append_query_params (const std::string &path, const Params ¶ms);
2315
2319
@@ -2352,8 +2356,6 @@ struct FileStat {
2352
2356
int ret_ = -1 ;
2353
2357
};
2354
2358
2355
- std::string decode_path (const std::string &s, bool convert_plus_to_space);
2356
-
2357
2359
std::string trim_copy (const std::string &s);
2358
2360
2359
2361
void divide (
@@ -2854,43 +2856,6 @@ inline std::string encode_path(const std::string &s) {
2854
2856
return result;
2855
2857
}
2856
2858
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
-
2894
2859
inline std::string file_extension (const std::string &path) {
2895
2860
std::smatch m;
2896
2861
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) {
4615
4580
case_ignore::equal (key, " Referer" )) {
4616
4581
fn (key, val);
4617
4582
} else {
4618
- fn (key, decode_path (val, false ));
4583
+ fn (key, decode_path_component (val));
4619
4584
}
4620
4585
4621
4586
return true ;
@@ -5263,9 +5228,9 @@ inline std::string params_to_query_str(const Params ¶ms) {
5263
5228
5264
5229
for (auto it = params.begin (); it != params.end (); ++it) {
5265
5230
if (it != params.begin ()) { query += " &" ; }
5266
- query += it->first ;
5231
+ query += encode_query_component ( it->first ) ;
5267
5232
query += " =" ;
5268
- query += httplib::encode_uri_component (it->second );
5233
+ query += encode_query_component (it->second );
5269
5234
}
5270
5235
return query;
5271
5236
}
@@ -5288,7 +5253,7 @@ inline void parse_query_text(const char *data, std::size_t size,
5288
5253
});
5289
5254
5290
5255
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));
5292
5257
}
5293
5258
});
5294
5259
}
@@ -5611,7 +5576,7 @@ class FormDataParser {
5611
5576
5612
5577
std::smatch m2;
5613
5578
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...
5615
5580
} else {
5616
5581
is_valid_ = false ;
5617
5582
return false ;
@@ -6517,9 +6482,154 @@ inline std::string decode_uri(const std::string &value) {
6517
6482
return result;
6518
6483
}
6519
6484
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;
6523
6633
}
6524
6634
6525
6635
inline 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 {
7404
7514
detail::divide (req.target , ' ?' ,
7405
7515
[&](const char *lhs_data, std::size_t lhs_size,
7406
7516
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));
7409
7519
detail::parse_query_text (rhs_data, rhs_size, req.params );
7410
7520
});
7411
7521
}
@@ -8678,7 +8788,7 @@ inline bool ClientImpl::redirect(Request &req, Response &res, Error &error) {
8678
8788
if (next_host.empty ()) { next_host = host_; }
8679
8789
if (next_path.empty ()) { next_path = " /" ; }
8680
8790
8681
- auto path = detail::decode_path (next_path, true ) + next_query;
8791
+ auto path = decode_query_component (next_path, true ) + next_query;
8682
8792
8683
8793
// Same host redirect - use current client
8684
8794
if (next_scheme == scheme && next_host == host_ && next_port == port_) {
@@ -8966,15 +9076,28 @@ inline bool ClientImpl::write_request(Stream &strm, Request &req,
8966
9076
{
8967
9077
detail::BufferStream bstrm;
8968
9078
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
+ }
8972
9089
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;
8975
9093
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
+ }
8977
9098
9099
+ // Write request line and headers
9100
+ detail::write_request_line (bstrm, req.method , path_with_query);
8978
9101
header_writer_ (bstrm, req.headers );
8979
9102
8980
9103
// Flush buffer
0 commit comments