ithewei il y a 3 ans
Parent
commit
80db2c4fd0

+ 1 - 0
Makefile

@@ -223,6 +223,7 @@ unittest: prepare
 	$(CC)  -g -Wall -O0 -std=c99   -I. -Iutil            -o bin/sha1              unittest/sha1_test.c          util/sha1.c
 	$(CXX) -g -Wall -O0 -std=c++11 -I. -Ibase -Icpputil  -o bin/hstring_test      unittest/hstring_test.cpp     cpputil/hstring.cpp
 	$(CXX) -g -Wall -O0 -std=c++11 -I. -Ibase -Icpputil  -o bin/hpath_test        unittest/hpath_test.cpp       cpputil/hpath.cpp
+	$(CXX) -g -Wall -O0 -std=c++11 -I. -Ibase -Icpputil  -o bin/hurl_test         unittest/hurl_test.cpp        cpputil/hurl.cpp base/hbase.c
 	$(CXX) -g -Wall -O0 -std=c++11 -I. -Ibase -Icpputil  -o bin/ls                unittest/listdir_test.cpp     cpputil/hdir.cpp
 	$(CXX) -g -Wall -O0 -std=c++11 -I. -Ibase -Icpputil  -o bin/ifconfig          unittest/ifconfig_test.cpp    cpputil/ifconfig.cpp
 	$(CXX) -g -Wall -O0 -std=c++11 -I. -Ibase -Icpputil  -o bin/defer_test        unittest/defer_test.cpp

+ 90 - 0
base/hbase.c

@@ -167,6 +167,16 @@ bool hv_strcontains(const char* str, const char* sub) {
     return strstr(str, sub) != NULL;
 }
 
+char* hv_strnchr(const char* s, char c, size_t n) {
+    assert(s != NULL);
+    const char* p = s;
+    while (*p != '\0' && n-- > 0) {
+        if (*p == c) return (char*)p;
+        ++p;
+    }
+    return NULL;
+}
+
 char* hv_strrchr_dir(const char* filepath) {
     char* p = (char*)filepath;
     while (*p) ++p;
@@ -409,3 +419,83 @@ time_t hv_parse_time(const char* str) {
     }
     return time + n;
 }
+
+int hv_parse_url(hurl_t* stURL, const char* strURL) {
+    if (stURL == NULL || strURL == NULL) return -1;
+    memset(stURL, 0, sizeof(hurl_t));
+    const char* begin = strURL;
+    const char* end = strURL;
+    while (*end != '\0') ++end;
+    if (end - begin > 65535) return -2;
+    // scheme://
+    const char* sp = strURL;
+    const char* ep = strstr(sp, "://");
+    if (ep) {
+        // stURL->fields[HV_URL_SCHEME].off = sp - begin;
+        stURL->fields[HV_URL_SCHEME].len = ep - sp;
+        sp = ep + 3;
+    }
+    // user:pswd@host:port
+    ep = strchr(sp, '/');
+    if (ep == NULL) ep = end;
+    const char* user = sp;
+    const char* host = sp;
+    const char* pos = hv_strnchr(sp, '@', ep - sp);
+    if (pos) {
+        // user:pswd
+        const char* pswd = hv_strnchr(user, ':', pos - user);
+        if (pswd) {
+            stURL->fields[HV_URL_PASSWORD].off = pswd + 1 - begin;
+            stURL->fields[HV_URL_PASSWORD].len = pos - pswd - 1;
+        } else {
+            pswd = pos;
+        }
+        stURL->fields[HV_URL_USERNAME].off = user - begin;
+        stURL->fields[HV_URL_USERNAME].len = pswd - user;
+        // @
+        host = pos + 1;
+    }
+    // port
+    const char* port = hv_strnchr(host, ':', ep - host);
+    if (port) {
+        stURL->fields[HV_URL_PORT].off = port + 1 - begin;
+        stURL->fields[HV_URL_PORT].len = ep - port - 1;
+        // atoi
+        for (unsigned short i = 1; i <= stURL->fields[HV_URL_PORT].len; ++i) {
+            stURL->port = stURL->port * 10 + (port[i] - '0');
+        }
+    } else {
+        port = ep;
+        // set default port
+        stURL->port = 80;
+        if (stURL->fields[HV_URL_SCHEME].len > 0) {
+            if (strncmp(strURL, "https://", 8) == 0) {
+                stURL->port = 443;
+            }
+        }
+    }
+    // host
+    stURL->fields[HV_URL_HOST].off = host - begin;
+    stURL->fields[HV_URL_HOST].len = port - host;
+    if (ep == end) return 0;
+    // /path
+    sp = ep;
+    ep = strchr(sp, '?');
+    if (ep == NULL) ep = end;
+    stURL->fields[HV_URL_PATH].off = sp - begin;
+    stURL->fields[HV_URL_PATH].len = ep - sp;
+    if (ep == end) return 0;
+    // ?query
+    sp = ep + 1;
+    ep = strchr(sp, '#');
+    if (ep == NULL) ep = end;
+    stURL->fields[HV_URL_QUERY].off = sp - begin;
+    stURL->fields[HV_URL_QUERY].len = ep - sp;
+    if (ep == end) return 0;
+    // #fragment
+    sp = ep + 1;
+    ep = end;
+    stURL->fields[HV_URL_FRAGMENT].off = sp - begin;
+    stURL->fields[HV_URL_FRAGMENT].len = ep - sp;
+    return 0;
+}

+ 25 - 0
base/hbase.h

@@ -80,6 +80,8 @@ HV_EXPORT char* hv_strncat(char* dest, const char* src, size_t n);
 #define strlcat hv_strncat
 #endif
 
+HV_EXPORT char* hv_strnchr(const char* s, char c, size_t n);
+
 #define hv_strrchr_dot(str) strrchr(str, '.')
 HV_EXPORT char* hv_strrchr_dir(const char* filepath);
 
@@ -113,6 +115,29 @@ HV_EXPORT size_t hv_parse_size(const char* str);
 // 1w2d3h4m5s => ?s
 HV_EXPORT time_t hv_parse_time(const char* str);
 
+// scheme:[//[user[:password]@]host[:port]][/path][?query][#fragment]
+typedef enum {
+    HV_URL_SCHEME,
+    HV_URL_USERNAME,
+    HV_URL_PASSWORD,
+    HV_URL_HOST,
+    HV_URL_PORT,
+    HV_URL_PATH,
+    HV_URL_QUERY,
+    HV_URL_FRAGMENT,
+    HV_URL_FIELD_NUM,
+} hurl_field_e;
+
+typedef struct hurl_s {
+    struct {
+        unsigned short off;
+        unsigned short len;
+    } fields[HV_URL_FIELD_NUM];
+    unsigned short port;
+} hurl_t;
+
+HV_EXPORT int hv_parse_url(hurl_t* stURL, const char* strURL);
+
 END_EXTERN_C
 
 #endif // HV_BASE_H_

+ 89 - 4
cpputil/hurl.cpp

@@ -1,6 +1,7 @@
 #include "hurl.h"
 
 #include "hdef.h"
+#include "hbase.h"
 
 /*
 static bool Curl_isunreserved(unsigned char in)
@@ -46,10 +47,10 @@ static inline unsigned char hex2i(char hex) {
         hex <= 'F' ? hex - 'A' + 10 : hex - 'a' + 10;
 }
 
-std::string url_escape(const char* istr, const char* unescaped_chars) {
+std::string HUrl::escape(const std::string& str, const char* unescaped_chars) {
     std::string ostr;
     static char tab[] = "0123456789ABCDEF";
-    const unsigned char* p = reinterpret_cast<const unsigned char*>(istr);
+    const unsigned char* p = reinterpret_cast<const unsigned char*>(str.c_str());
     char szHex[4] = "%00";
     while (*p != '\0') {
         if (is_unambiguous(*p) || char_in_str(*p, unescaped_chars)) {
@@ -65,9 +66,9 @@ std::string url_escape(const char* istr, const char* unescaped_chars) {
     return ostr;
 }
 
-std::string url_unescape(const char* istr) {
+std::string HUrl::unescape(const std::string& str) {
     std::string ostr;
-    const char* p = istr;
+    const char* p = str.c_str();
     while (*p != '\0') {
         if (*p == '%' &&
             IS_HEX(p[1]) &&
@@ -82,3 +83,87 @@ std::string url_unescape(const char* istr) {
     }
     return ostr;
 }
+
+
+bool HUrl::parse(const std::string& url) {
+    reset();
+    this->url = url;
+    hurl_t stURL;
+    if (hv_parse_url(&stURL, url.c_str()) != 0) {
+        return false;
+    }
+    int len = stURL.fields[HV_URL_SCHEME].len;
+    if (len > 0) {
+        scheme = url.substr(stURL.fields[HV_URL_SCHEME].off, len);
+    }
+    len = stURL.fields[HV_URL_USERNAME].len;
+    if (len > 0) {
+        username = url.substr(stURL.fields[HV_URL_USERNAME].off, len);
+        len = stURL.fields[HV_URL_PASSWORD].len;
+        if (len > 0) {
+            password = url.substr(stURL.fields[HV_URL_PASSWORD].off, len);
+        }
+    }
+    len = stURL.fields[HV_URL_HOST].len;
+    if (len > 0) {
+        host = url.substr(stURL.fields[HV_URL_HOST].off, len);
+    }
+    port = stURL.port;
+    len = stURL.fields[HV_URL_PATH].len;
+    if (len > 0) {
+        path = url.substr(stURL.fields[HV_URL_PATH].off, len);
+    } else {
+        path = "/";
+    }
+    len = stURL.fields[HV_URL_QUERY].len;
+    if (len > 0) {
+        query = url.substr(stURL.fields[HV_URL_QUERY].off, len);
+    }
+    len = stURL.fields[HV_URL_FRAGMENT].len;
+    if (len > 0) {
+        fragment = url.substr(stURL.fields[HV_URL_FRAGMENT].off, len);
+    }
+    return true;
+}
+
+const std::string& HUrl::dump() {
+    url.clear();
+    // scheme://
+    if (!scheme.empty()) {
+        url += scheme;
+        url += "://";
+    }
+    // user:pswd@
+    if (!username.empty()) {
+        url += username;
+        if (!password.empty()) {
+            url += ":";
+            url += password;
+        }
+        url += "@";
+    }
+    // host:port
+    if (!host.empty()) {
+        url += host;
+        if (port != 80 && port != 443) {
+            char buf[16] = {0};
+            snprintf(buf, sizeof(buf), ":%d", port);
+            url += port;
+        }
+    }
+    // /path
+    if (!path.empty()) {
+        url += path;
+    }
+    // ?query
+    if (!query.empty()) {
+        url += '?';
+        url += query;
+    }
+    // #fragment
+    if (!fragment.empty()) {
+        url += '#';
+        url += fragment;
+    }
+    return url;
+}

+ 36 - 3
cpputil/hurl.h

@@ -1,11 +1,44 @@
 #ifndef HV_URL_H_
 #define HV_URL_H_
 
-#include <string>
+#include <string> // import std::string
 
 #include "hexport.h"
 
-HV_EXPORT std::string url_escape(const char* istr, const char* unescaped_chars = "");
-HV_EXPORT std::string url_unescape(const char* istr);
+class HV_EXPORT HUrl {
+public:
+    static std::string escape(const std::string& str, const char* unescaped_chars = "");
+    static std::string unescape(const std::string& str);
+    static inline std::string escapeUrl(const std::string& url) {
+        return escape(url, ":/@?=&#");
+    }
+
+    HUrl() : port(0) {}
+    ~HUrl() {}
+
+    bool parse(const std::string& url);
+    const std::string& dump();
+    void reset() {
+        url.clear();
+        scheme.clear();
+        username.clear();
+        password.clear();
+        host.clear();
+        port = 0;
+        path.clear();
+        query.clear();
+        fragment.clear();
+    }
+
+    std::string url;
+    std::string scheme;
+    std::string username;
+    std::string password;
+    std::string host;
+    int         port;
+    std::string path;
+    std::string query;
+    std::string fragment;
+};
 
 #endif // HV_URL_H_

+ 6 - 2
docs/API.md

@@ -106,6 +106,7 @@
 - hv_strstartswith
 - hv_strendswith
 - hv_strcontains
+- hv_strnchr
 - hv_strrchr_dot
 - hv_strrchr_dir
 - hv_basename
@@ -126,6 +127,7 @@
 - hv_getboolean
 - hv_parse_size
 - hv_parse_time
+- hv_parse_url
 
 ### hversion.h
 - hv_version
@@ -306,8 +308,10 @@
 - listdir
 
 ### hurl.h
-- url_escape
-- url_unescape
+- HUrl::escape
+- HUrl::unescape
+- HUrl::parse
+- HUrl::dump
 
 ### hscope.h
 - defer

+ 1 - 2
examples/curl.cpp

@@ -263,8 +263,7 @@ int main(int argc, char* argv[]) {
             req.method = HTTP_POST;
         }
     }
-    // http://127.0.0.1:8080@user:pswd/path?k1=v1&k2=v2#fragment
-    req.url = url_escape(url, ":/@?=&#");
+    req.url = HUrl::escapeUrl(url);
     req.http_cb = [](HttpMessage* res, http_parser_state state, const char* data, size_t size) {
         if (state == HP_HEADERS_COMPLETE) {
             if (verbose) {

+ 10 - 13
http/HttpMessage.cpp

@@ -5,7 +5,6 @@
 #include "htime.h"
 #include "hlog.h"
 #include "hurl.h"
-#include "http_parser.h" // for http_parser_url
 
 using namespace hv;
 
@@ -528,15 +527,14 @@ query:
 
 void HttpRequest::ParseUrl() {
     DumpUrl();
-    http_parser_url parser;
-    http_parser_url_init(&parser);
-    http_parser_parse_url(url.c_str(), url.size(), 0, &parser);
+    hurl_t parser;
+    hv_parse_url(&parser, url.c_str());
     // scheme
-    std::string scheme_ = url.substr(parser.field_data[UF_SCHEMA].off, parser.field_data[UF_SCHEMA].len);
+    std::string scheme_ = url.substr(parser.fields[HV_URL_SCHEME].off, parser.fields[HV_URL_SCHEME].len);
     // host
     std::string host_(host);
-    if (parser.field_set & (1<<UF_HOST)) {
-        host_ = url.substr(parser.field_data[UF_HOST].off, parser.field_data[UF_HOST].len);
+    if (parser.fields[HV_URL_HOST].len > 0) {
+        host_ = url.substr(parser.fields[HV_URL_HOST].off, parser.fields[HV_URL_HOST].len);
     }
     // port
     int port_ = parser.port ? parser.port : strcmp(scheme_.c_str(), "https") ? DEFAULT_HTTP_PORT : DEFAULT_HTTPS_PORT;
@@ -547,12 +545,12 @@ void HttpRequest::ParseUrl() {
     }
     FillHost(host_.c_str(), port_);
     // path
-    if (parser.field_set & (1<<UF_PATH)) {
-        path = url.substr(parser.field_data[UF_PATH].off);
+    if (parser.fields[HV_URL_PATH].len > 0) {
+        path = url.substr(parser.fields[HV_URL_PATH].off);
     }
     // query
-    if (parser.field_set & (1<<UF_QUERY)) {
-        parse_query_params(url.c_str()+parser.field_data[UF_QUERY].off, query_params);
+    if (parser.fields[HV_URL_QUERY].len > 0) {
+        parse_query_params(url.c_str()+parser.fields[HV_URL_QUERY].off, query_params);
     }
 }
 
@@ -560,8 +558,7 @@ std::string HttpRequest::Path() {
     const char* s = path.c_str();
     const char* e = s;
     while (*e && *e != '?' && *e != '#') ++e;
-    std::string path_no_query(s, e);
-    return url_unescape(path_no_query.c_str());
+    return HUrl::unescape(std::string(s, e));
 }
 
 void HttpRequest::FillHost(const char* host, int port) {

+ 4 - 4
http/http_content.cpp

@@ -12,9 +12,9 @@ std::string dump_query_params(const QueryParams& query_params) {
         if (query_string.size() != 0) {
             query_string += '&';
         }
-        query_string += url_escape(pair.first.c_str());
+        query_string += HUrl::escape(pair.first);
         query_string += '=';
-        query_string += url_escape(pair.second.c_str());
+        query_string += HUrl::escape(pair.second);
     }
     return query_string;
 }
@@ -37,7 +37,7 @@ int parse_query_params(const char* query_string, QueryParams& query_params) {
             if (key_len && value_len) {
                 std::string strkey = std::string(key, key_len);
                 std::string strvalue = std::string(value, value_len);
-                query_params[url_unescape(strkey.c_str())] = url_unescape(strvalue.c_str());
+                query_params[HUrl::unescape(strkey)] = HUrl::unescape(strvalue);
                 key_len = value_len = 0;
             }
             state = s_key;
@@ -55,7 +55,7 @@ int parse_query_params(const char* query_string, QueryParams& query_params) {
     if (key_len && value_len) {
         std::string strkey = std::string(key, key_len);
         std::string strvalue = std::string(value, value_len);
-        query_params[url_unescape(strkey.c_str())] = url_unescape(strvalue.c_str());
+        query_params[HUrl::unescape(strkey)] = HUrl::unescape(strvalue);
         key_len = value_len = 0;
     }
     return query_params.size() == 0 ? -1 : 0;

+ 2 - 0
http/http_parser.c

@@ -2154,6 +2154,7 @@ http_errno_description(enum http_errno err) {
   return http_strerror_tab[err].description;
 }
 
+#ifdef WITH_HTTP_PRASER_URL
 static enum http_host_state
 http_parse_host_char(enum http_host_state s, const char ch) {
   switch(s) {
@@ -2445,6 +2446,7 @@ http_parser_parse_url(const char *buf, size_t buflen, int is_connect,
 
   return 0;
 }
+#endif
 
 void
 http_parser_pause(http_parser *parser, int paused) {

+ 4 - 0
http/http_parser.h

@@ -212,6 +212,7 @@ struct http_parser_settings {
 };
 
 
+#ifdef WITH_HTTP_PRASER_URL
 enum http_parser_url_fields
   { UF_SCHEMA           = 0
   , UF_HOST             = 1
@@ -240,6 +241,7 @@ struct http_parser_url {
     uint16_t len;               /* Length of run in buffer */
   } field_data[UF_MAX];
 };
