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.
This commit is contained in:
Andreas Kling 2019-08-10 17:27:56 +02:00
parent aaccf6ee4e
commit ed43770b2f
Notes: sideshowbarker 2024-07-19 12:46:11 +09:00
5 changed files with 223 additions and 1 deletions

View file

@ -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)

49
AK/Tests/TestURL.cpp Normal file
View file

@ -0,0 +1,49 @@
#include <AK/TestSuite.h>
#include <AK/URL.h>
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)

130
AK/URL.cpp Normal file
View file

@ -0,0 +1,130 @@
#include <AK/StringBuilder.h>
#include <AK/URL.h>
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<char, 256> 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();
}
}

38
AK/URL.h Normal file
View file

@ -0,0 +1,38 @@
#pragma once
#include <AK/AKString.h>
#include <AK/StringView.h>
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();
}

View file

@ -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 \