commit 6c1b6edfc7152b7563a2c08f59ea85bc076144a9
parent 129652abd587f40d96ca9b735d8c552cd2b8ee64
Author: Michael Skec
Date:   Fri,  1 Dec 2023 08:28:51 +1100
base uri_str implementation + add query support
uri_str generates string from URI structure.  Truncations and overflows
not fully tested yet.  URI fragments are completely disregarded at the
moment.
Diffstat:
| M | str.h |  |  | 2 | +- | 
| M | test.c |  |  | 463 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------------- | 
| M | uri.c |  |  | 138 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------- | 
| M | uri.h |  |  | 6 | +++--- | 
4 files changed, 452 insertions(+), 157 deletions(-)
diff --git a/str.h b/str.h
@@ -43,7 +43,7 @@ str_equal(const char *restrict s1, const char *restrict s2, size_t n)
 static inline bool
 strz_equal(const char *restrict s1, const char *restrict s2)
 {
-    return strcmp(s1, s2);
+    return strcmp(s1, s2) == 0;
 }
 
 /* string copy (implemented as strlcpy)  */
diff --git a/test.c b/test.c
@@ -135,26 +135,27 @@ test_str(void)
 }
 
 static bool
-test_uri(const char *string, struct uri expected)
+test_uri(const char *uristring_in, struct uri expected)
 {
-    char uristring[512];
+    char uristring_expected[512];
     bool equal;
     struct uri parsed;
 
-    parsed = uri_parse(string, strz_len(string));
+    parsed = uri_parse(uristring_in, strz_len(uristring_in));
     equal = uri_equal(parsed, expected, 0);
 
-    uri_str(&expected, uristring, sizeof(uristring), 0);
+    uri_str(uristring_expected, &expected, sizeof(uristring_expected), 0);
 
     fprintf(stdout, "test %s:\n"
-                    "\t'%s' vs '%s'\n",
+                    "\t'%s'",
             equal ? "passed" : "FAILED",
-            string, uristring);
+            uristring_in);
 
 #if !VERBOSE_TEST_RESULTS
     if (!equal)
 #endif /* !VERBOSE_TEST_RESULTS */
     {
+        fprintf(stdout, " not equivalent to '%s':\n", uristring_in);
         fprintf(stdout, "\tport          %d\t\texpected %d\n",
                 parsed.port, expected.port);
         fprintf(stdout, "\tprotocol      %d\t\texpected %d\n",
@@ -168,6 +169,35 @@ test_uri(const char *string, struct uri expected)
         fprintf(stdout, "\tquery        '%s'\t\texpected '%s'\n",
                 parsed.query, expected.query);
     }
+    else
+    {
+        fputc('\n', stdout);
+    }
+
+    return equal;
+}
+
+static bool
+test_uri_str(const struct uri uri, const char *expected, uint32_t flags)
+{
+    char actual[512];
+    bool equal;
+
+    uri_str(actual, &uri, sizeof(actual), flags);
+    equal = strz_equal(expected, actual);
+
+    if (!equal)
+    {
+        fprintf(stdout, "test failed:\n"
+                "\t'%s' != expected '%s'\n",
+                actual, expected);
+    }
+    else
+    {
+        fprintf(stdout, "test passed:\n"
+                "\t'%s' == expected '%s'\n",
+                actual, expected);
+    }
 
     return equal;
 }
@@ -175,143 +205,314 @@ test_uri(const char *string, struct uri expected)
 static bool
 test_uris(void)
 {
+    struct uri uri;
+
     fprintf(stdout, "testing URI functions...\n");
 
     fprintf(stdout, " - uri_str...\n");
 
-    char uristring_actual[512];
-    char uristring_expect[512];
-    struct uri testuri;
-
-#if 0
-    str_copy_fixed(uristring_expect, "gopher://");
-    memset(&testuri, 0, sizeof(testuri));
-    testuri.port = 0;
-    testuri.protocol = PROTOCOL_GOPHER;
-    str_copy_fixed(testuri.protocol_str, "gopher");
-    uri_str(&testuri, uristring_actual, sizeof(uristring_actual), 0);
-    if (!str_equal_fixed(uristring_expect, uristring_actual))
+    memset(&uri, 0, sizeof(uri));
+    uri.protocol = PROTOCOL_GOPHER;
+    str_copy_fixed(uri.protocol_str, "gopher");
+    if (!test_uri_str(uri, "gopher://", 0))
+        return false;
+
+    memset(&uri, 0, sizeof(uri));
+    str_copy_fixed(uri.host, "example.com");
+    uri.protocol = PROTOCOL_GOPHER;
+    str_copy_fixed(uri.protocol_str, "gopher");
+    if (!test_uri_str(uri, "gopher://example.com", 0))
+        return false;
+
+    memset(&uri, 0, sizeof(uri));
+    str_copy_fixed(uri.host, "example.com");
+    uri.port = 70;
+    uri.protocol = PROTOCOL_GOPHER;
+    str_copy_fixed(uri.protocol_str, "gopher");
+    if (!test_uri_str(uri, "gopher://example.com:70", 0))
+        return false;
+
+    memset(&uri, 0, sizeof(uri));
+    str_copy_fixed(uri.host, "example.com");
+    uri.port = 70;
+    uri.protocol = PROTOCOL_GOPHER;
+    str_copy_fixed(uri.protocol_str, "gopher");
+    if (!test_uri_str(uri, "gopher://example.com", URISTR_NO_PORT_BIT))
+        return false;
+
+    memset(&uri, 0, sizeof(uri));
+    str_copy_fixed(uri.host, "example.com");
+    str_copy_fixed(uri.path, "/");
+    uri.protocol = PROTOCOL_GOPHER;
+    str_copy_fixed(uri.protocol_str, "gopher");
+    if (!test_uri_str(uri, "gopher://example.com/", 0))
+        return false;
+
+    memset(&uri, 0, sizeof(uri));
+    str_copy_fixed(uri.host, "example.com");
+    str_copy_fixed(uri.path, "/index/");
+    uri.protocol = PROTOCOL_GOPHER;
+    str_copy_fixed(uri.protocol_str, "gopher");
+    if (!test_uri_str(uri, "gopher://example.com/index/", 0))
+        return false;
+
+    memset(&uri, 0, sizeof(uri));
+    str_copy_fixed(uri.host, "example.com");
+    str_copy_fixed(uri.path, "/index/");
+    uri.protocol = PROTOCOL_GOPHER;
+    str_copy_fixed(uri.protocol_str, "gopher");
+    if (!test_uri_str(uri, "gopher://example.com/index", URISTR_NOSLASH_BIT))
+        return false;
+
+    memset(&uri, 0, sizeof(uri));
+    str_copy_fixed(uri.host, "example.com");
+    str_copy_fixed(uri.path, "/");
+    uri.protocol = PROTOCOL_GOPHER;
+    str_copy_fixed(uri.protocol_str, "gopher");
+    if (!test_uri_str(uri, "gopher://example.com", URISTR_NOSLASH_BIT))
+        return false;
+
+    memset(&uri, 0, sizeof(uri));
+    str_copy_fixed(uri.host, "example.com");
+    str_copy_fixed(uri.path, "");
+    uri.protocol = PROTOCOL_GOPHER;
+    str_copy_fixed(uri.protocol_str, "gopher");
+    if (!test_uri_str(uri, "gopher://example.com", URISTR_NOSLASH_BIT))
+        return false;
+
+    memset(&uri, 0, sizeof(uri));
+    str_copy_fixed(uri.host, "example.com");
+    str_copy_fixed(uri.path, "/index");
+    uri.protocol = PROTOCOL_GOPHER;
+    str_copy_fixed(uri.protocol_str, "gopher");
+    if (!test_uri_str(uri, "gopher://example.com/index", URISTR_NOSLASH_BIT))
+        return false;
+
+    memset(&uri, 0, sizeof(uri));
+    str_copy_fixed(uri.host, "");
+    str_copy_fixed(uri.path, "index/test/");
+    uri.protocol = PROTOCOL_NONE;
+    if (!test_uri_str(uri, "index/test", URISTR_NOSLASH_BIT))
+        return false;
+
+    memset(&uri, 0, sizeof(uri));
+    str_copy_fixed(uri.host, "");
+    str_copy_fixed(uri.path, "index/test/");
+    uri.protocol = PROTOCOL_FILE;
+    str_copy_fixed(uri.protocol_str, "file");
+    if (!test_uri_str(uri, "file://index/test", URISTR_NOSLASH_BIT))
+        return false;
+
+    memset(&uri, 0, sizeof(uri));
+    str_copy_fixed(uri.host, "");
+    str_copy_fixed(uri.path, "/index/test/");
+    uri.protocol = PROTOCOL_FILE;
+    str_copy_fixed(uri.protocol_str, "file");
+    if (!test_uri_str(uri, "file:///index/test", URISTR_NOSLASH_BIT))
+        return false;
+
+    memset(&uri, 0, sizeof(uri));
+    str_copy_fixed(uri.host, "example.com");
+    str_copy_fixed(uri.path, "/");
+    str_copy_fixed(uri.query, "q=test");
+    uri.protocol = PROTOCOL_GOPHER;
+    str_copy_fixed(uri.protocol_str, "gopher");
+    /* unorthodox URI style (no slash with query) but we allow it anyway */
+    if (!test_uri_str(uri, "gopher://example.com?q=test", URISTR_NOSLASH_BIT))
+        return false;
+
+    memset(&uri, 0, sizeof(uri));
+    str_copy_fixed(uri.host, "example.com");
+    str_copy_fixed(uri.path, "/");
+    str_copy_fixed(uri.query, "q=test");
+    uri.protocol = PROTOCOL_GOPHER;
+    str_copy_fixed(uri.protocol_str, "gopher");
+    if (!test_uri_str(uri, "gopher://example.com",
+                      URISTR_NOSLASH_BIT | URISTR_NOQUERY_BIT))
+    {
+        return false;
+    }
+
+    memset(&uri, 0, sizeof(uri));
+    str_copy_fixed(uri.host, "example.com");
+    str_copy_fixed(uri.path, "/");
+    str_copy_fixed(uri.query, "q=test");
+    uri.protocol = PROTOCOL_GOPHER;
+    str_copy_fixed(uri.protocol_str, "gopher");
+    if (!test_uri_str(uri, "gopher://example.com/?q=test", 0))
     {
-        fprintf(stdout, "test failed:\n"
-                "\t'%s' != expected '%s'\n",
-                uristring_actual, uristring_expect);
         return false;
     }
-    fprintf(stdout, "test passed:\n"
-            "\t'%s' == expected '%s'\n",
-            uristring_actual, uristring_expect);
-#endif
+
 
     /* TODO: oversized hostnames, oversized paths, test truncation, etc. */
 
     fprintf(stdout, " - uri_parse...\n");
 
-    struct uri expected;
-
-    memset(&expected, 0, sizeof(expected));
-    if (!test_uri("", expected))
-        return false;
-
-    memset(&expected, 0, sizeof(expected));
-    expected.port = 0;
-    expected.protocol = PROTOCOL_FILE;
-    str_copy_fixed(expected.protocol_str, "file");
-    str_copy_fixed(expected.host, "");
-    str_copy_fixed(expected.path, "README/");
-    expected.query[0] = '\0';
-    if (!test_uri("file://README/", expected))
-        return false;
-
-    memset(&expected, 0, sizeof(expected));
-    expected.port = 0;
-    expected.protocol = PROTOCOL_FILE;
-    str_copy_fixed(expected.protocol_str, "file");
-    str_copy_fixed(expected.host, "");
-    str_copy_fixed(expected.path, "/home/mike/src/c/sr71/README");
-    expected.query[0] = '\0';
-    if (!test_uri("file:///home/mike/src/c/sr71/README", expected))
-        return false;
-
-    memset(&expected, 0, sizeof(expected));
-    expected.port = 0;
-    expected.protocol = PROTOCOL_GOPHER;
-    str_copy_fixed(expected.protocol_str, "gopher");
-    str_copy_fixed(expected.host, "example.com");
-    str_copy_fixed(expected.path, "");
-    expected.query[0] = '\0';
-    if (!test_uri("gopher://example.com", expected)) return false;
-
-    memset(&expected, 0, sizeof(expected));
-    expected.port = 0;
-    expected.protocol = PROTOCOL_GOPHER;
-    str_copy_fixed(expected.protocol_str, "gopher");
-    str_copy_fixed(expected.host, "example.com");
-    str_copy_fixed(expected.path, "/test.txt");
-    expected.query[0] = '\0';
-    if (!test_uri("gopher://example.com/test.txt", expected)) return false;
-
-    memset(&expected, 0, sizeof(expected));
-    expected.port = 70;
-    expected.protocol = PROTOCOL_GOPHER;
-    str_copy_fixed(expected.protocol_str, "gopher");
-    str_copy_fixed(expected.host, "example.com");
-    str_copy_fixed(expected.path, "/test.txt");
-    expected.query[0] = '\0';
-    if (!test_uri("gopher://example.com:70/test.txt", expected)) return false;
-
-    memset(&expected, 0, sizeof(expected));
-    expected.port = 70;
-    expected.protocol = PROTOCOL_GOPHER;
-    str_copy_fixed(expected.protocol_str, "gopher");
-    str_copy_fixed(expected.host, "example.com");
-    str_copy_fixed(expected.path, "/1/test.txt");
-    expected.query[0] = '\0';
-    if (!test_uri("gopher://example.com:70/1/test.txt", expected)) return false;
-
-    memset(&expected, 0, sizeof(expected));
-    expected.port = 0;
-    expected.protocol = PROTOCOL_GOPHER;
-    str_copy_fixed(expected.protocol_str, "gopher");
-    str_copy_fixed(expected.host, "example.com");
-    str_copy_fixed(expected.path, "/1/test.txt");
-    expected.query[0] = '\0';
-    if (!test_uri("gopher://example.com/1/test.txt", expected)) return false;
-
-    memset(&expected, 0, sizeof(expected));
-    expected.port = 0;
-    expected.protocol = PROTOCOL_NONE;
-    str_copy_fixed(expected.protocol_str, "");
-    str_copy_fixed(expected.host, "");
-    str_copy_fixed(expected.path, "test.txt");
-    expected.query[0] = '\0';
-    if (!test_uri("test.txt", expected)) return false;
-
-    memset(&expected, 0, sizeof(expected));
-    expected.port = 0;
-    expected.protocol = PROTOCOL_NONE;
-    str_copy_fixed(expected.protocol_str, "");
-    str_copy_fixed(expected.host, "");
-    str_copy_fixed(expected.path, "/test.txt");
-    expected.query[0] = '\0';
-    if (!test_uri("/test.txt", expected)) return false;
-
-    memset(&expected, 0, sizeof(expected));
-    expected.port = 70;
-    expected.protocol = PROTOCOL_GOPHER;
-    str_copy_fixed(expected.protocol_str, "gopher");
-    str_copy_fixed(expected.host, "example.com");
-    str_copy_fixed(expected.path, "/");
-    expected.query[0] = '\0';
-    if (!test_uri("gopher://example.com:70/", expected)) return false;
-
-    memset(&expected, 0, sizeof(expected));
-    expected.port = 70;
-    expected.protocol = PROTOCOL_GOPHER;
-    str_copy_fixed(expected.protocol_str, "gopher");
-    str_copy_fixed(expected.host, "example.com");
-    str_copy_fixed(expected.path, "/");
-    expected.query[0] = '\0'; /* TODO: query */
-    if (!test_uri("gopher://example.com:70/?q=test", expected)) return false;
+    memset(&uri, 0, sizeof(uri));
+    if (!test_uri("", uri))
+        return false;
+
+    memset(&uri, 0, sizeof(uri));
+    uri.port = 0;
+    uri.protocol = PROTOCOL_FILE;
+    str_copy_fixed(uri.protocol_str, "file");
+    str_copy_fixed(uri.host, "");
+    str_copy_fixed(uri.path, "README/");
+    uri.query[0] = '\0';
+    if (!test_uri("file://README/", uri))
+        return false;
+
+    memset(&uri, 0, sizeof(uri));
+    uri.port = 0;
+    uri.protocol = PROTOCOL_FILE;
+    str_copy_fixed(uri.protocol_str, "file");
+    str_copy_fixed(uri.host, "");
+    str_copy_fixed(uri.path, "/home/mike/src/c/sr71/README");
+    uri.query[0] = '\0';
+    if (!test_uri("file:///home/mike/src/c/sr71/README", uri))
+        return false;
+
+    memset(&uri, 0, sizeof(uri));
+    uri.port = 0;
+    uri.protocol = PROTOCOL_GOPHER;
+    str_copy_fixed(uri.protocol_str, "gopher");
+    str_copy_fixed(uri.host, "example.com");
+    str_copy_fixed(uri.path, "");
+    uri.query[0] = '\0';
+    if (!test_uri("gopher://example.com", uri)) return false;
+
+    memset(&uri, 0, sizeof(uri));
+    uri.port = 0;
+    uri.protocol = PROTOCOL_GOPHER;
+    str_copy_fixed(uri.protocol_str, "gopher");
+    str_copy_fixed(uri.host, "example.com");
+    str_copy_fixed(uri.path, "/test.txt");
+    uri.query[0] = '\0';
+    if (!test_uri("gopher://example.com/test.txt", uri)) return false;
+
+    memset(&uri, 0, sizeof(uri));
+    uri.port = 70;
+    uri.protocol = PROTOCOL_GOPHER;
+    str_copy_fixed(uri.protocol_str, "gopher");
+    str_copy_fixed(uri.host, "example.com");
+    str_copy_fixed(uri.path, "/test.txt");
+    uri.query[0] = '\0';
+    if (!test_uri("gopher://example.com:70/test.txt", uri)) return false;
+
+    memset(&uri, 0, sizeof(uri));
+    uri.port = 70;
+    uri.protocol = PROTOCOL_GOPHER;
+    str_copy_fixed(uri.protocol_str, "gopher");
+    str_copy_fixed(uri.host, "example.com");
+    str_copy_fixed(uri.path, "/1/test.txt");
+    uri.query[0] = '\0';
+    if (!test_uri("gopher://example.com:70/1/test.txt", uri)) return false;
+
+    memset(&uri, 0, sizeof(uri));
+    uri.port = 0;
+    uri.protocol = PROTOCOL_GOPHER;
+    str_copy_fixed(uri.protocol_str, "gopher");
+    str_copy_fixed(uri.host, "example.com");
+    str_copy_fixed(uri.path, "/1/test.txt");
+    uri.query[0] = '\0';
+    if (!test_uri("gopher://example.com/1/test.txt", uri)) return false;
+
+    memset(&uri, 0, sizeof(uri));
+    uri.port = 0;
+    uri.protocol = PROTOCOL_NONE;
+    str_copy_fixed(uri.protocol_str, "");
+    str_copy_fixed(uri.host, "");
+    str_copy_fixed(uri.path, "test.txt");
+    uri.query[0] = '\0';
+    if (!test_uri("test.txt", uri)) return false;
+
+    memset(&uri, 0, sizeof(uri));
+    uri.port = 0;
+    uri.protocol = PROTOCOL_NONE;
+    str_copy_fixed(uri.protocol_str, "");
+    str_copy_fixed(uri.host, "");
+    str_copy_fixed(uri.path, "/test.txt");
+    uri.query[0] = '\0';
+    if (!test_uri("/test.txt", uri)) return false;
+
+    memset(&uri, 0, sizeof(uri));
+    uri.port = 70;
+    uri.protocol = PROTOCOL_GOPHER;
+    str_copy_fixed(uri.protocol_str, "gopher");
+    str_copy_fixed(uri.host, "example.com");
+    str_copy_fixed(uri.path, "/");
+    uri.query[0] = '\0';
+    if (!test_uri("gopher://example.com:70/", uri)) return false;
+
+    memset(&uri, 0, sizeof(uri));
+    uri.port = 70;
+    uri.protocol = PROTOCOL_GOPHER;
+    str_copy_fixed(uri.protocol_str, "gopher");
+    str_copy_fixed(uri.host, "example.com");
+    str_copy_fixed(uri.path, "/");
+    str_copy_fixed(uri.query, "q=test");
+    if (!test_uri("gopher://example.com:70/?q=test", uri)) return false;
+
+    memset(&uri, 0, sizeof(uri));
+    uri.port = 70;
+    uri.protocol = PROTOCOL_GOPHER;
+    str_copy_fixed(uri.protocol_str, "gopher");
+    str_copy_fixed(uri.host, "example.com");
+    str_copy_fixed(uri.path, "/");
+    str_copy_fixed(uri.query, "q=test");
+    if (!test_uri("gopher://example.com:70/?q=test#ignored-fragment", uri))
+        return false;
+
+    memset(&uri, 0, sizeof(uri));
+    uri.port = 70;
+    uri.protocol = PROTOCOL_GOPHER;
+    str_copy_fixed(uri.protocol_str, "gopher");
+    str_copy_fixed(uri.host, "example.com");
+    str_copy_fixed(uri.path, "/");
+    if (!test_uri("gopher://example.com:70/#ignored-fragment", uri))
+        return false;
+
+    memset(&uri, 0, sizeof(uri));
+    uri.protocol = PROTOCOL_GOPHER;
+    str_copy_fixed(uri.protocol_str, "gopher");
+    str_copy_fixed(uri.host, "example.com");
+    str_copy_fixed(uri.path, "");
+    str_copy_fixed(uri.query, "q=test");
+    if (!test_uri("gopher://example.com?q=test", uri))
+        return false;
+
+    memset(&uri, 0, sizeof(uri));
+    uri.protocol = PROTOCOL_GOPHER;
+    str_copy_fixed(uri.protocol_str, "gopher");
+    str_copy_fixed(uri.host, "example.com");
+    str_copy_fixed(uri.path, "");
+    if (!test_uri("gopher://example.com#ignored-fragment", uri))
+        return false;
+
+    memset(&uri, 0, sizeof(uri));
+    uri.protocol = PROTOCOL_NONE;
+    str_copy_fixed(uri.path, "example/");
+    str_copy_fixed(uri.query, "q=test");
+    if (!test_uri("example/?q=test", uri))
+        return false;
+
+    memset(&uri, 0, sizeof(uri));
+    uri.protocol = PROTOCOL_NONE;
+    str_copy_fixed(uri.path, "/");
+    str_copy_fixed(uri.query, "q=test");
+    if (!test_uri("/?q=test", uri))
+        return false;
+
+    memset(&uri, 0, sizeof(uri));
+    uri.protocol = PROTOCOL_NONE;
+    str_copy_fixed(uri.query, "q=test");
+    if (!test_uri("?q=test", uri))
+        return false;
+
+    memset(&uri, 0, sizeof(uri));
+    if (!test_uri("#test-fragment", uri))
+        return false;
 
     return true;
 }
diff --git a/uri.c b/uri.c
@@ -11,7 +11,10 @@ uri_parse(const char *uristr, int uristr_len)
     int colon = 0,
         hostname_start = 0,
         hostname_len = 0,
-        protocol_name_len = 0;
+        protocol_name_len = 0,
+        path_start = 0,
+        path_len = 0,
+        query = 0;
 
     /*
      * Look for a colon.  We expect a maximum of two colons in a URI:
@@ -136,7 +139,7 @@ uri_parse(const char *uristr, int uristr_len)
         {
             c = uristr[hostname_len + hostname_start];
 
-            if (c == '\0' || c == ':' || c == '/')
+            if (c == '\0' || c == ':' || c == '/' || c == '?' || c == '#')
                 break;
         }
 
@@ -176,7 +179,7 @@ uri_parse(const char *uristr, int uristr_len)
      */
     if (uri.protocol == PROTOCOL_FILE) /* local URI */
     {
-        int n_slashes, path_start, path_len, path_size;
+        int n_slashes, path_size;
 
         ASSERT(protocol_name_len > 0);
         ASSERT(uristr[protocol_name_len] == ':');
@@ -195,7 +198,9 @@ uri_parse(const char *uristr, int uristr_len)
         /* Find length of path.  Generally ends at the end of the string. */
         for (path_len = 0;
              (path_start + path_len < uristr_len) &&
-                 (uristr[path_start + path_len] != '\0');
+                 (uristr[path_start + path_len] != '\0') &&
+                 (uristr[path_start + path_len] != '?') &&
+                 (uristr[path_start + path_len] != '#'); /* ignore fragment */
              ++path_len)
             ;
 
@@ -209,13 +214,18 @@ uri_parse(const char *uristr, int uristr_len)
     }
     else if (hostname_len > 0) /* URI with a hostname */
     {
-        int path_start, path_len, path_size;
+        int path_size;
 
-        /* Start at the end of hostname and after port. */
+        /* Start may start at the end of hostname and after port, at the first
+         * slash.  If we encounter the query (?) or fragment (#), we leave the
+         * "path start" point there, even though the path length will be 0, so
+         * that we can still find the query later on. */
         for (path_start = hostname_start + hostname_len;
              path_start < uristr_len &&
-             uristr[path_start] != '\0' &&
-                 uristr[path_start] != '/';
+                 (uristr[path_start] != '\0') &&
+                 (uristr[path_start] != '/') &&
+                 (uristr[path_start] != '?') &&
+                 (uristr[path_start] != '#'); /* ignore fragment */
              ++path_start)
         {
             /* Skip past the port if there is one */
@@ -231,8 +241,9 @@ uri_parse(const char *uristr, int uristr_len)
         /* Length is to end of the string or to the query. */
         for (path_len = 0;
              path_start + path_len < uristr_len &&
-             uristr[path_start + path_len] != '\0' &&
-                 uristr[path_start + path_len] != '?';
+                 (uristr[path_start + path_len] != '\0') &&
+                 (uristr[path_start + path_len] != '?') &&
+                 (uristr[path_start + path_len] != '#'); /* ignore fragment */
              ++path_len)
              ;
 
@@ -243,18 +254,19 @@ uri_parse(const char *uristr, int uristr_len)
 
         str_copy(uri.path, uristr + path_start, path_size);
     }
-    else /* relative URI--we use beginning section as path. */
+    else /* relative URI--we use whole beginning section as path. */
     {
-        int path_start = 0, path_len, path_size;
+        int path_size;
 
         ASSERT(protocol_name_len == 0);
 
         /* Find length of path.  Goes until the end of the string or at
-         * query. */
+         * query or fragment. */
         for (path_len = 0;
              (path_start + path_len < uristr_len) &&
-             (uristr[path_start + path_len] != '\0') &&
-             (uristr[path_start + path_len] != '?');
+                 (uristr[path_start + path_len] != '\0') &&
+                 (uristr[path_start + path_len] != '?') &&
+                 (uristr[path_start + path_len] != '#'); /* ignore fragment */
              ++path_len)
             ;
 
@@ -267,23 +279,105 @@ uri_parse(const char *uristr, int uristr_len)
         str_copy(uri.path, uristr + path_start, path_size);
     }
 
+    /* Find query, which always appears after the path */
+    query = str_ichr(uristr + path_start + path_len, '?',
+                     uristr_len - path_start - path_len) +
+                     path_start + path_len + 1 /* +1 to read after the '?' */;
+    if (query < uristr_len)
+    {
+        int query_len, query_size;
+
+        /* Find length of query.  Goes until the end of the string or at
+         * query. */
+        for (query_len = 0;
+             (query + query_len < uristr_len) &&
+                 (uristr[query + query_len] != '\0') &&
+                 (uristr[query + query_len] != '#'); /* ignore fragment */
+             ++query_len)
+            ;
+
+        ASSERT(query_len > 0);
+
+        query_size = query_len + 1;
+        ASSERT(query_size < sizeof(uri.query) && "big query");
+        query_size = min(sizeof(uri.query), query_size);
+        /* TODO: log truncation */
+
+        /* Copy query */
+        str_copy(uri.query, uristr + query, query_size);
+    }
 
     return uri;
 }
 
 size_t
