1002 lines
24 KiB
C++
1002 lines
24 KiB
C++
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
#include "Utilities/URLParser.h"
|
|
|
|
|
|
namespace Electra
|
|
{
|
|
namespace URLComponents
|
|
{
|
|
static const FString KeepCharsPath(TEXT("/"));
|
|
static const FString KeepCharsFragment(TEXT("!$&'()*+,;=;@/?"));
|
|
|
|
static const FString URN(TEXT("urn:"));
|
|
static const FString Data(TEXT("data:"));
|
|
static const FString Slash(TEXT("/"));
|
|
static const FString DotSlash(TEXT("./"));
|
|
static const FString SlashSlash(TEXT("//"));
|
|
static const FString SchemeFile(TEXT("file"));
|
|
static const FString SchemeData(TEXT("data"));
|
|
static const FString SchemeHttp(TEXT("http"));
|
|
static const FString SchemeHttps(TEXT("https"));
|
|
static const FString Colon(TEXT(":"));
|
|
static const FString QuestionMark(TEXT("?"));
|
|
static const FString HashTag(TEXT("#"));
|
|
static const FString Ampersand(TEXT("&"));
|
|
static const FString At(TEXT("@"));
|
|
static const FString BracketOpen(TEXT("["));
|
|
static const FString BracketClose(TEXT("]"));
|
|
static const FString Dot(TEXT("."));
|
|
static const FString DotDot(TEXT(".."));
|
|
static const FString Port80(TEXT("80"));
|
|
static const FString Port443(TEXT("443"));
|
|
}
|
|
|
|
static inline void _SwapStrings(FString& a, FString& b)
|
|
{
|
|
FString s = MoveTemp(a);
|
|
a = MoveTemp(b);
|
|
b = MoveTemp(s);
|
|
}
|
|
|
|
void FURL_RFC3986::Empty()
|
|
{
|
|
Scheme.Empty();
|
|
UserInfo.Empty();
|
|
Host.Empty();
|
|
Port.Empty();
|
|
Path.Empty();
|
|
Query.Empty();
|
|
Fragment.Empty();
|
|
bIsFile = false;
|
|
bIsData = false;
|
|
}
|
|
|
|
void FURL_RFC3986::Swap(FURL_RFC3986& Other)
|
|
{
|
|
_SwapStrings(Scheme, Other.Scheme);
|
|
_SwapStrings(UserInfo, Other.UserInfo);
|
|
_SwapStrings(Host, Other.Host);
|
|
_SwapStrings(Port, Other.Port);
|
|
_SwapStrings(Path, Other.Path);
|
|
_SwapStrings(Query, Other.Query);
|
|
_SwapStrings(Fragment, Other.Fragment);
|
|
bool b = bIsFile; bIsFile = Other.bIsFile; Other.bIsFile = b;
|
|
b = bIsData; bIsData = Other.bIsData; Other.bIsData = b;
|
|
}
|
|
|
|
bool FURL_RFC3986::Parse(const FString& InURL)
|
|
{
|
|
Empty();
|
|
if (InURL.IsEmpty())
|
|
{
|
|
return true;
|
|
}
|
|
// Not handling URNs here.
|
|
if (InURL.StartsWith(URLComponents::URN))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Short circuit data URLs
|
|
if (InURL.StartsWith(URLComponents::Data))
|
|
{
|
|
bIsData = true;
|
|
Scheme = URLComponents::SchemeData;
|
|
Path = InURL.Mid(5);
|
|
return true;
|
|
}
|
|
|
|
/*
|
|
RFC 3986, section 3: Syntax components
|
|
|
|
foo://example.com:8042/over/there?name=ferret#nose
|
|
\_/ \______________/\_________/ \_________/ \__/
|
|
| | | | |
|
|
scheme authority path query fragment
|
|
*/
|
|
StringHelpers::FStringIterator it(InURL);
|
|
if (InURL.StartsWith(URLComponents::SlashSlash))
|
|
{
|
|
it += 2;
|
|
if (ParseAuthority(it))
|
|
{
|
|
return ParsePathAndQueryFragment(it);
|
|
}
|
|
return false;
|
|
}
|
|
// Check if the URL begins with a path, query or fragment.
|
|
else if (IsPathSeparator(*it) || IsQueryOrFragmentSeparator(*it) || *it == TCHAR('.'))
|
|
{
|
|
return ParsePathAndQueryFragment(it);
|
|
}
|
|
else
|
|
{
|
|
// If we get a URL without a scheme we will trip over the colon separating host and port, mistaking it for
|
|
// the scheme delimiter. For our purposes we only handle URLs that have a scheme when they have an authority!
|
|
FString Component;
|
|
Component.Reserve(InURL.Len());
|
|
// Parse out the first component up to the delimiting colon, path, query or fragment.
|
|
while(it && !IsColonSeparator(*it) && !IsQueryOrFragmentSeparator(*it) && !IsPathSeparator(*it))
|
|
{
|
|
Component += *it++;
|
|
}
|
|
// If we found the colon we assume it separates the scheme from the authority.
|
|
if (it && IsColonSeparator(*it))
|
|
{
|
|
++it;
|
|
if (!it)
|
|
{
|
|
// Cannot just be a scheme with no authority or path.
|
|
return false;
|
|
}
|
|
// Assume we parsed the scheme.
|
|
Scheme = MoveTemp(Component);
|
|
bIsFile = Scheme.Equals(URLComponents::SchemeFile);
|
|
|
|
// Does an absolute path ('/') or an authority ('//') follow?
|
|
if (IsPathSeparator(*it))
|
|
{
|
|
++it;
|
|
// Authority?
|
|
if (it && IsPathSeparator(*it))
|
|
{
|
|
++it;
|
|
|
|
// Check for a third '/' in Windows filenames like "file:///c:/autoexec.bat"
|
|
if (bIsFile && IsPathSeparator(*it) && it.GetRemainingLength() > 3)
|
|
{
|
|
const TCHAR* Next = it.GetRemainder();
|
|
if (Next[2] == TCHAR(':') && Next[3] == TCHAR('/'))
|
|
{
|
|
++it;
|
|
}
|
|
}
|
|
|
|
// Parse out the authority and if successful continue parsing out the path.
|
|
if (!ParseAuthority(it))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Unwind the absolute path '/' and parse the path.
|
|
--it;
|
|
}
|
|
}
|
|
if (!ParsePathAndQueryFragment(it))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Got either '/', '?' or '#' while scanning for the scheme. Rewind the iterator and handle the entire thing as a path with or without query or fragment.
|
|
it.Reset();
|
|
return ParsePathAndQueryFragment(it);
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
|
|
void FURL_RFC3986::SetScheme(const FString& InNewScheme)
|
|
{
|
|
Scheme = InNewScheme;
|
|
bIsFile = Scheme.Equals(URLComponents::SchemeFile);
|
|
bIsData = Scheme.Equals(URLComponents::SchemeData);
|
|
}
|
|
void FURL_RFC3986::SetHost(const FString& InNewHost)
|
|
{
|
|
Host = InNewHost;
|
|
}
|
|
void FURL_RFC3986::SetPort(const FString& InNewPort)
|
|
{
|
|
Port = InNewPort;
|
|
}
|
|
void FURL_RFC3986::SetPath(const FString& InNewPath)
|
|
{
|
|
Path = InNewPath;
|
|
}
|
|
void FURL_RFC3986::SetQuery(const FString& InNewQuery)
|
|
{
|
|
Query = InNewQuery;
|
|
}
|
|
void FURL_RFC3986::SetFragment(const FString& InNewFragment)
|
|
{
|
|
Fragment = InNewFragment;
|
|
}
|
|
|
|
|
|
FString FURL_RFC3986::GetScheme() const
|
|
{
|
|
return Scheme;
|
|
}
|
|
|
|
FString FURL_RFC3986::GetHost() const
|
|
{
|
|
return Host;
|
|
}
|
|
|
|
FString FURL_RFC3986::GetPort() const
|
|
{
|
|
return Port;
|
|
}
|
|
|
|
FString FURL_RFC3986::GetPath() const
|
|
{
|
|
return Path;
|
|
}
|
|
|
|
FString FURL_RFC3986::GetQuery() const
|
|
{
|
|
return Query;
|
|
}
|
|
|
|
FString FURL_RFC3986::GetFragment() const
|
|
{
|
|
return Fragment;
|
|
}
|
|
|
|
FString FURL_RFC3986::Get(bool bIncludeQuery, bool bIncludeFragment)
|
|
{
|
|
if (bIsData)
|
|
{
|
|
return FString::Printf(TEXT("%s%s"), *URLComponents::Data, *Path);
|
|
}
|
|
FString URL;
|
|
URL.Reserve(Scheme.Len() + UserInfo.Len() + Host.Len() + Port.Len() + Path.Len() + Query.Len() + Fragment.Len() + 32);
|
|
if (IsAbsolute())
|
|
{
|
|
FString Authority = GetAuthority();
|
|
URL = Scheme;
|
|
URL += URLComponents::Colon;
|
|
if (Authority.Len() || bIsFile)
|
|
{
|
|
URL += URLComponents::SlashSlash;
|
|
URL += Authority;
|
|
}
|
|
if (Path.Len())
|
|
{
|
|
if (Authority.Len() && !IsPathSeparator(Path[0]))
|
|
{
|
|
URL += URLComponents::Slash;
|
|
}
|
|
UrlEncode(URL, Path, URLComponents::KeepCharsPath);
|
|
}
|
|
else if ((Query.Len() && bIncludeQuery) || (Fragment.Len() && bIncludeFragment))
|
|
{
|
|
URL += URLComponents::Slash;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
UrlEncode(URL, Path, URLComponents::KeepCharsPath);
|
|
}
|
|
if (Query.Len() && bIncludeQuery)
|
|
{
|
|
URL += URLComponents::QuestionMark;
|
|
URL += Query;
|
|
}
|
|
if (Fragment.Len() && bIncludeFragment)
|
|
{
|
|
URL += URLComponents::HashTag;
|
|
UrlEncode(URL, Fragment, URLComponents::KeepCharsFragment);
|
|
}
|
|
return URL;
|
|
}
|
|
|
|
FString FURL_RFC3986::GetPath(bool bIncludeQuery, bool bIncludeFragment)
|
|
{
|
|
if (bIsData)
|
|
{
|
|
return Path;
|
|
}
|
|
FString URL;
|
|
URL.Reserve(Path.Len() + Query.Len() + Fragment.Len() + 32);
|
|
if (IsAbsolute())
|
|
{
|
|
if (Path.Len())
|
|
{
|
|
if (GetAuthority().Len() && !IsPathSeparator(Path[0]))
|
|
{
|
|
URL += URLComponents::Slash;
|
|
}
|
|
UrlEncode(URL, Path, URLComponents::KeepCharsPath);
|
|
}
|
|
else if ((Query.Len() && bIncludeQuery) || (Fragment.Len() && bIncludeFragment))
|
|
{
|
|
URL += URLComponents::Slash;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
UrlEncode(URL, Path, URLComponents::KeepCharsPath);
|
|
}
|
|
if (Query.Len() && bIncludeQuery)
|
|
{
|
|
URL += URLComponents::QuestionMark;
|
|
URL += Query;
|
|
}
|
|
if (Fragment.Len() && bIncludeFragment)
|
|
{
|
|
URL += URLComponents::HashTag;
|
|
UrlEncode(URL, Fragment, URLComponents::KeepCharsFragment);
|
|
}
|
|
return URL;
|
|
}
|
|
|
|
void FURL_RFC3986::GetQueryParams(TArray<FQueryParam>& OutQueryParams, const FString& InQueryParameters, bool bPerformUrlDecoding, bool bSameNameReplacesValue)
|
|
{
|
|
TArray<FString> ValuePairs;
|
|
InQueryParameters.ParseIntoArray(ValuePairs, *URLComponents::Ampersand, true);
|
|
for(int32 i=0; i<ValuePairs.Num(); ++i)
|
|
{
|
|
int32 EqPos = 0;
|
|
ValuePairs[i].FindChar(TCHAR('='), EqPos);
|
|
FQueryParam qp;
|
|
bool bOk = true;
|
|
if (bPerformUrlDecoding)
|
|
{
|
|
bOk = UrlDecode(qp.Name, ValuePairs[i].Mid(0, EqPos)) && UrlDecode(qp.Value, ValuePairs[i].Mid(EqPos+1));
|
|
}
|
|
else
|
|
{
|
|
qp.Name = ValuePairs[i].Mid(0, EqPos);
|
|
qp.Value = ValuePairs[i].Mid(EqPos+1);
|
|
}
|
|
if (bOk)
|
|
{
|
|
if (!bSameNameReplacesValue)
|
|
{
|
|
OutQueryParams.Emplace(MoveTemp(qp));
|
|
}
|
|
else
|
|
{
|
|
bool bFound = false;
|
|
for(int32 j=0; j<OutQueryParams.Num(); ++j)
|
|
{
|
|
if (OutQueryParams[j].Name.Equals(qp.Name))
|
|
{
|
|
OutQueryParams[j] = MoveTemp(qp);
|
|
bFound = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!bFound)
|
|
{
|
|
OutQueryParams.Emplace(MoveTemp(qp));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void FURL_RFC3986::GetQueryParams(TArray<FQueryParam>& OutQueryParams, bool bPerformUrlDecoding, bool bSameNameReplacesValue)
|
|
{
|
|
GetQueryParams(OutQueryParams, Query, bPerformUrlDecoding, bSameNameReplacesValue);
|
|
}
|
|
|
|
void FURL_RFC3986::SetQueryParams(const TArray<FQueryParam>& InQueryParams)
|
|
{
|
|
Query.Empty();
|
|
if (bIsData)
|
|
{
|
|
return;
|
|
}
|
|
|
|
int32 Len=0;
|
|
for(int32 i=0; i<InQueryParams.Num(); ++i)
|
|
{
|
|
Len += InQueryParams[i].Name.Len() + InQueryParams[i].Value.Len() + 2;
|
|
}
|
|
Query.Reserve(Len);
|
|
for(int32 i=0; i<InQueryParams.Num(); ++i)
|
|
{
|
|
Query += InQueryParams[i].Name + '=' + InQueryParams[i].Value;
|
|
if (i != InQueryParams.Num()-1)
|
|
{
|
|
Query.AppendChar(TCHAR('&'));
|
|
}
|
|
}
|
|
}
|
|
|
|
// Appends or prepends additional query parameters.
|
|
void FURL_RFC3986::AddOrUpdateQueryParams(const TArray<FQueryParam>& InQueryParams)
|
|
{
|
|
if (bIsData)
|
|
{
|
|
return;
|
|
}
|
|
TArray<FQueryParam> Current;
|
|
GetQueryParams(Current, false, true);
|
|
for(int32 i=0; i<InQueryParams.Num(); ++i)
|
|
{
|
|
bool bUpdated = false;
|
|
for(int32 j=0; j<Current.Num(); ++j)
|
|
{
|
|
if (Current[j].Name.Equals(InQueryParams[i].Name))
|
|
{
|
|
Current[j].Value = InQueryParams[i].Value;
|
|
bUpdated = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!bUpdated)
|
|
{
|
|
Current.Emplace(InQueryParams[i]);
|
|
}
|
|
}
|
|
SetQueryParams(Current);
|
|
}
|
|
|
|
void FURL_RFC3986::AddOrUpdateQueryParams(const FString& InQueryParameters)
|
|
{
|
|
if (InQueryParameters.Len() > 0)
|
|
{
|
|
FString _Query(InQueryParameters);
|
|
if (_Query[0] == TCHAR('?') || _Query[0] == TCHAR('&'))
|
|
{
|
|
_Query.RightChopInline(1, EAllowShrinking::No);
|
|
}
|
|
if (_Query.Len() && _Query[_Query.Len()-1] == TCHAR('&'))
|
|
{
|
|
_Query.LeftChopInline(1, EAllowShrinking::No);
|
|
}
|
|
TArray<FQueryParam> qp;
|
|
GetQueryParams(qp, _Query, false, true);
|
|
AddOrUpdateQueryParams(qp);
|
|
}
|
|
}
|
|
|
|
bool FURL_RFC3986::IsAbsolute() const
|
|
{
|
|
return Scheme.Len() > 0;
|
|
}
|
|
|
|
bool FURL_RFC3986::GetPathComponents(TArray<FString>& OutPathComponents) const
|
|
{
|
|
if (bIsData)
|
|
{
|
|
int32 CommaPos = INDEX_NONE;
|
|
if (Path.FindChar(TCHAR(','), CommaPos))
|
|
{
|
|
FString MediaType = Path.Mid(0, CommaPos);
|
|
MediaType.ParseIntoArray(OutPathComponents, TEXT(";"), true);
|
|
OutPathComponents.Emplace(Path.Mid(CommaPos + 1));
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
GetPathComponents(OutPathComponents, GetPath());
|
|
return true;
|
|
}
|
|
|
|
FString FURL_RFC3986::GetLastPathComponent() const
|
|
{
|
|
TArray<FString> Components;
|
|
GetPathComponents(Components, GetPath());
|
|
return Components.Num() ? Components.Last() : FString();
|
|
}
|
|
|
|
FString FURL_RFC3986::GetAuthority() const
|
|
{
|
|
FString Authority;
|
|
Authority.Reserve(Scheme.Len() + UserInfo.Len() + Host.Len() + Port.Len() + 32);
|
|
if (UserInfo.Len())
|
|
{
|
|
Authority += UserInfo;
|
|
Authority += URLComponents::At;
|
|
}
|
|
if (bIsFile)
|
|
{
|
|
Authority += Host;
|
|
}
|
|
else
|
|
{
|
|
// Is the host an IPv6 address that needs to be enclosed in square brackets?
|
|
int32 DummyPos = 0;
|
|
if (Host.FindChar(TCHAR(':'), DummyPos))
|
|
{
|
|
Authority += URLComponents::BracketOpen;
|
|
Authority += Host;
|
|
Authority += URLComponents::BracketClose;
|
|
}
|
|
else
|
|
{
|
|
Authority += Host;
|
|
}
|
|
// Need to append a port?
|
|
if (Port.Len())
|
|
{
|
|
Authority += URLComponents::Colon;
|
|
Authority += Port;
|
|
}
|
|
}
|
|
return Authority;
|
|
}
|
|
|
|
bool FURL_RFC3986::HasSameOriginAs(const FURL_RFC3986& Other)
|
|
{
|
|
/* RFC 6454:
|
|
5. Comparing Origins
|
|
|
|
Two origins are "the same" if, and only if, they are identical. In
|
|
particular:
|
|
|
|
o If the two origins are scheme/host/port triples, the two origins
|
|
are the same if, and only if, they have identical schemes, hosts,
|
|
and ports.
|
|
*/
|
|
return Scheme.Equals(Other.Scheme) && Host.Equals(Other.Host) && Port.Equals(Other.Port);
|
|
}
|
|
|
|
bool FURL_RFC3986::ParseAuthority(StringHelpers::FStringIterator& it)
|
|
{
|
|
UserInfo.Empty();
|
|
FString Component;
|
|
Component.Reserve(it.GetRemainingLength());
|
|
while(it && !IsPathSeparator(*it) && !IsQueryOrFragmentSeparator(*it))
|
|
{
|
|
// If there is a user-info delimiter?
|
|
if (*it == TCHAR('@'))
|
|
{
|
|
// Yes, what we have gathered so far is the user info. Set it and gather from scratch.
|
|
UserInfo = Component;
|
|
Component.Empty();
|
|
}
|
|
else
|
|
{
|
|
Component += *it;
|
|
}
|
|
++it;
|
|
}
|
|
StringHelpers::FStringIterator SubIt(Component);
|
|
return ParseHostAndPort(SubIt);
|
|
}
|
|
|
|
bool FURL_RFC3986::ParseHostAndPort(StringHelpers::FStringIterator& it)
|
|
{
|
|
if (!it)
|
|
{
|
|
return true;
|
|
}
|
|
Host.Empty();
|
|
Host.Reserve(it.GetRemainingLength());
|
|
if (bIsFile)
|
|
{
|
|
// For file:// scheme we have to consider Windows drive letters with a colon, eg. file://D:/
|
|
// We must not stop parsing at the colon since it will not indicate a port for the file scheme anyway.
|
|
while(it)
|
|
{
|
|
Host += *it++;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// IPv6 adress in [xxx:xxx:xxx] notation?
|
|
if (*it == TCHAR('['))
|
|
{
|
|
++it;
|
|
while(it && *it != TCHAR(']'))
|
|
{
|
|
Host += *it++;
|
|
}
|
|
if (!it)
|
|
{
|
|
// Need to have at least the closing ']'
|
|
return false;
|
|
}
|
|
++it;
|
|
}
|
|
else
|
|
{
|
|
while(it && !IsColonSeparator(*it))
|
|
{
|
|
Host += *it++;
|
|
}
|
|
}
|
|
if (it && IsColonSeparator(*it))
|
|
{
|
|
++it;
|
|
Port.Empty();
|
|
Port.Reserve(16);
|
|
while(it)
|
|
{
|
|
Port += *it++;
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool FURL_RFC3986::ParsePathAndQueryFragment(StringHelpers::FStringIterator& it)
|
|
{
|
|
if (it)
|
|
{
|
|
if (!IsQueryOrFragmentSeparator(*it))
|
|
{
|
|
if (!ParsePath(it))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
if (it && IsQuerySeparator(*it))
|
|
{
|
|
if (!ParseQuery(++it))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
if (it && IsFragmentSeparator(*it))
|
|
{
|
|
return ParseFragment(++it);
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool FURL_RFC3986::ParsePath(StringHelpers::FStringIterator& it)
|
|
{
|
|
Path.Empty();
|
|
Path.Reserve(it.GetRemainingLength());
|
|
while(it && !IsQueryOrFragmentSeparator(*it))
|
|
{
|
|
Path += *it++;
|
|
}
|
|
// URL decode the path internally.
|
|
FString Decoded;
|
|
if (UrlDecode(Decoded, Path))
|
|
{
|
|
Path = Decoded;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool FURL_RFC3986::ParseQuery(StringHelpers::FStringIterator& it)
|
|
{
|
|
Query.Empty();
|
|
Query.Reserve(it.GetRemainingLength());
|
|
// Query is all up to the end or the start of the fragment.
|
|
while(it && !IsFragmentSeparator(*it))
|
|
{
|
|
Query += *it++;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool FURL_RFC3986::ParseFragment(StringHelpers::FStringIterator& it)
|
|
{
|
|
// Fragment being the last part of the URL extends all the way to the end.
|
|
Fragment = it.GetRemainder();
|
|
it.SetToEnd();
|
|
// URL decode the fragment internally.
|
|
FString Decoded;
|
|
if (UrlDecode(Decoded, Fragment))
|
|
{
|
|
Fragment = Decoded;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool FURL_RFC3986::UrlDecode(FString& OutResult, const FString& InUrlToDecode)
|
|
{
|
|
TArray<uint8> AsciiString;
|
|
AsciiString.Reserve(InUrlToDecode.Len() + 1);
|
|
auto IsHexChar = [](ANSICHAR In) ->bool
|
|
{
|
|
return (In >= ANSICHAR('0') && In <= ANSICHAR('9')) || (In >= ANSICHAR('a') && In <= ANSICHAR('f')) || (In >= ANSICHAR('A') && In <= ANSICHAR('F'));
|
|
};
|
|
auto HexVal = [](ANSICHAR In) -> uint8
|
|
{
|
|
if (In >= ANSICHAR('0') && In <= ANSICHAR('9'))
|
|
{
|
|
return In - ANSICHAR('0');
|
|
}
|
|
else if (In >= ANSICHAR('a') && In <= ANSICHAR('f'))
|
|
{
|
|
return In - ANSICHAR('a') + 10;
|
|
}
|
|
else
|
|
{
|
|
return In - ANSICHAR('A') + 10;
|
|
}
|
|
};
|
|
// The URL to be decoded should strictly speaking consist only of ASCII characters, but depending on how this is used
|
|
// it may as well be a UTF8 encoded string. We decode UTF8 to ASCII, which is not doing anything if the URL is already
|
|
// only composed of ASCII characters, then we unescape it and finally convert it back to UTF8, which we need to do
|
|
// to get any UTF8 characters back that are represented in the URL through escaping.
|
|
FTCHARToUTF8 UTF8String(*InUrlToDecode);
|
|
for(const ANSICHAR* InUTF8Bytes=UTF8String.Get(); *InUTF8Bytes; ++InUTF8Bytes)
|
|
{
|
|
// Escaped?
|
|
if (*InUTF8Bytes == ANSICHAR('%') && InUTF8Bytes[1] && IsHexChar(InUTF8Bytes[1]) && InUTF8Bytes[2] && IsHexChar(InUTF8Bytes[2]))
|
|
{
|
|
uint8 hi = HexVal(*(++InUTF8Bytes));
|
|
uint8 lo = HexVal(*(++InUTF8Bytes));
|
|
AsciiString.Add((uint8)(hi * 16 + lo));
|
|
}
|
|
else
|
|
{
|
|
AsciiString.Add((uint8)*InUTF8Bytes);
|
|
}
|
|
}
|
|
AsciiString.Add((uint8)0);
|
|
// Convert to UTF8 to get any escaped sequences back to what they were.
|
|
OutResult = UTF8_TO_TCHAR((const ANSICHAR*)AsciiString.GetData());
|
|
return true;
|
|
}
|
|
|
|
bool FURL_RFC3986::UrlEncode(FString& OutResult, const FString& InUrlToEncode, const FString& InCharsToKeep)
|
|
{
|
|
auto AnsiString = StringCast<ANSICHAR>(*InCharsToKeep);
|
|
const ANSICHAR* InKeepCharsASCII = AnsiString.Get();
|
|
auto KeepUnchanged = [InKeepCharsASCII](ANSICHAR In) -> bool
|
|
{
|
|
for(const ANSICHAR* Res=InKeepCharsASCII; *Res; ++Res)
|
|
{
|
|
if (*Res == In)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
};
|
|
auto IsHexChar = [](ANSICHAR In) ->bool
|
|
{
|
|
return (In >= ANSICHAR('0') && In <= ANSICHAR('9')) || (In >= ANSICHAR('a') && In <= ANSICHAR('f')) || (In >= ANSICHAR('A') && In <= ANSICHAR('F'));
|
|
};
|
|
|
|
OutResult.Reserve(InUrlToEncode.Len() * 3); // assume we need to encode every character
|
|
FTCHARToUTF8 UTF8String(*InUrlToEncode);
|
|
for(const ANSICHAR* InUTF8Bytes=UTF8String.Get(); *InUTF8Bytes; ++InUTF8Bytes)
|
|
{
|
|
uint8 c = (uint8)(*InUTF8Bytes);
|
|
// Unreserved character?
|
|
if ((c >= uint8('a') && c <= uint8('z')) || (c >= uint8('0') && c <= uint8('9')) || (c >= uint8('A') && c <= uint8('Z')) || c == uint8('-') || c == uint8('_') || c == uint8('.') || c == uint8('~'))
|
|
{
|
|
OutResult += TCHAR(c);
|
|
}
|
|
// Already escaped?
|
|
else if (c == uint8('%') && InUTF8Bytes[1] && IsHexChar(InUTF8Bytes[1]) && InUTF8Bytes[2] && IsHexChar(InUTF8Bytes[2]))
|
|
{
|
|
OutResult += TCHAR(c);
|
|
OutResult += TCHAR(*(++InUTF8Bytes));
|
|
OutResult += TCHAR(*(++InUTF8Bytes));
|
|
}
|
|
else if (KeepUnchanged(*InUTF8Bytes))
|
|
{
|
|
OutResult += TCHAR(c);
|
|
}
|
|
else
|
|
{
|
|
OutResult += FString::Printf(TEXT("%%%02X"), c);
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
FString FURL_RFC3986::GetStandardPortForScheme(const FString& InScheme, bool bIgnoreCase)
|
|
{
|
|
if (InScheme.Equals(URLComponents::SchemeHttp, bIgnoreCase ? ESearchCase::IgnoreCase : ESearchCase::CaseSensitive))
|
|
{
|
|
return URLComponents::Port80;
|
|
}
|
|
else if (InScheme.Equals(URLComponents::SchemeHttps, bIgnoreCase ? ESearchCase::IgnoreCase : ESearchCase::CaseSensitive))
|
|
{
|
|
return URLComponents::Port443;
|
|
}
|
|
return FString();
|
|
}
|
|
|
|
|
|
FString FURL_RFC3986::GetUrlEncodeSubDelimsChars()
|
|
{
|
|
return FString(TEXT("!$&'()*+,;="));
|
|
}
|
|
|
|
void FURL_RFC3986::GetPathComponents(TArray<FString>& OutPathComponents, const FString& InPath)
|
|
{
|
|
TArray<FString> Components;
|
|
// Split on '/', ignoring resulting empty parts (eg. "a//b" gives ["a", "b"] only instead of ["a", "", "b"] !!
|
|
InPath.ParseIntoArray(Components, *URLComponents::Slash, true);
|
|
OutPathComponents.Append(Components);
|
|
}
|
|
|
|
FURL_RFC3986& FURL_RFC3986::ResolveWith(const FString& InChildURL)
|
|
{
|
|
FURL_RFC3986 Other;
|
|
if (Other.Parse(InChildURL))
|
|
{
|
|
ResolveWith(Other);
|
|
}
|
|
return *this;
|
|
}
|
|
|
|
FURL_RFC3986& FURL_RFC3986::ResolveAgainst(const FString& InParentURL)
|
|
{
|
|
FURL_RFC3986 Parent;
|
|
if (Parent.Parse(InParentURL))
|
|
{
|
|
Parent.ResolveWith(*this);
|
|
Swap(Parent);
|
|
}
|
|
return *this;
|
|
}
|
|
|
|
/**
|
|
* Resolve URL as per RFC 3986 section 5.2
|
|
*/
|
|
void FURL_RFC3986::ResolveWith(const FURL_RFC3986& Other)
|
|
{
|
|
// data urls cannot be resolved, but we handle them here for consistency.
|
|
if (Other.bIsData)
|
|
{
|
|
Empty();
|
|
Scheme = Other.Scheme;
|
|
Path = Other.Path;
|
|
}
|
|
else if (bIsData && Other.Scheme.IsEmpty())
|
|
{
|
|
// Nothing to do.
|
|
return;
|
|
}
|
|
else if (!Other.Scheme.IsEmpty())
|
|
{
|
|
Scheme = Other.Scheme;
|
|
UserInfo = Other.UserInfo;
|
|
Host = Other.Host;
|
|
Port = Other.Port;
|
|
Path = Other.Path;
|
|
Query = Other.Query;
|
|
RemoveDotSegments();
|
|
}
|
|
else
|
|
{
|
|
if (!Other.Host.IsEmpty())
|
|
{
|
|
UserInfo = Other.UserInfo;
|
|
Host = Other.Host;
|
|
Port = Other.Port;
|
|
Path = Other.Path;
|
|
Query = Other.Query;
|
|
RemoveDotSegments();
|
|
}
|
|
else
|
|
{
|
|
if (Other.Path.IsEmpty())
|
|
{
|
|
if (!Other.Query.IsEmpty())
|
|
{
|
|
Query = Other.Query;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (IsPathSeparator(Other.Path[0]))
|
|
{
|
|
Path = Other.Path;
|
|
}
|
|
else
|
|
{
|
|
MergePath(Other.Path);
|
|
}
|
|
RemoveDotSegments();
|
|
Query = Other.Query;
|
|
}
|
|
}
|
|
}
|
|
Fragment = Other.Fragment;
|
|
bIsFile = Scheme.Equals(URLComponents::SchemeFile);
|
|
bIsData = Scheme.Equals(URLComponents::SchemeData);
|
|
}
|
|
|
|
void FURL_RFC3986::BuildPathFromSegments(const TArray<FString>& Components, bool bAddLeadingSlash, bool bAddTrailingSlash)
|
|
{
|
|
Path.Empty();
|
|
int32 ComponentLen = 0;
|
|
for(int32 i=0, iMax=Components.Num(); i<iMax; ++i)
|
|
{
|
|
ComponentLen += Components[i].Len() + 1;
|
|
}
|
|
Path.Reserve(ComponentLen + 8);
|
|
int32 DummyPos = 0;
|
|
for(int32 i=0, iMax=Components.Num(); i<iMax; ++i)
|
|
{
|
|
if (i == 0)
|
|
{
|
|
if (bAddLeadingSlash)
|
|
{
|
|
Path = URLComponents::Slash;
|
|
}
|
|
/*
|
|
As per RFC 3986 Section 4.2 Relative Reference:
|
|
A path segment that contains a colon character (e.g., "this:that")
|
|
cannot be used as the first segment of a relative-path reference, as
|
|
it would be mistaken for a scheme name. Such a segment must be
|
|
preceded by a dot-segment (e.g., "./this:that") to make a relative-
|
|
path reference.
|
|
*/
|
|
else if (Scheme.IsEmpty() && Components[0].FindChar(TCHAR(':'), DummyPos))
|
|
{
|
|
Path = URLComponents::DotSlash;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Path += TCHAR('/');
|
|
}
|
|
Path += Components[i];
|
|
}
|
|
if (bAddTrailingSlash)
|
|
{
|
|
Path += TCHAR('/');
|
|
}
|
|
}
|
|
|
|
void FURL_RFC3986::MergePath(const FString& InPathToMerge)
|
|
{
|
|
// RFC 3986 Section 5.2.3. Merge Paths
|
|
if (Host.Len() && Path.IsEmpty())
|
|
{
|
|
Path.Reserve(InPathToMerge.Len() + 1);
|
|
Path = URLComponents::Slash;
|
|
Path += InPathToMerge;
|
|
}
|
|
else
|
|
{
|
|
int32 LastSlashPos = INDEX_NONE;
|
|
// Is there a '/' somewhere in the current path that's not at the end?
|
|
if (Path.FindLastChar(TCHAR('/'), LastSlashPos))
|
|
{
|
|
if (LastSlashPos != Path.Len()-1)
|
|
{
|
|
Path.LeftInline(LastSlashPos+1);
|
|
}
|
|
Path += InPathToMerge;
|
|
}
|
|
else
|
|
{
|
|
Path = InPathToMerge;
|
|
}
|
|
}
|
|
}
|
|
|
|
void FURL_RFC3986::RemoveDotSegments()
|
|
{
|
|
if (Path.Len())
|
|
{
|
|
TArray<FString> NewSegments;
|
|
// Remember the current path starting or ending in a slash.
|
|
bool bHasLeadingSlash = IsPathSeparator(Path[0]);
|
|
bool bHasTrailingSlash = Path.EndsWith(URLComponents::Slash);
|
|
// Split the path into its segments
|
|
TArray<FString> Segments;
|
|
GetPathComponents(Segments);
|
|
for(int32 i=0; i<Segments.Num(); ++i)
|
|
{
|
|
// For every ".." go up one level.
|
|
if (Segments[i].Equals(URLComponents::DotDot))
|
|
{
|
|
if (NewSegments.Num())
|
|
{
|
|
NewSegments.Pop();
|
|
}
|
|
}
|
|
// Add non- "." segments.
|
|
else if (!Segments[i].Equals(URLComponents::Dot))
|
|
{
|
|
NewSegments.Add(Segments[i]);
|
|
}
|
|
}
|
|
BuildPathFromSegments(NewSegments, bHasLeadingSlash, bHasTrailingSlash);
|
|
}
|
|
}
|
|
|
|
|
|
|
|
} // namespace Electra
|