feat: common TLS library, weather app, UI scaling fix in Window Server, and more

This commit is contained in:
2026-02-21 13:05:28 +01:00
parent 9d5e7eac8d
commit e0bfc97f0f
32 changed files with 1440 additions and 1048 deletions
+14
View File
@@ -789,6 +789,16 @@ namespace Zenith {
return count; return count;
} }
// ---- Window scale syscalls ----
static int Sys_WinSetScale(int scale) {
return WinServer::SetScale(scale);
}
static int Sys_WinGetScale() {
return WinServer::GetScale();
}
// ---- Window server syscalls ---- // ---- Window server syscalls ----
static int Sys_WinCreate(const char* title, int w, int h, WinCreateResult* result) { static int Sys_WinCreate(const char* title, int w, int h, WinCreateResult* result) {
@@ -1003,6 +1013,10 @@ namespace Zenith {
return (int64_t)Sys_Kill((int)frame->arg1); return (int64_t)Sys_Kill((int)frame->arg1);
case SYS_DEVLIST: case SYS_DEVLIST:
return (int64_t)Sys_DevList((DevInfo*)frame->arg1, (int)frame->arg2); return (int64_t)Sys_DevList((DevInfo*)frame->arg1, (int)frame->arg2);
case SYS_WINSETSCALE:
return (int64_t)Sys_WinSetScale((int)frame->arg1);
case SYS_WINGETSCALE:
return (int64_t)Sys_WinGetScale();
default: default:
return -1; return -1;
} }
+4 -1
View File
@@ -75,6 +75,8 @@ namespace Zenith {
static constexpr uint64_t SYS_WINMAP = 59; static constexpr uint64_t SYS_WINMAP = 59;
static constexpr uint64_t SYS_WINSENDEVENT = 60; static constexpr uint64_t SYS_WINSENDEVENT = 60;
static constexpr uint64_t SYS_WINRESIZE = 64; static constexpr uint64_t SYS_WINRESIZE = 64;
static constexpr uint64_t SYS_WINSETSCALE = 65;
static constexpr uint64_t SYS_WINGETSCALE = 66;
// Process management syscalls // Process management syscalls
static constexpr uint64_t SYS_PROCLIST = 61; static constexpr uint64_t SYS_PROCLIST = 61;
@@ -135,12 +137,13 @@ namespace Zenith {
// Window server shared types // Window server shared types
struct WinEvent { struct WinEvent {
uint8_t type; // 0=key, 1=mouse, 2=resize, 3=close uint8_t type; // 0=key, 1=mouse, 2=resize, 3=close, 4=scale
uint8_t _pad[3]; uint8_t _pad[3];
union { union {
KeyEvent key; KeyEvent key;
struct { int32_t x, y, scroll; uint8_t buttons, prev_buttons; } mouse; struct { int32_t x, y, scroll; uint8_t buttons, prev_buttons; } mouse;
struct { int32_t w, h; } resize; struct { int32_t w, h; } resize;
struct { int32_t scale; } scale;
}; };
}; };
+23
View File
@@ -14,6 +14,7 @@
namespace WinServer { namespace WinServer {
static WindowSlot g_slots[MaxWindows]; static WindowSlot g_slots[MaxWindows];
static int g_uiScale = 1;
int Create(int ownerPid, uint64_t ownerPml4, const char* title, int w, int h, int Create(int ownerPid, uint64_t ownerPml4, const char* title, int w, int h,
uint64_t& heapNext, uint64_t& outVa) { uint64_t& heapNext, uint64_t& outVa) {
@@ -201,6 +202,28 @@ namespace WinServer {
return 0; return 0;
} }
int SetScale(int scale) {
if (scale < 0) scale = 0;
if (scale > 2) scale = 2;
g_uiScale = scale;
// Broadcast scale event to all active windows
Zenith::WinEvent ev;
memset(&ev, 0, sizeof(ev));
ev.type = 4;
ev.scale.scale = scale;
for (int i = 0; i < MaxWindows; i++) {
if (g_slots[i].used) {
SendEvent(i, &ev);
}
}
return 0;
}
int GetScale() {
return g_uiScale;
}
void CleanupProcess(int pid) { void CleanupProcess(int pid) {
for (int i = 0; i < MaxWindows; i++) { for (int i = 0; i < MaxWindows; i++) {
if (g_slots[i].used && g_slots[i].ownerPid == pid) { if (g_slots[i].used && g_slots[i].ownerPid == pid) {
+2
View File
@@ -40,5 +40,7 @@ namespace WinServer {
int Resize(int windowId, int callerPid, uint64_t ownerPml4, int newW, int newH, int Resize(int windowId, int callerPid, uint64_t ownerPml4, int newW, int newH,
uint64_t& heapNext, uint64_t& outVa); uint64_t& heapNext, uint64_t& outVa);
void CleanupProcess(int pid); void CleanupProcess(int pid);
int SetScale(int scale);
int GetScale();
} }
+19 -9
View File
@@ -57,7 +57,7 @@ BINDIR := bin
PROGRAMS := $(notdir $(wildcard src/*)) PROGRAMS := $(notdir $(wildcard src/*))
# Programs with custom Makefiles (built separately). # Programs with custom Makefiles (built separately).
CUSTOM_BUILDS := doom fetch wiki wikipedia desktop CUSTOM_BUILDS := doom fetch wiki wikipedia weather desktop
SYSTEM_PROGRAMS := $(filter-out $(CUSTOM_BUILDS),$(PROGRAMS)) SYSTEM_PROGRAMS := $(filter-out $(CUSTOM_BUILDS),$(PROGRAMS))
# Build targets: system programs go to bin/os/, games are handled separately. # Build targets: system programs go to bin/os/, games are handled separately.
@@ -82,9 +82,9 @@ CA_CERTS := $(BINDIR)/etc/ca-certificates.crt
# Home directory placeholder. # Home directory placeholder.
HOMEKEEP := $(BINDIR)/home/.keep HOMEKEEP := $(BINDIR)/home/.keep
.PHONY: all clean doom fetch wiki wikipedia desktop icons fonts bearssl libc .PHONY: all clean doom fetch wiki wikipedia weather desktop icons fonts bearssl libc tls
all: bearssl libc $(TARGETS) fetch wiki wikipedia doom desktop icons fonts $(GAME_DATA) $(MANDST) $(WWWDST) $(CA_CERTS) $(HOMEKEEP) all: bearssl libc tls $(TARGETS) fetch wiki wikipedia weather doom desktop icons fonts $(GAME_DATA) $(MANDST) $(WWWDST) $(CA_CERTS) $(HOMEKEEP)
# Build BearSSL static library. # Build BearSSL static library.
bearssl: bearssl:
@@ -94,18 +94,26 @@ bearssl:
libc: libc:
$(MAKE) -C lib/libc $(MAKE) -C lib/libc
# Build fetch via its own Makefile (depends on bearssl and libc). # Build shared TLS helper library.
fetch: bearssl libc tls: bearssl libc
$(MAKE) -C lib/tls
# Build fetch via its own Makefile (depends on bearssl, libc, and tls).
fetch: bearssl libc tls
$(MAKE) -C src/fetch $(MAKE) -C src/fetch
# Build wiki via its own Makefile (depends on bearssl and libc). # Build wiki via its own Makefile (depends on bearssl, libc, and tls).
wiki: bearssl libc wiki: bearssl libc tls
$(MAKE) -C src/wiki $(MAKE) -C src/wiki
# Build wikipedia standalone GUI client (depends on bearssl and libc). # Build wikipedia standalone GUI client (depends on bearssl, libc, and tls).
wikipedia: bearssl libc wikipedia: bearssl libc tls
$(MAKE) -C src/wikipedia $(MAKE) -C src/wikipedia
# Build weather standalone GUI client (depends on bearssl, libc, and tls).
weather: bearssl libc tls
$(MAKE) -C src/weather
# Build desktop via its own Makefile. # Build desktop via its own Makefile.
desktop: desktop:
$(MAKE) -C src/desktop $(MAKE) -C src/desktop
@@ -157,7 +165,9 @@ clean:
rm -rf $(BINDIR) obj rm -rf $(BINDIR) obj
$(MAKE) -C lib/bearssl clean $(MAKE) -C lib/bearssl clean
$(MAKE) -C lib/libc clean $(MAKE) -C lib/libc clean
$(MAKE) -C lib/tls clean
$(MAKE) -C src/fetch clean $(MAKE) -C src/fetch clean
$(MAKE) -C src/doom clean $(MAKE) -C src/doom clean
$(MAKE) -C src/desktop clean $(MAKE) -C src/desktop clean
$(MAKE) -C src/wikipedia clean $(MAKE) -C src/wikipedia clean
$(MAKE) -C src/weather clean
+4 -1
View File
@@ -75,6 +75,8 @@ namespace Zenith {
static constexpr uint64_t SYS_WINMAP = 59; static constexpr uint64_t SYS_WINMAP = 59;
static constexpr uint64_t SYS_WINSENDEVENT = 60; static constexpr uint64_t SYS_WINSENDEVENT = 60;
static constexpr uint64_t SYS_WINRESIZE = 64; static constexpr uint64_t SYS_WINRESIZE = 64;
static constexpr uint64_t SYS_WINSETSCALE = 65;
static constexpr uint64_t SYS_WINGETSCALE = 66;
// Process management syscalls // Process management syscalls
static constexpr uint64_t SYS_PROCLIST = 61; static constexpr uint64_t SYS_PROCLIST = 61;
@@ -135,12 +137,13 @@ namespace Zenith {
// Window server shared types // Window server shared types
struct WinEvent { struct WinEvent {
uint8_t type; // 0=key, 1=mouse, 2=resize, 3=close uint8_t type; // 0=key, 1=mouse, 2=resize, 3=close, 4=scale
uint8_t _pad[3]; uint8_t _pad[3];
union { union {
KeyEvent key; KeyEvent key;
struct { int32_t x, y, scroll; uint8_t buttons, prev_buttons; } mouse; struct { int32_t x, y, scroll; uint8_t buttons, prev_buttons; } mouse;
struct { int32_t w, h; } resize; struct { int32_t w, h; } resize;
struct { int32_t scale; } scale;
}; };
}; };
+2
View File
@@ -73,6 +73,8 @@ struct DesktopState {
SvgIcon icon_settings; SvgIcon icon_settings;
SvgIcon icon_reboot; SvgIcon icon_reboot;
SvgIcon icon_weather;
SvgIcon icon_doom; SvgIcon icon_doom;
SvgIcon icon_procmgr; SvgIcon icon_procmgr;
SvgIcon icon_mandelbrot; SvgIcon icon_mandelbrot;
+134 -4
View File
@@ -77,6 +77,52 @@ struct SvgGradientTable {
} }
}; };
// ---------------------------------------------------------------------------
// CSS class color table (.ClassName { color: #rrggbb; })
// Used to resolve fill:currentColor via the element's class= attribute.
// ---------------------------------------------------------------------------
static constexpr int SVG_MAX_CSS_CLASSES = 8;
struct SvgCssClass {
char name[48];
Color color;
};
struct SvgCssTable {
SvgCssClass entries[SVG_MAX_CSS_CLASSES];
int count;
void clear() { count = 0; }
void add(const char* name, Color c) {
if (count >= SVG_MAX_CSS_CLASSES) return;
int i = 0;
while (name[i] && i < 47) { entries[count].name[i] = name[i]; i++; }
entries[count].name[i] = '\0';
entries[count].color = c;
count++;
}
// Look up class by name (first token if multiple classes are listed).
// Returns true if found.
bool lookup(const char* cls, Color* out) const {
for (int i = 0; i < count; i++) {
const char* a = entries[i].name;
const char* b = cls;
bool match = true;
while (*a && *b) {
if (*a != *b) { match = false; break; }
a++; b++;
}
if (match && *a == '\0' && (*b == '\0' || *b == ' ')) {
*out = entries[i].color;
return true;
}
}
return false;
}
};
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Fixed-point number parser (NO floating point) // Fixed-point number parser (NO floating point)
// Parses strings like "3.25", "-0.5", ".1115", "16" // Parses strings like "3.25", "-0.5", ".1115", "16"
@@ -815,6 +861,52 @@ inline void svg_rasterize(const SvgEdgeList& el, uint32_t* pixels, int w, int h,
zenith::free(isect); zenith::free(isect);
} }
// ---------------------------------------------------------------------------
// Parse CSS class color mappings out of a <style> block.
// Handles: .ClassName { color: #rrggbb; }
// ---------------------------------------------------------------------------
inline void svg_parse_css_table(const char* style_data, int style_len, SvgCssTable* css) {
const char* p = style_data;
const char* end = style_data + style_len;
while (p < end && css->count < SVG_MAX_CSS_CLASSES) {
// Find next '.' that starts a class selector
while (p < end && *p != '.') ++p;
if (p >= end) break;
++p; // skip '.'
// Read class name (stops at whitespace or '{')
char name[48]; int ni = 0;
while (p < end && *p != ' ' && *p != '\t' && *p != '{' && ni < 47)
name[ni++] = *p++;
name[ni] = '\0';
// Find opening brace
while (p < end && *p != '{') ++p;
if (p >= end) break;
const char* brace_open = p++;
// Find closing brace
const char* brace_close = brace_open;
while (brace_close < end && *brace_close != '}') ++brace_close;
// Search for "color:" inside the braces (not "background-color:" etc.)
int block_len = (int)(brace_close - brace_open);
const char* cp = svg_strstr(brace_open, block_len, "color:");
if (cp) {
// Make sure it's not prefixed (e.g. "background-color:")
if (cp > brace_open && *(cp - 1) != '-') {
cp += 6;
while (cp < brace_close && (*cp == ' ' || *cp == '\t')) ++cp;
if (cp < brace_close && *cp == '#') {
Color c = svg_parse_hex_color(cp);
css->add(name, c);
}
}
}
p = brace_close + 1;
}
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Resolve a fill value that might be url(#id) against gradient table // Resolve a fill value that might be url(#id) against gradient table
// Returns: 1 = resolved, 0 = not a url() ref, -1 = fill="none" // Returns: 1 = resolved, 0 = not a url() ref, -1 = fill="none"
@@ -840,11 +932,29 @@ inline int svg_resolve_fill_value(const char* val, Color* out_color, const SvgGr
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Per-element fill color extraction // Per-element fill color extraction
// Returns: 1 = color found in out_color, 0 = use default, -1 = fill="none" // Returns: 1 = color found in out_color, 0 = use default, -1 = fill="none"
// When fill:currentColor is encountered and css != nullptr, the element's
// class= attribute is looked up to resolve the actual color.
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
inline int svg_get_element_fill(const char* elem, int elemLen, Color* out_color, inline int svg_get_element_fill(const char* elem, int elemLen, Color* out_color,
const SvgGradientTable* grads = nullptr) { const SvgGradientTable* grads = nullptr,
const SvgCssTable* css = nullptr) {
char buf[128]; char buf[128];
// Helper: try to resolve currentColor via the element's class= attribute.
// ColorScheme-Text is the "foreground" color — callers control it via fill_color,
// so we skip it here and let the fallback fill_color apply.
// Accent classes (Highlight, NeutralText, etc.) use their CSS-defined colors.
auto resolve_current_color = [&]() -> int {
if (!css || css->count == 0) return 0; // fall back to fill_color
char cls[64];
int cLen = svg_get_attr(elem, elemLen, " class", cls, sizeof(cls));
if (cLen <= 0) return 0;
if (svg_strncmp(cls, "ColorScheme-Text", 16)) return 0; // use fill_color
Color c;
if (css->lookup(cls, &c)) { *out_color = c; return 1; }
return 0;
};
// Check style="..." first (higher CSS priority) // Check style="..." first (higher CSS priority)
int sLen = svg_get_attr(elem, elemLen, " style", buf, sizeof(buf)); int sLen = svg_get_attr(elem, elemLen, " style", buf, sizeof(buf));
if (sLen > 0) { if (sLen > 0) {
@@ -857,6 +967,9 @@ inline int svg_get_element_fill(const char* elem, int elemLen, Color* out_color,
} else { } else {
fp += 5; fp += 5;
while (*fp == ' ') ++fp; while (*fp == ' ') ++fp;
// Check for currentColor before calling resolve_fill_value
if (svg_strncmp(fp, "currentColor", 12))
return resolve_current_color();
return svg_resolve_fill_value(fp, out_color, grads); return svg_resolve_fill_value(fp, out_color, grads);
} }
} }
@@ -865,6 +978,8 @@ inline int svg_get_element_fill(const char* elem, int elemLen, Color* out_color,
// Check fill="..." attribute // Check fill="..." attribute
int fLen = svg_get_attr(elem, elemLen, " fill", buf, sizeof(buf)); int fLen = svg_get_attr(elem, elemLen, " fill", buf, sizeof(buf));
if (fLen > 0) { if (fLen > 0) {
if (svg_strncmp(buf, "currentColor", 12))
return resolve_current_color();
return svg_resolve_fill_value(buf, out_color, grads); return svg_resolve_fill_value(buf, out_color, grads);
} }
@@ -1070,6 +1185,21 @@ inline SvgIcon svg_render(const char* svg_data, int svg_len, int target_w, int t
} }
} }
// Pre-parse CSS class color mappings from <style> block
SvgCssTable css;
css.clear();
{
const char* sp = svg_strstr(svg_data, svg_len, "<style");
if (sp) {
// Advance past the opening tag to the content
while (sp < svg_data + svg_len && *sp != '>') ++sp;
if (sp < svg_data + svg_len) ++sp; // skip '>'
const char* se = svg_strstr(sp, (int)(svg_data + svg_len - sp), "</style>");
if (se)
svg_parse_css_table(sp, (int)(se - sp), &css);
}
}
// Shared edge list (cleared per element for multi-color support) // Shared edge list (cleared per element for multi-color support)
SvgEdgeList el; SvgEdgeList el;
el.init(SVG_MAX_EDGES); el.init(SVG_MAX_EDGES);
@@ -1113,7 +1243,7 @@ inline SvgIcon svg_render(const char* svg_data, int svg_len, int target_w, int t
// Determine fill color for this element // Determine fill color for this element
Color elem_color = fill_color; Color elem_color = fill_color;
int fillResult = svg_get_element_fill(elem_start, elem_len, &elem_color, &grads); int fillResult = svg_get_element_fill(elem_start, elem_len, &elem_color, &grads, &css);
if (fillResult == -1) { p = elem_end; continue; } // fill="none" if (fillResult == -1) { p = elem_end; continue; } // fill="none"
int alpha = svg_get_element_opacity(elem_start, elem_len); int alpha = svg_get_element_opacity(elem_start, elem_len);
@@ -1146,7 +1276,7 @@ inline SvgIcon svg_render(const char* svg_data, int svg_len, int target_w, int t
} }
Color elem_color = fill_color; Color elem_color = fill_color;
int fillResult = svg_get_element_fill(elem_start, elem_len, &elem_color, &grads); int fillResult = svg_get_element_fill(elem_start, elem_len, &elem_color, &grads, &css);
if (fillResult == -1) { p = elem_end; continue; } if (fillResult == -1) { p = elem_end; continue; }
int alpha = svg_get_element_opacity(elem_start, elem_len); int alpha = svg_get_element_opacity(elem_start, elem_len);
@@ -1189,7 +1319,7 @@ inline SvgIcon svg_render(const char* svg_data, int svg_len, int target_w, int t
} }
Color elem_color = fill_color; Color elem_color = fill_color;
int fillResult = svg_get_element_fill(elem_start, elem_len, &elem_color, &grads); int fillResult = svg_get_element_fill(elem_start, elem_len, &elem_color, &grads, &css);
if (fillResult == -1) { p = elem_end; continue; } if (fillResult == -1) { p = elem_end; continue; }
int alpha = svg_get_element_opacity(elem_start, elem_len); int alpha = svg_get_element_opacity(elem_start, elem_len);
+2 -2
View File
@@ -50,7 +50,7 @@ struct CachedGlyph {
}; };
struct GlyphCache { struct GlyphCache {
CachedGlyph glyphs[128]; CachedGlyph glyphs[256]; // Latin-1 Supplement (U+0080..U+00FF) included
int pixel_size; int pixel_size;
float scale; float scale;
int ascent, descent, line_gap; int ascent, descent, line_gap;
@@ -127,7 +127,7 @@ struct TrueTypeFont {
} }
CachedGlyph* get_glyph(GlyphCache* gc, int codepoint) { CachedGlyph* get_glyph(GlyphCache* gc, int codepoint) {
if (codepoint < 0 || codepoint >= 128) return nullptr; if (codepoint < 0 || codepoint >= 256) return nullptr;
CachedGlyph* g = &gc->glyphs[codepoint]; CachedGlyph* g = &gc->glyphs[codepoint];
if (g->loaded) return g; if (g->loaded) return g;
+45
View File
@@ -0,0 +1,45 @@
/*
* tls.hpp
* Shared TLS helper library for ZenithOS
* Trust anchor loading, BearSSL time, TLS I/O, HTTPS fetch
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
extern "C" {
#include <bearssl.h>
}
#include <stddef.h>
#include <stdint.h>
namespace tls {
struct TrustAnchors {
br_x509_trust_anchor* anchors;
size_t count;
size_t capacity;
};
TrustAnchors load_trust_anchors();
void get_bearssl_time(uint32_t* days, uint32_t* seconds);
int tls_send_all(int fd, const unsigned char* data, size_t len);
int tls_recv_some(int fd, unsigned char* buf, size_t maxlen);
// Optional abort callback (for Ctrl+Q in terminal apps). nullptr = no abort.
using AbortCheckFn = bool (*)();
int tls_exchange(int fd, br_ssl_engine_context* eng,
const char* request, int reqLen,
char* respBuf, int respMax,
AbortCheckFn abort_check = nullptr);
// High-level: socket -> TLS setup -> exchange -> cleanup, all in one call.
int https_fetch(const char* host, uint32_t ip, uint16_t port,
const char* request, int reqLen,
const TrustAnchors& tas,
char* respBuf, int respMax,
AbortCheckFn abort_check = nullptr);
} // namespace tls
+6
View File
@@ -320,5 +320,11 @@ namespace zenith {
inline uint64_t win_resize(int id, int w, int h) { inline uint64_t win_resize(int id, int w, int h) {
return (uint64_t)syscall3(Zenith::SYS_WINRESIZE, (uint64_t)id, (uint64_t)w, (uint64_t)h); return (uint64_t)syscall3(Zenith::SYS_WINRESIZE, (uint64_t)id, (uint64_t)w, (uint64_t)h);
} }
inline int win_setscale(int scale) {
return (int)syscall1(Zenith::SYS_WINSETSCALE, (uint64_t)scale);
}
inline int win_getscale() {
return (int)syscall0(Zenith::SYS_WINGETSCALE);
}
} }
+72
View File
@@ -0,0 +1,72 @@
# Makefile for shared TLS helper library on ZenithOS
# Copyright (c) 2026 Daniel Hammer
MAKEFLAGS += -rR
.SUFFIXES:
# ---- Toolchain ----
TOOLCHAIN_PREFIX := $(shell cd ../../.. && pwd)/toolchain/local/bin/x86_64-elf-
ifneq ($(wildcard $(TOOLCHAIN_PREFIX)gcc),)
CXX := $(TOOLCHAIN_PREFIX)g++
AR := $(TOOLCHAIN_PREFIX)ar
else
CXX := g++
AR := ar
endif
# ---- Paths ----
BEARSSL := ../bearssl
LIBC_INC := ../../include/libc
PROG_INC := ../../include
OBJDIR := obj
# ---- Compiler flags ----
CXXFLAGS := \
-std=gnu++20 \
-g -O2 -pipe \
-Wall \
-Wextra \
-Wno-unused-parameter \
-nostdinc \
-ffreestanding \
-fno-stack-protector \
-fno-stack-check \
-fno-PIC \
-fno-rtti \
-fno-exceptions \
-ffunction-sections \
-fdata-sections \
-m64 \
-march=x86-64 \
-mno-80387 \
-mno-mmx \
-mno-sse \
-mno-sse2 \
-mno-red-zone \
-mcmodel=small \
-I $(PROG_INC) \
-isystem $(LIBC_INC) \
-I $(BEARSSL)/inc \
-isystem ../../../kernel/freestnd-c-hdrs/x86_64/include \
-isystem ../../../kernel/freestnd-cxx-hdrs/x86_64/include
# ---- Target ----
TARGET := libtls.a
.PHONY: all clean
all: $(TARGET)
$(TARGET): $(OBJDIR)/tls.o
$(AR) rcs $@ $^
$(OBJDIR)/tls.o: tls.cpp Makefile
@mkdir -p $(OBJDIR)
$(CXX) $(CXXFLAGS) -c $< -o $@
clean:
rm -rf $(OBJDIR) $(TARGET)
Binary file not shown.
Binary file not shown.
+279
View File
@@ -0,0 +1,279 @@
/*
* tls.cpp
* Shared TLS helper library for ZenithOS
* Extracted from fetch, wiki, wikipedia, and weather apps
* Copyright (c) 2025-2026 Daniel Hammer
*/
#include <tls/tls.hpp>
#include <zenith/syscall.h>
extern "C" {
#include <string.h>
#include <stdlib.h>
}
// ============================================================================
// File-local: PEM/DER certificate parsing helpers
// ============================================================================
namespace {
struct DerAccum { unsigned char* data; size_t len, cap; };
struct DnAccum { unsigned char* data; size_t len, cap; };
void der_append(void* ctx, const void* buf, size_t len) {
DerAccum* a = (DerAccum*)ctx;
if (a->len + len > a->cap) {
size_t nc = a->cap * 2;
if (nc < a->len + len) nc = a->len + len + 4096;
unsigned char* nb = (unsigned char*)malloc(nc);
if (!nb) return;
if (a->data) { memcpy(nb, a->data, a->len); free(a->data); }
a->data = nb; a->cap = nc;
}
memcpy(a->data + a->len, buf, len);
a->len += len;
}
void dn_append(void* ctx, const void* buf, size_t len) {
DnAccum* a = (DnAccum*)ctx;
if (a->len + len > a->cap) {
size_t nc = a->cap * 2;
if (nc < a->len + len) nc = a->len + len + 256;
unsigned char* nb = (unsigned char*)malloc(nc);
if (!nb) return;
if (a->data) { memcpy(nb, a->data, a->len); free(a->data); }
a->data = nb; a->cap = nc;
}
memcpy(a->data + a->len, buf, len);
a->len += len;
}
void ta_add(tls::TrustAnchors* tas, const br_x509_trust_anchor* ta) {
if (tas->count >= tas->capacity) {
size_t nc = tas->capacity == 0 ? 64 : tas->capacity * 2;
br_x509_trust_anchor* na = (br_x509_trust_anchor*)malloc(nc * sizeof(*na));
if (!na) return;
if (tas->anchors) { memcpy(na, tas->anchors, tas->count * sizeof(*na)); free(tas->anchors); }
tas->anchors = na; tas->capacity = nc;
}
tas->anchors[tas->count++] = *ta;
}
bool process_cert_der(tls::TrustAnchors* tas, const unsigned char* der, size_t der_len) {
static br_x509_decoder_context dc; // ~2KB+, keep off stack
DnAccum dn = {nullptr, 0, 0};
br_x509_decoder_init(&dc, dn_append, &dn);
br_x509_decoder_push(&dc, der, der_len);
br_x509_pkey* pk = br_x509_decoder_get_pkey(&dc);
if (!pk) { if (dn.data) free(dn.data); return false; }
br_x509_trust_anchor ta;
memset(&ta, 0, sizeof(ta));
ta.dn.data = dn.data; ta.dn.len = dn.len; ta.flags = 0;
if (br_x509_decoder_isCA(&dc)) ta.flags |= BR_X509_TA_CA;
switch (pk->key_type) {
case BR_KEYTYPE_RSA:
ta.pkey.key_type = BR_KEYTYPE_RSA;
ta.pkey.key.rsa.nlen = pk->key.rsa.nlen;
ta.pkey.key.rsa.n = (unsigned char*)malloc(pk->key.rsa.nlen);
if (ta.pkey.key.rsa.n) memcpy(ta.pkey.key.rsa.n, pk->key.rsa.n, pk->key.rsa.nlen);
ta.pkey.key.rsa.elen = pk->key.rsa.elen;
ta.pkey.key.rsa.e = (unsigned char*)malloc(pk->key.rsa.elen);
if (ta.pkey.key.rsa.e) memcpy(ta.pkey.key.rsa.e, pk->key.rsa.e, pk->key.rsa.elen);
break;
case BR_KEYTYPE_EC:
ta.pkey.key_type = BR_KEYTYPE_EC;
ta.pkey.key.ec.curve = pk->key.ec.curve;
ta.pkey.key.ec.qlen = pk->key.ec.qlen;
ta.pkey.key.ec.q = (unsigned char*)malloc(pk->key.ec.qlen);
if (ta.pkey.key.ec.q) memcpy(ta.pkey.key.ec.q, pk->key.ec.q, pk->key.ec.qlen);
break;
default:
if (dn.data) free(dn.data);
return false;
}
ta_add(tas, &ta);
return true;
}
} // anonymous namespace
// ============================================================================
// Exported functions
// ============================================================================
namespace tls {
TrustAnchors load_trust_anchors() {
TrustAnchors tas = {nullptr, 0, 0};
int fh = zenith::open("0:/etc/ca-certificates.crt");
if (fh < 0) return tas;
uint64_t fsize = zenith::getsize(fh);
if (fsize == 0 || fsize > 512 * 1024) { zenith::close(fh); return tas; }
unsigned char* pem = (unsigned char*)malloc(fsize + 1);
if (!pem) { zenith::close(fh); return tas; }
zenith::read(fh, pem, 0, fsize);
zenith::close(fh);
pem[fsize] = 0;
static br_pem_decoder_context pc; // keep off stack
br_pem_decoder_init(&pc);
DerAccum der = {nullptr, 0, 0};
bool inCert = false;
size_t offset = 0;
while (offset < fsize) {
size_t pushed = br_pem_decoder_push(&pc, pem + offset, fsize - offset);
offset += pushed;
int ev = br_pem_decoder_event(&pc);
if (ev == BR_PEM_BEGIN_OBJ) {
inCert = (strcmp(br_pem_decoder_name(&pc), "CERTIFICATE") == 0);
br_pem_decoder_setdest(&pc, inCert ? der_append : nullptr, inCert ? &der : nullptr);
if (inCert) der.len = 0;
} else if (ev == BR_PEM_END_OBJ) {
if (inCert && der.len > 0) process_cert_der(&tas, der.data, der.len);
inCert = false;
} else if (ev == BR_PEM_ERROR) {
break;
}
}
if (der.data) free(der.data);
free(pem);
return tas;
}
void get_bearssl_time(uint32_t* days, uint32_t* seconds) {
Zenith::DateTime dt;
zenith::gettime(&dt);
int y = dt.Year, m = dt.Month, d = dt.Day;
uint32_t total = 365u * (uint32_t)y
+ (uint32_t)(y/4) - (uint32_t)(y/100) + (uint32_t)(y/400);
const int md[] = {0,31,28,31,30,31,30,31,31,30,31,30,31};
for (int mo = 1; mo < m && mo <= 12; mo++) total += md[mo];
if (y%4==0 && (y%100!=0 || y%400==0) && m > 2) total++;
total += d - 1;
*days = total;
*seconds = (uint32_t)(dt.Hour*3600 + dt.Minute*60 + dt.Second);
}
int tls_send_all(int fd, const unsigned char* data, size_t len) {
size_t sent = 0;
uint64_t deadline = zenith::get_milliseconds() + 15000;
while (sent < len) {
int r = zenith::send(fd, data + sent, (uint32_t)(len - sent));
if (r > 0) { sent += r; deadline = zenith::get_milliseconds() + 15000; }
else if (r < 0) return -1;
else { if (zenith::get_milliseconds() >= deadline) return -1; zenith::sleep_ms(1); }
}
return (int)sent;
}
int tls_recv_some(int fd, unsigned char* buf, size_t maxlen) {
uint64_t deadline = zenith::get_milliseconds() + 15000;
while (true) {
int r = zenith::recv(fd, buf, (uint32_t)maxlen);
if (r > 0) return r;
if (r < 0) return -1;
if (zenith::get_milliseconds() >= deadline) return -1;
zenith::sleep_ms(1);
}
}
int tls_exchange(int fd, br_ssl_engine_context* eng,
const char* request, int reqLen,
char* respBuf, int respMax,
AbortCheckFn abort_check) {
bool requestSent = false;
int respLen = 0;
uint64_t deadline = zenith::get_milliseconds() + 30000;
while (true) {
unsigned state = br_ssl_engine_current_state(eng);
if (state & BR_SSL_CLOSED) {
int err = br_ssl_engine_last_error(eng);
if (err != BR_ERR_OK && err != BR_ERR_IO && respLen == 0) return -1;
return respLen;
}
if (abort_check && abort_check()) {
br_ssl_engine_close(eng);
return respLen > 0 ? respLen : -1;
}
if (state & BR_SSL_SENDREC) {
size_t len; unsigned char* buf = br_ssl_engine_sendrec_buf(eng, &len);
int sent = tls_send_all(fd, buf, len);
if (sent < 0) { br_ssl_engine_close(eng); return respLen > 0 ? respLen : -1; }
br_ssl_engine_sendrec_ack(eng, len);
deadline = zenith::get_milliseconds() + 30000; continue;
}
if (state & BR_SSL_RECVAPP) {
size_t len; unsigned char* buf = br_ssl_engine_recvapp_buf(eng, &len);
size_t toCopy = len;
if (respLen + (int)toCopy > respMax - 1) toCopy = respMax - 1 - respLen;
if (toCopy > 0) { memcpy(respBuf + respLen, buf, toCopy); respLen += toCopy; }
br_ssl_engine_recvapp_ack(eng, len);
deadline = zenith::get_milliseconds() + 30000; continue;
}
if ((state & BR_SSL_SENDAPP) && !requestSent) {
size_t len; unsigned char* buf = br_ssl_engine_sendapp_buf(eng, &len);
size_t toWrite = (size_t)reqLen;
if (toWrite > len) toWrite = len;
memcpy(buf, request, toWrite);
br_ssl_engine_sendapp_ack(eng, toWrite);
br_ssl_engine_flush(eng, 0);
requestSent = true;
deadline = zenith::get_milliseconds() + 30000; continue;
}
if (state & BR_SSL_RECVREC) {
size_t len; unsigned char* buf = br_ssl_engine_recvrec_buf(eng, &len);
int got = tls_recv_some(fd, buf, len);
if (got < 0) { br_ssl_engine_close(eng); return respLen > 0 ? respLen : -1; }
br_ssl_engine_recvrec_ack(eng, got);
deadline = zenith::get_milliseconds() + 30000; continue;
}
if (zenith::get_milliseconds() >= deadline) return respLen > 0 ? respLen : -1;
zenith::sleep_ms(1);
}
}
int https_fetch(const char* host, uint32_t ip, uint16_t port,
const char* request, int reqLen,
const TrustAnchors& tas,
char* respBuf, int respMax,
AbortCheckFn abort_check) {
int fd = zenith::socket(Zenith::SOCK_TCP);
if (fd < 0) return -1;
if (zenith::connect(fd, ip, port) < 0) { zenith::closesocket(fd); return -1; }
br_ssl_client_context* cc = (br_ssl_client_context*)malloc(sizeof(*cc));
br_x509_minimal_context* xc = (br_x509_minimal_context*)malloc(sizeof(*xc));
void* iobuf = malloc(BR_SSL_BUFSIZE_BIDI);
if (!cc || !xc || !iobuf) {
free(cc); free(xc); free(iobuf);
zenith::closesocket(fd); return -1;
}
br_ssl_client_init_full(cc, xc, tas.anchors, tas.count);
uint32_t days, secs;
get_bearssl_time(&days, &secs);
br_x509_minimal_set_time(xc, days, secs);
unsigned char seed[32];
zenith::getrandom(seed, sizeof(seed));
br_ssl_engine_set_buffer(&cc->eng, iobuf, BR_SSL_BUFSIZE_BIDI, 1);
br_ssl_engine_inject_entropy(&cc->eng, seed, sizeof(seed));
if (!br_ssl_client_reset(cc, host, 0)) {
zenith::closesocket(fd); free(cc); free(xc); free(iobuf); return -1;
}
int respLen = tls_exchange(fd, &cc->eng, request, reqLen, respBuf, respMax, abort_check);
zenith::closesocket(fd);
free(cc); free(xc); free(iobuf);
return respLen;
}
} // namespace tls
+1 -1
View File
@@ -61,7 +61,7 @@ LDFLAGS := \
# ---- C++ source files ---- # ---- C++ source files ----
SRCS := main.cpp font_data.cpp stb_truetype_impl.cpp app_terminal.cpp app_filemanager.cpp app_sysinfo.cpp app_calculator.cpp app_texteditor.cpp app_klog.cpp app_wiki.cpp app_procmgr.cpp app_mandelbrot.cpp app_devexplorer.cpp app_settings.cpp app_doom.cpp SRCS := main.cpp font_data.cpp stb_truetype_impl.cpp app_terminal.cpp app_filemanager.cpp app_sysinfo.cpp app_calculator.cpp app_texteditor.cpp app_klog.cpp app_wiki.cpp app_weather.cpp app_procmgr.cpp app_mandelbrot.cpp app_devexplorer.cpp app_settings.cpp app_doom.cpp
OBJS := $(addprefix $(OBJDIR)/,$(SRCS:.cpp=.o)) OBJS := $(addprefix $(OBJDIR)/,$(SRCS:.cpp=.o))
# ---- Target ---- # ---- Target ----
+3
View File
@@ -458,18 +458,21 @@ static void settings_on_mouse(Window* win, MouseEvent& ev) {
if (mx >= bx && mx < bx + sbw && cy >= y && cy < y + btn_h) { if (mx >= bx && mx < bx + sbw && cy >= y && cy < y + btn_h) {
s.ui_scale = 0; s.ui_scale = 0;
apply_ui_scale(0); apply_ui_scale(0);
zenith::win_setscale(0);
return; return;
} }
// Default // Default
if (mx >= bx + sbw + 8 && mx < bx + sbw * 2 + 8 && cy >= y && cy < y + btn_h) { if (mx >= bx + sbw + 8 && mx < bx + sbw * 2 + 8 && cy >= y && cy < y + btn_h) {
s.ui_scale = 1; s.ui_scale = 1;
apply_ui_scale(1); apply_ui_scale(1);
zenith::win_setscale(1);
return; return;
} }
// Large // Large
if (mx >= bx + (sbw + 8) * 2 && mx < bx + sbw * 3 + 16 && cy >= y && cy < y + btn_h) { if (mx >= bx + (sbw + 8) * 2 && mx < bx + sbw * 3 + 16 && cy >= y && cy < y + btn_h) {
s.ui_scale = 2; s.ui_scale = 2;
apply_ui_scale(2); apply_ui_scale(2);
zenith::win_setscale(2);
return; return;
} }
} }
+13
View File
@@ -0,0 +1,13 @@
/*
* app_weather.cpp
* ZenithOS Desktop - Weather app launcher
* Spawns weather.elf as a standalone Window Server process
* Copyright (c) 2026 Daniel Hammer
*/
#include "apps_common.hpp"
void open_weather(DesktopState* ds) {
(void)ds;
zenith::spawn("0:/os/weather.elf");
}
+1
View File
@@ -167,6 +167,7 @@ void open_texteditor(DesktopState* ds);
void open_texteditor_with_file(DesktopState* ds, const char* path); void open_texteditor_with_file(DesktopState* ds, const char* path);
void open_klog(DesktopState* ds); void open_klog(DesktopState* ds);
void open_wiki(DesktopState* ds); void open_wiki(DesktopState* ds);
void open_weather(DesktopState* ds);
void open_procmgr(DesktopState* ds); void open_procmgr(DesktopState* ds);
void open_mandelbrot(DesktopState* ds); void open_mandelbrot(DesktopState* ds);
void open_devexplorer(DesktopState* ds); void open_devexplorer(DesktopState* ds);
+9 -3
View File
@@ -61,6 +61,8 @@ void gui::desktop_init(DesktopState* ds) {
ds->icon_settings = svg_load("0:/icons/help-about.svg", 20, 20, defColor); ds->icon_settings = svg_load("0:/icons/help-about.svg", 20, 20, defColor);
ds->icon_reboot = svg_load("0:/icons/system-reboot.svg", 20, 20, defColor); ds->icon_reboot = svg_load("0:/icons/system-reboot.svg", 20, 20, defColor);
ds->icon_weather = svg_load("0:/icons/weather-widget.svg", 20, 20, defColor);
ds->icon_doom = svg_load("0:/icons/doom.svg", 20, 20, defColor); ds->icon_doom = svg_load("0:/icons/doom.svg", 20, 20, defColor);
ds->icon_procmgr = svg_load("0:/icons/system-monitor.svg", 20, 20, defColor); ds->icon_procmgr = svg_load("0:/icons/system-monitor.svg", 20, 20, defColor);
ds->icon_mandelbrot = svg_load("0:/icons/applications-science.svg", 20, 20, defColor); ds->icon_mandelbrot = svg_load("0:/icons/applications-science.svg", 20, 20, defColor);
@@ -76,6 +78,7 @@ void gui::desktop_init(DesktopState* ds) {
ds->settings.show_shadows = true; ds->settings.show_shadows = true;
ds->settings.clock_24h = true; ds->settings.clock_24h = true;
ds->settings.ui_scale = 1; ds->settings.ui_scale = 1;
zenith::win_setscale(1);
ds->ctx_menu_open = false; ds->ctx_menu_open = false;
ds->ctx_menu_x = 0; ds->ctx_menu_x = 0;
@@ -421,7 +424,7 @@ struct MenuRow {
int app_id; // -1 for category headers / dividers int app_id; // -1 for category headers / dividers
}; };
static constexpr int MENU_ROW_COUNT = 18; static constexpr int MENU_ROW_COUNT = 19;
static const MenuRow menu_rows[MENU_ROW_COUNT] = { static const MenuRow menu_rows[MENU_ROW_COUNT] = {
{ true, "Applications", -1 }, { true, "Applications", -1 },
{ false, "Terminal", 0 }, { false, "Terminal", 0 },
@@ -430,6 +433,7 @@ static const MenuRow menu_rows[MENU_ROW_COUNT] = {
{ false, "Calculator", 3 }, { false, "Calculator", 3 },
{ true, "Internet", -1 }, { true, "Internet", -1 },
{ false, "Wikipedia", 9 }, { false, "Wikipedia", 9 },
{ false, "Weather", 13 },
{ true, "System", -1 }, { true, "System", -1 },
{ false, "System Info", 2 }, { false, "System Info", 2 },
{ false, "Kernel Log", 5 }, { false, "Kernel Log", 5 },
@@ -470,7 +474,7 @@ static void desktop_draw_app_menu(DesktopState* ds) {
draw_rect(fb, menu_x, menu_y, MENU_W, menu_h, colors::BORDER); draw_rect(fb, menu_x, menu_y, MENU_W, menu_h, colors::BORDER);
// Icon lookup by app_id // Icon lookup by app_id
SvgIcon* icons[13] = { SvgIcon* icons[14] = {
&ds->icon_terminal, // 0 &ds->icon_terminal, // 0
&ds->icon_filemanager, // 1 &ds->icon_filemanager, // 1
&ds->icon_sysinfo, // 2 &ds->icon_sysinfo, // 2
@@ -484,6 +488,7 @@ static void desktop_draw_app_menu(DesktopState* ds) {
&ds->icon_doom, // 10 &ds->icon_doom, // 10
&ds->icon_settings, // 11 &ds->icon_settings, // 11
&ds->icon_reboot, // 12 &ds->icon_reboot, // 12
&ds->icon_weather, // 13
}; };
int mx = ds->mouse.x; int mx = ds->mouse.x;
@@ -519,7 +524,7 @@ static void desktop_draw_app_menu(DesktopState* ds) {
// Icon // Icon
int icon_x = item_rect.x + 8; int icon_x = item_rect.x + 8;
int icon_y = item_rect.y + (row_h - 20) / 2; int icon_y = item_rect.y + (row_h - 20) / 2;
if (row.app_id >= 0 && row.app_id < 13) { if (row.app_id >= 0 && row.app_id < 14) {
SvgIcon* icon = icons[row.app_id]; SvgIcon* icon = icons[row.app_id];
if (icon && icon->pixels) { if (icon && icon->pixels) {
fb.blit_alpha(icon_x, icon_y, icon->width, icon->height, icon->pixels); fb.blit_alpha(icon_x, icon_y, icon->width, icon->height, icon->pixels);
@@ -1099,6 +1104,7 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
case 10: open_doom(ds); break; case 10: open_doom(ds); break;
case 11: open_settings(ds); break; case 11: open_settings(ds); break;
case 12: open_reboot_dialog(ds); break; case 12: open_reboot_dialog(ds); break;
case 13: open_weather(ds); break;
} }
ds->app_menu_open = false; ds->app_menu_open = false;
} }
+2 -1
View File
@@ -16,6 +16,7 @@ endif
# ---- Paths ---- # ---- Paths ----
BEARSSL := ../../lib/bearssl BEARSSL := ../../lib/bearssl
TLS_LIB := ../../lib/tls
LIBC_LIB := ../../lib/libc LIBC_LIB := ../../lib/libc
LIBC_INC := ../../include/libc LIBC_INC := ../../include/libc
PROG_INC := ../../include PROG_INC := ../../include
@@ -67,7 +68,7 @@ LDFLAGS := \
# ---- Libraries ---- # ---- Libraries ----
LIBS := $(BEARSSL)/libbearssl.a $(LIBC_LIB)/liblibc.a LIBS := $(TLS_LIB)/libtls.a $(BEARSSL)/libbearssl.a $(LIBC_LIB)/liblibc.a
# ---- Target ---- # ---- Target ----
+29 -423
View File
@@ -8,9 +8,9 @@
#include <zenith/syscall.h> #include <zenith/syscall.h>
#include <zenith/string.h> #include <zenith/string.h>
#include <tls/tls.hpp>
extern "C" { extern "C" {
#include <bearssl.h>
#include <string.h> #include <string.h>
#include <stdlib.h> #include <stdlib.h>
#include <stdio.h> #include <stdio.h>
@@ -156,366 +156,15 @@ static void parse_status_text(const char* buf, int len, char* out, int outMax) {
out[j] = '\0'; out[j] = '\0';
} }
// ---- Trust anchor loading ---- // ---- Keyboard abort check for TLS ----
struct TrustAnchors { static bool check_keyboard_abort() {
br_x509_trust_anchor* anchors;
size_t count;
size_t capacity;
};
// Accumulate DER-decoded certificate data
struct DerAccum {
unsigned char* data;
size_t len;
size_t cap;
};
static void der_append(void* ctx, const void* buf, size_t len) {
DerAccum* a = (DerAccum*)ctx;
if (a->len + len > a->cap) {
size_t newcap = a->cap * 2;
if (newcap < a->len + len) newcap = a->len + len + 4096;
unsigned char* nb = (unsigned char*)malloc(newcap);
if (!nb) return;
if (a->data) {
memcpy(nb, a->data, a->len);
free(a->data);
}
a->data = nb;
a->cap = newcap;
}
memcpy(a->data + a->len, buf, len);
a->len += len;
}
// Accumulate DN data from X.509 decoder
struct DnAccum {
unsigned char* data;
size_t len;
size_t cap;
};
static void dn_append(void* ctx, const void* buf, size_t len) {
DnAccum* a = (DnAccum*)ctx;
if (a->len + len > a->cap) {
size_t newcap = a->cap * 2;
if (newcap < a->len + len) newcap = a->len + len + 256;
unsigned char* nb = (unsigned char*)malloc(newcap);
if (!nb) return;
if (a->data) {
memcpy(nb, a->data, a->len);
free(a->data);
}
a->data = nb;
a->cap = newcap;
}
memcpy(a->data + a->len, buf, len);
a->len += len;
}
static void ta_add(TrustAnchors* tas, const br_x509_trust_anchor* ta) {
if (tas->count >= tas->capacity) {
size_t newcap = tas->capacity == 0 ? 64 : tas->capacity * 2;
br_x509_trust_anchor* na = (br_x509_trust_anchor*)malloc(
newcap * sizeof(br_x509_trust_anchor));
if (!na) return;
if (tas->anchors) {
memcpy(na, tas->anchors, tas->count * sizeof(br_x509_trust_anchor));
free(tas->anchors);
}
tas->anchors = na;
tas->capacity = newcap;
}
tas->anchors[tas->count++] = *ta;
}
// Process a single DER certificate into a trust anchor
static bool process_cert_der(TrustAnchors* tas, const unsigned char* der, size_t der_len) {
br_x509_decoder_context dc;
DnAccum dn = {nullptr, 0, 0};
br_x509_decoder_init(&dc, dn_append, &dn);
br_x509_decoder_push(&dc, der, der_len);
br_x509_pkey* pk = br_x509_decoder_get_pkey(&dc);
if (!pk) {
if (dn.data) free(dn.data);
return false;
}
br_x509_trust_anchor ta;
memset(&ta, 0, sizeof(ta));
// Copy DN
ta.dn.data = dn.data;
ta.dn.len = dn.len;
ta.flags = 0;
if (br_x509_decoder_isCA(&dc))
ta.flags |= BR_X509_TA_CA;
// Deep-copy public key data
switch (pk->key_type) {
case BR_KEYTYPE_RSA: {
ta.pkey.key_type = BR_KEYTYPE_RSA;
ta.pkey.key.rsa.nlen = pk->key.rsa.nlen;
ta.pkey.key.rsa.n = (unsigned char*)malloc(pk->key.rsa.nlen);
if (ta.pkey.key.rsa.n) memcpy(ta.pkey.key.rsa.n, pk->key.rsa.n, pk->key.rsa.nlen);
ta.pkey.key.rsa.elen = pk->key.rsa.elen;
ta.pkey.key.rsa.e = (unsigned char*)malloc(pk->key.rsa.elen);
if (ta.pkey.key.rsa.e) memcpy(ta.pkey.key.rsa.e, pk->key.rsa.e, pk->key.rsa.elen);
break;
}
case BR_KEYTYPE_EC: {
ta.pkey.key_type = BR_KEYTYPE_EC;
ta.pkey.key.ec.curve = pk->key.ec.curve;
ta.pkey.key.ec.qlen = pk->key.ec.qlen;
ta.pkey.key.ec.q = (unsigned char*)malloc(pk->key.ec.qlen);
if (ta.pkey.key.ec.q) memcpy(ta.pkey.key.ec.q, pk->key.ec.q, pk->key.ec.qlen);
break;
}
default:
if (dn.data) free(dn.data);
return false;
}
ta_add(tas, &ta);
return true;
}
static TrustAnchors load_trust_anchors(bool verbose) {
TrustAnchors tas = {nullptr, 0, 0};
int fh = zenith::open("0:/etc/ca-certificates.crt");
if (fh < 0) {
printf("Warning: could not open CA certificate bundle\n");
return tas;
}
uint64_t fsize = zenith::getsize(fh);
if (fsize == 0 || fsize > 512 * 1024) {
zenith::close(fh);
printf("Warning: CA cert file invalid size\n");
return tas;
}
unsigned char* pem = (unsigned char*)malloc(fsize + 1);
if (!pem) {
zenith::close(fh);
printf("Warning: out of memory loading CA certs\n");
return tas;
}
zenith::read(fh, pem, 0, fsize);
zenith::close(fh);
pem[fsize] = 0;
// Parse PEM -> DER certificates -> trust anchors
br_pem_decoder_context pc;
br_pem_decoder_init(&pc);
DerAccum der = {nullptr, 0, 0};
bool inCert = false;
size_t offset = 0;
while (offset < fsize) {
size_t pushed = br_pem_decoder_push(&pc, pem + offset, fsize - offset);
offset += pushed;
int event = br_pem_decoder_event(&pc);
if (event == BR_PEM_BEGIN_OBJ) {
const char* name = br_pem_decoder_name(&pc);
inCert = (strcmp(name, "CERTIFICATE") == 0);
if (inCert) {
der.len = 0;
br_pem_decoder_setdest(&pc, der_append, &der);
} else {
br_pem_decoder_setdest(&pc, nullptr, nullptr);
}
} else if (event == BR_PEM_END_OBJ) {
if (inCert && der.len > 0) {
process_cert_der(&tas, der.data, der.len);
}
inCert = false;
} else if (event == BR_PEM_ERROR) {
break;
}
}
if (der.data) free(der.data);
free(pem);
if (verbose) {
char msg[64];
snprintf(msg, sizeof(msg), "Loaded %u trust anchors\n", (unsigned)tas.count);
zenith::print(msg);
}
return tas;
}
// ---- Time conversion for certificate validation ----
// Returns days since January 1, 0 AD (Gregorian) and seconds within the day.
// This is the format BearSSL's br_x509_minimal_set_time() expects.
static void get_bearssl_time(uint32_t* days, uint32_t* seconds) {
Zenith::DateTime dt;
zenith::gettime(&dt);
int y = dt.Year;
int m = dt.Month;
int d = dt.Day;
// Days from year 0 to start of year y (Gregorian proleptic calendar)
uint32_t total_days = 365 * (uint32_t)y
+ (uint32_t)(y / 4)
- (uint32_t)(y / 100)
+ (uint32_t)(y / 400);
// Add days for completed months in this year
const int mdays[] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
for (int mo = 1; mo < m && mo <= 12; mo++) {
total_days += mdays[mo];
}
// Leap day for this year if we're past February
bool leap = (y % 4 == 0 && (y % 100 != 0 || y % 400 == 0));
if (leap && m > 2) total_days++;
total_days += d - 1;
*days = total_days;
*seconds = (uint32_t)(dt.Hour * 3600 + dt.Minute * 60 + dt.Second);
}
// ---- TLS I/O loop ----
static int tls_send_all(int fd, const unsigned char* data, size_t len) {
size_t sent = 0;
uint64_t deadline = zenith::get_milliseconds() + 15000;
while (sent < len) {
int r = zenith::send(fd, data + sent, (uint32_t)(len - sent));
if (r > 0) {
sent += r;
deadline = zenith::get_milliseconds() + 15000;
} else if (r < 0) {
return -1;
} else {
if (zenith::get_milliseconds() >= deadline) return -1;
zenith::sleep_ms(1);
}
}
return (int)sent;
}
static int tls_recv_some(int fd, unsigned char* buf, size_t maxlen) {
uint64_t deadline = zenith::get_milliseconds() + 15000;
while (true) {
int r = zenith::recv(fd, buf, (uint32_t)maxlen);
if (r > 0) return r;
if (r < 0) return -1;
if (zenith::get_milliseconds() >= deadline) return -1;
zenith::sleep_ms(1);
}
}
// Run BearSSL I/O loop until handshake + app data exchange is done
// Returns: number of response bytes in respBuf, or -1 on error
static int tls_exchange(int fd, br_ssl_engine_context* eng,
const char* request, int reqLen,
char* respBuf, int respMax, bool verbose) {
bool requestSent = false;
int respLen = 0;
uint64_t deadline = zenith::get_milliseconds() + 30000;
while (true) {
unsigned state = br_ssl_engine_current_state(eng);
if (state & BR_SSL_CLOSED) {
int err = br_ssl_engine_last_error(eng);
if (err != BR_ERR_OK && err != BR_ERR_IO) {
char msg[64];
snprintf(msg, sizeof(msg), "TLS error: %d\n", err);
zenith::print(msg);
if (respLen == 0) return -1;
}
return respLen;
}
// Check for keyboard abort
if (zenith::is_key_available()) { if (zenith::is_key_available()) {
Zenith::KeyEvent ev; Zenith::KeyEvent ev;
zenith::getkey(&ev); zenith::getkey(&ev);
if (ev.pressed && ev.ctrl && ev.ascii == 'q') { if (ev.pressed && ev.ctrl && ev.ascii == 'q') return true;
br_ssl_engine_close(eng);
return respLen > 0 ? respLen : -1;
}
}
// Send record data to network
if (state & BR_SSL_SENDREC) {
size_t len;
unsigned char* buf = br_ssl_engine_sendrec_buf(eng, &len);
int sent = tls_send_all(fd, buf, len);
if (sent < 0) {
br_ssl_engine_close(eng);
return respLen > 0 ? respLen : -1;
}
br_ssl_engine_sendrec_ack(eng, len);
deadline = zenith::get_milliseconds() + 30000;
continue;
}
// Read application data from TLS
if (state & BR_SSL_RECVAPP) {
size_t len;
unsigned char* buf = br_ssl_engine_recvapp_buf(eng, &len);
size_t toCopy = len;
if (respLen + (int)toCopy > respMax - 1)
toCopy = respMax - 1 - respLen;
if (toCopy > 0) {
memcpy(respBuf + respLen, buf, toCopy);
respLen += toCopy;
}
br_ssl_engine_recvapp_ack(eng, len);
deadline = zenith::get_milliseconds() + 30000;
continue;
}
// Send application data (HTTP request) into TLS
if ((state & BR_SSL_SENDAPP) && !requestSent) {
size_t len;
unsigned char* buf = br_ssl_engine_sendapp_buf(eng, &len);
size_t toWrite = (size_t)reqLen;
if (toWrite > len) toWrite = len;
memcpy(buf, request, toWrite);
br_ssl_engine_sendapp_ack(eng, toWrite);
br_ssl_engine_flush(eng, 0);
requestSent = true;
deadline = zenith::get_milliseconds() + 30000;
continue;
}
// Receive record data from network
if (state & BR_SSL_RECVREC) {
size_t len;
unsigned char* buf = br_ssl_engine_recvrec_buf(eng, &len);
int got = tls_recv_some(fd, buf, len);
if (got < 0) {
br_ssl_engine_close(eng);
return respLen > 0 ? respLen : -1;
}
br_ssl_engine_recvrec_ack(eng, got);
deadline = zenith::get_milliseconds() + 30000;
continue;
}
// Nothing actionable — wait
if (zenith::get_milliseconds() >= deadline) {
return respLen > 0 ? respLen : -1;
}
zenith::sleep_ms(1);
} }
return false;
} }
// ---- Plain HTTP exchange (no TLS) ---- // ---- Plain HTTP exchange (no TLS) ----
@@ -706,19 +355,6 @@ extern "C" void _start() {
zenith::print(msg); zenith::print(msg);
} }
// Create and connect socket
int fd = zenith::socket(Zenith::SOCK_TCP);
if (fd < 0) {
zenith::print("Error: failed to create socket\n");
zenith::exit(1);
}
if (zenith::connect(fd, serverIp, port) < 0) {
zenith::print("Error: connection failed\n");
zenith::closesocket(fd);
zenith::exit(1);
}
// Build HTTP request // Build HTTP request
char request[1024]; char request[1024];
int reqLen = snprintf(request, sizeof(request), int reqLen = snprintf(request, sizeof(request),
@@ -740,7 +376,6 @@ extern "C" void _start() {
char* respBuf = (char*)malloc(RESP_MAX); char* respBuf = (char*)malloc(RESP_MAX);
if (!respBuf) { if (!respBuf) {
zenith::print("Error: out of memory\n"); zenith::print("Error: out of memory\n");
zenith::closesocket(fd);
zenith::exit(1); zenith::exit(1);
} }
@@ -748,35 +383,21 @@ extern "C" void _start() {
if (useHttps) { if (useHttps) {
// ---- TLS handshake and exchange ---- // ---- TLS handshake and exchange ----
tls::TrustAnchors tas = tls::load_trust_anchors();
// Load trust anchors if (verbose) {
TrustAnchors tas = load_trust_anchors(verbose); char msg[64];
snprintf(msg, sizeof(msg), "Loaded %u trust anchors\n", (unsigned)tas.count);
zenith::print(msg);
}
if (tas.count == 0) { if (tas.count == 0) {
zenith::print("Error: no trust anchors loaded\n"); zenith::print("Error: no trust anchors loaded\n");
free(respBuf); free(respBuf);
zenith::closesocket(fd);
zenith::exit(1); zenith::exit(1);
} }
// Initialize BearSSL client
// Allocate contexts on heap to avoid stack overflow
br_ssl_client_context* cc = (br_ssl_client_context*)malloc(sizeof(br_ssl_client_context));
br_x509_minimal_context* xc = (br_x509_minimal_context*)malloc(sizeof(br_x509_minimal_context));
if (!cc || !xc) {
zenith::print("Error: out of memory for TLS context\n");
free(respBuf);
zenith::closesocket(fd);
zenith::exit(1);
}
br_ssl_client_init_full(cc, xc, tas.anchors, tas.count);
// Set time for certificate validation
uint32_t days, secs;
get_bearssl_time(&days, &secs);
br_x509_minimal_set_time(xc, days, secs);
if (verbose) { if (verbose) {
uint32_t days, secs;
tls::get_bearssl_time(&days, &secs);
Zenith::DateTime dt; Zenith::DateTime dt;
zenith::gettime(&dt); zenith::gettime(&dt);
char tmsg[128]; char tmsg[128];
@@ -785,54 +406,39 @@ extern "C" void _start() {
(unsigned)dt.Hour, (unsigned)dt.Minute, (unsigned)dt.Second, (unsigned)dt.Hour, (unsigned)dt.Minute, (unsigned)dt.Second,
(unsigned)days, (unsigned)secs); (unsigned)days, (unsigned)secs);
zenith::print(tmsg); zenith::print(tmsg);
}
// Seed the PRNG with RDRAND entropy
unsigned char seed[32];
zenith::getrandom(seed, sizeof(seed));
br_ssl_engine_set_buffer(&cc->eng, malloc(BR_SSL_BUFSIZE_BIDI),
BR_SSL_BUFSIZE_BIDI, 1);
// Inject entropy
br_ssl_engine_inject_entropy(&cc->eng, seed, sizeof(seed));
// Reset client with server name for SNI
if (!br_ssl_client_reset(cc, hostStr, 0)) {
int err = br_ssl_engine_last_error(&cc->eng);
char msg[64];
snprintf(msg, sizeof(msg), "Error: TLS reset failed (err=%d)\n", err);
zenith::print(msg);
free(respBuf);
zenith::closesocket(fd);
zenith::exit(1);
}
if (verbose) {
zenith::print("TLS handshake...\n"); zenith::print("TLS handshake...\n");
} }
// Run TLS I/O loop respLen = tls::https_fetch(hostStr, serverIp, port,
respLen = tls_exchange(fd, &cc->eng, request, reqLen, request, reqLen, tas,
respBuf, RESP_MAX, verbose); respBuf, RESP_MAX, check_keyboard_abort);
if (verbose && respLen > 0) { if (verbose && respLen > 0) {
zenith::print("TLS connection established\n"); zenith::print("TLS connection established\n");
} }
// Cleanup (we don't bother freeing everything since we're exiting)
} else { } else {
// ---- Plain HTTP ---- // ---- Plain HTTP ----
int fd = zenith::socket(Zenith::SOCK_TCP);
if (fd < 0) {
zenith::print("Error: failed to create socket\n");
zenith::exit(1);
}
if (zenith::connect(fd, serverIp, port) < 0) {
zenith::print("Error: connection failed\n");
zenith::closesocket(fd);
zenith::exit(1);
}
respLen = plain_http_exchange(fd, request, reqLen, respBuf, RESP_MAX); respLen = plain_http_exchange(fd, request, reqLen, respBuf, RESP_MAX);
zenith::closesocket(fd);
if (respLen == -2) { if (respLen == -2) {
zenith::print("\nAborted.\n"); zenith::print("\nAborted.\n");
zenith::closesocket(fd);
zenith::exit(0); zenith::exit(0);
} }
} }
zenith::closesocket(fd);
if (respLen <= 0) { if (respLen <= 0) {
zenith::print("Error: no response received\n"); zenith::print("Error: no response received\n");
zenith::exit(1); zenith::exit(1);
+94
View File
@@ -0,0 +1,94 @@
# Makefile for weather (standalone Weather GUI client) on ZenithOS
# Copyright (c) 2026 Daniel Hammer
MAKEFLAGS += -rR
.SUFFIXES:
# ---- Toolchain ----
TOOLCHAIN_PREFIX := $(shell cd ../../.. && pwd)/toolchain/local/bin/x86_64-elf-
ifneq ($(wildcard $(TOOLCHAIN_PREFIX)gcc),)
CXX := $(TOOLCHAIN_PREFIX)g++
else
CXX := g++
endif
# ---- Paths ----
BEARSSL := ../../lib/bearssl
TLS_LIB := ../../lib/tls
LIBC_LIB := ../../lib/libc
LIBC_INC := ../../include/libc
PROG_INC := ../../include
LINK_LD := ../../link.ld
BINDIR := ../../bin
OBJDIR := obj
# ---- Compiler flags ----
CXXFLAGS := \
-std=gnu++20 \
-g -O2 -pipe \
-Wall \
-Wextra \
-Wno-unused-parameter \
-Wno-unused-function \
-nostdinc \
-ffreestanding \
-fno-stack-protector \
-fno-stack-check \
-fno-PIC \
-fno-rtti \
-fno-exceptions \
-ffunction-sections \
-fdata-sections \
-m64 \
-march=x86-64 \
-msse \
-msse2 \
-mno-red-zone \
-mcmodel=small \
-I $(PROG_INC) \
-isystem $(LIBC_INC) \
-I $(BEARSSL)/inc \
-isystem ../../../kernel/freestnd-c-hdrs/x86_64/include \
-isystem ../../../kernel/freestnd-cxx-hdrs/x86_64/include
# ---- Linker flags ----
LDFLAGS := \
-nostdlib \
-static \
-Wl,--build-id=none \
-Wl,--gc-sections \
-Wl,-m,elf_x86_64 \
-z max-page-size=0x1000 \
-T $(LINK_LD)
# ---- Libraries ----
LIBS := $(TLS_LIB)/libtls.a $(BEARSSL)/libbearssl.a $(LIBC_LIB)/liblibc.a
# ---- Source files ----
SRCS := main.cpp stb_truetype_impl.cpp
OBJS := $(addprefix $(OBJDIR)/,$(SRCS:.cpp=.o))
# ---- Target ----
TARGET := $(BINDIR)/os/weather.elf
.PHONY: all clean
all: $(TARGET)
$(TARGET): $(OBJS) $(LIBS) $(LINK_LD) Makefile
mkdir -p $(BINDIR)/os
$(CXX) $(CXXFLAGS) $(LDFLAGS) $(OBJS) $(LIBS) -o $@
$(OBJDIR)/%.o: %.cpp Makefile
mkdir -p $(OBJDIR)
$(CXX) $(CXXFLAGS) -c $< -o $@
clean:
rm -rf $(OBJDIR) $(TARGET)
+575
View File
@@ -0,0 +1,575 @@
/*
* main.cpp
* ZenithOS Weather app - standalone Window Server process
* Fetches current weather from wttr.in via HTTPS (BearSSL)
* Displays temperature, description, feels like, and location
* Copyright (c) 2026 Daniel Hammer
*/
#include <zenith/syscall.h>
#include <zenith/string.h>
#include <zenith/heap.h>
#include <gui/gui.hpp>
#include <gui/svg.hpp>
#include <gui/truetype.hpp>
#include <tls/tls.hpp>
extern "C" {
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
}
using namespace gui;
using namespace gui::colors;
// ============================================================================
// Constants
// ============================================================================
static constexpr int INIT_W = 380;
static constexpr int INIT_H = 280;
static constexpr int HEADER_H = 160;
static constexpr int FOOTER_H = 50;
static constexpr int ICON_SIZE = 80;
static constexpr int ICON_X = 28;
static constexpr int ICON_Y = 40;
static constexpr int INFO_X = ICON_X + ICON_SIZE + 20; // 128
static constexpr int TEMP_Y = 40;
static int TEMP_SIZE = 40;
static constexpr int DESC_Y = 92;
static int DESC_SIZE = 17;
static constexpr int FEELS_Y = 116;
static int LABEL_SIZE = 15;
static constexpr int RESP_MAX = 65536;
static const char WTTR_HOST[] = "wttr.in";
// ============================================================================
// App state
// ============================================================================
enum class AppPhase { IDLE, LOADING, DONE, ERR };
static AppPhase g_phase = AppPhase::IDLE;
static char g_temp[32] = {};
static char g_desc[128] = {};
static char g_feels[48] = {};
static char g_location[128] = {};
static char g_status[256] = {};
static int g_win_w = INIT_W;
static int g_win_h = INIT_H;
static char* g_resp_buf = nullptr;
// Fonts
static TrueTypeFont* g_font = nullptr; // Roboto Medium
static TrueTypeFont* g_font_bold = nullptr; // Roboto Bold
// Weather icon (loaded per-fetch based on weather code)
static SvgIcon g_icon = {nullptr, 0, 0};
static char g_icon_name[64] = {};
// TLS state (lazy-init on first fetch)
static bool g_tls_ready = false;
static uint32_t g_server_ip = 0;
static tls::TrustAnchors g_tas = {nullptr, 0, 0};
// ============================================================================
// Pixel buffer helpers
// ============================================================================
static void px_fill(uint32_t* px, int bw, int bh,
int x, int y, int w, int h, Color c) {
uint32_t v = c.to_pixel();
int x0 = x < 0 ? 0 : x, y0 = y < 0 ? 0 : y;
int x1 = x + w > bw ? bw : x + w;
int y1 = y + h > bh ? bh : y + h;
for (int row = y0; row < y1; row++)
for (int col = x0; col < x1; col++)
px[row * bw + col] = v;
}
static void px_hline(uint32_t* px, int bw, int bh, int x, int y, int len, Color c) {
if (y < 0 || y >= bh) return;
uint32_t v = c.to_pixel();
int x1 = x + len > bw ? bw : x + len;
for (int col = x < 0 ? 0 : x; col < x1; col++)
px[y * bw + col] = v;
}
// Integer square root (Newton's method).
static int isqrt(int n) {
if (n <= 0) return 0;
int x = n, y = (x + 1) / 2;
while (y < x) { x = y; y = (x + n / x) / 2; }
return x;
}
// Fill a rounded rectangle directly onto the pixel buffer.
static void px_fill_rounded(uint32_t* px, int bw, int bh,
int x, int y, int w, int h, int r, Color c) {
if (r <= 0) { px_fill(px, bw, bh, x, y, w, h, c); return; }
if (r > w / 2) r = w / 2;
if (r > h / 2) r = h / 2;
uint32_t v = c.to_pixel();
// Corner arc centers
int cx_l = x + r;
int cx_r = x + w - 1 - r;
for (int row = y; row < y + h; row++) {
if (row < 0 || row >= bh) continue;
int left, right;
if (row < y + r) {
int dy = y + r - row; // 1..r
int dx = isqrt(r * r - dy * dy);
left = cx_l - dx;
right = cx_r + dx + 1;
} else if (row >= y + h - r) {
int dy = row - (y + h - 1 - r); // 1..r
int dx = isqrt(r * r - dy * dy);
left = cx_l - dx;
right = cx_r + dx + 1;
} else {
left = x;
right = x + w;
}
if (left < 0) left = 0;
if (right > bw) right = bw;
for (int col = left; col < right; col++)
px[row * bw + col] = v;
}
}
// Alpha-composite an SvgIcon onto the pixel buffer.
static void draw_icon(uint32_t* px, int bw, int bh,
int dx, int dy, const SvgIcon& icon) {
if (!icon.pixels) return;
for (int row = 0; row < icon.height; row++) {
int ty = dy + row;
if (ty < 0 || ty >= bh) continue;
for (int col = 0; col < icon.width; col++) {
int tx = dx + col;
if (tx < 0 || tx >= bw) continue;
uint32_t s = icon.pixels[row * icon.width + col];
uint8_t sa = (s >> 24) & 0xFF;
if (sa == 0) continue;
if (sa == 255) { px[ty * bw + tx] = s; continue; }
uint8_t sr = (s >> 16) & 0xFF, sg = (s >> 8) & 0xFF, sb = s & 0xFF;
uint32_t d = px[ty * bw + tx];
uint8_t dr = (d >> 16) & 0xFF, dg = (d >> 8) & 0xFF, db = d & 0xFF;
uint32_t inv = 255 - sa;
px[ty * bw + tx] = 0xFF000000u
| (((sr * sa + dr * inv) / 255u) << 16)
| (((sg * sa + dg * inv) / 255u) << 8)
| ((sb * sa + db * inv) / 255u);
}
}
}
// ============================================================================
// HTTP parsing
// ============================================================================
static int find_header_end(const char* buf, int len) {
for (int i = 0; i + 3 < len; i++)
if (buf[i]=='\r' && buf[i+1]=='\n' && buf[i+2]=='\r' && buf[i+3]=='\n')
return i + 4;
return -1;
}
static int parse_status_code(const char* buf, int len) {
int i = 0;
while (i < len && buf[i] != ' ') i++;
if (i >= len || i + 3 >= len) return -1;
i++;
if (buf[i] < '0' || buf[i] > '9') return -1;
return (buf[i]-'0')*100 + (buf[i+1]-'0')*10 + (buf[i+2]-'0');
}
// ============================================================================
// JSON parsing
// ============================================================================
// Extract value of a simple JSON string field: "key":"value"
static int extract_json_string(const char* buf, int len, const char* key,
char* out, int maxOut) {
int klen = (int)strlen(key);
for (int i = 0; i < len - klen - 3; i++) {
if (buf[i] != '"') continue;
if (memcmp(buf + i + 1, key, klen) != 0) continue;
if (buf[i + 1 + klen] != '"') continue;
if (buf[i + 2 + klen] != ':') continue;
int p = i + 3 + klen;
while (p < len && (buf[p]==' ' || buf[p]=='\t')) p++;
if (p >= len || buf[p] != '"') continue;
p++;
int j = 0;
while (p < len && j < maxOut - 1 && buf[p] != '"') {
if (buf[p] == '\\' && p + 1 < len) { p++; out[j++] = buf[p]; }
else out[j++] = buf[p];
p++;
}
out[j] = '\0';
return j;
}
out[0] = '\0';
return 0;
}
// Find the first occurrence of needle in buf[0..len-1]. Returns offset or -1.
static int find_substr(const char* buf, int len, const char* needle) {
int nlen = (int)strlen(needle);
if (nlen > len) return -1;
for (int i = 0; i <= len - nlen; i++) {
if (memcmp(buf + i, needle, nlen) == 0) return i;
}
return -1;
}
// Extract the "value" string within a named JSON array-of-objects field.
// e.g., "weatherDesc":[{"value":"Partly cloudy"}] → "Partly cloudy"
// Works by finding section_key first, then the first "value" after it.
static int extract_array_value(const char* buf, int len, const char* section_key,
char* out, int maxOut) {
int pos = find_substr(buf, len, section_key);
if (pos < 0) { out[0] = '\0'; return 0; }
return extract_json_string(buf + pos, len - pos, "value", out, maxOut);
}
// Parse a signed integer from a decimal string.
static int parse_int(const char* s) {
int sign = 1, val = 0, i = 0;
if (s[0] == '-') { sign = -1; i = 1; }
while (s[i] >= '0' && s[i] <= '9') val = val * 10 + (s[i++] - '0');
return sign * val;
}
// ============================================================================
// Weather code → icon filename
// Maps wttr.in WMO-style weather codes to Flat-Remix panel icon names.
// ============================================================================
static const char* weather_code_to_icon(int code) {
switch (code) {
case 113: return "weather-clear.svg";
case 116: return "weather-few-clouds.svg";
case 119: return "weather-clouds.svg";
case 122: return "weather-overcast.svg";
case 143: return "weather-mist.svg";
case 248: case 260: return "weather-fog.svg";
case 176: case 263: case 266: case 353: return "weather-showers-scattered.svg";
case 293: case 296: case 299: case 302:
case 305: case 308: case 356: case 359: return "weather-showers.svg";
case 179: case 362: case 365: case 368: return "weather-snow-scattered.svg";
case 323: case 326: case 329: case 332:
case 335: case 338: case 371: case 374: return "weather-snow.svg";
case 227: case 230: return "weather-snow.svg";
case 182: case 311: case 314: case 317: case 320: return "weather-snow-rain.svg";
case 185: case 281: case 284: return "weather-freezing-rain.svg";
case 350: case 377: return "weather-hail.svg";
case 200: case 386: case 389: case 392: case 395: return "weather-storm.svg";
default: return "weather-none-available.svg";
}
}
// Load the weather icon for the given icon filename (caches the last result).
static void load_weather_icon(const char* icon_name) {
if (strcmp(g_icon_name, icon_name) == 0 && g_icon.pixels) return;
if (g_icon.pixels) svg_free(g_icon);
static char path[80];
snprintf(path, sizeof(path), "0:/icons/%s", icon_name);
// ColorScheme-Text paths (main shape) use this fill_color; accent classes
// (ColorScheme-Highlight=blue, ColorScheme-NeutralText=yellow, etc.) use their
// CSS-defined colors as parsed from the SVG's <style> block by svg_load.
static constexpr Color ICON_FG = Color::from_rgb(0x5C, 0x61, 0x6C); // dark neutral
g_icon = svg_load(path, ICON_SIZE, ICON_SIZE, ICON_FG);
int i = 0;
while (icon_name[i] && i < 63) { g_icon_name[i] = icon_name[i]; i++; }
g_icon_name[i] = '\0';
}
// ============================================================================
// UI scale
// ============================================================================
static void apply_scale(int scale) {
switch (scale) {
case 0: TEMP_SIZE=32; DESC_SIZE=14; LABEL_SIZE=12; break;
case 2: TEMP_SIZE=50; DESC_SIZE=21; LABEL_SIZE=19; break;
default: TEMP_SIZE=40; DESC_SIZE=17; LABEL_SIZE=15; break;
}
}
// ============================================================================
// Network fetch (blocking — called from the event loop)
// ============================================================================
static void do_fetch() {
// Lazy init: resolve DNS and load CA certificates once
if (!g_tls_ready) {
g_server_ip = zenith::resolve(WTTR_HOST);
if (g_server_ip == 0) {
snprintf(g_status, sizeof(g_status),
"Error: could not resolve %s", WTTR_HOST);
g_phase = AppPhase::ERR; return;
}
g_tas = tls::load_trust_anchors();
if (g_tas.count == 0) {
snprintf(g_status, sizeof(g_status), "Error: no CA certificates loaded");
g_phase = AppPhase::ERR; return;
}
g_tls_ready = true;
}
static char request[512];
int reqLen = snprintf(request, sizeof(request),
"GET /?format=j1 HTTP/1.0\r\n"
"Host: %s\r\n"
"User-Agent: ZenithOS/1.0 weather\r\n"
"Accept: application/json\r\n"
"Connection: close\r\n"
"\r\n",
WTTR_HOST);
int respLen = tls::https_fetch(WTTR_HOST, g_server_ip, 443,
request, reqLen, g_tas,
g_resp_buf, RESP_MAX);
if (respLen <= 0) {
snprintf(g_status, sizeof(g_status), "Error: no response from server");
g_phase = AppPhase::ERR; return;
}
g_resp_buf[respLen] = '\0';
int headerEnd = find_header_end(g_resp_buf, respLen);
if (headerEnd < 0) {
snprintf(g_status, sizeof(g_status), "Error: malformed HTTP response");
g_phase = AppPhase::ERR; return;
}
int status = parse_status_code(g_resp_buf, headerEnd);
if (status != 200) {
snprintf(g_status, sizeof(g_status), "Error: HTTP %d from server", status);
g_phase = AppPhase::ERR; return;
}
const char* body = g_resp_buf + headerEnd;
int bodyLen = respLen - headerEnd;
// Extract core weather fields
static char temp_raw[16], feels_raw[16], code_raw[8];
extract_json_string(body, bodyLen, "temp_C", temp_raw, sizeof(temp_raw));
extract_json_string(body, bodyLen, "FeelsLikeC", feels_raw, sizeof(feels_raw));
extract_json_string(body, bodyLen, "weatherCode", code_raw, sizeof(code_raw));
extract_array_value(body, bodyLen, "\"weatherDesc\"", g_desc, sizeof(g_desc));
static char area[64], country[64];
extract_array_value(body, bodyLen, "\"areaName\"", area, sizeof(area));
extract_array_value(body, bodyLen, "\"country\"", country, sizeof(country));
// Degree sign is U+00B0 = 0xB0 in Latin-1 (single byte, within the 256-entry glyph cache)
snprintf(g_temp, sizeof(g_temp), "%s\xb0""C", temp_raw);
snprintf(g_feels, sizeof(g_feels), "Feels like: %s\xb0""C", feels_raw);
if (area[0] && country[0])
snprintf(g_location, sizeof(g_location), "%s, %s", area, country);
else if (area[0])
snprintf(g_location, sizeof(g_location), "%s", area);
else
snprintf(g_location, sizeof(g_location), "Unknown location");
// Load matching weather icon
int code = parse_int(code_raw);
load_weather_icon(weather_code_to_icon(code));
g_phase = AppPhase::DONE;
}
// ============================================================================
// Theme colors
// ============================================================================
static constexpr Color CONTENT_BG = Color::from_rgb(0xFF, 0xFF, 0xFF); // white
static constexpr Color FOOTER_BG = Color::from_rgb(0xF5, 0xF5, 0xF5); // light gray
static constexpr Color DIVIDER = Color::from_rgb(0xCC, 0xCC, 0xCC); // subtle border
static constexpr Color DARK_TEXT = Color::from_rgb(0x33, 0x33, 0x33);
static constexpr Color MID_TEXT = Color::from_rgb(0x88, 0x88, 0x88);
static constexpr Color HINT_TEXT = Color::from_rgb(0x99, 0x99, 0x99);
static constexpr Color ERR_TEXT = Color::from_rgb(0xCC, 0x22, 0x22);
static constexpr Color BTN_BG = Color::from_rgb(0x36, 0x7B, 0xF0); // accent blue
static constexpr Color WHITE_TEXT = Color::from_rgb(0xFF, 0xFF, 0xFF);
// ============================================================================
// Rendering
// ============================================================================
static constexpr int BTN_W = 110;
static constexpr int BTN_H = 28;
static constexpr int BTN_RADIUS = 6;
static void render(uint32_t* pixels) {
// ── Background ───────────────────────────────────────────────────────────
px_fill(pixels, g_win_w, g_win_h, 0, 0,
g_win_w, g_win_h - FOOTER_H, CONTENT_BG);
px_fill(pixels, g_win_w, g_win_h, 0, g_win_h - FOOTER_H,
g_win_w, FOOTER_H, FOOTER_BG);
// Divider between content and location strip
px_hline(pixels, g_win_w, g_win_h, 0, HEADER_H, g_win_w, DIVIDER);
// Divider above footer
px_hline(pixels, g_win_w, g_win_h, 0, g_win_h - FOOTER_H, g_win_w, DIVIDER);
if (!g_font) return;
// ── Main content area (y=0..HEADER_H) ────────────────────────────────────
if (g_phase == AppPhase::LOADING) {
g_font->draw_to_buffer(pixels, g_win_w, g_win_h,
20, HEADER_H / 2 - 9,
"Fetching weather data...", HINT_TEXT, 18);
} else if (g_phase == AppPhase::ERR) {
g_font->draw_to_buffer(pixels, g_win_w, g_win_h,
20, 20, g_status, ERR_TEXT, 15);
} else if (g_phase == AppPhase::IDLE) {
g_font->draw_to_buffer(pixels, g_win_w, g_win_h,
20, HEADER_H / 2 - 9,
"Click Refresh to check weather.", HINT_TEXT, 18);
} else { // DONE
// Weather icon
draw_icon(pixels, g_win_w, g_win_h, ICON_X, ICON_Y, g_icon);
// Temperature (large bold)
TrueTypeFont* temp_font = g_font_bold ? g_font_bold : g_font;
temp_font->draw_to_buffer(pixels, g_win_w, g_win_h,
INFO_X, TEMP_Y, g_temp, DARK_TEXT, TEMP_SIZE);
// Weather description
g_font->draw_to_buffer(pixels, g_win_w, g_win_h,
INFO_X, DESC_Y, g_desc, DARK_TEXT, DESC_SIZE);
// Feels like
g_font->draw_to_buffer(pixels, g_win_w, g_win_h,
INFO_X, FEELS_Y, g_feels, MID_TEXT, LABEL_SIZE);
}
// ── Location strip (y=HEADER_H..g_win_h-FOOTER_H) ────────────────────────
if (g_phase == AppPhase::DONE) {
g_font->draw_to_buffer(pixels, g_win_w, g_win_h,
20, HEADER_H + 14, g_location, DARK_TEXT, LABEL_SIZE);
}
// ── Refresh button (rounded, in footer) ───────────────────────────────────
int btn_x = (g_win_w - BTN_W) / 2;
int btn_y = g_win_h - FOOTER_H + (FOOTER_H - BTN_H) / 2;
px_fill_rounded(pixels, g_win_w, g_win_h,
btn_x, btn_y, BTN_W, BTN_H, BTN_RADIUS, BTN_BG);
if (g_phase == AppPhase::LOADING) {
const char* lbl = "Loading...";
int sw = g_font->measure_text(lbl, 14);
g_font->draw_to_buffer(pixels, g_win_w, g_win_h,
btn_x + (BTN_W - sw) / 2, btn_y + (BTN_H - 14) / 2,
lbl, WHITE_TEXT, 14);
} else {
const char* lbl = "Refresh";
int sw = g_font->measure_text(lbl, 15);
g_font->draw_to_buffer(pixels, g_win_w, g_win_h,
btn_x + (BTN_W - sw) / 2, btn_y + (BTN_H - 15) / 2,
lbl, WHITE_TEXT, 15);
}
}
// ============================================================================
// Entry point
// ============================================================================
extern "C" void _start() {
// Allocate response buffer from heap
g_resp_buf = (char*)malloc(RESP_MAX + 1);
if (!g_resp_buf) zenith::exit(1);
// Load fonts
auto load_font = [](const char* path) -> TrueTypeFont* {
TrueTypeFont* f = (TrueTypeFont*)zenith::malloc(sizeof(TrueTypeFont));
if (!f) return nullptr;
zenith::memset(f, 0, sizeof(TrueTypeFont));
if (!f->init(path)) { zenith::mfree(f); return nullptr; }
return f;
};
g_font = load_font("0:/fonts/Roboto-Medium.ttf");
g_font_bold = load_font("0:/fonts/Roboto-Bold.ttf");
if (!g_font) zenith::exit(1);
apply_scale(zenith::win_getscale());
// Create window
Zenith::WinCreateResult wres;
if (zenith::win_create("Weather", INIT_W, INIT_H, &wres) < 0 || wres.id < 0)
zenith::exit(1);
int win_id = wres.id;
uint32_t* pixels = (uint32_t*)(uintptr_t)wres.pixelVa;
// Initial fetch on startup
g_phase = AppPhase::LOADING;
render(pixels);
zenith::win_present(win_id);
do_fetch();
// Event loop
while (true) {
Zenith::WinEvent ev;
int r = zenith::win_poll(win_id, &ev);
if (r < 0) break;
if (r == 0) {
zenith::sleep_ms(16);
render(pixels);
zenith::win_present(win_id);
continue;
}
if (ev.type == 3) break; // close
if (ev.type == 4) {
apply_scale(ev.scale.scale);
}
if (ev.type == 1) {
// Mouse — check for Refresh button click
bool just_clicked = (ev.mouse.buttons & 1) && !(ev.mouse.prev_buttons & 1);
if (just_clicked && g_phase != AppPhase::LOADING) {
int btn_x = (g_win_w - BTN_W) / 2;
int btn_y = g_win_h - FOOTER_H + (FOOTER_H - BTN_H) / 2;
int mx = ev.mouse.x, my = ev.mouse.y;
if (mx >= btn_x && mx < btn_x + BTN_W &&
my >= btn_y && my < btn_y + BTN_H) {
g_phase = AppPhase::LOADING;
render(pixels);
zenith::win_present(win_id);
do_fetch();
}
}
}
render(pixels);
zenith::win_present(win_id);
}
if (g_icon.pixels) svg_free(g_icon);
zenith::win_destroy(win_id);
zenith::exit(0);
}
@@ -0,0 +1,35 @@
/*
* stb_truetype_impl.cpp
* Single compilation unit for stb_truetype in ZenithOS freestanding environment
* Copyright (c) 2026 Daniel Hammer
*/
#include <cstdint>
#include <cstddef>
#include <zenith/heap.h>
#include <zenith/string.h>
#include <gui/stb_math.h>
// Override all stb_truetype dependencies before including the implementation
#define STBTT_ifloor(x) ((int) stb_floor(x))
#define STBTT_iceil(x) ((int) stb_ceil(x))
#define STBTT_sqrt(x) stb_sqrt(x)
#define STBTT_pow(x,y) stb_pow(x,y)
#define STBTT_fmod(x,y) stb_fmod(x,y)
#define STBTT_cos(x) stb_cos(x)
#define STBTT_acos(x) stb_acos(x)
#define STBTT_fabs(x) stb_fabs(x)
#define STBTT_malloc(x,u) ((void)(u), zenith::malloc(x))
#define STBTT_free(x,u) ((void)(u), zenith::mfree(x))
#define STBTT_memcpy(d,s,n) zenith::memcpy(d,s,n)
#define STBTT_memset(d,v,n) zenith::memset(d,v,n)
#define STBTT_strlen(x) zenith::slen(x)
#define STBTT_assert(x) ((void)(x))
#define STB_TRUETYPE_IMPLEMENTATION
#include <gui/stb_truetype.h>
+2 -1
View File
@@ -16,6 +16,7 @@ endif
# ---- Paths ---- # ---- Paths ----
BEARSSL := ../../lib/bearssl BEARSSL := ../../lib/bearssl
TLS_LIB := ../../lib/tls
LIBC_LIB := ../../lib/libc LIBC_LIB := ../../lib/libc
LIBC_INC := ../../include/libc LIBC_INC := ../../include/libc
PROG_INC := ../../include PROG_INC := ../../include
@@ -67,7 +68,7 @@ LDFLAGS := \
# ---- Libraries ---- # ---- Libraries ----
LIBS := $(BEARSSL)/libbearssl.a $(LIBC_LIB)/liblibc.a LIBS := $(TLS_LIB)/libtls.a $(BEARSSL)/libbearssl.a $(LIBC_LIB)/liblibc.a
# ---- Target ---- # ---- Target ----
+11 -340
View File
@@ -10,9 +10,9 @@
#include <zenith/syscall.h> #include <zenith/syscall.h>
#include <zenith/string.h> #include <zenith/string.h>
#include <tls/tls.hpp>
extern "C" { extern "C" {
#include <bearssl.h>
#include <string.h> #include <string.h>
#include <stdlib.h> #include <stdlib.h>
#include <stdio.h> #include <stdio.h>
@@ -37,14 +37,8 @@ struct WikiLine {
// ---- Global state (loaded once, reused across fetches) ---- // ---- Global state (loaded once, reused across fetches) ----
struct TrustAnchors {
br_x509_trust_anchor* anchors;
size_t count;
size_t capacity;
};
static uint32_t g_serverIp = 0; static uint32_t g_serverIp = 0;
static TrustAnchors g_tas = {nullptr, 0, 0}; static tls::TrustAnchors g_tas = {nullptr, 0, 0};
// ---- Screen buffer for flicker-free rendering ---- // ---- Screen buffer for flicker-free rendering ----
@@ -75,340 +69,20 @@ static void sb_cursor_to(int row, int col) {
sb_putc('H'); sb_putc('H');
} }
// ---- Trust anchor loading ---- // ---- Keyboard abort check for TLS ----
struct DerAccum {
unsigned char* data;
size_t len;
size_t cap;
};
static void der_append(void* ctx, const void* buf, size_t len) {
DerAccum* a = (DerAccum*)ctx;
if (a->len + len > a->cap) {
size_t newcap = a->cap * 2;
if (newcap < a->len + len) newcap = a->len + len + 4096;
unsigned char* nb = (unsigned char*)malloc(newcap);
if (!nb) return;
if (a->data) {
memcpy(nb, a->data, a->len);
free(a->data);
}
a->data = nb;
a->cap = newcap;
}
memcpy(a->data + a->len, buf, len);
a->len += len;
}
struct DnAccum {
unsigned char* data;
size_t len;
size_t cap;
};
static void dn_append(void* ctx, const void* buf, size_t len) {
DnAccum* a = (DnAccum*)ctx;
if (a->len + len > a->cap) {
size_t newcap = a->cap * 2;
if (newcap < a->len + len) newcap = a->len + len + 256;
unsigned char* nb = (unsigned char*)malloc(newcap);
if (!nb) return;
if (a->data) {
memcpy(nb, a->data, a->len);
free(a->data);
}
a->data = nb;
a->cap = newcap;
}
memcpy(a->data + a->len, buf, len);
a->len += len;
}
static void ta_add(TrustAnchors* tas, const br_x509_trust_anchor* ta) {
if (tas->count >= tas->capacity) {
size_t newcap = tas->capacity == 0 ? 64 : tas->capacity * 2;
br_x509_trust_anchor* na = (br_x509_trust_anchor*)malloc(
newcap * sizeof(br_x509_trust_anchor));
if (!na) return;
if (tas->anchors) {
memcpy(na, tas->anchors, tas->count * sizeof(br_x509_trust_anchor));
free(tas->anchors);
}
tas->anchors = na;
tas->capacity = newcap;
}
tas->anchors[tas->count++] = *ta;
}
static bool process_cert_der(TrustAnchors* tas, const unsigned char* der, size_t der_len) {
static br_x509_decoder_context dc; // ~2KB+, keep off stack
DnAccum dn = {nullptr, 0, 0};
br_x509_decoder_init(&dc, dn_append, &dn);
br_x509_decoder_push(&dc, der, der_len);
br_x509_pkey* pk = br_x509_decoder_get_pkey(&dc);
if (!pk) {
if (dn.data) free(dn.data);
return false;
}
br_x509_trust_anchor ta;
memset(&ta, 0, sizeof(ta));
ta.dn.data = dn.data;
ta.dn.len = dn.len;
ta.flags = 0;
if (br_x509_decoder_isCA(&dc))
ta.flags |= BR_X509_TA_CA;
switch (pk->key_type) {
case BR_KEYTYPE_RSA: {
ta.pkey.key_type = BR_KEYTYPE_RSA;
ta.pkey.key.rsa.nlen = pk->key.rsa.nlen;
ta.pkey.key.rsa.n = (unsigned char*)malloc(pk->key.rsa.nlen);
if (ta.pkey.key.rsa.n) memcpy(ta.pkey.key.rsa.n, pk->key.rsa.n, pk->key.rsa.nlen);
ta.pkey.key.rsa.elen = pk->key.rsa.elen;
ta.pkey.key.rsa.e = (unsigned char*)malloc(pk->key.rsa.elen);
if (ta.pkey.key.rsa.e) memcpy(ta.pkey.key.rsa.e, pk->key.rsa.e, pk->key.rsa.elen);
break;
}
case BR_KEYTYPE_EC: {
ta.pkey.key_type = BR_KEYTYPE_EC;
ta.pkey.key.ec.curve = pk->key.ec.curve;
ta.pkey.key.ec.qlen = pk->key.ec.qlen;
ta.pkey.key.ec.q = (unsigned char*)malloc(pk->key.ec.qlen);
if (ta.pkey.key.ec.q) memcpy(ta.pkey.key.ec.q, pk->key.ec.q, pk->key.ec.qlen);
break;
}
default:
if (dn.data) free(dn.data);
return false;
}
ta_add(tas, &ta);
return true;
}
static TrustAnchors load_trust_anchors() {
TrustAnchors tas = {nullptr, 0, 0};
int fh = zenith::open("0:/etc/ca-certificates.crt");
if (fh < 0) return tas;
uint64_t fsize = zenith::getsize(fh);
if (fsize == 0 || fsize > 512 * 1024) {
zenith::close(fh);
return tas;
}
unsigned char* pem = (unsigned char*)malloc(fsize + 1);
if (!pem) { zenith::close(fh); return tas; }
zenith::read(fh, pem, 0, fsize);
zenith::close(fh);
pem[fsize] = 0;
static br_pem_decoder_context pc; // keep off stack
br_pem_decoder_init(&pc);
DerAccum der = {nullptr, 0, 0};
bool inCert = false;
size_t offset = 0;
while (offset < fsize) {
size_t pushed = br_pem_decoder_push(&pc, pem + offset, fsize - offset);
offset += pushed;
int event = br_pem_decoder_event(&pc);
if (event == BR_PEM_BEGIN_OBJ) {
const char* name = br_pem_decoder_name(&pc);
inCert = (strcmp(name, "CERTIFICATE") == 0);
if (inCert) {
der.len = 0;
br_pem_decoder_setdest(&pc, der_append, &der);
} else {
br_pem_decoder_setdest(&pc, nullptr, nullptr);
}
} else if (event == BR_PEM_END_OBJ) {
if (inCert && der.len > 0)
process_cert_der(&tas, der.data, der.len);
inCert = false;
} else if (event == BR_PEM_ERROR) {
break;
}
}
if (der.data) free(der.data);
free(pem);
return tas;
}
// ---- Time conversion for certificate validation ----
static void get_bearssl_time(uint32_t* days, uint32_t* seconds) {
Zenith::DateTime dt;
zenith::gettime(&dt);
int y = dt.Year;
int m = dt.Month;
int d = dt.Day;
uint32_t total_days = 365 * (uint32_t)y
+ (uint32_t)(y / 4)
- (uint32_t)(y / 100)
+ (uint32_t)(y / 400);
const int mdays[] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
for (int mo = 1; mo < m && mo <= 12; mo++)
total_days += mdays[mo];
bool leap = (y % 4 == 0 && (y % 100 != 0 || y % 400 == 0));
if (leap && m > 2) total_days++;
total_days += d - 1;
*days = total_days;
*seconds = (uint32_t)(dt.Hour * 3600 + dt.Minute * 60 + dt.Second);
}
// ---- TLS I/O ----
static int tls_send_all(int fd, const unsigned char* data, size_t len) {
size_t sent = 0;
uint64_t deadline = zenith::get_milliseconds() + 15000;
while (sent < len) {
int r = zenith::send(fd, data + sent, (uint32_t)(len - sent));
if (r > 0) { sent += r; deadline = zenith::get_milliseconds() + 15000; }
else if (r < 0) return -1;
else { if (zenith::get_milliseconds() >= deadline) return -1; zenith::sleep_ms(1); }
}
return (int)sent;
}
static int tls_recv_some(int fd, unsigned char* buf, size_t maxlen) {
uint64_t deadline = zenith::get_milliseconds() + 15000;
while (true) {
int r = zenith::recv(fd, buf, (uint32_t)maxlen);
if (r > 0) return r;
if (r < 0) return -1;
if (zenith::get_milliseconds() >= deadline) return -1;
zenith::sleep_ms(1);
}
}
static int tls_exchange(int fd, br_ssl_engine_context* eng,
const char* request, int reqLen,
char* respBuf, int respMax) {
bool requestSent = false;
int respLen = 0;
uint64_t deadline = zenith::get_milliseconds() + 30000;
while (true) {
unsigned state = br_ssl_engine_current_state(eng);
if (state & BR_SSL_CLOSED) {
int err = br_ssl_engine_last_error(eng);
if (err != BR_ERR_OK && err != BR_ERR_IO) {
if (respLen == 0) return -1;
}
return respLen;
}
static bool check_keyboard_abort() {
if (zenith::is_key_available()) { if (zenith::is_key_available()) {
Zenith::KeyEvent ev; Zenith::KeyEvent ev;
zenith::getkey(&ev); zenith::getkey(&ev);
if (ev.pressed && ev.ctrl && ev.ascii == 'q') { if (ev.pressed && ev.ctrl && ev.ascii == 'q') return true;
br_ssl_engine_close(eng);
return respLen > 0 ? respLen : -1;
}
}
if (state & BR_SSL_SENDREC) {
size_t len;
unsigned char* buf = br_ssl_engine_sendrec_buf(eng, &len);
int sent = tls_send_all(fd, buf, len);
if (sent < 0) { br_ssl_engine_close(eng); return respLen > 0 ? respLen : -1; }
br_ssl_engine_sendrec_ack(eng, len);
deadline = zenith::get_milliseconds() + 30000;
continue;
}
if (state & BR_SSL_RECVAPP) {
size_t len;
unsigned char* buf = br_ssl_engine_recvapp_buf(eng, &len);
size_t toCopy = len;
if (respLen + (int)toCopy > respMax - 1)
toCopy = respMax - 1 - respLen;
if (toCopy > 0) { memcpy(respBuf + respLen, buf, toCopy); respLen += toCopy; }
br_ssl_engine_recvapp_ack(eng, len);
deadline = zenith::get_milliseconds() + 30000;
continue;
}
if ((state & BR_SSL_SENDAPP) && !requestSent) {
size_t len;
unsigned char* buf = br_ssl_engine_sendapp_buf(eng, &len);
size_t toWrite = (size_t)reqLen;
if (toWrite > len) toWrite = len;
memcpy(buf, request, toWrite);
br_ssl_engine_sendapp_ack(eng, toWrite);
br_ssl_engine_flush(eng, 0);
requestSent = true;
deadline = zenith::get_milliseconds() + 30000;
continue;
}
if (state & BR_SSL_RECVREC) {
size_t len;
unsigned char* buf = br_ssl_engine_recvrec_buf(eng, &len);
int got = tls_recv_some(fd, buf, len);
if (got < 0) { br_ssl_engine_close(eng); return respLen > 0 ? respLen : -1; }
br_ssl_engine_recvrec_ack(eng, got);
deadline = zenith::get_milliseconds() + 30000;
continue;
}
if (zenith::get_milliseconds() >= deadline)
return respLen > 0 ? respLen : -1;
zenith::sleep_ms(1);
} }
return false;
} }
// ---- HTTPS fetch wrapper (reusable for multiple requests) ---- // ---- HTTPS fetch wrapper ----
static int wiki_fetch(const char* path, char* respBuf, int respMax) { static int wiki_fetch(const char* path, char* respBuf, int respMax) {
int fd = zenith::socket(Zenith::SOCK_TCP);
if (fd < 0) return -1;
if (zenith::connect(fd, g_serverIp, 443) < 0) {
zenith::closesocket(fd);
return -1;
}
br_ssl_client_context* cc = (br_ssl_client_context*)malloc(sizeof(br_ssl_client_context));
br_x509_minimal_context* xc = (br_x509_minimal_context*)malloc(sizeof(br_x509_minimal_context));
void* iobuf = malloc(BR_SSL_BUFSIZE_BIDI);
if (!cc || !xc || !iobuf) {
zenith::closesocket(fd);
return -1;
}
br_ssl_client_init_full(cc, xc, g_tas.anchors, g_tas.count);
uint32_t days, secs;
get_bearssl_time(&days, &secs);
br_x509_minimal_set_time(xc, days, secs);
unsigned char seed[32];
zenith::getrandom(seed, sizeof(seed));
br_ssl_engine_set_buffer(&cc->eng, iobuf, BR_SSL_BUFSIZE_BIDI, 1);
br_ssl_engine_inject_entropy(&cc->eng, seed, sizeof(seed));
if (!br_ssl_client_reset(cc, WIKI_HOST, 0)) {
zenith::closesocket(fd);
free(cc); free(xc); free(iobuf);
return -1;
}
static char request[2560]; // keep off stack static char request[2560]; // keep off stack
int reqLen = snprintf(request, sizeof(request), int reqLen = snprintf(request, sizeof(request),
"GET %s HTTP/1.0\r\n" "GET %s HTTP/1.0\r\n"
@@ -418,12 +92,9 @@ static int wiki_fetch(const char* path, char* respBuf, int respMax) {
"Connection: close\r\n" "Connection: close\r\n"
"\r\n", "\r\n",
path, WIKI_HOST); path, WIKI_HOST);
return tls::https_fetch(WIKI_HOST, g_serverIp, 443,
int respLen = tls_exchange(fd, &cc->eng, request, reqLen, respBuf, respMax); request, reqLen, g_tas,
respBuf, respMax, check_keyboard_abort);
zenith::closesocket(fd);
free(cc); free(xc); free(iobuf);
return respLen;
} }
// ---- HTTP response parsing ---- // ---- HTTP response parsing ----
@@ -1030,7 +701,7 @@ extern "C" void _start() {
zenith::exit(1); zenith::exit(1);
} }
g_tas = load_trust_anchors(); g_tas = tls::load_trust_anchors();
if (g_tas.count == 0) { if (g_tas.count == 0) {
if (mode == MODE_DUMP) { zenith::print("\x01"); zenith::sleep_ms(100); zenith::exit(1); } if (mode == MODE_DUMP) { zenith::print("\x01"); zenith::sleep_ms(100); zenith::exit(1); }
zenith::print("\033[1;31mError:\033[0m no CA certificates loaded\n"); zenith::print("\033[1;31mError:\033[0m no CA certificates loaded\n");
Binary file not shown.
+2 -1
View File
@@ -16,6 +16,7 @@ endif
# ---- Paths ---- # ---- Paths ----
BEARSSL := ../../lib/bearssl BEARSSL := ../../lib/bearssl
TLS_LIB := ../../lib/tls
LIBC_LIB := ../../lib/libc LIBC_LIB := ../../lib/libc
LIBC_INC := ../../include/libc LIBC_INC := ../../include/libc
PROG_INC := ../../include PROG_INC := ../../include
@@ -66,7 +67,7 @@ LDFLAGS := \
# ---- Libraries ---- # ---- Libraries ----
LIBS := $(BEARSSL)/libbearssl.a $(LIBC_LIB)/liblibc.a LIBS := $(TLS_LIB)/libtls.a $(BEARSSL)/libbearssl.a $(LIBC_LIB)/liblibc.a
# ---- Source files ---- # ---- Source files ----
+27 -255
View File
@@ -10,9 +10,9 @@
#include <zenith/heap.h> #include <zenith/heap.h>
#include <gui/gui.hpp> #include <gui/gui.hpp>
#include <gui/truetype.hpp> #include <gui/truetype.hpp>
#include <tls/tls.hpp>
extern "C" { extern "C" {
#include <bearssl.h>
#include <string.h> #include <string.h>
#include <stdlib.h> #include <stdlib.h>
#include <stdio.h> #include <stdio.h>
@@ -27,11 +27,11 @@ using namespace gui::colors;
static constexpr int INIT_W = 820; static constexpr int INIT_W = 820;
static constexpr int INIT_H = 580; static constexpr int INIT_H = 580;
static constexpr int TOOLBAR_H = 42; static int TOOLBAR_H = 42;
static constexpr int SCROLLBAR_W = 14; static constexpr int SCROLLBAR_W = 14;
static constexpr int FONT_SIZE = 18; static int FONT_SIZE = 18;
static constexpr int TITLE_SIZE = 32; static int TITLE_SIZE = 32;
static constexpr int SECTION_SIZE = 24; static int SECTION_SIZE = 24;
static constexpr int TEXT_PAD = 16; static constexpr int TEXT_PAD = 16;
static constexpr int RESP_MAX = 131072; static constexpr int RESP_MAX = 131072;
static constexpr int MAX_LINES = 2000; static constexpr int MAX_LINES = 2000;
@@ -79,13 +79,7 @@ static TrueTypeFont* g_font_serif = nullptr; // NotoSerif SemiBold (headings)
// TLS state (lazy-init on first search) // TLS state (lazy-init on first search)
static bool g_tls_ready = false; static bool g_tls_ready = false;
static uint32_t g_server_ip = 0; static uint32_t g_server_ip = 0;
static tls::TrustAnchors g_tas = {nullptr, 0, 0};
struct TrustAnchors {
br_x509_trust_anchor* anchors;
size_t count;
size_t capacity;
};
static TrustAnchors g_tas = {nullptr, 0, 0};
// ============================================================================ // ============================================================================
// Pixel buffer helpers // Pixel buffer helpers
@@ -124,252 +118,23 @@ static void px_rect_outline(uint32_t* px, int bw, int x, int y, int w, int h, Co
} }
// ============================================================================ // ============================================================================
// Trust anchor loading // UI scale
// ============================================================================ // ============================================================================
struct DerAccum { unsigned char* data; size_t len, cap; }; static void apply_scale(int scale) {
struct DnAccum { unsigned char* data; size_t len, cap; }; switch (scale) {
case 0: FONT_SIZE=14; TITLE_SIZE=26; SECTION_SIZE=20; TOOLBAR_H=34; break;
static void der_append(void* ctx, const void* buf, size_t len) { case 2: FONT_SIZE=22; TITLE_SIZE=40; SECTION_SIZE=30; TOOLBAR_H=52; break;
DerAccum* a = (DerAccum*)ctx; default: FONT_SIZE=18; TITLE_SIZE=32; SECTION_SIZE=24; TOOLBAR_H=42; break;
if (a->len + len > a->cap) {
size_t nc = a->cap * 2;
if (nc < a->len + len) nc = a->len + len + 4096;
unsigned char* nb = (unsigned char*)malloc(nc);
if (!nb) return;
if (a->data) { memcpy(nb, a->data, a->len); free(a->data); }
a->data = nb; a->cap = nc;
} }
memcpy(a->data + a->len, buf, len); if (g_font) g_line_h = g_font->get_line_height(FONT_SIZE) + 4;
a->len += len;
}
static void dn_append(void* ctx, const void* buf, size_t len) {
DnAccum* a = (DnAccum*)ctx;
if (a->len + len > a->cap) {
size_t nc = a->cap * 2;
if (nc < a->len + len) nc = a->len + len + 256;
unsigned char* nb = (unsigned char*)malloc(nc);
if (!nb) return;
if (a->data) { memcpy(nb, a->data, a->len); free(a->data); }
a->data = nb; a->cap = nc;
}
memcpy(a->data + a->len, buf, len);
a->len += len;
}
static void ta_add(TrustAnchors* tas, const br_x509_trust_anchor* ta) {
if (tas->count >= tas->capacity) {
size_t nc = tas->capacity == 0 ? 64 : tas->capacity * 2;
br_x509_trust_anchor* na = (br_x509_trust_anchor*)malloc(nc * sizeof(*na));
if (!na) return;
if (tas->anchors) { memcpy(na, tas->anchors, tas->count * sizeof(*na)); free(tas->anchors); }
tas->anchors = na; tas->capacity = nc;
}
tas->anchors[tas->count++] = *ta;
}
static bool process_cert_der(TrustAnchors* tas, const unsigned char* der, size_t der_len) {
static br_x509_decoder_context dc;
DnAccum dn = {nullptr, 0, 0};
br_x509_decoder_init(&dc, dn_append, &dn);
br_x509_decoder_push(&dc, der, der_len);
br_x509_pkey* pk = br_x509_decoder_get_pkey(&dc);
if (!pk) { if (dn.data) free(dn.data); return false; }
br_x509_trust_anchor ta;
memset(&ta, 0, sizeof(ta));
ta.dn.data = dn.data; ta.dn.len = dn.len; ta.flags = 0;
if (br_x509_decoder_isCA(&dc)) ta.flags |= BR_X509_TA_CA;
switch (pk->key_type) {
case BR_KEYTYPE_RSA:
ta.pkey.key_type = BR_KEYTYPE_RSA;
ta.pkey.key.rsa.nlen = pk->key.rsa.nlen;
ta.pkey.key.rsa.n = (unsigned char*)malloc(pk->key.rsa.nlen);
if (ta.pkey.key.rsa.n) memcpy(ta.pkey.key.rsa.n, pk->key.rsa.n, pk->key.rsa.nlen);
ta.pkey.key.rsa.elen = pk->key.rsa.elen;
ta.pkey.key.rsa.e = (unsigned char*)malloc(pk->key.rsa.elen);
if (ta.pkey.key.rsa.e) memcpy(ta.pkey.key.rsa.e, pk->key.rsa.e, pk->key.rsa.elen);
break;
case BR_KEYTYPE_EC:
ta.pkey.key_type = BR_KEYTYPE_EC;
ta.pkey.key.ec.curve = pk->key.ec.curve;
ta.pkey.key.ec.qlen = pk->key.ec.qlen;
ta.pkey.key.ec.q = (unsigned char*)malloc(pk->key.ec.qlen);
if (ta.pkey.key.ec.q) memcpy(ta.pkey.key.ec.q, pk->key.ec.q, pk->key.ec.qlen);
break;
default:
if (dn.data) free(dn.data);
return false;
}
ta_add(tas, &ta);
return true;
}
static TrustAnchors load_trust_anchors() {
TrustAnchors tas = {nullptr, 0, 0};
int fh = zenith::open("0:/etc/ca-certificates.crt");
if (fh < 0) return tas;
uint64_t fsize = zenith::getsize(fh);
if (fsize == 0 || fsize > 512 * 1024) { zenith::close(fh); return tas; }
unsigned char* pem = (unsigned char*)malloc(fsize + 1);
if (!pem) { zenith::close(fh); return tas; }
zenith::read(fh, pem, 0, fsize);
zenith::close(fh);
pem[fsize] = 0;
static br_pem_decoder_context pc;
br_pem_decoder_init(&pc);
DerAccum der = {nullptr, 0, 0};
bool inCert = false;
size_t offset = 0;
while (offset < fsize) {
size_t pushed = br_pem_decoder_push(&pc, pem + offset, fsize - offset);
offset += pushed;
int ev = br_pem_decoder_event(&pc);
if (ev == BR_PEM_BEGIN_OBJ) {
inCert = (strcmp(br_pem_decoder_name(&pc), "CERTIFICATE") == 0);
br_pem_decoder_setdest(&pc, inCert ? der_append : nullptr, inCert ? &der : nullptr);
if (inCert) der.len = 0;
} else if (ev == BR_PEM_END_OBJ) {
if (inCert && der.len > 0) process_cert_der(&tas, der.data, der.len);
inCert = false;
} else if (ev == BR_PEM_ERROR) {
break;
}
}
if (der.data) free(der.data);
free(pem);
return tas;
} }
// ============================================================================ // ============================================================================
// BearSSL time // HTTPS fetch wrapper
// ============================================================================ // ============================================================================
static void get_bearssl_time(uint32_t* days, uint32_t* seconds) {
Zenith::DateTime dt;
zenith::gettime(&dt);
int y = dt.Year, m = dt.Month, d = dt.Day;
uint32_t total = 365u * (uint32_t)y
+ (uint32_t)(y/4) - (uint32_t)(y/100) + (uint32_t)(y/400);
const int md[] = {0,31,28,31,30,31,30,31,31,30,31,30,31};
for (int mo = 1; mo < m && mo <= 12; mo++) total += md[mo];
if (y%4==0 && (y%100!=0 || y%400==0) && m > 2) total++;
total += d - 1;
*days = total;
*seconds = (uint32_t)(dt.Hour*3600 + dt.Minute*60 + dt.Second);
}
// ============================================================================
// TLS I/O
// ============================================================================
static int tls_send_all(int fd, const unsigned char* data, size_t len) {
size_t sent = 0;
uint64_t deadline = zenith::get_milliseconds() + 15000;
while (sent < len) {
int r = zenith::send(fd, data + sent, (uint32_t)(len - sent));
if (r > 0) { sent += r; deadline = zenith::get_milliseconds() + 15000; }
else if (r < 0) return -1;
else { if (zenith::get_milliseconds() >= deadline) return -1; zenith::sleep_ms(1); }
}
return (int)sent;
}
static int tls_recv_some(int fd, unsigned char* buf, size_t maxlen) {
uint64_t deadline = zenith::get_milliseconds() + 15000;
while (true) {
int r = zenith::recv(fd, buf, (uint32_t)maxlen);
if (r > 0) return r;
if (r < 0) return -1;
if (zenith::get_milliseconds() >= deadline) return -1;
zenith::sleep_ms(1);
}
}
static int tls_exchange(int fd, br_ssl_engine_context* eng,
const char* request, int reqLen,
char* respBuf, int respMax) {
bool requestSent = false;
int respLen = 0;
uint64_t deadline = zenith::get_milliseconds() + 30000;
while (true) {
unsigned state = br_ssl_engine_current_state(eng);
if (state & BR_SSL_CLOSED) {
int err = br_ssl_engine_last_error(eng);
if (err != BR_ERR_OK && err != BR_ERR_IO && respLen == 0) return -1;
return respLen;
}
if (state & BR_SSL_SENDREC) {
size_t len; unsigned char* buf = br_ssl_engine_sendrec_buf(eng, &len);
int sent = tls_send_all(fd, buf, len);
if (sent < 0) { br_ssl_engine_close(eng); return respLen > 0 ? respLen : -1; }
br_ssl_engine_sendrec_ack(eng, len);
deadline = zenith::get_milliseconds() + 30000; continue;
}
if (state & BR_SSL_RECVAPP) {
size_t len; unsigned char* buf = br_ssl_engine_recvapp_buf(eng, &len);
size_t toCopy = len;
if (respLen + (int)toCopy > respMax - 1) toCopy = respMax - 1 - respLen;
if (toCopy > 0) { memcpy(respBuf + respLen, buf, toCopy); respLen += toCopy; }
br_ssl_engine_recvapp_ack(eng, len);
deadline = zenith::get_milliseconds() + 30000; continue;
}
if ((state & BR_SSL_SENDAPP) && !requestSent) {
size_t len; unsigned char* buf = br_ssl_engine_sendapp_buf(eng, &len);
size_t toWrite = (size_t)reqLen;
if (toWrite > len) toWrite = len;
memcpy(buf, request, toWrite);
br_ssl_engine_sendapp_ack(eng, toWrite);
br_ssl_engine_flush(eng, 0);
requestSent = true;
deadline = zenith::get_milliseconds() + 30000; continue;
}
if (state & BR_SSL_RECVREC) {
size_t len; unsigned char* buf = br_ssl_engine_recvrec_buf(eng, &len);
int got = tls_recv_some(fd, buf, len);
if (got < 0) { br_ssl_engine_close(eng); return respLen > 0 ? respLen : -1; }
br_ssl_engine_recvrec_ack(eng, got);
deadline = zenith::get_milliseconds() + 30000; continue;
}
if (zenith::get_milliseconds() >= deadline) return respLen > 0 ? respLen : -1;
zenith::sleep_ms(1);
}
}
static int wiki_fetch(const char* path, char* respBuf, int respMax) { static int wiki_fetch(const char* path, char* respBuf, int respMax) {
int fd = zenith::socket(Zenith::SOCK_TCP);
if (fd < 0) return -1;
if (zenith::connect(fd, g_server_ip, 443) < 0) { zenith::closesocket(fd); return -1; }
br_ssl_client_context* cc = (br_ssl_client_context*)malloc(sizeof(*cc));
br_x509_minimal_context* xc = (br_x509_minimal_context*)malloc(sizeof(*xc));
void* iobuf = malloc(BR_SSL_BUFSIZE_BIDI);
if (!cc || !xc || !iobuf) {
if (cc) free(cc); if (xc) free(xc); if (iobuf) free(iobuf);
zenith::closesocket(fd); return -1;
}
br_ssl_client_init_full(cc, xc, g_tas.anchors, g_tas.count);
uint32_t days, secs;
get_bearssl_time(&days, &secs);
br_x509_minimal_set_time(xc, days, secs);
unsigned char seed[32];
zenith::getrandom(seed, sizeof(seed));
br_ssl_engine_set_buffer(&cc->eng, iobuf, BR_SSL_BUFSIZE_BIDI, 1);
br_ssl_engine_inject_entropy(&cc->eng, seed, sizeof(seed));
if (!br_ssl_client_reset(cc, WIKI_HOST, 0)) {
zenith::closesocket(fd); free(cc); free(xc); free(iobuf); return -1;
}
static char request[2560]; static char request[2560];
int reqLen = snprintf(request, sizeof(request), int reqLen = snprintf(request, sizeof(request),
"GET %s HTTP/1.0\r\n" "GET %s HTTP/1.0\r\n"
@@ -379,11 +144,9 @@ static int wiki_fetch(const char* path, char* respBuf, int respMax) {
"Connection: close\r\n" "Connection: close\r\n"
"\r\n", "\r\n",
path, WIKI_HOST); path, WIKI_HOST);
return tls::https_fetch(WIKI_HOST, g_server_ip, 443,
int respLen = tls_exchange(fd, &cc->eng, request, reqLen, respBuf, respMax); request, reqLen, g_tas,
zenith::closesocket(fd); respBuf, respMax);
free(cc); free(xc); free(iobuf);
return respLen;
} }
// ============================================================================ // ============================================================================
@@ -624,7 +387,7 @@ static void do_search(const char* query) {
"Error: could not resolve en.wikipedia.org"); "Error: could not resolve en.wikipedia.org");
g_phase = AppPhase::ERR; return; g_phase = AppPhase::ERR; return;
} }
g_tas = load_trust_anchors(); g_tas = tls::load_trust_anchors();
if (g_tas.count == 0) { if (g_tas.count == 0) {
snprintf(g_status, sizeof(g_status), "Error: no CA certificates loaded"); snprintf(g_status, sizeof(g_status), "Error: no CA certificates loaded");
g_phase = AppPhase::ERR; return; g_phase = AppPhase::ERR; return;
@@ -793,6 +556,8 @@ extern "C" void _start() {
g_line_h = g_font->get_line_height(FONT_SIZE) + 4; g_line_h = g_font->get_line_height(FONT_SIZE) + 4;
apply_scale(zenith::win_getscale());
// Create window // Create window
Zenith::WinCreateResult wres; Zenith::WinCreateResult wres;
if (zenith::win_create("Wikipedia", INIT_W, INIT_H, &wres) < 0 || wres.id < 0) if (zenith::win_create("Wikipedia", INIT_W, INIT_H, &wres) < 0 || wres.id < 0)
@@ -823,6 +588,13 @@ extern "C" void _start() {
// ---- Handle event ---- // ---- Handle event ----
if (ev.type == 3) break; // close if (ev.type == 3) break; // close
if (ev.type == 4) {
apply_scale(ev.scale.scale);
if (g_phase == AppPhase::DONE && g_line_count > 0) {
build_display_lines(g_title, g_extract_buf, g_extract_len);
}
}
if (ev.type == 2) { if (ev.type == 2) {
int new_w = ev.resize.w; int new_w = ev.resize.w;
int new_h = ev.resize.h; int new_h = ev.resize.h;
BIN
View File
Binary file not shown.
+24
View File
@@ -57,6 +57,30 @@ ICONS=(
"apps/scalable/applications-science.svg" "apps/scalable/applications-science.svg"
"apps/scalable/hardware.svg" "apps/scalable/hardware.svg"
"apps/scalable/unsettings.svg" # settings icon; toolbox in flat remix "apps/scalable/unsettings.svg" # settings icon; toolbox in flat remix
# Weather app icon (for desktop app menu launcher)
"apps/scalable/weather-widget.svg"
# Weather condition icons (panel, colorful, used by weather.elf)
"panel/weather-clear.svg"
"panel/weather-clear-night.svg"
"panel/weather-few-clouds.svg"
"panel/weather-few-clouds-night.svg"
"panel/weather-clouds.svg"
"panel/weather-clouds-night.svg"
"panel/weather-overcast.svg"
"panel/weather-mist.svg"
"panel/weather-fog.svg"
"panel/weather-showers.svg"
"panel/weather-showers-scattered.svg"
"panel/weather-snow.svg"
"panel/weather-snow-scattered.svg"
"panel/weather-snow-rain.svg"
"panel/weather-freezing-rain.svg"
"panel/weather-hail.svg"
"panel/weather-storm.svg"
"panel/weather-many-clouds.svg"
"panel/weather-windy.svg"
"panel/weather-severe-alert.svg"
"panel/weather-none-available.svg"
) )
copied=0 copied=0