From ea816c1c2fd47eab647d6fab96c9ca4bfeecd5bb Mon Sep 17 00:00:00 2001 From: Josh Durgin Date: Thu, 10 Oct 2013 15:50:39 -0700 Subject: [PATCH 1/8] rgw: skip read_policy checks for system_users A system user should still be able to examine suspended buckets, and get -ENOENT instead of -EACCESS for a deleted object. Fixes: #6616 Backport: dumpling Signed-off-by: Josh Durgin --- src/rgw/rgw_op.cc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/rgw/rgw_op.cc b/src/rgw/rgw_op.cc index aa7ff9bb3f5..c750276596f 100644 --- a/src/rgw/rgw_op.cc +++ b/src/rgw/rgw_op.cc @@ -269,7 +269,7 @@ static int read_policy(RGWRados *store, struct req_state *s, string oid = object; rgw_obj obj; - if (bucket_info.flags & BUCKET_SUSPENDED) { + if (!s->system_request && bucket_info.flags & BUCKET_SUSPENDED) { ldout(s->cct, 0) << "NOTICE: bucket " << bucket_info.bucket.name << " is suspended" << dendl; return -ERR_USER_SUSPENDED; } @@ -292,7 +292,7 @@ static int read_policy(RGWRados *store, struct req_state *s, if (ret < 0) return ret; string& owner = bucket_policy.get_owner().get_id(); - if (owner.compare(s->user.user_id) != 0 && + if (!s->system_request && owner.compare(s->user.user_id) != 0 && !bucket_policy.verify_permission(s->user.user_id, s->perm_mask, RGW_PERM_READ)) ret = -EACCES; else From e74776f4176470122485a79a4c07e9c12c9fc036 Mon Sep 17 00:00:00 2001 From: Josh Durgin Date: Thu, 24 Oct 2013 08:18:19 -0700 Subject: [PATCH 2/8] cls_log: always return final marker from log_list There's no reason to restrict returning the marker to the case where less than the whole log is returned, since there's already a truncated flag to tell the client what happened. Giving the client the last marker makes it easy to consume when the log entries do not contain their own marker. If the last marker is not returned, the client cannot get the last marker without racing with updates to the log. Backport: dumpling Signed-off-by: Josh Durgin --- src/cls/log/cls_log.cc | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/cls/log/cls_log.cc b/src/cls/log/cls_log.cc index 46ba357480c..7b254fdbea5 100644 --- a/src/cls/log/cls_log.cc +++ b/src/cls/log/cls_log.cc @@ -211,9 +211,8 @@ static int cls_log_list(cls_method_context_t hctx, bufferlist *in, bufferlist *o if (iter == keys.end()) done = true; - else - ret.marker = marker; + ret.marker = marker; ret.truncated = !done; ::encode(ret, *out); From c275912509255f8bb4c854e181318b45ab0f8564 Mon Sep 17 00:00:00 2001 From: Josh Durgin Date: Thu, 24 Oct 2013 08:26:19 -0700 Subject: [PATCH 3/8] rgw: include marker and truncated flag in data log list api Consumers of this api need to know their position in the log. It's readily available when fetching the log, so return it. Without the marker in this call, a client could not easily or efficiently figure out its position in the log, since it would require getting the global last marker in the log, and then reading all the log entries. This would be slow for large logs, and would be subject to races that would cause potentially very expensive duplicate work. Returning this atomically while fetching the log entries simplifies all of this. Fixes: #6615 Backport: dumpling Signed-off-by: Josh Durgin --- src/cls/log/cls_log_client.cc | 3 ++- src/cls/log/cls_log_client.h | 3 ++- src/rgw/rgw_bucket.cc | 10 +++++++--- src/rgw/rgw_bucket.h | 5 ++++- src/rgw/rgw_rados.cc | 8 ++++++-- src/rgw/rgw_rados.h | 3 ++- src/rgw/rgw_rest_log.cc | 24 ++++++++++++++++-------- src/rgw/rgw_rest_log.h | 4 +++- 8 files changed, 42 insertions(+), 18 deletions(-) diff --git a/src/cls/log/cls_log_client.cc b/src/cls/log/cls_log_client.cc index ea8adf11145..e5b47bf81f2 100644 --- a/src/cls/log/cls_log_client.cc +++ b/src/cls/log/cls_log_client.cc @@ -107,7 +107,8 @@ public: }; void cls_log_list(librados::ObjectReadOperation& op, utime_t& from, utime_t& to, - string& in_marker, int max_entries, list& entries, + const string& in_marker, int max_entries, + list& entries, string *out_marker, bool *truncated) { bufferlist inbl; diff --git a/src/cls/log/cls_log_client.h b/src/cls/log/cls_log_client.h index 3b4c96d1d3a..16229c992b9 100644 --- a/src/cls/log/cls_log_client.h +++ b/src/cls/log/cls_log_client.h @@ -18,7 +18,8 @@ void cls_log_add(librados::ObjectWriteOperation& op, const utime_t& timestamp, const string& section, const string& name, bufferlist& bl); void cls_log_list(librados::ObjectReadOperation& op, utime_t& from, utime_t& to, - string& in_marker, int max_entries, list& entries, + const string& in_marker, int max_entries, + list& entries, string *out_marker, bool *truncated); void cls_log_trim(librados::ObjectWriteOperation& op, const utime_t& from_time, const utime_t& to_time, diff --git a/src/rgw/rgw_bucket.cc b/src/rgw/rgw_bucket.cc index 3267bc51948..31afebe9ea3 100644 --- a/src/rgw/rgw_bucket.cc +++ b/src/rgw/rgw_bucket.cc @@ -1187,12 +1187,16 @@ int RGWDataChangesLog::add_entry(rgw_bucket& bucket) { } int RGWDataChangesLog::list_entries(int shard, utime_t& start_time, utime_t& end_time, int max_entries, - list& entries, string& marker, bool *truncated) { + list& entries, + const string& marker, + string *out_marker, + bool *truncated) { list log_entries; int ret = store->time_log_list(oids[shard], start_time, end_time, - max_entries, log_entries, marker, truncated); + max_entries, log_entries, marker, + out_marker, truncated); if (ret < 0) return ret; @@ -1220,7 +1224,7 @@ int RGWDataChangesLog::list_entries(utime_t& start_time, utime_t& end_time, int for (; marker.shard < num_shards && (int)entries.size() < max_entries; marker.shard++, marker.marker.clear()) { int ret = list_entries(marker.shard, start_time, end_time, max_entries - entries.size(), entries, - marker.marker, &truncated); + marker.marker, NULL, &truncated); if (ret == -ENOENT) { continue; } diff --git a/src/rgw/rgw_bucket.h b/src/rgw/rgw_bucket.h index 5ee6a9b41cd..47795403dc6 100644 --- a/src/rgw/rgw_bucket.h +++ b/src/rgw/rgw_bucket.h @@ -352,7 +352,10 @@ public: int add_entry(rgw_bucket& bucket); int renew_entries(); int list_entries(int shard, utime_t& start_time, utime_t& end_time, int max_entries, - list& entries, string& marker, bool *truncated); + list& entries, + const string& marker, + string *out_marker, + bool *truncated); int trim_entries(int shard_id, const utime_t& start_time, const utime_t& end_time, const string& start_marker, const string& end_marker); int trim_entries(const utime_t& start_time, const utime_t& end_time, diff --git a/src/rgw/rgw_rados.cc b/src/rgw/rgw_rados.cc index 20ca8d8eb8f..4d6f8ef4530 100644 --- a/src/rgw/rgw_rados.cc +++ b/src/rgw/rgw_rados.cc @@ -1586,7 +1586,10 @@ int RGWRados::time_log_add(const string& oid, list& entries) } int RGWRados::time_log_list(const string& oid, utime_t& start_time, utime_t& end_time, - int max_entries, list& entries, string& marker, bool *truncated) + int max_entries, list& entries, + const string& marker, + string *out_marker, + bool *truncated) { librados::IoCtx io_ctx; @@ -1596,7 +1599,8 @@ int RGWRados::time_log_list(const string& oid, utime_t& start_time, utime_t& end return r; librados::ObjectReadOperation op; - cls_log_list(op, start_time, end_time, marker, max_entries, entries, &marker, truncated); + cls_log_list(op, start_time, end_time, marker, max_entries, entries, + out_marker, truncated); bufferlist obl; diff --git a/src/rgw/rgw_rados.h b/src/rgw/rgw_rados.h index b37652d9f3f..874492ffe69 100644 --- a/src/rgw/rgw_rados.h +++ b/src/rgw/rgw_rados.h @@ -1382,7 +1382,8 @@ public: int time_log_add(const string& oid, list& entries); int time_log_add(const string& oid, const utime_t& ut, const string& section, const string& key, bufferlist& bl); int time_log_list(const string& oid, utime_t& start_time, utime_t& end_time, - int max_entries, list& entries, string& marker, bool *truncated); + int max_entries, list& entries, + const string& marker, string *out_marker, bool *truncated); int time_log_info(const string& oid, cls_log_header *header); int time_log_trim(const string& oid, const utime_t& start_time, const utime_t& end_time, const string& from_marker, const string& to_marker); diff --git a/src/rgw/rgw_rest_log.cc b/src/rgw/rgw_rest_log.cc index 74e3c445ee9..18f0697b0b2 100644 --- a/src/rgw/rgw_rest_log.cc +++ b/src/rgw/rgw_rest_log.cc @@ -471,10 +471,12 @@ void RGWOp_DATALog_List::execute() { } } - bool truncated; do { + // Note that last_marker is updated to be the marker of the last + // entry listed http_ret = store->data_log->list_entries(shard_id, ut_st, ut_et, - max_entries, entries, marker, &truncated); + max_entries, entries, marker, + &last_marker, &truncated); if (http_ret < 0) break; @@ -491,12 +493,18 @@ void RGWOp_DATALog_List::send_response() { if (http_ret < 0) return; - s->formatter->open_array_section("entries"); - for (list::iterator iter = entries.begin(); - iter != entries.end(); ++iter) { - rgw_data_change& entry = *iter; - encode_json("entry", entry, s->formatter); - flusher.flush(); + s->formatter->open_object_section("log_entries"); + s->formatter->dump_string("marker", last_marker); + s->formatter->dump_bool("truncated", truncated); + { + s->formatter->open_array_section("entries"); + for (list::iterator iter = entries.begin(); + iter != entries.end(); ++iter) { + rgw_data_change& entry = *iter; + encode_json("entry", entry, s->formatter); + flusher.flush(); + } + s->formatter->close_section(); } s->formatter->close_section(); flusher.flush(); diff --git a/src/rgw/rgw_rest_log.h b/src/rgw/rgw_rest_log.h index 2d60e289b84..404db84aa7b 100644 --- a/src/rgw/rgw_rest_log.h +++ b/src/rgw/rgw_rest_log.h @@ -175,9 +175,11 @@ public: class RGWOp_DATALog_List : public RGWRESTOp { list entries; + string last_marker; + bool truncated; int http_ret; public: - RGWOp_DATALog_List() : http_ret(0) {} + RGWOp_DATALog_List() : truncated(false), http_ret(0) {} ~RGWOp_DATALog_List() {} int check_caps(RGWUserCaps& caps) { From e0e8fb1b2b4a308b2a9317e10c6fd53ad48dbfaf Mon Sep 17 00:00:00 2001 From: Josh Durgin Date: Thu, 24 Oct 2013 08:34:24 -0700 Subject: [PATCH 4/8] rgw: update metadata log list to match data log list Send the last marker whether the log is truncated in the same format as data log list, so clients don't have more needless complexity handling the difference. Keep bucket index logs the same, since they contain the marker already, and are not used in exactly the same way metadata and data logs are. Backport: dumpling Signed-off-by: Josh Durgin --- src/rgw/rgw_admin.cc | 2 +- src/rgw/rgw_metadata.cc | 10 ++++++---- src/rgw/rgw_metadata.h | 4 +++- src/rgw/rgw_rest_log.cc | 22 ++++++++++++++-------- src/rgw/rgw_rest_log.h | 4 +++- 5 files changed, 27 insertions(+), 15 deletions(-) diff --git a/src/rgw/rgw_admin.cc b/src/rgw/rgw_admin.cc index b23bf3ba5d4..e19ed206c26 100644 --- a/src/rgw/rgw_admin.cc +++ b/src/rgw/rgw_admin.cc @@ -1980,7 +1980,7 @@ next: bool truncated; do { - int ret = meta_log->list_entries(handle, 1000, entries, &truncated); + int ret = meta_log->list_entries(handle, 1000, entries, NULL, &truncated); if (ret < 0) { cerr << "ERROR: meta_log->list_entries(): " << cpp_strerror(-ret) << std::endl; return -ret; diff --git a/src/rgw/rgw_metadata.cc b/src/rgw/rgw_metadata.cc index 23f73e26531..1dd6107dc90 100644 --- a/src/rgw/rgw_metadata.cc +++ b/src/rgw/rgw_metadata.cc @@ -109,9 +109,10 @@ void RGWMetadataLog::complete_list_entries(void *handle) { } int RGWMetadataLog::list_entries(void *handle, - int max_entries, - list& entries, - bool *truncated) { + int max_entries, + list& entries, + string *last_marker, + bool *truncated) { LogListCtx *ctx = static_cast(handle); if (!max_entries) { @@ -120,7 +121,8 @@ int RGWMetadataLog::list_entries(void *handle, } int ret = store->time_log_list(ctx->cur_oid, ctx->from_time, ctx->end_time, - max_entries, entries, ctx->marker, truncated); + max_entries, entries, ctx->marker, + last_marker, truncated); if ((ret < 0) && (ret != -ENOENT)) return ret; diff --git a/src/rgw/rgw_metadata.h b/src/rgw/rgw_metadata.h index df4d3f73afa..0145208dc28 100644 --- a/src/rgw/rgw_metadata.h +++ b/src/rgw/rgw_metadata.h @@ -150,7 +150,9 @@ public: void complete_list_entries(void *handle); int list_entries(void *handle, int max_entries, - list& entries, bool *truncated); + list& entries, + string *out_marker, + bool *truncated); int trim(int shard_id, const utime_t& from_time, const utime_t& end_time, const string& start_marker, const string& end_marker); int get_info(int shard_id, RGWMetadataLogInfo *info); diff --git a/src/rgw/rgw_rest_log.cc b/src/rgw/rgw_rest_log.cc index 18f0697b0b2..2de424fb6e5 100644 --- a/src/rgw/rgw_rest_log.cc +++ b/src/rgw/rgw_rest_log.cc @@ -79,9 +79,9 @@ void RGWOp_MDLog_List::execute() { meta_log->init_list_entries(shard_id, ut_st, ut_et, marker, &handle); - bool truncated; do { - http_ret = meta_log->list_entries(handle, max_entries, entries, &truncated); + http_ret = meta_log->list_entries(handle, max_entries, entries, + &last_marker, &truncated); if (http_ret < 0) break; @@ -100,12 +100,18 @@ void RGWOp_MDLog_List::send_response() { if (http_ret < 0) return; - s->formatter->open_array_section("entries"); - for (list::iterator iter = entries.begin(); - iter != entries.end(); ++iter) { - cls_log_entry& entry = *iter; - store->meta_mgr->dump_log_entry(entry, s->formatter); - flusher.flush(); + s->formatter->open_object_section("log_entries"); + s->formatter->dump_string("marker", last_marker); + s->formatter->dump_bool("truncated", truncated); + { + s->formatter->open_array_section("entries"); + for (list::iterator iter = entries.begin(); + iter != entries.end(); ++iter) { + cls_log_entry& entry = *iter; + store->meta_mgr->dump_log_entry(entry, s->formatter); + flusher.flush(); + } + s->formatter->close_section(); } s->formatter->close_section(); flusher.flush(); diff --git a/src/rgw/rgw_rest_log.h b/src/rgw/rgw_rest_log.h index 404db84aa7b..ff1bf3466d3 100644 --- a/src/rgw/rgw_rest_log.h +++ b/src/rgw/rgw_rest_log.h @@ -74,9 +74,11 @@ public: class RGWOp_MDLog_List : public RGWRESTOp { list entries; + string last_marker; + bool truncated; int http_ret; public: - RGWOp_MDLog_List() : http_ret(0) {} + RGWOp_MDLog_List() : truncated(false), http_ret(0) {} ~RGWOp_MDLog_List() {} int check_caps(RGWUserCaps& caps) { From dd308cd481b368f90a64220847b91fc233d92a59 Mon Sep 17 00:00:00 2001 From: Josh Durgin Date: Thu, 24 Oct 2013 08:37:25 -0700 Subject: [PATCH 5/8] rgw: move url escaping to a common place This is useful outside of the s3 interface. Rename url_escape() url_encode() for consistency with the exsting common url_decode() function. This is in preparation for the next commit, which needs to escape url-unsafe characters in another place. Backport: dumpling Signed-off-by: Josh Durgin --- src/rgw/rgw_common.cc | 53 +++++++++++++++++++++++++++++++++++++ src/rgw/rgw_common.h | 1 + src/rgw/rgw_rest_s3.cc | 60 +++--------------------------------------- 3 files changed, 57 insertions(+), 57 deletions(-) diff --git a/src/rgw/rgw_common.cc b/src/rgw/rgw_common.cc index 0b2d0035992..e51b08889cb 100644 --- a/src/rgw/rgw_common.cc +++ b/src/rgw/rgw_common.cc @@ -705,6 +705,59 @@ bool url_decode(string& src_str, string& dest_str) return true; } +static void escape_char(char c, string& dst) +{ + char buf[16]; + snprintf(buf, sizeof(buf), "%%%.2X", (unsigned int)c); + dst.append(buf); +} + +static bool char_needs_url_encoding(char c) +{ + if (c < 0x20 || c >= 0x7f) + return true; + + switch (c) { + case 0x20: + case 0x22: + case 0x23: + case 0x25: + case 0x26: + case 0x2B: + case 0x2C: + case 0x2F: + case 0x3A: + case 0x3B: + case 0x3C: + case 0x3E: + case 0x3D: + case 0x3F: + case 0x40: + case 0x5B: + case 0x5D: + case 0x5C: + case 0x5E: + case 0x60: + case 0x7B: + case 0x7D: + return true; + } + return false; +} + +void url_encode(const string& src, string& dst) +{ + const char *p = src.c_str(); + for (unsigned i = 0; i < src.size(); i++, p++) { + if (char_needs_url_encoding(*p)) { + escape_char(*p, dst); + continue; + } + + dst.append(p, 1); + } +} + string rgw_trim_whitespace(const string& src) { if (src.empty()) { diff --git a/src/rgw/rgw_common.h b/src/rgw/rgw_common.h index b3d39e57c2d..a7ef250b7d8 100644 --- a/src/rgw/rgw_common.h +++ b/src/rgw/rgw_common.h @@ -1257,6 +1257,7 @@ extern bool verify_object_permission(struct req_state *s, int perm); /** Convert an input URL into a sane object name * by converting %-escaped strings into characters, etc*/ extern bool url_decode(string& src_str, string& dest_str); +extern void url_encode(const string& src, string& dst); extern void calc_hmac_sha1(const char *key, int key_len, const char *msg, int msg_len, char *dest); diff --git a/src/rgw/rgw_rest_s3.cc b/src/rgw/rgw_rest_s3.cc index 8ea21d452e6..12bd377d8a0 100644 --- a/src/rgw/rgw_rest_s3.cc +++ b/src/rgw/rgw_rest_s3.cc @@ -1085,59 +1085,6 @@ int RGWPostObj_ObjStore_S3::get_data(bufferlist& bl) return bl.length(); } -static void escape_char(char c, string& dst) -{ - char buf[16]; - snprintf(buf, sizeof(buf), "%%%.2X", (unsigned int)c); - dst.append(buf); -} - -static bool char_needs_url_encoding(char c) -{ - if (c < 0x20 || c >= 0x7f) - return true; - - switch (c) { - case 0x20: - case 0x22: - case 0x23: - case 0x25: - case 0x26: - case 0x2B: - case 0x2C: - case 0x2F: - case 0x3A: - case 0x3B: - case 0x3C: - case 0x3E: - case 0x3D: - case 0x3F: - case 0x40: - case 0x5B: - case 0x5D: - case 0x5C: - case 0x5E: - case 0x60: - case 0x7B: - case 0x7D: - return true; - } - return false; -} - -static void url_escape(const string& src, string& dst) -{ - const char *p = src.c_str(); - for (unsigned i = 0; i < src.size(); i++, p++) { - if (char_needs_url_encoding(*p)) { - escape_char(*p, dst); - continue; - } - - dst.append(p, 1); - } -} - void RGWPostObj_ObjStore_S3::send_response() { if (ret == 0 && parts.count("success_action_redirect")) { @@ -1154,11 +1101,10 @@ void RGWPostObj_ObjStore_S3::send_response() string etag_url; - url_escape(s->bucket_name_str, bucket); - url_escape(s->object_str, key); - url_escape(etag_str, etag_url); + url_encode(s->bucket_name_str, bucket); + url_encode(s->object_str, key); + url_encode(etag_str, etag_url); - redirect.append("?bucket="); redirect.append(bucket); redirect.append("&key="); From ec45b3b88c485140781b23d2c4f582f2cc26ea43 Mon Sep 17 00:00:00 2001 From: Josh Durgin Date: Thu, 24 Oct 2013 08:42:48 -0700 Subject: [PATCH 6/8] rgw: escape bucket and object names in StreamReadRequests This fixes copy operations for objects that contain unsafe characters, like a newline, which would return a 403 otherwise, since the GET to the source rgw would be unable to verify the signature on a partially valid bucket name. Fixes: #6604 Backport: dumpling Signed-off-by: Josh Durgin --- src/rgw/rgw_rest_client.cc | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/rgw/rgw_rest_client.cc b/src/rgw/rgw_rest_client.cc index ea80b5b84f8..7f4e2b6582d 100644 --- a/src/rgw/rgw_rest_client.cc +++ b/src/rgw/rgw_rest_client.cc @@ -540,7 +540,10 @@ int RGWRESTStreamWriteRequest::complete(string& etag, time_t *mtime) int RGWRESTStreamReadRequest::get_obj(RGWAccessKey& key, map& extra_headers, rgw_obj& obj) { - string resource = obj.bucket.name + "/" + obj.object; + string urlsafe_bucket, urlsafe_object; + url_encode(obj.bucket.name, urlsafe_bucket); + url_encode(obj.object, urlsafe_object); + string resource = urlsafe_bucket + "/" + urlsafe_object; string new_url = url; if (new_url[new_url.size() - 1] != '/') new_url.append("/"); From f9a6d71904796817b6df6b1ae7818070c0f34a8c Mon Sep 17 00:00:00 2001 From: Josh Durgin Date: Thu, 24 Oct 2013 08:46:31 -0700 Subject: [PATCH 7/8] radosgw-admin: remove unused function escape_str() This was added before formatters were used for dumping logs. Signed-off-by: Josh Durgin --- src/rgw/rgw_admin.cc | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/src/rgw/rgw_admin.cc b/src/rgw/rgw_admin.cc index e19ed206c26..a14f1a2c763 100644 --- a/src/rgw/rgw_admin.cc +++ b/src/rgw/rgw_admin.cc @@ -468,27 +468,6 @@ ReplicaLogType get_replicalog_type(const string& name) { return ReplicaLog_Invalid; } -string escape_str(string& src, char c) -{ - int pos = 0; - string dest; - - do { - int new_pos = src.find(c, pos); - if (new_pos >= 0) { - dest += src.substr(pos, new_pos - pos); - dest += "\\"; - dest += c; - } else { - dest += src.substr(pos); - return dest; - } - pos = new_pos + 1; - } while (pos < (int)src.size()); - - return dest; -} - static void show_user_info(RGWUserInfo& info, Formatter *formatter) { encode_json("user_info", info, formatter); From cfe845115bccf574e607899014cdbf2d213c6fb0 Mon Sep 17 00:00:00 2001 From: Josh Durgin Date: Thu, 24 Oct 2013 09:47:16 -0700 Subject: [PATCH 8/8] rgw: eliminate one unnecessary case statement 0x21 '!' is the first character that doesn't need encoding, so we can expand the lower bound check. Signed-off-by: Josh Durgin --- src/rgw/rgw_common.cc | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/rgw/rgw_common.cc b/src/rgw/rgw_common.cc index e51b08889cb..cb87c18b93d 100644 --- a/src/rgw/rgw_common.cc +++ b/src/rgw/rgw_common.cc @@ -714,11 +714,10 @@ static void escape_char(char c, string& dst) static bool char_needs_url_encoding(char c) { - if (c < 0x20 || c >= 0x7f) + if (c <= 0x20 || c >= 0x7f) return true; switch (c) { - case 0x20: case 0x22: case 0x23: case 0x25: