8 Commits
Author SHA1 Message Date
tesses50 d99001d6eb Fix NX, add constant time compare
Build and Deploy on Tag / 🔨 Build for everything else (git.tesses.org/tesses50/linux-riscv64:latest, riscv64-linux-gnu, riscv64, asrock) (push) Successful in 54s
Build and Deploy on Tag / 🔨 Build for everything else (git.tesses.org/tesses50/linux-x86:latest, i386-linux-gnu, 386, asrock) (push) Successful in 54s
Build and Deploy on Tag / 🔨 Build for everything else (git.tesses.org/tesses50/linux-arm64:latest, aarch64-linux-gnu, arm64, asrock) (push) Successful in 55s
Build and Deploy on Tag / 🔨 Build win32 and update the tap 🍺 (push) Successful in 1m11s
Build and Deploy on Tag / 🔨 Build for PowerPC (push) Successful in 2m10s
Build and Deploy on Tag / 🔨 Build for everything else (git.tesses.org/tesses50/linux-x64:latest, x86_64-linux-gnu, amd64, msi-walmart) (push) Successful in 2m15s
Build and Deploy on Tag / 🔨 Build for everything else (git.tesses.org/tesses50/linux-arm:latest, arm-linux-gnueabihf, arm7, msi-walmart) (push) Successful in 2m19s
2026-10-25 17:48:20 -05:00
tesses50 fae86d2eb5 Fix NX, add constant time compare, add TF_Span
Build and Deploy on Tag / 🔨 Build for everything else (git.tesses.org/tesses50/linux-x64:latest, x86_64-linux-gnu, amd64, msi-walmart) (push) Successful in 1m44s
Build and Deploy on Tag / 🔨 Build for everything else (git.tesses.org/tesses50/linux-arm:latest, arm-linux-gnueabihf, arm7, msi-walmart) (push) Successful in 2m11s
Build and Deploy on Tag / 🔨 Build for everything else (git.tesses.org/tesses50/linux-x86:latest, i386-linux-gnu, 386, asrock) (push) Successful in 2m33s
Build and Deploy on Tag / 🔨 Build for everything else (git.tesses.org/tesses50/linux-riscv64:latest, riscv64-linux-gnu, riscv64, asrock) (push) Successful in 2m34s
Build and Deploy on Tag / 🔨 Build for everything else (git.tesses.org/tesses50/linux-arm64:latest, aarch64-linux-gnu, arm64, asrock) (push) Successful in 2m38s
Build and Deploy on Tag / 🔨 Build for PowerPC (push) Successful in 2m57s
Build and Deploy on Tag / 🔨 Build win32 and update the tap 🍺 (push) Successful in 3m13s
2026-10-25 16:57:42 -05:00
tesses50 b964ea04df Add haiku 2026-10-17 05:13:42 -05:00
tesses50 e4ca34aa59 Make html parser use case insensitive unordered_map for attributes
Build and Deploy on Tag / 🔨 Build for PowerPC (push) Successful in 49s
Build and Deploy on Tag / 🔨 Build for everything else (git.tesses.org/tesses50/linux-riscv64:latest, riscv64-linux-gnu, riscv64, asrock) (push) Successful in 1m5s
Build and Deploy on Tag / 🔨 Build for everything else (git.tesses.org/tesses50/linux-x86:latest, i386-linux-gnu, 386, asrock) (push) Successful in 1m9s
Build and Deploy on Tag / 🔨 Build for everything else (git.tesses.org/tesses50/linux-arm:latest, arm-linux-gnueabihf, arm7, fw13-ubuntu-latest) (push) Successful in 1m10s
Build and Deploy on Tag / 🔨 Build for everything else (git.tesses.org/tesses50/linux-x64:latest, x86_64-linux-gnu, amd64, msi-walmart) (push) Successful in 1m34s
Build and Deploy on Tag / 🔨 Build for everything else (git.tesses.org/tesses50/linux-arm64:latest, aarch64-linux-gnu, arm64, msi-walmart) (push) Successful in 1m39s
Build and Deploy on Tag / 🔨 Build win32 and update the tap 🍺 (push) Successful in 1m56s
2026-09-25 16:35:03 -05:00
tesses50 9ddbfefa41 Make html parser use case insensitive unordered_map for attributes
Build and Deploy on Tag / 🔨 Build for everything else (git.tesses.org/tesses50/linux-riscv64:latest, riscv64-linux-gnu, riscv64, asrock) (push) Failing after 45s
Build and Deploy on Tag / 🔨 Build for everything else (git.tesses.org/tesses50/linux-x86:latest, i386-linux-gnu, 386, asrock) (push) Failing after 46s
Build and Deploy on Tag / 🔨 Build for PowerPC (push) Failing after 47s
Build and Deploy on Tag / 🔨 Build for everything else (git.tesses.org/tesses50/linux-arm:latest, arm-linux-gnueabihf, arm7, fw13-ubuntu-latest) (push) Failing after 50s
Build and Deploy on Tag / 🔨 Build for everything else (git.tesses.org/tesses50/linux-x64:latest, x86_64-linux-gnu, amd64, msi-walmart) (push) Failing after 1m8s
Build and Deploy on Tag / 🔨 Build win32 and update the tap 🍺 (push) Failing after 1m11s
Build and Deploy on Tag / 🔨 Build for everything else (git.tesses.org/tesses50/linux-arm64:latest, aarch64-linux-gnu, arm64, msi-walmart) (push) Failing after 1m17s
2026-09-25 16:28:13 -05:00
tesses50 15f08f474f Make html parser use case insensitive unordered_map for attributes
Build and Deploy on Tag / 🔨 Build for everything else (git.tesses.org/tesses50/linux-riscv64:latest, riscv64-linux-gnu, riscv64, asrock) (push) Failing after 30s
Build and Deploy on Tag / 🔨 Build for everything else (git.tesses.org/tesses50/linux-x86:latest, i386-linux-gnu, 386, asrock) (push) Failing after 33s
Build and Deploy on Tag / 🔨 Build for PowerPC (push) Failing after 35s
Build and Deploy on Tag / 🔨 Build for everything else (git.tesses.org/tesses50/linux-arm:latest, arm-linux-gnueabihf, arm7, fw13-ubuntu-latest) (push) Failing after 38s
Build and Deploy on Tag / 🔨 Build win32 and update the tap 🍺 (push) Failing after 43s
Build and Deploy on Tag / 🔨 Build for everything else (git.tesses.org/tesses50/linux-x64:latest, x86_64-linux-gnu, amd64, msi-walmart) (push) Failing after 43s
Build and Deploy on Tag / 🔨 Build for everything else (git.tesses.org/tesses50/linux-arm64:latest, aarch64-linux-gnu, arm64, msi-walmart) (push) Failing after 46s
2026-09-25 16:17:11 -05:00
tesses50 95442f8acc Fixed a stoll bug that was there since day one, html parser, fix pkgconfig
Build and Deploy on Tag / 🔨 Build for PowerPC (push) Successful in 49s
Build and Deploy on Tag / 🔨 Build for everything else (git.tesses.org/tesses50/linux-riscv64:latest, riscv64-linux-gnu, riscv64, asrock) (push) Successful in 1m7s
Build and Deploy on Tag / 🔨 Build for everything else (git.tesses.org/tesses50/linux-x86:latest, i386-linux-gnu, 386, asrock) (push) Successful in 1m7s
Build and Deploy on Tag / 🔨 Build for everything else (git.tesses.org/tesses50/linux-arm:latest, arm-linux-gnueabihf, arm7, fw13-ubuntu-latest) (push) Successful in 1m9s
Build and Deploy on Tag / 🔨 Build for everything else (git.tesses.org/tesses50/linux-x64:latest, x86_64-linux-gnu, amd64, msi-walmart) (push) Successful in 1m29s
Build and Deploy on Tag / 🔨 Build for everything else (git.tesses.org/tesses50/linux-arm64:latest, aarch64-linux-gnu, arm64, msi-walmart) (push) Successful in 1m40s
Build and Deploy on Tag / 🔨 Build win32 and update the tap 🍺 (push) Failing after 1m57s
2026-09-22 11:39:52 -05:00
tesses50 dbeae862c5 Fixed a stoll bug that was there since day one, html parser, fix pkgconfig
Build and Deploy on Tag / 🔨 Build for everything else (git.tesses.org/tesses50/linux-arm:latest, arm-linux-gnueabihf, arm7, fw13-ubuntu-latest) (push) Failing after 38s
Build and Deploy on Tag / 🔨 Build for everything else (git.tesses.org/tesses50/linux-x64:latest, x86_64-linux-gnu, amd64, msi-walmart) (push) Failing after 41s
Build and Deploy on Tag / 🔨 Build for PowerPC (push) Failing after 43s
Build and Deploy on Tag / 🔨 Build for everything else (git.tesses.org/tesses50/linux-arm64:latest, aarch64-linux-gnu, arm64, msi-walmart) (push) Failing after 42s
Build and Deploy on Tag / 🔨 Build win32 and update the tap 🍺 (push) Failing after 45s
Build and Deploy on Tag / 🔨 Build for everything else (git.tesses.org/tesses50/linux-x86:latest, i386-linux-gnu, 386, asrock) (push) Failing after 47s
Build and Deploy on Tag / 🔨 Build for everything else (git.tesses.org/tesses50/linux-riscv64:latest, riscv64-linux-gnu, riscv64, asrock) (push) Failing after 1m0s
2026-09-22 11:09:50 -05:00
29 changed files with 1186 additions and 128 deletions
+10 -4
View File
@@ -11,7 +11,7 @@ env:
jobs: jobs:
build-ppc: build-ppc:
name: "🔨 Build for PowerPC" name: "🔨 Build for PowerPC"
runs-on: fw13-ubuntu-latest runs-on: msi-walmart
container: container:
image: git.tesses.org/tesses50/linux-ppc:latest image: git.tesses.org/tesses50/linux-ppc:latest
steps: steps:
@@ -26,10 +26,11 @@ jobs:
artifacts/** artifacts/**
update-tap-and-build-win32: update-tap-and-build-win32:
name: "🔨 Build win32 and update the tap 🍺" name: "🔨 Build win32 and update the tap 🍺"
runs-on: fw13-ubuntu-latest runs-on: asrock
container: git.tesses.org/tesses50/global-containers-mingw:latest container: git.tesses.org/tesses50/global-containers-mingw:latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
url: https://git.tesses.org/tesses50/tessesframework.git
- run: | - run: |
mkdir artifacts mkdir artifacts
env -C Packaging/WinToolsIntel bash build.sh env -C Packaging/WinToolsIntel bash build.sh
@@ -58,31 +59,36 @@ jobs:
git push git push
build-packages: build-packages:
name: "🔨 Build for everything else" name: "🔨 Build for everything else"
runs-on: fw13-ubuntu-latest
strategy: strategy:
fail-fast: true fail-fast: true
matrix: matrix:
include: include:
- container: "git.tesses.org/tesses50/linux-x64:latest" - container: "git.tesses.org/tesses50/linux-x64:latest"
puter: "msi-walmart"
nfpmName: "amd64" nfpmName: "amd64"
debianName: "x86_64-linux-gnu" debianName: "x86_64-linux-gnu"
- container: "git.tesses.org/tesses50/linux-x86:latest" - container: "git.tesses.org/tesses50/linux-x86:latest"
puter: "asrock"
nfpmName: "386" nfpmName: "386"
debianName: "i386-linux-gnu" debianName: "i386-linux-gnu"
- container: "git.tesses.org/tesses50/linux-arm:latest" - container: "git.tesses.org/tesses50/linux-arm:latest"
puter: "msi-walmart"
nfpmName: "arm7" nfpmName: "arm7"
debianName: "arm-linux-gnueabihf" debianName: "arm-linux-gnueabihf"
- container: "git.tesses.org/tesses50/linux-arm64:latest" - container: "git.tesses.org/tesses50/linux-arm64:latest"
puter: "asrock"
nfpmName: "arm64" nfpmName: "arm64"
debianName: "aarch64-linux-gnu" debianName: "aarch64-linux-gnu"
- container: "git.tesses.org/tesses50/linux-riscv64:latest" - container: "git.tesses.org/tesses50/linux-riscv64:latest"
nfpmName: "riscv64" nfpmName: "riscv64"
debianName: "riscv64-linux-gnu" debianName: "riscv64-linux-gnu"
puter: "asrock"
runs-on: ${{ matrix.puter }}
container: container:
image: ${{ matrix.container }} image: ${{ matrix.container }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
url: https://git.tesses.org/tesses50/tessesframework.git
- run: bash build-for-nfpm.sh ${{ matrix.nfpmName }} ${{ matrix.debianName }} - run: bash build-for-nfpm.sh ${{ matrix.nfpmName }} ${{ matrix.debianName }}
- uses: akkuman/gitea-release-action@v1 - uses: akkuman/gitea-release-action@v1
env: env:
-1
View File
@@ -7,7 +7,6 @@ set(CMAKE_CXX_STANDARD 17)
include(GNUInstallDirs) include(GNUInstallDirs)
include(CheckLibraryExists) include(CheckLibraryExists)
include(FetchContent) include(FetchContent)
include(cmake/sources.cmake)
include(cmake/options.cmake) include(cmake/options.cmake)
set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON) set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON)
+2
View File
@@ -22,6 +22,7 @@ Tesses Framework
- EventLoop - EventLoop
- Basic crytography using mbedtls - Basic crytography using mbedtls
- a LCG Random number generator (do not use for crypto, use it for non security things like number guessing games and stuff, where rand would be ok) - a LCG Random number generator (do not use for crypto, use it for non security things like number guessing games and stuff, where rand would be ok)
- Simple HTML parser
## Supported Platforms ## Supported Platforms
- Linux - Linux
@@ -29,6 +30,7 @@ Tesses Framework
- MacOS - MacOS
- FreeBSD (including GhostBSD) - FreeBSD (including GhostBSD)
- NetBSD - NetBSD
- Haiku (only static builds work as of right now)
- Wii (using libogc2, may be broken) - Wii (using libogc2, may be broken)
- Switch (using libnx, may be broken) - Switch (using libnx, may be broken)
- Android (kinda, uses legacy and hardcoded /sdcard api unless you use TF_AllowPortable(std::string)) - Android (kinda, uses legacy and hardcoded /sdcard api unless you use TF_AllowPortable(std::string))
+83
View File
@@ -0,0 +1,83 @@
#include "TessesFramework/TessesFramework.hpp"
using namespace Tesses::Framework::Streams;
using namespace Tesses::Framework::Serialization;
using namespace Tesses::Framework::TextStreams;
Json::JToken Html2JToken(std::shared_ptr<Html::HtmlNode> node) {
if (node->isText) {
return node->text_or_tag;
} else {
Json::JObject obj;
obj.SetValue("Tag", node->text_or_tag);
Json::JObject attrs;
for (auto &item : node->attributes) {
if (item.second) {
attrs.SetValue(item.first, *item.second);
} else {
attrs.SetValue(item.first, nullptr);
}
}
obj.SetValue("Attributes", attrs);
Json::JArray childern;
for (auto &item : node->children)
childern.Add(Html2JToken(item));
obj.SetValue("Children", childern);
return obj;
}
}
std::shared_ptr<FileStream> OpenWrite(std::string dest) {
if (dest == "-") {
return std::make_shared<FileStream>(stdout, false, "w");
} else {
auto strm = std::make_shared<FileStream>(dest, "w");
if (!strm->CanWrite()) {
return nullptr;
}
return strm;
}
}
std::shared_ptr<FileStream> OpenRead(std::string src) {
if (src == "-") {
return std::make_shared<FileStream>(stdin, false, "r");
} else {
auto strm = std::make_shared<FileStream>(src, "r");
if (!strm->CanRead()) {
return nullptr;
}
return strm;
}
}
int main(int argc, char **argv) {
if (argc < 3) {
std::cout << "USAGE: " << argv[0] << " SRC DEST" << std::endl;
std::cout << "SRC: html file or - for stdin to convert to json doc"
<< std::endl;
std::cout << "DEST: json file or - for stdout" << std::endl;
return 0;
}
auto src = OpenRead(argv[1]);
auto dest = OpenWrite(argv[2]);
if (src == nullptr) {
std::cerr << "ERROR: Input could not be read" << std::endl;
return 1;
}
if (dest == nullptr) {
std::cerr << "ERROR: Output could not be read" << std::endl;
return 1;
}
StreamWriter writer(dest);
auto html = Html::Parse(std::make_shared<StreamReader>(src));
writer.WriteLine(Json::Json::Encode(Html2JToken(html)));
return 0;
}
+77
View File
@@ -0,0 +1,77 @@
#include "TessesFramework/TessesFramework.hpp"
using namespace Tesses::Framework::Streams;
using namespace Tesses::Framework::Serialization;
using namespace Tesses::Framework::TextStreams;
std::shared_ptr<Stream> OpenRead(std::string src) {
if (src == "-") {
return std::make_shared<FileStream>(stdin, false, "r");
} else if (src.find("http://") == 0 || src.find("https://") == 0) {
Tesses::Framework::Http::HttpRequest req;
req.url = src;
req.method = "GET";
Tesses::Framework::Http::HttpResponse resp(req);
if ((int)resp.statusCode < 200 || (int)resp.statusCode > 299) {
throw std::runtime_error(
"Response does not indicate success: " +
std::to_string((int)resp.statusCode) + " " +
Tesses::Framework::Http::HttpUtils::StatusCodeString(
resp.statusCode) +
".");
}
return resp.ReadAsStream();
} else {
auto strm = std::make_shared<FileStream>(src, "r");
if (!strm->CanRead()) {
return nullptr;
}
return strm;
}
}
int main(int argc, char **argv) {
if (argc < 2) {
std::cout << "USAGE: " << argv[0] << " SRC" << std::endl;
std::cout << "SRC: html file or url or - for stdin" << std::endl;
return 0;
}
auto src = OpenRead(argv[1]);
if (src == nullptr) {
std::cerr << "ERROR: Input could not be read" << std::endl;
return 1;
}
auto html = Html::Parse(std::make_shared<StreamReader>(src));
std::vector<std::shared_ptr<const Html::HtmlNode>> titleTags;
html->FindNodes("title", titleTags);
for (auto &title : titleTags) {
Tesses::Framework::Console::WriteView("Title: ");
Tesses::Framework::Console::WriteLine(title->InnerText());
break;
}
std::vector<std::shared_ptr<const Html::HtmlNode>> metaTags;
html->FindNodes("meta", metaTags);
for (auto &meta : metaTags) {
auto prop_off = meta->attributes.find("property");
auto content_off = meta->attributes.find("content");
if (!(prop_off != meta->attributes.end() &&
content_off != meta->attributes.end()))
continue;
auto &prop = prop_off->second;
auto &content = content_off->second;
if (prop && content && prop->find("og:") == 0) {
Tesses::Framework::Console::WriteView(prop.value());
Tesses::Framework::Console::WriteView(": ");
Tesses::Framework::Console::WriteLineView(content.value());
}
}
return 0;
}
+4 -3
View File
@@ -39,7 +39,7 @@ int main(int argc, char **argv) {
Args args(argc, argv); Args args(argc, argv);
if (args.positional.size() < 2) if (args.positional.size() < 2)
usage(args); usage(args);
int64_t num;
for (auto &opt : args.options) { for (auto &opt : args.options) {
if (opt.first == "tracker") if (opt.first == "tracker")
trackers.push_back(opt.second); trackers.push_back(opt.second);
@@ -49,8 +49,9 @@ int main(int argc, char **argv) {
comment = opt.second; comment = opt.second;
else if (opt.first == "created_by") else if (opt.first == "created_by")
created_by = opt.second; created_by = opt.second;
else if (opt.first == "piece_length") else if (opt.first == "piece_length" &&
pieceLength = std::stoll(opt.second); Serialization::BitConverter::TryParseSigned(opt.second, num))
pieceLength = num;
} }
for (auto &flag : args.flags) { for (auto &flag : args.flags) {
+9
View File
@@ -1,5 +1,14 @@
# Changelog # Changelog
## 0.0.10
Fix NX, add constant time compare
## 0.0.9
Make html parser use case insensitive unordered_map for attributes
## 0.0.8
Fixed a stoll bug that was there since day one, html parser, fix pkgconfig
## 0.0.7 ## 0.0.7
Fix license compliance and fix things up, add reverse proxy, fix some security flaws with HttpUtils Fix license compliance and fix things up, add reverse proxy, fix some security flaws with HttpUtils
+9
View File
@@ -73,4 +73,13 @@ add_executable(tsimplereverseproxy apps/tsimplereverseproxy.cpp)
target_link_libraries(tsimplereverseproxy PUBLIC tessesframework) target_link_libraries(tsimplereverseproxy PUBLIC tessesframework)
install(TARGETS tsimplereverseproxy DESTINATION "${CMAKE_INSTALL_BINDIR}") install(TARGETS tsimplereverseproxy DESTINATION "${CMAKE_INSTALL_BINDIR}")
add_executable(thtml2json apps/thtml2json.cpp)
target_link_libraries(thtml2json PUBLIC tessesframework)
install(TARGETS thtml2json DESTINATION "${CMAKE_INSTALL_BINDIR}")
add_executable(topengraph apps/topengraph.cpp)
target_link_libraries(topengraph PUBLIC tessesframework)
install(TARGETS topengraph DESTINATION "${CMAKE_INSTALL_BINDIR}")
endif() endif()
+7 -9
View File
@@ -11,28 +11,26 @@ endif()
FetchContent_Declare( FetchContent_Declare(
mbedtls mbedtls
URL https://downloads.tesses.net/cache/libraries/source/mbedtls-3.6.4.tar.bz2 URL https://downloads.tesses.net/cache/libraries/source/mbedtls-3.6.7.tar.bz2
) )
if(NOT mbedtls_POPULATED) if(NOT mbedtls_POPULATED)
set(BUILD_SHARED_LIBS OFF CACHE BOOL "Build static" FORCE) set(BUILD_SHARED_LIBS OFF CACHE BOOL "Build static" FORCE)
if(NOT TESSESFRAMEWORK_STATIC) if(NOT TESSESFRAMEWORK_STATIC)
set(CMAKE_POSITION_INDEPENDENT_CODE ON CACHE BOOL "Build PIC" FORCE) set(CMAKE_POSITION_INDEPENDENT_CODE ON CACHE BOOL "Build PIC" FORCE)
set(CMAKE_SKIP_INSTALL_RULES ON)
set(DISABLE_PACKAGE_CONFIG_AND_INSTALL ON CACHE BOOL "" FORCE)
endif() endif()
FetchContent_Populate(mbedtls) FetchContent_Populate(mbedtls)
if(NOT TESSESFRAMEWORK_STATIC)
install(CODE "set(CMAKE_INSTALL_LOCAL_ONLY TRUE)" ALL_COMPONENTS)
endif()
add_subdirectory(${mbedtls_SOURCE_DIR} ${mbedtls_BINARY_DIR})
if(NOT TESSESFRAMEWORK_STATIC) if(NOT TESSESFRAMEWORK_STATIC)
set(CMAKE_SKIP_INSTALL_RULES OFF) add_subdirectory(${mbedtls_SOURCE_DIR} ${mbedtls_BINARY_DIR} EXCLUDE_FROM_ALL)
else()
add_subdirectory(${mbedtls_SOURCE_DIR} ${mbedtls_BINARY_DIR})
endif() endif()
endif() endif()
else() else()
if(TESSESFRAMEWORK_MBEDTLS_PORTLIB STREQUAL "") if(TESSESFRAMEWORK_MBEDTLS_PORTLIB STREQUAL "")
+1
View File
@@ -22,6 +22,7 @@ src/Serialization/Json.cpp
src/Serialization/SQLite.cpp src/Serialization/SQLite.cpp
src/Serialization/BitConverter.cpp src/Serialization/BitConverter.cpp
src/Serialization/Bencode.cpp src/Serialization/Bencode.cpp
src/Serialization/Html.cpp
src/Platform/Environment.cpp src/Platform/Environment.cpp
src/Platform/Process.cpp src/Platform/Process.cpp
src/Streams/FileStream.cpp src/Streams/FileStream.cpp
+1 -1
View File
@@ -1,3 +1,3 @@
set(TESSESFRAMEWORK_MAJOR_VERSION 0) set(TESSESFRAMEWORK_MAJOR_VERSION 0)
set(TESSESFRAMEWORK_MINOR_VERSION 0) set(TESSESFRAMEWORK_MINOR_VERSION 0)
set(TESSESFRAMEWORK_PATCH_VERSION 7) set(TESSESFRAMEWORK_PATCH_VERSION 10)
+3
View File
@@ -36,8 +36,11 @@
#include <string> #include <string>
#include <vector> #include <vector>
namespace Tesses::Framework { namespace Tesses::Framework {
class TF_Timer_Handle; class TF_Timer_Handle;
// Used for internal purposes, don't use unless you want to run timer events on // Used for internal purposes, don't use unless you want to run timer events on
// another thread // another thread
@@ -316,6 +316,9 @@ typedef enum {
*/ */
bool PBKDF2(std::vector<uint8_t> &output, std::string pass, bool PBKDF2(std::vector<uint8_t> &output, std::string pass,
std::vector<uint8_t> &salt, long itterations, ShaVersion version); std::vector<uint8_t> &salt, long itterations, ShaVersion version);
bool ConstantEquals(const std::vector<uint8_t>& left, const std::vector<uint8_t>& right);
/** /**
* @brief Get secure random bytes * @brief Get secure random bytes
* *
+1 -1
View File
@@ -98,7 +98,7 @@ class ServerContext {
void SendErrorPage(bool showPath); void SendErrorPage(bool showPath);
void SendNotFound(); void SendNotFound();
void SendBadRequest(); void SendBadRequest();
void SendException(std::exception &ex); void SendException(const std::exception &ex);
void SendServerSentEvents(std::shared_ptr<ServerSentEvents> sse); void SendServerSentEvents(std::shared_ptr<ServerSentEvents> sse);
std::shared_ptr<Tesses::Framework::Streams::Stream> OpenResponseStream(); std::shared_ptr<Tesses::Framework::Streams::Stream> OpenResponseStream();
std::shared_ptr<Tesses::Framework::Streams::Stream> OpenRequestStream(); std::shared_ptr<Tesses::Framework::Streams::Stream> OpenRequestStream();
+4 -2
View File
@@ -157,9 +157,9 @@ class Uri {
std::string GetPathAndQuery(); std::string GetPathAndQuery();
uint16_t GetPort(); uint16_t GetPort();
std::string HostPort(); std::string HostPort();
bool Relative(std::string url, Uri &uri); bool Relative(std::string_view url, Uri &uri);
std::string ToString(); std::string ToString();
static bool TryParse(std::string url, Uri &uri); static bool TryParse(std::string_view url, Uri &uri);
std::string scheme; std::string scheme;
std::string host; std::string host;
uint16_t port; uint16_t port;
@@ -199,6 +199,8 @@ class HttpUtils {
static std::string UrlPathEncode(std::string_view v, static std::string UrlPathEncode(std::string_view v,
bool ignoreSpace = false); bool ignoreSpace = false);
static std::string HtmlEncode(std::string_view v); static std::string HtmlEncode(std::string_view v);
// not all escapes are supported
static std::string HtmlDecode(std::string_view v);
static std::string HtmlP(std::string_view text); static std::string HtmlP(std::string_view text);
static void SplitString(std::vector<std::string> &out, static void SplitString(std::vector<std::string> &out,
std::string_view text, std::string_view delimiter, std::string_view text, std::string_view delimiter,
@@ -83,6 +83,12 @@ class BitConverter {
static void FromUuid(uint8_t &b, const Uuid &uuid); static void FromUuid(uint8_t &b, const Uuid &uuid);
static bool TryParseSigned(const std::string &str, int64_t &val,
int base = 10);
static bool TryParseUnsigned(const std::string &str, uint64_t &val,
int base = 10);
static bool TryParseDouble(const std::string &str, double &val);
static inline bool IsLittleEndian() { static inline bool IsLittleEndian() {
uint8_t a[2]; uint8_t a[2];
a[0] = 0x01; a[0] = 0x01;
@@ -0,0 +1,90 @@
/*
TessesFramework a library to make C++ easier for me, used in CrossLang:
https://git.tesses.org/tesses50/crosslang
Copyright (C) 2026 Mike Nolan
SPDX-License-Identifier: GPL-3.0-or-later WITH TessesFramework-Exception-1.0
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include "../TextStreams/TextReader.hpp"
#include <algorithm>
namespace Tesses::Framework::Serialization::Html {
struct CaseInsensitiveHash {
size_t operator()(const std::string &s) const {
std::string lower = s;
std::transform(lower.begin(), lower.end(), lower.begin(), ::tolower);
return std::hash<std::string>{}(lower);
}
};
struct CaseInsensitiveEqual {
bool operator()(const std::string &a, const std::string &b) const {
return strcasecmp(a.c_str(), b.c_str()) == 0;
}
};
struct HtmlNode;
struct HtmlNode : public std::enable_shared_from_this<HtmlNode> {
std::string text_or_tag;
std::unordered_map<std::string, std::optional<std::string>,
CaseInsensitiveHash, CaseInsensitiveEqual>
attributes;
std::vector<std::shared_ptr<HtmlNode>> children;
bool isText = false;
void FindNodes(std::string_view tag,
std::vector<std::shared_ptr<const HtmlNode>> &nodes) const;
std::vector<std::shared_ptr<const HtmlNode>>
FindNodes(std::string_view tag) const;
void ToString(std::string &str) const;
std::string ToString() const;
void InnerText(std::string &str) const;
std::string InnerText() const;
static std::shared_ptr<HtmlNode> FromText(std::string_view text);
static std::shared_ptr<HtmlNode>
FromNode(std::string_view name,
std::initializer_list<
std::pair<std::string, std::optional<std::string>>>
attrs,
std::initializer_list<std::shared_ptr<HtmlNode>> nodes);
static std::shared_ptr<HtmlNode> FromNode(
std::string_view name,
std::vector<std::pair<std::string, std::optional<std::string>>> attrs,
std::vector<std::shared_ptr<HtmlNode>> nodes);
template <typename IttrAttrs, typename IttrNodes>
static std::shared_ptr<HtmlNode>
FromNode(std::string_view name, IttrAttrs attrBegin, IttrAttrs attrEnd,
IttrNodes nodesBegin, IttrNodes nodesEnd) {
auto node = std::make_shared<HtmlNode>();
node->isText = false;
node->text_or_tag = std::string{name};
for (auto it = attrBegin; it != attrEnd; ++it)
node->attributes[it->first] = it->second;
node->children.insert(node->children.end(), nodesBegin, nodesEnd);
return node;
}
};
std::shared_ptr<HtmlNode>
Parse(std::shared_ptr<TextStreams::TextReader> reader);
} // namespace Tesses::Framework::Serialization::Html
@@ -80,6 +80,7 @@ class NetworkStream : public Stream {
size_t ReadFrom(uint8_t *buff, size_t sz, std::string &ip, uint16_t &port); size_t ReadFrom(uint8_t *buff, size_t sz, std::string &ip, uint16_t &port);
size_t WriteTo(const uint8_t *buff, size_t sz, std::string ip, size_t WriteTo(const uint8_t *buff, size_t sz, std::string ip,
uint16_t port); uint16_t port);
static std::vector<std::pair<std::string, std::string>> static std::vector<std::pair<std::string, std::string>>
GetIPs(bool ipV6 = false); GetIPs(bool ipV6 = false);
@@ -35,6 +35,7 @@ class Stream {
virtual size_t Write(const uint8_t *buff, size_t sz); virtual size_t Write(const uint8_t *buff, size_t sz);
size_t ReadBlock(uint8_t *buff, size_t sz); size_t ReadBlock(uint8_t *buff, size_t sz);
void WriteBlock(const uint8_t *buff, size_t sz); void WriteBlock(const uint8_t *buff, size_t sz);
virtual bool CanRead(); virtual bool CanRead();
virtual bool CanWrite(); virtual bool CanWrite();
virtual bool CanSeek(); virtual bool CanSeek();
@@ -50,6 +50,7 @@
#include "Random.hpp" #include "Random.hpp"
#include "Serialization/Bencode.hpp" #include "Serialization/Bencode.hpp"
#include "Serialization/BitConverter.hpp" #include "Serialization/BitConverter.hpp"
#include "Serialization/Html.hpp"
#include "Serialization/Json.hpp" #include "Serialization/Json.hpp"
#include "Serialization/SQLite.hpp" #include "Serialization/SQLite.hpp"
#include "Streams/BufferedStream.hpp" #include "Streams/BufferedStream.hpp"
+2
View File
@@ -11,6 +11,8 @@ set(PKGCONFIG_DEPS "")
endif() endif()
configure_file(tessesframework.pc.in tessesframework.pc @ONLY) configure_file(tessesframework.pc.in tessesframework.pc @ONLY)
message(NOTICE "SrcFile ${CMAKE_CURRENT_BINARY_DIR}/tessesframework.pc")
message(NOTICE "Outdir ${CMAKE_INSTALL_LIBDIR}/pkgconfig")
install(FILES install(FILES
${CMAKE_CURRENT_BINARY_DIR}/tessesframework.pc ${CMAKE_CURRENT_BINARY_DIR}/tessesframework.pc
DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig) DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig)
+97 -39
View File
@@ -35,6 +35,7 @@ using StreamReader = Tesses::Framework::TextStreams::StreamReader;
#include <mbedtls/net_sockets.h> #include <mbedtls/net_sockets.h>
#include <mbedtls/ssl.h> #include <mbedtls/ssl.h>
#include <mbedtls/x509.h> #include <mbedtls/x509.h>
#include <mbedtls/version.h>
#endif #endif
#include <cstring> #include <cstring>
using namespace Tesses::Framework::Streams; using namespace Tesses::Framework::Streams;
@@ -56,7 +57,6 @@ class ClientTLSPrivateData {
mbedtls_pk_context client_key; mbedtls_pk_context client_key;
~ClientTLSPrivateData() { ~ClientTLSPrivateData() {
strm->SetSendTimeout((uint64_t)7);
mbedtls_ssl_close_notify(&ssl); mbedtls_ssl_close_notify(&ssl);
if (mTLS) { if (mTLS) {
mbedtls_x509_crt_free(&client_cert); mbedtls_x509_crt_free(&client_cert);
@@ -127,15 +127,23 @@ ClientTLSStream::ClientTLSStream(
if ((ret = mbedtls_ctr_drbg_seed( if ((ret = mbedtls_ctr_drbg_seed(
&data->ctr_drbg, mbedtls_entropy_func, &data->entropy, &data->ctr_drbg, mbedtls_entropy_func, &data->entropy,
(const unsigned char *)pers, strlen(pers))) != 0) { (const unsigned char *)pers, strlen(pers))) != 0) {
printf("FAILED mbedtls_ctr_drbg_seed\n");
return; char buffer[100];
mbedtls_strerror(ret, buffer, sizeof(buffer));
std::string msg = "mbedtls_ctr_drbg_seed failed: " + std::string(buffer);
delete data;
throw std::runtime_error(msg);
} }
ret = mbedtls_x509_crt_parse( ret = mbedtls_x509_crt_parse(
&data->cachain, (const unsigned char *)cert.c_str(), cert.size() + 1); &data->cachain, (const unsigned char *)cert.c_str(), cert.size() + 1);
if (ret != 0) { if (ret != 0) {
printf("FAILED mbedtls_x509_crt_parse cert %i\n", ret); char buffer[100];
return; mbedtls_strerror(ret, buffer, sizeof(buffer));
std::string msg = "mbedtls_x509_crt_parse failed: " + std::string(buffer);
delete data;
throw std::runtime_error(msg);
} }
if ((ret = mbedtls_ssl_config_defaults(&data->conf, MBEDTLS_SSL_IS_CLIENT, if ((ret = mbedtls_ssl_config_defaults(&data->conf, MBEDTLS_SSL_IS_CLIENT,
@@ -143,8 +151,9 @@ ClientTLSStream::ClientTLSStream(
MBEDTLS_SSL_PRESET_DEFAULT)) != 0) { MBEDTLS_SSL_PRESET_DEFAULT)) != 0) {
char buffer[100]; char buffer[100];
mbedtls_strerror(ret, buffer, sizeof(buffer)); mbedtls_strerror(ret, buffer, sizeof(buffer));
printf("FAILED mbedtls_ssl_conf_defaults %s\n", buffer); std::string msg = "mbedtls_ssl_conf_defaults failed: " + std::string(buffer);
return; delete data;
throw std::runtime_error(msg);
} }
mbedtls_ssl_conf_rng(&data->conf, mbedtls_ctr_drbg_random, &data->ctr_drbg); mbedtls_ssl_conf_rng(&data->conf, mbedtls_ctr_drbg_random, &data->ctr_drbg);
@@ -161,18 +170,25 @@ ClientTLSStream::ClientTLSStream(
mbedtls_ssl_set_bio(&data->ssl, static_cast<void *>(data), strm_send, mbedtls_ssl_set_bio(&data->ssl, static_cast<void *>(data), strm_send,
strm_recv, NULL); strm_recv, NULL);
if ((ret = mbedtls_ssl_setup(&data->ssl, &data->conf) != 0)) { if ((ret = mbedtls_ssl_setup(&data->ssl, &data->conf) != 0)) {
printf("FAILED mbedtls_ssl_setup %i\n", ret); char errbuf[100];
return; mbedtls_strerror(ret, errbuf, sizeof(errbuf));
std::string msg = "SSL setup failed: " + std::string(errbuf);
delete data;
throw std::runtime_error(msg);
} }
if ((ret = mbedtls_ssl_set_hostname(&data->ssl, domain.c_str()) != 0)) { if ((ret = mbedtls_ssl_set_hostname(&data->ssl, domain.c_str()) != 0)) {
printf("FAILED mbedtls_ssl_set_hostname %i\n", ret); char errbuf[100];
return; mbedtls_strerror(ret, errbuf, sizeof(errbuf));
delete data;
throw std::runtime_error("mbedtls_ssl_set_hostname failed: " + std::string(errbuf));
} }
if ((ret = mbedtls_ssl_handshake(&data->ssl)) != 0) { if ((ret = mbedtls_ssl_handshake(&data->ssl)) != 0) {
char buffer[100];
mbedtls_strerror(ret, buffer, sizeof(buffer)); char errbuf[100];
printf("FAILED mbedtls_ssl_handshake %s\n", buffer); mbedtls_strerror(ret, errbuf, sizeof(errbuf));
return; delete data;
throw std::runtime_error("mbedtls_ssl_handshake failed: " + std::string(errbuf));
} }
uint32_t flags; uint32_t flags;
if ((flags = mbedtls_ssl_get_verify_result(&data->ssl)) != 0) { if ((flags = mbedtls_ssl_get_verify_result(&data->ssl)) != 0) {
@@ -224,16 +240,20 @@ ClientTLSStream::ClientTLSStream(
if ((ret = mbedtls_ctr_drbg_seed( if ((ret = mbedtls_ctr_drbg_seed(
&data->ctr_drbg, mbedtls_entropy_func, &data->entropy, &data->ctr_drbg, mbedtls_entropy_func, &data->entropy,
(const unsigned char *)pers, strlen(pers))) != 0) { (const unsigned char *)pers, strlen(pers))) != 0) {
printf("FAILED mbedtls_ctr_drbg_seed\n"); char errbuf[100];
return; mbedtls_strerror(ret, errbuf, sizeof(errbuf));
delete data;
throw std::runtime_error("mbedtls_ctr_drbg_seed failed: " + std::string(errbuf));
} }
ret = mbedtls_x509_crt_parse( ret = mbedtls_x509_crt_parse(
&data->cachain, (const unsigned char *)cert.c_str(), cert.size() + 1); &data->cachain, (const unsigned char *)cert.c_str(), cert.size() + 1);
if (ret != 0) { if (ret != 0) {
printf("FAILED mbedtls_x509_crt_parse chain %i\n", ret); char errbuf[100];
return; mbedtls_strerror(ret, errbuf, sizeof(errbuf));
delete data;
throw std::runtime_error("mbedtls_x509_crt_parse chain failed: " + std::string(errbuf));
} }
ret = mbedtls_x509_crt_parse( ret = mbedtls_x509_crt_parse(
@@ -241,30 +261,44 @@ ClientTLSStream::ClientTLSStream(
keyStore.certificate.size() + 1); keyStore.certificate.size() + 1);
if (ret != 0) { if (ret != 0) {
printf("FAILED mbedtls_x509_crt_parse client_certificate %i\n", ret);
return; char errbuf[100];
mbedtls_strerror(ret, errbuf, sizeof(errbuf));
delete data;
throw std::runtime_error("mbedtls_x509_crt_parse client_certificate failed: " + std::string(errbuf));
} }
ret = mbedtls_pk_parse_key( ret = mbedtls_pk_parse_key(
&data->client_key, (const unsigned char *)keyStore.certificate.c_str(), &data->client_key, (const unsigned char *)keyStore.key.c_str(),
keyStore.certificate.size() + 1, keyStore.key.size() + 1,
keyStore.password.empty() keyStore.password.empty()
? NULL ? NULL
: (const unsigned char *)keyStore.password.c_str(), : (const unsigned char *)keyStore.password.c_str(),
keyStore.password.size(), mbedtls_entropy_func, &data->entropy); keyStore.password.size() + 1
#if MBEDTLS_VERSION_MAJOR == 3
, mbedtls_entropy_func, &data->entropy
#endif
);
if (ret != 0) { if (ret != 0) {
printf("FAILED mbedtls_x509_crt_parse client_certificate %i\n", ret); char errbuf[100];
return; mbedtls_strerror(ret, errbuf, sizeof(errbuf));
delete data;
throw std::runtime_error("mbedtls_pk_parse_key failed: " + std::string(errbuf));
} }
if ((ret = mbedtls_ssl_config_defaults(&data->conf, MBEDTLS_SSL_IS_CLIENT, if ((ret = mbedtls_ssl_config_defaults(&data->conf, MBEDTLS_SSL_IS_CLIENT,
MBEDTLS_SSL_TRANSPORT_STREAM, MBEDTLS_SSL_TRANSPORT_STREAM,
MBEDTLS_SSL_PRESET_DEFAULT)) != 0) { MBEDTLS_SSL_PRESET_DEFAULT)) != 0) {
char buffer[100];
mbedtls_strerror(ret, buffer, sizeof(buffer)); char errbuf[100];
printf("FAILED mbedtls_ssl_conf_defaults %s\n", buffer); mbedtls_strerror(ret, errbuf, sizeof(errbuf));
return; delete data;
throw std::runtime_error("mbedtls_ssl_conf_defaults failed: " + std::string(errbuf));
} }
mbedtls_ssl_conf_rng(&data->conf, mbedtls_ctr_drbg_random, &data->ctr_drbg); mbedtls_ssl_conf_rng(&data->conf, mbedtls_ctr_drbg_random, &data->ctr_drbg);
@@ -284,18 +318,26 @@ ClientTLSStream::ClientTLSStream(
mbedtls_ssl_set_bio(&data->ssl, static_cast<void *>(data), strm_send, mbedtls_ssl_set_bio(&data->ssl, static_cast<void *>(data), strm_send,
strm_recv, NULL); strm_recv, NULL);
if ((ret = mbedtls_ssl_setup(&data->ssl, &data->conf) != 0)) { if ((ret = mbedtls_ssl_setup(&data->ssl, &data->conf) != 0)) {
printf("FAILED mbedtls_ssl_setup %i\n", ret);
return; char errbuf[100];
mbedtls_strerror(ret, errbuf, sizeof(errbuf));
delete data;
throw std::runtime_error("mbedtls_ssl_setup failed: " + std::string(errbuf));
} }
if ((ret = mbedtls_ssl_set_hostname(&data->ssl, domain.c_str()) != 0)) { if ((ret = mbedtls_ssl_set_hostname(&data->ssl, domain.c_str()) != 0)) {
printf("FAILED mbedtls_ssl_set_hostname %i\n", ret); char errbuf[100];
return; mbedtls_strerror(ret, errbuf, sizeof(errbuf));
delete data;
throw std::runtime_error("mbedtls_ssl_set_hostname failed: " + std::string(errbuf));
} }
if ((ret = mbedtls_ssl_handshake(&data->ssl)) != 0) { if ((ret = mbedtls_ssl_handshake(&data->ssl)) != 0) {
char buffer[100];
mbedtls_strerror(ret, buffer, sizeof(buffer)); char errbuf[100];
printf("FAILED mbedtls_ssl_handshake %s\n", buffer); mbedtls_strerror(ret, errbuf, sizeof(errbuf));
return; delete data;
throw std::runtime_error("mbedtls_ssl_handshake failed: " + std::string(errbuf));
} }
uint32_t flags; uint32_t flags;
if ((flags = mbedtls_ssl_get_verify_result(&data->ssl)) != 0) { if ((flags = mbedtls_ssl_get_verify_result(&data->ssl)) != 0) {
@@ -327,10 +369,16 @@ size_t ClientTLSStream::Read(uint8_t *buffer, size_t len) {
return 0; return 0;
int r = mbedtls_ssl_read(&priv->ssl, buffer, len); int r = mbedtls_ssl_read(&priv->ssl, buffer, len);
if (r == -30848) { if (r == 0) {
priv->eos = true; priv->eos = true;
return 0; return 0;
} }
if (r < 0) {
char errbuf[100];
mbedtls_strerror(r, errbuf, sizeof(errbuf));
std::string msg = "Read failed: " + std::string(errbuf);
throw std::runtime_error(msg);
}
return (size_t)r; return (size_t)r;
#else #else
return (size_t)0; return (size_t)0;
@@ -342,6 +390,16 @@ size_t ClientTLSStream::Write(const uint8_t *buffer, size_t len) {
if (!priv->success) if (!priv->success)
return 0; return 0;
int r = mbedtls_ssl_write(&priv->ssl, buffer, len); int r = mbedtls_ssl_write(&priv->ssl, buffer, len);
if (r == 0) {
priv->eos = true;
return 0;
}
if (r < 0) {
char errbuf[100];
mbedtls_strerror(r, errbuf, sizeof(errbuf));
std::string msg = "Write failed: " + std::string(errbuf);
throw std::runtime_error(msg);
}
return (size_t)r; return (size_t)r;
#else #else
return (size_t)0; return (size_t)0;
+16
View File
@@ -377,4 +377,20 @@ bool RandomBytes(std::vector<uint8_t> &output, std::string personal_str) {
#endif #endif
return false; return false;
} }
bool ConstantEquals(const std::vector<uint8_t>& left, const std::vector<uint8_t>& right)
{
if (left.size() != right.size()) return false;
if (left.empty()) return true;
const auto* pa = left.data();
const auto* pb = right.data();
const auto n = left.size();
uint8_t diff = 0;
for (size_t i = 0; i < n; i++)
diff |= pa[i] ^ pb[i];
return diff == 0;
}
} // namespace Tesses::Framework::Crypto } // namespace Tesses::Framework::Crypto
+12 -2
View File
@@ -282,13 +282,23 @@ FIFOCreationResult LocalFilesystem::CreateFIFO(VFSPath path, uint32_t mod) {
void LocalFilesystem::Lock(VFSPath path) { void LocalFilesystem::Lock(VFSPath path) {
auto p2 = VFSPathToSystem(path); auto p2 = VFSPathToSystem(path);
const char *fopenPath = p2.c_str(); const char *fopenPath = p2.c_str();
while (true) { for (int i = 0; i < 10; i++) {
FILE *fp = fopen(fopenPath, "wx"); FILE *fp = fopen(fopenPath, "wx");
if (fp) { if (fp) {
fclose(fp); fclose(fp);
break; return;
} }
if (errno != EEXIST) {
std::string error_msg("Failed to acquire lock on ");
error_msg += p2;
error_msg += ": ";
error_msg += strerror(errno);
throw std::runtime_error(error_msg);
}
TF_Sleep(100);
} }
throw std::runtime_error("Lock acquisition timed out for " + p2);
} }
void LocalFilesystem::Unlock(VFSPath path) { void LocalFilesystem::Unlock(VFSPath path) {
std::error_code error; std::error_code error;
+99 -39
View File
@@ -39,6 +39,8 @@
#include "TessesFramework/Filesystem/VFS.hpp" #include "TessesFramework/Filesystem/VFS.hpp"
#include "TessesFramework/Filesystem/VFSFix.hpp" #include "TessesFramework/Filesystem/VFSFix.hpp"
// clang-format on // clang-format on
#include "TessesFramework/Serialization/BitConverter.hpp"
using FileStream = Tesses::Framework::Streams::FileStream; using FileStream = Tesses::Framework::Streams::FileStream;
using Stream = Tesses::Framework::Streams::Stream; using Stream = Tesses::Framework::Streams::Stream;
using SeekOrigin = Tesses::Framework::Streams::SeekOrigin; using SeekOrigin = Tesses::Framework::Streams::SeekOrigin;
@@ -607,12 +609,34 @@ uint16_t HttpServer::GetPort() {
return server->GetPort(); return server->GetPort();
return 0; return 0;
} }
std::shared_ptr<Stream> ServerContext::OpenResponseStream() { std::shared_ptr<Stream> ServerContext::OpenResponseStream() {
if (sent) if (sent)
return nullptr; return nullptr;
int64_t length = -1; int64_t length = -1;
if (!this->responseHeaders.TryGetFirstInt("Content-Length", length)) try {
length = -1; if (!this->responseHeaders.TryGetFirstInt("Content-Length", length))
length = -1;
} catch (std::invalid_argument &invalid) {
std::string error =
"<!DOCTYPE html><html lang=\"en\"><head><meta "
"charset=\"UTF-8\"> <meta name=\"viewport\" "
"content=\"width=device-width, "
"initial-scale=1.0\"><title>Internal Server Error at " +
HttpUtils::HtmlEncode(this->originalPath) +
"</title><meta name=\"color-scheme\" content=\"dark "
"light\"></head><body><h1>Internal Server Error at " +
HttpUtils::HtmlEncode(this->originalPath) +
"</h1><p>what(): " + HttpUtils::HtmlEncode(invalid.what()) +
"</p></body></html>";
this->responseHeaders.SetValue("Content-Length",
static_cast<int64_t>(error.size()));
this->statusCode = StatusCode::InternalServerError;
this->WriteHeaders();
this->strm->WriteBlock(reinterpret_cast<const uint8_t *>(error.data()),
error.size());
return nullptr;
}
if (this->version == "HTTP/1.1" && length == -1) if (this->version == "HTTP/1.1" && length == -1)
this->responseHeaders.SetValue("Transfer-Encoding", "chunked"); this->responseHeaders.SetValue("Transfer-Encoding", "chunked");
@@ -648,6 +672,10 @@ void HttpServer::StartAccepting() {
uint16_t port; uint16_t port;
auto sock = svr->GetStream(ip, port); auto sock = svr->GetStream(ip, port);
if (!TF_IsRunning()) // we need to die, that continue is not what we
// want if we must die from being done
break;
TF_LOG("New Host IP: " + ip + ":" + std::to_string(port)); TF_LOG("New Host IP: " + ip + ":" + std::to_string(port));
if (sock == nullptr) { if (sock == nullptr) {
@@ -708,9 +736,14 @@ ServerContext::ServerContext(std::shared_ptr<Stream> strm, bool debug)
} }
std::shared_ptr<Stream> ServerContext::GetStream() { return this->strm; } std::shared_ptr<Stream> ServerContext::GetStream() { return this->strm; }
void ServerContext::SendBytes(std::vector<uint8_t> buff) { void ServerContext::SendBytes(std::vector<uint8_t> buff) {
std::shared_ptr<MemoryStream> strm = std::make_shared<MemoryStream>(false);
strm->GetBuffer() = buff; if (sent)
SendStream(strm); return;
this->responseHeaders.SetValue("Content-Length",
static_cast<int64_t>(buff.size()));
auto resp = OpenResponseStream();
if (resp)
resp->WriteBlock(buff.data(), buff.size());
} }
ServerContext &ServerContext::WithLastModified(Date::DateTime dt) { ServerContext &ServerContext::WithLastModified(Date::DateTime dt) {
this->responseHeaders.SetValue("Last-Modified", dt); this->responseHeaders.SetValue("Last-Modified", dt);
@@ -718,11 +751,13 @@ ServerContext &ServerContext::WithLastModified(Date::DateTime dt) {
} }
void ServerContext::SendText(std::string text) { void ServerContext::SendText(std::string text) {
std::shared_ptr<MemoryStream> strm = std::make_shared<MemoryStream>(false); if (sent)
return;
auto &buff = strm->GetBuffer(); this->responseHeaders.SetValue("Content-Length",
buff.insert(buff.end(), text.begin(), text.end()); static_cast<int64_t>(text.size()));
SendStream(strm); auto resp = OpenResponseStream();
if (resp)
resp->WriteBlock(reinterpret_cast<uint8_t *>(text.data()), text.size());
} }
void ServerContext::SendErrorPage(bool showPath) { void ServerContext::SendErrorPage(bool showPath) {
if (sent) if (sent)
@@ -792,22 +827,34 @@ void ServerContext::SendStream(std::shared_ptr<Stream> strm) {
int64_t begin = 0; int64_t begin = 0;
int64_t end = -1; int64_t end = -1;
if (dash.size() == 1 && if (dash.size() == 2) {
res[0].find_first_of('-') != std::string::npos) {
// NUMBER-
begin = std::stoll(dash[0]);
} else if (dash.size() == 2) {
// NUMBER-NUMBER // NUMBER-NUMBER
// or // or
//-NUMBER //-NUMBER
if (dash[0].empty()) {
if (dash[0].size() > 0) {
// NUMBER-NUMBER
begin = std::stoll(dash[0]);
end = std::stoll(dash[1]);
} else {
//-NUMBER //-NUMBER
end = std::stoll(dash[1]);
if (!Serialization::BitConverter::TryParseSigned(
dash[1], end))
throw std::runtime_error(
"Failed to parse the end of the Range header");
} else if (dash[1].empty()) {
if (!Serialization::BitConverter::TryParseSigned(
dash[0], begin))
throw std::runtime_error(
"Failed to parse the end of the Range header");
} else {
// NUMBER-NUMBER
if (!Serialization::BitConverter::TryParseSigned(
dash[0], begin))
throw std::runtime_error(
"Failed to parse the beginning of the Range "
"header");
if (!Serialization::BitConverter::TryParseSigned(
dash[1], end))
throw std::runtime_error(
"Failed to parse the end of the Range header");
} }
} else { } else {
this->statusCode = BadRequest; this->statusCode = BadRequest;
@@ -886,6 +933,8 @@ void ServerContext::SendStream(std::shared_ptr<Stream> strm) {
} else { } else {
auto chunkedStream = this->OpenResponseStream(); auto chunkedStream = this->OpenResponseStream();
if (!chunkedStream)
throw std::runtime_error("Can't open response stream");
if (method != "HEAD") if (method != "HEAD")
strm->CopyTo(chunkedStream); strm->CopyTo(chunkedStream);
@@ -945,24 +994,28 @@ ServerContext &ServerContext::WithStatusCode(StatusCode code) {
this->statusCode = code; this->statusCode = code;
return *this; return *this;
} }
void ServerContext::SendException(std::exception &ex) { void ServerContext::SendException(const std::exception &ex) {
if (this->debug) { try {
if (this->debug) {
this->WithMimeType("text/html") this->WithMimeType("text/html")
.WithStatusCode(StatusCode::InternalServerError) .WithStatusCode(StatusCode::InternalServerError)
.SendText("<!DOCTYPE html><html lang=\"en\"><head><meta " .SendText(
"charset=\"UTF-8\"> <meta name=\"viewport\" " "<!DOCTYPE html><html lang=\"en\"><head><meta "
"content=\"width=device-width, " "charset=\"UTF-8\"> <meta name=\"viewport\" "
"initial-scale=1.0\"><title>Internal Server Error at " + "content=\"width=device-width, "
HttpUtils::HtmlEncode(this->originalPath) + "initial-scale=1.0\"><title>Internal Server Error at " +
"</title><meta name=\"color-scheme\" content=\"dark " HttpUtils::HtmlEncode(this->originalPath) +
"light\"></head><body><h1>Internal Server Error at " + "</title><meta name=\"color-scheme\" content=\"dark "
HttpUtils::HtmlEncode(this->originalPath) + "light\"></head><body><h1>Internal Server Error at " +
"</h1><p>what(): " + HttpUtils::HtmlEncode(ex.what()) + HttpUtils::HtmlEncode(this->originalPath) +
"</p></body></html>"); "</h1><p>what(): " + HttpUtils::HtmlEncode(ex.what()) +
} else { "</p></body></html>");
this->WithStatusCode(StatusCode::InternalServerError) } else {
.SendErrorPage(true); this->WithStatusCode(StatusCode::InternalServerError)
.SendErrorPage(true);
}
} catch (...) {
} }
} }
@@ -1106,6 +1159,13 @@ void HttpServer::Process(std::shared_ptr<Stream> strm,
ctx.requestHeaders.TryGetFirst("Content-Type", type) && ctx.requestHeaders.TryGetFirst("Content-Type", type) &&
type == "application/x-www-form-urlencoded" && type == "application/x-www-form-urlencoded" &&
ctx.requestHeaders.TryGetFirstInt("Content-Length", length)) { ctx.requestHeaders.TryGetFirstInt("Content-Length", length)) {
if (length <= 0) {
ctx.statusCode = StatusCode::BadRequest;
ctx.SendErrorPage(true);
return;
}
size_t len = (size_t)length; size_t len = (size_t)length;
std::vector<uint8_t> buffer(len); std::vector<uint8_t> buffer(len);
len = bStrm->ReadBlock(buffer.data(), len); len = bStrm->ReadBlock(buffer.data(), len);
+152 -21
View File
@@ -21,14 +21,19 @@
#include "TessesFramework/Http/HttpUtils.hpp" #include "TessesFramework/Http/HttpUtils.hpp"
#include "TessesFramework/Filesystem/VFS.hpp" #include "TessesFramework/Filesystem/VFS.hpp"
#include "TessesFramework/Serialization/BitConverter.hpp"
#include "TessesFramework/Text/StringConverter.hpp"
#include <algorithm> #include <algorithm>
#include <iostream> #include <iostream>
#include <sstream> #include <sstream>
using VFSPath = Tesses::Framework::Filesystem::VFSPath; using VFSPath = Tesses::Framework::Filesystem::VFSPath;
namespace Tesses::Framework::Http { namespace Tesses::Framework::Http {
bool Uri::Relative(std::string url, Uri &uri) { bool Uri::Relative(std::string_view url, Uri &uri) {
auto index = url.find_first_of("//"); auto path = this->path;
if (path.empty())
path = "/";
auto index = url.find("//");
if (index != std::string::npos) { if (index != std::string::npos) {
if (Uri::TryParse(url, uri)) { if (Uri::TryParse(url, uri)) {
if (index == 0) if (index == 0)
@@ -63,7 +68,10 @@ bool Uri::Relative(std::string url, Uri &uri) {
auto fourthPart = HttpUtils::SplitString(thirdPart[1], "?", 2); auto fourthPart = HttpUtils::SplitString(thirdPart[1], "?", 2);
VFSPath p = VFSPath(this->path, fourthPart[0]); VFSPath p =
VFSPath(path, (path.back() == '/' || fourthPart[0].empty())
? fourthPart[0]
: "../" + fourthPart[0]);
uri.path = uri.path =
p.CollapseRelativeParents().ToString(); // this should be safe p.CollapseRelativeParents().ToString(); // this should be safe
if (fourthPart.size() == 2) { if (fourthPart.size() == 2) {
@@ -98,21 +106,33 @@ uint16_t Uri::GetPort() {
return 69; return 69;
return 0; return 0;
} }
bool Uri::TryParse(std::string url, Uri &uri) { bool Uri::TryParse(std::string_view url, Uri &uri) {
uri.scheme = ""; uri.scheme = "";
uri.port = 0; uri.port = 0;
auto firstPart = HttpUtils::SplitString(url, "//", 2); auto firstPart = HttpUtils::SplitString(url, "//", 2);
if (firstPart.size() == 2) if (firstPart.size() == 2)
uri.scheme = firstPart[0]; uri.scheme = firstPart[0];
else if (firstPart.empty()) else
return false; return false;
auto secondPart = HttpUtils::SplitString( auto secondPart = HttpUtils::SplitString(firstPart[1], "/", 2);
firstPart.size() == 2 ? firstPart[1] : firstPart[0], "/", 2);
if (secondPart.size() == 1) { if (secondPart.size() == 1) {
uri.path = "/"; uri.path = "/";
auto hp = secondPart[0];
auto fragPart = HttpUtils::SplitString(hp, "#", 2);
if (fragPart.size() == 2) {
uri.hash = fragPart[1];
hp = fragPart[0];
}
auto queryPart = HttpUtils::SplitString(hp, "?", 2);
if (queryPart.size() == 2) {
HttpUtils::QueryParamsDecode(uri.query, queryPart[1]);
hp = queryPart[0];
}
secondPart[0] = hp;
} else if (secondPart.size() == 2) { } else if (secondPart.size() == 2) {
auto thirdPart = HttpUtils::SplitString(secondPart[1], "#", 2); auto thirdPart = HttpUtils::SplitString(secondPart[1], "#", 2);
if (thirdPart.empty()) if (thirdPart.empty())
@@ -135,14 +155,25 @@ bool Uri::TryParse(std::string url, Uri &uri) {
if (hostPortPart.empty()) if (hostPortPart.empty())
return false; return false;
if (hostPortPart.size() == 2) { if (hostPortPart.size() == 2) {
uri.port = (uint16_t)std::stoul(hostPortPart[1]); uint64_t portNum;
if (!Serialization::BitConverter::TryParseUnsigned(hostPortPart[1],
portNum))
return false;
if (portNum > 65535)
return false;
uri.port = static_cast<uint16_t>(portNum);
} }
uri.host = hostPortPart[0]; uri.host = hostPortPart[0];
return true; return true;
} }
Uri::Uri() : query(true) {} Uri::Uri() : query(true) {}
std::string Uri::GetPathAndQuery() { return this->path + this->GetQuery(); } std::string Uri::GetPathAndQuery() {
if (this->path.empty() || this->path.front() != '/')
return '/' + this->path + this->GetQuery();
return this->path + this->GetQuery();
}
std::string Uri::GetQuery() { std::string Uri::GetQuery() {
if (this->query.kvp.empty()) if (this->query.kvp.empty())
return ""; return "";
@@ -532,9 +563,16 @@ std::string HttpUtils::HtmlP(std::string_view text) {
auto flush = [&]() -> void { auto flush = [&]() -> void {
if (!builder.empty()) { if (!builder.empty()) {
// Collect trailing punctuation
std::string trailing;
static const char *punct = ".,;:!?'\"";
while (!builder.empty() && std::strchr(punct, builder.back())) {
trailing.insert(trailing.begin(), builder.back());
builder.pop_back();
}
if (builder.find("http://") == 0 || builder.find("https://") == 0 || if (builder.find("http://") == 0 || builder.find("https://") == 0 ||
builder.find("ftp://") == 0 || builder.find("ftps://") == 0 || builder.find("ftp://") == 0 || builder.find("ftps://") == 0 ||
builder.find("magnet:") == 0 || builder.find("btmh:") == 0) { builder.find("magnet:") == 0) {
newText += "<a href=\"" + HttpUtils::HtmlEncode(builder) + newText += "<a href=\"" + HttpUtils::HtmlEncode(builder) +
"\">" + HttpUtils::HtmlEncode(builder) + "</a>"; "\">" + HttpUtils::HtmlEncode(builder) + "</a>";
} else if (builder.find("mailto:") == 0) { } else if (builder.find("mailto:") == 0) {
@@ -548,6 +586,7 @@ std::string HttpUtils::HtmlP(std::string_view text) {
} else { } else {
newText += HttpUtils::HtmlEncode(builder); newText += HttpUtils::HtmlEncode(builder);
} }
newText += HttpUtils::HtmlEncode(trailing);
builder = ""; builder = "";
} }
}; };
@@ -564,7 +603,7 @@ std::string HttpUtils::HtmlP(std::string_view text) {
break; break;
case '\t': case '\t':
flush(); flush();
newText += "&tab;"; newText += "&nbsp;&nbsp;&nbsp;&nbsp;";
break; break;
case '\r': case '\r':
flush(); flush();
@@ -578,6 +617,106 @@ std::string HttpUtils::HtmlP(std::string_view text) {
return newText; return newText;
} }
static std::unordered_map<std::string, std::string> htmlencodings = {
{"&quot;", "\""},
{"&apos;", "'"},
{"&amp;", "&"},
{"&lt;", "<"},
{"&gt;", ">"},
{"&nbsp;", "\xC2\xA0"},
{"&copy;", "\xC2\xA9"},
{"&reg;", "\xC2\xAE"},
{"&trade;", "\xE2\x84\xA2"},
{"&mdash;", "\xE2\x80\x94"},
{"&ndash;", "\xE2\x80\x93"},
{"&hellip;", "\xE2\x80\xA6"},
{"&rsquo;", "\xE2\x80\x99"},
{"&lsquo;", "\xE2\x80\x98"},
{"&rdquo;", "\xE2\x80\x9D"},
{"&ldquo;", "\xE2\x80\x9C"},
{"&euro;", "\xE2\x82\xAC"},
{"&pound;", "\xC2\xA3"},
{"&yen;", "\xC2\xA5"},
{"&deg;", "\xC2\xB0"},
{"&plusmn;", "\xC2\xB1"},
{"&times;", "\xC3\x97"},
{"&divide;", "\xC3\xB7"},
{"&laquo;", "\xC2\xAB"},
{"&raquo;", "\xC2\xBB"},
{"&bull;", "\xE2\x80\xA2"},
{"&middot;", "\xC2\xB7"},
{"&frac12;", "\xC2\xBD"},
{"&frac14;", "\xC2\xBC"},
{"&frac34;", "\xC2\xBE"},
};
std::string HttpUtils::HtmlDecode(std::string_view view) {
std::string myHtml = {};
std::string tmp = {};
bool inEscaped = false;
for (size_t i = 0; i < view.size(); i++) {
if (inEscaped) {
if (view[i] == ';') {
inEscaped = false;
if (tmp.size() > 2 && tmp[1] == '#') //&#N
{
if (tmp[2] == 'x' || tmp[2] == 'X') {
auto res = tmp.substr(3);
uint64_t code;
if (Serialization::BitConverter::TryParseUnsigned(
res, code, 16)) {
if (code <= 0x10FFFF &&
(code < 0xD800 || code > 0xDFFF)) {
Text::StringConverter::UTF8::FromUTF32(
myHtml, {static_cast<char32_t>(code)});
} else {
myHtml += "\xEF\xBF\xBD";
}
} else {
myHtml += "\xEF\xBF\xBD";
}
} else {
auto res = tmp.substr(2);
uint64_t code;
if (Serialization::BitConverter::TryParseUnsigned(
res, code, 10)) {
if (code <= 0x10FFFF &&
(code < 0xD800 || code > 0xDFFF)) {
Text::StringConverter::UTF8::FromUTF32(
myHtml, {static_cast<char32_t>(code)});
} else {
myHtml += "\xEF\xBF\xBD";
}
} else {
myHtml += "\xEF\xBF\xBD";
}
}
} else {
tmp += ';';
auto result = htmlencodings.find(tmp);
if (result != htmlencodings.end()) {
myHtml += result->second;
} else {
myHtml += "\xEF\xBF\xBD";
}
}
} else {
tmp += view[i];
}
} else {
if (view[i] == '&') {
tmp = "&";
inEscaped = true;
} else {
myHtml += view[i];
}
}
}
if (inEscaped)
myHtml += tmp;
return myHtml;
}
std::string HttpUtils::HtmlEncode(std::string_view html) { std::string HttpUtils::HtmlEncode(std::string_view html) {
std::string myHtml = {}; std::string myHtml = {};
for (auto item : html) { for (auto item : html) {
@@ -838,16 +977,8 @@ bool HttpDictionary::TryGetFirstInt(std::string key, int64_t &value) {
std::string val; std::string val;
if (!TryGetFirst(key, val)) if (!TryGetFirst(key, val))
return false; return false;
try {
size_t off = 0; return Serialization::BitConverter::TryParseSigned(val, value);
auto v = std::stoll(val, &off);
if (off != val.size())
return false;
value = v;
} catch (std::exception &ex) {
return false;
}
return true;
} }
bool HttpDictionary::TryGetFirstDate(std::string key, Date::DateTime &dt) { bool HttpDictionary::TryGetFirstDate(std::string key, Date::DateTime &dt) {
std::string val; std::string val;
+10 -6
View File
@@ -332,14 +332,18 @@ std::string GetPlatform() {
#if TARGET_OS_WATCH #if TARGET_OS_WATCH
return "Apple Watch"; return "Apple Watch";
#endif #endif
#if __EMSCRIPTEN__
return "WebAssembly";
#endif
return "Unknown Apple Device"; return "Unknown Apple Device";
#endif #endif
#if defined(__EMSCRIPTEN__)
return "WebAssembly";
#endif
#if defined(__HAIKU__)
return "Haiku";
#endif
#if defined(__TessesOS__)
return "TessesOS";
#endif
return "Unknown"; return "Unknown";
} }
} // namespace Tesses::Framework::Platform::Environment } // namespace Tesses::Framework::Platform::Environment
+57
View File
@@ -284,4 +284,61 @@ void BitConverter::FromSint16LE(uint8_t &b, int16_t v) {
memcpy(&dest, &v, sizeof(uint16_t)); memcpy(&dest, &v, sizeof(uint16_t));
FromUint16LE(b, dest); FromUint16LE(b, dest);
} }
bool BitConverter::TryParseSigned(const std::string &str, int64_t &val,
int base) {
try {
if (str.find_first_not_of(" \t") != 0)
return false;
size_t pos = 0;
int64_t v = std::stoll(str, &pos, base);
if (pos < str.size())
return false;
val = v;
return true;
} catch (std::out_of_range &arg) {
return false;
} catch (std::invalid_argument &arg) {
return false;
}
}
bool BitConverter::TryParseUnsigned(const std::string &str, uint64_t &val,
int base) {
try {
if (str.find_first_not_of(" \t") != 0)
return false;
if (str[0] == '-' || str[0] == '+')
return false;
size_t pos = 0;
uint64_t v = std::stoull(str, &pos, base);
if (pos < str.size())
return false;
val = v;
return true;
} catch (std::out_of_range &arg) {
return false;
} catch (std::invalid_argument &arg) {
return false;
}
}
bool BitConverter::TryParseDouble(const std::string &str, double &val) {
try {
if (str.find_first_not_of(" \t") != 0)
return false;
size_t pos = 0;
double v = std::stod(str, &pos);
if (pos < str.size())
return false;
val = v;
return true;
} catch (std::out_of_range &arg) {
return false;
} catch (std::invalid_argument &arg) {
return false;
}
}
} // namespace Tesses::Framework::Serialization } // namespace Tesses::Framework::Serialization
+428
View File
@@ -0,0 +1,428 @@
#include "TessesFramework/Serialization/Html.hpp"
#include "TessesFramework/Console.hpp"
#include "TessesFramework/Http/HttpUtils.hpp"
#include <unordered_set>
namespace Tesses::Framework::Serialization::Html {
static std::unordered_set<std::string> html_nodes_with_no_body = {
"area", "base", "br", "col", "embed", "hr", "img",
"input", "link", "meta", "source", "track", "wbr",
};
std::string HtmlNode::ToString() const {
std::string text;
HtmlNode::ToString(text);
return text;
}
void HtmlNode::InnerText(std::string &str) const {
if (this->isText) {
str += this->text_or_tag;
} else {
for (const auto &item : this->children)
item->InnerText(str);
}
}
std::string HtmlNode::InnerText() const {
std::string innerText;
InnerText(innerText);
return innerText;
}
void HtmlNode::ToString(std::string &str) const {
if (this->isText) {
str += Http::HttpUtils::HtmlEncode(this->text_or_tag);
} else {
str += "<";
str += this->text_or_tag;
for (auto &attr : this->attributes) {
str += ' ';
str += attr.first;
if (attr.second) {
str += "=\"";
str += Http::HttpUtils::HtmlEncode(attr.second.value());
str += "\"";
}
}
str += ">";
if (html_nodes_with_no_body.count(this->text_or_tag) == 0) {
for (auto &child : this->children)
child->ToString(str);
str += "</";
str += this->text_or_tag;
str += ">";
}
}
}
std::shared_ptr<HtmlNode> HtmlNode::FromText(std::string_view text) {
auto node = std::make_shared<HtmlNode>();
node->isText = true;
node->text_or_tag = std::string{text};
return node;
}
std::shared_ptr<HtmlNode> HtmlNode::FromNode(
std::string_view name,
std::initializer_list<std::pair<std::string, std::optional<std::string>>>
attrs,
std::initializer_list<std::shared_ptr<HtmlNode>> nodes) {
return HtmlNode::FromNode(name, attrs.begin(), attrs.end(), nodes.begin(),
nodes.end());
}
std::shared_ptr<HtmlNode> HtmlNode::FromNode(
std::string_view name,
std::vector<std::pair<std::string, std::optional<std::string>>> attrs,
std::vector<std::shared_ptr<HtmlNode>> nodes) {
return HtmlNode::FromNode(name, attrs.begin(), attrs.end(), nodes.begin(),
nodes.end());
}
void HtmlNode::FindNodes(
std::string_view tag,
std::vector<std::shared_ptr<const HtmlNode>> &nodes) const {
if (!this->isText) {
if (this->text_or_tag == tag)
nodes.push_back(this->shared_from_this());
for (const auto &node : this->children)
node->FindNodes(tag, nodes);
}
}
std::vector<std::shared_ptr<const HtmlNode>>
HtmlNode::FindNodes(std::string_view tag) const {
std::vector<std::shared_ptr<const HtmlNode>> nodes;
FindNodes(tag, nodes);
return nodes;
}
enum class HtmlLexTokenType { SYMBOL, TEXT, STRING };
struct HtmlLexToken {
std::string text;
HtmlLexTokenType type;
HtmlLexToken(HtmlLexTokenType type, std::string_view text)
: text(text), type(type) {}
};
struct HtmlLexer {
std::shared_ptr<TextStreams::TextReader> reader;
std::vector<HtmlLexToken> &tokens;
int32_t _peeked = -1;
std::string builder;
bool ignoreSpaceMode = false;
std::string nodeName = {};
HtmlLexer(std::shared_ptr<TextStreams::TextReader> reader,
std::vector<HtmlLexToken> &tokens)
: reader(std::move(reader)), tokens(tokens) {}
int32_t Peek() {
if (_peeked != -1)
return _peeked;
_peeked = Read();
return _peeked;
}
int64_t Read() {
if (_peeked != -1) {
auto tmp = _peeked;
_peeked = -1;
return tmp;
}
return reader->ReadChar();
}
void Flush() {
if (!builder.empty()) {
this->tokens.emplace_back(HtmlLexTokenType::TEXT, builder);
if (ignoreSpaceMode && nodeName.empty())
nodeName = builder;
builder = "";
}
}
void SkipTo(std::string_view fmt) {
if (fmt.empty())
return;
std::string window;
window.resize(fmt.size());
while (fmt != window) {
int32_t read = Read();
if (read == -1)
break;
for (size_t i = 0; i < window.size() - 1; i++) {
window[i] = window[i + 1];
}
window.back() = static_cast<char>(read);
}
}
void Lex() {
int read;
while ((read = Read()) != -1) {
int peek = Peek();
switch (read) {
case '"':
case '\'': {
if (ignoreSpaceMode) {
Flush();
char quote = static_cast<char>(read);
std::string val;
int c;
while ((c = Read()) != -1 && c != quote)
val += static_cast<char>(c);
tokens.emplace_back(HtmlLexTokenType::STRING, val);
} else {
builder += static_cast<char>(read);
}
break;
}
case '/': {
if (ignoreSpaceMode && peek == '>') {
Flush();
Read();
ignoreSpaceMode = false;
tokens.emplace_back(HtmlLexTokenType::SYMBOL, "/>");
} else if (ignoreSpaceMode) {
builder += '/';
} else {
builder += '/';
}
break;
}
case '>': {
if (ignoreSpaceMode) {
Flush();
ignoreSpaceMode = false;
tokens.emplace_back(HtmlLexTokenType::SYMBOL, ">");
if (nodeName == "script" || nodeName == "style") {
nodeName = "_";
std::string builder2 = "";
bool eof = false;
while ((read = Read()) != -1) {
if (read == '<') {
peek = Peek();
if (peek == '/') {
Read();
break;
} else if (peek == -1) {
eof = true;
break;
} else {
builder2 += '<';
}
} else {
builder2 += static_cast<char>(read);
}
}
if (read != -1 && !eof) {
tokens.emplace_back(HtmlLexTokenType::TEXT,
builder2);
ignoreSpaceMode = true;
tokens.emplace_back(HtmlLexTokenType::SYMBOL, "</");
} else {
return;
}
}
} else {
builder += '>';
}
} break;
case '<': {
Flush();
if (peek == '!') {
//<!-- -->
//<!DOCTYPE html>
Read();
peek = Peek();
if (peek == '-') {
Read();
peek = Peek();
if (peek == '-') {
Read();
SkipTo("-->");
} else {
SkipTo(">");
}
} else {
SkipTo(">");
}
} else if (peek == '/') {
Read();
ignoreSpaceMode = true;
tokens.emplace_back(HtmlLexTokenType::SYMBOL, "</");
} else {
ignoreSpaceMode = true;
tokens.emplace_back(HtmlLexTokenType::SYMBOL, "<");
nodeName = {};
}
} break;
case '=': {
if (!ignoreSpaceMode) {
builder += "=";
} else {
Flush();
tokens.emplace_back(HtmlLexTokenType::SYMBOL, "=");
}
} break;
case ' ':
case '\n':
case '\t':
case '\r':
if (!ignoreSpaceMode) {
builder += static_cast<char>(read);
} else {
Flush();
}
break;
default:
builder += static_cast<char>(read);
break;
}
}
Flush();
}
};
struct HtmlParser {
std::vector<HtmlLexToken> &tokens;
size_t index = 0;
HtmlParser(std::vector<HtmlLexToken> &tokens) : tokens(tokens) {}
bool IsSymbol(std::string_view type) {
if (Done())
return false;
auto &obj = Peak();
if (obj.type != HtmlLexTokenType::SYMBOL)
return false;
if (obj.text != type)
return false;
index++;
return true;
}
bool Done() { return index >= tokens.size(); }
HtmlLexToken &Read() { return tokens[index++]; }
HtmlLexToken &Peak() { return tokens[index]; }
void Parse(std::string_view name, std::shared_ptr<HtmlNode> &node,
bool root = false) {
while (!Done()) {
auto &curToken = Peak();
switch (curToken.type) {
case HtmlLexTokenType::TEXT: {
Read();
node->children.push_back(HtmlNode::FromText(
Http::HttpUtils::HtmlDecode(curToken.text)));
} break;
case HtmlLexTokenType::SYMBOL: {
if (curToken.text == "<") {
Read();
if (Done()) {
return;
}
auto &val = Peak();
if (val.type == HtmlLexTokenType::TEXT) {
Read();
std::shared_ptr<HtmlNode> newNode =
std::make_shared<HtmlNode>();
newNode->isText = false;
newNode->text_or_tag = val.text;
while (!Done()) {
auto &k = Peak();
if (k.type == HtmlLexTokenType::TEXT) {
Read();
if (IsSymbol("=")) {
if (!Done()) {
auto &v = Peak();
if (v.type ==
HtmlLexTokenType::STRING) {
Read();
newNode->attributes[k.text] =
Http::HttpUtils::HtmlDecode(
v.text);
continue;
}
}
}
newNode->attributes[k.text] = std::nullopt;
continue;
}
if (k.type == HtmlLexTokenType::SYMBOL &&
(k.text == ">" || k.text == "/>"))
break;
}
if (!IsSymbol("/>")) {
IsSymbol(">");
if (html_nodes_with_no_body.count(val.text) == 0)
Parse(val.text, newNode);
}
node->children.push_back(newNode);
}
} else if (curToken.text == "</") {
Read();
if (Done()) {
node->children.push_back(HtmlNode::FromText("</"));
return;
}
auto &val = Peak();
Read();
IsSymbol(">");
if (val.text == name && !root)
return;
}
} break;
}
}
}
std::shared_ptr<HtmlNode> Parse() {
std::shared_ptr<HtmlNode> node = std::make_shared<HtmlNode>();
node->isText = false;
node->text_or_tag = ":root";
Parse(":root", node, true);
return node;
}
};
std::shared_ptr<HtmlNode>
Parse(std::shared_ptr<TextStreams::TextReader> reader) {
std::vector<HtmlLexToken> tokens;
HtmlLexer lexer(reader, tokens);
lexer.Lex();
HtmlParser parser(tokens);
return parser.Parse();
}
} // namespace Tesses::Framework::Serialization::Html