-uri_str(const struct uri *const u,
-       char *buffer,
-       size_t buffer_size,
-       uint32_t flags)
+uri_str(char *buffer,
+        const struct uri *const u,
+        size_t buffer_size,
+        uint32_t flags)
 {
     ASSERT(u);
     ASSERT(buffer);
     ASSERT(buffer_size > 0);
 
-    memset(buffer, 0, buffer_size);
+    int pos = 0;
+    buffer[pos] = '\0';
+
+    /* Write the scheme first */
+    if (u->protocol != PROTOCOL_NONE)
+    {
+        ASSERT(u->protocol_str[0] && "uri has no protocol");
+
+        int protocol_len;
+
+        /* Write protocol string */
+        protocol_len = (int)str_copy(buffer, u->protocol_str, buffer_size);
+
+        /* Write the colon following protocol */
+        protocol_len += (int)str_copy(buffer + protocol_len, ":",
+                                      buffer_size - protocol_len);
+
+        /* Write slashes if the protocol has them */
+        if (!(u->flags & URI_NO_PROTOCOL_SLASHES_BIT))
+        {
+            protocol_len += (int)str_copy(buffer + protocol_len, "//",
+                                          buffer_size - protocol_len);
+        }
+
+        pos += protocol_len;
+    }
+
+    /* Write the hostname */
+    if (u->host[0] != '\0')
+    {
+        pos += (int)str_copy(buffer + pos, u->host, buffer_size - pos);
+    }
+
+    /* Write the port */
+    if (u->port > 0 && !(flags & URISTR_NO_PORT_BIT))
+    {
+        pos += (int)snprintf(buffer + pos, buffer_size - pos, ":%d", u->port);
+    }
+
+    /* Write path */
+    if (u->path[0] != '\0')
+    {
+        pos += (int)str_copy(buffer + pos, u->path, buffer_size - pos);
+
+        /* trim trailing slash */
+        if ((flags & URISTR_NOSLASH_BIT) && pos > 0 && buffer[pos - 1] == '/')
+            buffer[--pos] = '\0';
+    }
+
+    /* Write query */
+    if (u->query[0] != '\0' && !(flags & URISTR_NOQUERY_BIT))
+    {
+        /* Write the leading '?' followed by query */
+        pos += (int)str_copy(buffer + pos, "?", buffer_size - pos);
+        pos += (int)str_copy(buffer + pos, u->query, buffer_size - pos);
+    }
 
-    return str_copy(buffer, "(uri_str is unimplemented)", buffer_size);
+    return pos;
 }
 
 bool
@@ -300,7 +394,7 @@ uri_equal(struct uri u1, struct uri u2, uint32_t flags)
     }
 
     /* Check that query matches */
-    if ((flags & URIEQ_QUERY_BIT) &&
+    if (!(flags & URIEQ_IGNORE_QUERY_BIT) &&
         !str_equal(u1.query, u2.query, sizeof(u1.query)))
     {
         return false;
diff --git a/uri.h b/uri.h
@@ -91,14 +91,14 @@ struct uri uri_parse(const char *uristr, int uristr_len);
 #define URISTR_NOQUERY_BIT  0x20 /* omit query */
 
 /* Convert URI to string */
-size_t uri_str(const struct uri *const u,
-               char *b,
+size_t uri_str(char *b,
+               const struct uri *const u,
                size_t b_size,
                uint32_t flags);
 
 /* urieq flags */
 #define URIEQ_IGNORE_TRAILING_SLASH_BIT 0x01
-#define URIEQ_QUERY_BIT 0x02
+#define URIEQ_IGNORE_QUERY_BIT 0x02
 
 /* @return true if URIs are equal */
 bool uri_equal(struct uri u1, struct uri u2, uint32_t flags);