From 5e75e09e5d51cb00c7fd5f19c1a9c7f3b4091c96 Mon Sep 17 00:00:00 2001 From: Leonmmcoset Date: Sat, 11 Apr 2026 22:27:54 +0800 Subject: [PATCH] Stage 24 --- clks/drivers/video/framebuffer.c | 26 +++- clks/drivers/video/psf_font.c | 98 ++++++++++++- clks/drivers/video/psf_font.h | 3 +- clks/include/clks/panic.h | 10 ++ clks/kernel/interrupts.c | 7 +- clks/kernel/kmain.c | 2 +- clks/kernel/panic.c | 239 +++++++++++++++++++++++++++++++ clks/kernel/shell.c | 9 ++ docs/README.md | 1 + docs/stage24.md | 48 +++++++ ramdisk/system/tty.psf | Bin 4100 -> 12320 bytes scripts/gen-tty-psf.ps1 | 206 ++++++++++++++++++++------ 12 files changed, 591 insertions(+), 58 deletions(-) create mode 100644 clks/include/clks/panic.h create mode 100644 clks/kernel/panic.c create mode 100644 docs/stage24.md diff --git a/clks/drivers/video/framebuffer.c b/clks/drivers/video/framebuffer.c index 2a60b4d..c86b2e5 100644 --- a/clks/drivers/video/framebuffer.c +++ b/clks/drivers/video/framebuffer.c @@ -19,7 +19,7 @@ static struct clks_fb_state clks_fb = { .address = CLKS_NULL, .info = {0, 0, 0, 0}, .font = CLKS_NULL, - .external_font = {0, 0, 0, 0, CLKS_NULL}, + .external_font = {0, 0, 0, 0, 0, CLKS_NULL}, .external_font_active = CLKS_FALSE, .glyph_width = 8U, .glyph_height = 8U, @@ -149,6 +149,7 @@ void clks_fb_draw_char(u32 x, u32 y, char ch, u32 fg_rgb, u32 bg_rgb) { u32 col; u32 cols; u32 rows; + u32 row_stride; if (clks_fb.ready == CLKS_FALSE || clks_fb.font == CLKS_NULL) { return; @@ -167,15 +168,26 @@ void clks_fb_draw_char(u32 x, u32 y, char ch, u32 fg_rgb, u32 bg_rgb) { rows = 8U; } - if (cols > 8U) { - cols = 8U; + row_stride = clks_fb.font->bytes_per_row; + + if (row_stride == 0U) { + row_stride = (cols + 7U) / 8U; + } + + if (row_stride == 0U) { + return; + } + + if (((usize)row_stride * (usize)rows) > (usize)clks_fb.font->bytes_per_glyph) { + return; } for (row = 0U; row < rows; row++) { - u8 bits = glyph[row]; + const u8 *row_bits = glyph + ((usize)row * (usize)row_stride); for (col = 0U; col < cols; col++) { - u8 mask = (u8)(1U << (7U - col)); + u8 bits = row_bits[col >> 3U]; + u8 mask = (u8)(0x80U >> (col & 7U)); u32 color = (bits & mask) != 0U ? fg_rgb : bg_rgb; clks_fb_put_pixel(x + col, y + row, color); } @@ -183,7 +195,7 @@ void clks_fb_draw_char(u32 x, u32 y, char ch, u32 fg_rgb, u32 bg_rgb) { } clks_bool clks_fb_load_psf_font(const void *blob, u64 blob_size) { - struct clks_psf_font parsed = {0, 0, 0, 0, CLKS_NULL}; + struct clks_psf_font parsed = {0, 0, 0, 0, 0, CLKS_NULL}; if (clks_psf_parse_font(blob, blob_size, &parsed) == CLKS_FALSE) { return CLKS_FALSE; @@ -201,4 +213,4 @@ u32 clks_fb_cell_width(void) { u32 clks_fb_cell_height(void) { return clks_fb.glyph_height == 0U ? 8U : clks_fb.glyph_height; -} \ No newline at end of file +} diff --git a/clks/drivers/video/psf_font.c b/clks/drivers/video/psf_font.c index adc8597..1bfb629 100644 --- a/clks/drivers/video/psf_font.c +++ b/clks/drivers/video/psf_font.c @@ -10,12 +10,26 @@ #define CLKS_PSF1_GLYPH_BYTES 8U #define CLKS_PSF1_BLOB_SIZE (CLKS_PSF1_HEADER_SIZE + (CLKS_PSF1_GLYPH_COUNT * CLKS_PSF1_GLYPH_BYTES)) +#define CLKS_PSF2_MAGIC 0x864AB572U +#define CLKS_PSF2_HEADER_MIN_SIZE 32U + struct clks_psf1_header { u8 magic[2]; u8 mode; u8 charsize; }; +struct clks_psf2_header { + u32 magic; + u32 version; + u32 headersize; + u32 flags; + u32 length; + u32 charsize; + u32 height; + u32 width; +}; + struct clks_psf_seed_glyph { u8 code; u8 rows[CLKS_PSF1_GLYPH_BYTES]; @@ -74,7 +88,7 @@ static const struct clks_psf_seed_glyph clks_psf_seed_table[] = { }; static u8 clks_psf_default_blob[CLKS_PSF1_BLOB_SIZE]; -static struct clks_psf_font clks_psf_default = {8U, 8U, 0U, 0U, CLKS_NULL}; +static struct clks_psf_font clks_psf_default = {8U, 8U, 0U, 0U, 1U, CLKS_NULL}; static clks_bool clks_psf_default_ready = CLKS_FALSE; static clks_bool clks_psf_parse_psf1(const u8 *blob, usize blob_size, struct clks_psf_font *out_font) { @@ -114,10 +128,70 @@ static clks_bool clks_psf_parse_psf1(const u8 *blob, usize blob_size, struct clk out_font->height = glyph_bytes; out_font->glyph_count = glyph_count; out_font->bytes_per_glyph = glyph_bytes; + out_font->bytes_per_row = 1U; out_font->glyphs = blob + CLKS_PSF1_HEADER_SIZE; return CLKS_TRUE; } +static clks_bool clks_psf_parse_psf2(const u8 *blob, usize blob_size, struct clks_psf_font *out_font) { + const struct clks_psf2_header *hdr; + u32 bytes_per_row; + usize min_bytes_per_glyph; + usize payload_size; + + if (blob == CLKS_NULL || out_font == CLKS_NULL) { + return CLKS_FALSE; + } + + if (blob_size < CLKS_PSF2_HEADER_MIN_SIZE) { + return CLKS_FALSE; + } + + hdr = (const struct clks_psf2_header *)blob; + + if (hdr->magic != CLKS_PSF2_MAGIC) { + return CLKS_FALSE; + } + + if (hdr->headersize < CLKS_PSF2_HEADER_MIN_SIZE || hdr->headersize > blob_size) { + return CLKS_FALSE; + } + + if (hdr->width == 0U || hdr->height == 0U || hdr->length == 0U || hdr->charsize == 0U) { + return CLKS_FALSE; + } + + bytes_per_row = (hdr->width + 7U) / 8U; + + if (bytes_per_row == 0U) { + return CLKS_FALSE; + } + + min_bytes_per_glyph = (usize)bytes_per_row * (usize)hdr->height; + + if (min_bytes_per_glyph > hdr->charsize) { + return CLKS_FALSE; + } + + if ((usize)hdr->length > (((usize)-1) / (usize)hdr->charsize)) { + return CLKS_FALSE; + } + + payload_size = (usize)hdr->length * (usize)hdr->charsize; + + if (payload_size > (blob_size - (usize)hdr->headersize)) { + return CLKS_FALSE; + } + + out_font->width = hdr->width; + out_font->height = hdr->height; + out_font->glyph_count = hdr->length; + out_font->bytes_per_glyph = hdr->charsize; + out_font->bytes_per_row = bytes_per_row; + out_font->glyphs = blob + hdr->headersize; + return CLKS_TRUE; +} + static void clks_psf_seed_default_blob(void) { struct clks_psf1_header *hdr; u32 i; @@ -157,6 +231,7 @@ const struct clks_psf_font *clks_psf_default_font(void) { clks_psf_default.height = 8U; clks_psf_default.glyph_count = 1U; clks_psf_default.bytes_per_glyph = CLKS_PSF1_GLYPH_BYTES; + clks_psf_default.bytes_per_row = 1U; clks_psf_default.glyphs = clks_psf_unknown; } @@ -185,9 +260,26 @@ const u8 *clks_psf_glyph(const struct clks_psf_font *font, u32 codepoint) { } clks_bool clks_psf_parse_font(const void *blob, u64 blob_size, struct clks_psf_font *out_font) { - if (blob_size == 0ULL) { + const u8 *bytes; + u32 magic32; + + if (blob == CLKS_NULL || out_font == CLKS_NULL || blob_size == 0ULL) { return CLKS_FALSE; } - return clks_psf_parse_psf1((const u8 *)blob, (usize)blob_size, out_font); + bytes = (const u8 *)blob; + + if (blob_size >= 4ULL) { + magic32 = + ((u32)bytes[0]) | + (((u32)bytes[1]) << 8U) | + (((u32)bytes[2]) << 16U) | + (((u32)bytes[3]) << 24U); + + if (magic32 == CLKS_PSF2_MAGIC) { + return clks_psf_parse_psf2(bytes, (usize)blob_size, out_font); + } + } + + return clks_psf_parse_psf1(bytes, (usize)blob_size, out_font); } diff --git a/clks/drivers/video/psf_font.h b/clks/drivers/video/psf_font.h index 8089f79..26c43db 100644 --- a/clks/drivers/video/psf_font.h +++ b/clks/drivers/video/psf_font.h @@ -8,6 +8,7 @@ struct clks_psf_font { u32 height; u32 glyph_count; u32 bytes_per_glyph; + u32 bytes_per_row; const u8 *glyphs; }; @@ -15,4 +16,4 @@ const struct clks_psf_font *clks_psf_default_font(void); const u8 *clks_psf_glyph(const struct clks_psf_font *font, u32 codepoint); clks_bool clks_psf_parse_font(const void *blob, u64 blob_size, struct clks_psf_font *out_font); -#endif \ No newline at end of file +#endif diff --git a/clks/include/clks/panic.h b/clks/include/clks/panic.h new file mode 100644 index 0000000..8ceffe8 --- /dev/null +++ b/clks/include/clks/panic.h @@ -0,0 +1,10 @@ +#ifndef CLKS_PANIC_H +#define CLKS_PANIC_H + +#include +#include + +CLKS_NORETURN void clks_panic(const char *reason); +CLKS_NORETURN void clks_panic_exception(const char *name, u64 vector, u64 error_code, u64 rip); + +#endif \ No newline at end of file diff --git a/clks/kernel/interrupts.c b/clks/kernel/interrupts.c index a9cbf14..ec65204 100644 --- a/clks/kernel/interrupts.c +++ b/clks/kernel/interrupts.c @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -247,11 +248,7 @@ void clks_interrupt_dispatch(struct clks_interrupt_frame *frame) { } if (vector < 32U) { - clks_log(CLKS_LOG_ERROR, "EXC", clks_exception_names[vector]); - clks_log_hex(CLKS_LOG_ERROR, "EXC", "VECTOR", vector); - clks_log_hex(CLKS_LOG_ERROR, "EXC", "ERROR", frame->error_code); - clks_log_hex(CLKS_LOG_ERROR, "EXC", "RIP", frame->rip); - clks_cpu_halt_forever(); + clks_panic_exception(clks_exception_names[vector], vector, frame->error_code, frame->rip); } if (vector == CLKS_IRQ_TIMER) { diff --git a/clks/kernel/kmain.c b/clks/kernel/kmain.c index 4a04587..70d314b 100644 --- a/clks/kernel/kmain.c +++ b/clks/kernel/kmain.c @@ -96,7 +96,7 @@ void clks_kernel_main(void) { clks_tty_init(); } - clks_log(CLKS_LOG_INFO, "BOOT", "CLEONOS Stage23 START"); + clks_log(CLKS_LOG_INFO, "BOOT", "CLEONOS Stage24 START"); if (boot_fb == CLKS_NULL) { clks_log(CLKS_LOG_WARN, "VIDEO", "NO FRAMEBUFFER FROM LIMINE"); diff --git a/clks/kernel/panic.c b/clks/kernel/panic.c new file mode 100644 index 0000000..83d94b5 --- /dev/null +++ b/clks/kernel/panic.c @@ -0,0 +1,239 @@ +#include +#include +#include +#include +#include +#include + +#define CLKS_PANIC_BG 0x00200000U +#define CLKS_PANIC_FG 0x00FFE0E0U + +struct clks_panic_console { + u32 cols; + u32 rows; + u32 row; + u32 col; + u32 cell_w; + u32 cell_h; +}; + +static clks_bool clks_panic_active = CLKS_FALSE; + +static inline void clks_panic_disable_interrupts(void) { +#if defined(CLKS_ARCH_X86_64) + __asm__ volatile("cli"); +#elif defined(CLKS_ARCH_AARCH64) + __asm__ volatile("msr daifset, #0xf"); +#endif +} + +static void clks_panic_u64_to_hex(u64 value, char out[19]) { + int nibble; + + out[0] = '0'; + out[1] = 'X'; + + for (nibble = 0; nibble < 16; nibble++) { + u8 current = (u8)((value >> ((15 - nibble) * 4)) & 0x0FULL); + out[2 + nibble] = (current < 10U) ? (char)('0' + current) : (char)('A' + (current - 10U)); + } + + out[18] = '\0'; +} + +static clks_bool clks_panic_console_init(struct clks_panic_console *console) { + struct clks_framebuffer_info info; + + if (console == CLKS_NULL || clks_fb_ready() == CLKS_FALSE) { + return CLKS_FALSE; + } + + info = clks_fb_info(); + + console->cell_w = clks_fb_cell_width(); + console->cell_h = clks_fb_cell_height(); + + if (console->cell_w == 0U) { + console->cell_w = 8U; + } + + if (console->cell_h == 0U) { + console->cell_h = 8U; + } + + console->cols = info.width / console->cell_w; + console->rows = info.height / console->cell_h; + console->row = 0U; + console->col = 0U; + + if (console->cols == 0U || console->rows == 0U) { + return CLKS_FALSE; + } + + return CLKS_TRUE; +} + +static void clks_panic_console_newline(struct clks_panic_console *console) { + if (console == CLKS_NULL) { + return; + } + + console->col = 0U; + + if (console->row + 1U < console->rows) { + console->row++; + } +} + +static void clks_panic_console_put_char(struct clks_panic_console *console, char ch) { + u32 x; + u32 y; + + if (console == CLKS_NULL) { + return; + } + + if (ch == '\n') { + clks_panic_console_newline(console); + return; + } + + if (ch == '\r') { + console->col = 0U; + return; + } + + if (console->row >= console->rows || console->col >= console->cols) { + return; + } + + x = console->col * console->cell_w; + y = console->row * console->cell_h; + clks_fb_draw_char(x, y, ch, CLKS_PANIC_FG, CLKS_PANIC_BG); + + console->col++; + + if (console->col >= console->cols) { + clks_panic_console_newline(console); + } +} + +static void clks_panic_console_write(struct clks_panic_console *console, const char *text) { + usize i = 0U; + + if (console == CLKS_NULL || text == CLKS_NULL) { + return; + } + + while (text[i] != '\0') { + clks_panic_console_put_char(console, text[i]); + i++; + } +} + +static void clks_panic_serial_write_line(const char *line) { + if (line == CLKS_NULL) { + return; + } + + clks_serial_write(line); + clks_serial_write("\n"); +} + +static CLKS_NORETURN void clks_panic_halt_loop(void) { + clks_cpu_halt_forever(); +} + +CLKS_NORETURN void clks_panic(const char *reason) { + struct clks_panic_console console; + + clks_panic_disable_interrupts(); + + if (clks_panic_active == CLKS_TRUE) { + clks_panic_halt_loop(); + } + + clks_panic_active = CLKS_TRUE; + + clks_panic_serial_write_line("[PANIC] CLeonOS KERNEL PANIC"); + + if (reason != CLKS_NULL) { + clks_panic_serial_write_line(reason); + } + + if (clks_panic_console_init(&console) == CLKS_TRUE) { + clks_fb_clear(CLKS_PANIC_BG); + + clks_panic_console_write(&console, "CLeonOS KERNEL PANIC\n"); + clks_panic_console_write(&console, "====================\n\n"); + + if (reason != CLKS_NULL) { + clks_panic_console_write(&console, "REASON: "); + clks_panic_console_write(&console, reason); + clks_panic_console_write(&console, "\n"); + } + + clks_panic_console_write(&console, "\nSystem halted. Please reboot the VM.\n"); + } + + clks_panic_halt_loop(); +} + +CLKS_NORETURN void clks_panic_exception(const char *name, u64 vector, u64 error_code, u64 rip) { + struct clks_panic_console console; + char hex_buf[19]; + + clks_panic_disable_interrupts(); + + if (clks_panic_active == CLKS_TRUE) { + clks_panic_halt_loop(); + } + + clks_panic_active = CLKS_TRUE; + + clks_panic_serial_write_line("[PANIC] CPU EXCEPTION"); + + if (name != CLKS_NULL) { + clks_panic_serial_write_line(name); + } + + clks_panic_u64_to_hex(vector, hex_buf); + clks_panic_serial_write_line(hex_buf); + clks_panic_u64_to_hex(error_code, hex_buf); + clks_panic_serial_write_line(hex_buf); + clks_panic_u64_to_hex(rip, hex_buf); + clks_panic_serial_write_line(hex_buf); + + if (clks_panic_console_init(&console) == CLKS_TRUE) { + clks_fb_clear(CLKS_PANIC_BG); + + clks_panic_console_write(&console, "CLeonOS KERNEL PANIC\n"); + clks_panic_console_write(&console, "====================\n\n"); + clks_panic_console_write(&console, "TYPE: CPU EXCEPTION\n"); + + if (name != CLKS_NULL) { + clks_panic_console_write(&console, "NAME: "); + clks_panic_console_write(&console, name); + clks_panic_console_write(&console, "\n"); + } + + clks_panic_u64_to_hex(vector, hex_buf); + clks_panic_console_write(&console, "VECTOR: "); + clks_panic_console_write(&console, hex_buf); + clks_panic_console_write(&console, "\n"); + + clks_panic_u64_to_hex(error_code, hex_buf); + clks_panic_console_write(&console, "ERROR: "); + clks_panic_console_write(&console, hex_buf); + clks_panic_console_write(&console, "\n"); + + clks_panic_u64_to_hex(rip, hex_buf); + clks_panic_console_write(&console, "RIP: "); + clks_panic_console_write(&console, hex_buf); + clks_panic_console_write(&console, "\n\n"); + + clks_panic_console_write(&console, "System halted. Please reboot the VM.\n"); + } + + clks_panic_halt_loop(); +} \ No newline at end of file diff --git a/clks/kernel/shell.c b/clks/kernel/shell.c index 7aaf927..9400a5a 100644 --- a/clks/kernel/shell.c +++ b/clks/kernel/shell.c @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -696,6 +697,7 @@ static clks_bool clks_shell_cmd_help(void) { clks_shell_writeln(" dmesg [n]"); clks_shell_writeln(" shstat"); clks_shell_writeln(" rusttest"); + clks_shell_writeln(" panic"); clks_shell_writeln(" exec "); clks_shell_writeln(" clear"); clks_shell_writeln(" kbdstat"); @@ -1209,6 +1211,11 @@ static clks_bool clks_shell_cmd_rusttest(void) { return CLKS_TRUE; } +static clks_bool clks_shell_cmd_panic(void) { + clks_panic("MANUAL PANIC FROM KERNEL SHELL"); + return CLKS_FALSE; +} + static void clks_shell_execute_line(const char *line) { char line_buf[CLKS_SHELL_LINE_MAX]; char cmd[CLKS_SHELL_CMD_MAX]; @@ -1271,6 +1278,8 @@ static void clks_shell_execute_line(const char *line) { success = clks_shell_cmd_shstat(); } else if (clks_shell_streq(cmd, "rusttest") == CLKS_TRUE) { success = clks_shell_cmd_rusttest(); + } else if (clks_shell_streq(cmd, "panic") == CLKS_TRUE) { + success = clks_shell_cmd_panic(); } else if (clks_shell_streq(cmd, "exec") == CLKS_TRUE || clks_shell_streq(cmd, "run") == CLKS_TRUE) { success = clks_shell_cmd_exec(arg); } else if (clks_shell_streq(cmd, "clear") == CLKS_TRUE) { diff --git a/docs/README.md b/docs/README.md index 6234d3c..3dd2797 100644 --- a/docs/README.md +++ b/docs/README.md @@ -20,6 +20,7 @@ - `stage21.md` - `stage22.md` - `stage23.md` +- `stage24.md` ## Notes - Stage docs use a fixed template: goal, implementation, acceptance criteria, build targets, QEMU command, and debugging notes. diff --git a/docs/stage24.md b/docs/stage24.md new file mode 100644 index 0000000..a7fa4a2 --- /dev/null +++ b/docs/stage24.md @@ -0,0 +1,48 @@ +# CLeonOS Stage24 + +## Stage Goal +- Add a kernel shell command `panic`. +- Trigger a deliberate kernel panic from shell command input. +- Show a dedicated panic screen in framebuffer mode for manual panic and CPU exceptions. + +## What Was Implemented +- New panic subsystem: + - `clks/include/clks/panic.h` + - `clks/kernel/panic.c` +- Panic subsystem behavior: + - disable interrupts before panic halt + - write panic summary to serial (`-serial stdio`) + - draw full-screen panic page on framebuffer + - halt forever after panic +- Exception path integration: + - `clks/kernel/interrupts.c` now routes `vector < 32` to `clks_panic_exception(...)` +- Shell integration: + - `clks/kernel/shell.c` adds `panic` command + - `panic` command calls `clks_panic("MANUAL PANIC FROM KERNEL SHELL")` + - `help` output includes `panic` +- Stage banner updated: + - `CLEONOS Stage24 START` + +## Acceptance Criteria +- Kernel boots and prints `CLEONOS Stage24 START`. +- `help` lists `panic`. +- Typing `panic` in kernel shell shows panic page and system halts. +- Triggering CPU exceptions (if any) also enters the same panic screen flow. + +## Build Targets +- `make setup` +- `make userapps` +- `make iso` +- `make run` +- `make debug` + +## QEMU Command +- `qemu-system-x86_64 -M q35 -m 1024M -cdrom build/CLeonOS-x86_64.iso -serial stdio` + +## Common Bugs and Debugging +- `panic` command not found: + - Confirm `help` output contains `panic` and shell dispatch includes `clks_shell_cmd_panic()`. +- Panic screen not visible but serial has panic logs: + - Ensure framebuffer is available from Limine and `clks_fb_ready()` is true. +- Exception still prints old logs instead of panic page: + - Check `clks_interrupt_dispatch()` exception branch routes to `clks_panic_exception(...)`. diff --git a/ramdisk/system/tty.psf b/ramdisk/system/tty.psf index f59610e4a64dc9a85efa19b5c19fbb6bd001d3e6..d9b47104b65851e4ec9d7fd2c09fd6228a6c753d 100644 GIT binary patch literal 12320 zcmeHK&2HmH45lH#x-2a0Ex`tMuf5qlWF2&2`P4(7Vq62+X0uJ?E>L8#LBvzvsCNQA z^w<~J!@NoRamEwXV93X@=jaan22$;yYud+SbqC8 z)KVTT?Mkibk$wO@+AGds^_6_GQ!_Q0ok*9{Kh3G6Pi)9gMN4^*ulzUv#eecY@@QlH zVXci)SnJ<2E_jZbyhd&A(-~b*!8x~O*7Ob*=akWeyOeQ;^$j$|hg$QS)V834&Uw|* zI+O^us~5t2$kkr)T(8huD7|#3{+IH;B*x$*OG2X9WI(C0>I^6=!qs5Af)>1zr5G{x z4rHvs(@IJ%2E4j31Im|2%>f&|mr_bhU_m7xV3V^8mZ_g+@YC40EC0%KgSQ|=C1?We zWlTMhoWb5e_zHL#s5+^bfZFL}tz=j%z|0uC#Ofl(PKZ&Ny9j;D_fkn^Fi!C6?rMc` zUhsYho``LL?>$=oJrFOs64hU+Jv`2@-ZJ)JOPS*vCY*DLvoSWh*{S=~mFd50BIsmX zZzq!T2GLc)ih|*@ZC7vqsqua@>+>|vTY5m$NzHYPoeEjl^|sQme5OjRv4zA0YO2}k^d|VL9oJv0wMWg6ecnUYqxxI?2KwinzWzs`CldD`Cs!#h=KmSZ@)gfoptvPjpu)dpV69#s)!S>=l+e$0rC3i)m|H= zJJ7AU-QLKJT!EtY7_GKmr`qpxi>t^c&Tk;Ggv2>(CTf3kjNiqf)?L;rG($f0{Z=eK z7rbEY6vjy3xA)LEQBQe(tq^LBb{-jP58_T;OF-qjeOvdbufHV<%R|;hcksgTlG@U! z3idZEW3vs;n10F+Kr9#Dng#_^_;PfI;{mY`tY%V94c$^HNfS5iRXUEOQ1RO z;=u?zB7D40@*EEkl~6;3wq&C)$&%;G(*^;}KqLBNFC_R;@cD z;}=AX&k)UZgw-v-44X!S&(r8t9gX+$Dxq){XU&jRv3lKbQ}4i_HJ+XzmXZm8$>+a-3#4{6B@?!n^(*_2}6aUjeiR%n*me z|8_xEdn2s?`p@eEbyc6kyY@YkoSQQ>!|8Yj-3{fOwd9)h)3auWHBMZ^n-eT*?Kh2_ z$3~WDZJx|~yK7m{ew*eAfrLOpAR&+tNC+eZ5&{W1=WH#oP34a!_E2qFbR;JKg)^-ut^0PY=y;gLte=7R`3oHsPY z>tzEaAD;BGZXMV4J+6ET!^xAPoYzHSRXm<*|EHvw7mnllJlhc7xUM_JX$a>$T~$9j z&&JpaAt2*hei(_%x+s>`{ss}K>}<3-czjtDmY?HYo;dE5>@(8}8$YP}Ipm89qI$Y` zR%OF%w4k7(@MqJ_JQaV$GV&kBLOeb{Jw5%pk=i9s)3ub{c1y-ugo9JGgewI=91`A0 zFpEOkd{zGXSbf3;;Zu2Y^LTqJZN5r*qb@5?6KMUlh!BfY&JVPE6d*hRzYvCEscgORJv?u?Y7Z$Hu2dQ3L6# zQ~;&TPl!5>k0%q_Ck9WeuSX}x$0YyzD&>5dlrMyDrA$+0knj!0g`>mN(a<>4w`ze< zo5MZdbseoR)x&Ep-OnJEl1#n-bRaD391cvLxVb#1_z~_UCUWR6V|fJPg7}W(e2l?g zp7aJV!5gule(WPs{2Bl~|5Q~}V5XD_nEG-Bf(;ULd9B4MU*Bgm0cNEjPXl1=8DVIU zJgD8Ddj-9}H}*kQ8#e4CW0W5(8x_h2nHe1V;1LrfOaOiWcHX1tuO30W4~*$-&OaMv z+;RMAGMU)_17q9m{cg7#nzBqdPnIvQ