+#endif
 
 
 /* Returns the library version. Bits 16-23 contain the major version number,
@@ -284,6 +286,7 @@ const char *http_errno_name(enum http_errno err);
 /* Return a string description of the given error */
 const char *http_errno_description(enum http_errno err);
 
+#ifdef WITH_HTTP_PRASER_URL
 /* Initialize all http_parser_url members to 0 */
 void http_parser_url_init(struct http_parser_url *u);
 
@@ -291,6 +294,7 @@ void http_parser_url_init(struct http_parser_url *u);
 int http_parser_parse_url(const char *buf, size_t buflen,
                           int is_connect,
                           struct http_parser_url *u);
+#endif
 
 /* Pause or un-pause the parser; a nonzero value pauses */
 void http_parser_pause(http_parser *parser, int paused);

+ 4 - 0
http/server/HttpContext.h

@@ -32,6 +32,10 @@ struct HV_EXPORT HttpContext {
         return request->Path();
     }
 
+    std::string fullpath() {
+        return request->FullPath();
+    }
+
     std::string host() {
         return request->Host();
     }

+ 0 - 1
http/server/HttpHandler.cpp

@@ -163,7 +163,6 @@ int HttpHandler::HandleHttpRequest() {
     pReq->scheme = ssl ? "https" : "http";
     pReq->client_addr.ip = ip;
     pReq->client_addr.port = port;
-    pReq->Host();
     pReq->ParseUrl();
     // NOTE: Not all users want to parse body, we comment it out.
     // pReq->ParseBody();

+ 1 - 0
scripts/unittest.sh

@@ -19,6 +19,7 @@ bin/sha1
 bin/defer_test
 bin/hstring_test
 bin/hpath_test
+bin/hurl_test
 # bin/hatomic_test
 # bin/hatomic_cpp_test
 # bin/hthread_test

+ 4 - 0
unittest/CMakeLists.txt

@@ -48,6 +48,9 @@ target_include_directories(hstring_test PRIVATE .. ../base ../cpputil)
 add_executable(hpath_test hpath_test.cpp ../cpputil/hpath.cpp)
 target_include_directories(hpath_test PRIVATE .. ../base ../cpputil)
 
+add_executable(hurl_test hurl_test.cpp ../cpputil/hurl.cpp ../base/hbase.c)
+target_include_directories(hurl_test PRIVATE .. ../base ../cpputil)
+
 add_executable(ls listdir_test.cpp ../cpputil/hdir.cpp)
 target_include_directories(ls PRIVATE .. ../base ../cpputil)
 
@@ -101,6 +104,7 @@ add_custom_target(unittest DEPENDS
     sha1
     hstring_test
     hpath_test
+    hurl_test
     ls
     ifconfig
     defer_test

+ 84 - 3
unittest/hbase_test.c

@@ -1,6 +1,10 @@
 #include "hbase.h"
 
 int main(int argc, char* argv[]) {
+    char buf[16] = {0};
+    printf("hv_rand(10, 99) -> %d\n", hv_rand(10, 99));
+    printf("hv_random_string(buf, 10) -> %s\n", hv_random_string(buf, 10));
+
     assert(hv_getboolean("1"));
     assert(hv_getboolean("yes"));
 
@@ -20,9 +24,86 @@ int main(int argc, char* argv[]) {
             3 * 60 +
             4);
 
-    char buf[16] = {0};
-    printf("%d\n", hv_rand(10, 99));
-    printf("%s\n", hv_random_string(buf, 10));
+    const char* test_urls[] = {
+        "http://user:pswd@www.example.com:80/path?query#fragment",
+        "http://user:pswd@www.example.com/path?query#fragment",
+        "http://www.example.com/path?query#fragment",
+        "http://www.example.com/path?query",
+        "http://www.example.com/path",
+        "www.example.com/path",
+        "/path",
+    };
+    hurl_t stURL;
+    for (int i = 0; i < ARRAY_SIZE(test_urls); ++i) {
+        const char* strURL = test_urls[i];
+        printf("%s =>\n", strURL);
+        hv_parse_url(&stURL, strURL);
+        assert(stURL.port == 80);
+        // scheme://
+        if (stURL.fields[HV_URL_SCHEME].len > 0) {
+            const char* scheme = strURL + stURL.fields[HV_URL_SCHEME].off;
+            int len = stURL.fields[HV_URL_SCHEME].len;
+            assert(len == 4);
+            assert(strncmp(scheme, "http", len) == 0);
+            printf("%.*s://", len, scheme);
+        }
+        // user:pswd@
+        if (stURL.fields[HV_URL_USERNAME].len > 0) {
+            const char* user = strURL + stURL.fields[HV_URL_USERNAME].off;
+            int len = stURL.fields[HV_URL_USERNAME].len;
+            assert(len == 4);
+            assert(strncmp(user, "user", len) == 0);
+            printf("%.*s", len, user);
+            if (stURL.fields[HV_URL_PASSWORD].len > 0) {
+                const char* pswd = strURL + stURL.fields[HV_URL_PASSWORD].off;
+                int len = stURL.fields[HV_URL_PASSWORD].len;
+                assert(len == 4);
+                assert(strncmp(pswd, "pswd", len) == 0);
+                printf(":%.*s", len, pswd);
+            }
+            printf("@");
+        }
+        // host:port
+        if (stURL.fields[HV_URL_HOST].len > 0) {
+            const char* host = strURL + stURL.fields[HV_URL_HOST].off;
+            int len = stURL.fields[HV_URL_HOST].len;
+            assert(len == strlen("www.example.com"));
+            assert(strncmp(host, "www.example.com", len) == 0);
+            printf("%.*s", len, host);
+            if (stURL.fields[HV_URL_PORT].len > 0) {
+                const char* port = strURL + stURL.fields[HV_URL_PORT].off;
+                int len = stURL.fields[HV_URL_PORT].len;
+                assert(len == 2);
+                assert(strncmp(port, "80", len) == 0);
+                printf(":%.*s", len, port);
+            }
+        }
+        // /path
+        if (stURL.fields[HV_URL_PATH].len > 0) {
+            const char* path = strURL + stURL.fields[HV_URL_PATH].off;
+            int len = stURL.fields[HV_URL_PATH].len;
+            assert(len == 5);
+            assert(strncmp(path, "/path", len) == 0);
+            printf("%.*s", len, path);
+        }
+        // ?query
+        if (stURL.fields[HV_URL_QUERY].len > 0) {
+            const char* query = strURL + stURL.fields[HV_URL_QUERY].off;
+            int len = stURL.fields[HV_URL_QUERY].len;
+            assert(len == 5);
+            assert(strncmp(query, "query", len) == 0);
+            printf("?%.*s", len, query);
+        }
+        // #fragment
+        if (stURL.fields[HV_URL_FRAGMENT].len > 0) {
+            const char* fragment = strURL + stURL.fields[HV_URL_FRAGMENT].off;
+            int len = stURL.fields[HV_URL_FRAGMENT].len;
+            assert(len == 8);
+            assert(strncmp(fragment, "fragment", len) == 0);
+            printf("#%.*s", len, fragment);
+        }
+        printf("\n");
+    }
 
     return 0;
 }

+ 23 - 0
unittest/hurl_test.cpp

@@ -0,0 +1,23 @@
+#include <assert.h>
+
+#include "hurl.h"
+
+int main(int argc, char** argv) {
+    std::string strURL = "http://www.example.com/path?query#fragment";
+    HUrl url;
+    if (!url.parse(strURL)) {
+        printf("parse url %s error!\n", strURL.c_str());
+        return -1;
+    }
+    std::string dumpURL = url.dump();
+    printf("%s =>\n%s\n", strURL.c_str(), dumpURL.c_str());
+    assert(strURL == dumpURL);
+
+    const char* str = "中 文";
+    std::string escaped = HUrl::escape(str);
+    std::string unescaped = HUrl::unescape(escaped.c_str());
+    printf("%s => %s\n", str, escaped.c_str());
+    assert(str == unescaped);
+
+    return 0;
+}