From ed43770b2f2f4e988eec730f0b27f03ea02a13f0 Mon Sep 17 00:00:00 2001 From: Andreas Kling Date: Sat, 10 Aug 2019 17:27:56 +0200 Subject: [PATCH] AK: Add a basic URL class to help us handle URL's We're gonna need these as we start to write more networking programs. --- AK/Tests/Makefile | 6 +- AK/Tests/TestURL.cpp | 49 +++++++++++++++ AK/URL.cpp | 130 ++++++++++++++++++++++++++++++++++++++++ AK/URL.h | 38 ++++++++++++ Libraries/LibC/Makefile | 1 + 5 files changed, 223 insertions(+), 1 deletion(-) create mode 100644 AK/Tests/TestURL.cpp create mode 100644 AK/URL.cpp create mode 100644 AK/URL.h diff --git a/AK/Tests/Makefile b/AK/Tests/Makefile index 380c9eab60..b1c7b8a248 100644 --- a/AK/Tests/Makefile +++ b/AK/Tests/Makefile @@ -1,4 +1,4 @@ -PROGRAMS = TestString TestQueue TestVector TestHashMap TestJSON TestWeakPtr TestNonnullRefPtr TestRefPtr TestFixedArray TestFileSystemPath +PROGRAMS = TestString TestQueue TestVector TestHashMap TestJSON TestWeakPtr TestNonnullRefPtr TestRefPtr TestFixedArray TestFileSystemPath TestURL CXXFLAGS = -std=c++17 -Wall -Wextra -ggdb3 -O2 -I../ -I../../ @@ -13,6 +13,7 @@ SHARED_TEST_OBJS = \ ../JsonValue.o \ ../JsonParser.o \ ../FileSystemPath.o \ + ../URL.o \ .cpp.o: @echo "HOST_CXX $<"; $(PRE_CXX) $(CXX) $(CXXFLAGS) -o $@ -c $< @@ -58,6 +59,9 @@ TestOptional: TestOptional.o $(SHARED_TEST_OBJS) TestFileSystemPath: TestFileSystemPath.o $(SHARED_TEST_OBJS) $(PRE_CXX) $(CXX) $(CXXFLAGS) -o $@ TestFileSystemPath.o $(SHARED_TEST_OBJS) +TestURL: TestURL.o $(SHARED_TEST_OBJS) + $(PRE_CXX) $(CXX) $(CXXFLAGS) -o $@ TestURL.o $(SHARED_TEST_OBJS) + clean: rm -f $(SHARED_TEST_OBJS) diff --git a/AK/Tests/TestURL.cpp b/AK/Tests/TestURL.cpp new file mode 100644 index 0000000000..5d3b0b33a8 --- /dev/null +++ b/AK/Tests/TestURL.cpp @@ -0,0 +1,49 @@ +#include + +#include + +TEST_CASE(construct) +{ + EXPECT_EQ(URL().is_valid(), false); +} + +TEST_CASE(basic) +{ + { + URL url("http://www.serenityos.org/index.html"); + EXPECT_EQ(url.is_valid(), true); + EXPECT_EQ(url.protocol(), "http"); + EXPECT_EQ(url.port(), 80); + EXPECT_EQ(url.path(), "/index.html"); + } + { + URL url("https://localhost:1234/~anon/test/page.html"); + EXPECT_EQ(url.is_valid(), true); + EXPECT_EQ(url.protocol(), "https"); + EXPECT_EQ(url.port(), 1234); + EXPECT_EQ(url.path(), "/~anon/test/page.html"); + } +} + +TEST_CASE(some_bad_urls) +{ + EXPECT_EQ(URL("http:serenityos.org").is_valid(), false); + EXPECT_EQ(URL("http:/serenityos.org").is_valid(), false); + EXPECT_EQ(URL("http//serenityos.org").is_valid(), false); + EXPECT_EQ(URL("http:///serenityos.org").is_valid(), false); + EXPECT_EQ(URL("serenityos.org").is_valid(), false); + EXPECT_EQ(URL("://serenityos.org").is_valid(), false); + EXPECT_EQ(URL("http://serenityos.org:80:80/").is_valid(), false); + EXPECT_EQ(URL("http://serenityos.org:80:80").is_valid(), false); + EXPECT_EQ(URL("http://serenityos.org:abc").is_valid(), false); + EXPECT_EQ(URL("http://serenityos.org:abc:80").is_valid(), false); + EXPECT_EQ(URL("http://serenityos.org:abc:80/").is_valid(), false); + EXPECT_EQ(URL("http://serenityos.org:/abc/").is_valid(), false); +} + +TEST_CASE(serialization) +{ + EXPECT_EQ(URL("http://www.serenityos.org/").to_string(), "http://www.serenityos.org:80/"); +} + +TEST_MAIN(URL) diff --git a/AK/URL.cpp b/AK/URL.cpp new file mode 100644 index 0000000000..1a026d9e5f --- /dev/null +++ b/AK/URL.cpp @@ -0,0 +1,130 @@ +#include +#include + +namespace AK { + +static inline bool is_valid_protocol_character(char ch) +{ + return ch >= 'a' && ch <= 'z'; +} + +static inline bool is_valid_hostname_character(char ch) +{ + return ch && ch != '/' && ch != ':'; +} + +static inline bool is_digit(char ch) +{ + return ch >= '0' && ch <= '9'; +} + +bool URL::parse(const StringView& string) +{ + enum class State { + InProtocol, + InHostname, + InPort, + InPath, + }; + + Vector buffer; + State state { State::InProtocol }; + + int index = 0; + + auto peek = [&] { + if (index >= string.length()) + return '\0'; + return string[index]; + }; + + auto consume = [&] { + if (index >= string.length()) + return '\0'; + return string[index++]; + }; + + while (index < string.length()) { + switch (state) { + case State::InProtocol: + if (is_valid_protocol_character(peek())) { + buffer.append(consume()); + continue; + } + if (consume() != ':') + return false; + if (consume() != '/') + return false; + if (consume() != '/') + return false; + if (buffer.is_empty()) + return false; + m_protocol = String::copy(buffer); + buffer.clear(); + state = State::InHostname; + continue; + case State::InHostname: + if (is_valid_hostname_character(peek())) { + buffer.append(consume()); + continue; + } + if (buffer.is_empty()) + return false; + m_host = String::copy(buffer); + buffer.clear(); + if (peek() == ':') { + consume(); + state = State::InPort; + continue; + } + if (peek() == '/') { + state = State::InPath; + continue; + } + return false; + case State::InPort: + if (is_digit(peek())) { + buffer.append(consume()); + continue; + } + if (buffer.is_empty()) + return false; + { + bool ok; + m_port = String::copy(buffer).to_uint(ok); + buffer.clear(); + if (!ok) + return false; + } + if (peek() == '/') { + state = State::InPath; + continue; + } + return false; + case State::InPath: + buffer.append(consume()); + continue; + } + } + m_path = String::copy(buffer); + return true; +} + +URL::URL(const StringView& string) +{ + m_valid = parse(string); +} + +String URL::to_string() const +{ + StringBuilder builder; + builder.append(m_protocol); + builder.append("://"); + builder.append(m_host); + builder.append(':'); + builder.append(String::number(m_port)); + builder.append(m_path); + return builder.to_string(); +} + +} diff --git a/AK/URL.h b/AK/URL.h new file mode 100644 index 0000000000..30a3ec353f --- /dev/null +++ b/AK/URL.h @@ -0,0 +1,38 @@ +#pragma once + +#include +#include + +namespace AK { + +class URL { +public: + URL() {} + URL(const StringView&); + + bool is_valid() const { return m_valid; } + String protocol() const { return m_protocol; } + String host() const { return m_host; } + String path() const { return m_path; } + u16 port() const { return m_port; } + + String to_string() const; + +private: + bool parse(const StringView&); + + bool m_valid { false }; + u16 m_port { 80 }; + String m_protocol; + String m_host; + String m_path; +}; + +} + +using AK::URL; + +inline const LogStream& operator<<(const LogStream& stream, const URL& value) +{ + return stream << value.to_string(); +} diff --git a/Libraries/LibC/Makefile b/Libraries/LibC/Makefile index f37082fc49..004a32b3b1 100644 --- a/Libraries/LibC/Makefile +++ b/Libraries/LibC/Makefile @@ -6,6 +6,7 @@ AK_OBJS = \ ../../AK/StringView.o \ ../../AK/StringBuilder.o \ ../../AK/FileSystemPath.o \ + ../../AK/URL.o \ ../../AK/JsonValue.o \ ../../AK/JsonArray.o \ ../../AK/JsonObject.o \