Files
trail-mate/apps/linux_lvgl/tests/linux_http_test.cpp
T

221 lines
12 KiB
C++

#include "bounded_fixture_file.h"
#include "lvgl.h"
#include "platform/linux/http_download.h"
#include "platform/linux/route_image_tasks.h"
#include "platform/ui/route_storage.h"
#include <algorithm>
#include <cassert>
#include <chrono>
#include <fcntl.h>
#include <filesystem>
#include <fstream>
#include <sys/file.h>
#include <sys/stat.h>
#include <thread>
#include <unistd.h>
namespace http = platform::ui::http_client;
namespace routes = platform::ui::route_storage;
int main(int argc, char** argv)
{
assert(argc == 3);
const std::string base = argv[1], bad_tls = argv[2];
http::Request request;
request.max_bytes = 1024;
request.timeout_ms = 2000;
std::string output, error;
http::TransferStats stats;
std::string url = base + "/ok";
request.url = url.c_str();
assert(http::get_text(request, output, error, &stats));
assert(output == "Ab!9" && stats.bytes == 4 && stats.http_status == 200);
url = base + "/redirect";
request.url = url.c_str();
assert(http::get_text(request, output, error, &stats) && output == "Ab!9");
url = base + "/chunked";
request.url = url.c_str();
assert(http::get_text(request, output, error, &stats) && output == std::string("A\0B", 3));
for (const auto* path : {"/missing", "/partial", "/oversize", "/file-redirect", "/not-modified"})
{
url = base + path;
request.url = url.c_str();
assert(!http::get_text(request, output, error, &stats) && output.empty() && !error.empty());
assert(error != "No error");
}
url = bad_tls + "/ok";
request.url = url.c_str();
assert(!http::get_text(request, output, error, &stats) && output.empty());
url = base + "/ok";
request.url = url.c_str();
assert(!http::download(
request, [](const uint8_t*, size_t, void*)
{ return false; },
nullptr, error, &stats));
std::atomic_bool cancelled{true};
assert(!platform::linux_runtime::http_download(
request, [](const uint8_t*, size_t, void*)
{ return true; },
nullptr, error, &stats, &cancelled));
url = base + "/slow";
request.url = url.c_str();
request.timeout_ms = 80;
const auto started = std::chrono::steady_clock::now();
assert(!http::get_text(request, output, error, &stats));
assert(std::chrono::steady_clock::now() - started < std::chrono::seconds(2));
const auto root = std::filesystem::temp_directory_path() / ("trailmate-http-" + std::to_string(getpid()));
std::filesystem::create_directories(root);
setenv("TRAIL_MATE_SD_ROOT", root.c_str(), 1);
std::string asset;
std::size_t image_count = 99;
for (const std::string& invalid : std::vector<std::string>{".", "..", "../escape", "/absolute", std::string(49, 'x'), "p-" + std::string(64, 'G')})
{
assert(!routes::ensure_route_asset_dir(invalid, asset) && asset.empty());
assert(!routes::count_route_saved_images(invalid, 64, image_count) && image_count == 0);
}
const auto package_id = "p-" + std::string(64, 'a');
assert(routes::ensure_route_asset_dir(package_id, asset));
assert(std::filesystem::path(asset).filename() == package_id);
assert(routes::count_route_saved_images(package_id, 64, image_count) && image_count == 0);
const auto outside = root / "outside-assets";
std::filesystem::create_directories(outside);
std::filesystem::create_directory_symlink(outside, root / "routes/.trailmate/escaped-asset");
assert(!routes::ensure_route_asset_dir("escaped-asset", asset));
assert(!routes::count_route_saved_images("escaped-asset", 64, image_count));
assert(!std::filesystem::exists(outside / "images"));
std::filesystem::create_directories(root / "routes/.trailmate/escaped-images");
std::filesystem::create_directory_symlink(outside, root / "routes/.trailmate/escaped-images/images");
assert(!routes::ensure_route_asset_dir("escaped-images", asset));
assert(routes::ensure_route_asset_dir("fixture", asset));
std::ofstream(outside / "foreign.jpg") << "untrusted";
std::filesystem::create_symlink(outside / "foreign.jpg", std::filesystem::path(asset) / "images/img-0064.jpg");
assert(routes::count_route_saved_images("fixture", 64, image_count) && image_count == 0);
const auto image = std::filesystem::path(asset) / "images/img-0001.jpg";
url = base + "/image";
auto downloaded = routes::download_route_image(url, image.string());
assert(downloaded.ok && downloaded.bytes && downloaded.http_status == 200);
uint8_t reference[32]{};
assert(routes::route_image_reference_hash(url, reference));
assert(routes::route_image_source_matches(image.string(), reference));
const auto original_size = std::filesystem::file_size(image);
const int lease = open((std::filesystem::path(asset) / ".route-images.lock").c_str(), O_RDWR | O_CREAT | O_CLOEXEC, 0600);
assert(lease >= 0 && flock(lease, LOCK_EX | LOCK_NB) == 0);
assert(!routes::download_route_image(base + "/jpeg", image.string()).ok);
assert(std::filesystem::file_size(image) == original_size && routes::route_image_source_matches(image.string(), reference));
close(lease);
auto wait_image_task = []
{
const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5);
while (routes::route_image_download_status().busy && std::chrono::steady_clock::now() < deadline)
std::this_thread::sleep_for(std::chrono::milliseconds(10));
assert(!routes::route_image_download_status().busy);
return routes::route_image_download_status().phase;
};
routes::RouteImageCacheItem cache;
cache.source_path = image.string();
cache.preview_path = (std::filesystem::path(asset) / "thumbs/thumb-0001.bmp").string();
cache.view_path = (std::filesystem::path(asset) / "views/view-0001.bmp").string();
cache.source_hash_valid = true;
std::copy_n(reference, 32, cache.source_hash);
assert(routes::start_route_image_cache_build("fixture", {cache}, error));
assert(wait_image_task() == routes::RouteImageDownloadPhase::Done);
auto bitmap = [](const std::string& path, uint32_t width, uint32_t height)
{
const auto data = read_fixture_bytes(path);
assert(data.size() >= 54 && data[0] == 'B' && data[1] == 'M');
auto integer = [&](size_t at)
{ uint32_t value = 0; for (unsigned i = 0; i < 4; ++i) value |= uint32_t(uint8_t(data[at + i])) << (8 * i); return value; };
assert(integer(18) == width && integer(22) == height && integer(2) == data.size());
return data;
};
auto thumb = bitmap(cache.preview_path, 200, 120);
(void)bitmap(cache.view_path, 320, 180);
lv_init();
std::vector<uint16_t> frame(200 * 120);
auto* display = lv_display_create(200, 120);
lv_display_set_color_format(display, LV_COLOR_FORMAT_RGB565);
lv_display_set_buffers(display, frame.data(), nullptr, static_cast<uint32_t>(frame.size() * sizeof(uint16_t)), LV_DISPLAY_RENDER_MODE_DIRECT);
unsigned flushes = 0;
lv_display_set_user_data(display, &flushes);
lv_display_set_flush_cb(display, [](lv_display_t* target, const lv_area_t*, uint8_t*)
{ ++*static_cast<unsigned*>(lv_display_get_user_data(target)); lv_display_flush_ready(target); });
lv_obj_set_style_pad_all(lv_screen_active(), 0, 0);
auto* rendered = lv_image_create(lv_screen_active());
const auto image_uri = "A:" + cache.preview_path;
lv_image_set_src(rendered, image_uri.c_str());
lv_obj_set_pos(rendered, 0, 0);
lv_obj_update_layout(lv_screen_active());
lv_refr_now(display);
assert(flushes && frame[60 * 200 + 50] == 0xF800 && frame[60 * 200 + 150] == 0x07E0);
lv_display_delete(display);
lv_deinit();
const auto left_pixel = 54 + (120 - 1 - 60) * 600 + 50 * 3;
assert(uint8_t(thumb[left_pixel + 2]) == 255 && uint8_t(thumb[left_pixel]) == 0);
// Binding length alone cannot detect same-size corruption; cache admission must hash.
std::string original;
{
const auto bytes = read_fixture_bytes(image);
original.assign(bytes.begin(), bytes.end());
}
auto corrupt = original;
corrupt.back() ^= 1;
std::ofstream(image, std::ios::binary).write(corrupt.data(), corrupt.size());
assert(routes::start_route_image_cache_build("fixture", {cache}, error));
assert(wait_image_task() == routes::RouteImageDownloadPhase::Failed);
std::ofstream(image, std::ios::binary).write(original.data(), original.size());
auto invalid_cache = cache;
invalid_cache.preview_path = (root / "outside-cache.bmp").string();
assert(routes::start_route_image_cache_build("fixture", {invalid_cache}, error));
assert(wait_image_task() == routes::RouteImageDownloadPhase::Failed && !std::filesystem::exists(invalid_cache.preview_path));
assert(!routes::start_route_image_cache_build("fixture", {cache}, error, routes::RouteImageTaskPresentation::PageOnly, "signed-package.kml"));
const auto jpeg_path = (std::filesystem::path(asset) / "images/img-0002.jpg").string();
assert(routes::download_route_image(base + "/jpeg", jpeg_path).ok);
routes::RouteImageCacheItem jpeg;
jpeg.source_path = jpeg_path;
jpeg.source_hash_valid = routes::route_image_reference_hash(base + "/jpeg", jpeg.source_hash);
assert(routes::start_route_image_cache_build("fixture", {jpeg}, error));
assert(wait_image_task() == routes::RouteImageDownloadPhase::Done);
auto jpeg_bmp = bitmap((std::filesystem::path(asset) / "views/view-0002.bmp").string(), 320, 180);
const auto middle = 54 + (180 - 1 - 90) * 960 + 160 * 3;
assert(uint8_t(jpeg_bmp[middle]) > 240 && uint8_t(jpeg_bmp[middle + 2]) < 16);
for (const auto* path : {"/missing", "/partial", "/oversize"})
{
const auto failed = routes::download_route_image(base + path, image.string(), 1024);
assert(!failed.ok && std::filesystem::file_size(image) == original_size);
assert(routes::route_image_source_matches(image.string(), reference));
}
assert(!routes::download_route_image(url, (root / "escape.jpg").string()).ok);
std::filesystem::create_directory_symlink(root.parent_path(), root / "routes/.trailmate/escape");
assert(!routes::download_route_image(url, (root / "routes/.trailmate/escape/images/img-0001.jpg").string()).ok);
const auto fifo = std::filesystem::path(asset) / "images/img-0003.jpg";
assert(mkfifo(fifo.c_str(), 0600) == 0);
std::filesystem::copy_file(image.string() + ".source", fifo.string() + ".source");
auto special = cache;
special.source_path = fifo.string();
special.preview_path.clear();
special.view_path.clear();
assert(routes::start_route_image_cache_build("fixture", {special}, error));
assert(wait_image_task() == routes::RouteImageDownloadPhase::Failed);
assert(!routes::download_route_image(base + "/image", fifo.string()).ok);
std::string batch_asset;
assert(routes::ensure_route_asset_dir("batch", batch_asset));
const auto batch_image = (std::filesystem::path(batch_asset) / "images/img-0001.jpg").string();
assert(routes::start_route_image_download("batch", {{base + "/image", batch_image, {}, {}}}, error));
assert(wait_image_task() == routes::RouteImageDownloadPhase::Done);
(void)bitmap((std::filesystem::path(batch_asset) / "thumbs/thumb-0001.bmp").string(), 200, 120);
(void)bitmap((std::filesystem::path(batch_asset) / "views/view-0001.bmp").string(), 320, 180);
std::string other_asset;
assert(routes::ensure_route_asset_dir("other", other_asset));
const auto pending = (std::filesystem::path(other_asset) / "images/img-0001.jpg").string();
assert(routes::start_route_image_download("other", {{base + "/slow", pending, {}, {}}}, error));
std::this_thread::sleep_for(std::chrono::milliseconds(100));
const auto stopping = std::chrono::steady_clock::now();
platform::linux_runtime::stop_route_image_tasks();
assert(std::chrono::steady_clock::now() - stopping < std::chrono::seconds(3));
assert(!routes::route_image_download_status().busy);
assert(!std::filesystem::exists(pending));
for (const auto& item : std::filesystem::directory_iterator(std::filesystem::path(other_asset) / "images"))
assert(item.path().filename().string().find(".download-") == std::string::npos);
std::filesystem::remove_all(root);
}