diff --git a/HttpProxy/CMakeLists.txt b/HttpProxy/CMakeLists.txt index d6cb30c..589813c 100644 --- a/HttpProxy/CMakeLists.txt +++ b/HttpProxy/CMakeLists.txt @@ -1,3 +1,7 @@ +project(HttpProxy + DESCRIPTION "router api is copy of boost_1_84_0/libs/url/example/router" +) + find_package(Boost COMPONENTS url REQUIRED) add_library(HttpProxy @@ -5,10 +9,9 @@ add_library(HttpProxy ProxyHttpSession.h ProxyHttpSession.cpp ProxyListener.h ProxyListener.cpp ProxyTcpSession.h ProxyTcpSession.cpp - TemplateMatchs.h TemplateMatchs.cpp - TemplateSegmentRule.h TemplateSegmentRule.cpp - UrlRouter.h UrlRouter.cpp - UrlRouterPrivate.h UrlRouterPrivate.cpp + + detail/impl/router.cpp + impl/matches.cpp ) target_include_directories(HttpProxy diff --git a/HttpProxy/TemplateMatchs.cpp b/HttpProxy/TemplateMatchs.cpp deleted file mode 100644 index a248e90..0000000 --- a/HttpProxy/TemplateMatchs.cpp +++ /dev/null @@ -1,12 +0,0 @@ -#include "TemplateMatchs.h" - -TemplateMatchStorageBase::const_reference TemplateMatchStorageBase::at(boost::core::string_view id) const { - for (std::size_t i = 0; i < size(); ++i) { - if (ids()[i] == id) return matches()[i]; - } - boost::throw_exception(std::out_of_range("")); -} - -TemplateMatchStorageBase::const_reference TemplateMatchStorageBase::operator[](boost::core::string_view id) const { - return at(id); -} diff --git a/HttpProxy/TemplateMatchs.h b/HttpProxy/TemplateMatchs.h deleted file mode 100644 index 6559efd..0000000 --- a/HttpProxy/TemplateMatchs.h +++ /dev/null @@ -1,57 +0,0 @@ -#ifndef __TEMPLATEMATCHS_H__ -#define __TEMPLATEMATCHS_H__ - -#include - -class TemplateMatchStorageBase { -public: - using const_reference = boost::core::string_view const &; - - virtual boost::core::string_view *matches() = 0; - virtual const boost::core::string_view *matches() const = 0; - - virtual boost::core::string_view *ids() = 0; - virtual const boost::core::string_view *ids() const = 0; - virtual std::size_t size() const = 0; - virtual void resize(std::size_t) = 0; - - const_reference at(boost::core::string_view id) const; - - const_reference operator[](boost::core::string_view id) const; -}; - -template -class TemplateMatchStorage : public TemplateMatchStorageBase { -public: - boost::core::string_view *matches() final { - return m_matches; - } - - virtual const boost::core::string_view *matches() const final { - return m_matches; - } - - boost::core::string_view *ids() final { - return m_ids; - } - - const boost::core::string_view *ids() const final { - return m_ids; - } - - std::size_t size() const final { - return m_size; - } - void resize(std::size_t n) final { - m_size = n; - } - -private: - boost::core::string_view m_matches[N]; - boost::core::string_view m_ids[N]; - std::size_t m_size; -}; - -using TemplateMatches = TemplateMatchStorage<20>; - -#endif // __TEMPLATEMATCHS_H__ diff --git a/HttpProxy/TemplateSegmentRule.cpp b/HttpProxy/TemplateSegmentRule.cpp deleted file mode 100644 index ff94027..0000000 --- a/HttpProxy/TemplateSegmentRule.cpp +++ /dev/null @@ -1,96 +0,0 @@ -#include "TemplateSegmentRule.h" -#include "BoostLog.h" -#include -#include -#include -#include -#include -#include - -boost::system::result TemplateSegmentRule::parse(const char *&iterator, - const char *end) const noexcept { - TemplateSegmentRule::value_type ret; - bool isTemplate = false; - if (iterator != end && *iterator == '{') { - auto it0 = iterator; - ++iterator; - auto rightBraces = boost::urls::grammar::find_if(iterator, end, boost::urls::grammar::lut_chars('}')); - if (rightBraces != end) { - boost::core::string_view segment(iterator, rightBraces); - static constexpr auto modifiers_cs = boost::urls::grammar::lut_chars("?*+"); - static constexpr auto id_rule = boost::urls::grammar::tuple_rule( - boost::urls::grammar::optional_rule(boost::urls::detail::arg_id_rule), - boost::urls::grammar::optional_rule(boost::urls::grammar::delim_rule(modifiers_cs))); - if (segment.empty() || boost::urls::grammar::parse(segment, id_rule)) { - isTemplate = true; - iterator = rightBraces + 1; - ret.m_string = boost::core::string_view(it0, rightBraces + 1); - ret.m_isLiteral = false; - if (segment.ends_with('?')) - ret.modifier = TemplateSegment::Modifier::Optional; - else if (segment.ends_with('*')) - ret.modifier = TemplateSegment::Modifier::Star; - else if (segment.ends_with('+')) - ret.modifier = TemplateSegment::Modifier::Plus; - } - } - if (!isTemplate) iterator = it0; - } - if (!isTemplate) { - auto rv = boost::urls::grammar::parse(iterator, end, boost::urls::detail::segment_rule); - BOOST_ASSERT(rv); - rv->decode({}, boost::urls::string_token::assign_to(ret.m_string)); - ret.m_isLiteral = true; - } - return ret; -} - -bool TemplateSegment::isLiteral() const { - return m_isLiteral; -} - -bool TemplateSegment::isStar() const { - return modifier == Modifier::Star; -} - -bool TemplateSegment::isPlus() const { - return modifier == Modifier::Plus; -} - -bool TemplateSegment::isOptional() const { - return modifier == Modifier::Optional; -} - -bool TemplateSegment::hasModifier() const { - return !m_isLiteral && modifier != Modifier::None; -} - -boost::core::string_view TemplateSegment::id() const { - BOOST_ASSERT(!isLiteral()); - boost::core::string_view r = {m_string}; - r.remove_prefix(1); - r.remove_suffix(1); - if (r.ends_with('?') || r.ends_with('+') || r.ends_with('*')) r.remove_suffix(1); - return r; -} - -boost::core::string_view TemplateSegment::string() const { - return m_string; -} - -bool TemplateSegment::match(boost::urls::pct_string_view segement) const { - if (m_isLiteral) return *segement == m_string; - return true; // other nodes match any string -} - -bool TemplateSegment::operator==(const TemplateSegment &other) const { - if (m_isLiteral != other.m_isLiteral) return false; - if (m_isLiteral) return m_string == other.m_string; - return modifier == other.modifier; -} - -bool TemplateSegment::operator<(const TemplateSegment &other) const { - if (other.m_isLiteral) return false; - if (m_isLiteral) return !other.m_isLiteral; - return modifier < other.modifier; -} diff --git a/HttpProxy/TemplateSegmentRule.h b/HttpProxy/TemplateSegmentRule.h deleted file mode 100644 index 10c347a..0000000 --- a/HttpProxy/TemplateSegmentRule.h +++ /dev/null @@ -1,63 +0,0 @@ -#ifndef __TEMPLATESEGMENTRULE_H__ -#define __TEMPLATESEGMENTRULE_H__ - -#include -#include -#include -#include -#include -#include -#include - -class TemplateSegment { - friend class TemplateSegmentRule; - -public: - enum class Modifier { - None, - Optional, // {id?} - Star, // {id*} - Plus // {id+} - }; - -public: - bool isLiteral() const; - bool isStar() const; - bool isPlus() const; - bool isOptional() const; - bool hasModifier() const; - boost::core::string_view id() const; - boost::core::string_view string() const; - bool match(boost::urls::pct_string_view segement) const; - bool operator==(const TemplateSegment &other) const; - - // segments have precedence: - // - literal - // - unique - // - optional - // - plus - // - star - bool operator<(const TemplateSegment &other) const; - -private: - Modifier modifier = Modifier::None; - std::string m_string; - bool m_isLiteral = true; -}; - -class TemplateSegmentRule { -public: - using value_type = TemplateSegment; - boost::system::result parse(char const *&iterator, char const *end) const noexcept; -}; - -constexpr auto templateSegmentRule = TemplateSegmentRule{}; - -constexpr auto templatePathRule = boost::urls::grammar::tuple_rule( - boost::urls::grammar::squelch(boost::urls::grammar::optional_rule(boost::urls::grammar::delim_rule('/'))), - boost::urls::grammar::range_rule( - templateSegmentRule, - boost::urls::grammar::tuple_rule(boost::urls::grammar::squelch(boost::urls::grammar::delim_rule('/')), - templateSegmentRule))); - -#endif // __TEMPLATESEGMENTRULE_H__ diff --git a/HttpProxy/UrlRouter.cpp b/HttpProxy/UrlRouter.cpp deleted file mode 100644 index 50fd831..0000000 --- a/HttpProxy/UrlRouter.cpp +++ /dev/null @@ -1 +0,0 @@ -#include "UrlRouter.h" \ No newline at end of file diff --git a/HttpProxy/UrlRouter.h b/HttpProxy/UrlRouter.h deleted file mode 100644 index eec6ae0..0000000 --- a/HttpProxy/UrlRouter.h +++ /dev/null @@ -1,47 +0,0 @@ -#ifndef __URLROUTER_H__ -#define __URLROUTER_H__ - -#include "TemplateMatchs.h" -#include "UrlRouterPrivate.h" -#include - -template -class UrlRouter : public UrlRouterPrivate { -public: - template - void insert(std::string_view pattern, U &&v) { - BOOST_STATIC_ASSERT(std::is_same::value || std::is_convertible::value || - std::is_base_of::value); - using ResourceType = - typename std::decay_t, U, Resource>>; - - class Implementation : public AnyResource { - public: - explicit Implementation(U &&u_) : resource(std::forward(u_)) { - } - void const *get() const noexcept override { - return static_cast(&resource); - } - - private: - ResourceType resource; - }; - auto resource = std::make_shared(std::forward(v)); - insertImpl(pattern, std::dynamic_pointer_cast(resource)); - } - - const Resource *find(boost::urls::segments_encoded_view path, TemplateMatchStorageBase &matches) const noexcept { - boost::core::string_view *matches_it = matches.matches(); - boost::core::string_view *ids_it = matches.ids(); - AnyResource const *p = findImpl(path, matches_it, ids_it); - if (p) { - BOOST_ASSERT(matches_it >= matches.matches()); - matches.resize(static_cast(matches_it - matches.matches())); - return reinterpret_cast(p->get()); - } - matches.resize(0); - return nullptr; - }; -}; - -#endif // __URLROUTER_H__ diff --git a/HttpProxy/UrlRouterPrivate.cpp b/HttpProxy/UrlRouterPrivate.cpp deleted file mode 100644 index fd511be..0000000 --- a/HttpProxy/UrlRouterPrivate.cpp +++ /dev/null @@ -1,323 +0,0 @@ -#include "UrlRouterPrivate.h" -#include "BoostLog.h" -#include "TemplateSegmentRule.h" -#include -#include - -UrlRouterPrivate::UrlRouterPrivate() { - m_nodes.push_back(SegementNode{}); -} - -void UrlRouterPrivate::insertImpl(boost::core::string_view pattern, const std::shared_ptr &resource) { - if (pattern.starts_with("/")) pattern.remove_prefix(1); - auto segements = boost::urls::grammar::parse(pattern, templatePathRule); - if (!segements) { - segements.value(); - } - auto iterator = segements->begin(); - auto end = segements->end(); - auto currentNode = &m_nodes.front(); - int level = 0; - while (iterator != end) { - boost::core::string_view segement = (*iterator).string(); - if (segement == ".") { - ++iterator; - continue; - } else if (segement == "..") { - if (currentNode == &m_nodes.front()) { - --level; - ++iterator; - continue; - } - std::size_t parentIndex = currentNode->parentIndex; - if (currentNode == &m_nodes.back() && !currentNode->resource && currentNode->childIndexes.empty()) { - auto p = &m_nodes[parentIndex]; - std::size_t currentIndex = currentNode - m_nodes.data(); - p->childIndexes.erase(std::remove(p->childIndexes.begin(), p->childIndexes.end(), currentIndex)); - m_nodes.pop_back(); - } - currentNode = &m_nodes[parentIndex]; - ++iterator; - continue; - } - if (level < 0) { - ++level; - ++iterator; - continue; - } - auto cit = std::find_if(currentNode->childIndexes.begin(), currentNode->childIndexes.end(), - [this, &iterator](std::size_t ci) -> bool { return m_nodes[ci].segment == *iterator; }); - if (cit != currentNode->childIndexes.end()) { - currentNode = &m_nodes[*cit]; - } else { - SegementNode child; - child.segment = *iterator; - std::size_t currentIndex = currentNode - m_nodes.data(); - child.parentIndex = currentIndex; - m_nodes.push_back(std::move(child)); - m_nodes[currentIndex].childIndexes.push_back(m_nodes.size() - 1); - if (m_nodes[currentIndex].childIndexes.size() > 1) { - // keep nodes sorted - auto &cs = m_nodes[currentIndex].childIndexes; - std::size_t n = cs.size() - 1; - while (n) { - if (m_nodes[cs.begin()[n]].segment < m_nodes[cs.begin()[n - 1]].segment) - std::swap(cs.begin()[n], cs.begin()[n - 1]); - else - break; - --n; - } - } - currentNode = &m_nodes.back(); - } - ++iterator; - } - if (level != 0) { - boost::urls::detail::throw_invalid_argument(); - } - currentNode->resource = resource; - currentNode->templatePath = pattern; -} - -const AnyResource *UrlRouterPrivate::findImpl(boost::urls::segments_encoded_view path, - boost::core::string_view *&matches, - boost::core::string_view *&ids) const noexcept { - if (path.empty()) path = boost::urls::segments_encoded_view("./"); - const SegementNode *p = tryMatch(path.begin(), path.end(), &m_nodes.front(), 0, matches, ids); - if (p) return p->resource.get(); - return nullptr; -} - -const SegementNode *UrlRouterPrivate::tryMatch(boost::urls::segments_encoded_base::const_iterator it, - boost::urls::segments_encoded_base::const_iterator end, - const SegementNode *current, int level, - boost::core::string_view *&matches, - boost::core::string_view *&ids) const { - while (it != end) { - boost::urls::pct_string_view s = *it; - if (*s == ".") { - // ignore segment - ++it; - continue; - } else if (*s == "..") { // move back to the parent node - ++it; - if (level <= 0 && current != &m_nodes.front()) { - if (!current->segment.isLiteral()) { - --matches; - --ids; - } - current = &m_nodes[current->parentIndex]; - } else - // there's no parent, so we - // discount that from the implicit - // tree beyond terminals - --level; - continue; - } - if (level < 0) { - ++level; - ++it; - continue; - } - - bool branch = false; - if (current->childIndexes.size() > 1) { - int branches_lb = 0; - for (auto i : current->childIndexes) { - auto &c = m_nodes[i]; - if (c.segment.isLiteral() || !c.segment.hasModifier()) { - // a literal path counts only - // if it matches - branches_lb += c.segment.match(s); - } else { - // everything not matching - // a single path counts as - // more than one path already - branches_lb = 2; - } - if (branches_lb > 1) { - // already know we need to - // branch - branch = true; - break; - } - } - } - const SegementNode *r = nullptr; - bool matchAny = false; - for (auto index : current->childIndexes) { - auto &child = m_nodes[index]; - if (!child.segment.match(s)) continue; - if (child.segment.isLiteral()) { - if (branch) { - r = tryMatch(std::next(it), end, &child, level, matches, ids); - if (r) break; - } else { - current = &child; - matchAny = true; - break; - } - } else if (!child.segment.hasModifier()) { - if (branch) { - auto matches0 = matches; - auto ids0 = ids; - *matches++ = *it; - *ids++ = child.segment.id(); - r = tryMatch(std::next(it), end, &child, level, matches, ids); - if (r) { - break; - } else { - // rewind - matches = matches0; - ids = ids0; - } - } else { - // only path possible - *matches++ = *it; - *ids++ = child.segment.id(); - current = &child; - matchAny = true; - break; - } - } else if (child.segment.isOptional()) { - auto matches0 = matches; - auto ids0 = ids; - *matches++ = *it; - *ids++ = child.segment.id(); - r = tryMatch(std::next(it), end, &child, level, matches, ids); - if (r) break; - matches = matches0; - ids = ids0; - *matches++ = {}; - *ids++ = child.segment.id(); - r = tryMatch(it, end, &child, level, matches, ids); - if (r) break; - matches = matches0; - ids = ids0; - } else { - auto first = it; - std::size_t ndotdot = 0; - std::size_t nnondot = 0; - auto it1 = it; - while (it1 != end) { - if (*it1 == "..") { - ++ndotdot; - if (ndotdot >= (nnondot + child.segment.isStar())) break; - } else if (*it1 != ".") { - ++nnondot; - } - ++it1; - } - if (it1 != end) break; - auto matches0 = matches; - auto ids0 = ids; - *matches++ = *it; - *ids++ = child.segment.id(); - if (child.segment.isPlus()) { - ++first; - } - auto start = end; - while (start != first) { - r = tryMatch(start, end, &child, level, matches, ids); - if (r) { - boost::core::string_view prev = *std::prev(start); - *matches0 = {matches0->data(), prev.data() + prev.size()}; - break; - } - matches = matches0 + 1; - ids = ids0 + 1; - --start; - } - if (r) { - break; - } - matches = matches0 + 1; - ids = ids0 + 1; - r = tryMatch(start, end, &child, level, matches, ids); - if (r) { - if (!child.segment.isPlus()) *matches0 = {}; - break; - } - } - } - if (r) return r; - if (!matchAny) ++level; - ++it; - } - if (level != 0) { // the path ended below or above an existing node - return nullptr; - } - if (!current->resource) { - return findOptionalResource(current, m_nodes, matches, ids); - } - return current; -} - -const SegementNode *UrlRouterPrivate::findOptionalResource(const SegementNode *root, - const std::vector &ns, - boost::core::string_view *&matches, - boost::core::string_view *&ids) { - BOOST_ASSERT(root); - if (root->resource) return root; - BOOST_ASSERT(!root->childIndexes.empty()); - for (auto index : root->childIndexes) { - auto &child = ns[index]; - if (!child.segment.isOptional() && !child.segment.isStar()) continue; - // Child nodes are also potentially optional. - auto matches0 = matches; - auto ids0 = ids; - *matches++ = {}; - *ids++ = child.segment.id(); - auto n = findOptionalResource(&child, ns, matches, ids); - if (n) return n; - matches = matches0; - ids = ids0; - } - return nullptr; -} - -bool ChildIndexVector::empty() const { - return m_size == 0; -} - -size_t ChildIndexVector::size() const { - return m_size; -} - -size_t *ChildIndexVector::begin() { - if (m_childIndexes) return m_childIndexes; - return m_staticChildIndexes; -} - -const size_t *ChildIndexVector::begin() const { - return const_cast(this)->begin(); -} - -size_t *ChildIndexVector::end() { - return begin() + m_size; -} - -const size_t *ChildIndexVector::end() const { - return const_cast(this)->end(); -} - -void ChildIndexVector::erase(size_t *it) { - BOOST_ASSERT(it - begin() >= 0); - std::memmove(it - 1, it, end() - it); - --m_size; -} - -void ChildIndexVector::push_back(size_t v) { - if (m_size == N && !m_childIndexes) { - m_childIndexes = new std::size_t[N * 2]; - m_capcity = N * 2; - std::memcpy(m_childIndexes, m_staticChildIndexes, N * sizeof(std::size_t)); - } else if (m_childIndexes && m_size == m_capcity) { - auto *tmp = new std::size_t[m_capcity * 2]; - std::memcpy(tmp, m_childIndexes, m_capcity * sizeof(std::size_t)); - delete[] m_childIndexes; - m_childIndexes = tmp; - m_capcity = m_capcity * 2; - } - begin()[m_size++] = v; -} diff --git a/HttpProxy/UrlRouterPrivate.h b/HttpProxy/UrlRouterPrivate.h deleted file mode 100644 index 63f6be2..0000000 --- a/HttpProxy/UrlRouterPrivate.h +++ /dev/null @@ -1,63 +0,0 @@ -#ifndef __URLROUTERPRIVATE_H__ -#define __URLROUTERPRIVATE_H__ - -#include "TemplateSegmentRule.h" -#include -#include -#include - -class AnyResource { -public: - virtual ~AnyResource() = default; - virtual void const *get() const noexcept = 0; -}; - -class ChildIndexVector { - static constexpr std::size_t N = 5; - -public: - bool empty() const; - std::size_t size() const; - std::size_t *begin(); - const std::size_t *begin() const; - std::size_t *end(); - const std::size_t *end() const; - void erase(std::size_t *it); - void push_back(std::size_t v); - -private: - std::size_t m_capcity{0}; - std::size_t m_size{0}; - std::size_t *m_childIndexes{nullptr}; - std::size_t m_staticChildIndexes[N]{}; -}; - -class SegementNode { -public: - static constexpr std::size_t npos{std::size_t(-1)}; - std::size_t parentIndex{npos}; - ChildIndexVector childIndexes; - TemplateSegment segment; - std::string templatePath; - std::shared_ptr resource; -}; - -class UrlRouterPrivate { -public: - UrlRouterPrivate(); - void insertImpl(boost::core::string_view pattern, const std::shared_ptr &resource); - const AnyResource *findImpl(boost::urls::segments_encoded_view path, boost::core::string_view *&matches, - boost::core::string_view *&ids) const noexcept; - -protected: - SegementNode const *tryMatch(boost::urls::segments_encoded_view::const_iterator it, - boost::urls::segments_encoded_view::const_iterator end, const SegementNode *current, - int level, boost::core::string_view *&matches, boost::core::string_view *&ids) const; - static SegementNode const *findOptionalResource(const SegementNode *root, std::vector const &ns, - boost::core::string_view *&matches, boost::core::string_view *&ids); - -private: - std::vector m_nodes; -}; - -#endif // __URLROUTERPRIVATE_H__ diff --git a/HttpProxy/detail/impl/router.cpp b/HttpProxy/detail/impl/router.cpp new file mode 100644 index 0000000..9c990a4 --- /dev/null +++ b/HttpProxy/detail/impl/router.cpp @@ -0,0 +1,949 @@ +// +// Copyright (c) 2023 Alan de Freitas (alandefreitas@gmail.com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/boostorg/url +// + +#ifndef BOOST_URL_DETAIL_ROUTER_IPP +#define BOOST_URL_DETAIL_ROUTER_IPP + +#include "../router.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace boost { +namespace urls { +namespace detail { + +// A path segment template +class segment_template +{ + enum class modifier : unsigned char + { + none, + // {id?} + optional, + // {id*} + star, + // {id+} + plus + }; + + std::string str_; + bool is_literal_ = true; + modifier modifier_ = modifier::none; + + friend struct segment_template_rule_t; +public: + segment_template() = default; + + bool + match(pct_string_view seg) const; + + core::string_view + string() const + { + return str_; + } + + core::string_view + id() const; + + bool + empty() const + { + return str_.empty(); + } + + bool + is_literal() const + { + return is_literal_; + } + + bool + has_modifier() const + { + return !is_literal() && + modifier_ != modifier::none; + } + + bool + is_optional() const + { + return modifier_ == modifier::optional; + } + + bool + is_star() const + { + return modifier_ == modifier::star; + } + + bool + is_plus() const + { + return modifier_ == modifier::plus; + } + + friend + bool operator==( + segment_template const& a, + segment_template const& b) + { + if (a.is_literal_ != b.is_literal_) + return false; + if (a.is_literal_) + return a.str_ == b.str_; + return a.modifier_ == b.modifier_; + } + + // segments have precedence: + // - literal + // - unique + // - optional + // - plus + // - star + friend + bool operator<( + segment_template const& a, + segment_template const& b) + { + if (b.is_literal()) + return false; + if (a.is_literal()) + return !b.is_literal(); + return a.modifier_ < b.modifier_; + } +}; + +// A segment template is either a literal string +// or a replacement field (as in a format_string). +// Fields cannot contain format specs and might +// have one of the following modifiers: +// - ?: optional segment +// - *: zero or more segments +// - +: one or more segments +struct segment_template_rule_t +{ + using value_type = segment_template; + + system::result + parse( + char const*& it, + char const* end + ) const noexcept; +}; + +constexpr auto segment_template_rule = segment_template_rule_t{}; + +constexpr auto path_template_rule = + grammar::tuple_rule( + grammar::squelch( + grammar::optional_rule( + grammar::delim_rule('/'))), + grammar::range_rule( + segment_template_rule, + grammar::tuple_rule( + grammar::squelch(grammar::delim_rule('/')), + segment_template_rule))); + +bool +segment_template:: +match(pct_string_view seg) const +{ + if (is_literal_) + return *seg == str_; + + // other nodes match any string + return true; +} + +core::string_view +segment_template:: +id() const +{ + // if (is_literal_) return {}; + BOOST_ASSERT(!is_literal()); + core::string_view r = {str_}; + r.remove_prefix(1); + r.remove_suffix(1); + if (r.ends_with('?') || + r.ends_with('+') || + r.ends_with('*')) + r.remove_suffix(1); + return r; +} + +auto +segment_template_rule_t:: +parse( + char const*& it, + char const* end) const noexcept + -> system::result +{ + segment_template t; + if (it != end && + *it == '{') + { + // replacement field + auto it0 = it; + ++it; + auto send = + grammar::find_if( + it, end, grammar::lut_chars('}')); + if (send != end) + { + core::string_view s(it, send); + static constexpr auto modifiers_cs = + grammar::lut_chars("?*+"); + static constexpr auto id_rule = + grammar::tuple_rule( + grammar::optional_rule( + arg_id_rule), + grammar::optional_rule( + grammar::delim_rule(modifiers_cs))); + if (s.empty() || + grammar::parse(s, id_rule)) + { + it = send + 1; + + t.str_ = core::string_view(it0, send + 1); + t.is_literal_ = false; + if (s.ends_with('?')) + t.modifier_ = + segment_template::modifier::optional; + else if (s.ends_with('*')) + t.modifier_ = + segment_template::modifier::star; + else if (s.ends_with('+')) + t.modifier_ = + segment_template::modifier::plus; + return t; + } + } + it = it0; + } + // literal segment + auto rv = grammar::parse( + it, end, urls::detail::segment_rule); + BOOST_ASSERT(rv); + rv->decode({}, urls::string_token::assign_to(t.str_)); + t.is_literal_ = true; + return t; +} + +// a small vector for child nodes... +// we shouldn't expect many children per node, and +// we don't want to allocate for that. But we also +// cannot cap the max number of child nodes because +// especially the root nodes can potentially an +// exponentially higher number of child nodes. +class child_idx_vector +{ + static constexpr std::size_t N = 5; + std::size_t static_child_idx_[N]{}; + std::size_t* child_idx{nullptr}; + std::size_t size_{0}; + std::size_t cap_{0}; + +public: + ~child_idx_vector() + { + delete[] child_idx; + } + + child_idx_vector() = default; + + child_idx_vector(child_idx_vector const& other) + : size_{other.size_} + , cap_{other.cap_} + { + if (other.child_idx) + { + child_idx = new std::size_t[cap_]; + std::memcpy(child_idx, other.child_idx, size_ * sizeof(std::size_t)); + return; + } + std::memcpy(static_child_idx_, other.static_child_idx_, size_ * sizeof(std::size_t)); + } + + child_idx_vector(child_idx_vector&& other) + : child_idx{other.child_idx} + , size_{other.size_} + , cap_{other.cap_} + { + std::memcpy(static_child_idx_, other.static_child_idx_, N); + other.child_idx = nullptr; + } + + bool + empty() const + { + return size_ == 0; + } + + std::size_t + size() const + { + return size_; + } + + std::size_t* + begin() + { + if (child_idx) + return child_idx; + return static_child_idx_; + } + + std::size_t* + end() + { + return begin() + size_; + } + + std::size_t const* + begin() const + { + if (child_idx) + return child_idx; + return static_child_idx_; + } + + std::size_t const* + end() const + { + return begin() + size_; + } + + void + erase(std::size_t* it) + { + BOOST_ASSERT(it - begin() >= 0); + std::memmove(it - 1, it, end() - it); + --size_; + } + + void + push_back(std::size_t v) + { + if (size_ == N && !child_idx) + { + child_idx = new std::size_t[N*2]; + cap_ = N*2; + std::memcpy(child_idx, static_child_idx_, N * sizeof(std::size_t)); + } + else if (child_idx && size_ == cap_) + { + auto* tmp = new std::size_t[cap_*2]; + std::memcpy(tmp, child_idx, cap_ * sizeof(std::size_t)); + delete[] child_idx; + child_idx = tmp; + cap_ = cap_*2; + } + begin()[size_++] = v; + } +}; + +// A node in the resource tree +// Each segment in the resource tree might be +// associated with +struct node +{ + static constexpr std::size_t npos{std::size_t(-1)}; + + // literal segment or replacement field + detail::segment_template seg{}; + + // A pointer to the resource + router_base::any_resource const* resource{nullptr}; + + // The complete match for the resource + std::string path_template; + + // Index of the parent node in the + // implementation pool of nodes + std::size_t parent_idx{npos}; + + // Index of child nodes in the pool + detail::child_idx_vector child_idx; +}; + +class impl +{ + // Pool of nodes in the resource tree + std::vector nodes_; + +public: + impl() + { + // root node with no resource + nodes_.push_back(node{}); + } + + ~impl() + { + for (auto &r: nodes_) + delete r.resource; + } + + // include a node for a resource + void + insert_impl( + core::string_view path, + router_base::any_resource const* v); + + // match a node and return the element + router_base::any_resource const* + find_impl( + segments_encoded_view path, + core::string_view*& matches, + core::string_view*& ids) const; + +private: + // try to match from this root node + node const* + try_match( + segments_encoded_view::const_iterator it, + segments_encoded_view::const_iterator end, + node const* root, + int level, + core::string_view*& matches, + core::string_view*& ids) const; + + // check if a node has a resource when we + // also consider optional paths through + // the child nodes. + static + node const* + find_optional_resource( + const node* root, + std::vector const& ns, + core::string_view*& matches, + core::string_view*& ids); +}; + +node const* +impl:: +find_optional_resource( + const node* root, + std::vector const& ns, + core::string_view*& matches, + core::string_view*& ids) +{ + BOOST_ASSERT(root); + if (root->resource) + return root; + BOOST_ASSERT(!root->child_idx.empty()); + for (auto i: root->child_idx) + { + auto& c = ns[i]; + if (!c.seg.is_optional() && + !c.seg.is_star()) + continue; + // Child nodes are also + // potentially optional. + auto matches0 = matches; + auto ids0 = ids; + *matches++ = {}; + *ids++ = c.seg.id(); + auto n = find_optional_resource( + &c, ns, matches, ids); + if (n) + return n; + matches = matches0; + ids = ids0; + } + return nullptr; +} + +void +impl:: +insert_impl( + core::string_view path, + router_base::any_resource const* v) +{ + // Parse dynamic route segments + if (path.starts_with("/")) + path.remove_prefix(1); + auto segsr = + grammar::parse(path, detail::path_template_rule); + if (!segsr) + { + delete v; + segsr.value(); + } + auto segs = *segsr; + auto it = segs.begin(); + auto end = segs.end(); + + // Iterate existing nodes + node* cur = &nodes_.front(); + int level = 0; + while (it != end) + { + core::string_view seg = (*it).string(); + if (seg == ".") + { + ++it; + continue; + } + if (seg == "..") + { + // discount unmatched leaf or + // keep track of levels behind root + if (cur == &nodes_.front()) + { + --level; + ++it; + continue; + } + // move to parent deleting current + // if it carries no resource + std::size_t p_idx = cur->parent_idx; + if (cur == &nodes_.back() && + !cur->resource && + cur->child_idx.empty()) + { + node* p = &nodes_[p_idx]; + std::size_t cur_idx = cur - nodes_.data(); + p->child_idx.erase( + std::remove( + p->child_idx.begin(), + p->child_idx.end(), + cur_idx)); + nodes_.pop_back(); + } + cur = &nodes_[p_idx]; + ++it; + continue; + } + // discount unmatched root parent + if (level < 0) + { + ++level; + ++it; + continue; + } + // look for child + auto cit = std::find_if( + cur->child_idx.begin(), + cur->child_idx.end(), + [this, &it](std::size_t ci) -> bool + { + return nodes_[ci].seg == *it; + }); + if (cit != cur->child_idx.end()) + { + // move to existing child + cur = &nodes_[*cit]; + } + else + { + // create child if it doesn't exist + node child; + child.seg = *it; + std::size_t cur_id = cur - nodes_.data(); + child.parent_idx = cur_id; + nodes_.push_back(std::move(child)); + nodes_[cur_id].child_idx.push_back(nodes_.size() - 1); + if (nodes_[cur_id].child_idx.size() > 1) + { + // keep nodes sorted + auto& cs = nodes_[cur_id].child_idx; + std::size_t n = cs.size() - 1; + while (n) + { + if (nodes_[cs.begin()[n]].seg < nodes_[cs.begin()[n - 1]].seg) + std::swap(cs.begin()[n], cs.begin()[n - 1]); + else + break; + --n; + } + } + cur = &nodes_.back(); + } + ++it; + } + if (level != 0) + { + delete v; + urls::detail::throw_invalid_argument(); + } + cur->resource = v; + cur->path_template = path; +} + +node const* +impl:: +try_match( + segments_encoded_view::const_iterator it, + segments_encoded_view::const_iterator end, + node const* cur, + int level, + core::string_view*& matches, + core::string_view*& ids) const +{ + while (it != end) + { + pct_string_view s = *it; + if (*s == ".") + { + // ignore segment + ++it; + continue; + } + if (*s == "..") + { + + // move back to the parent node + ++it; + if (level <= 0 && + cur != &nodes_.front()) + { + if (!cur->seg.is_literal()) + { + --matches; + --ids; + } + cur = &nodes_[cur->parent_idx]; + } + else + // there's no parent, so we + // discount that from the implicit + // tree beyond terminals + --level; + continue; + } + + // we are in the implicit tree above the + // root, so discount that as a level + if (level < 0) + { + ++level; + ++it; + continue; + } + + // calculate the lower bound on the + // possible number of branches to + // determine if we need to branch. + // We branch when we might have more than + // one child matching node at this level. + // If so, we need to potentially branch + // to find which path leads to a valid + // resource. Otherwise, we can just + // consume the node and input without + // any recursive function calls. + bool branch = false; + if (cur->child_idx.size() > 1) + { + int branches_lb = 0; + for (auto i: cur->child_idx) + { + auto& c = nodes_[i]; + if (c.seg.is_literal() || + !c.seg.has_modifier()) + { + // a literal path counts only + // if it matches + branches_lb += c.seg.match(s); + } + else + { + // everything not matching + // a single path counts as + // more than one path already + branches_lb = 2; + } + if (branches_lb > 1) + { + // already know we need to + // branch + branch = true; + break; + } + } + } + + // attempt to match each child node + node const* r = nullptr; + bool match_any = false; + for (auto i: cur->child_idx) + { + auto& c = nodes_[i]; + if (c.seg.match(s)) + { + if (c.seg.is_literal()) + { + // just continue from the + // next segment + if (branch) + { + r = try_match( + std::next(it), end, + &c, level, + matches, ids); + if (r) + break; + } + else + { + cur = &c; + match_any = true; + break; + } + } + else if (!c.seg.has_modifier()) + { + // just continue from the + // next segment + if (branch) + { + auto matches0 = matches; + auto ids0 = ids; + *matches++ = *it; + *ids++ = c.seg.id(); + r = try_match( + std::next(it), end, &c, + level, matches, ids); + if (r) + { + break; + } + else + { + // rewind + matches = matches0; + ids = ids0; + } + } + else + { + // only path possible + *matches++ = *it; + *ids++ = c.seg.id(); + cur = &c; + match_any = true; + break; + } + } + else if (c.seg.is_optional()) + { + // attempt to match by ignoring + // and not ignoring the segment. + // we first try the complete + // continuation consuming the + // input, which is the + // longest and most likely + // match + auto matches0 = matches; + auto ids0 = ids; + *matches++ = *it; + *ids++ = c.seg.id(); + r = try_match( + std::next(it), end, + &c, level, matches, ids); + if (r) + break; + // rewind + matches = matches0; + ids = ids0; + // try complete continuation + // consuming no segment + *matches++ = {}; + *ids++ = c.seg.id(); + r = try_match( + it, end, &c, + level, matches, ids); + if (r) + break; + // rewind + matches = matches0; + ids = ids0; + } + else + { + // check if the next segments + // won't send us to a parent + // directory + auto first = it; + std::size_t ndotdot = 0; + std::size_t nnondot = 0; + auto it1 = it; + while (it1 != end) + { + if (*it1 == "..") + { + ++ndotdot; + if (ndotdot >= (nnondot + c.seg.is_star())) + break; + } + else if (*it1 != ".") + { + ++nnondot; + } + ++it1; + } + if (it1 != end) + break; + + // attempt to match many + // segments + auto matches0 = matches; + auto ids0 = ids; + *matches++ = *it; + *ids++ = c.seg.id(); + // if this is a plus seg, we + // already consumed the first + // segment + if (c.seg.is_plus()) + { + ++first; + } + // {*} is usually the last + // match in a path. + // try complete continuation + // match for every subrange + // from {last, last} to + // {first, last}. + // We also try {last, last} + // first because it is the + // longest match. + auto start = end; + while (start != first) + { + r = try_match( + start, end, &c, + level, matches, ids); + if (r) + { + core::string_view prev = *std::prev(start); + *matches0 = { + matches0->data(), + prev.data() + prev.size()}; + break; + } + matches = matches0 + 1; + ids = ids0 + 1; + --start; + } + if (r) + { + break; + } + // start == first + matches = matches0 + 1; + ids = ids0 + 1; + r = try_match( + start, end, &c, + level, matches, ids); + if (r) + { + if (!c.seg.is_plus()) + *matches0 = {}; + break; + } + } + } + } + // r represent we already found a terminal + // node which is a match + if (r) + return r; + // if we couldn't match anything, we go + // one level up in the implicit tree + // because the path might still have a + // "..". + if (!match_any) + ++level; + ++it; + } + if (level != 0) + { + // the path ended below or above an + // existing node + return nullptr; + } + if (!cur->resource) + { + // we consumed all the input and reached + // a node with no resource, but it might + // still have child optional segments + // with resources we can reach without + // consuming any input + return find_optional_resource( + cur, nodes_, matches, ids); + } + return cur; +} + +router_base::any_resource const* +impl:: +find_impl( + segments_encoded_view path, + core::string_view*& matches, + core::string_view*& ids) const +{ + // parse_path is inconsistent for empty paths + if (path.empty()) + path = segments_encoded_view("./"); + + // Iterate nodes from the root + node const*p = try_match( + path.begin(), path.end(), + &nodes_.front(), 0, + matches, ids); + if (p) + return p->resource; + return nullptr; +} + +router_base:: +router_base() + : impl_(new impl{}) {} + +router_base:: +~router_base() +{ + delete reinterpret_cast(impl_); +} + +void +router_base:: +insert_impl( + core::string_view s, + any_resource const* v) +{ + reinterpret_cast(impl_) + ->insert_impl(s, v); +} + +auto +router_base:: +find_impl( + segments_encoded_view path, + core::string_view*& matches, + core::string_view*& ids) const noexcept + -> any_resource const* +{ + return reinterpret_cast(impl_) + ->find_impl(path, matches, ids); +} + +} // detail +} // urls +} // boost + +#endif diff --git a/HttpProxy/detail/router.hpp b/HttpProxy/detail/router.hpp new file mode 100644 index 0000000..f6392d8 --- /dev/null +++ b/HttpProxy/detail/router.hpp @@ -0,0 +1,58 @@ +// +// Copyright (c) 2022 Alan de Freitas (alandefreitas@gmail.com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/boostorg/url +// + +#ifndef BOOST_URL_DETAIL_ROUTER_HPP +#define BOOST_URL_DETAIL_ROUTER_HPP + +#include +#include +#include +#include +#include +#include +#include + +namespace boost { +namespace urls { +namespace detail { + +class router_base +{ + void* impl_{nullptr}; + +public: + // A type-erased router resource + struct any_resource + { + virtual ~any_resource() = default; + virtual void const* get() const noexcept = 0; + }; + +protected: + router_base(); + + virtual ~router_base(); + + void + insert_impl( + core::string_view s, + any_resource const* v); + + any_resource const* + find_impl( + segments_encoded_view path, + core::string_view*& matches, + core::string_view*& names) const noexcept; +}; + +} // detail +} // urls +} // boost + +#endif diff --git a/HttpProxy/impl/matches.cpp b/HttpProxy/impl/matches.cpp new file mode 100644 index 0000000..22ff6ea --- /dev/null +++ b/HttpProxy/impl/matches.cpp @@ -0,0 +1,98 @@ +// +// Copyright (c) 2023 Alan de Freitas (alandefreitas@gmail.com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/boostorg/url +// + +#include "../matches.hpp" + +namespace boost { +namespace urls { + +auto +matches_base:: +at( size_type pos ) const + -> const_reference +{ + if (pos < size()) + { + return matches()[pos]; + } + boost::throw_exception( + std::out_of_range("")); +} + +auto +matches_base:: +operator[]( size_type pos ) const + -> const_reference +{ + BOOST_ASSERT(pos < size()); + return matches()[pos]; +} + +auto +matches_base:: +at( core::string_view id ) const + -> const_reference +{ + for (std::size_t i = 0; i < size(); ++i) + { + if (ids()[i] == id) + return matches()[i]; + } + boost::throw_exception( + std::out_of_range("")); +} + +auto +matches_base:: +operator[]( core::string_view id ) const + -> const_reference +{ + return at(id); +} + +auto +matches_base:: +find( core::string_view id ) const + -> const_iterator +{ + for (std::size_t i = 0; i < size(); ++i) + { + if (ids()[i] == id) + return matches() + i; + } + return matches() + size(); +} + +auto +matches_base:: +begin() const + -> const_iterator +{ + return &matches()[0]; +} + +auto +matches_base:: +end() const + -> const_iterator +{ + return &matches()[size()]; +} + +auto +matches_base:: +empty() const noexcept + -> bool +{ + return size() == 0; +} + +} // urls +} // boost + diff --git a/HttpProxy/impl/router.hpp b/HttpProxy/impl/router.hpp new file mode 100644 index 0000000..02ee866 --- /dev/null +++ b/HttpProxy/impl/router.hpp @@ -0,0 +1,75 @@ +// +// Copyright (c) 2022 Alan de Freitas (alandefreitas@gmail.com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/boostorg/url +// + +#include +#include +#include +#include + +namespace boost { +namespace urls { + +template +template +void +router:: +insert(core::string_view pattern, U&& v) +{ + BOOST_STATIC_ASSERT( + std::is_same::value || + std::is_convertible::value || + std::is_base_of::value); + using U_ = typename std::decay< + typename std::conditional< + std::is_base_of::value, U, T + >::type>::type; + struct impl : any_resource + { + U_ u; + + explicit + impl(U&& u_) + : u(std::forward(u_)) + { + } + + void const* + get() const noexcept override + { + return static_cast(&u); + } + }; + any_resource const* p = new impl( + std::forward(v)); + insert_impl( pattern, p ); +} + +template +T const* +router:: +find(segments_encoded_view path, matches_base& m) const noexcept +{ + core::string_view* matches_it = m.matches(); + core::string_view* ids_it = m.ids(); + any_resource const* p = find_impl( + path, matches_it, ids_it ); + if (p) + { + BOOST_ASSERT(matches_it >= m.matches()); + m.resize(static_cast( + matches_it - m.matches())); + return reinterpret_cast< + T const*>(p->get()); + } + m.resize(0); + return nullptr; +} + +} // urls +} // boost diff --git a/HttpProxy/matches.hpp b/HttpProxy/matches.hpp new file mode 100644 index 0000000..b02f54d --- /dev/null +++ b/HttpProxy/matches.hpp @@ -0,0 +1,159 @@ +// +// Copyright (c) 2022 Alan de Freitas (alandefreitas@gmail.com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/boostorg/url +// + +#ifndef BOOST_URL_MATCHES_HPP +#define BOOST_URL_MATCHES_HPP + +#include +#include + +namespace boost { +namespace urls { + +// Base route match results +class matches_base +{ +public: + using iterator = core::string_view*; + using const_iterator = core::string_view const*; + using size_type = std::size_t; + using difference_type = std::ptrdiff_t; + using reference = core::string_view&; + using const_reference = core::string_view const&; + using pointer = core::string_view*; + using const_pointer = core::string_view const*; + + matches_base() = default; + + virtual ~matches_base() = default; + + virtual + core::string_view const* + matches() const = 0; + + virtual + core::string_view const* + ids() const = 0; + + virtual + core::string_view* + matches() = 0; + + virtual + core::string_view* + ids() = 0; + + virtual + std::size_t + size() const = 0; + + virtual + void + resize(std::size_t) = 0; + + const_reference + at( size_type pos ) const; + + const_reference + at( core::string_view id ) const; + + const_reference + operator[]( size_type pos ) const; + + const_reference + operator[]( core::string_view id ) const; + + const_iterator + find( core::string_view id ) const; + + const_iterator + begin() const; + + const_iterator + end() const; + + bool + empty() const noexcept; +}; + +/// A range type with the match results +template +class matches_storage + : public matches_base +{ + core::string_view matches_storage_[N]; + core::string_view ids_storage_[N]; + std::size_t size_; + + matches_storage( + core::string_view matches[N], + core::string_view ids[N], + std::size_t n) + { + for (std::size_t i = 0; i < n; ++i) + { + matches_storage_[i] = matches[i]; + ids_storage_[i] = ids[i]; + } + } + + virtual + core::string_view* + matches() override + { + return matches_storage_; + } + + virtual + core::string_view* + ids() override + { + return ids_storage_; + } + +public: + matches_storage() = default; + + virtual + core::string_view const* + matches() const override + { + return matches_storage_; + } + + virtual + core::string_view const* + ids() const override + { + return ids_storage_; + } + + virtual + std::size_t + size() const override + { + return size_; + } + + virtual + void + resize(std::size_t n) override + { + size_ = n; + } +}; + +/// Default type for storing route match results +using matches = matches_storage<20>; + +} // urls +} // boost + +#endif + diff --git a/HttpProxy/router.hpp b/HttpProxy/router.hpp new file mode 100644 index 0000000..d6d11a1 --- /dev/null +++ b/HttpProxy/router.hpp @@ -0,0 +1,90 @@ +// +// Copyright (c) 2022 Alan de Freitas (alandefreitas@gmail.com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/boostorg/url +// + +#ifndef BOOST_URL_ROUTER_HPP +#define BOOST_URL_ROUTER_HPP + +#include +#include +#include "detail/router.hpp" +#include "matches.hpp" + +namespace boost { +namespace urls { + +/** A URL router. + + This container matches static and dynamic + URL requests to an object which represents + how the it should be handled. These + values are usually callback functions. + + @tparam T type of resource associated with + each path template + + @tparam N maximum number of replacement fields + in a path template + + @par Exception Safety + + @li Functions marked `noexcept` provide the + no-throw guarantee, otherwise: + + @li Functions which throw offer the strong + exception safety guarantee. + + @see + @ref parse_absolute_uri, + @ref parse_relative_ref, + @ref parse_uri, + @ref parse_uri_reference, + @ref resolve. +*/ +template +class router + : private detail::router_base +{ +public: + /// Constructor + router() = default; + + /** Route the specified URL path to a resource + + @param path A url path with dynamic segments + @param resource A resource the path corresponds to + + @see + https://fmt.dev/latest/syntax.html + */ + template + void + insert(core::string_view pattern, U&& v); + + /** Match URL path to corresponding resource + + @param request Request path + @return The match results + */ + T const* + find(segments_encoded_view path, matches_base& m) const noexcept; + +#ifdef BOOST_URL_DOCS + /// @copydoc find + T const* + find(segments_encoded_view path, matches& m) const noexcept; +#endif +}; + +} // urls +} // boost + +#include "impl/router.hpp" + +#endif +