From 765f00935022eb54628a5c5c8e6d9d5a566f68f5 Mon Sep 17 00:00:00 2001 From: Peter Johnson Date: Sat, 27 Jun 2020 10:47:41 -0700 Subject: [PATCH] [wpiutil] Add HttpQueryMap and HttpPath/HttpPathRef (#2544) --- wpiutil/src/main/native/cpp/HttpUtil.cpp | 59 ++++- .../src/main/native/include/wpi/HttpUtil.h | 228 +++++++++++++++++- .../src/main/native/include/wpi/HttpUtil.inl | 4 + 3 files changed, 289 insertions(+), 2 deletions(-) diff --git a/wpiutil/src/main/native/cpp/HttpUtil.cpp b/wpiutil/src/main/native/cpp/HttpUtil.cpp index 37fea6014d..866ee10091 100644 --- a/wpiutil/src/main/native/cpp/HttpUtil.cpp +++ b/wpiutil/src/main/native/cpp/HttpUtil.cpp @@ -1,5 +1,5 @@ /*----------------------------------------------------------------------------*/ -/* Copyright (c) 2016-2018 FIRST. All Rights Reserved. */ +/* Copyright (c) 2016-2020 FIRST. All Rights Reserved. */ /* Open Source Software - may be modified and shared by FRC teams. The code */ /* must be accompanied by the FIRST BSD license file in the root directory of */ /* the project. */ @@ -86,6 +86,63 @@ StringRef EscapeURI(const Twine& str, SmallVectorImpl& buf, return StringRef{buf.data(), buf.size()}; } +HttpQueryMap::HttpQueryMap(wpi::StringRef query) { + wpi::SmallVector queryElems; + query.split(queryElems, '&', 100, false); + for (auto elem : queryElems) { + auto [nameEsc, valueEsc] = elem.split('='); + wpi::SmallString<64> nameBuf; + bool err = false; + auto name = wpi::UnescapeURI(nameEsc, nameBuf, &err); + // note: ignores duplicates + if (!err) m_elems.try_emplace(name, valueEsc); + } +} + +std::optional HttpQueryMap::Get( + wpi::StringRef name, wpi::SmallVectorImpl& buf) const { + auto it = m_elems.find(name); + if (it == m_elems.end()) return {}; + bool err = false; + auto val = wpi::UnescapeURI(it->second, buf, &err); + if (err) return {}; + return val; +} + +HttpPath::HttpPath(wpi::StringRef path) { + // special-case root path to be a single empty element + if (path == "/") { + m_pathEnds.emplace_back(0); + return; + } + wpi::SmallVector pathElems; + path.split(pathElems, '/', 100, false); + for (auto elem : pathElems) { + wpi::SmallString<64> buf; + bool err = false; + auto val = wpi::UnescapeURI(elem, buf, &err); + if (err) { + m_pathEnds.clear(); + return; + } + m_pathBuf += val; + m_pathEnds.emplace_back(m_pathBuf.size()); + } +} + +bool HttpPath::startswith(size_t start, ArrayRef match) const { + if (m_pathEnds.size() < (start + match.size())) return false; + bool first = start == 0; + auto p = m_pathEnds.begin() + start; + for (auto m : match) { + auto val = m_pathBuf.slice(first ? 0 : *(p - 1), *p); + if (val != m) return false; + first = false; + ++p; + } + return true; +} + bool ParseHttpHeaders(raw_istream& is, SmallVectorImpl* contentType, SmallVectorImpl* contentLength) { if (contentType) contentType->clear(); diff --git a/wpiutil/src/main/native/include/wpi/HttpUtil.h b/wpiutil/src/main/native/include/wpi/HttpUtil.h index b0668ebc2f..75b6e58606 100644 --- a/wpiutil/src/main/native/include/wpi/HttpUtil.h +++ b/wpiutil/src/main/native/include/wpi/HttpUtil.h @@ -1,5 +1,5 @@ /*----------------------------------------------------------------------------*/ -/* Copyright (c) 2016-2018 FIRST. All Rights Reserved. */ +/* Copyright (c) 2016-2020 FIRST. All Rights Reserved. */ /* Open Source Software - may be modified and shared by FRC teams. The code */ /* must be accompanied by the FIRST BSD license file in the root directory of */ /* the project. */ @@ -8,7 +8,9 @@ #ifndef WPIUTIL_WPI_HTTPUTIL_H_ #define WPIUTIL_WPI_HTTPUTIL_H_ +#include #include +#include #include #include #include @@ -59,6 +61,230 @@ bool ParseHttpHeaders(raw_istream& is, SmallVectorImpl* contentType, bool FindMultipartBoundary(wpi::raw_istream& is, StringRef boundary, std::string* saveBuf); +/** + * Map for looking up elements of the query portion of a URI. Does not + * handle multiple elements with the same name. This is a reference type; + * it does not make a copy of the query string, so it is important that the + * query string has a lifetime at least as long as this object. + */ +class HttpQueryMap { + public: + /** + * Constructs an empty map (with no entries). + */ + HttpQueryMap() = default; + + /** + * Constructs from an escaped query string. Note: does not make a copy of + * the query string, so it must not be a temporary. + * + * @param query query string + */ + explicit HttpQueryMap(StringRef query); + + /** + * Gets an element of the query string. Both the name and the returned + * value are unescaped strings. + * + * @param name name (unescaped) + * @param buf result buffer for value + * @return Optional unescaped value. Returns an empty optional if the + * name is not present in the query map. + */ + std::optional Get(StringRef name, + SmallVectorImpl& buf) const; + + private: + StringMap m_elems; +}; + +class HttpPathRef; + +/** + * Class for HTTP path matching. A root path is represented as a single + * empty element, otherwise the path consists of each non-empty element + * between the '/' characters: + * - "" -> [] + * - "/" -> [""] + * - "/foo" -> ["foo"] + * - "/foo/bar" -> ["foo", "bar"] + * - "/foo//bar/" -> ["foo", "bar"] + * + * All path elements are unescaped. + */ +class HttpPath { + public: + /** + * Constructs an empty HTTP path. + */ + HttpPath() = default; + + /** + * Constructs a HTTP path from an escaped path string. Makes a copy of the + * path, so it's safe to be a temporary. + */ + explicit HttpPath(StringRef path); + + /** + * Evaluates to true if the path is not empty. + */ + explicit operator bool() const { return !empty(); } + + /** + * Returns true if the path has no elements. + */ + bool empty() const { return m_pathEnds.empty(); } + + /** + * Returns number of elements in the path. + */ + size_t size() const { return m_pathEnds.size(); } + + /** + * Returns true if the path exactly matches the provided match list. + * + * @param match match list + * @return True if path equals match list + */ + bool equals(std::initializer_list match) const { + return equals(0, makeArrayRef(match.begin(), match.end())); + } + bool equals(ArrayRef match) const { return equals(0, match); } + bool equals(StringRef match) const { return equals(0, makeArrayRef(match)); } + + /** + * Returns true if the elements of the path starting at the "start" element + * match the provided match list, and there are no additional elements. + * + * @param start element to start matching at + * @param match match list + * @return True if match + */ + bool equals(size_t start, std::initializer_list match) const { + return equals(start, makeArrayRef(match.begin(), match.end())); + } + bool equals(size_t start, ArrayRef match) const { + if (m_pathEnds.size() != (start + match.size())) return false; + return startswith(start, match); + } + bool equals(size_t start, StringRef match) const { + return equals(start, makeArrayRef(match)); + } + + /** + * Returns true if the first elements of the path match the provided match + * list. The path may have additional elements. + * + * @param match match list + * @return True if path starts with match list + */ + bool startswith(std::initializer_list match) const { + return startswith(0, makeArrayRef(match.begin(), match.end())); + } + bool startswith(ArrayRef match) const { + return startswith(0, match); + } + bool startswith(StringRef match) const { + return startswith(0, makeArrayRef(match)); + } + + /** + * Returns true if the elements of the path starting at the "start" element + * match the provided match list. The path may have additional elements. + * + * @param start element to start matching at + * @param match match list + * @return True if path starting at the start element matches the match list + */ + bool startswith(size_t start, std::initializer_list match) const { + return startswith(start, makeArrayRef(match.begin(), match.end())); + } + + bool startswith(size_t start, ArrayRef match) const; + + bool startswith(size_t start, StringRef match) const { + return startswith(start, makeArrayRef(match)); + } + + /** + * Gets a single element of the path. + */ + StringRef operator[](size_t n) const { + return m_pathBuf.slice(n == 0 ? 0 : m_pathEnds[n - 1], m_pathEnds[n]); + } + + /** + * Returns a path reference with the first N elements of the path removed. + */ + HttpPathRef drop_front(size_t n) const; + + private: + SmallString<128> m_pathBuf; + SmallVector m_pathEnds; +}; + +/** + * Proxy reference object for a portion of a HttpPath. + */ +class HttpPathRef { + public: + HttpPathRef() = default; + /*implicit*/ HttpPathRef(const HttpPath& path, // NOLINT(runtime/explicit) + size_t start = 0) + : m_path(&path), m_start(start) {} + + explicit operator bool() const { return !empty(); } + bool empty() const { return m_path && m_path->size() == m_start; } + size_t size() const { return m_path ? m_path->size() - m_start : 0; } + + bool equals(std::initializer_list match) const { + return equals(0, makeArrayRef(match.begin(), match.end())); + } + bool equals(ArrayRef match) const { return equals(0, match); } + bool equals(StringRef match) const { return equals(0, makeArrayRef(match)); } + + bool equals(size_t start, std::initializer_list match) const { + return equals(start, makeArrayRef(match.begin(), match.end())); + } + bool equals(size_t start, ArrayRef match) const { + return m_path ? m_path->equals(m_start + start, match) : false; + } + bool equals(size_t start, StringRef match) const { + return equals(start, makeArrayRef(match)); + } + + bool startswith(std::initializer_list match) const { + return startswith(0, makeArrayRef(match.begin(), match.end())); + } + bool startswith(ArrayRef match) const { + return startswith(0, match); + } + bool startswith(StringRef match) const { + return startswith(0, makeArrayRef(match)); + } + + bool startswith(size_t start, std::initializer_list match) const { + return startswith(start, makeArrayRef(match.begin(), match.end())); + } + bool startswith(size_t start, ArrayRef match) const { + return m_path ? m_path->startswith(m_start + start, match) : false; + } + bool startswith(size_t start, StringRef match) const { + return startswith(start, makeArrayRef(match)); + } + + StringRef operator[](size_t n) const { + return m_path ? m_path->operator[](m_start + n) : StringRef{}; + } + HttpPathRef drop_front(size_t n) const { + return m_path ? m_path->drop_front(m_start + n) : HttpPathRef{}; + } + + private: + const HttpPath* m_path = nullptr; + size_t m_start = 0; +}; + class HttpLocation { public: HttpLocation() = default; diff --git a/wpiutil/src/main/native/include/wpi/HttpUtil.inl b/wpiutil/src/main/native/include/wpi/HttpUtil.inl index c6d6ed4330..f66c746d76 100644 --- a/wpiutil/src/main/native/include/wpi/HttpUtil.inl +++ b/wpiutil/src/main/native/include/wpi/HttpUtil.inl @@ -12,6 +12,10 @@ namespace wpi { +inline HttpPathRef HttpPath::drop_front(size_t n) const { + return HttpPathRef(*this, n); +} + template HttpRequest::HttpRequest(const HttpLocation& loc, const T& extraParams) : host{loc.host}, port{loc.port} {