From a1836c85690833d90ca4b356557b8f8ce8353c11 Mon Sep 17 00:00:00 2001 From: Ryan Date: Tue, 24 Dec 2024 22:04:09 +0100 Subject: [PATCH] Hello libcode-uri --- .editorconfig | 17 ++ .gitattributes | 1 + .gitea/workflows/on-push.yaml | 24 +++ .gitignore | 32 ++++ LICENSE | 31 ++++ README.md | 21 +++ build/.gitignore | 4 + build/bootstrap.build | 7 + build/export.build | 6 + build/root.build | 16 ++ buildfile | 5 + code/uri/.gitignore | 9 + code/uri/buildfile | 66 +++++++ code/uri/grammar.hxx | 133 ++++++++++++++ code/uri/uri.cxx | 284 +++++++++++++++++++++++++++++ code/uri/uri.hxx | 118 ++++++++++++ code/uri/uri.test.cxx | 327 ++++++++++++++++++++++++++++++++++ code/uri/uri.txx | 242 +++++++++++++++++++++++++ code/uri/version.hxx.in | 37 ++++ manifest | 12 ++ repositories.manifest | 6 + tests/.gitignore | 8 + tests/build/.gitignore | 4 + tests/build/bootstrap.build | 5 + tests/build/root.build | 16 ++ tests/buildfile | 1 + 26 files changed, 1432 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitattributes create mode 100644 .gitea/workflows/on-push.yaml create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 build/.gitignore create mode 100644 build/bootstrap.build create mode 100644 build/export.build create mode 100644 build/root.build create mode 100644 buildfile create mode 100644 code/uri/.gitignore create mode 100644 code/uri/buildfile create mode 100644 code/uri/grammar.hxx create mode 100644 code/uri/uri.cxx create mode 100644 code/uri/uri.hxx create mode 100644 code/uri/uri.test.cxx create mode 100644 code/uri/uri.txx create mode 100644 code/uri/version.hxx.in create mode 100644 manifest create mode 100644 repositories.manifest create mode 100644 tests/.gitignore create mode 100644 tests/build/.gitignore create mode 100644 tests/build/bootstrap.build create mode 100644 tests/build/root.build create mode 100644 tests/buildfile diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..ec9f3de --- /dev/null +++ b/.editorconfig @@ -0,0 +1,17 @@ +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.md] +indent_size = 4 +max_line_length = off +trim_trailing_whitespace = false + +[*.yaml] +indent_size = 2 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..176a458 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto diff --git a/.gitea/workflows/on-push.yaml b/.gitea/workflows/on-push.yaml new file mode 100644 index 0000000..23a9e11 --- /dev/null +++ b/.gitea/workflows/on-push.yaml @@ -0,0 +1,24 @@ +name: on-push +on: [push] + +jobs: + build-and-test: + runs-on: linux + container: code.helloryan.se/infra/buildenv/cxx-amd64-fedora-40:latest + volumes: + - /build + steps: + - name: Clone repository + uses: actions/checkout@v3 + - name: Authenticate + run: | + git config unset http.https://code.helloryan.se/.extraheader + echo "${{ secrets.NETRC }}" >> ~/.netrc + - name: Initialize + run: | + bpkg create -d /build cc config.cc.coptions="-Wall -Werror" + bdep init -A /build + - name: Build + run: b + - name: Test + run: b test diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8c19e5f --- /dev/null +++ b/.gitignore @@ -0,0 +1,32 @@ +.bdep/ +Doxyfile + +# Local default options files. +# +.build2/local/ + +# Compiler/linker output. +# +*.d +*.t +*.i +*.i.* +*.ii +*.ii.* +*.o +*.obj +*.gcm +*.pcm +*.ifc +*.so +*.dylib +*.dll +*.a +*.lib +*.exp +*.pdb +*.ilk +*.exe +*.exe.dlls/ +*.exe.manifest +*.pc diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..dfc745b --- /dev/null +++ b/LICENSE @@ -0,0 +1,31 @@ +Copyright © 2024 Ryan. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +3. All advertising materials mentioning features or use of this software must + display the following acknowledgement: + + This product includes software developed by Ryan, http://helloryan.se/. + +4. Neither the name(s) of the copyright holder(s) nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY COPYRIGHT HOLDER "AS IS" AND ANY EXPRESS OR +IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +NO EVENT SHALL COPYRIGHT HOLDER BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT +NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..9028ce1 --- /dev/null +++ b/README.md @@ -0,0 +1,21 @@ +# libcode-uri + +![Build status](https://code.helloryan.se/code/libcode-uri/actions/workflows/on-push.yaml/badge.svg) + +## Requirements + +None, other than a modern C++-compiler. + +## Building + +See the wiki, https://code.helloryan.se/code/wiki/wiki/Build-Instructions, for +build instructions. + +## Contact + +Please report bugs and issues by sending an e-mail to: ryan@helloryan.se. + +## Contributing + +Please send an e-mail to ryan@helloryan.se to request an account and +write-access to the libcode-uri repository. diff --git a/build/.gitignore b/build/.gitignore new file mode 100644 index 0000000..974e01d --- /dev/null +++ b/build/.gitignore @@ -0,0 +1,4 @@ +/config.build +/root/ +/bootstrap/ +build/ diff --git a/build/bootstrap.build b/build/bootstrap.build new file mode 100644 index 0000000..f857395 --- /dev/null +++ b/build/bootstrap.build @@ -0,0 +1,7 @@ +project = libcode-uri + +using version +using config +using test +using install +using dist diff --git a/build/export.build b/build/export.build new file mode 100644 index 0000000..74e2ba5 --- /dev/null +++ b/build/export.build @@ -0,0 +1,6 @@ +$out_root/ +{ + include code/uri/ +} + +export $out_root/code/uri/$import.target diff --git a/build/root.build b/build/root.build new file mode 100644 index 0000000..21e0a2e --- /dev/null +++ b/build/root.build @@ -0,0 +1,16 @@ +# Uncomment to suppress warnings coming from external libraries. +# +#cxx.internal.scope = current + +cxx.std = latest + +using cxx + +hxx{*}: extension = hxx +ixx{*}: extension = ixx +txx{*}: extension = txx +cxx{*}: extension = cxx + +# The test target for cross-testing (running tests under Wine, etc). +# +test.target = $cxx.target diff --git a/buildfile b/buildfile new file mode 100644 index 0000000..bbe185e --- /dev/null +++ b/buildfile @@ -0,0 +1,5 @@ +./: {code/ tests/} doc{README.md} legal{LICENSE} manifest + +# Don't install tests. +# +tests/: install = false diff --git a/code/uri/.gitignore b/code/uri/.gitignore new file mode 100644 index 0000000..b1ed0e0 --- /dev/null +++ b/code/uri/.gitignore @@ -0,0 +1,9 @@ +# Generated version header. +# +version.hxx + +# Unit test executables and Testscript output directories +# (can be symlinks). +# +*.test +test-*.test diff --git a/code/uri/buildfile b/code/uri/buildfile new file mode 100644 index 0000000..24e8c63 --- /dev/null +++ b/code/uri/buildfile @@ -0,0 +1,66 @@ +intf_libs = # Interface dependencies. +impl_libs = # Implementation dependencies. + +./: lib{code-uri}: libul{code-uri} + +libul{code-uri}: {hxx ixx txx cxx}{** -**.test... -version} \ + {hxx }{ version} + +libul{code-uri}: $impl_libs $intf_libs + +# Unit tests. +# +exe{*.test}: +{ + test = true + install = false +} + +test_libs = # Test dependencies. +import test_libs =+ libcode-validation%lib{code-validation} + +for t: cxx{**.test...} +{ + d = $directory($t) + n = $name($t)... + + ./: $d/exe{$n}: $t $d/{hxx ixx txx}{+$n} $d/testscript{+$n} $test_libs + $d/exe{$n}: lib{code-uri}: bin.whole = false + $d/exe{$n}: test.arguments = --print-failure +} + +hxx{version}: in{version} $src_root/manifest +{ + dist = true + clean = ($src_root != $out_root) +} + +# Build options. +# +cxx.poptions =+ "-I$out_root" "-I$src_root" + +# Export options. +# +lib{code-uri}: +{ + cxx.export.poptions = "-I$out_root" "-I$src_root" + cxx.export.libs = $intf_libs +} + +# For pre-releases use the complete version to make sure they cannot +# be used in place of another pre-release or the final version. See +# the version module for details on the version.* variable values. +# +if $version.pre_release + lib{code-uri}: bin.lib.version = "-$version.project_id" +else + lib{code-uri}: bin.lib.version = "-$version.major.$version.minor" + +# Install into the code/uri/ subdirectory of, say, /usr/include/ +# recreating subdirectories. +# +{hxx ixx txx}{*}: +{ + install = include/code/uri/ + install.subdirs = true +} diff --git a/code/uri/grammar.hxx b/code/uri/grammar.hxx new file mode 100644 index 0000000..6a547d1 --- /dev/null +++ b/code/uri/grammar.hxx @@ -0,0 +1,133 @@ +#ifndef code__uri__grammar_hxx_ +#define code__uri__grammar_hxx_ + +namespace code::uri::grammar +{ + + inline + bool + is_alpha(char c) + { + return ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z'); + } + + inline + bool + is_digit(char c) + { + return '0' <= c && c <= '9'; + } + + inline + bool + is_unreserved(char c) + { + if (is_alpha(c) || is_digit(c)) { + return true; + } + + switch (c) { + case '-': + case '.': + case '_': + case '~': + return true; + } + + return false; + } + + inline + bool + is_subdelim(char c) + { + switch (c) { + case '!': + case '$': + case '&': + case '(': + case ')': + case '*': + case '+': + case ',': + case ';': + case '=': + case '\'': + return true; + } + + return false; + } + + inline + bool + is_scheme_start(char c) + { + return is_alpha(c); + } + + inline + bool + is_scheme(char c) + { + return is_alpha(c) || is_digit(c) || c == '+' || c == '-' || c == '.'; + } + + inline + bool + is_userinfo(char c) + { + return is_unreserved(c) || is_subdelim(c) || c == ':' || c == '%'; + } + + inline + bool + is_regname(char c) + { + return is_unreserved(c) || is_subdelim(c); + } + + inline + bool + is_host(char c) + { + return is_regname(c); + } + + inline + bool + is_port(char c) + { + return is_digit(c); + } + + inline + bool + is_pchar(char c) + { + return is_unreserved(c) || is_subdelim(c) || c == ':' || c == '@' || c == '%'; + } + + inline + bool + is_segment_nc(char c) + { + return is_pchar(c) && c != ':'; + } + + inline + bool + is_query(char c) + { + return is_pchar(c) || c == '/' || c == '?'; + } + + inline + bool + is_fragment(char c) + { + return is_pchar(c) || c == '/' || c == '?'; + } +} // namespace code::uri::grammar + +#endif diff --git a/code/uri/uri.cxx b/code/uri/uri.cxx new file mode 100644 index 0000000..80fb692 --- /dev/null +++ b/code/uri/uri.cxx @@ -0,0 +1,284 @@ +#include + +#include +#include +#include + +namespace code::uri +{ + + uri_t:: + uri_t() + {} + + uri_t:: + uri_t(std::string scheme, std::string host, std::string path) + : scheme_{std::move(scheme)}, + host_{std::move(host)}, + path_{std::move(path)} + {} + + uri_t:: + uri_t(std::string scheme, + std::string host, + std::string port, + std::string path) + : scheme_{std::move(scheme)}, + host_{std::move(host)}, + port_{std::move(port)}, + path_{std::move(path)} + {} + + uri_t:: + uri_t(std::string scheme, + std::string host, + std::string port, + std::string path, + std::string query) + : scheme_{std::move(scheme)}, + host_{std::move(host)}, + port_{std::move(port)}, + path_{std::move(path)}, + query_{std::move(query)} + {} + + uri_t:: + uri_t(std::string scheme, + std::string host, + std::string port, + std::string path, + std::string query, + std::string fragment) + : scheme_{std::move(scheme)}, + host_{std::move(host)}, + port_{std::move(port)}, + path_{std::move(path)}, + query_{std::move(query)}, + fragment_{std::move(fragment)} + {} + + uri_t:: + uri_t(std::string scheme, + std::string userinfo, + std::string host, + std::string port, + std::string path, + std::string query, + std::string fragment) + : scheme_{std::move(scheme)}, + userinfo_{std::move(userinfo)}, + host_{std::move(host)}, + port_{std::move(port)}, + path_{std::move(path)}, + query_{std::move(query)}, + fragment_{std::move(fragment)} + {} + + uri_t:: + uri_t(std::optional scheme, + std::optional userinfo, + std::optional host, + std::optional port, + std::string path, + std::optional query, + std::optional fragment) + : scheme_{std::move(scheme)}, + userinfo_{std::move(userinfo)}, + host_{std::move(host)}, + port_{std::move(port)}, + path_{std::move(path)}, + query_{std::move(query)}, + fragment_{std::move(fragment)} + {} + + std::optional const& + uri_t:: + scheme() const + { + return scheme_; + } + + std::string + uri_t:: + scheme_str() const + { + return scheme().value_or(std::string{}); + } + + std::optional const& + uri_t:: + userinfo() const + { + return userinfo_; + } + + std::string + uri_t:: + userinfo_str() const + { + return userinfo().value_or(std::string{}); + } + + std::optional const& + uri_t:: + host() const + { + return host_; + } + + std::string + uri_t:: + host_str() const + { + return host().value_or(std::string{}); + } + + std::optional const& + uri_t:: + port() const + { + return port_; + } + + std::string + uri_t:: + port_str() const + { + return port().value_or(std::string{}); + } + + std::string + uri_t:: + path_str() const + { + return path_; + } + + std::optional const& + uri_t:: + query() const + { + return query_; + } + + std::string + uri_t:: + query_str() const + { + return query().value_or(std::string{}); + } + + std::optional const& + uri_t:: + fragment() const + { + return fragment_; + } + + std::string + uri_t:: + fragment_str() const + { + return fragment().value_or(std::string{}); + } + + std::string + to_string(uri_t const& uri) + { + std::ostringstream str; + + // Scheme + // + if (auto scheme = uri.scheme(); scheme) { + str <<*scheme <<':'; + } + + // Authority + // + if (auto host = uri.host(); host) { + str <<"//"; + + // Userinfo + // + if (auto userinfo = uri.userinfo(); userinfo) { + str <<*userinfo <<'@'; + } + + // Host + // + str <<*host; + + // Port + if (auto port = uri.port(); port) { + str <<':' <<*port; + } + } + + // Path + // + str < segments; + + for (std::string segment; std::getline(path, segment, '/');) { + std::cout << "found segment: " << segment << '\n'; + + if (segment.empty()) { + continue; + } + if (segment == ".") { + continue; + } + if (segment == "..") { + if (!segments.empty()) { + segments.pop_back(); + } + continue; + } + segments.push_back(segment); + } + + std::string normalized; + + for (auto const& j : segments) { + normalized += '/'; + normalized += j; + } + + return uri_t{ + uri.scheme(), + uri.userinfo(), + uri.host(), + uri.port(), + normalized.empty() ? "/" : normalized, + uri.query(), + uri.fragment() + }; + } + + std::optional + try_parse(std::string const& str) + { + return try_parse(str.begin(), str.end()); + } + +} // namespace uri diff --git a/code/uri/uri.hxx b/code/uri/uri.hxx new file mode 100644 index 0000000..3b39667 --- /dev/null +++ b/code/uri/uri.hxx @@ -0,0 +1,118 @@ +#ifndef code__uri__uri_hxx_ +#define code__uri__uri_hxx_ + +#include + +#include +#include +#include + +namespace code::uri +{ + + class uri_t + { + public: + uri_t(); + + uri_t(std::string, std::string, std::string); + + uri_t(std::string, std::string, std::string, std::string); + + uri_t(std::string, + std::string, + std::string, + std::string, + std::string); + + uri_t(std::string, + std::string, + std::string, + std::string, + std::string, + std::string); + + uri_t(std::string, + std::string, + std::string, + std::string, + std::string, + std::string, + std::string); + + uri_t(std::optional, + std::optional, + std::optional, + std::optional, + std::string, + std::optional, + std::optional); + + std::optional const& + scheme() const; + + std::string + scheme_str() const; + + std::optional const& + userinfo() const; + + std::string + userinfo_str() const; + + std::optional const& + host() const; + + std::string + host_str() const; + + std::optional const& + port() const; + + std::string + port_str() const; + + std::string + path_str() const; + + std::optional const& + query() const; + + std::string + query_str() const; + + std::optional const& + fragment() const; + + std::string + fragment_str() const; + + private: + std::optional scheme_; + std::optional userinfo_; + std::optional host_; + std::optional port_; + std::string path_; + std::optional query_; + std::optional fragment_; + + }; + + std::string + to_string(uri_t const&); + + uri_t + normalize_path(uri_t const&); + + template + std::optional + try_parse(Iterator first, Iterator last); + + std::optional + try_parse(std::string const&); + +} // namespace code::uri + +#include + +#endif diff --git a/code/uri/uri.test.cxx b/code/uri/uri.test.cxx new file mode 100644 index 0000000..716bd49 --- /dev/null +++ b/code/uri/uri.test.cxx @@ -0,0 +1,327 @@ +#include + +#include + +#include +#include +#include + +VALIDATION_TEST(test_01) +{ + auto opt_uri = code::uri::try_parse(""); + + VALIDATION_ASSERT_TRUE((bool)opt_uri); + + auto uri = *opt_uri; + + VALIDATION_ASSERT_FALSE((bool)uri.scheme()); + VALIDATION_ASSERT_FALSE((bool)uri.userinfo()); + VALIDATION_ASSERT_FALSE((bool)uri.host()); + VALIDATION_ASSERT_FALSE((bool)uri.port()); + VALIDATION_ASSERT_FALSE((bool)uri.query()); + VALIDATION_ASSERT_FALSE((bool)uri.fragment()); + + VALIDATION_ASSERT_TRUE(uri.path_str().empty()); +} + +VALIDATION_TEST(test_02) +{ + auto opt_uri = code::uri::try_parse("http:///index.html"); + + VALIDATION_ASSERT_TRUE((bool)opt_uri); + + auto uri = *opt_uri; + + VALIDATION_ASSERT_EQUAL((bool)uri.scheme(), true); + VALIDATION_ASSERT_EQUAL((bool)uri.userinfo(), false); + VALIDATION_ASSERT_EQUAL((bool)uri.host(), true); + VALIDATION_ASSERT_EQUAL((bool)uri.port(), false); + VALIDATION_ASSERT_EQUAL((bool)uri.query(), false); + VALIDATION_ASSERT_EQUAL((bool)uri.fragment(), false); + + VALIDATION_ASSERT_EQUAL(uri.scheme_str(), "http"); + VALIDATION_ASSERT_EQUAL(uri.host_str(), ""); + VALIDATION_ASSERT_EQUAL(uri.path_str(), "/index.html"); +} + +VALIDATION_TEST(test_03) +{ + auto opt_uri = code::uri::try_parse("http://host.domain./index.html"); + + VALIDATION_ASSERT_TRUE((bool)opt_uri); + + auto uri = *opt_uri; + + VALIDATION_ASSERT_TRUE((bool)uri.scheme()); + VALIDATION_ASSERT_FALSE((bool)uri.userinfo()); + VALIDATION_ASSERT_TRUE((bool)uri.host()); + VALIDATION_ASSERT_FALSE((bool)uri.port()); + VALIDATION_ASSERT_FALSE((bool)uri.query()); + VALIDATION_ASSERT_FALSE((bool)uri.fragment()); + + VALIDATION_ASSERT_EQUAL(uri.scheme_str(), "http"); + VALIDATION_ASSERT_EQUAL(uri.host_str(), "host.domain."); + VALIDATION_ASSERT_EQUAL(uri.path_str(), "/index.html"); +} + +VALIDATION_TEST(test_04) +{ + auto opt_uri = code::uri::try_parse("https://host.domain.:8443/index.html"); + + VALIDATION_ASSERT_TRUE((bool)opt_uri); + + auto uri = *opt_uri; + + VALIDATION_ASSERT_TRUE((bool)uri.scheme()); + VALIDATION_ASSERT_FALSE((bool)uri.userinfo()); + VALIDATION_ASSERT_TRUE((bool)uri.host()); + VALIDATION_ASSERT_TRUE((bool)uri.port()); + VALIDATION_ASSERT_FALSE((bool)uri.query()); + VALIDATION_ASSERT_FALSE((bool)uri.fragment()); + + VALIDATION_ASSERT_EQUAL(uri.scheme_str(), "https"); + VALIDATION_ASSERT_EQUAL(uri.host_str(), "host.domain."); + VALIDATION_ASSERT_EQUAL(uri.port_str(), "8443"); + VALIDATION_ASSERT_EQUAL(uri.path_str(), "/index.html"); +} + +VALIDATION_TEST(test_05) +{ + auto opt_uri = code::uri::try_parse("https://host.domain.:8443/secodeh?q=hamsters"); + + VALIDATION_ASSERT_TRUE((bool)opt_uri); + + auto uri = *opt_uri; + + VALIDATION_ASSERT_TRUE((bool)uri.scheme()); + VALIDATION_ASSERT_FALSE((bool)uri.userinfo()); + VALIDATION_ASSERT_TRUE((bool)uri.host()); + VALIDATION_ASSERT_TRUE((bool)uri.port()); + VALIDATION_ASSERT_TRUE((bool)uri.query()); + VALIDATION_ASSERT_FALSE((bool)uri.fragment()); + + VALIDATION_ASSERT_EQUAL(uri.scheme_str(), "https"); + VALIDATION_ASSERT_EQUAL(uri.host_str(), "host.domain."); + VALIDATION_ASSERT_EQUAL(uri.port_str(), "8443"); + VALIDATION_ASSERT_EQUAL(uri.path_str(), "/secodeh"); + VALIDATION_ASSERT_EQUAL(uri.query_str(), "q=hamsters"); +} + +VALIDATION_TEST(test_06) +{ + auto opt_uri = code::uri::try_parse("https://host.domain.:8443/secodeh?q=hamsters#results"); + + VALIDATION_ASSERT_TRUE((bool)opt_uri); + + auto uri = *opt_uri; + + VALIDATION_ASSERT_TRUE((bool)uri.scheme()); + VALIDATION_ASSERT_FALSE((bool)uri.userinfo()); + VALIDATION_ASSERT_TRUE((bool)uri.host()); + VALIDATION_ASSERT_TRUE((bool)uri.port()); + VALIDATION_ASSERT_TRUE((bool)uri.query()); + VALIDATION_ASSERT_TRUE((bool)uri.fragment()); + + VALIDATION_ASSERT_EQUAL(uri.scheme_str(), "https"); + VALIDATION_ASSERT_EQUAL(uri.host_str(), "host.domain."); + VALIDATION_ASSERT_EQUAL(uri.port_str(), "8443"); + VALIDATION_ASSERT_EQUAL(uri.path_str(), "/secodeh"); + VALIDATION_ASSERT_EQUAL(uri.query_str(), "q=hamsters"); + VALIDATION_ASSERT_EQUAL(uri.fragment_str(), "results"); +} + +VALIDATION_TEST(test_07) +{ + auto opt_uri = code::uri::try_parse( + "https://admin:qwerty@host.domain.:8443/secodeh?q=hamsters#results" + ); + + VALIDATION_ASSERT_TRUE((bool)opt_uri); + + auto uri = *opt_uri; + + VALIDATION_ASSERT_TRUE((bool)uri.scheme()); + VALIDATION_ASSERT_TRUE((bool)uri.userinfo()); + VALIDATION_ASSERT_TRUE((bool)uri.host()); + VALIDATION_ASSERT_TRUE((bool)uri.port()); + VALIDATION_ASSERT_TRUE((bool)uri.query()); + VALIDATION_ASSERT_TRUE((bool)uri.fragment()); + + VALIDATION_ASSERT_EQUAL(uri.scheme_str(), "https"); + VALIDATION_ASSERT_EQUAL(uri.userinfo_str(), "admin:qwerty"); + VALIDATION_ASSERT_EQUAL(uri.host_str(), "host.domain."); + VALIDATION_ASSERT_EQUAL(uri.port_str(), "8443"); + VALIDATION_ASSERT_EQUAL(uri.path_str(), "/secodeh"); + VALIDATION_ASSERT_EQUAL(uri.query_str(), "q=hamsters"); + VALIDATION_ASSERT_EQUAL(uri.fragment_str(), "results"); +} + +VALIDATION_TEST(test_08) +{ + auto opt_uri = code::uri::try_parse("//host.domain./index.html"); + + VALIDATION_ASSERT_TRUE((bool)opt_uri); + + auto uri = *opt_uri; + + VALIDATION_ASSERT_FALSE((bool)uri.scheme()); + VALIDATION_ASSERT_FALSE((bool)uri.userinfo()); + VALIDATION_ASSERT_TRUE((bool)uri.host()); + VALIDATION_ASSERT_FALSE((bool)uri.port()); + VALIDATION_ASSERT_FALSE((bool)uri.query()); + VALIDATION_ASSERT_FALSE((bool)uri.fragment()); + + VALIDATION_ASSERT_EQUAL(uri.host_str(), "host.domain."); + VALIDATION_ASSERT_EQUAL(uri.path_str(), "/index.html"); +} + +VALIDATION_TEST(test_09) +{ + auto opt_uri = code::uri::try_parse("/index.html"); + + VALIDATION_ASSERT_TRUE((bool)opt_uri); + + auto uri = *opt_uri; + + VALIDATION_ASSERT_EQUAL((bool)uri.scheme(), false); + VALIDATION_ASSERT_FALSE((bool)uri.userinfo()); + VALIDATION_ASSERT_FALSE((bool)uri.host()); + VALIDATION_ASSERT_FALSE((bool)uri.port()); + VALIDATION_ASSERT_FALSE((bool)uri.query()); + VALIDATION_ASSERT_FALSE((bool)uri.fragment()); + + VALIDATION_ASSERT_EQUAL(uri.path_str(), "/index.html"); +} + +VALIDATION_TEST(test_10) +{ + auto opt_uri = code::uri::try_parse("index.html"); + + VALIDATION_ASSERT_TRUE((bool)opt_uri); + + auto uri = *opt_uri; + + VALIDATION_ASSERT_EQUAL((bool)uri.scheme(), false); + VALIDATION_ASSERT_FALSE((bool)uri.userinfo()); + VALIDATION_ASSERT_FALSE((bool)uri.host()); + VALIDATION_ASSERT_FALSE((bool)uri.port()); + VALIDATION_ASSERT_FALSE((bool)uri.query()); + VALIDATION_ASSERT_FALSE((bool)uri.fragment()); + + VALIDATION_ASSERT_EQUAL(uri.path_str(), "index.html"); +} + +VALIDATION_TEST(test_11) +{ + auto opt_uri = code::uri::try_parse("/files/index:1.html"); + + VALIDATION_ASSERT_TRUE((bool)opt_uri); + + auto uri = *opt_uri; + + VALIDATION_ASSERT_EQUAL((bool)uri.scheme(), false); + VALIDATION_ASSERT_EQUAL((bool)uri.userinfo(), false); + VALIDATION_ASSERT_EQUAL((bool)uri.host(), false); + VALIDATION_ASSERT_EQUAL((bool)uri.port(), false); + VALIDATION_ASSERT_EQUAL((bool)uri.query(), false); + VALIDATION_ASSERT_EQUAL((bool)uri.fragment(), false); + + VALIDATION_ASSERT_EQUAL(uri.path_str(), "/files/index:1.html"); +} + +VALIDATION_TEST(test_12) +{ + auto opt_uri = code::uri::try_parse("files/index:1.html"); + + VALIDATION_ASSERT_TRUE((bool)opt_uri); + + auto uri = *opt_uri; + + VALIDATION_ASSERT_EQUAL((bool)uri.scheme(), false); + VALIDATION_ASSERT_EQUAL((bool)uri.userinfo(), false); + VALIDATION_ASSERT_EQUAL((bool)uri.host(), false); + VALIDATION_ASSERT_EQUAL((bool)uri.port(), false); + VALIDATION_ASSERT_EQUAL((bool)uri.query(), false); + VALIDATION_ASSERT_EQUAL((bool)uri.fragment(), false); + + VALIDATION_ASSERT_EQUAL(uri.path_str(), "files/index:1.html"); +} + +VALIDATION_TEST(test_13) +{ + auto opt_uri = code::uri::try_parse("?q=hamsters"); + + VALIDATION_ASSERT_TRUE((bool)opt_uri); + + auto uri = *opt_uri; + + VALIDATION_ASSERT_EQUAL((bool)uri.scheme(), false); + VALIDATION_ASSERT_EQUAL((bool)uri.userinfo(), false); + VALIDATION_ASSERT_EQUAL((bool)uri.host(), false); + VALIDATION_ASSERT_EQUAL((bool)uri.port(), false); + VALIDATION_ASSERT_EQUAL((bool)uri.query(), true); + VALIDATION_ASSERT_EQUAL((bool)uri.fragment(), false); + + VALIDATION_ASSERT_EQUAL(uri.query_str(), "q=hamsters"); +} + +VALIDATION_TEST(test_14) +{ + auto opt_uri = code::uri::try_parse("?q=hamsters#results"); + + VALIDATION_ASSERT_TRUE((bool)opt_uri); + + auto uri = *opt_uri; + + VALIDATION_ASSERT_EQUAL((bool)uri.scheme(), false); + VALIDATION_ASSERT_EQUAL((bool)uri.userinfo(), false); + VALIDATION_ASSERT_EQUAL((bool)uri.host(), false); + VALIDATION_ASSERT_EQUAL((bool)uri.port(), false); + VALIDATION_ASSERT_EQUAL((bool)uri.query(), true); + VALIDATION_ASSERT_EQUAL((bool)uri.fragment(), true); + + VALIDATION_ASSERT_EQUAL(uri.query_str(), "q=hamsters"); + VALIDATION_ASSERT_EQUAL(uri.fragment_str(), "results"); +} + +VALIDATION_TEST(test_15) +{ + auto opt_uri = code::uri::try_parse("#results"); + + VALIDATION_ASSERT_TRUE((bool)opt_uri); + + auto uri = *opt_uri; + + VALIDATION_ASSERT_EQUAL((bool)uri.scheme(), false); + VALIDATION_ASSERT_EQUAL((bool)uri.userinfo(), false); + VALIDATION_ASSERT_EQUAL((bool)uri.host(), false); + VALIDATION_ASSERT_EQUAL((bool)uri.port(), false); + VALIDATION_ASSERT_EQUAL((bool)uri.query(), false); + VALIDATION_ASSERT_EQUAL((bool)uri.fragment(), true); + + VALIDATION_ASSERT_EQUAL(uri.fragment_str(), "results"); +} + +VALIDATION_TEST(test_16) +{ + auto opt_uri = code::uri::try_parse("#results?gui-sort=asc"); + + VALIDATION_ASSERT_TRUE((bool)opt_uri); + + auto uri = *opt_uri; + + VALIDATION_ASSERT_EQUAL((bool)uri.scheme(), false); + VALIDATION_ASSERT_EQUAL((bool)uri.userinfo(), false); + VALIDATION_ASSERT_EQUAL((bool)uri.host(), false); + VALIDATION_ASSERT_EQUAL((bool)uri.port(), false); + VALIDATION_ASSERT_EQUAL((bool)uri.query(), false); + VALIDATION_ASSERT_EQUAL((bool)uri.fragment(), true); + + VALIDATION_ASSERT_EQUAL(uri.fragment_str(), "results?gui-sort=asc"); +} + +int +main(int argc, char* argv[]) +{ + return code::validation::main(argc, argv); +} diff --git a/code/uri/uri.txx b/code/uri/uri.txx new file mode 100644 index 0000000..da54311 --- /dev/null +++ b/code/uri/uri.txx @@ -0,0 +1,242 @@ +namespace code::uri +{ + + template + std::optional + try_parse(Iterator first, Iterator last) + { + std::optional opt_scheme; + std::optional opt_userinfo; + std::optional opt_host; + std::optional opt_port; + std::string path; + std::optional opt_query; + std::optional opt_fragment; + + auto try_parse_scheme = [&](auto init) + { + auto c = init; + + std::string scheme; + while (c != last && grammar::is_scheme(*c)) { + scheme += *c++; + } + + if (c != last && *c == ':') { + ++c; // skips ':' + opt_scheme = std::move(scheme); + return c; + } + + return init; + }; + + auto try_parse_userinfo = [&](auto init) + { + auto c = init; + + std::string userinfo; + + while (c != last && grammar::is_userinfo(*c)) { + userinfo += *c++; + } + + if (c != last && *c == '@') { + ++c; // skips '@' + opt_userinfo = std::move(userinfo); + return c; + } + + return init; + }; + + auto try_parse_host = [&](auto init) + { + auto c = init; + + opt_host = std::string{}; + + while (c != last && grammar::is_host(*c)) { + *opt_host += *c++; + } + + return c; + }; + + auto try_parse_port = [&](auto init) + { + auto c = init; + + if (c != last && *c == ':') { + ++c; // skips ':' + + opt_port = std::string{}; + + while (c != last && grammar::is_digit(*c)) { + *opt_port += *c++; + } + + return c; + } + + return init; + }; + + auto try_parse_authority = [&](auto init) + { + auto c = init; + + if (c == last || *c != '/') { + return init; + } + + ++c; // skips first '/' + + if (c == last || *c != '/') { + return init; + } + + ++c; // skips second '/' + + c = try_parse_userinfo(c); + c = try_parse_host(c); + c = try_parse_port(c); + + return c; + }; + + auto try_parse_path = [&](auto init) + { + auto c = init; + + while (c != last && (grammar::is_pchar(*c) || *c == '/')) { + path += *c++; + } + + return c; + }; + + auto try_parse_query = [&](auto init) + { + auto c = init; + + if (c != last && *c == '?') { + ++c; // skips '?' + opt_query = std::string{}; + + while (c != last && grammar::is_query(*c)) { + *opt_query += *c++; + } + + return c; + } + + return init; + }; + + auto try_parse_fragment = [&](auto init) + { + auto c = init; + + if (c != last && *c == '#') { + ++c; // skips '?#' + + opt_fragment = std::string{}; + + while (c != last && grammar::is_fragment(*c)) { + *opt_fragment += *c++; + } + + return c; + } + + return init; + }; + + first = try_parse_scheme(first); + first = try_parse_authority(first); + first = try_parse_path(first); + first = try_parse_query(first); + first = try_parse_fragment(first); + + if (first != last) { + return std::nullopt; + } + + auto percent_decode = [](std::string const& input) + { + auto hex_to_char = [](char c) + { + if (c>= '0' && c <= '9') + return c - '0'; + + if (c>= 'a' && c <= 'f') + return c - 'a' + 10; + + if (c>= 'A' && c <= 'F') + return c - 'A' + 10; + + throw std::invalid_argument{"invalid hex character"}; + }; + + auto make_byte = [&](char a, char b) + { + return hex_to_char(a) << 4 | hex_to_char(b); + }; + + std::string str; + + auto j = input.begin(); + + while (j != input.end()) { + if ('%' == *j) { + ++j; + + if (j == input.end()) { + break; + } + + char one = *j++; + + if (j == input.end()) { + break; + } + + char two = *j++; + + str += make_byte(one, two); + + continue; + } + + str += *j; + ++j; + } + + return str; + }; + + if (opt_userinfo) { + opt_userinfo = percent_decode(*opt_userinfo); + } + + path = percent_decode(path); + + if (opt_query) { + opt_query = percent_decode(*opt_query); + } + + if (opt_fragment) { + opt_fragment = percent_decode(*opt_fragment); + } + + return uri_t{opt_scheme, + opt_userinfo, + opt_host, + opt_port, + path, + opt_query, + opt_fragment}; + } + +} // namespace code::uri diff --git a/code/uri/version.hxx.in b/code/uri/version.hxx.in new file mode 100644 index 0000000..58c408a --- /dev/null +++ b/code/uri/version.hxx.in @@ -0,0 +1,37 @@ +#ifndef code__uri__version_hxx_ +#define code__uri__version_hxx_ + +// The numeric version format is AAAAABBBBBCCCCCDDDE where: +// +// AAAAA - major version number +// BBBBB - minor version number +// CCCCC - bugfix version number +// DDD - alpha / beta (DDD + 500) version number +// E - final (0) / snapshot (1) +// +// When DDDE is not 0, 1 is subtracted from AAAAABBBBBCCCCC. For example: +// +// Version AAAAABBBBBCCCCCDDDE +// +// 0.1.0 0000000001000000000 +// 0.1.2 0000000001000020000 +// 1.2.3 0000100002000030000 +// 2.2.0-a.1 0000200001999990010 +// 3.0.0-b.2 0000299999999995020 +// 2.2.0-a.1.z 0000200001999990011 +// +#define LIBCODE_URI_VERSION $libcode_uri.version.project_number$ULL +#define LIBCODE_URI_VERSION_STR "$libcode_uri.version.project$" +#define LIBCODE_URI_VERSION_ID "$libcode_uri.version.project_id$" +#define LIBCODE_URI_VERSION_FULL "$libcode_uri.version$" + +#define LIBCODE_URI_VERSION_MAJOR $libcode_uri.version.major$ +#define LIBCODE_URI_VERSION_MINOR $libcode_uri.version.minor$ +#define LIBCODE_URI_VERSION_PATCH $libcode_uri.version.patch$ + +#define LIBCODE_URI_PRE_RELEASE $libcode_uri.version.pre_release$ + +#define LIBCODE_URI_SNAPSHOT_SN $libcode_uri.version.snapshot_sn$ULL +#define LIBCODE_URI_SNAPSHOT_ID "$libcode_uri.version.snapshot_id$" + +#endif diff --git a/manifest b/manifest new file mode 100644 index 0000000..748a8bc --- /dev/null +++ b/manifest @@ -0,0 +1,12 @@ +: 1 +name: libcode-uri +version: 0.1.0-a.0.z +language: c++ +summary: code-uri C++ library +license: BSD-4-Clause +description-file: README.md +url: https://helloryan.se/code/ +email: ryan@helloryan.se +depends: * build2 >= 0.17.0 +depends: * bpkg >= 0.17.0 +depends: libcode-validation ^0.1.0- diff --git a/repositories.manifest b/repositories.manifest new file mode 100644 index 0000000..c149189 --- /dev/null +++ b/repositories.manifest @@ -0,0 +1,6 @@ +: 1 +summary: libcode-uri project repository + +: +role: prerequisite +location: https://code.helloryan.se/code/libcode-validation.git##HEAD diff --git a/tests/.gitignore b/tests/.gitignore new file mode 100644 index 0000000..662178d --- /dev/null +++ b/tests/.gitignore @@ -0,0 +1,8 @@ +# Test executables. +# +driver + +# Testscript output directories (can be symlinks). +# +test +test-* diff --git a/tests/build/.gitignore b/tests/build/.gitignore new file mode 100644 index 0000000..974e01d --- /dev/null +++ b/tests/build/.gitignore @@ -0,0 +1,4 @@ +/config.build +/root/ +/bootstrap/ +build/ diff --git a/tests/build/bootstrap.build b/tests/build/bootstrap.build new file mode 100644 index 0000000..a07b5ea --- /dev/null +++ b/tests/build/bootstrap.build @@ -0,0 +1,5 @@ +project = # Unnamed tests subproject. + +using config +using test +using dist diff --git a/tests/build/root.build b/tests/build/root.build new file mode 100644 index 0000000..a67b2fe --- /dev/null +++ b/tests/build/root.build @@ -0,0 +1,16 @@ +cxx.std = latest + +using cxx + +hxx{*}: extension = hxx +ixx{*}: extension = ixx +txx{*}: extension = txx +cxx{*}: extension = cxx + +# Every exe{} in this subproject is by default a test. +# +exe{*}: test = true + +# The test target for cross-testing (running tests under Wine, etc). +# +test.target = $cxx.target diff --git a/tests/buildfile b/tests/buildfile new file mode 100644 index 0000000..aeeab15 --- /dev/null +++ b/tests/buildfile @@ -0,0 +1 @@ +./: {*/ -build/}