From 01dca717650a780bf8724dcbb29a5f558ea9418c Mon Sep 17 00:00:00 2001 From: CarterPerez-dev Date: Sun, 5 Jul 2026 15:36:46 -0400 Subject: [PATCH] feat: rveng M3 web app + M3.5 static-analysis engine depth M3 React web app over the existing FastAPI: hex viewer, disassembly pane, section map, strings, challenge runner with reveal-after-solve, and progress. Scaffolded from the open no-auth react-scss template; components kept self-contained for CertGames extraction. M3.5 static-analysis depth, all still no-execution and KAT-traced to the gate binary: PLT/GOT import resolution (plt.py), RIP-relative cross-references and callers (xref.py), basic-block control-flow graphs (cfg.py), and prologue-scan function discovery for stripped binaries (discover.py). Adds the 06-stripped-gate challenge. The API now resolves call names, exposes rip targets, and serves /cfg, /xrefs, and discovered functions. M2 API hardening from a read-only audit: challenge-loader error isolation, a request body-size middleware, session length bounds, CORS, and the disasm gate annotation gated behind solved state so it no longer pre-solves the challenge. 96 backend tests green; frontend typecheck, build, and lint clean. --- .../06-stripped-gate/challenge.json | 7 + .../challenges/06-stripped-gate/source.c | 23 + .../rveng/challenges/06-stripped-gate/target | Bin 0 -> 14416 bytes .../rveng/docs/research/05-static-analysis.md | 191 ++ .../advanced/rveng/docs/research/INDEX.md | 1 + .../advanced/rveng/frontend/.dockerignore | 15 + PROJECTS/advanced/rveng/frontend/.env.example | 6 + PROJECTS/advanced/rveng/frontend/.gitignore | 25 + .../advanced/rveng/frontend/.stylelintignore | 22 + PROJECTS/advanced/rveng/frontend/biome.json | 94 + PROJECTS/advanced/rveng/frontend/index.html | 48 + PROJECTS/advanced/rveng/frontend/package.json | 50 + .../advanced/rveng/frontend/pnpm-lock.yaml | 2603 +++++++++++++++++ .../public/assets/android-chrome-192x192.png | Bin 0 -> 21898 bytes .../public/assets/android-chrome-512x512.png | Bin 0 -> 112195 bytes .../public/assets/apple-touch-icon.png | Bin 0 -> 19669 bytes .../frontend/public/assets/favicon-16x16.png | Bin 0 -> 454 bytes .../frontend/public/assets/favicon-32x32.png | Bin 0 -> 1136 bytes .../rveng/frontend/public/assets/favicon.ico | Bin 0 -> 15406 bytes .../frontend/public/assets/site.webmanifest | 1 + PROJECTS/advanced/rveng/frontend/src/App.tsx | 36 + .../advanced/rveng/frontend/src/api/client.ts | 107 + .../rveng/frontend/src/api/hooks/index.ts | 119 + .../advanced/rveng/frontend/src/api/index.ts | 7 + .../rveng/frontend/src/api/types/index.ts | 136 + .../src/components/AnswerRunner.module.scss | 77 + .../frontend/src/components/AnswerRunner.tsx | 75 + .../src/components/CfgPane.module.scss | 95 + .../rveng/frontend/src/components/CfgPane.tsx | 79 + .../src/components/ChallengeCard.module.scss | 60 + .../frontend/src/components/ChallengeCard.tsx | 30 + .../src/components/DisasmPane.module.scss | 133 + .../frontend/src/components/DisasmPane.tsx | 112 + .../src/components/HexViewer.module.scss | 108 + .../frontend/src/components/HexViewer.tsx | 122 + .../src/components/MissionPanel.module.scss | 65 + .../frontend/src/components/MissionPanel.tsx | 36 + .../src/components/ProgressBar.module.scss | 33 + .../frontend/src/components/ProgressBar.tsx | 31 + .../src/components/SectionMap.module.scss | 67 + .../frontend/src/components/SectionMap.tsx | 48 + .../src/components/SourceReveal.module.scss | 39 + .../frontend/src/components/SourceReveal.tsx | 18 + .../src/components/StringsPane.module.scss | 74 + .../frontend/src/components/StringsPane.tsx | 49 + .../src/components/Workspace.module.scss | 104 + .../frontend/src/components/Workspace.tsx | 139 + .../rveng/frontend/src/components/index.tsx | 16 + .../advanced/rveng/frontend/src/config.ts | 102 + .../rveng/frontend/src/core/api/api.config.ts | 22 + .../rveng/frontend/src/core/api/errors.ts | 116 + .../rveng/frontend/src/core/api/index.ts | 8 + .../frontend/src/core/api/query.config.ts | 93 + .../rveng/frontend/src/core/app/routers.tsx | 28 + .../frontend/src/core/app/shell.module.scss | 62 + .../rveng/frontend/src/core/app/shell.tsx | 44 + .../frontend/src/core/app/toast.module.scss | 4 + .../rveng/frontend/src/core/lib/index.ts | 6 + .../frontend/src/core/lib/shell.ui.store.ts | 63 + .../advanced/rveng/frontend/src/lib/format.ts | 48 + .../rveng/frontend/src/lib/session.ts | 35 + PROJECTS/advanced/rveng/frontend/src/main.tsx | 15 + .../src/pages/browser/browser.module.scss | 44 + .../frontend/src/pages/browser/index.tsx | 53 + .../frontend/src/pages/workspace/index.tsx | 28 + .../src/pages/workspace/workspace.module.scss | 24 + .../advanced/rveng/frontend/src/styles.scss | 31 + .../rveng/frontend/src/styles/_fonts.scss | 9 + .../rveng/frontend/src/styles/_index.scss | 8 + .../rveng/frontend/src/styles/_mixins.scss | 42 + .../rveng/frontend/src/styles/_reset.scss | 75 + .../rveng/frontend/src/styles/_tokens.scss | 85 + .../rveng/frontend/stylelint.config.js | 107 + .../advanced/rveng/frontend/tsconfig.app.json | 31 + .../advanced/rveng/frontend/tsconfig.json | 7 + .../rveng/frontend/tsconfig.node.json | 24 + .../advanced/rveng/frontend/vite.config.ts | 70 + PROJECTS/advanced/rveng/src/rveng/api/app.py | 109 +- .../advanced/rveng/src/rveng/api/limits.py | 4 + .../rveng/src/rveng/api/middleware.py | 63 + .../advanced/rveng/src/rveng/api/schemas.py | 43 +- .../advanced/rveng/src/rveng/api/store.py | 57 +- .../advanced/rveng/src/rveng/engine/cfg.py | 106 + .../advanced/rveng/src/rveng/engine/disasm.py | 54 +- .../rveng/src/rveng/engine/discover.py | 41 + .../advanced/rveng/src/rveng/engine/plt.py | 101 + .../advanced/rveng/src/rveng/engine/xref.py | 55 + PROJECTS/advanced/rveng/tests/conftest.py | 10 + .../rveng/tests/fixtures/gate_stripped | Bin 0 -> 14416 bytes PROJECTS/advanced/rveng/tests/test_api.py | 126 +- PROJECTS/advanced/rveng/tests/test_cfg.py | 45 + .../advanced/rveng/tests/test_discover.py | 42 + PROJECTS/advanced/rveng/tests/test_plt.py | 51 + PROJECTS/advanced/rveng/tests/test_xref.py | 40 + 94 files changed, 7286 insertions(+), 46 deletions(-) create mode 100644 PROJECTS/advanced/rveng/challenges/06-stripped-gate/challenge.json create mode 100644 PROJECTS/advanced/rveng/challenges/06-stripped-gate/source.c create mode 100755 PROJECTS/advanced/rveng/challenges/06-stripped-gate/target create mode 100644 PROJECTS/advanced/rveng/docs/research/05-static-analysis.md create mode 100644 PROJECTS/advanced/rveng/frontend/.dockerignore create mode 100644 PROJECTS/advanced/rveng/frontend/.env.example create mode 100644 PROJECTS/advanced/rveng/frontend/.gitignore create mode 100644 PROJECTS/advanced/rveng/frontend/.stylelintignore create mode 100644 PROJECTS/advanced/rveng/frontend/biome.json create mode 100644 PROJECTS/advanced/rveng/frontend/index.html create mode 100644 PROJECTS/advanced/rveng/frontend/package.json create mode 100644 PROJECTS/advanced/rveng/frontend/pnpm-lock.yaml create mode 100644 PROJECTS/advanced/rveng/frontend/public/assets/android-chrome-192x192.png create mode 100644 PROJECTS/advanced/rveng/frontend/public/assets/android-chrome-512x512.png create mode 100644 PROJECTS/advanced/rveng/frontend/public/assets/apple-touch-icon.png create mode 100644 PROJECTS/advanced/rveng/frontend/public/assets/favicon-16x16.png create mode 100644 PROJECTS/advanced/rveng/frontend/public/assets/favicon-32x32.png create mode 100644 PROJECTS/advanced/rveng/frontend/public/assets/favicon.ico create mode 100644 PROJECTS/advanced/rveng/frontend/public/assets/site.webmanifest create mode 100644 PROJECTS/advanced/rveng/frontend/src/App.tsx create mode 100644 PROJECTS/advanced/rveng/frontend/src/api/client.ts create mode 100644 PROJECTS/advanced/rveng/frontend/src/api/hooks/index.ts create mode 100644 PROJECTS/advanced/rveng/frontend/src/api/index.ts create mode 100644 PROJECTS/advanced/rveng/frontend/src/api/types/index.ts create mode 100644 PROJECTS/advanced/rveng/frontend/src/components/AnswerRunner.module.scss create mode 100644 PROJECTS/advanced/rveng/frontend/src/components/AnswerRunner.tsx create mode 100644 PROJECTS/advanced/rveng/frontend/src/components/CfgPane.module.scss create mode 100644 PROJECTS/advanced/rveng/frontend/src/components/CfgPane.tsx create mode 100644 PROJECTS/advanced/rveng/frontend/src/components/ChallengeCard.module.scss create mode 100644 PROJECTS/advanced/rveng/frontend/src/components/ChallengeCard.tsx create mode 100644 PROJECTS/advanced/rveng/frontend/src/components/DisasmPane.module.scss create mode 100644 PROJECTS/advanced/rveng/frontend/src/components/DisasmPane.tsx create mode 100644 PROJECTS/advanced/rveng/frontend/src/components/HexViewer.module.scss create mode 100644 PROJECTS/advanced/rveng/frontend/src/components/HexViewer.tsx create mode 100644 PROJECTS/advanced/rveng/frontend/src/components/MissionPanel.module.scss create mode 100644 PROJECTS/advanced/rveng/frontend/src/components/MissionPanel.tsx create mode 100644 PROJECTS/advanced/rveng/frontend/src/components/ProgressBar.module.scss create mode 100644 PROJECTS/advanced/rveng/frontend/src/components/ProgressBar.tsx create mode 100644 PROJECTS/advanced/rveng/frontend/src/components/SectionMap.module.scss create mode 100644 PROJECTS/advanced/rveng/frontend/src/components/SectionMap.tsx create mode 100644 PROJECTS/advanced/rveng/frontend/src/components/SourceReveal.module.scss create mode 100644 PROJECTS/advanced/rveng/frontend/src/components/SourceReveal.tsx create mode 100644 PROJECTS/advanced/rveng/frontend/src/components/StringsPane.module.scss create mode 100644 PROJECTS/advanced/rveng/frontend/src/components/StringsPane.tsx create mode 100644 PROJECTS/advanced/rveng/frontend/src/components/Workspace.module.scss create mode 100644 PROJECTS/advanced/rveng/frontend/src/components/Workspace.tsx create mode 100644 PROJECTS/advanced/rveng/frontend/src/components/index.tsx create mode 100644 PROJECTS/advanced/rveng/frontend/src/config.ts create mode 100644 PROJECTS/advanced/rveng/frontend/src/core/api/api.config.ts create mode 100644 PROJECTS/advanced/rveng/frontend/src/core/api/errors.ts create mode 100644 PROJECTS/advanced/rveng/frontend/src/core/api/index.ts create mode 100644 PROJECTS/advanced/rveng/frontend/src/core/api/query.config.ts create mode 100644 PROJECTS/advanced/rveng/frontend/src/core/app/routers.tsx create mode 100644 PROJECTS/advanced/rveng/frontend/src/core/app/shell.module.scss create mode 100644 PROJECTS/advanced/rveng/frontend/src/core/app/shell.tsx create mode 100644 PROJECTS/advanced/rveng/frontend/src/core/app/toast.module.scss create mode 100644 PROJECTS/advanced/rveng/frontend/src/core/lib/index.ts create mode 100644 PROJECTS/advanced/rveng/frontend/src/core/lib/shell.ui.store.ts create mode 100644 PROJECTS/advanced/rveng/frontend/src/lib/format.ts create mode 100644 PROJECTS/advanced/rveng/frontend/src/lib/session.ts create mode 100644 PROJECTS/advanced/rveng/frontend/src/main.tsx create mode 100644 PROJECTS/advanced/rveng/frontend/src/pages/browser/browser.module.scss create mode 100644 PROJECTS/advanced/rveng/frontend/src/pages/browser/index.tsx create mode 100644 PROJECTS/advanced/rveng/frontend/src/pages/workspace/index.tsx create mode 100644 PROJECTS/advanced/rveng/frontend/src/pages/workspace/workspace.module.scss create mode 100644 PROJECTS/advanced/rveng/frontend/src/styles.scss create mode 100644 PROJECTS/advanced/rveng/frontend/src/styles/_fonts.scss create mode 100644 PROJECTS/advanced/rveng/frontend/src/styles/_index.scss create mode 100644 PROJECTS/advanced/rveng/frontend/src/styles/_mixins.scss create mode 100644 PROJECTS/advanced/rveng/frontend/src/styles/_reset.scss create mode 100644 PROJECTS/advanced/rveng/frontend/src/styles/_tokens.scss create mode 100644 PROJECTS/advanced/rveng/frontend/stylelint.config.js create mode 100644 PROJECTS/advanced/rveng/frontend/tsconfig.app.json create mode 100644 PROJECTS/advanced/rveng/frontend/tsconfig.json create mode 100644 PROJECTS/advanced/rveng/frontend/tsconfig.node.json create mode 100644 PROJECTS/advanced/rveng/frontend/vite.config.ts create mode 100644 PROJECTS/advanced/rveng/src/rveng/api/middleware.py create mode 100644 PROJECTS/advanced/rveng/src/rveng/engine/cfg.py create mode 100644 PROJECTS/advanced/rveng/src/rveng/engine/discover.py create mode 100644 PROJECTS/advanced/rveng/src/rveng/engine/plt.py create mode 100644 PROJECTS/advanced/rveng/src/rveng/engine/xref.py create mode 100755 PROJECTS/advanced/rveng/tests/fixtures/gate_stripped create mode 100644 PROJECTS/advanced/rveng/tests/test_cfg.py create mode 100644 PROJECTS/advanced/rveng/tests/test_discover.py create mode 100644 PROJECTS/advanced/rveng/tests/test_plt.py create mode 100644 PROJECTS/advanced/rveng/tests/test_xref.py diff --git a/PROJECTS/advanced/rveng/challenges/06-stripped-gate/challenge.json b/PROJECTS/advanced/rveng/challenges/06-stripped-gate/challenge.json new file mode 100644 index 00000000..ac6c9c4a --- /dev/null +++ b/PROJECTS/advanced/rveng/challenges/06-stripped-gate/challenge.json @@ -0,0 +1,7 @@ +{ + "id": "06-stripped-gate", + "module": "disassembly", + "title": "Gate in the dark", + "mission": "This binary is stripped: no symbol table, so there are no function names to look up. Discover the functions by their prologue, disassemble the one that checks a number, and read the constant it compares against. Answer in decimal or hex.", + "answer": { "category": "found_value", "expected": 1337 } +} diff --git a/PROJECTS/advanced/rveng/challenges/06-stripped-gate/source.c b/PROJECTS/advanced/rveng/challenges/06-stripped-gate/source.c new file mode 100644 index 00000000..1c42d31b --- /dev/null +++ b/PROJECTS/advanced/rveng/challenges/06-stripped-gate/source.c @@ -0,0 +1,23 @@ +#include +#include + +int check(int n) { + if (n == 1337) { + return 1; + } + return 0; +} + +int main(int argc, char **argv) { + char *secret = "the_flag_is_here"; + int n = 0; + if (argc > 1) { + n = atoi(argv[1]); + } + if (check(n)) { + printf("unlocked: %s\n", secret); + } else { + printf("wrong number\n"); + } + return 0; +} diff --git a/PROJECTS/advanced/rveng/challenges/06-stripped-gate/target b/PROJECTS/advanced/rveng/challenges/06-stripped-gate/target new file mode 100755 index 0000000000000000000000000000000000000000..602810968153700212a4848bef59d31c006723ce GIT binary patch literal 14416 zcmeHOU2IfE6rSBuy8N_VM6gnRE(Q`bblXB{pai&3OD{^a@+UsvcH7-{H@3UY?pDS|a*jNSu@0 z`^`CXzBzNxy)*kWb6=_3zSZmT2o|r{C{Szko6@AtLNruLm27K8kub$VQ7Wc`Wf~l& ztU**IMt`2FVDyn6nvr*c>QCgEvW9V?)C9y(!aYPp-W@Tis4$wZR7T-fI`U){>4L70 z@dgE8bn;|8L$xCeQxyoe6OYl!!`w`iDffvDipN;tRra-n%tIAL!P01LgsZ4uY-7u%gB!r{ZW{=a;ef+E;VI=(NN}GJm&4Z zYKO7>Py5+Gyb5Z#g7VK;Me!Ip-gCgieBQ6{5`NC(v@sz}mU;ePXE!I?BRo)gZYm3Fxon@VC ztIMjZ0;xn`m23yw;`X3;Uos249-6`v+nLI~=pObuup4l=1#>)AJE9|Pr$oq3#6&O% z*VPRKV^+{(yXFz8_XFz8_XFz8_XFzA*LCApBbKPeh%m1=m2`5daU%fEng@e&hCs7JAWY&Z+D{5Yt#FUzNxDK(D;9Epz2X zEAy?@Gcw+=tFE%I@{%<$5S{}~&3hhPe{KmBFL@2ylkN9A)hC5Gy8^yYTO06f&pnG? z*v%QAp&>im(%awSB$(n;>3`&=gv_{g?&c=z+(e$`xoBOvZO_TIP(UrX`9BpezIAv* zA#_Zn=kB&LS4R)RRb0&f9ez-F_VxcI9{OK9ijJA?$s^s9>)=a&y5KD4<66Fqwyz&+ zAknN9GY((s~fg@`rvjz-N5Kax3k$y-Q=;l zNl@N}PQZ090-td!a}k=@E1MXQAx8Iu@3msEHYn@pZZOEIOK>aYJ-+L%Vo)tQ13Cjb z13Cjb13Cjb13Cjb13Cjb13Cl$rwriwnH`M;TiQac!B{F7jU*!?9dAoCzZ41A7|T;L z#erlZ-fG0t?M;!S@Ra7Q$2C@5+Zz8qo9zXx7|&)K02={c2Auzc`tex>I0QNhu6d$( z4(<@1PQRygc7dz*2au_K)p{QKeht37c86#hYn+Yh2{9B7k zo+_Soz;{?|TCo0!)yrhF$lD9Y9_Zs@^xItIf5Tf_IQ<3a2&h=!3CAv|KLnVo_jV}< zFyXo&_7`L0+ARvM{dr|cpL7Ov26P5=26P5=26P5=26P5=26P4<%nX>Y0E^-$bpD<~ z`)oX+&_3xxrHK*pf0T6I&s<76?<-zFy4YyS68^qI;rd^0XA`KO$}aN+)z3r)WS0Vg`&m&+6o`3*47?|El=gM_%|l*>qL1(fiUt-Sz`M*^@sb7 zzco={lc^Mr59_V+diuy8)@!BCd4->qQa?@gME}o#_m8^+R@436O1Ov6?N?u0TVt$v zGSU84al1ez1=?UA_c&e^}w@ws;HHEy8ipZCifu15K}WHbJ^;pFo^e%{}Y|Ccxg z|I=^^zUG0?ERPp{AA{f1;Bm|xe!f{>2ZdkZS!X_v?^84&na_Kp-TkixKIVY=JpKoX z-%Ej}17d9E^EiAOZ0O2-o=+O-LV5lG7b))k?*uE3UFP$6JxzR`7rFoJ$JhvboC{dz z`9Qt|0ChZnxcd)PQgD*MP|WA~XPn|QpX0l)zl-==WNRWfpBY53$c`$`d2W6W*f1y9 zKPfyP_?61@B-P5kav2f-J9F|4;v2*_(a9lX&h6jNK%JA{Nc=`}eARWsn9ohV<>K?_ z)yN#>!SgozQ(9RSZxf&Mf9HK4In*r|^3#Re!_(z{QUmres773F3l+ z+?nqm8_qz%IsW;59DYy7U`IlC|NFrXdx+#1LKpsg=l5XT@mOcP44h&IpFjWQ3mIT% zKPXCJo$)KM; + + +# Static Analysis Depth Research + +The M3.5 depth features that lift rveng from a hex-and-disasm viewer to a real +static reverse-engineering workbench: import resolution through the PLT, cross +references with RIP-relative operand math, control-flow-graph reconstruction, +and function discovery in stripped binaries. Every offset, opcode, and address +below is traced to the sample `gate` binary (`gcc -no-pie -fno-stack-protector +-O0`) and its stripped copy `gate_stripped` (`strip gate`), captured with +`readelf` and `objdump -M intel`. Nothing here executes a binary: PLT, GOT, and +relocation tables are inspected as data, exactly like the header and sections. + +## Import resolution through the PLT + +A dynamically linked call does not jump straight to libc. It jumps to a small +stub in `.plt` that reads a pointer from `.got.plt`, which the dynamic linker +fills in. To name a call the way objdump prints `call 401030 `, the +engine resolves the stub back to the imported symbol. + +Sections that carry the mapping, from `readelf -SW gate`: + +- `.plt` PROGBITS at `0x401020`, entry size `0x10` (16 bytes). +- `.got.plt` PROGBITS at `0x403fe8`, entry size 8. +- `.rela.plt` RELA at `0x400520`, entry size `0x18` (24 bytes), 3 entries. +- `.dynsym` DYNSYM at `0x4003d0`, entry size `0x18`, its linked string table is + `.dynstr`. + +The `.plt` layout from `objdump -d -j .plt`: + +``` +401020 : push/jmp/nop the PLT0 resolver stub (not an import) +401030 : ff 25 ca 2f 00 00 jmp QWORD PTR [rip+0x2fca] -> 404000 +401040 : ff 25 c2 2f 00 00 jmp QWORD PTR [rip+0x2fc2] -> 404008 +401050 : ff 25 ba 2f 00 00 jmp QWORD PTR [rip+0x2fba] -> 404010 +``` + +The first `.plt` slot (`0x401020`) is the resolver trampoline (PLT0), not an +import. Real entries begin at `0x401030` and step by 16. + +Resolution algorithm, robust across PIE and non-PIE: + +1. For a PLT entry, decode its leading `jmp QWORD PTR [rip+disp32]` (opcode + `ff 25`). The GOT slot it reads is `entry_addr + 6 + disp32`. For `0x401030`: + `0x401036 + 0x2fca = 0x404000`. +2. `.rela.plt` maps each GOT slot to a dynamic symbol. Each `Elf64_Rela` is + `{ r_offset: u64, r_info: u64, r_addend: i64 }`. The symbol index is + `r_info >> 32`; the type is `r_info & 0xffffffff` (7 = `R_X86_64_JUMP_SLOT`). + From `readelf -rW`: + + ``` + r_offset 0x404000 sym 2 JUMP_SLOT puts + r_offset 0x404008 sym 3 JUMP_SLOT printf + r_offset 0x404010 sym 5 JUMP_SLOT atoi + ``` +3. Look the symbol index up in `.dynsym` (same 24-byte `Elf64_Sym` layout as + `.symtab`, names via `.dynstr`) to get `puts`, `printf`, `atoi`. + +So `0x401030 -> 0x404000 -> dynsym[2] -> puts`. A `call` whose target is a `.plt` +entry is then annotated with that name. + +KAT (call-site to import, from `main` in `objdump -d`): + +- `401199: call 401050` resolves to `atoi`. +- `4011a6: call 401146` is `check`, a local function, not a PLT entry. +- `4011c5: call 401040` resolves to `printf`. +- `4011d6: call 401030` resolves to `puts`. + +VERIFIED, and load-bearing for the stripped module: `.plt`, `.got.plt`, +`.rela.plt`, and `.dynsym` all SURVIVE `strip`. `readelf -SW gate_stripped` +still shows them; `readelf -rW gate_stripped` still lists the three JUMP_SLOT +relocations to `puts`, `printf`, `atoi`. Stripping removes `.symtab` and +`.strtab` (local names like `check`, `main`), never the dynamic-linking tables. +A stripped binary can still name its library calls. This is a real teaching +point, not an accident. + +Robustness note an adversarial check flagged: binaries built with +`-fcf-protection` (default on many modern toolchains) grow an `endbr64` and a +`.plt.sec` section, so the `jmp [rip+x]` may live in `.plt.sec` rather than +`.plt`. Resolving by decoding the stub's `jmp [rip+disp]` wherever it sits (and +matching the GOT slot against `.rela.plt`) handles both layouts; resolving by +the PLT entry's pushed relocation index does not. The engine decodes the jmp. +Our curated `-no-pie -O0` samples use the classic single `.plt`. + +## RIP-relative cross references + +x86-64 addresses data PC-relative. `lea rax,[rip+disp]` computes an absolute +address as `address_of_next_instruction + disp`, where the next-instruction +address is `insn.address + insn.size`. This is how a function points at a string +without a relocation in a non-PIE binary. + +KAT (from `main`, `objdump -d -j .text`): + +- `401173: lea rax,[rip+0xe8a]` with size 7 resolves to `0x40117a + 0xe8a = + 0x402004`, the `.rodata` string `the_flag_is_here`. +- `4011b6: lea rax,[rip+0xe58]` (size 7) resolves to `0x4011bd + 0xe58 = + 0x402015`, the `unlocked: %s` format string. +- `4011cc: lea rax,[rip+0xe50]` (size 7) resolves to `0x4011d3 + 0xe50 = + 0x402023`, the `wrong number` string. + +capstone exposes the displacement on the memory operand whose base register is +RIP. The engine computes `insn.address + insn.size + disp` and, when that +address falls inside a known section, labels the reference (a `.rodata` hit is a +string; a `.text` hit is code). + +Cross references are the inverse index: for a target address, the list of +instructions that reach it. Two reference kinds cover these challenges: + +- control-flow refs, from `call`/`jmp`/`jcc` branch targets. Example: `check` at + `0x401146` is referenced by `4011a6: call 401146` in `main`. +- data refs, from resolved RIP-relative operands. Example: the flag string at + `0x402004` is referenced by `401173: lea` in `main`. + +"What calls this function" and "what reads this string" are the two questions a +learner asks constantly; both are this same reverse map. + +## Control-flow graph reconstruction + +A basic block is a straight run of instructions with one entry and one exit: no +branch lands in the middle, no branch leaves except at the end. The standard +leader algorithm: + +1. The first instruction of the function is a leader. +2. Any branch target inside the function is a leader. +3. The instruction following any conditional or unconditional jump is a leader. + +A block runs from a leader up to the instruction before the next leader. `call` +does NOT end a block: it returns, so control falls through. Blocks end at +conditional jumps, unconditional jumps, and `ret`. This matches how IDA and +Ghidra draw function graphs. + +KAT: `check` (`0x401146`..`0x401163`), from `objdump -d`: + +``` +401146 push rbp | B0 (entry) +401147 mov rbp,rsp | +40114a mov [rbp-4],edi | +40114d cmp [rbp-4],0x539 | +401154 jne 40115d | -> ends B0 +401156 mov eax,1 | B1 (fallthrough of jne) +40115b jmp 401162 | -> ends B1 +40115d mov eax,0 | B2 (jne taken target) +401162 pop rbp | B3 (jmp target, also B2 fallthrough) +401163 ret | -> ends B3 +``` + +Leaders: `0x401146` (start), `0x401156` (after the `jne`), `0x40115d` (the `jne` +target), `0x401162` (the `jmp` target). Four basic blocks. Edges: + +- B0 ends in `jne`: to B2 (taken, `0x40115d`) and to B1 (fallthrough, `0x401156`). +- B1 ends in `jmp`: to B3 (`0x401162`). +- B2 ends by fallthrough (its successor `0x401162` is a leader): to B3. +- B3 ends in `ret`: no successors. + +A clean diamond, which is exactly the shape that teaches "the gate splits the +flow and both sides rejoin". Edge kinds worth labeling for the learner: taken, +fallthrough, unconditional. + +## Function discovery in stripped binaries + +`strip` removes `.symtab`, so `check` and `main` have no names and the symbol +table is empty. Disassembly must start from something other than a symbol. + +For the curated `-O0` challenges, every C function opens with the frame-pointer +prologue `push rbp; mov rbp,rsp`, bytes `55 48 89 e5`. Scanning executable +sections for that pattern recovers function entry points. Verified in +`gate_stripped`: `0x401146` (was `check`) and `0x401164` (was `main`) both begin +`55 48 89 e5`; `_start` at `0x401060` does not (it has a different prologue), so +the scan cleanly separates user functions from the runtime start-up code. + +Honest limits an adversarial check must record: this heuristic only finds +functions that keep a frame pointer. Anything built with `-O2` +(frame-pointer-omitted), or hand-written assembly, will be missed; real tools +add call-target recovery, symbol hints, and unwind-table (`.eh_frame`) parsing. +For rveng's curated, frame-pointer-preserving samples the prologue scan is +sufficient and honest, and it is the right first lesson in "how do you even find +a function when the names are gone". The stripped challenge disassembles a +discovered region by address, never by name, and grades a found-value answer, so +no symbol is ever leaked. + +## Facts pulled forward for the KATs + +- PLT entries: `0x401030 -> puts`, `0x401040 -> printf`, `0x401050 -> atoi`; + GOT slots `0x404000/8/10`; `.rela.plt` symbol index is `r_info >> 32`. +- RIP-relative: `target = insn.address + insn.size + disp`; `0x401173 lea -> + 0x402004` (the flag string). +- `check` CFG: 4 basic blocks, leaders `0x401146/0x401156/0x40115d/0x401162`, + diamond edges as above. +- Stripped discovery: prologue `55 48 89 e5` finds `check` and `main`, not + `_start`; `.dynsym`/`.rela.plt`/`.plt` survive `strip` so imports stay named. diff --git a/PROJECTS/advanced/rveng/docs/research/INDEX.md b/PROJECTS/advanced/rveng/docs/research/INDEX.md index 098d099c..52c3c015 100644 --- a/PROJECTS/advanced/rveng/docs/research/INDEX.md +++ b/PROJECTS/advanced/rveng/docs/research/INDEX.md @@ -23,6 +23,7 @@ binaries safe, and it is a hard constraint, not a preference. | `02-x86-64-capstone.md` | implementing `engine/disasm.py`: the minimal x86-64 instruction set these challenges use, the cmp/jcc gate pattern, capstone 5.0.9 API, the objdump-vs-capstone byte-for-byte agreement KAT, the Intel-syntax constant correction | | `03-pedagogy.md` | implementing `engine/challenge.py` and the runner: the solve-then-reveal loop, the three answer-spec categories (found-value, identified-symbol, patched-bytes), module-to-category mapping, grading rules | | `04-no-execution-posture.md` | any API or engine work touching binaries: the enumeration proving every operation is read-only, why patch grading is a static diff, the API input constraints, the standing no-execution rule | +| `05-static-analysis.md` | implementing the M3.5 depth engine (`plt.py`, `xref.py`, `cfg.py`, stripped discovery): PLT/GOT/.rela.plt/.dynsym import resolution, RIP-relative operand math, the basic-block leader algorithm, prologue-scan function discovery, all KAT-traced to `gate` and `gate_stripped` | Key verified facts pulled forward: - ELF header cross-checks exactly to raw bytes: entry `0x401060`, e_shoff diff --git a/PROJECTS/advanced/rveng/frontend/.dockerignore b/PROJECTS/advanced/rveng/frontend/.dockerignore new file mode 100644 index 00000000..a0256ec7 --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/.dockerignore @@ -0,0 +1,15 @@ +node_modules +build +dist +.git +.gitignore +*.md +.env* +.vscode +.idea +*.log +npm-debug.log* +pnpm-debug.log* +.DS_Store +coverage +.nyc_output diff --git a/PROJECTS/advanced/rveng/frontend/.env.example b/PROJECTS/advanced/rveng/frontend/.env.example new file mode 100644 index 00000000..f2f18850 --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/.env.example @@ -0,0 +1,6 @@ +# ©AngelaMos | 2026 +# .env.example + +# Backend the Vite dev server proxies /api to. +# Run the API with: uv run uvicorn rveng.api.server:app --port 8000 +VITE_API_TARGET=http://localhost:8000 diff --git a/PROJECTS/advanced/rveng/frontend/.gitignore b/PROJECTS/advanced/rveng/frontend/.gitignore new file mode 100644 index 00000000..61cb0c2e --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/.gitignore @@ -0,0 +1,25 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local +.vite + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/PROJECTS/advanced/rveng/frontend/.stylelintignore b/PROJECTS/advanced/rveng/frontend/.stylelintignore new file mode 100644 index 00000000..37da03e1 --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/.stylelintignore @@ -0,0 +1,22 @@ +# ©AngelaMos | 2025 +# .stylelintignore + +# Dependencies +node_modules/ + +# Production builds +dist/ +build/ +out/ + +# JS/TS files +**/*.js +**/*.jsx +**/*.ts +**/*.tsx + +# Generated files +*.min.css + +# Error system styles - ignore from linting +src/core/app/_toastStyles.scss diff --git a/PROJECTS/advanced/rveng/frontend/biome.json b/PROJECTS/advanced/rveng/frontend/biome.json new file mode 100644 index 00000000..7f29029a --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/biome.json @@ -0,0 +1,94 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.3.8/schema.json", + "vcs": { + "enabled": true, + "clientKind": "git", + "useIgnoreFile": true + }, + "files": { + "includes": ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx", "**/*.json"] + }, + "formatter": { + "enabled": true, + "indentStyle": "space", + "indentWidth": 2, + "lineWidth": 82, + "lineEnding": "lf" + }, + "javascript": { + "formatter": { + "quoteStyle": "single", + "jsxQuoteStyle": "double", + "semicolons": "asNeeded", + "trailingCommas": "es5", + "arrowParentheses": "always" + } + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true, + "complexity": { + "noExcessiveCognitiveComplexity": { + "level": "error", + "options": { "maxAllowedComplexity": 25 } + }, + "noForEach": "off", + "useLiteralKeys": "off" + }, + "correctness": { + "noUnusedVariables": "error", + "noUnusedImports": "error", + "useExhaustiveDependencies": "warn", + "useHookAtTopLevel": "error", + "noUndeclaredVariables": "error" + }, + "style": { + "useImportType": "error", + "useConst": "error", + "useTemplate": "error", + "useSelfClosingElements": "error", + "useFragmentSyntax": "error", + "noNonNullAssertion": "error", + "useConsistentArrayType": { + "level": "error", + "options": { "syntax": "shorthand" } + }, + "useNamingConvention": "off" + }, + "suspicious": { + "noExplicitAny": "error", + "noDebugger": "error", + "noConsole": "warn", + "noArrayIndexKey": "warn", + "noAssignInExpressions": "error", + "noDoubleEquals": "error", + "noRedeclare": "error", + "noVar": "error" + }, + "security": { + "noDangerouslySetInnerHtml": "error" + }, + "a11y": { + "useAltText": "error", + "useAnchorContent": "error", + "useKeyWithClickEvents": "error", + "noStaticElementInteractions": "error", + "useButtonType": "error", + "useValidAnchor": "error" + } + } + }, + "overrides": [ + { + "includes": ["src/main.tsx"], + "linter": { + "rules": { + "style": { + "noNonNullAssertion": "off" + } + } + } + } + ] +} diff --git a/PROJECTS/advanced/rveng/frontend/index.html b/PROJECTS/advanced/rveng/frontend/index.html new file mode 100644 index 00000000..840bb5f5 --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/index.html @@ -0,0 +1,48 @@ + + + + + + + + + + + rveng - reverse-engineering lab + + + + +
+ + + diff --git a/PROJECTS/advanced/rveng/frontend/package.json b/PROJECTS/advanced/rveng/frontend/package.json new file mode 100644 index 00000000..b7d1721b --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/package.json @@ -0,0 +1,50 @@ +{ + "name": "rveng-web", + "private": true, + "version": "1.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "preview": "vite preview", + "lint": "biome check .", + "lint:fix": "biome check --write .", + "format": "biome format --write .", + "typecheck": "tsc -b", + "lint:scss": "stylelint '**/*.scss'", + "lint:scss:fix": "stylelint '**/*.scss' --fix" + }, + "dependencies": { + "@tanstack/react-query": "^5.90.12", + "axios": "^1.13.0", + "react": "^19.2.1", + "react-dom": "^19.2.0", + "react-error-boundary": "^6.0.0", + "react-icon": "^1.0.0", + "react-icons": "^5.5.0", + "react-router-dom": "^7.1.1", + "sonner": "^2.0.7", + "zod": "^4.1.13", + "zustand": "^5.0.9" + }, + "devDependencies": { + "@biomejs/biome": "^2.3.8", + "@tanstack/react-query-devtools": "^5.91.1", + "@types/node": "^24.10.2", + "@types/react": "^19.2.7", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.1", + "sass": "^1.95.0", + "stylelint": "^16.26.1", + "stylelint-config-prettier-scss": "^1.0.0", + "stylelint-config-standard-scss": "^16.0.0", + "typescript": "~5.9.3", + "vite": "npm:rolldown-vite@7.2.5", + "vite-tsconfig-paths": "^5.1.0" + }, + "pnpm": { + "overrides": { + "vite": "npm:rolldown-vite@7.2.5" + } + } +} diff --git a/PROJECTS/advanced/rveng/frontend/pnpm-lock.yaml b/PROJECTS/advanced/rveng/frontend/pnpm-lock.yaml new file mode 100644 index 00000000..d04a7163 --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/pnpm-lock.yaml @@ -0,0 +1,2603 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +overrides: + vite: npm:rolldown-vite@7.2.5 + +importers: + + .: + dependencies: + '@tanstack/react-query': + specifier: ^5.90.12 + version: 5.90.12(react@19.2.1) + axios: + specifier: ^1.13.0 + version: 1.13.2 + react: + specifier: ^19.2.1 + version: 19.2.1 + react-dom: + specifier: ^19.2.0 + version: 19.2.1(react@19.2.1) + react-error-boundary: + specifier: ^6.0.0 + version: 6.0.0(react@19.2.1) + react-icon: + specifier: ^1.0.0 + version: 1.0.0(babel-runtime@5.8.38)(react@19.2.1) + react-icons: + specifier: ^5.5.0 + version: 5.5.0(react@19.2.1) + react-router-dom: + specifier: ^7.1.1 + version: 7.10.1(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + sonner: + specifier: ^2.0.7 + version: 2.0.7(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + zod: + specifier: ^4.1.13 + version: 4.1.13 + zustand: + specifier: ^5.0.9 + version: 5.0.9(@types/react@19.2.7)(react@19.2.1) + devDependencies: + '@biomejs/biome': + specifier: ^2.3.8 + version: 2.3.8 + '@tanstack/react-query-devtools': + specifier: ^5.91.1 + version: 5.91.1(@tanstack/react-query@5.90.12(react@19.2.1))(react@19.2.1) + '@types/node': + specifier: ^24.10.2 + version: 24.10.2 + '@types/react': + specifier: ^19.2.7 + version: 19.2.7 + '@types/react-dom': + specifier: ^19.2.3 + version: 19.2.3(@types/react@19.2.7) + '@vitejs/plugin-react': + specifier: ^5.1.1 + version: 5.1.2(rolldown-vite@7.2.5(@types/node@24.10.2)(sass@1.95.0)) + sass: + specifier: ^1.95.0 + version: 1.95.0 + stylelint: + specifier: ^16.26.1 + version: 16.26.1(typescript@5.9.3) + stylelint-config-prettier-scss: + specifier: ^1.0.0 + version: 1.0.0(stylelint@16.26.1(typescript@5.9.3)) + stylelint-config-standard-scss: + specifier: ^16.0.0 + version: 16.0.0(postcss@8.5.6)(stylelint@16.26.1(typescript@5.9.3)) + typescript: + specifier: ~5.9.3 + version: 5.9.3 + vite: + specifier: npm:rolldown-vite@7.2.5 + version: rolldown-vite@7.2.5(@types/node@24.10.2)(sass@1.95.0) + vite-tsconfig-paths: + specifier: ^5.1.0 + version: 5.1.4(rolldown-vite@7.2.5(@types/node@24.10.2)(sass@1.95.0))(typescript@5.9.3) + +packages: + + '@babel/code-frame@7.27.1': + resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.28.5': + resolution: {integrity: sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.28.5': + resolution: {integrity: sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.28.5': + resolution: {integrity: sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.27.2': + resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.27.1': + resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.28.3': + resolution: {integrity: sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-plugin-utils@7.27.1': + resolution: {integrity: sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.28.4': + resolution: {integrity: sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.28.5': + resolution: {integrity: sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-transform-react-jsx-self@7.27.1': + resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-source@7.27.1': + resolution: {integrity: sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/runtime@7.28.4': + resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==} + engines: {node: '>=6.9.0'} + + '@babel/template@7.27.2': + resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.28.5': + resolution: {integrity: sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.28.5': + resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==} + engines: {node: '>=6.9.0'} + + '@biomejs/biome@2.3.8': + resolution: {integrity: sha512-Qjsgoe6FEBxWAUzwFGFrB+1+M8y/y5kwmg5CHac+GSVOdmOIqsAiXM5QMVGZJ1eCUCLlPZtq4aFAQ0eawEUuUA==} + engines: {node: '>=14.21.3'} + hasBin: true + + '@biomejs/cli-darwin-arm64@2.3.8': + resolution: {integrity: sha512-HM4Zg9CGQ3txTPflxD19n8MFPrmUAjaC7PQdLkugeeC0cQ+PiVrd7i09gaBS/11QKsTDBJhVg85CEIK9f50Qww==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [darwin] + + '@biomejs/cli-darwin-x64@2.3.8': + resolution: {integrity: sha512-lUDQ03D7y/qEao7RgdjWVGCu+BLYadhKTm40HkpJIi6kn8LSv5PAwRlew/DmwP4YZ9ke9XXoTIQDO1vAnbRZlA==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [darwin] + + '@biomejs/cli-linux-arm64-musl@2.3.8': + resolution: {integrity: sha512-PShR4mM0sjksUMyxbyPNMxoKFPVF48fU8Qe8Sfx6w6F42verbwRLbz+QiKNiDPRJwUoMG1nPM50OBL3aOnTevA==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + + '@biomejs/cli-linux-arm64@2.3.8': + resolution: {integrity: sha512-Uo1OJnIkJgSgF+USx970fsM/drtPcQ39I+JO+Fjsaa9ZdCN1oysQmy6oAGbyESlouz+rzEckLTF6DS7cWse95g==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + + '@biomejs/cli-linux-x64-musl@2.3.8': + resolution: {integrity: sha512-YGLkqU91r1276uwSjiUD/xaVikdxgV1QpsicT0bIA1TaieM6E5ibMZeSyjQ/izBn4tKQthUSsVZacmoJfa3pDA==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + + '@biomejs/cli-linux-x64@2.3.8': + resolution: {integrity: sha512-QDPMD5bQz6qOVb3kiBui0zKZXASLo0NIQ9JVJio5RveBEFgDgsvJFUvZIbMbUZT3T00M/1wdzwWXk4GIh0KaAw==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + + '@biomejs/cli-win32-arm64@2.3.8': + resolution: {integrity: sha512-H4IoCHvL1fXKDrTALeTKMiE7GGWFAraDwBYFquE/L/5r1927Te0mYIGseXi4F+lrrwhSWbSGt5qPFswNoBaCxg==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [win32] + + '@biomejs/cli-win32-x64@2.3.8': + resolution: {integrity: sha512-RguzimPoZWtBapfKhKjcWXBVI91tiSprqdBYu7tWhgN8pKRZhw24rFeNZTNf6UiBfjCYCi9eFQs/JzJZIhuK4w==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [win32] + + '@cacheable/memory@2.0.6': + resolution: {integrity: sha512-7e8SScMocHxcAb8YhtkbMhGG+EKLRIficb1F5sjvhSYsWTZGxvg4KIDp8kgxnV2PUJ3ddPe6J9QESjKvBWRDkg==} + + '@cacheable/utils@2.3.2': + resolution: {integrity: sha512-8kGE2P+HjfY8FglaOiW+y8qxcaQAfAhVML+i66XJR3YX5FtyDqn6Txctr3K2FrbxLKixRRYYBWMbuGciOhYNDg==} + + '@csstools/css-parser-algorithms@3.0.5': + resolution: {integrity: sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-syntax-patches-for-csstree@1.0.20': + resolution: {integrity: sha512-8BHsjXfSciZxjmHQOuVdW2b8WLUPts9a+mfL13/PzEviufUEW2xnvQuOlKs9dRBHgRqJ53SF/DUoK9+MZk72oQ==} + engines: {node: '>=18'} + + '@csstools/css-tokenizer@3.0.4': + resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==} + engines: {node: '>=18'} + + '@csstools/media-query-list-parser@4.0.3': + resolution: {integrity: sha512-HAYH7d3TLRHDOUQK4mZKf9k9Ph/m8Akstg66ywKR4SFAigjs3yBiUeZtFxywiTm5moZMAp/5W/ZuFnNXXYLuuQ==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/selector-specificity@5.0.0': + resolution: {integrity: sha512-PCqQV3c4CoVm3kdPhyeZ07VmBRdH2EpMFA/pd9OASpOEC3aXNGoqPDAZ80D0cLpMBxnmk0+yNhGsEx31hq7Gtw==} + engines: {node: '>=18'} + peerDependencies: + postcss-selector-parser: ^7.0.0 + + '@dual-bundle/import-meta-resolve@4.2.1': + resolution: {integrity: sha512-id+7YRUgoUX6CgV0DtuhirQWodeeA7Lf4i2x71JS/vtA5pRb/hIGWlw+G6MeXvsM+MXrz0VAydTGElX1rAfgPg==} + + '@emnapi/core@1.7.1': + resolution: {integrity: sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==} + + '@emnapi/runtime@1.7.1': + resolution: {integrity: sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==} + + '@emnapi/wasi-threads@1.1.0': + resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@keyv/bigmap@1.3.0': + resolution: {integrity: sha512-KT01GjzV6AQD5+IYrcpoYLkCu1Jod3nau1Z7EsEuViO3TZGRacSbO9MfHmbJ1WaOXFtWLxPVj169cn2WNKPkIg==} + engines: {node: '>= 18'} + peerDependencies: + keyv: ^5.5.4 + + '@keyv/serialize@1.1.1': + resolution: {integrity: sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==} + + '@napi-rs/wasm-runtime@1.1.0': + resolution: {integrity: sha512-Fq6DJW+Bb5jaWE69/qOE0D1TUN9+6uWhCeZpdnSBk14pjLcCWR7Q8n49PTSPHazM37JqrsdpEthXy2xn6jWWiA==} + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@oxc-project/runtime@0.97.0': + resolution: {integrity: sha512-yH0zw7z+jEws4dZ4IUKoix5Lh3yhqIJWF9Dc8PWvhpo7U7O+lJrv7ZZL4BeRO0la8LBQFwcCewtLBnVV7hPe/w==} + engines: {node: ^20.19.0 || >=22.12.0} + + '@oxc-project/types@0.97.0': + resolution: {integrity: sha512-lxmZK4xFrdvU0yZiDwgVQTCvh2gHWBJCBk5ALsrtsBWhs0uDIi+FTOnXRQeQfs304imdvTdaakT/lqwQ8hkOXQ==} + + '@parcel/watcher-android-arm64@2.5.1': + resolution: {integrity: sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [android] + + '@parcel/watcher-darwin-arm64@2.5.1': + resolution: {integrity: sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [darwin] + + '@parcel/watcher-darwin-x64@2.5.1': + resolution: {integrity: sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [darwin] + + '@parcel/watcher-freebsd-x64@2.5.1': + resolution: {integrity: sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [freebsd] + + '@parcel/watcher-linux-arm-glibc@2.5.1': + resolution: {integrity: sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + + '@parcel/watcher-linux-arm-musl@2.5.1': + resolution: {integrity: sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + + '@parcel/watcher-linux-arm64-glibc@2.5.1': + resolution: {integrity: sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + + '@parcel/watcher-linux-arm64-musl@2.5.1': + resolution: {integrity: sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + + '@parcel/watcher-linux-x64-glibc@2.5.1': + resolution: {integrity: sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + + '@parcel/watcher-linux-x64-musl@2.5.1': + resolution: {integrity: sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + + '@parcel/watcher-win32-arm64@2.5.1': + resolution: {integrity: sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [win32] + + '@parcel/watcher-win32-ia32@2.5.1': + resolution: {integrity: sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==} + engines: {node: '>= 10.0.0'} + cpu: [ia32] + os: [win32] + + '@parcel/watcher-win32-x64@2.5.1': + resolution: {integrity: sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [win32] + + '@parcel/watcher@2.5.1': + resolution: {integrity: sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==} + engines: {node: '>= 10.0.0'} + + '@rolldown/binding-android-arm64@1.0.0-beta.50': + resolution: {integrity: sha512-XlEkrOIHLyGT3avOgzfTFSjG+f+dZMw+/qd+Y3HLN86wlndrB/gSimrJCk4gOhr1XtRtEKfszpadI3Md4Z4/Ag==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.0.0-beta.50': + resolution: {integrity: sha512-+JRqKJhoFlt5r9q+DecAGPLZ5PxeLva+wCMtAuoFMWPoZzgcYrr599KQ+Ix0jwll4B4HGP43avu9My8KtSOR+w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.0.0-beta.50': + resolution: {integrity: sha512-fFXDjXnuX7/gQZQm/1FoivVtRcyAzdjSik7Eo+9iwPQ9EgtA5/nB2+jmbzaKtMGG3q+BnZbdKHCtOacmNrkIDA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.0.0-beta.50': + resolution: {integrity: sha512-F1b6vARy49tjmT/hbloplzgJS7GIvwWZqt+tAHEstCh0JIh9sa8FAMVqEmYxDviqKBaAI8iVvUREm/Kh/PD26Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.50': + resolution: {integrity: sha512-U6cR76N8T8M6lHj7EZrQ3xunLPxSvYYxA8vJsBKZiFZkT8YV4kjgCO3KwMJL0NOjQCPGKyiXO07U+KmJzdPGRw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.50': + resolution: {integrity: sha512-ONgyjofCrrE3bnh5GZb8EINSFyR/hmwTzZ7oVuyUB170lboza1VMCnb8jgE6MsyyRgHYmN8Lb59i3NKGrxrYjw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@rolldown/binding-linux-arm64-musl@1.0.0-beta.50': + resolution: {integrity: sha512-L0zRdH2oDPkmB+wvuTl+dJbXCsx62SkqcEqdM+79LOcB+PxbAxxjzHU14BuZIQdXcAVDzfpMfaHWzZuwhhBTcw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@rolldown/binding-linux-x64-gnu@1.0.0-beta.50': + resolution: {integrity: sha512-gyoI8o/TGpQd3OzkJnh1M2kxy1Bisg8qJ5Gci0sXm9yLFzEXIFdtc4EAzepxGvrT2ri99ar5rdsmNG0zP0SbIg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@rolldown/binding-linux-x64-musl@1.0.0-beta.50': + resolution: {integrity: sha512-zti8A7M+xFDpKlghpcCAzyOi+e5nfUl3QhU023ce5NCgUxRG5zGP2GR9LTydQ1rnIPwZUVBWd4o7NjZDaQxaXA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@rolldown/binding-openharmony-arm64@1.0.0-beta.50': + resolution: {integrity: sha512-eZUssog7qljrrRU9Mi0eqYEPm3Ch0UwB+qlWPMKSUXHNqhm3TvDZarJQdTevGEfu3EHAXJvBIe0YFYr0TPVaMA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-wasm32-wasi@1.0.0-beta.50': + resolution: {integrity: sha512-nmCN0nIdeUnmgeDXiQ+2HU6FT162o+rxnF7WMkBm4M5Ds8qTU7Dzv2Wrf22bo4ftnlrb2hKK6FSwAJSAe2FWLg==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.50': + resolution: {integrity: sha512-7kcNLi7Ua59JTTLvbe1dYb028QEPaJPJQHqkmSZ5q3tJueUeb6yjRtx8mw4uIqgWZcnQHAR3PrLN4XRJxvgIkA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-ia32-msvc@1.0.0-beta.50': + resolution: {integrity: sha512-lL70VTNvSCdSZkDPPVMwWn/M2yQiYvSoXw9hTLgdIWdUfC3g72UaruezusR6ceRuwHCY1Ayu2LtKqXkBO5LIwg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ia32] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.0.0-beta.50': + resolution: {integrity: sha512-4qU4x5DXWB4JPjyTne/wBNPqkbQU8J45bl21geERBKtEittleonioACBL1R0PsBu0Aq21SwMK5a9zdBkWSlQtQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.0-beta.50': + resolution: {integrity: sha512-5e76wQiQVeL1ICOZVUg4LSOVYg9jyhGCin+icYozhsUzM+fHE7kddi1bdiE0jwVqTfkjba3jUFbEkoC9WkdvyA==} + + '@rolldown/pluginutils@1.0.0-beta.53': + resolution: {integrity: sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==} + + '@tanstack/query-core@5.90.12': + resolution: {integrity: sha512-T1/8t5DhV/SisWjDnaiU2drl6ySvsHj1bHBCWNXd+/T+Hh1cf6JodyEYMd5sgwm+b/mETT4EV3H+zCVczCU5hg==} + + '@tanstack/query-devtools@5.91.1': + resolution: {integrity: sha512-l8bxjk6BMsCaVQH6NzQEE/bEgFy1hAs5qbgXl0xhzezlaQbPk6Mgz9BqEg2vTLPOHD8N4k+w/gdgCbEzecGyNg==} + + '@tanstack/react-query-devtools@5.91.1': + resolution: {integrity: sha512-tRnJYwEbH0kAOuToy8Ew7bJw1lX3AjkkgSlf/vzb+NpnqmHPdWM+lA2DSdGQSLi1SU0PDRrrCI1vnZnci96CsQ==} + peerDependencies: + '@tanstack/react-query': ^5.90.10 + react: ^18 || ^19 + + '@tanstack/react-query@5.90.12': + resolution: {integrity: sha512-graRZspg7EoEaw0a8faiUASCyJrqjKPdqJ9EwuDRUF9mEYJ1YPczI9H+/agJ0mOJkPCJDk0lsz5QTrLZ/jQ2rg==} + peerDependencies: + react: ^18 || ^19 + + '@tybys/wasm-util@0.10.1': + resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} + + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.28.0': + resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + + '@types/node@24.10.2': + resolution: {integrity: sha512-WOhQTZ4G8xZ1tjJTvKOpyEVSGgOTvJAfDK3FNFgELyaTpzhdgHVHeqW8V+UJvzF5BT+/B54T/1S2K6gd9c7bbA==} + + '@types/react-dom@19.2.3': + resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} + peerDependencies: + '@types/react': ^19.2.0 + + '@types/react@19.2.7': + resolution: {integrity: sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==} + + '@vitejs/plugin-react@5.1.2': + resolution: {integrity: sha512-EcA07pHJouywpzsoTUqNh5NwGayl2PPVEJKUSinGGSxFGYn+shYbqMGBg6FXDqgXum9Ou/ecb+411ssw8HImJQ==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + + ajv@8.17.1: + resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + array-union@2.1.0: + resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} + engines: {node: '>=8'} + + astral-regex@2.0.0: + resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} + engines: {node: '>=8'} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + axios@1.13.2: + resolution: {integrity: sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==} + + babel-runtime@5.8.38: + resolution: {integrity: sha512-KpgoA8VE/pMmNCrnEeeXqFG24TIH11Z3ZaimIhJWsin8EbfZy3WzFKUTIan10ZIDgRVvi9EkLbruJElJC9dRlg==} + + balanced-match@2.0.0: + resolution: {integrity: sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA==} + + baseline-browser-mapping@2.9.5: + resolution: {integrity: sha512-D5vIoztZOq1XM54LUdttJVc96ggEsIfju2JBvht06pSzpckp3C7HReun67Bghzrtdsq9XdMGbSSB3v3GhMNmAA==} + hasBin: true + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + browserslist@4.28.1: + resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + cacheable@2.3.0: + resolution: {integrity: sha512-HHiAvOBmlcR2f3SQ7kdlYD8+AUJG+wlFZ/Ze8tl1Vzvz0MdOh8IYA/EFU4ve8t1/sZ0j4MGi7ST5MoTwHessQA==} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + caniuse-lite@1.0.30001759: + resolution: {integrity: sha512-Pzfx9fOKoKvevQf8oCXoyNRQ5QyxJj+3O0Rqx2V5oxT61KGx8+n6hV/IUyJeifUci2clnmmKVpvtiqRzgiWjSw==} + + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + colord@2.9.3: + resolution: {integrity: sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cookie@1.1.1: + resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} + engines: {node: '>=18'} + + core-js@1.2.7: + resolution: {integrity: sha512-ZiPp9pZlgxpWRu0M+YWbm6+aQ84XEfH1JRXvfOc/fILWI0VKhLC2LX13X1NYq4fULzLMq7Hfh43CSo2/aIaUPA==} + deprecated: core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js. + + cosmiconfig@9.0.0: + resolution: {integrity: sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==} + engines: {node: '>=14'} + peerDependencies: + typescript: '>=4.9.5' + peerDependenciesMeta: + typescript: + optional: true + + css-functions-list@3.2.3: + resolution: {integrity: sha512-IQOkD3hbR5KrN93MtcYuad6YPuTSUhntLHDuLEbFWE+ff2/XSZNdZG+LcbbIW5AXKg/WFIfYItIzVoHngHXZzA==} + engines: {node: '>=12 || >=16'} + + css-tree@3.1.0: + resolution: {integrity: sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + detect-libc@1.0.3: + resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==} + engines: {node: '>=0.10'} + hasBin: true + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + dir-glob@3.0.1: + resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} + engines: {node: '>=8'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + electron-to-chromium@1.5.267: + resolution: {integrity: sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + env-paths@2.2.1: + resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} + engines: {node: '>=6'} + + error-ex@1.3.4: + resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fast-uri@3.1.0: + resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} + + fastest-levenshtein@1.0.16: + resolution: {integrity: sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==} + engines: {node: '>= 4.9.1'} + + fastq@1.19.1: + resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + file-entry-cache@11.1.1: + resolution: {integrity: sha512-TPVFSDE7q91Dlk1xpFLvFllf8r0HyOMOlnWy7Z2HBku5H3KhIeOGInexrIeg2D64DosVB/JXkrrk6N/7Wriq4A==} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + flat-cache@6.1.19: + resolution: {integrity: sha512-l/K33newPTZMTGAnnzaiqSl6NnH7Namh8jBNjrgjprWxGmZUuxx/sJNIRaijOh3n7q7ESbhNZC+pvVZMFdeU4A==} + + flatted@3.3.3: + resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + + follow-redirects@1.15.11: + resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + + form-data@4.0.5: + resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} + engines: {node: '>= 6'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + global-modules@2.0.0: + resolution: {integrity: sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==} + engines: {node: '>=6'} + + global-prefix@3.0.0: + resolution: {integrity: sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==} + engines: {node: '>=6'} + + globby@11.1.0: + resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} + engines: {node: '>=10'} + + globjoin@0.1.4: + resolution: {integrity: sha512-xYfnw62CKG8nLkZBfWbhWwDw02CHty86jfPcc2cr3ZfeuK9ysoVPPEUxf21bAD/rWAgk52SuBrLJlefNy8mvFg==} + + globrex@0.1.2: + resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hashery@1.3.0: + resolution: {integrity: sha512-fWltioiy5zsSAs9ouEnvhsVJeAXRybGCNNv0lvzpzNOSDbULXRy7ivFWwCCv4I5Am6kSo75hmbsCduOoc2/K4w==} + engines: {node: '>=20'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + hookified@1.14.0: + resolution: {integrity: sha512-pi1ynXIMFx/uIIwpWJ/5CEtOHLGtnUB0WhGeeYT+fKcQ+WCQbm3/rrkAXnpfph++PgepNqPdTC2WTj8A6k6zoQ==} + + html-tags@3.3.1: + resolution: {integrity: sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==} + engines: {node: '>=8'} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + + immutable@5.1.4: + resolution: {integrity: sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + + is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-plain-object@5.0.0: + resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==} + engines: {node: '>=0.10.0'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@4.1.1: + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} + hasBin: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + keyv@5.5.5: + resolution: {integrity: sha512-FA5LmZVF1VziNc0bIdCSA1IoSVnDCqE8HJIZZv2/W8YmoAM50+tnUgJR/gQZwEeIMleuIOnRnHA/UaZRNeV4iQ==} + + kind-of@6.0.3: + resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} + engines: {node: '>=0.10.0'} + + known-css-properties@0.37.0: + resolution: {integrity: sha512-JCDrsP4Z1Sb9JwG0aJ8Eo2r7k4Ou5MwmThS/6lcIe1ICyb7UBJKGRIUUdqc2ASdE/42lgz6zFUnzAIhtXnBVrQ==} + + lightningcss-android-arm64@1.30.2: + resolution: {integrity: sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.30.2: + resolution: {integrity: sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.30.2: + resolution: {integrity: sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.30.2: + resolution: {integrity: sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.30.2: + resolution: {integrity: sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.30.2: + resolution: {integrity: sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-arm64-musl@1.30.2: + resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-x64-gnu@1.30.2: + resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-linux-x64-musl@1.30.2: + resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-win32-arm64-msvc@1.30.2: + resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.30.2: + resolution: {integrity: sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.30.2: + resolution: {integrity: sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==} + engines: {node: '>= 12.0.0'} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + lodash.truncate@4.4.2: + resolution: {integrity: sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + mathml-tag-names@2.1.3: + resolution: {integrity: sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==} + + mdn-data@2.12.2: + resolution: {integrity: sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==} + + mdn-data@2.25.0: + resolution: {integrity: sha512-T2LPsjgUE/tgMmRXREVmwsux89DwWfNjiynOeXuLd2mX6jphGQ2YE3Ukz7LQ2VOFKiVZU/Ee1GqzHiipZCjymw==} + + meow@13.2.0: + resolution: {integrity: sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==} + engines: {node: '>=18'} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + node-addon-api@7.1.1: + resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} + + node-releases@2.0.27: + resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + + path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + + postcss-media-query-parser@0.2.3: + resolution: {integrity: sha512-3sOlxmbKcSHMjlUXQZKQ06jOswE7oVkXPxmZdoB1r5l0q6gTFTQSHxNxOrCccElbW7dxNytifNEo8qidX2Vsig==} + + postcss-resolve-nested-selector@0.1.6: + resolution: {integrity: sha512-0sglIs9Wmkzbr8lQwEyIzlDOOC9bGmfVKcJTaxv3vMmd3uo4o4DerC3En0bnmgceeql9BfC8hRkp7cg0fjdVqw==} + + postcss-safe-parser@7.0.1: + resolution: {integrity: sha512-0AioNCJZ2DPYz5ABT6bddIqlhgwhpHZ/l65YAYo0BCIn0xiDpsnTHz0gnoTGk0OXZW0JRs+cDwL8u/teRdz+8A==} + engines: {node: '>=18.0'} + peerDependencies: + postcss: ^8.4.31 + + postcss-scss@4.0.9: + resolution: {integrity: sha512-AjKOeiwAitL/MXxQW2DliT28EKukvvbEWx3LBmJIRN8KfBGZbRTxNYW0kSqi1COiTZ57nZ9NW06S6ux//N1c9A==} + engines: {node: '>=12.0'} + peerDependencies: + postcss: ^8.4.29 + + postcss-selector-parser@7.1.1: + resolution: {integrity: sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==} + engines: {node: '>=4'} + + postcss-value-parser@4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} + + proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + + qified@0.5.3: + resolution: {integrity: sha512-kXuQdQTB6oN3KhI6V4acnBSZx8D2I4xzZvn9+wFLLFCoBNQY/sFnCW6c43OL7pOQ2HvGV4lnWIXNmgfp7cTWhQ==} + engines: {node: '>=20'} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + react-dom@19.2.1: + resolution: {integrity: sha512-ibrK8llX2a4eOskq1mXKu/TGZj9qzomO+sNfO98M6d9zIPOEhlBkMkBUBLd1vgS0gQsLDBzA+8jJBVXDnfHmJg==} + peerDependencies: + react: ^19.2.1 + + react-error-boundary@6.0.0: + resolution: {integrity: sha512-gdlJjD7NWr0IfkPlaREN2d9uUZUlksrfOx7SX62VRerwXbMY6ftGCIZua1VG1aXFNOimhISsTq+Owp725b9SiA==} + peerDependencies: + react: '>=16.13.1' + + react-icon@1.0.0: + resolution: {integrity: sha512-VzSlpBHnLanVw79mOxyq98hWDi6DlxK9qPiZ1bAK6bLurMBCaxO/jjyYUrRx9+JGLc/NbnwOmyE/W5Qglbb2QA==} + peerDependencies: + babel-runtime: ^5.3.3 + react: '>=0.12.0' + + react-icons@5.5.0: + resolution: {integrity: sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==} + peerDependencies: + react: '*' + + react-refresh@0.18.0: + resolution: {integrity: sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==} + engines: {node: '>=0.10.0'} + + react-router-dom@7.10.1: + resolution: {integrity: sha512-JNBANI6ChGVjA5bwsUIwJk7LHKmqB4JYnYfzFwyp2t12Izva11elds2jx7Yfoup2zssedntwU0oZ5DEmk5Sdaw==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: '>=18' + react-dom: '>=18' + + react-router@7.10.1: + resolution: {integrity: sha512-gHL89dRa3kwlUYtRQ+m8NmxGI6CgqN+k4XyGjwcFoQwwCWF6xXpOCUlDovkXClS0d0XJN/5q7kc5W3kiFEd0Yw==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: '>=18' + react-dom: '>=18' + peerDependenciesMeta: + react-dom: + optional: true + + react@19.2.1: + resolution: {integrity: sha512-DGrYcCWK7tvYMnWh79yrPHt+vdx9tY+1gPZa7nJQtO/p8bLTDaHp4dzwEhQB7pZ4Xe3ok4XKuEPrVuc+wlpkmw==} + engines: {node: '>=0.10.0'} + + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rolldown-vite@7.2.5: + resolution: {integrity: sha512-u09tdk/huMiN8xwoiBbig197jKdCamQTtOruSalOzbqGje3jdHiV0njQlAW0YvzoahkirFePNQ4RYlfnRQpXZA==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + esbuild: ^0.25.0 + jiti: '>=1.21.0' + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + rolldown@1.0.0-beta.50: + resolution: {integrity: sha512-JFULvCNl/anKn99eKjOSEubi0lLmNqQDAjyEMME2T4CwezUDL0i6t1O9xZsu2OMehPnV2caNefWpGF+8TnzB6A==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + sass@1.95.0: + resolution: {integrity: sha512-9QMjhLq+UkOg/4bb8Lt8A+hJZvY3t+9xeZMKSBtBEgxrXA3ed5Ts4NDreUkYgJP1BTmrscQE/xYhf7iShow6lw==} + engines: {node: '>=14.0.0'} + hasBin: true + + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + set-cookie-parser@2.7.2: + resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + + slice-ansi@4.0.0: + resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==} + engines: {node: '>=10'} + + sonner@2.0.7: + resolution: {integrity: sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==} + peerDependencies: + react: ^18.0.0 || ^19.0.0 || ^19.0.0-rc + react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-rc + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + stylelint-config-prettier-scss@1.0.0: + resolution: {integrity: sha512-Gr2qLiyvJGKeDk0E/+awNTrZB/UtNVPLqCDOr07na/sLekZwm26Br6yYIeBYz3ulsEcQgs5j+2IIMXCC+wsaQA==} + engines: {node: 14.* || 16.* || >= 18} + hasBin: true + peerDependencies: + stylelint: '>=15.0.0' + + stylelint-config-recommended-scss@16.0.2: + resolution: {integrity: sha512-aUTHhPPWCvFyWaxtckJlCPaXTDFsp4pKO8evXNCsW9OwsaUWyMd6jvcUhSmfGWPrTddvzNqK4rS/UuSLcbVGdQ==} + engines: {node: '>=20'} + peerDependencies: + postcss: ^8.3.3 + stylelint: ^16.24.0 + peerDependenciesMeta: + postcss: + optional: true + + stylelint-config-recommended@17.0.0: + resolution: {integrity: sha512-WaMSdEiPfZTSFVoYmJbxorJfA610O0tlYuU2aEwY33UQhSPgFbClrVJYWvy3jGJx+XW37O+LyNLiZOEXhKhJmA==} + engines: {node: '>=18.12.0'} + peerDependencies: + stylelint: ^16.23.0 + + stylelint-config-standard-scss@16.0.0: + resolution: {integrity: sha512-/FHECLUu+med/e6OaPFpprG86ShC4SYT7Tzb2PTVdDjJsehhFBOioSlWqYFqJxmGPIwO3AMBxNo+kY3dxrbczA==} + engines: {node: '>=20'} + peerDependencies: + postcss: ^8.3.3 + stylelint: ^16.23.1 + peerDependenciesMeta: + postcss: + optional: true + + stylelint-config-standard@39.0.1: + resolution: {integrity: sha512-b7Fja59EYHRNOTa3aXiuWnhUWXFU2Nfg6h61bLfAb5GS5fX3LMUD0U5t4S8N/4tpHQg3Acs2UVPR9jy2l1g/3A==} + engines: {node: '>=18.12.0'} + peerDependencies: + stylelint: ^16.23.0 + + stylelint-scss@6.13.0: + resolution: {integrity: sha512-kZPwFUJkfup2gP1enlrS2h9U5+T5wFoqzJ1n/56AlpwSj28kmFe7ww/QFydvPsg5gLjWchAwWWBLtterynZrOw==} + engines: {node: '>=18.12.0'} + peerDependencies: + stylelint: ^16.8.2 + + stylelint@16.26.1: + resolution: {integrity: sha512-v20V59/crfc8sVTAtge0mdafI3AdnzQ2KsWe6v523L4OA1bJO02S7MO2oyXDCS6iWb9ckIPnqAFVItqSBQr7jw==} + engines: {node: '>=18.12.0'} + hasBin: true + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-hyperlinks@3.2.0: + resolution: {integrity: sha512-zFObLMyZeEwzAoKCyu1B91U79K2t7ApXuQfo8OuxwXLDgcKxuwM+YvcbIhm6QWqz7mHUH1TVytR1PwVVjEuMig==} + engines: {node: '>=14.18'} + + svg-tags@1.0.0: + resolution: {integrity: sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==} + + table@6.9.0: + resolution: {integrity: sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==} + engines: {node: '>=10.0.0'} + + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + tsconfck@3.1.6: + resolution: {integrity: sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==} + engines: {node: ^18 || >=20} + hasBin: true + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@7.16.0: + resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + + update-browserslist-db@1.2.2: + resolution: {integrity: sha512-E85pfNzMQ9jpKkA7+TJAi4TJN+tBCuWh5rUcS/sv6cFi+1q9LYDwDI5dpUL0u/73EElyQ8d3TEaeW4sPedBqYA==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + vite-tsconfig-paths@5.1.4: + resolution: {integrity: sha512-cYj0LRuLV2c2sMqhqhGpaO3LretdtMn/BVX4cPLanIZuwwrkVl+lK84E/miEXkCHWXuq65rhNN4rXsBcOB3S4w==} + peerDependencies: + vite: '*' + peerDependenciesMeta: + vite: + optional: true + + which@1.3.1: + resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} + hasBin: true + + write-file-atomic@5.0.1: + resolution: {integrity: sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + zod@4.1.13: + resolution: {integrity: sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==} + + zustand@5.0.9: + resolution: {integrity: sha512-ALBtUj0AfjJt3uNRQoL1tL2tMvj6Gp/6e39dnfT6uzpelGru8v1tPOGBzayOWbPJvujM8JojDk3E1LxeFisBNg==} + engines: {node: '>=12.20.0'} + peerDependencies: + '@types/react': '>=18.0.0' + immer: '>=9.0.6' + react: '>=18.0.0' + use-sync-external-store: '>=1.2.0' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + use-sync-external-store: + optional: true + +snapshots: + + '@babel/code-frame@7.27.1': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.28.5': {} + + '@babel/core@7.28.5': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.28.5 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.5) + '@babel/helpers': 7.28.4 + '@babel/parser': 7.28.5 + '@babel/template': 7.27.2 + '@babel/traverse': 7.28.5 + '@babel/types': 7.28.5 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.28.5': + dependencies: + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@7.27.2': + dependencies: + '@babel/compat-data': 7.28.5 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.28.1 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-globals@7.28.0': {} + + '@babel/helper-module-imports@7.27.1': + dependencies: + '@babel/traverse': 7.28.5 + '@babel/types': 7.28.5 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.28.3(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-module-imports': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.28.5 + transitivePeerDependencies: + - supports-color + + '@babel/helper-plugin-utils@7.27.1': {} + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/helper-validator-option@7.27.1': {} + + '@babel/helpers@7.28.4': + dependencies: + '@babel/template': 7.27.2 + '@babel/types': 7.28.5 + + '@babel/parser@7.28.5': + dependencies: + '@babel/types': 7.28.5 + + '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/runtime@7.28.4': {} + + '@babel/template@7.27.2': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 + + '@babel/traverse@7.28.5': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.28.5 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.28.5 + '@babel/template': 7.27.2 + '@babel/types': 7.28.5 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.28.5': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + + '@biomejs/biome@2.3.8': + optionalDependencies: + '@biomejs/cli-darwin-arm64': 2.3.8 + '@biomejs/cli-darwin-x64': 2.3.8 + '@biomejs/cli-linux-arm64': 2.3.8 + '@biomejs/cli-linux-arm64-musl': 2.3.8 + '@biomejs/cli-linux-x64': 2.3.8 + '@biomejs/cli-linux-x64-musl': 2.3.8 + '@biomejs/cli-win32-arm64': 2.3.8 + '@biomejs/cli-win32-x64': 2.3.8 + + '@biomejs/cli-darwin-arm64@2.3.8': + optional: true + + '@biomejs/cli-darwin-x64@2.3.8': + optional: true + + '@biomejs/cli-linux-arm64-musl@2.3.8': + optional: true + + '@biomejs/cli-linux-arm64@2.3.8': + optional: true + + '@biomejs/cli-linux-x64-musl@2.3.8': + optional: true + + '@biomejs/cli-linux-x64@2.3.8': + optional: true + + '@biomejs/cli-win32-arm64@2.3.8': + optional: true + + '@biomejs/cli-win32-x64@2.3.8': + optional: true + + '@cacheable/memory@2.0.6': + dependencies: + '@cacheable/utils': 2.3.2 + '@keyv/bigmap': 1.3.0(keyv@5.5.5) + hookified: 1.14.0 + keyv: 5.5.5 + + '@cacheable/utils@2.3.2': + dependencies: + hashery: 1.3.0 + keyv: 5.5.5 + + '@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-syntax-patches-for-csstree@1.0.20': {} + + '@csstools/css-tokenizer@3.0.4': {} + + '@csstools/media-query-list-parser@4.0.3(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/selector-specificity@5.0.0(postcss-selector-parser@7.1.1)': + dependencies: + postcss-selector-parser: 7.1.1 + + '@dual-bundle/import-meta-resolve@4.2.1': {} + + '@emnapi/core@1.7.1': + dependencies: + '@emnapi/wasi-threads': 1.1.0 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.7.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.1.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@keyv/bigmap@1.3.0(keyv@5.5.5)': + dependencies: + hashery: 1.3.0 + hookified: 1.14.0 + keyv: 5.5.5 + + '@keyv/serialize@1.1.1': {} + + '@napi-rs/wasm-runtime@1.1.0': + dependencies: + '@emnapi/core': 1.7.1 + '@emnapi/runtime': 1.7.1 + '@tybys/wasm-util': 0.10.1 + optional: true + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.19.1 + + '@oxc-project/runtime@0.97.0': {} + + '@oxc-project/types@0.97.0': {} + + '@parcel/watcher-android-arm64@2.5.1': + optional: true + + '@parcel/watcher-darwin-arm64@2.5.1': + optional: true + + '@parcel/watcher-darwin-x64@2.5.1': + optional: true + + '@parcel/watcher-freebsd-x64@2.5.1': + optional: true + + '@parcel/watcher-linux-arm-glibc@2.5.1': + optional: true + + '@parcel/watcher-linux-arm-musl@2.5.1': + optional: true + + '@parcel/watcher-linux-arm64-glibc@2.5.1': + optional: true + + '@parcel/watcher-linux-arm64-musl@2.5.1': + optional: true + + '@parcel/watcher-linux-x64-glibc@2.5.1': + optional: true + + '@parcel/watcher-linux-x64-musl@2.5.1': + optional: true + + '@parcel/watcher-win32-arm64@2.5.1': + optional: true + + '@parcel/watcher-win32-ia32@2.5.1': + optional: true + + '@parcel/watcher-win32-x64@2.5.1': + optional: true + + '@parcel/watcher@2.5.1': + dependencies: + detect-libc: 1.0.3 + is-glob: 4.0.3 + micromatch: 4.0.8 + node-addon-api: 7.1.1 + optionalDependencies: + '@parcel/watcher-android-arm64': 2.5.1 + '@parcel/watcher-darwin-arm64': 2.5.1 + '@parcel/watcher-darwin-x64': 2.5.1 + '@parcel/watcher-freebsd-x64': 2.5.1 + '@parcel/watcher-linux-arm-glibc': 2.5.1 + '@parcel/watcher-linux-arm-musl': 2.5.1 + '@parcel/watcher-linux-arm64-glibc': 2.5.1 + '@parcel/watcher-linux-arm64-musl': 2.5.1 + '@parcel/watcher-linux-x64-glibc': 2.5.1 + '@parcel/watcher-linux-x64-musl': 2.5.1 + '@parcel/watcher-win32-arm64': 2.5.1 + '@parcel/watcher-win32-ia32': 2.5.1 + '@parcel/watcher-win32-x64': 2.5.1 + optional: true + + '@rolldown/binding-android-arm64@1.0.0-beta.50': + optional: true + + '@rolldown/binding-darwin-arm64@1.0.0-beta.50': + optional: true + + '@rolldown/binding-darwin-x64@1.0.0-beta.50': + optional: true + + '@rolldown/binding-freebsd-x64@1.0.0-beta.50': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.50': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.50': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.0.0-beta.50': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.0.0-beta.50': + optional: true + + '@rolldown/binding-linux-x64-musl@1.0.0-beta.50': + optional: true + + '@rolldown/binding-openharmony-arm64@1.0.0-beta.50': + optional: true + + '@rolldown/binding-wasm32-wasi@1.0.0-beta.50': + dependencies: + '@napi-rs/wasm-runtime': 1.1.0 + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.50': + optional: true + + '@rolldown/binding-win32-ia32-msvc@1.0.0-beta.50': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.0.0-beta.50': + optional: true + + '@rolldown/pluginutils@1.0.0-beta.50': {} + + '@rolldown/pluginutils@1.0.0-beta.53': {} + + '@tanstack/query-core@5.90.12': {} + + '@tanstack/query-devtools@5.91.1': {} + + '@tanstack/react-query-devtools@5.91.1(@tanstack/react-query@5.90.12(react@19.2.1))(react@19.2.1)': + dependencies: + '@tanstack/query-devtools': 5.91.1 + '@tanstack/react-query': 5.90.12(react@19.2.1) + react: 19.2.1 + + '@tanstack/react-query@5.90.12(react@19.2.1)': + dependencies: + '@tanstack/query-core': 5.90.12 + react: 19.2.1 + + '@tybys/wasm-util@0.10.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.28.0 + + '@types/babel__generator@7.27.0': + dependencies: + '@babel/types': 7.28.5 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 + + '@types/babel__traverse@7.28.0': + dependencies: + '@babel/types': 7.28.5 + + '@types/node@24.10.2': + dependencies: + undici-types: 7.16.0 + + '@types/react-dom@19.2.3(@types/react@19.2.7)': + dependencies: + '@types/react': 19.2.7 + + '@types/react@19.2.7': + dependencies: + csstype: 3.2.3 + + '@vitejs/plugin-react@5.1.2(rolldown-vite@7.2.5(@types/node@24.10.2)(sass@1.95.0))': + dependencies: + '@babel/core': 7.28.5 + '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.28.5) + '@rolldown/pluginutils': 1.0.0-beta.53 + '@types/babel__core': 7.20.5 + react-refresh: 0.18.0 + vite: rolldown-vite@7.2.5(@types/node@24.10.2)(sass@1.95.0) + transitivePeerDependencies: + - supports-color + + ajv@8.17.1: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.0 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + ansi-regex@5.0.1: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + argparse@2.0.1: {} + + array-union@2.1.0: {} + + astral-regex@2.0.0: {} + + asynckit@0.4.0: {} + + axios@1.13.2: + dependencies: + follow-redirects: 1.15.11 + form-data: 4.0.5 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + + babel-runtime@5.8.38: + dependencies: + core-js: 1.2.7 + + balanced-match@2.0.0: {} + + baseline-browser-mapping@2.9.5: {} + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + browserslist@4.28.1: + dependencies: + baseline-browser-mapping: 2.9.5 + caniuse-lite: 1.0.30001759 + electron-to-chromium: 1.5.267 + node-releases: 2.0.27 + update-browserslist-db: 1.2.2(browserslist@4.28.1) + + cacheable@2.3.0: + dependencies: + '@cacheable/memory': 2.0.6 + '@cacheable/utils': 2.3.2 + hookified: 1.14.0 + keyv: 5.5.5 + qified: 0.5.3 + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + callsites@3.1.0: {} + + caniuse-lite@1.0.30001759: {} + + chokidar@4.0.3: + dependencies: + readdirp: 4.1.2 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + colord@2.9.3: {} + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + convert-source-map@2.0.0: {} + + cookie@1.1.1: {} + + core-js@1.2.7: {} + + cosmiconfig@9.0.0(typescript@5.9.3): + dependencies: + env-paths: 2.2.1 + import-fresh: 3.3.1 + js-yaml: 4.1.1 + parse-json: 5.2.0 + optionalDependencies: + typescript: 5.9.3 + + css-functions-list@3.2.3: {} + + css-tree@3.1.0: + dependencies: + mdn-data: 2.12.2 + source-map-js: 1.2.1 + + cssesc@3.0.0: {} + + csstype@3.2.3: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + delayed-stream@1.0.0: {} + + detect-libc@1.0.3: + optional: true + + detect-libc@2.1.2: {} + + dir-glob@3.0.1: + dependencies: + path-type: 4.0.0 + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + electron-to-chromium@1.5.267: {} + + emoji-regex@8.0.0: {} + + env-paths@2.2.1: {} + + error-ex@1.3.4: + dependencies: + is-arrayish: 0.2.1 + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + escalade@3.2.0: {} + + fast-deep-equal@3.1.3: {} + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-uri@3.1.0: {} + + fastest-levenshtein@1.0.16: {} + + fastq@1.19.1: + dependencies: + reusify: 1.1.0 + + fdir@6.5.0(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + + file-entry-cache@11.1.1: + dependencies: + flat-cache: 6.1.19 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + flat-cache@6.1.19: + dependencies: + cacheable: 2.3.0 + flatted: 3.3.3 + hookified: 1.14.0 + + flatted@3.3.3: {} + + follow-redirects@1.15.11: {} + + form-data@4.0.5: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.2 + mime-types: 2.1.35 + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + gensync@1.0.0-beta.2: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + global-modules@2.0.0: + dependencies: + global-prefix: 3.0.0 + + global-prefix@3.0.0: + dependencies: + ini: 1.3.8 + kind-of: 6.0.3 + which: 1.3.1 + + globby@11.1.0: + dependencies: + array-union: 2.1.0 + dir-glob: 3.0.1 + fast-glob: 3.3.3 + ignore: 5.3.2 + merge2: 1.4.1 + slash: 3.0.0 + + globjoin@0.1.4: {} + + globrex@0.1.2: {} + + gopd@1.2.0: {} + + has-flag@4.0.0: {} + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hashery@1.3.0: + dependencies: + hookified: 1.14.0 + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + hookified@1.14.0: {} + + html-tags@3.3.1: {} + + ignore@5.3.2: {} + + ignore@7.0.5: {} + + immutable@5.1.4: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + imurmurhash@0.1.4: {} + + ini@1.3.8: {} + + is-arrayish@0.2.1: {} + + is-extglob@2.1.1: {} + + is-fullwidth-code-point@3.0.0: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-number@7.0.0: {} + + is-plain-object@5.0.0: {} + + isexe@2.0.0: {} + + js-tokens@4.0.0: {} + + js-yaml@4.1.1: + dependencies: + argparse: 2.0.1 + + jsesc@3.1.0: {} + + json-parse-even-better-errors@2.3.1: {} + + json-schema-traverse@1.0.0: {} + + json5@2.2.3: {} + + keyv@5.5.5: + dependencies: + '@keyv/serialize': 1.1.1 + + kind-of@6.0.3: {} + + known-css-properties@0.37.0: {} + + lightningcss-android-arm64@1.30.2: + optional: true + + lightningcss-darwin-arm64@1.30.2: + optional: true + + lightningcss-darwin-x64@1.30.2: + optional: true + + lightningcss-freebsd-x64@1.30.2: + optional: true + + lightningcss-linux-arm-gnueabihf@1.30.2: + optional: true + + lightningcss-linux-arm64-gnu@1.30.2: + optional: true + + lightningcss-linux-arm64-musl@1.30.2: + optional: true + + lightningcss-linux-x64-gnu@1.30.2: + optional: true + + lightningcss-linux-x64-musl@1.30.2: + optional: true + + lightningcss-win32-arm64-msvc@1.30.2: + optional: true + + lightningcss-win32-x64-msvc@1.30.2: + optional: true + + lightningcss@1.30.2: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.30.2 + lightningcss-darwin-arm64: 1.30.2 + lightningcss-darwin-x64: 1.30.2 + lightningcss-freebsd-x64: 1.30.2 + lightningcss-linux-arm-gnueabihf: 1.30.2 + lightningcss-linux-arm64-gnu: 1.30.2 + lightningcss-linux-arm64-musl: 1.30.2 + lightningcss-linux-x64-gnu: 1.30.2 + lightningcss-linux-x64-musl: 1.30.2 + lightningcss-win32-arm64-msvc: 1.30.2 + lightningcss-win32-x64-msvc: 1.30.2 + + lines-and-columns@1.2.4: {} + + lodash.truncate@4.4.2: {} + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + math-intrinsics@1.1.0: {} + + mathml-tag-names@2.1.3: {} + + mdn-data@2.12.2: {} + + mdn-data@2.25.0: {} + + meow@13.2.0: {} + + merge2@1.4.1: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + ms@2.1.3: {} + + nanoid@3.3.11: {} + + node-addon-api@7.1.1: + optional: true + + node-releases@2.0.27: {} + + normalize-path@3.0.0: {} + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + parse-json@5.2.0: + dependencies: + '@babel/code-frame': 7.27.1 + error-ex: 1.3.4 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + + path-type@4.0.0: {} + + picocolors@1.1.1: {} + + picomatch@2.3.1: {} + + picomatch@4.0.3: {} + + postcss-media-query-parser@0.2.3: {} + + postcss-resolve-nested-selector@0.1.6: {} + + postcss-safe-parser@7.0.1(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + + postcss-scss@4.0.9(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + + postcss-selector-parser@7.1.1: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss-value-parser@4.2.0: {} + + postcss@8.5.6: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + proxy-from-env@1.1.0: {} + + qified@0.5.3: + dependencies: + hookified: 1.14.0 + + queue-microtask@1.2.3: {} + + react-dom@19.2.1(react@19.2.1): + dependencies: + react: 19.2.1 + scheduler: 0.27.0 + + react-error-boundary@6.0.0(react@19.2.1): + dependencies: + '@babel/runtime': 7.28.4 + react: 19.2.1 + + react-icon@1.0.0(babel-runtime@5.8.38)(react@19.2.1): + dependencies: + babel-runtime: 5.8.38 + react: 19.2.1 + + react-icons@5.5.0(react@19.2.1): + dependencies: + react: 19.2.1 + + react-refresh@0.18.0: {} + + react-router-dom@7.10.1(react-dom@19.2.1(react@19.2.1))(react@19.2.1): + dependencies: + react: 19.2.1 + react-dom: 19.2.1(react@19.2.1) + react-router: 7.10.1(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + + react-router@7.10.1(react-dom@19.2.1(react@19.2.1))(react@19.2.1): + dependencies: + cookie: 1.1.1 + react: 19.2.1 + set-cookie-parser: 2.7.2 + optionalDependencies: + react-dom: 19.2.1(react@19.2.1) + + react@19.2.1: {} + + readdirp@4.1.2: {} + + require-from-string@2.0.2: {} + + resolve-from@4.0.0: {} + + resolve-from@5.0.0: {} + + reusify@1.1.0: {} + + rolldown-vite@7.2.5(@types/node@24.10.2)(sass@1.95.0): + dependencies: + '@oxc-project/runtime': 0.97.0 + fdir: 6.5.0(picomatch@4.0.3) + lightningcss: 1.30.2 + picomatch: 4.0.3 + postcss: 8.5.6 + rolldown: 1.0.0-beta.50 + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 24.10.2 + fsevents: 2.3.3 + sass: 1.95.0 + + rolldown@1.0.0-beta.50: + dependencies: + '@oxc-project/types': 0.97.0 + '@rolldown/pluginutils': 1.0.0-beta.50 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.0.0-beta.50 + '@rolldown/binding-darwin-arm64': 1.0.0-beta.50 + '@rolldown/binding-darwin-x64': 1.0.0-beta.50 + '@rolldown/binding-freebsd-x64': 1.0.0-beta.50 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-beta.50 + '@rolldown/binding-linux-arm64-gnu': 1.0.0-beta.50 + '@rolldown/binding-linux-arm64-musl': 1.0.0-beta.50 + '@rolldown/binding-linux-x64-gnu': 1.0.0-beta.50 + '@rolldown/binding-linux-x64-musl': 1.0.0-beta.50 + '@rolldown/binding-openharmony-arm64': 1.0.0-beta.50 + '@rolldown/binding-wasm32-wasi': 1.0.0-beta.50 + '@rolldown/binding-win32-arm64-msvc': 1.0.0-beta.50 + '@rolldown/binding-win32-ia32-msvc': 1.0.0-beta.50 + '@rolldown/binding-win32-x64-msvc': 1.0.0-beta.50 + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + sass@1.95.0: + dependencies: + chokidar: 4.0.3 + immutable: 5.1.4 + source-map-js: 1.2.1 + optionalDependencies: + '@parcel/watcher': 2.5.1 + + scheduler@0.27.0: {} + + semver@6.3.1: {} + + set-cookie-parser@2.7.2: {} + + signal-exit@4.1.0: {} + + slash@3.0.0: {} + + slice-ansi@4.0.0: + dependencies: + ansi-styles: 4.3.0 + astral-regex: 2.0.0 + is-fullwidth-code-point: 3.0.0 + + sonner@2.0.7(react-dom@19.2.1(react@19.2.1))(react@19.2.1): + dependencies: + react: 19.2.1 + react-dom: 19.2.1(react@19.2.1) + + source-map-js@1.2.1: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + stylelint-config-prettier-scss@1.0.0(stylelint@16.26.1(typescript@5.9.3)): + dependencies: + stylelint: 16.26.1(typescript@5.9.3) + + stylelint-config-recommended-scss@16.0.2(postcss@8.5.6)(stylelint@16.26.1(typescript@5.9.3)): + dependencies: + postcss-scss: 4.0.9(postcss@8.5.6) + stylelint: 16.26.1(typescript@5.9.3) + stylelint-config-recommended: 17.0.0(stylelint@16.26.1(typescript@5.9.3)) + stylelint-scss: 6.13.0(stylelint@16.26.1(typescript@5.9.3)) + optionalDependencies: + postcss: 8.5.6 + + stylelint-config-recommended@17.0.0(stylelint@16.26.1(typescript@5.9.3)): + dependencies: + stylelint: 16.26.1(typescript@5.9.3) + + stylelint-config-standard-scss@16.0.0(postcss@8.5.6)(stylelint@16.26.1(typescript@5.9.3)): + dependencies: + stylelint: 16.26.1(typescript@5.9.3) + stylelint-config-recommended-scss: 16.0.2(postcss@8.5.6)(stylelint@16.26.1(typescript@5.9.3)) + stylelint-config-standard: 39.0.1(stylelint@16.26.1(typescript@5.9.3)) + optionalDependencies: + postcss: 8.5.6 + + stylelint-config-standard@39.0.1(stylelint@16.26.1(typescript@5.9.3)): + dependencies: + stylelint: 16.26.1(typescript@5.9.3) + stylelint-config-recommended: 17.0.0(stylelint@16.26.1(typescript@5.9.3)) + + stylelint-scss@6.13.0(stylelint@16.26.1(typescript@5.9.3)): + dependencies: + css-tree: 3.1.0 + is-plain-object: 5.0.0 + known-css-properties: 0.37.0 + mdn-data: 2.25.0 + postcss-media-query-parser: 0.2.3 + postcss-resolve-nested-selector: 0.1.6 + postcss-selector-parser: 7.1.1 + postcss-value-parser: 4.2.0 + stylelint: 16.26.1(typescript@5.9.3) + + stylelint@16.26.1(typescript@5.9.3): + dependencies: + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-syntax-patches-for-csstree': 1.0.20 + '@csstools/css-tokenizer': 3.0.4 + '@csstools/media-query-list-parser': 4.0.3(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/selector-specificity': 5.0.0(postcss-selector-parser@7.1.1) + '@dual-bundle/import-meta-resolve': 4.2.1 + balanced-match: 2.0.0 + colord: 2.9.3 + cosmiconfig: 9.0.0(typescript@5.9.3) + css-functions-list: 3.2.3 + css-tree: 3.1.0 + debug: 4.4.3 + fast-glob: 3.3.3 + fastest-levenshtein: 1.0.16 + file-entry-cache: 11.1.1 + global-modules: 2.0.0 + globby: 11.1.0 + globjoin: 0.1.4 + html-tags: 3.3.1 + ignore: 7.0.5 + imurmurhash: 0.1.4 + is-plain-object: 5.0.0 + known-css-properties: 0.37.0 + mathml-tag-names: 2.1.3 + meow: 13.2.0 + micromatch: 4.0.8 + normalize-path: 3.0.0 + picocolors: 1.1.1 + postcss: 8.5.6 + postcss-resolve-nested-selector: 0.1.6 + postcss-safe-parser: 7.0.1(postcss@8.5.6) + postcss-selector-parser: 7.1.1 + postcss-value-parser: 4.2.0 + resolve-from: 5.0.0 + string-width: 4.2.3 + supports-hyperlinks: 3.2.0 + svg-tags: 1.0.0 + table: 6.9.0 + write-file-atomic: 5.0.1 + transitivePeerDependencies: + - supports-color + - typescript + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-hyperlinks@3.2.0: + dependencies: + has-flag: 4.0.0 + supports-color: 7.2.0 + + svg-tags@1.0.0: {} + + table@6.9.0: + dependencies: + ajv: 8.17.1 + lodash.truncate: 4.4.2 + slice-ansi: 4.0.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + tsconfck@3.1.6(typescript@5.9.3): + optionalDependencies: + typescript: 5.9.3 + + tslib@2.8.1: + optional: true + + typescript@5.9.3: {} + + undici-types@7.16.0: {} + + update-browserslist-db@1.2.2(browserslist@4.28.1): + dependencies: + browserslist: 4.28.1 + escalade: 3.2.0 + picocolors: 1.1.1 + + util-deprecate@1.0.2: {} + + vite-tsconfig-paths@5.1.4(rolldown-vite@7.2.5(@types/node@24.10.2)(sass@1.95.0))(typescript@5.9.3): + dependencies: + debug: 4.4.3 + globrex: 0.1.2 + tsconfck: 3.1.6(typescript@5.9.3) + optionalDependencies: + vite: rolldown-vite@7.2.5(@types/node@24.10.2)(sass@1.95.0) + transitivePeerDependencies: + - supports-color + - typescript + + which@1.3.1: + dependencies: + isexe: 2.0.0 + + write-file-atomic@5.0.1: + dependencies: + imurmurhash: 0.1.4 + signal-exit: 4.1.0 + + yallist@3.1.1: {} + + zod@4.1.13: {} + + zustand@5.0.9(@types/react@19.2.7)(react@19.2.1): + optionalDependencies: + '@types/react': 19.2.7 + react: 19.2.1 diff --git a/PROJECTS/advanced/rveng/frontend/public/assets/android-chrome-192x192.png b/PROJECTS/advanced/rveng/frontend/public/assets/android-chrome-192x192.png new file mode 100644 index 0000000000000000000000000000000000000000..188b12551b1620ae587f6c687b7b8e9b60755bdb GIT binary patch literal 21898 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 literal 0 HcmV?d00001 diff --git a/PROJECTS/advanced/rveng/frontend/public/assets/android-chrome-512x512.png b/PROJECTS/advanced/rveng/frontend/public/assets/android-chrome-512x512.png new file mode 100644 index 0000000000000000000000000000000000000000..063c71c3d8fe1b51ac1f2bfc8b16e0e0d2c552ac GIT binary patch literal 112195 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®)I1TBcCYlnJuM?`fefXZ%s3 zlj&Fw)PZ6Tjxf}`r60syKo{J^$c zy{bI=7vv1r-N&?WA1nB&Diu-t&vJQgFO2KO#T28CzT&roq0`9%AQ zwfU=8gPuKI>-n>d4qs&rF@S{!@$^y06`!qu~HJrzR&6w9ayOEm+BW_*%LgnHp50k8kC8cxs)0vqDPj+g9u8c z1S)$)^y<2Wx&Y8nM}TFVLI9ees~24(LVVDlkk4DttMa=p&>QMKCS}XM9Uc=qr!mV8 zt;V~a(f(Fg2QtX!Y+Z$Sv{{K2k}(XKquQkPW(M`4sQ%pmhkoi32!W%($1#$daw^9p>wW-zQYIWD?RM%VY) z)+sybOh`G<9U79j@5~9K{r3)L4A|!XDa~u({5lovx`NCI22+zE?%_`%u&=v<9$RZsNXl z9rwBNYOBOND+>K)R9?_#K-%Y%@qnM~Pi8W0BJbvbLUbrrQHpYLSP>io%_i&Qc&%fw zUKgcAFo{lG-zf;j5uph4g5a3t;8OJ{q!|RQZF%NZsyAF{U?Jkh2+qqwXEf50b37(R33cST{bmb{%OqPc&r)bUBC+)aSzL=RJ#|Z;0tr6dxYPe{40~Psk>dq zj;sQaz=zu6vaGg#oB86N7IgdmC2o*0B;SL;&zF(wb0Kv5IS}nG2lXCIJj@?Tsf(NF z7u1$IJChFC{rhis_$^)GL?Af96N|S}Z5DPL0o?`NU;6}{Zi34*V71RD6y+>67n7GJ(KWr2x*_@fonX_UBhdv&(Z?~3IduG>U=CnG|ZHVUphWq>B0S1y7S{p zJ$dW^BLWl0`5Z5HeuDh_0LTguBMCktuxh-l)2KDZK&BSAUo+2i%e=mTNCaO~Ys5v_ ztcXy=yTiKvegs_%NS&|CBpMt@ZonT!iG|S?9OX#+Uh z$;QZZG%fJvqHXzVo9k~yO@xL1GxAiSP+Y>9nA8AH}$vfgzt?)C8IP$T{ddnyb0zN)f|`kBjO;fU=XmF`otuo1!!X{thLciNw!d=4WPvMq8O=#!m5bT4+B#$@RDW@BDD6 zhxgC;+04BA@05M?&muxqR_$sYxYh&RwcN)13_TFcIoK6}B91}}p`(+7%x2x`=#}*J z(V!;}Pj&F(oOg}`K`JSexpEokhde4GLJ?7Q@pa8-Zi5n}M1YehSLp2*Vi9~zQV|zr zvm!zfKL5{6SROUb1PO$i!}PU&|Z^O*0jqD{Ama(cq6SzrI4tQuZayk zPV^NSe$L2eA)<1rUQ}Ra+Gm<+cNunV8_T2ITo#vgSKI1X(r(#pBj8W%sMQQ7$+^6x z%n>tGn1hi?P8r@epZLhd$5!>aT z8y4sO4gb@VK`)-KFiYOFp>^mam;v@H4I6qYzR0Tl}JKNT_5UrFA(HEU3<9!)F1{G@NkC9 z*Y=s2$WFzGi}J+!S;XCb4kR1&CU_{fneM_iT>()$#_Ae|7#a;PSu+KxQKJ6RQ|LHD|7s}!EwoS;mv!a4`O49ggo%Sothwh9ess=DcAZ$ zx8qLrQHRh2F4>JGt5eiE@>=~f_CP6w3=sGrNLXFhO9Y-m@Jd6c^{Uh9NvW>@5{m){tXwhy6cBJC=xGQl zX%1v)9n0p`workN=yuBH({E~n$p1zeQxs8Z+4Ws`IAfQ0eOW*IiU`Jg6HpTXtNr~p zL|8E7|72%W;o$oM_N|feaYn`{&_dnFE~7y#9ttjSJwshC@C*F0j_(S-n;^GQo@te* z?t7f+K(1j|Iwi4McliRAhpl2gAWwD_?8-$XFm2mPZ6E7nTcOSM83p-Lc^SpFp-7eI zy5aiUfH?hwKJF);xd5gY7{{iLJq3HWbt{Ia9Tdu?Sk?$>TSsp#$26AdWfZsLcl*9n zfA9?R-rWhfuoy7!0vCM?HjRCw&c)@7t}JZeS)o}5RYhPEpIV+HH9rW2afd8z>hBH& zuMB#H_SAc^?!`AyXI~f9p98h4dEj~vbl0;R_w(~WQP+hQi(VbO_^d&jyjISMAX}h? zztR!{%F?-b}N=HyIUqR#*ldNN$8>w5KlPdP?JH&Q~G zwjd~K!o0|Uno}yJkxvmLp(Y{}@is_-T@i1S8Qb&|7gBYsU9nQIWQ)vPle(^}i0Bj% zHeDqbg7%0+Bp)Ws<&=ca%9Na;$Pes{4cuAzmbn1BaI4$v(K515@N+qlDBRCH%)>5C zC>ra9`YzngxY(6aX1ocMZENMZ4eajL3p-$sVZ5f-j5_`CUezQ=T_AiWm&7b4!DW=L`6g|MJI^?faR;*(;PO~KEr-nZw8)eo^4LGHej{hP~UJLGV?E$ z&hqO`qAlrsHSjY5)&tLWAt6g^-s7XHmN)o)gRJ$L4(Ro1g6)`nW&CTb5HJ*LfqfVUthE>$w%n6nUJ2aW_b!&+?gfbJ?_mAMh0AWWmp(Zkh`-qh8==s8QGfZrf|p4P|znvdp$_-URo9U)=6;1$xnwYdP?8 zr)?RYX!H2vILKBz#D#1v&va~4wU;vEi2en-1@dm6clo>2j#32!X( zjG(J9mhH1n!*ye~oC~(R0lRJ1V}BZX_7ApagL!0-vhWT5ftNs)gO*7?anzsqxtRM{ zCeg=Woxv-BP^O--$0*o8LMqxpaSa9^W-9S)nKSywivhz60{SraFoe!|73Y7ZyIRr4 z7{Np9lQN@g(OvC&y9e4M@^=4xhOG`gnZV@GL)k%TEY=|AKLqsZV5NiSD;>Vv==8KG z@oK;UE_ng4S+g+MAnSl+6q4EqDWnPqFl{dnF;`r6!kp{xpL#MsN2}%D1ox*c0G?tt zFs_zi@$p<<1APiRwGo9H8gNY^;0yoQ2gGtD;FrLE##nYzslFxwWl*;5L81|USCw~L zM#*k_t?%$^eW!mRsTdiWIE5%oDd&RDg3baol`rhEjBz1n8C6;HFEHxJ3~GOvUPYoU zG4R`ws}1wXLz+%IGIl0lx{GnFGWKCUqyH}Ibir27X97Fx;>AxzuWVnixEcoG-;ftI zxhDIrwq@E1KD3@ucZg|Jm~Yg2l?m=O;6793{uY_O4ju;V5GWuckV;|vOrOjn!q|DC z74QC!UTt*9&*SOYhEsjWlk-$ujYZ*JkS;Zues!NW)P4FF@$f`_))(c zKn%FVp7YbBJGY--M z+=ox43@`Jwt#dCf%Aw3+z(~+Un~^8#@`xab%(6}*u1nboX6lzc4)p>qCsgEU9BnAF z?u)YPS7G@PJPw!c`PF`YPK>MkVq2y$T`rVI2z`tP)6d7aLpJXMS9Q6bb`}ex{Zg*; zRP{^32e|^@koa9r_F?T8ytWAqER51D!$QF23kW@>B4c~?AJk=oBKO!2Kw!VP>rffO zjzV1*Kx|h&SdesGuP*#ce*0RTFLdxC>*?c_ofGS-knqZ_}vSA|IKsVzk5s|+He$^wC3gKhOJWK zBp~Ii3<`m$Oo5uBgswu+rHOV;7vm6UMVqF}eWv0+oJ275x1n>pga{~a#qMi`TCO2p zZ@e2UkM-i9?W2~j1Pl_mMSXWG83vinI~U@=rbc!oXp4*?pxfCq~@~6 zb3fn53K*}`Qj=Xm|4*UnoW^-mxnN60bwF2bb6q53(_;H7#&K<5E`|B*z{$EID{=ll z@E(4kZu_$5fK$>I=%LN+Gc0Fp>+Jk5>zI8f<{pgQ0n-A_6@GS&Y8&=(8@a0z#R+Aq zLt*mad5EcPmUG)!4t^GaFaq5G01yC4L_t)$ZlSJQ!dNn|J-vLM<`mSa@_*|yX*vIkk9b1wji&uOQr5aJ0;4iGVX6)A;}hobJ@JP z>@jPZwsDMt{-1-4+?!Ci@7h5@_5umRjoA(!!EJlBW%fa}IktLW%dkwfn>kmC?bbD7 zv}x9#ned2GpJylcgkRHS!@#0pv&ke=W0Xm?y=Yt%2rBwU^)=ll8yJ+7%PhH_W4S&X zb+H&cCxEiT5;4w!N~zGOgP*qo6f7!=SYTw?)~XhM$^H1RaE?O{@15y~Z(iyCoin|9 zS*k4sU1E&B5A}Vgj#mL{SM$JIJmBd5mgd&7O@I&U2ub^eBn-?S# z!BeH%#E$uv&)kDNmAQqoPV3*n#Iw#qIak=$T^ZLi#>{jsyFSVV{jkG3eUCTUO_$2| zvvmm=b*YYV3E^Zu$Oe<0e;**zHlxZgsC}1_vkiHt-DMw_`#Bvr*LUJi=pR5rE^%^@ zSPUfV$IgTf9_ec5d@m4{%ht_wRCpdr^b>hKH5EYdQ5zYfKG7I?YJbW{J1{=q#9^zW1{3r3x^ zQO-s5aar`4;d;Oy`)1@ywF6!)cU89-x43={$h&gFHF9R2oMen;#>k=#K&|%z=(WyE zh&ongp;5tDnSQy}JLPOxXN)@i+5_IeGwmc_!T3(_21WWvl@=(s!sSv5agO*OE+FMw#O<*B1Y@}0O!5toLbaa$icuVVb zr=q3ymN(S(owd^^DY2+YHXcseeek`&a%*3w7tzC44 z*}m>H`ff>(2_3%L=<&l7J$`Ve<5%J+azXIwYOqX1(qS01S+7UblYv2{{^{T(!n7hn zogwR{EK!;fl+i`Bb(bYImZ6KqO@79D{Er=s>7P$qmpCaX zjM;_g6Y6-F0^L%mUCje;@<8{dhQ|NgJTM0eQxHTagW%=>Q-d94d!>|25ESntLoZ)! zbnniQ?*8~n&!26yUK31HuVD&eiJ+HJmH8S7+6ferMG&0A!KvzUR4lGpeWh+20jtcw z68dQuRhssH=*I?m!0^|*!)gp!?~J#rEzH}<&wQBDZC}uDA>(pU$DQyEiAou|-i%V_QmtP3a@&Ym&eUBBc2qhx zbH~sPPX=SUje(>rRay9X`r85n<%GQ@FB$KKO1VMu?*X#zh$@x>bf+`l2vz;CU2JF` zc&i860^qIg`J5W3=;SCAZ0$^^fHHbvYNsvbHOV4t7z!rO+u zU$77;+N_h(kO^8vUI3&7k3IuOttIzFuz;VGIT1(|30c^*l^XPd`W(t8YBNTIM5Gha z;JVMQ;C1aE8t?ts`*XBdCg-fnC`u_`;wA1^N(FxT8IXoaeqhHvqR7vPlfQTFY=Y6F zH=@ec=YxJcg6?OY`O?!vFJn(aKJ%>6 zfyW{u6al(U9e_2pxqU=&@^!-mSFb_}`f0*NIWq^#=3G=N#Q0?dKFru!yda_Mg>>Si zBBy2jxjH)zog5XNpG$rXC*&^135f-TFwy!|FDjL`ujYZ*J*QYu^gl~_{&Yid>NITj2~IuWo(>zb$rX1XRu(XbK!ZalxHeY&ZUra^A`5W?4^kDUbw!?cbRTxSB&*C zJ+lFwA@ss$EX!9=#yZ7?%-j*#ll3HD|dDe|ic+noxFDAm)p!&gaVLq6jceSf|;93u~1;Dj#`HYza5P<;N#t#P=1HVB0-qFD`x&`kW z{fmDKou5fBpC!F`KIr_kCz!DS7)-qdSJdD4{!MpCOM?s{9ZGj2NFyOINGsjV5Yiwi z-AGDEHv@vSG>r7n-OVt}J)i$?eb>65!kKxWefG6q*R{px?`n@;ommb@j2Gt@vfisX zm`lQ_a!64p<(5%@VfE`;!>MWUhU!Vf56f$7>Tx;(>Fui+j{+DgM%N}Y{`xUypVKp@ zXgnC7CwY*Lna3ND?~F?cH7F82em0uuJ$WlPI})?*y2+^Oh<=BhI_~wCn+8uNU~^Jm!Cl&riH`Ki6(L&bLwiMB{mJzN|!X>%gM{yj?aT z@GFWvG8&&r9Y7jdsR!Ac2@%J-GG`gRPluDJR};;ksI_kerJChKN{NiQ`?q&|UVAWf zWSjly^Kw95EbO9nFoAd`!l@+Uq|*;7{@-bNnLlXw$ra}@k5WyQ{yg$m!cdcE(*L&z zb4ig2@UO-8NxaET4QWiBiliU4WvwYfAIj9cCW>E+APjU8ZpXR_HAF)fr@MA1Oi?1T zVBsZ%`{+k^yp2B7^&tiYX-DJCBQ(~-3S64@o@@_rlyhA3(%|M)7JwOlPlWKOb_}2u z^Dgn@PvqwaXx1moUhjKGzRFX0tr^j7YW`(q}nc6{r5o3O!pwj5=8B~R2+X)DqE za)9CeohkG~4;9XBINVBn7gG48VpO}5v6UhaYA2DJB((ELmqP=%TI~qh>iDdbr`PwP z_c!~e3dH9UAhFSejLJ}yF_Kz2oFHcRzqY7hIi#grz(g3bWQIyoFADd3S#2uvgZ^c( z#}5&K?Fuf z%FTyX6rzmQzRp%0$9%)K)UuLe>afJ?O(eEc_>G|JG|IJJiOOXY*?0Ty6ULo5nq=`kVKB@uvwhX|JJh(g@>OpB;Lt zG#Z)rw+X%kmWsZ9dh-hHF0pOmbS7N}+bWb>jHALA-J9=UsRfbhi%pZtXQw!oRmFTjJtcOuS|8qLjK( zF21O1J~B0`PDsvTny6l4e#AeC5WW%&wBn8uJP}hM9DROh3wh%xUZbfnR5!R?*EIxH zr`WD1Wf{d#g=gaPnTBS+c|&Zvg+zMV245Gj$H|fyS3xyW_|?*qIU}@WsW>pUaM&Hf zG}Q2rd30!hyEVVb%LMvVyCA$P0$R1iaEHaESVAs1ExhuZY*C>~z5Y^j?xPVXM*6we zW4bBf%VGY843ZTd*no|0?DSHjfRfz!IcN;4+%vKJ%1tYt3Kvm z67xrmc9hTI9o$z} zbS&OpUMccRn3|VIA=JTTPu#Zb>r0XEc@3{QHSg1eCLP`>Nyk)x+x8)57DAu+=>a`hCW<&hVK<8%v$m@&dj*8~plkKK-!1A-`hqWIqIeUMTuXNaBW^$c z|3)IcXd&9g+2>|Nf&Af{r%_PjBwEDrXXG}OAM)DoJ{QfVRu2(t`cb%+4G}}cp7=6R`O^#keDv%las`j0k@cu^&%F&mF@~+%- ztjoL!Hf{+w$hn@D0^UJFAtwFf*SbUb-J!fcHH@Lt-Ji(!bytl^h|<5k^0LFrk%izIQjf0q&n7QEo#Ff^9{;a0 znPz|=F-c*_;I`i)k&Yw zJl`Gr<4KfhXsctWsxM(i{Z9)xOCMY( zc5lkKtkUCGivUzp<>k*bTWAD5LIU!O=pUu^p97^La`tA!FT_ee_=Z3eyn6hY?uU=@ zPCIvo8kPxVB#2VY0Vw%0XA?qIPjUW{TJ^b2-KmU49VI@CosXe;J!5YxAx*r z7~HsN^m05QvOOW_)$1j@ta9~wws&dt+#4A@VZtG(m{#|$5L3e)D~-}-C&%iOT`*ze zla`Lz7Fp1%i~r;$;zp@sNSJdeF zKf5p|nFQ%nN6sz1@V{MkhpgVJOzezWefn5nHanWA(>k_xaHZj5v%#8w^@+vF_Y4$h zbf0C6oJF(j$9zi^x#r1q@qHyE$z@fW)8X8o^nlNn%uF_sY~Bv|VHH(D0vEPb^_l_9Nwmd$Sm5RI zE@IX*+uwC-IjC(Bf#ABtpxfG)PW3Q8sP(x0b}WaD`UO1JT^#3) zZx=qZFvG-Fq$@4oo6P+PL5el}`A4NAfzdw5RmYbLL zYrn~l>|$uO@$DkE&X+P1h-ZrbJ@Y79r}G#Wn~0t90d7Uu?JwfJ~Tj56SQcHpW%OG`Ot{+ zsU8Y&^9v|z;0VH(q$Bj?vS~jWGb^HWD$#lq)%~1*?w!p(_YbhZ=?v>W- ze;cn;d8vfCD}fqezX6*?#QTVz@A5_L!=9P)8}F%;3Q)uCeqVa5+9>RwUuhS--WHrx zR2II9w?4e8?9FV7!@@&HCb-zttMW+3Da*3nRc3l=SBg(J&yV~J`OEehYAkYyU>dQ} ze1WyZ`47I=Re_kBM5Nswl)N30IfyP`Mo5KNcH+OY`MMoZN3~Cp&3j)95zb)uX8DV5 z_@9PsNO$@00hj#SYj4>$*>uA2Xru7=PEh=IC)K;HEk$t1*=omzchwCUV0lVbTSDwL zofR7;3er>sPdVjrO^g z@mhIT?)saK8L%inR7x3|8M~C2)5S}zpP!(N8@brS$^Ur~g-$A~_635dEe{LTm71=JN`7iBUxG~nu$NQ6z2c*+EQ>Q z!LL|tLoO~U0?nenMKMB&G5$|HI@h}#99%dT^Rpo0%x@f1l<~e?`YqWi1|5GBT~Zr< z;v6o>9NQMF&0(pNtXDoObZ|gMiRxp7@#OkL|!d^UP^*ag}&lV zf*8_iK8Q4#)56~M=i{-XDdE5N$Fx!!%Dq?BFxS;UeD0B?^7Uw*MhfPBy;?}-o(AD@ zC5zlk!eN50&JV-=uHn>_*w;F&zsC9fD>ujRi$J_#@}F#v>HGRP=B3O88>jOL<4zj$pFK zf>=ojLp!3@w2?4l(XHLYVOJo32x`nTxX>ey8z)2_ZO7}oYkki74lsqr6U?i6`ezb% zGLkL(uZV7)}{ z8V&2!f@(b+Zh{~Fox}|=qFeoXAuV%wv$>cO;a-=)haZQ0RyX9Ejc}5XdXQ!8@dYy| z<>*7F*upJqa)gm0+#>pFPL9dg-U%ww3DymTro0|^UClfaiZH~H#V{a=wyph)dylb& zCXDfKR__1_QaU4Ct2F%5H5}G* zm1?xlx!V>Fh+8X{IWoABQHN?XCjqg%7tOUfEq`*^9sQk)`;t=Z{H4c>5&{fw8e_CM zNOao%NX^OMffh`fannPAgyqsqxjLPOB1J}e?b;qITLEo$%NW03Hc_$J6)L&ISXJk( z$JccSYZ}UgfK(YOt~L6rwFzw%v$PAQDg-irTaIT;A5=?-qL!QoOSo8I0gkU<+S0Gk zoaQN$&qArEaAar!cI?}Z@1*1iA%0|~7?V5WVa!)6Kj`-5RlXnD`xtLEj=XwXlZy4r zn3WQR*X}T+@u+d<%yhFifC$ z#>`Imy7A)}hC>NX`j>N7>2%zzLenwp_)S5#w=N{^mQ^uyMPlup!BW3Sr0QC2+Uv|L zg68q9p-jfqwL-Kw%EtR97HJQm|31nrTzo)P8lWwte>R{O5YR}d>|jMAY>{`co8Q0f zuy}RlCSEQpTJlOV*YT#wbLzj)6!Pf3&CTwnm@6Ut|B7_ zA_Y)RXdesmsJi{%?q6(?t#-ro6B&A@`hMyKD`Q!gLjTExwF|hw)PspUZ+;i;CR$d- zi9r4v!X5qB>_K3qZtHQ_Wg(uQGc4CCBrJ@?B|ME3kdEt~U>q*;SNs zuD&rXaj_WUERS3`mxJXS?^6z>~>=z#1DYsG6oXuV;Oz zsHI2PusBlijyaTkG>oQM^FfdmLx_U?YAo}ijF~-%56jzTa4~WHZJFMR6G)F*K$?K_ z1tjEya?QF6k~~rJjKvwSR4=Wk6VQe5DVSc^LRoSmd%tTzfwB^(5FE(c^owS{TNm4e zD`E07saN__DioX#!Sfw{{6Q|QcT0E6YA>zR_DmSh5VC^c>y;^USk_Du0Uve8$N-E!2rgLvBYA?hn|=)gUexfpwyoqGDtI4~M2& z`E+hipz7IFub{z`@yt^~A+H;}OjoZb1AnDzgBSj$e+` zCFD)&hVdUna)IwbV&@aYDjk!rpgWD#^NabHu6kJzRH9#hP$?*gppiaqm00IQ)Ut&& zaxbYjo1?EN&{_?-~aiEIzO<93lHLm7i063zx&?M z_HJIHwL9X&qP)9bf$YJl?O>}cY zBd=1sRA@%8X>}K%m?$y54IkTx1V@;17JBwQf;jCm*vWmDA|wkSIxqE_-veA&*f?zl z-Dawr(wm(;z-IO;#Jc`M_Kz`%24*t}PtNdS(_eN}@v&qMmo437xfN>!^M1P{F4P4! zkF1A-M#R=AP7$yhVe7{SvGaq;z;$d&)Ddw%tS_`pYlc@zlT!pWf@KEMhm9#^{@xAi za#cGzgH{O=i0U^QOmf6WimxVf(NI$EP;@cj!bOB5f$ukg9x2V@M_v44*P;@`ZX%$X%FxV6mMF=7+k6r9YuS-_u(UYl&P9Hjzs#$ZiT4UK>r1+j!_`uL zz1aVw>pWecBHmZM6244jnHclDw%=YMN7YPGGo5QOL{A|!=cCxq8&B!Y3u0WLfu+kH$xi__y z%gW-eb+Z^5-TSm>>(;>TsJs+1;m#*iaaVGgF*eGK@YEnnb0Hhe=kkCNuW+)EqOb^A znlw@sSbrfZEA9O=ea2Pe)Z&4B`A~rWL-j0NBK-78bbshfxYx6Wu}3D#XD|RE_pG-V zytA{%6zHP%%u97@JYn1e?C_j+tvns&b!k0riQEmvCl(bnn=`5OgLeBdZTGTb3z+$c z@lz9hQKRq7?g6?6F4c35`un<+o*@+=#vU;M2jv5TUz8z%k~Pd@B0{nE1%@B{Pepmx zBrOcLez`aOW8rTs(KfOq`vyzGiT*7>((*+safzsG?!X2D9fTtp*&2x=xDj>)&TWxgS=?p zgqn_Z%&5==t_RlnVjaSNrol`eoUyMNpT=EfeoDp!@$2foqb8pZPz?t+&OIfZK zG0CgzQx4Y5yVueb&&$0Qmo`7#w^s|Bd^`evyUCEDKbQoGPXnLHo*ooIu(9MG#I0)Z z&CTY6^3R8+I6quaOhHj`9s*CZtQN0(V{@>p8BVxL18cktDVeo&J;0F56!L$~cUX^zD_6Ta%dJ=NFAdKKzx)L-<{m7W|L$X=oPsX&B@HVSYDC%5 z9$P$+Tx}D{sgfs~ulfQVw+|fC9V#csP!-YM&|^ZzBfZGj<;^t-Qh~gDUpPwC^Y=R(b75 z>T=3~UZD;C5;O}WC7uPW8aK*6im-Nhj@GPA^dPVa6zxBuve`Z!TKY!Ub16Uh^51(G zQD>3BGeLJNnQadCb?gdw&c59j>co%LlfoU{#r=2J3KI?CsZ#IYw2!#T6Vw4h-s$*- zG6t$G?DNuU=x{9abK-vG8;ZUu$lmR|`oTqsTq47L!8Pk@XXk>MX2WTHMbN;(!fZLM z*fcmW>-v!C;M}4@bm!Wk|L3pYF^bLfohK!~r7pB0sYW98f02|e%}bs;Gq_s&i4fNK zT=h0H;IvuLmYbqT#D#1QmsRh_x>MtfE7~Y-=Lv=ebf02hnZiUD zA`Ljp#aYm1ztp(H^Y&bXN;#GSZjUihW2Bj_;yVLK1|oQiBsf{iAnt595+mft!*AVG zSqLe7BX8nySVyAcO%2B zo`ml>sD)$h9>W(LdgVRPS2gfoFrEa|@5A;U)Vn+W>Ak~$D5zW5Mp#U(GEUyGsKAi# z(lSPn=uDyv!{I@rkf-+z6++kee49^oC&c~J^v6QfY9XY}sVP87vDqi%?$*7lyS+L1 zmZ9zGYtZefJ<+|FB((Zj{7GF?8adhdUYh&m#RP^G_O>oJpWo1IBl<*F$!fJi|2vFp zO`^0uXF4}Ad)lnkP90Uw2w7kk4wHZ{iS`lT2CZQ^q%xU<{n1)mm7G^1nE;TAn#T*< zhN4ASTUY3|25h*Zwk8aF=yu9SoUn+KY7ea_Fa&(%(CQ^*N%SUlc);B@P1D6H`)!y# z8X+jTo2K^rV;0<%CQNORu5N6C&I zZzWEJefntGHFhqVC+U-DL%%FNJ`@yGr`v5PU*Kil>(rB2mQD||-5m>IEfU$tQJm4r zsPQ9Nrq&umDn0P2QeYz{~txZ9=1DDFz-w$>wU9MY+@JR)AP4^@Fh&J#- zKQ?p&IW{&s*y%H|vy1pKDXDsDOfU0=1#c2dOZ;XkbE7C*dtj%IS|140Xz(cDX)avW zpv}fMzY8hog|sIiS#(VX*Qu-vHYKY7MJ*H}Cb5BpbtNM z77UqF==diqx84+{4t#TG!UdHLSr5@$`j zMj%(F{o8~{e#P{CT~+$6K}p5!k1-qpljnq1b(q>|@i z5MVpb;*nw>sw!^|OMOXk)2$pC#I*|Z8%(;)%yqA9K##*?zm3n2uW6srwGqT+mH70b z5Iplw?N?f|T5n5Xc1vw>;m^XKvW=k{-^1-Bctm!Jc&z>~?bUbZD=utrDyh(Fhq&dHm%_UcTAFo6iube}^H zKL)GohyATD*ewaj&C|*8UqrlSO%nY<4zwMqeg7)a+L2IPlsO+IzM$8dqT6L>R6a|o6aWVy7*FP zwL!z}t^(zv_=A!51)s7@(4Nb6h`97*vzP9$DKW^jwTE8g&M=-|9;vbZ53%^1a5jWa zq$~O5t!%sG?~##xOJ)Ukd+y71$up{%zaM7|foBIkV7M-DrK<=N@c_zA=`IoHTgtV0 z096a}qWsWLbV}8IzhwM6;U&&>OvoAFcw128N&msBm|E3yH|LSBZzr4IK8F&m8SEEA z$kL(o%1*;GzG2P`%OL%t<*~l}&?cUTSnTd+p<{6VqdvoyzLfUz$!)f2Wk#$vGh;kQ znq4Nbs29hvD84iwpx;dVkZ=C^csr_R{GT$t$4A@eR3`?|*Gw4ZdQihncs#>h>xyIJ zSd3ri=`ClXCis^6s0F>3wyIzdw$}ioK0W%QJRh+l8F_T`U1gAAi_pm+QtY9pLkqLT zc|qyO;KQqnJwG{x%Sk3D@%);p$1aF%clm5;=~FP`phS-NH2-KV(e6eH(ai|y@PV27 zoaG-}Y<)MUx6S)l0K7OqtU}k_c(y>{Lm6ZT1tvf$V=DfcYJY&V!fj7E&~lA%5{yRncA6I)m>a8GjCa)A+U0bOh40 zM#3y@!m9ubY`3@r>46b&o**{|t$-jcmnAhva}0vxXNVVS63&z{`q`JLzlEVzD}SB;Sv`5NA!!4jF#KLJ#u`E?h+^ckr#v4nlWy6=DO%!d}505N+RmIq1cmD;Z*P&-I`?) zWX7O%53I8$>_N#iOQimY(a73)B3)eU_$K=9f%vrA>n`eh@Y89$8KUxZRcv-Eit9g$Bk2d?kpJMT z&lByb)~6w$Z<6}R54T*Uu#7R;p5(|uuB4ah&qN@zM2>UzIY9$s26FxkI}^Y3&q(^H z5xYY~q&a;z6nGda|B_Q8FWg^yJpv~vYf3f5Qx+XH_X+&yUMZ7uk2nGLEP$UkxK8+f za13AIu9Yeu8btOy%NK%P^Vg>?R|`o9lWM{foX{QzGbu3up#mf}-!+D7I2uOfN!^OQ z7Dr}IZoI-$)ueQW0WBK3Sm!J0stl22W4z<)n*0{KAIr&{koKN>yo@Y$Cl)a~?IBis zs!?hfuL8jS*c-;1#ATSYI2}+On=Q%e&O5ZF?FnfZDwF&c>g;Tge~1aE&Rg^vT_|vn zsv@MadF;Cr?Pp81$B*Ny80&prlU@A)a+{DA_*Kc~H%iN&fgLC~^&#~s-9H}pBQ3Sv zCCE~6ey=${(@r;k>^mjv>0-3T;>#VmE6tH9aV(OL^z^&^AO z5NMvGYK3za$U7=;)%N=#kxP&?M{?TJXyNH4$0UAKs5_f+w1CD>Ixh_ca9OZ1b^GX* zXcGj(C{>LO7j5aU$0m2Kcbi6jia?Sh)}te5LoO4bH{}iR_SCOmQQJAkLk9K z9D*{l`A$Hk&0uy4Urb{G^YeI58)gKE4NpzV4Itw>2R0_;roldCbzN^ze4;!%a@o)P z3hM2IXJF@mZ09863spGFTvJo@6>(d)0hc2D3|g1hL--jf{!c?daznIqv;UCx=C52z zufKAju+kKBk4ZeWbIZG+X3@`Ri+Y?i_B?&jLfqDYr$KN&Fa-2;;Q-%=lUj}DUc*T` zM$c2$s#q;KUB|sg^H)#4qxf9Ov>*4wP;f~NE9UF=Ko9e>;~`g=sZV9hR~rIA9qMx= zX4q$jSpFygkTZ8*se$ucsDt#S&AabEM`9H%O;SnKIE0+dzFLHPu|7(;WiJ3!1Kx-2 zdp6{aHRnld#`3bKT-vCnumAaH2w-91d_NAV2$Enm&(NnoSLXD!l;eN(X<0J=b8{Wr zgRV`kBNvp_v9U{MCsV~qL8WRdf)qd2(KSktV})J;P+bJe-(7F;ZTALA%4fMcDwJElPdyPz+Mh^%vvsYF4ZPOb3@(pZ0KHGS zZCYBHLLGV{W_0n4@`1Ig-3tYG1r@{YG`&2@#ifpnIIam=EyQ~}pY|($zT&|4sH4dD z+k1?LwiC}ZZM-5kCrSHK`O*eM>xQPi*sx}amD%=xT7dII`jG`SuU_tQnDkTbK4*(K z_cBs8@4$X~(>>voyBq3rj80QI!ylwh)faV-pjk$6Sn>m`7 zyka&LScLkSWF%2+#Q-Mh^^1a0A1yytzEH+y8g~W%hR(nL#YIf&nN&#l$%XkKPMX)H z@*#&Qw8T;f_2bj1@;}a^oPg7MQaCoHy#u9(!`eYXK>@M^E${<03BM-L8@Uow(JbJb zQ}Hy3FaHEjBcgpCe1ausPZ&im*LUs;cDmtP$@$V^kP)>H7cWg@33p%B9y}QQOQQa# zFnd$*)2a4s^U<8e$I!V|vZdwh|J9g-&(Ec+AHn}A?AkaJf8=JGVl_hJ@=`eENm395 zu9JHA^Hl0^{4*Hhyp!*GhYDuG8^A+}e*;hHT~6p!CjA;7wD?{WU<6URe2XWzcH>tr za(W&|Dghp!n8YTG0VJ!AZ&mj83UnoJ4}f>0eK8kuUlM3SKazW|Kal1+VqF7BbqBFZ z^gC6J03`#W3!k1TTTV>KNe!l}f3gW(cYyuScNAwd+roRjrD9Dc^CyYuqUdgK{GfMNbx zt+x_h)=}ee%j4d&aCK%95{Eb)MeQF&q~Z%m^G+Ptjv)!WxboD(Z{C51MmX00T!AOe;ADwD%F#m>PCT zYI&-J0vXipx5?%u`@spVTy(Awa2ufla1% zg2}s)io4jmd*`tg6+L!%!WoM@x++7-Qi|Fbs<@_!zb0DP58(Bt-=^8KvX4uhCoD>6 zzmmaq0C9csZYZjXov?4#F8YtsI#2Wd9jou=$b2B?gaXWHl_xRxc2)Ac-nS5DMBwBd ztiix^PfC?%?dbJFl1ufGByX@`jzl%18YLKZqYHXk#Tjx;6L|`-8CIt+CjFHUnn^){@@)FIhiK2NjPoR4qr(8(*L2wpfe?JAI<% zG{x5Rq&89Y{XS{bF2B#SZztX~DLI=hO3nUhs7-ameerC9EQoN-AP)dKnw*z*^eTwe z->d($hJM8m%5eS7J34^0;hP)kU{D2qeU>|9`UF!mwBg%qq*$G?k$$1a-_1VhW)}#+ z&8q*O(6P#T+*e1-l{bq$ePvg(2@{$SaVfKdr@;R1;VoQ}Nx99mPZk@HKkwta)Rq~B z?xjX#AToaga!Q`hz!DIj_1p#=(SD zbqht#LRceK#!&)C!`Jl*`Qc>l%_Bk6TlL+)xBySTyKCSpwo}qanFqb1;i({+CNziB z1FY((A8&gEK76;W5gQZ{)=tbjk2v9f>ZFaNBDCA%N8TO@p@pD2Mi?C}khD`_)5HUNlA5{3e``i%V zrwe>|1_+iHe0TGnr&B|4EjoxwqF#IG041;K{WJ3gHR^2L3-KQ%*Q9;R(G7sbvqq2t zaK%#2?PogGPFaUL`Y#-)V&O6`M*4X=p0Afq?{B8Yq+tLW%ipvgnYr5{fZAU$UyxRvRs)!z@VvY{&BJNu4^Y`~& ztAz~QD2gaVVSeST%}Ve0^KLn`M)J4ULZS1rNl>#>OJLB-)Ng&>v&y=l+c{}b_lVyyRn>?n*?(oKmwe#zW2t;tVw>ciEsig^I`5ZSdMVpPA#gZIS=;t$ z{Ig1yC&l*4hZQ?{_gA;MPK>y%OU6ew8cXL+U@Fw_$VdMn+2nGs7W&*v*xq5(R~JF} zUs|?2YT6mo!JaTD+M^X5Aonj&q(Vk7TH2^kg=Li#5S?NR-&QBw;4byv-37U#kzr;y`X0Ne(o z5O*5+_?cv?9>oX)AB7Jfs)nW8q|@wzkf82`J-&mr0VoGCQplbtLU%{(zmX^HJ`^=fA2l%<;K|K;>O!TCiCdte5rM!M z#I)5JQgF!3@USdO_FcFn+V8s>$7aywc+D+omhqp6cmGdPR{!arhVyMO0Yfy-h0Y6$ z0KBoD+2V}!py>qoR0fdyX1=C0{kY+g>HZZqbJfBmxdm>Ubwe0sW#d;3rS41BZ-Ny$ zRLDrsge3dC9^8e#X(Y~2{7c)Sd_C*Kb(PIk7Yd3LZb=kxZws9|j}agf=rv`yUmb_R z_ac`-(k6U_hgj?FZNEcVk3$EquO&JDU3dK)-h?Dr<&XTt(&>Xc}g!epj1sS1+o-_T$z!8`UUVv6Ha=$aDW;2SstQ# z^-ma*)%egdHrHn4Xdx0DQZdle?M8YFb z)B?@PJ&&IPv{R*Hf;W_U6*JN@a_NrcxERI8u-Wk}H?)17w6$<^Ul?N$yp>DgR@
P6t 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 literal 0 HcmV?d00001 diff --git a/PROJECTS/advanced/rveng/frontend/public/assets/apple-touch-icon.png b/PROJECTS/advanced/rveng/frontend/public/assets/apple-touch-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..54915be73d89ef74f929587324bb90d98dd0714d GIT binary patch literal 19669 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 literal 0 HcmV?d00001 diff --git a/PROJECTS/advanced/rveng/frontend/public/assets/favicon-32x32.png b/PROJECTS/advanced/rveng/frontend/public/assets/favicon-32x32.png new file mode 100644 index 0000000000000000000000000000000000000000..cd1b74063dc10ff8d79b455d0c0ed2dfdcf644fd GIT binary patch literal 1136 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 VeL(L6f93 +

+ + + ) +} diff --git a/PROJECTS/advanced/rveng/frontend/src/api/client.ts b/PROJECTS/advanced/rveng/frontend/src/api/client.ts new file mode 100644 index 00000000..9657d80d --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/src/api/client.ts @@ -0,0 +1,107 @@ +// =================== +// © AngelaMos | 2026 +// client.ts +// =================== + +import { API_ENDPOINTS } from '@/config' +import { apiClient } from '@/core/api' +import type { + CfgView, + ChallengeDetail, + ChallengeSummary, + DisasmView, + ElfView, + HexView, + ProgressView, + StringsView, + SubmitRequest, + SubmitResult, + Target, + XrefsView, +} from './types' + +export async function fetchChallenges(): Promise { + const { data } = await apiClient.get( + API_ENDPOINTS.CHALLENGES + ) + return data +} + +export async function fetchChallenge(cid: string): Promise { + const { data } = await apiClient.get( + API_ENDPOINTS.CHALLENGE(cid) + ) + return data +} + +export async function fetchHex( + cid: string, + offset: number, + length: number +): Promise { + const { data } = await apiClient.get(API_ENDPOINTS.HEX(cid), { + params: { offset, length }, + }) + return data +} + +export async function fetchElf(cid: string): Promise { + const { data } = await apiClient.get(API_ENDPOINTS.ELF(cid)) + return data +} + +export async function fetchDisasm( + cid: string, + target: Target, + session: string +): Promise { + const { data } = await apiClient.get(API_ENDPOINTS.DISASM(cid), { + params: { symbol: target.symbol, address: target.address, session }, + }) + return data +} + +export async function fetchCfg(cid: string, target: Target): Promise { + const { data } = await apiClient.get(API_ENDPOINTS.CFG(cid), { + params: { symbol: target.symbol, address: target.address }, + }) + return data +} + +export async function fetchXrefs( + cid: string, + target: number +): Promise { + const { data } = await apiClient.get(API_ENDPOINTS.XREFS(cid), { + params: { target }, + }) + return data +} + +export async function fetchStrings( + cid: string, + minLength: number +): Promise { + const { data } = await apiClient.get(API_ENDPOINTS.STRINGS(cid), { + params: { min_length: minLength }, + }) + return data +} + +export async function fetchProgress(session: string): Promise { + const { data } = await apiClient.get(API_ENDPOINTS.PROGRESS, { + params: { session }, + }) + return data +} + +export async function submitAnswer( + cid: string, + body: SubmitRequest +): Promise { + const { data } = await apiClient.post( + API_ENDPOINTS.SUBMIT(cid), + body + ) + return data +} diff --git a/PROJECTS/advanced/rveng/frontend/src/api/hooks/index.ts b/PROJECTS/advanced/rveng/frontend/src/api/hooks/index.ts new file mode 100644 index 00000000..990f7fb1 --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/src/api/hooks/index.ts @@ -0,0 +1,119 @@ +// =================== +// © AngelaMos | 2026 +// index.ts +// =================== + +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' +import { QUERY_KEYS } from '@/config' +import { QUERY_STRATEGIES } from '@/core/api' +import { getSessionId } from '@/lib/session' +import { + fetchCfg, + fetchChallenge, + fetchChallenges, + fetchDisasm, + fetchElf, + fetchHex, + fetchProgress, + fetchStrings, + fetchXrefs, + submitAnswer, +} from '../client' +import type { SubmitResult, Target } from '../types' + +export function useChallenges() { + return useQuery({ + queryKey: QUERY_KEYS.CHALLENGES, + queryFn: fetchChallenges, + ...QUERY_STRATEGIES.static, + }) +} + +export function useChallenge(cid: string) { + return useQuery({ + queryKey: QUERY_KEYS.CHALLENGE(cid), + queryFn: () => fetchChallenge(cid), + enabled: cid.length > 0, + ...QUERY_STRATEGIES.static, + }) +} + +export function useHex(cid: string, offset: number, length: number) { + return useQuery({ + queryKey: QUERY_KEYS.HEX(cid, offset, length), + queryFn: () => fetchHex(cid, offset, length), + enabled: cid.length > 0, + ...QUERY_STRATEGIES.static, + }) +} + +export function useElf(cid: string) { + return useQuery({ + queryKey: QUERY_KEYS.ELF(cid), + queryFn: () => fetchElf(cid), + enabled: cid.length > 0, + ...QUERY_STRATEGIES.static, + }) +} + +export function useDisasm(cid: string, target: Target | null) { + const session = getSessionId() + return useQuery({ + queryKey: QUERY_KEYS.DISASM(cid, target?.label ?? '', session), + queryFn: () => fetchDisasm(cid, target as Target, session), + enabled: cid.length > 0 && target !== null, + ...QUERY_STRATEGIES.standard, + }) +} + +export function useCfg(cid: string, target: Target | null) { + return useQuery({ + queryKey: QUERY_KEYS.CFG(cid, target?.label ?? ''), + queryFn: () => fetchCfg(cid, target as Target), + enabled: cid.length > 0 && target !== null, + ...QUERY_STRATEGIES.static, + }) +} + +export function useXrefs(cid: string, target: number | null) { + return useQuery({ + queryKey: QUERY_KEYS.XREFS(cid, target ?? -1), + queryFn: () => fetchXrefs(cid, target as number), + enabled: cid.length > 0 && target !== null, + ...QUERY_STRATEGIES.static, + }) +} + +export function useStrings(cid: string, minLength: number) { + return useQuery({ + queryKey: QUERY_KEYS.STRINGS(cid, minLength), + queryFn: () => fetchStrings(cid, minLength), + enabled: cid.length > 0, + ...QUERY_STRATEGIES.static, + }) +} + +export function useProgress() { + const session = getSessionId() + return useQuery({ + queryKey: QUERY_KEYS.PROGRESS(session), + queryFn: () => fetchProgress(session), + ...QUERY_STRATEGIES.standard, + }) +} + +export function useSubmit(cid: string) { + const queryClient = useQueryClient() + const session = getSessionId() + return useMutation({ + mutationFn: (answer: string) => submitAnswer(cid, { answer, session }), + onSuccess: (result) => { + if (result.correct) { + queryClient.invalidateQueries({ queryKey: QUERY_KEYS.PROGRESS(session) }) + queryClient.invalidateQueries({ + queryKey: ['challenges', cid, 'disasm'], + }) + } + }, + }) +} diff --git a/PROJECTS/advanced/rveng/frontend/src/api/index.ts b/PROJECTS/advanced/rveng/frontend/src/api/index.ts new file mode 100644 index 00000000..0840a7c1 --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/src/api/index.ts @@ -0,0 +1,7 @@ +// =================== +// © AngelaMos | 2025 +// index.ts +// =================== + +export * from './hooks' +export * from './types' diff --git a/PROJECTS/advanced/rveng/frontend/src/api/types/index.ts b/PROJECTS/advanced/rveng/frontend/src/api/types/index.ts new file mode 100644 index 00000000..2d8dede3 --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/src/api/types/index.ts @@ -0,0 +1,136 @@ +// =================== +// © AngelaMos | 2026 +// index.ts +// =================== + +import type { Category } from '@/config' + +export interface ChallengeSummary { + id: string + module: string + title: string +} + +export interface ChallengeDetail { + id: string + module: string + title: string + mission: string + category: Category + size: number +} + +export interface HexView { + base: number + length: number + lines: string[] +} + +export interface FunctionView { + name: string + value: number + size: number +} + +export interface DiscoveredFunction { + address: number + label: string +} + +export interface SectionView { + index: number + name: string + type: string + addr: number + offset: number + size: number + flags: string +} + +export interface ElfView { + type: number + machine: number + entry: number + sections: SectionView[] + functions: FunctionView[] + discovered: DiscoveredFunction[] +} + +export interface InstructionView { + address: number + mnemonic: string + op_str: string + bytes: string + immediate: number | null + branch_target: number | null + rip_target: number | null + call_name: string | null + is_gate: boolean +} + +export interface DisasmView { + symbol: string + instructions: InstructionView[] + gate_address: number | null +} + +export interface CfgBlock { + start: number + end: number + instructions: number[] +} + +export interface CfgEdge { + src: number + dst: number + kind: string +} + +export interface CfgView { + symbol: string + blocks: CfgBlock[] + edges: CfgEdge[] +} + +export interface XrefView { + from_addr: number + to_addr: number + kind: string +} + +export interface XrefsView { + target: number + references: XrefView[] +} + +export interface Target { + label: string + address: number + symbol?: string +} + +export interface StringView { + offset: number + text: string +} + +export interface StringsView { + strings: StringView[] +} + +export interface SubmitRequest { + answer: string + session: string +} + +export interface SubmitResult { + correct: boolean + message: string + revealed_source: string | null +} + +export interface ProgressView { + session: string + solved: string[] + total: number +} diff --git a/PROJECTS/advanced/rveng/frontend/src/components/AnswerRunner.module.scss b/PROJECTS/advanced/rveng/frontend/src/components/AnswerRunner.module.scss new file mode 100644 index 00000000..67f76d08 --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/src/components/AnswerRunner.module.scss @@ -0,0 +1,77 @@ +// =================== +// © AngelaMos | 2026 +// AnswerRunner.module.scss +// =================== + +@use '../styles/tokens' as *; +@use '../styles/fonts' as *; + +.runner { + display: flex; + flex-direction: column; + gap: $space-3; +} + +.field { + display: flex; + flex-direction: column; + gap: $space-2; +} + +.hint { + font-size: $font-size-sm; + color: $color-text-muted; +} + +.inputRow { + display: flex; + gap: $space-2; +} + +.input { + flex: 1; + padding: $space-2 $space-3; + font-family: $font-mono; + font-size: $font-size-sm; + border: 1px solid $color-border; + border-radius: $radius-sm; +} + +.inputRow button { + padding: $space-2 $space-4; + font-weight: $font-weight-medium; + color: $color-white; + background: $color-gray-900; + border: none; + border-radius: $radius-sm; + cursor: pointer; + + &:disabled { + opacity: 0.4; + cursor: not-allowed; + } +} + +.correct, +.incorrect { + padding: $space-2 $space-3; + font-size: $font-size-sm; + border-radius: $radius-sm; +} + +.correct { + color: $color-gray-900; + background: $color-gray-200; + border: 1px solid $color-gray-400; +} + +.incorrect { + color: $color-gray-700; + background: $color-gray-100; + border: 1px solid $color-border; +} + +.solvedNote { + font-size: $font-size-sm; + color: $color-text-muted; +} diff --git a/PROJECTS/advanced/rveng/frontend/src/components/AnswerRunner.tsx b/PROJECTS/advanced/rveng/frontend/src/components/AnswerRunner.tsx new file mode 100644 index 00000000..12faff6d --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/src/components/AnswerRunner.tsx @@ -0,0 +1,75 @@ +// =================== +// © AngelaMos | 2026 +// AnswerRunner.tsx +// =================== + +import { type FormEvent, useState } from 'react' +import { useSubmit } from '@/api/hooks' +import type { SubmitResult } from '@/api/types' +import { CATEGORY_HINT, type Category } from '@/config' +import styles from './AnswerRunner.module.scss' +import { SourceReveal } from './SourceReveal' + +interface AnswerRunnerProps { + cid: string + category: Category + solved: boolean +} + +export function AnswerRunner({ + cid, + category, + solved, +}: AnswerRunnerProps): React.ReactElement { + const [answer, setAnswer] = useState('') + const [result, setResult] = useState(null) + const submit = useSubmit(cid) + + const onSubmit = (event: FormEvent): void => { + event.preventDefault() + if (answer.trim().length === 0) { + return + } + submit.mutate(answer, { onSuccess: (data) => setResult(data) }) + } + + const pending = submit.isPending + const empty = answer.trim().length === 0 + + return ( +
+ + + {result && ( +
+ {result.message} +
+ )} + + {result?.correct && result.revealed_source && ( + + )} + + {solved && result === null && ( +

+ Already solved. Submit again to reveal the source. +

+ )} + + ) +} diff --git a/PROJECTS/advanced/rveng/frontend/src/components/CfgPane.module.scss b/PROJECTS/advanced/rveng/frontend/src/components/CfgPane.module.scss new file mode 100644 index 00000000..0a368472 --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/src/components/CfgPane.module.scss @@ -0,0 +1,95 @@ +// =================== +// © AngelaMos | 2026 +// CfgPane.module.scss +// =================== + +@use '../styles/tokens' as *; +@use '../styles/fonts' as *; + +.cfg { + display: flex; + flex-direction: column; + gap: $space-3; +} + +.summary { + font-size: $font-size-sm; + color: $color-text-muted; +} + +.blocks { + display: flex; + flex-direction: column; + gap: $space-3; +} + +.block { + display: flex; + flex-direction: column; + gap: $space-2; + padding: $space-3 $space-4; + background: $color-surface; + border: 1px solid $color-border; + border-left: 3px solid $color-gray-700; + border-radius: $radius-sm; +} + +.blockHead { + display: flex; + align-items: baseline; + gap: $space-3; +} + +.blockName { + font-weight: $font-weight-semibold; +} + +.blockRange { + font-family: $font-mono; + font-size: $font-size-sm; + color: $color-text-muted; +} + +.blockBody { + font-size: $font-size-sm; + color: $color-text-muted; +} + +.edgeRow { + display: flex; + flex-wrap: wrap; + gap: $space-2; +} + +.edge { + padding: $space-1 $space-2; + font-family: $font-mono; + font-size: $font-size-xs; + border: 1px solid $color-border; + border-radius: $radius-full; + color: $color-text; +} + +.taken { + border-color: $color-gray-600; + background: $color-gray-100; +} + +.fallthrough { + color: $color-text-muted; +} + +.jump { + border-color: $color-gray-500; +} + +.terminal { + font-size: $font-size-xs; + color: $color-text-muted; +} + +.state { + padding: $space-4; + color: $color-text-muted; + font-size: $font-size-sm; +} diff --git a/PROJECTS/advanced/rveng/frontend/src/components/CfgPane.tsx b/PROJECTS/advanced/rveng/frontend/src/components/CfgPane.tsx new file mode 100644 index 00000000..cfd48960 --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/src/components/CfgPane.tsx @@ -0,0 +1,79 @@ +// =================== +// © AngelaMos | 2026 +// CfgPane.tsx +// =================== + +import { useMemo } from 'react' +import { useCfg } from '@/api/hooks' +import type { Target } from '@/api/types' +import { formatAddr } from '@/lib/format' +import styles from './CfgPane.module.scss' + +interface CfgPaneProps { + cid: string + target: Target | null +} + +export function CfgPane({ cid, target }: CfgPaneProps): React.ReactElement { + const { data, isLoading, isError } = useCfg(cid, target) + + const outEdges = useMemo(() => { + const map = new Map() + for (const edge of data?.edges ?? []) { + const list = map.get(edge.src) ?? [] + list.push({ dst: edge.dst, kind: edge.kind }) + map.set(edge.src, list) + } + return map + }, [data]) + + if (target === null) { + return
No function selected
+ } + if (isLoading) { + return
Building graph...
+ } + if (isError || !data) { + return
Failed to build graph
+ } + + return ( +
+

+ {data.blocks.length} basic blocks, {data.edges.length} edges +

+
+ {data.blocks.map((block, index) => { + const edges = outEdges.get(block.start) ?? [] + return ( +
+
+ block {index} + + {formatAddr(block.start)} .. {formatAddr(block.end)} + +
+
+ {block.instructions.length} instructions +
+
+ {edges.length === 0 ? ( + terminal + ) : ( + edges.map((edge) => ( + + {edge.kind} to {formatAddr(edge.dst)} + + )) + )} +
+
+ ) + })} +
+
+ ) +} diff --git a/PROJECTS/advanced/rveng/frontend/src/components/ChallengeCard.module.scss b/PROJECTS/advanced/rveng/frontend/src/components/ChallengeCard.module.scss new file mode 100644 index 00000000..50fef78a --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/src/components/ChallengeCard.module.scss @@ -0,0 +1,60 @@ +// =================== +// © AngelaMos | 2026 +// ChallengeCard.module.scss +// =================== + +@use '../styles/tokens' as *; +@use '../styles/fonts' as *; + +.card { + display: flex; + flex-direction: column; + gap: $space-2; + padding: $space-4; + background: $color-bg; + border: 1px solid $color-border; + border-radius: $radius-md; + text-decoration: none; + color: inherit; + transition: + border-color $duration-fast $ease, + transform $duration-fast $ease; + + &:hover { + border-color: $color-gray-400; + transform: translateY(-2px); + } +} + +.top { + display: flex; + align-items: center; + justify-content: space-between; +} + +.module { + font-size: $font-size-xs; + text-transform: uppercase; + letter-spacing: 0.08em; + color: $color-text-muted; +} + +.solved { + padding: $space-1 $space-2; + font-size: $font-size-xs; + font-weight: $font-weight-semibold; + color: $color-gray-900; + background: $color-gray-200; + border-radius: $radius-full; +} + +.title { + font-size: $font-size-lg; + font-weight: $font-weight-semibold; +} + +.id { + font-family: $font-mono; + font-size: $font-size-xs; + color: $color-text-muted; +} diff --git a/PROJECTS/advanced/rveng/frontend/src/components/ChallengeCard.tsx b/PROJECTS/advanced/rveng/frontend/src/components/ChallengeCard.tsx new file mode 100644 index 00000000..10f751b1 --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/src/components/ChallengeCard.tsx @@ -0,0 +1,30 @@ +// =================== +// © AngelaMos | 2026 +// ChallengeCard.tsx +// =================== + +import { Link } from 'react-router-dom' +import type { ChallengeSummary } from '@/api/types' +import { ROUTES } from '@/config' +import styles from './ChallengeCard.module.scss' + +interface ChallengeCardProps { + challenge: ChallengeSummary + solved: boolean +} + +export function ChallengeCard({ + challenge, + solved, +}: ChallengeCardProps): React.ReactElement { + return ( + +
+ {challenge.module} + {solved && Solved} +
+

{challenge.title}

+ {challenge.id} + + ) +} diff --git a/PROJECTS/advanced/rveng/frontend/src/components/DisasmPane.module.scss b/PROJECTS/advanced/rveng/frontend/src/components/DisasmPane.module.scss new file mode 100644 index 00000000..d8e8bbea --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/src/components/DisasmPane.module.scss @@ -0,0 +1,133 @@ +// =================== +// © AngelaMos | 2026 +// DisasmPane.module.scss +// =================== + +@use '../styles/tokens' as *; +@use '../styles/fonts' as *; + +.disasm { + display: flex; + flex-direction: column; + gap: $space-3; +} + +.toolbar { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: $space-4; +} + +.picker { + display: flex; + align-items: center; + gap: $space-2; + font-size: $font-size-sm; + color: $color-text-muted; + + select { + padding: $space-1 $space-2; + font-family: $font-mono; + font-size: $font-size-sm; + border: 1px solid $color-border; + border-radius: $radius-sm; + background: $color-bg; + } +} + +.gateBanner { + padding: $space-1 $space-3; + font-family: $font-mono; + font-size: $font-size-sm; + color: $color-gray-900; + background: $color-gray-200; + border: 1px solid $color-gray-400; + border-radius: $radius-sm; + cursor: pointer; +} + +.listing { + overflow-x: auto; + padding: $space-2; + background: $color-gray-900; + border-radius: $radius-md; + font-family: $font-mono; + font-size: $font-size-sm; +} + +.row { + display: grid; + grid-template-columns: 6rem 9rem 4rem 1fr auto; + gap: $space-3; + align-items: center; + padding: $space-1 $space-2; + white-space: pre; + border-radius: $radius-sm; +} + +.gate { + background: $color-gray-700; + outline: 1px solid $color-gray-400; +} + +.addr { + color: $color-gray-500; +} + +.raw { + color: $color-gray-600; + overflow: hidden; + text-overflow: ellipsis; +} + +.mnemonic { + color: $color-white; + font-weight: $font-weight-semibold; +} + +.ops { + color: $color-gray-300; +} + +.callName { + color: $color-gray-100; + font-weight: $font-weight-semibold; +} + +.dataRef { + color: $color-gray-500; +} + +.callers { + font-family: $font-mono; + font-size: $font-size-sm; + color: $color-text-muted; +} + +.target { + justify-self: end; + padding: 0 $space-2; + font-family: $font-mono; + font-size: $font-size-xs; + color: $color-gray-400; + background: transparent; + border: 1px solid transparent; + border-radius: $radius-sm; +} + +.linkable { + color: $color-gray-100; + border-color: $color-gray-600; + cursor: pointer; + + &:hover { + background: $color-gray-800; + } +} + +.state { + padding: $space-4; + color: $color-text-muted; + font-size: $font-size-sm; +} diff --git a/PROJECTS/advanced/rveng/frontend/src/components/DisasmPane.tsx b/PROJECTS/advanced/rveng/frontend/src/components/DisasmPane.tsx new file mode 100644 index 00000000..a0dac146 --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/src/components/DisasmPane.tsx @@ -0,0 +1,112 @@ +// =================== +// © AngelaMos | 2026 +// DisasmPane.tsx +// =================== + +import { useMemo, useRef } from 'react' +import { useDisasm, useXrefs } from '@/api/hooks' +import type { Target } from '@/api/types' +import { formatAddr } from '@/lib/format' +import styles from './DisasmPane.module.scss' + +interface DisasmPaneProps { + cid: string + target: Target | null +} + +export function DisasmPane({ cid, target }: DisasmPaneProps): React.ReactElement { + const { data, isLoading, isError } = useDisasm(cid, target) + const xrefs = useXrefs(cid, target?.address ?? null) + const rowRefs = useRef>(new Map()) + + const addresses = useMemo( + () => new Set((data?.instructions ?? []).map((i) => i.address)), + [data] + ) + + if (target === null) { + return
No function selected
+ } + + const scrollTo = (address: number): void => { + rowRefs.current.get(address)?.scrollIntoView({ + block: 'center', + behavior: 'smooth', + }) + } + + const callers = (xrefs.data?.references ?? []).filter((r) => r.kind === 'call') + + return ( +
+
+ {data?.gate_address != null && ( + + )} + {callers.length > 0 && ( + + called by {callers.map((c) => formatAddr(c.from_addr)).join(', ')} + + )} +
+ + {isLoading &&
Disassembling...
} + {isError &&
Failed to disassemble
} + + {data && ( +
+ {data.instructions.map((ins) => { + const linkable = + ins.branch_target != null && addresses.has(ins.branch_target) + return ( +
{ + rowRefs.current.set(ins.address, el) + }} + className={`${styles.row} ${ins.is_gate ? styles.gate : ''}`} + > + {formatAddr(ins.address)} + {ins.bytes} + {ins.mnemonic} + + {ins.op_str} + {ins.call_name && ( + + {' '} + <{ins.call_name}> + + )} + {ins.rip_target != null && ( + + {' '} + # {formatAddr(ins.rip_target)} + + )} + + {ins.branch_target != null && ( + + )} +
+ ) + })} +
+ )} +
+ ) +} diff --git a/PROJECTS/advanced/rveng/frontend/src/components/HexViewer.module.scss b/PROJECTS/advanced/rveng/frontend/src/components/HexViewer.module.scss new file mode 100644 index 00000000..e009ee96 --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/src/components/HexViewer.module.scss @@ -0,0 +1,108 @@ +// =================== +// © AngelaMos | 2026 +// HexViewer.module.scss +// =================== + +@use '../styles/tokens' as *; +@use '../styles/fonts' as *; + +.hex { + display: flex; + flex-direction: column; + gap: $space-3; +} + +.toolbar { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: $space-4; +} + +.pager { + display: flex; + gap: $space-2; + + button { + padding: $space-1 $space-3; + font-size: $font-size-sm; + border: 1px solid $color-border; + border-radius: $radius-sm; + background: $color-surface; + color: $color-text; + cursor: pointer; + + &:disabled { + opacity: 0.4; + cursor: not-allowed; + } + } +} + +.range { + font-family: $font-mono; + font-size: $font-size-sm; + color: $color-text; +} + +.total { + color: $color-text-muted; +} + +.jump { + display: flex; + gap: $space-2; + margin-left: auto; + + input { + width: 12rem; + padding: $space-1 $space-2; + font-family: $font-mono; + font-size: $font-size-sm; + border: 1px solid $color-border; + border-radius: $radius-sm; + } + + button { + padding: $space-1 $space-3; + font-size: $font-size-sm; + border: 1px solid $color-border; + border-radius: $radius-sm; + background: $color-surface; + cursor: pointer; + } +} + +.dump { + overflow-x: auto; + padding: $space-3; + background: $color-gray-900; + border-radius: $radius-md; + font-family: $font-mono; + font-size: $font-size-sm; + line-height: $line-height-normal; +} + +.row { + display: flex; + gap: $space-4; + white-space: pre; +} + +.gutter { + color: $color-gray-500; +} + +.bytes { + color: $color-gray-100; +} + +.ascii { + color: $color-gray-400; +} + +.state { + padding: $space-4; + color: $color-text-muted; + font-size: $font-size-sm; +} diff --git a/PROJECTS/advanced/rveng/frontend/src/components/HexViewer.tsx b/PROJECTS/advanced/rveng/frontend/src/components/HexViewer.tsx new file mode 100644 index 00000000..6b3ef5ae --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/src/components/HexViewer.tsx @@ -0,0 +1,122 @@ +// =================== +// © AngelaMos | 2026 +// HexViewer.tsx +// =================== + +import { type FormEvent, useState } from 'react' +import { useHex } from '@/api/hooks' +import { HEX } from '@/config' +import { formatOffset } from '@/lib/format' +import styles from './HexViewer.module.scss' + +interface HexRow { + offset: string + bytes: string + ascii: string + raw: string +} + +const LINE_RE = /^([0-9a-f]{8}) {2}(.+?) {2}\|(.*)\|$/ + +function parseLine(line: string): HexRow { + const match = LINE_RE.exec(line) + if (match === null) { + return { offset: '', bytes: '', ascii: '', raw: line } + } + return { offset: match[1], bytes: match[2], ascii: match[3], raw: line } +} + +function parseTarget(text: string): number | null { + const token = text.trim().toLowerCase() + if (token.length === 0) { + return null + } + const value = token.startsWith('0x') + ? Number.parseInt(token.slice(2), 16) + : Number.parseInt(token, 10) + return Number.isNaN(value) || value < 0 ? null : value +} + +interface HexViewerProps { + cid: string + size: number +} + +export function HexViewer({ cid, size }: HexViewerProps): React.ReactElement { + const [offset, setOffset] = useState(0) + const [jump, setJump] = useState('') + const length = HEX.PAGE_LENGTH + const { data, isLoading, isError } = useHex(cid, offset, length) + + const canPrev = offset > 0 + const canNext = offset + length < size + + const gotoPrev = (): void => setOffset(Math.max(0, offset - length)) + const gotoNext = (): void => { + if (canNext) { + setOffset(offset + length) + } + } + + const onJump = (event: FormEvent): void => { + event.preventDefault() + const target = parseTarget(jump) + if (target === null) { + return + } + const aligned = Math.min(target, Math.max(0, size - 1)) + setOffset(aligned - (aligned % HEX.BYTES_PER_LINE)) + } + + return ( +
+
+
+ + +
+ + {data + ? `${formatOffset(data.base)} .. ${formatOffset(data.base + data.length)}` + : formatOffset(offset)} + of {size} bytes + +
+ setJump(e.target.value)} + placeholder="offset e.g. 0x2000" + aria-label="Jump to offset" + /> + +
+
+ + {isLoading &&
Loading hex...
} + {isError &&
Failed to load hex
} + + {data && ( +
+ {data.lines.map(parseLine).map((row) => ( +
+ {row.offset ? ( + <> + {row.offset} + {row.bytes} + {row.ascii} + + ) : ( + {row.raw} + )} +
+ ))} +
+ )} +
+ ) +} diff --git a/PROJECTS/advanced/rveng/frontend/src/components/MissionPanel.module.scss b/PROJECTS/advanced/rveng/frontend/src/components/MissionPanel.module.scss new file mode 100644 index 00000000..42a666be --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/src/components/MissionPanel.module.scss @@ -0,0 +1,65 @@ +// =================== +// © AngelaMos | 2026 +// MissionPanel.module.scss +// =================== + +@use '../styles/tokens' as *; + +.panel { + display: flex; + flex-direction: column; + gap: $space-3; + padding: $space-5; + background: $color-surface; + border: 1px solid $color-border; + border-radius: $radius-md; +} + +.head { + display: flex; + align-items: center; + justify-content: space-between; +} + +.module { + font-size: $font-size-xs; + text-transform: uppercase; + letter-spacing: 0.08em; + color: $color-text-muted; +} + +.solved { + padding: $space-1 $space-2; + font-size: $font-size-xs; + font-weight: $font-weight-semibold; + color: $color-gray-900; + background: $color-gray-200; + border-radius: $radius-full; +} + +.title { + font-size: $font-size-xl; + font-weight: $font-weight-semibold; +} + +.mission { + color: $color-text; + line-height: $line-height-relaxed; +} + +.meta { + display: flex; + gap: $space-3; + font-size: $font-size-sm; +} + +.category { + padding: $space-1 $space-2; + color: $color-text; + background: $color-gray-100; + border-radius: $radius-sm; +} + +.size { + color: $color-text-muted; +} diff --git a/PROJECTS/advanced/rveng/frontend/src/components/MissionPanel.tsx b/PROJECTS/advanced/rveng/frontend/src/components/MissionPanel.tsx new file mode 100644 index 00000000..27b39680 --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/src/components/MissionPanel.tsx @@ -0,0 +1,36 @@ +// =================== +// © AngelaMos | 2026 +// MissionPanel.tsx +// =================== + +import type { ChallengeDetail } from '@/api/types' +import { CATEGORY_LABEL } from '@/config' +import { formatBytes } from '@/lib/format' +import styles from './MissionPanel.module.scss' + +interface MissionPanelProps { + challenge: ChallengeDetail + solved: boolean +} + +export function MissionPanel({ + challenge, + solved, +}: MissionPanelProps): React.ReactElement { + return ( +
+
+ {challenge.module} + {solved && Solved} +
+

{challenge.title}

+

{challenge.mission}

+
+ + {CATEGORY_LABEL[challenge.category]} + + {formatBytes(challenge.size)} +
+
+ ) +} diff --git a/PROJECTS/advanced/rveng/frontend/src/components/ProgressBar.module.scss b/PROJECTS/advanced/rveng/frontend/src/components/ProgressBar.module.scss new file mode 100644 index 00000000..51a2fe34 --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/src/components/ProgressBar.module.scss @@ -0,0 +1,33 @@ +// =================== +// © AngelaMos | 2026 +// ProgressBar.module.scss +// =================== + +@use '../styles/tokens' as *; + +.wrap { + display: flex; + flex-direction: column; + gap: $space-2; +} + +.label { + display: flex; + justify-content: space-between; + font-size: $font-size-sm; + color: $color-text-muted; +} + +.track { + height: 0.5rem; + background: $color-gray-200; + border-radius: $radius-full; + overflow: hidden; +} + +.fill { + height: 100%; + background: $color-gray-700; + border-radius: $radius-full; + transition: width $duration-normal $ease; +} diff --git a/PROJECTS/advanced/rveng/frontend/src/components/ProgressBar.tsx b/PROJECTS/advanced/rveng/frontend/src/components/ProgressBar.tsx new file mode 100644 index 00000000..eb7d2fc1 --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/src/components/ProgressBar.tsx @@ -0,0 +1,31 @@ +// =================== +// © AngelaMos | 2026 +// ProgressBar.tsx +// =================== + +import styles from './ProgressBar.module.scss' + +interface ProgressBarProps { + solved: number + total: number +} + +export function ProgressBar({ + solved, + total, +}: ProgressBarProps): React.ReactElement { + const pct = total > 0 ? Math.round((solved / total) * 100) : 0 + return ( +
+
+ Progress + + {solved} / {total} + +
+
+
+
+
+ ) +} diff --git a/PROJECTS/advanced/rveng/frontend/src/components/SectionMap.module.scss b/PROJECTS/advanced/rveng/frontend/src/components/SectionMap.module.scss new file mode 100644 index 00000000..05043f74 --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/src/components/SectionMap.module.scss @@ -0,0 +1,67 @@ +// =================== +// © AngelaMos | 2026 +// SectionMap.module.scss +// =================== + +@use '../styles/tokens' as *; +@use '../styles/fonts' as *; + +.wrap { + overflow-x: auto; + border: 1px solid $color-border; + border-radius: $radius-md; +} + +.table { + width: 100%; + border-collapse: collapse; + font-size: $font-size-sm; + + th, + td { + padding: $space-2 $space-3; + text-align: left; + border-bottom: 1px solid $color-border; + } + + th { + font-weight: $font-weight-semibold; + color: $color-text-muted; + background: $color-surface; + } + + tbody tr:last-child td { + border-bottom: none; + } +} + +.exec { + background: $color-gray-50; + + .name { + font-weight: $font-weight-semibold; + } +} + +.name { + font-family: $font-mono; +} + +.mono { + font-family: $font-mono; + color: $color-text-muted; +} + +.num { + text-align: right; + font-variant-numeric: tabular-nums; +} + +.dim { + color: $color-text-muted; +} + +.flags { + font-family: $font-mono; + letter-spacing: 0.1em; +} diff --git a/PROJECTS/advanced/rveng/frontend/src/components/SectionMap.tsx b/PROJECTS/advanced/rveng/frontend/src/components/SectionMap.tsx new file mode 100644 index 00000000..64fb86c3 --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/src/components/SectionMap.tsx @@ -0,0 +1,48 @@ +// =================== +// © AngelaMos | 2026 +// SectionMap.tsx +// =================== + +import type { SectionView } from '@/api/types' +import { formatAddr, formatOffset } from '@/lib/format' +import styles from './SectionMap.module.scss' + +export function SectionMap({ + sections, +}: { + sections: SectionView[] +}): React.ReactElement { + return ( +
+ + + + + + + + + + + + + + {sections.map((section) => ( + + + + + + + + + + ))} + +
#NameTypeAddrOffsetSizeFlags
{section.index}{section.name || '(null)'}{section.type}{formatAddr(section.addr)}{formatOffset(section.offset)}{section.size}{section.flags || '-'}
+
+ ) +} diff --git a/PROJECTS/advanced/rveng/frontend/src/components/SourceReveal.module.scss b/PROJECTS/advanced/rveng/frontend/src/components/SourceReveal.module.scss new file mode 100644 index 00000000..7f3ea36f --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/src/components/SourceReveal.module.scss @@ -0,0 +1,39 @@ +// =================== +// © AngelaMos | 2026 +// SourceReveal.module.scss +// =================== + +@use '../styles/tokens' as *; +@use '../styles/fonts' as *; + +.reveal { + display: flex; + flex-direction: column; + gap: $space-2; + padding: $space-4; + background: $color-gray-900; + border-radius: $radius-md; +} + +.heading { + font-size: $font-size-base; + font-weight: $font-weight-semibold; + color: $color-gray-50; +} + +.note { + font-size: $font-size-sm; + color: $color-gray-400; +} + +.code { + overflow-x: auto; + padding: $space-3; + background: $color-black; + border-radius: $radius-sm; + font-family: $font-mono; + font-size: $font-size-sm; + line-height: $line-height-normal; + color: $color-gray-100; + white-space: pre; +} diff --git a/PROJECTS/advanced/rveng/frontend/src/components/SourceReveal.tsx b/PROJECTS/advanced/rveng/frontend/src/components/SourceReveal.tsx new file mode 100644 index 00000000..fb555399 --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/src/components/SourceReveal.tsx @@ -0,0 +1,18 @@ +// =================== +// © AngelaMos | 2026 +// SourceReveal.tsx +// =================== + +import styles from './SourceReveal.module.scss' + +export function SourceReveal({ source }: { source: string }): React.ReactElement { + return ( +
+

Source revealed

+

+ You reached the answer from the binary. Here is the C that produced it. +

+
{source}
+
+ ) +} diff --git a/PROJECTS/advanced/rveng/frontend/src/components/StringsPane.module.scss b/PROJECTS/advanced/rveng/frontend/src/components/StringsPane.module.scss new file mode 100644 index 00000000..d46e7175 --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/src/components/StringsPane.module.scss @@ -0,0 +1,74 @@ +// =================== +// © AngelaMos | 2026 +// StringsPane.module.scss +// =================== + +@use '../styles/tokens' as *; +@use '../styles/fonts' as *; + +.strings { + display: flex; + flex-direction: column; + gap: $space-3; +} + +.toolbar { + display: flex; + align-items: center; + gap: $space-4; +} + +.control { + display: flex; + align-items: center; + gap: $space-3; + font-size: $font-size-sm; + color: $color-text-muted; + + input { + width: 12rem; + } +} + +.count { + margin-left: auto; + font-size: $font-size-sm; + color: $color-text-muted; +} + +.list { + max-height: 28rem; + overflow-y: auto; + border: 1px solid $color-border; + border-radius: $radius-md; + font-family: $font-mono; + font-size: $font-size-sm; +} + +.row { + display: grid; + grid-template-columns: 8rem 1fr; + gap: $space-4; + padding: $space-1 $space-3; + border-bottom: 1px solid $color-border; + + &:last-child { + border-bottom: none; + } +} + +.offset { + color: $color-text-muted; +} + +.text { + color: $color-text; + white-space: pre-wrap; + word-break: break-all; +} + +.state { + padding: $space-4; + color: $color-text-muted; + font-size: $font-size-sm; +} diff --git a/PROJECTS/advanced/rveng/frontend/src/components/StringsPane.tsx b/PROJECTS/advanced/rveng/frontend/src/components/StringsPane.tsx new file mode 100644 index 00000000..407fae2b --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/src/components/StringsPane.tsx @@ -0,0 +1,49 @@ +// =================== +// © AngelaMos | 2026 +// StringsPane.tsx +// =================== + +import { useState } from 'react' +import { useStrings } from '@/api/hooks' +import { STRINGS } from '@/config' +import { formatOffset } from '@/lib/format' +import styles from './StringsPane.module.scss' + +export function StringsPane({ cid }: { cid: string }): React.ReactElement { + const [minLength, setMinLength] = useState(STRINGS.DEFAULT_MIN_LENGTH) + const { data, isLoading, isError } = useStrings(cid, minLength) + + return ( +
+
+ + {data && ( + {data.strings.length} strings + )} +
+ + {isLoading &&
Loading strings...
} + {isError &&
Failed to load strings
} + + {data && ( +
+ {data.strings.map((entry) => ( +
+ {formatOffset(entry.offset)} + {entry.text} +
+ ))} +
+ )} +
+ ) +} diff --git a/PROJECTS/advanced/rveng/frontend/src/components/Workspace.module.scss b/PROJECTS/advanced/rveng/frontend/src/components/Workspace.module.scss new file mode 100644 index 00000000..ed1e7ab3 --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/src/components/Workspace.module.scss @@ -0,0 +1,104 @@ +// =================== +// © AngelaMos | 2026 +// Workspace.module.scss +// =================== + +@use '../styles/tokens' as *; +@use '../styles/mixins' as *; + +.workspace { + display: grid; + grid-template-columns: minmax(20rem, 24rem) 1fr; + gap: $space-6; + align-items: start; + + @include breakpoint-down('lg') { + grid-template-columns: 1fr; + } +} + +.side { + display: flex; + flex-direction: column; + gap: $space-4; + position: sticky; + top: $space-4; + + @include breakpoint-down('lg') { + position: static; + } +} + +.runnerCard { + display: flex; + flex-direction: column; + gap: $space-3; + padding: $space-5; + background: $color-bg; + border: 1px solid $color-border; + border-radius: $radius-md; +} + +.runnerTitle { + font-size: $font-size-base; + font-weight: $font-weight-semibold; +} + +.analysis { + display: flex; + flex-direction: column; + gap: $space-4; + min-width: 0; +} + +.tabs { + display: flex; + gap: $space-1; + border-bottom: 1px solid $color-border; +} + +.tab { + padding: $space-2 $space-4; + font-size: $font-size-sm; + color: $color-text-muted; + background: transparent; + border: none; + border-bottom: 2px solid transparent; + cursor: pointer; + + &:hover { + color: $color-text; + } +} + +.activeTab { + color: $color-text; + font-weight: $font-weight-medium; + border-bottom-color: $color-gray-900; +} + +.picker { + display: flex; + align-items: center; + gap: $space-2; + font-size: $font-size-sm; + color: $color-text-muted; + + select { + padding: $space-1 $space-2; + font-size: $font-size-sm; + border: 1px solid $color-border; + border-radius: $radius-sm; + background: $color-bg; + } +} + +.pane { + min-width: 0; +} + +.state { + padding: $space-6; + color: $color-text-muted; + font-size: $font-size-sm; +} diff --git a/PROJECTS/advanced/rveng/frontend/src/components/Workspace.tsx b/PROJECTS/advanced/rveng/frontend/src/components/Workspace.tsx new file mode 100644 index 00000000..48aa37b7 --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/src/components/Workspace.tsx @@ -0,0 +1,139 @@ +// =================== +// © AngelaMos | 2026 +// Workspace.tsx +// =================== + +import { useMemo, useState } from 'react' +import { useChallenge, useElf, useProgress } from '@/api/hooks' +import type { Target } from '@/api/types' +import { AnswerRunner } from './AnswerRunner' +import { CfgPane } from './CfgPane' +import { DisasmPane } from './DisasmPane' +import { HexViewer } from './HexViewer' +import { MissionPanel } from './MissionPanel' +import { SectionMap } from './SectionMap' +import { StringsPane } from './StringsPane' +import styles from './Workspace.module.scss' + +const TABS = ['hex', 'disasm', 'graph', 'sections', 'strings'] as const +type Tab = (typeof TABS)[number] + +const TAB_LABEL: Record = { + hex: 'Hex', + disasm: 'Disassembly', + graph: 'Graph', + sections: 'Sections', + strings: 'Strings', +} + +const PREFERRED = ['check', 'main', '_start'] + +function pickDefault(targets: Target[]): Target | null { + for (const name of PREFERRED) { + const hit = targets.find((t) => t.label === name) + if (hit) { + return hit + } + } + return targets[0] ?? null +} + +export function Workspace({ cid }: { cid: string }): React.ReactElement { + const [tab, setTab] = useState('hex') + const [targetKey, setTargetKey] = useState('') + const challenge = useChallenge(cid) + const elf = useElf(cid) + const progress = useProgress() + + const targets = useMemo(() => { + const named = (elf.data?.functions ?? []) + .filter((f) => f.size > 0 && f.value > 0) + .map((f) => ({ label: f.name, address: f.value, symbol: f.name })) + if (named.length > 0) { + return named + } + return (elf.data?.discovered ?? []).map((d) => ({ + label: d.label, + address: d.address, + })) + }, [elf.data]) + + if (challenge.isLoading) { + return
Loading challenge...
+ } + if (challenge.isError || !challenge.data) { + return
Challenge not found
+ } + + const detail = challenge.data + const solved = progress.data?.solved.includes(cid) ?? false + const selected = + targets.find((t) => t.label === targetKey) ?? pickDefault(targets) + const needsTarget = tab === 'disasm' || tab === 'graph' + + return ( +
+ + +
+
+ {TABS.map((item) => ( + + ))} +
+ + {needsTarget && targets.length > 0 && ( + + )} + +
+ {tab === 'hex' && } + {tab === 'disasm' && + (elf.isLoading ? ( +
Loading ELF...
+ ) : ( + + ))} + {tab === 'graph' && + (elf.isLoading ? ( +
Loading ELF...
+ ) : ( + + ))} + {tab === 'sections' && + (elf.isLoading ? ( +
Loading ELF...
+ ) : ( + + ))} + {tab === 'strings' && } +
+
+
+ ) +} diff --git a/PROJECTS/advanced/rveng/frontend/src/components/index.tsx b/PROJECTS/advanced/rveng/frontend/src/components/index.tsx new file mode 100644 index 00000000..6b55e178 --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/src/components/index.tsx @@ -0,0 +1,16 @@ +// =================== +// © AngelaMos | 2026 +// index.tsx +// =================== + +export * from './AnswerRunner' +export * from './CfgPane' +export * from './ChallengeCard' +export * from './DisasmPane' +export * from './HexViewer' +export * from './MissionPanel' +export * from './ProgressBar' +export * from './SectionMap' +export * from './SourceReveal' +export * from './StringsPane' +export * from './Workspace' diff --git a/PROJECTS/advanced/rveng/frontend/src/config.ts b/PROJECTS/advanced/rveng/frontend/src/config.ts new file mode 100644 index 00000000..abc77f62 --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/src/config.ts @@ -0,0 +1,102 @@ +// =================== +// © AngelaMos | 2026 +// config.ts +// =================== + +export const API_ENDPOINTS = { + CHALLENGES: '/challenges', + CHALLENGE: (cid: string) => `/challenges/${cid}`, + HEX: (cid: string) => `/challenges/${cid}/hex`, + ELF: (cid: string) => `/challenges/${cid}/elf`, + DISASM: (cid: string) => `/challenges/${cid}/disasm`, + CFG: (cid: string) => `/challenges/${cid}/cfg`, + XREFS: (cid: string) => `/challenges/${cid}/xrefs`, + STRINGS: (cid: string) => `/challenges/${cid}/strings`, + SUBMIT: (cid: string) => `/challenges/${cid}/submit`, + PROGRESS: '/progress', +} as const + +export const QUERY_KEYS = { + CHALLENGES: ['challenges'] as const, + CHALLENGE: (cid: string) => ['challenges', cid] as const, + HEX: (cid: string, offset: number, length: number) => + ['challenges', cid, 'hex', offset, length] as const, + ELF: (cid: string) => ['challenges', cid, 'elf'] as const, + DISASM: (cid: string, label: string, session: string) => + ['challenges', cid, 'disasm', label, session] as const, + CFG: (cid: string, label: string) => ['challenges', cid, 'cfg', label] as const, + XREFS: (cid: string, target: number) => + ['challenges', cid, 'xrefs', target] as const, + STRINGS: (cid: string, minLength: number) => + ['challenges', cid, 'strings', minLength] as const, + PROGRESS: (session: string) => ['progress', session] as const, +} as const + +export const ROUTES = { + HOME: '/', + CHALLENGE: '/c/:cid', + challenge: (cid: string) => `/c/${cid}`, +} as const + +export const STORAGE_KEYS = { + UI: 'rveng-ui', + SESSION: 'rveng-session', +} as const + +export const CATEGORY = { + FOUND_VALUE: 'found_value', + IDENTIFIED_SYMBOL: 'identified_symbol', + PATCHED_BYTES: 'patched_bytes', +} as const + +export type Category = (typeof CATEGORY)[keyof typeof CATEGORY] + +export const CATEGORY_LABEL: Record = { + [CATEGORY.FOUND_VALUE]: 'Find a value', + [CATEGORY.IDENTIFIED_SYMBOL]: 'Name a symbol', + [CATEGORY.PATCHED_BYTES]: 'Patch bytes', +} + +export const CATEGORY_HINT: Record = { + [CATEGORY.FOUND_VALUE]: 'Submit the value in decimal or hex (0x...)', + [CATEGORY.IDENTIFIED_SYMBOL]: 'Submit the exact symbol name', + [CATEGORY.PATCHED_BYTES]: 'Submit the replacement bytes as hex (e.g. 9090)', +} + +export const HEX = { + DEFAULT_LENGTH: 256, + PAGE_LENGTH: 256, + MAX_LENGTH: 4096, + BYTES_PER_LINE: 16, +} as const + +export const STRINGS = { + DEFAULT_MIN_LENGTH: 4, + MIN_MIN_LENGTH: 1, + MAX_MIN_LENGTH: 64, +} as const + +export const QUERY_CONFIG = { + STALE_TIME: { + STATIC: Number.POSITIVE_INFINITY, + DEFAULT: 1000 * 60 * 5, + FREQUENT: 1000 * 30, + }, + GC_TIME: { + DEFAULT: 1000 * 60 * 30, + LONG: 1000 * 60 * 60, + }, + RETRY: { + DEFAULT: 2, + NONE: 0, + }, +} as const + +export const HTTP_STATUS = { + OK: 200, + BAD_REQUEST: 400, + NOT_FOUND: 404, + PAYLOAD_TOO_LARGE: 413, + UNPROCESSABLE: 422, + INTERNAL_SERVER: 500, +} as const diff --git a/PROJECTS/advanced/rveng/frontend/src/core/api/api.config.ts b/PROJECTS/advanced/rveng/frontend/src/core/api/api.config.ts new file mode 100644 index 00000000..9f8bc734 --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/src/core/api/api.config.ts @@ -0,0 +1,22 @@ +// =================== +// © AngelaMos | 2026 +// api.config.ts +// =================== + +import axios, { type AxiosInstance } from 'axios' +import { transformAxiosError } from './errors' + +const getBaseURL = (): string => { + return import.meta.env.VITE_API_URL ?? '/api' +} + +export const apiClient: AxiosInstance = axios.create({ + baseURL: getBaseURL(), + timeout: 15000, + headers: { 'Content-Type': 'application/json' }, +}) + +apiClient.interceptors.response.use( + (response) => response, + (error) => Promise.reject(transformAxiosError(error)) +) diff --git a/PROJECTS/advanced/rveng/frontend/src/core/api/errors.ts b/PROJECTS/advanced/rveng/frontend/src/core/api/errors.ts new file mode 100644 index 00000000..26982793 --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/src/core/api/errors.ts @@ -0,0 +1,116 @@ +/** + * ©AngelaMos | 2025 + * errors.ts + */ + +import type { AxiosError } from 'axios' + +export const ApiErrorCode = { + NETWORK_ERROR: 'NETWORK_ERROR', + VALIDATION_ERROR: 'VALIDATION_ERROR', + AUTHENTICATION_ERROR: 'AUTHENTICATION_ERROR', + AUTHORIZATION_ERROR: 'AUTHORIZATION_ERROR', + NOT_FOUND: 'NOT_FOUND', + CONFLICT: 'CONFLICT', + RATE_LIMITED: 'RATE_LIMITED', + SERVER_ERROR: 'SERVER_ERROR', + UNKNOWN_ERROR: 'UNKNOWN_ERROR', +} as const + +export type ApiErrorCode = (typeof ApiErrorCode)[keyof typeof ApiErrorCode] + +export class ApiError extends Error { + readonly code: ApiErrorCode + readonly statusCode: number + readonly details?: Record + + constructor( + message: string, + code: ApiErrorCode, + statusCode: number, + details?: Record + ) { + super(message) + this.name = 'ApiError' + this.code = code + this.statusCode = statusCode + this.details = details + } + + getUserMessage(): string { + const messages: Record = { + [ApiErrorCode.NETWORK_ERROR]: + 'Unable to connect. Please check your internet connection.', + [ApiErrorCode.VALIDATION_ERROR]: 'Please check your input and try again.', + [ApiErrorCode.AUTHENTICATION_ERROR]: + 'Your session has expired. Please log in again.', + [ApiErrorCode.AUTHORIZATION_ERROR]: + 'You do not have permission to perform this action.', + [ApiErrorCode.NOT_FOUND]: 'The requested resource was not found.', + [ApiErrorCode.CONFLICT]: + 'This operation conflicts with an existing resource.', + [ApiErrorCode.RATE_LIMITED]: + 'Too many requests. Please wait a moment and try again.', + [ApiErrorCode.SERVER_ERROR]: + 'Something went wrong on our end. Please try again later.', + [ApiErrorCode.UNKNOWN_ERROR]: + 'An unexpected error occurred. Please try again.', + } + return messages[this.code] + } +} + +interface ApiErrorResponse { + detail?: string | { msg: string; type: string }[] + message?: string +} + +export function transformAxiosError(error: AxiosError): ApiError { + if (!error.response) { + return new ApiError('Network error', ApiErrorCode.NETWORK_ERROR, 0) + } + + const { status } = error.response + const data = error.response.data as ApiErrorResponse | undefined + let message = 'An error occurred' + let details: Record | undefined + + if (data?.detail) { + if (typeof data.detail === 'string') { + message = data.detail + } else if (Array.isArray(data.detail)) { + details = { validation: [] } + data.detail.forEach((err) => { + details?.validation.push(err.msg) + }) + message = 'Validation error' + } + } else if (data?.message) { + message = data.message + } + + const codeMap: Record = { + 400: ApiErrorCode.VALIDATION_ERROR, + 401: ApiErrorCode.AUTHENTICATION_ERROR, + 403: ApiErrorCode.AUTHORIZATION_ERROR, + 404: ApiErrorCode.NOT_FOUND, + 409: ApiErrorCode.CONFLICT, + 413: ApiErrorCode.VALIDATION_ERROR, + 422: ApiErrorCode.VALIDATION_ERROR, + 429: ApiErrorCode.RATE_LIMITED, + 500: ApiErrorCode.SERVER_ERROR, + 502: ApiErrorCode.SERVER_ERROR, + 503: ApiErrorCode.SERVER_ERROR, + 504: ApiErrorCode.SERVER_ERROR, + } + + const code = codeMap[status] || ApiErrorCode.UNKNOWN_ERROR + + return new ApiError(message, code, status, details) +} + +declare module '@tanstack/react-query' { + interface Register { + defaultError: ApiError + } +} diff --git a/PROJECTS/advanced/rveng/frontend/src/core/api/index.ts b/PROJECTS/advanced/rveng/frontend/src/core/api/index.ts new file mode 100644 index 00000000..1818cd7e --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/src/core/api/index.ts @@ -0,0 +1,8 @@ +// =================== +// © AngelaMos | 2025 +// index.ts +// =================== + +export * from './api.config' +export * from './errors' +export * from './query.config' diff --git a/PROJECTS/advanced/rveng/frontend/src/core/api/query.config.ts b/PROJECTS/advanced/rveng/frontend/src/core/api/query.config.ts new file mode 100644 index 00000000..d6590232 --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/src/core/api/query.config.ts @@ -0,0 +1,93 @@ +// =================== +// © AngelaMos | 2026 +// query.config.ts +// =================== + +import { MutationCache, QueryCache, QueryClient } from '@tanstack/react-query' +import { toast } from 'sonner' +import { QUERY_CONFIG } from '@/config' +import { ApiError, ApiErrorCode } from './errors' + +const NO_RETRY_ERROR_CODES: readonly ApiErrorCode[] = [ + ApiErrorCode.NOT_FOUND, + ApiErrorCode.VALIDATION_ERROR, +] as const + +const shouldRetryQuery = (failureCount: number, error: Error): boolean => { + if (error instanceof ApiError) { + if (NO_RETRY_ERROR_CODES.includes(error.code)) { + return false + } + } + return failureCount < QUERY_CONFIG.RETRY.DEFAULT +} + +const calculateRetryDelay = (attemptIndex: number): number => { + const baseDelay = 500 + const maxDelay = 5000 + return Math.min(baseDelay * 2 ** attemptIndex, maxDelay) +} + +const handleQueryCacheError = ( + error: Error, + query: { state: { data: unknown } } +): void => { + if (query.state.data !== undefined) { + const message = + error instanceof ApiError + ? error.getUserMessage() + : 'Background update failed' + toast.error(message) + } +} + +const handleMutationCacheError = ( + error: Error, + _variables: unknown, + _context: unknown, + mutation: { options: { onError?: unknown } } +): void => { + if (mutation.options.onError === undefined) { + const message = + error instanceof ApiError ? error.getUserMessage() : 'Operation failed' + toast.error(message) + } +} + +export const QUERY_STRATEGIES = { + standard: { + staleTime: QUERY_CONFIG.STALE_TIME.DEFAULT, + gcTime: QUERY_CONFIG.GC_TIME.DEFAULT, + }, + static: { + staleTime: QUERY_CONFIG.STALE_TIME.STATIC, + gcTime: QUERY_CONFIG.GC_TIME.LONG, + refetchOnMount: false, + refetchOnWindowFocus: false, + }, +} as const + +export type QueryStrategy = keyof typeof QUERY_STRATEGIES + +export const queryClient = new QueryClient({ + defaultOptions: { + queries: { + staleTime: QUERY_CONFIG.STALE_TIME.DEFAULT, + gcTime: QUERY_CONFIG.GC_TIME.DEFAULT, + retry: shouldRetryQuery, + retryDelay: calculateRetryDelay, + refetchOnWindowFocus: false, + refetchOnMount: true, + refetchOnReconnect: true, + }, + mutations: { + retry: QUERY_CONFIG.RETRY.NONE, + }, + }, + queryCache: new QueryCache({ + onError: handleQueryCacheError, + }), + mutationCache: new MutationCache({ + onError: handleMutationCacheError, + }), +}) diff --git a/PROJECTS/advanced/rveng/frontend/src/core/app/routers.tsx b/PROJECTS/advanced/rveng/frontend/src/core/app/routers.tsx new file mode 100644 index 00000000..e8b12abb --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/src/core/app/routers.tsx @@ -0,0 +1,28 @@ +// =================== +// © AngelaMos | 2026 +// routers.tsx +// =================== + +import { createBrowserRouter } from 'react-router-dom' +import { ROUTES } from '@/config' +import { Shell } from './shell' + +export const router = createBrowserRouter([ + { + element: , + children: [ + { + path: ROUTES.HOME, + lazy: () => import('@/pages/browser'), + }, + { + path: ROUTES.CHALLENGE, + lazy: () => import('@/pages/workspace'), + }, + { + path: '*', + lazy: () => import('@/pages/browser'), + }, + ], + }, +]) diff --git a/PROJECTS/advanced/rveng/frontend/src/core/app/shell.module.scss b/PROJECTS/advanced/rveng/frontend/src/core/app/shell.module.scss new file mode 100644 index 00000000..a58244fe --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/src/core/app/shell.module.scss @@ -0,0 +1,62 @@ +// =================== +// © AngelaMos | 2026 +// shell.module.scss +// =================== + +@use '../../styles/tokens' as *; +@use '../../styles/fonts' as *; + +.shell { + display: flex; + flex-direction: column; + min-height: 100vh; + min-height: 100dvh; +} + +.header { + display: flex; + align-items: baseline; + gap: $space-3; + padding: $space-4 $space-6; + border-bottom: 1px solid $color-border; + background: $color-bg; +} + +.brand { + font-family: $font-mono; + font-size: $font-size-xl; + font-weight: $font-weight-bold; + color: $color-text; + text-decoration: none; +} + +.tag { + font-size: $font-size-sm; + color: $color-text-muted; +} + +.content { + flex: 1; + padding: $space-6; +} + +.loading { + padding: $space-8; + color: $color-text-muted; +} + +.error { + display: flex; + flex-direction: column; + gap: $space-3; + padding: $space-6; + + pre { + padding: $space-3; + background: $color-gray-100; + border-radius: $radius-sm; + font-family: $font-mono; + font-size: $font-size-sm; + overflow-x: auto; + } +} diff --git a/PROJECTS/advanced/rveng/frontend/src/core/app/shell.tsx b/PROJECTS/advanced/rveng/frontend/src/core/app/shell.tsx new file mode 100644 index 00000000..8d2ac468 --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/src/core/app/shell.tsx @@ -0,0 +1,44 @@ +/** + * ©AngelaMos | 2026 + * shell.tsx + */ + +import { Suspense } from 'react' +import { ErrorBoundary } from 'react-error-boundary' +import { Link, Outlet } from 'react-router-dom' +import { ROUTES } from '@/config' +import styles from './shell.module.scss' + +function ShellErrorFallback({ error }: { error: Error }): React.ReactElement { + return ( +
+

Something went wrong

+
{error.message}
+
+ ) +} + +function ShellLoading(): React.ReactElement { + return
Loading...
+} + +export function Shell(): React.ReactElement { + return ( +
+
+ + rveng + + reverse-engineering lab +
+ +
+ + }> + + + +
+
+ ) +} diff --git a/PROJECTS/advanced/rveng/frontend/src/core/app/toast.module.scss b/PROJECTS/advanced/rveng/frontend/src/core/app/toast.module.scss new file mode 100644 index 00000000..62a8ee32 --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/src/core/app/toast.module.scss @@ -0,0 +1,4 @@ +// =================== +// © AngelaMos | 2026 +// toast.module.scss +// =================== diff --git a/PROJECTS/advanced/rveng/frontend/src/core/lib/index.ts b/PROJECTS/advanced/rveng/frontend/src/core/lib/index.ts new file mode 100644 index 00000000..cdee46e2 --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/src/core/lib/index.ts @@ -0,0 +1,6 @@ +// =================== +// © AngelaMos | 2026 +// index.ts +// =================== + +export * from './shell.ui.store' diff --git a/PROJECTS/advanced/rveng/frontend/src/core/lib/shell.ui.store.ts b/PROJECTS/advanced/rveng/frontend/src/core/lib/shell.ui.store.ts new file mode 100644 index 00000000..d601a53b --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/src/core/lib/shell.ui.store.ts @@ -0,0 +1,63 @@ +/** + * ©AngelaMos | 2025 + * ui.store.ts + */ + +import { create } from 'zustand' +import { devtools, persist } from 'zustand/middleware' + +type Theme = 'light' | 'dark' | 'system' + +interface UIState { + theme: Theme + sidebarOpen: boolean + sidebarCollapsed: boolean + setTheme: (theme: Theme) => void + toggleSidebar: () => void + setSidebarOpen: (open: boolean) => void + toggleSidebarCollapsed: () => void +} + +export const useUIStore = create()( + devtools( + persist( + (set) => ({ + theme: 'dark', + sidebarOpen: false, + sidebarCollapsed: false, + + setTheme: (theme) => set({ theme }, false, 'ui/setTheme'), + + toggleSidebar: () => + set( + (state) => ({ sidebarOpen: !state.sidebarOpen }), + false, + 'ui/toggleSidebar' + ), + + setSidebarOpen: (open) => + set({ sidebarOpen: open }, false, 'ui/setSidebarOpen'), + + toggleSidebarCollapsed: () => + set( + (state) => ({ sidebarCollapsed: !state.sidebarCollapsed }), + false, + 'ui/toggleSidebarCollapsed' + ), + }), + { + name: 'ui-storage', + partialize: (state) => ({ + theme: state.theme, + sidebarCollapsed: state.sidebarCollapsed, + }), + } + ), + { name: 'UIStore' } + ) +) + +export const useTheme = (): Theme => useUIStore((s) => s.theme) +export const useSidebarOpen = (): boolean => useUIStore((s) => s.sidebarOpen) +export const useSidebarCollapsed = (): boolean => + useUIStore((s) => s.sidebarCollapsed) diff --git a/PROJECTS/advanced/rveng/frontend/src/lib/format.ts b/PROJECTS/advanced/rveng/frontend/src/lib/format.ts new file mode 100644 index 00000000..3f60dde0 --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/src/lib/format.ts @@ -0,0 +1,48 @@ +// =================== +// © AngelaMos | 2026 +// format.ts +// =================== + +export function toHex(value: number, pad = 0): string { + const body = value.toString(16).padStart(pad, '0') + return `0x${body}` +} + +export function formatAddr(value: number): string { + return toHex(value, 6) +} + +export function formatOffset(value: number): string { + return toHex(value, 8) +} + +const ELF_TYPES: Record = { + 0: 'NONE', + 1: 'REL', + 2: 'EXEC', + 3: 'DYN', + 4: 'CORE', +} + +const ELF_MACHINES: Record = { + 3: 'x86', + 40: 'ARM', + 62: 'x86-64', + 183: 'AArch64', + 243: 'RISC-V', +} + +export function elfTypeName(value: number): string { + return ELF_TYPES[value] ?? toHex(value) +} + +export function elfMachineName(value: number): string { + return ELF_MACHINES[value] ?? toHex(value) +} + +export function formatBytes(count: number): string { + if (count < 1024) { + return `${count} B` + } + return `${(count / 1024).toFixed(1)} KiB` +} diff --git a/PROJECTS/advanced/rveng/frontend/src/lib/session.ts b/PROJECTS/advanced/rveng/frontend/src/lib/session.ts new file mode 100644 index 00000000..2ff1da9c --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/src/lib/session.ts @@ -0,0 +1,35 @@ +// =================== +// © AngelaMos | 2026 +// session.ts +// =================== + +import { STORAGE_KEYS } from '@/config' + +function createId(): string { + if (typeof crypto !== 'undefined' && 'randomUUID' in crypto) { + return crypto.randomUUID() + } + return `s-${Math.random().toString(36).slice(2)}-${Date.now().toString(36)}` +} + +let cached: string | null = null + +export function getSessionId(): string { + if (cached !== null) { + return cached + } + try { + const existing = localStorage.getItem(STORAGE_KEYS.SESSION) + if (existing) { + cached = existing + return existing + } + const fresh = createId() + localStorage.setItem(STORAGE_KEYS.SESSION, fresh) + cached = fresh + return fresh + } catch { + cached = createId() + return cached + } +} diff --git a/PROJECTS/advanced/rveng/frontend/src/main.tsx b/PROJECTS/advanced/rveng/frontend/src/main.tsx new file mode 100644 index 00000000..1b1f2c8a --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/src/main.tsx @@ -0,0 +1,15 @@ +// =========================== +// ©AngelaMos | 2025 +// main.tsx +// =========================== + +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import App from './App' +import './styles.scss' + +createRoot(document.getElementById('root')!).render( + + + +) diff --git a/PROJECTS/advanced/rveng/frontend/src/pages/browser/browser.module.scss b/PROJECTS/advanced/rveng/frontend/src/pages/browser/browser.module.scss new file mode 100644 index 00000000..175a40d2 --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/src/pages/browser/browser.module.scss @@ -0,0 +1,44 @@ +// =================== +// © AngelaMos | 2026 +// browser.module.scss +// =================== + +@use '../../styles/tokens' as *; +@use '../../styles/mixins' as *; + +.page { + display: flex; + flex-direction: column; + gap: $space-6; + max-width: $breakpoint-xl; + margin: 0 auto; +} + +.header { + display: flex; + flex-direction: column; + gap: $space-2; +} + +.title { + font-size: $font-size-3xl; + font-weight: $font-weight-bold; +} + +.subtitle { + color: $color-text-muted; +} + +.progress { + max-width: 24rem; +} + +.grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr)); + gap: $space-4; +} + +.state { + color: $color-text-muted; +} diff --git a/PROJECTS/advanced/rveng/frontend/src/pages/browser/index.tsx b/PROJECTS/advanced/rveng/frontend/src/pages/browser/index.tsx new file mode 100644 index 00000000..19d4b023 --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/src/pages/browser/index.tsx @@ -0,0 +1,53 @@ +// =================== +// © AngelaMos | 2026 +// index.tsx +// =================== + +import { useChallenges, useProgress } from '@/api/hooks' +import { ChallengeCard, ProgressBar } from '@/components' +import styles from './browser.module.scss' + +export function Component(): React.ReactElement { + const challenges = useChallenges() + const progress = useProgress() + const solvedSet = new Set(progress.data?.solved ?? []) + + return ( +
+
+

Challenges

+

+ Read the binary. Reach the answer. Reveal the source. +

+
+ + {progress.data && ( +
+ +
+ )} + + {challenges.isLoading &&

Loading...

} + {challenges.isError && ( +

Failed to load challenges

+ )} + + {challenges.data && ( +
+ {challenges.data.map((challenge) => ( + + ))} +
+ )} +
+ ) +} + +Component.displayName = 'Browser' diff --git a/PROJECTS/advanced/rveng/frontend/src/pages/workspace/index.tsx b/PROJECTS/advanced/rveng/frontend/src/pages/workspace/index.tsx new file mode 100644 index 00000000..f17a1e31 --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/src/pages/workspace/index.tsx @@ -0,0 +1,28 @@ +// =================== +// © AngelaMos | 2026 +// index.tsx +// =================== + +import { Link, useParams } from 'react-router-dom' +import { Workspace } from '@/components' +import { ROUTES } from '@/config' +import styles from './workspace.module.scss' + +export function Component(): React.ReactElement { + const { cid } = useParams<{ cid: string }>() + + if (!cid) { + return
No challenge selected
+ } + + return ( +
+ + Back to challenges + + +
+ ) +} + +Component.displayName = 'ChallengeWorkspace' diff --git a/PROJECTS/advanced/rveng/frontend/src/pages/workspace/workspace.module.scss b/PROJECTS/advanced/rveng/frontend/src/pages/workspace/workspace.module.scss new file mode 100644 index 00000000..cf482729 --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/src/pages/workspace/workspace.module.scss @@ -0,0 +1,24 @@ +// =================== +// © AngelaMos | 2026 +// workspace.module.scss +// =================== + +@use '../../styles/tokens' as *; + +.page { + display: flex; + flex-direction: column; + gap: $space-4; +} + +.back { + align-self: flex-start; + font-size: $font-size-sm; + color: $color-text-muted; + text-decoration: none; + + &:hover { + color: $color-text; + text-decoration: underline; + } +} diff --git a/PROJECTS/advanced/rveng/frontend/src/styles.scss b/PROJECTS/advanced/rveng/frontend/src/styles.scss new file mode 100644 index 00000000..d3afe02d --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/src/styles.scss @@ -0,0 +1,31 @@ +// =================== +// © AngelaMos | 2026 +// styles.scss +// =================== + +@forward 'styles/tokens'; +@forward 'styles/fonts'; +@forward 'styles/mixins'; + +@use 'styles/reset'; +@use 'styles/tokens' as *; +@use 'styles/fonts' as *; + +body { + font-family: $font-sans; + color: $color-text; + background-color: $color-bg; +} + +#root { + min-height: 100vh; + min-height: 100dvh; + display: flex; + flex-direction: column; +} + +.app { + flex: 1; + display: flex; + flex-direction: column; +} diff --git a/PROJECTS/advanced/rveng/frontend/src/styles/_fonts.scss b/PROJECTS/advanced/rveng/frontend/src/styles/_fonts.scss new file mode 100644 index 00000000..049e125a --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/src/styles/_fonts.scss @@ -0,0 +1,9 @@ +// =================== +// © AngelaMos | 2026 +// _fonts.scss +// =================== + +$font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, + Arial, sans-serif; +$font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, 'Liberation Mono', + monospace; diff --git a/PROJECTS/advanced/rveng/frontend/src/styles/_index.scss b/PROJECTS/advanced/rveng/frontend/src/styles/_index.scss new file mode 100644 index 00000000..9d5d028c --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/src/styles/_index.scss @@ -0,0 +1,8 @@ +// =================== +// © AngelaMos | 2026 +// _index.scss +// =================== + +@forward 'tokens'; +@forward 'fonts'; +@forward 'mixins'; diff --git a/PROJECTS/advanced/rveng/frontend/src/styles/_mixins.scss b/PROJECTS/advanced/rveng/frontend/src/styles/_mixins.scss new file mode 100644 index 00000000..aef18b11 --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/src/styles/_mixins.scss @@ -0,0 +1,42 @@ +// =================== +// © AngelaMos | 2026 +// _mixins.scss +// =================== + +@use 'sass:map'; +@use 'tokens' as *; + +$breakpoints: ( + 'xs': $breakpoint-xs, + 'sm': $breakpoint-sm, + 'md': $breakpoint-md, + 'lg': $breakpoint-lg, + 'xl': $breakpoint-xl, + '2xl': $breakpoint-2xl, +); + +@mixin breakpoint-up($size) { + @media (min-width: map.get($breakpoints, $size)) { @content; } +} + +@mixin breakpoint-down($size) { + @media (max-width: map.get($breakpoints, $size)) { @content; } +} + +@mixin flex-center { + display: flex; + align-items: center; + justify-content: center; +} + +@mixin visually-hidden { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0 0 0 0); + white-space: nowrap; + border: 0; +} diff --git a/PROJECTS/advanced/rveng/frontend/src/styles/_reset.scss b/PROJECTS/advanced/rveng/frontend/src/styles/_reset.scss new file mode 100644 index 00000000..71e9d060 --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/src/styles/_reset.scss @@ -0,0 +1,75 @@ +// =================== +// © AngelaMos | 2026 +// _reset.scss +// =================== + +*, +*::before, +*::after { + box-sizing: border-box; +} + +* { + margin: 0; + padding: 0; + -webkit-tap-highlight-color: transparent; +} + +html { + font-size: 16px; + -webkit-text-size-adjust: 100%; +} + +body { + min-height: 100vh; + line-height: 1.5; + -webkit-font-smoothing: antialiased; + text-rendering: optimizeLegibility; +} + +img, +picture, +video, +canvas, +svg { + display: block; + max-width: 100%; +} + +input, +button, +textarea, +select { + font: inherit; + color: inherit; +} + +button { + cursor: pointer; + background: none; + border: none; +} + +a { + color: inherit; + text-decoration: none; +} + +ul, +ol { + list-style: none; +} + +p, +h1, +h2, +h3, +h4, +h5, +h6 { + overflow-wrap: break-word; +} + +#root { + isolation: isolate; +} diff --git a/PROJECTS/advanced/rveng/frontend/src/styles/_tokens.scss b/PROJECTS/advanced/rveng/frontend/src/styles/_tokens.scss new file mode 100644 index 00000000..7880360e --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/src/styles/_tokens.scss @@ -0,0 +1,85 @@ +// =================== +// © AngelaMos | 2026 +// _tokens.scss +// =================== +// +// Neutral baseline. No brand identity -- grayscale palette, system type, plain +// spacing scale. Design on top of these; nothing here implies a look. + +// Spacing (8px base) +$space-0: 0; +$space-1: 0.25rem; +$space-2: 0.5rem; +$space-3: 0.75rem; +$space-4: 1rem; +$space-5: 1.25rem; +$space-6: 1.5rem; +$space-8: 2rem; +$space-10: 2.5rem; +$space-12: 3rem; +$space-16: 4rem; +$space-20: 5rem; +$space-24: 6rem; + +// Font sizes +$font-size-xs: 0.75rem; +$font-size-sm: 0.875rem; +$font-size-base: 1rem; +$font-size-lg: 1.125rem; +$font-size-xl: 1.25rem; +$font-size-2xl: 1.5rem; +$font-size-3xl: 1.875rem; +$font-size-4xl: 2.25rem; + +// Weights / line-height / radius +$font-weight-normal: 400; +$font-weight-medium: 500; +$font-weight-semibold: 600; +$font-weight-bold: 700; +$line-height-tight: 1.25; +$line-height-normal: 1.5; +$line-height-relaxed: 1.75; +$radius-sm: 0.25rem; +$radius-md: 0.5rem; +$radius-lg: 0.75rem; +$radius-full: 9999px; + +// Neutral grayscale +$color-white: #ffffff; +$color-black: #000000; +$color-gray-50: #fafafa; +$color-gray-100: #f4f4f5; +$color-gray-200: #e4e4e7; +$color-gray-300: #d4d4d8; +$color-gray-400: #a1a1aa; +$color-gray-500: #71717a; +$color-gray-600: #52525b; +$color-gray-700: #3f3f46; +$color-gray-800: #27272a; +$color-gray-900: #18181b; + +// Neutral semantic aliases +$color-bg: $color-white; +$color-surface: $color-gray-50; +$color-border: $color-gray-200; +$color-text: $color-gray-900; +$color-text-muted: $color-gray-500; + +// Breakpoints +$breakpoint-xs: 480px; +$breakpoint-sm: 640px; +$breakpoint-md: 768px; +$breakpoint-lg: 1024px; +$breakpoint-xl: 1280px; +$breakpoint-2xl: 1536px; + +// Z-index +$z-base: 1; +$z-dropdown: 1000; +$z-modal: 1300; +$z-toast: 1400; + +// Motion +$duration-fast: 150ms; +$duration-normal: 250ms; +$ease: cubic-bezier(0.4, 0, 0.2, 1); diff --git a/PROJECTS/advanced/rveng/frontend/stylelint.config.js b/PROJECTS/advanced/rveng/frontend/stylelint.config.js new file mode 100644 index 00000000..56ebd7bc --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/stylelint.config.js @@ -0,0 +1,107 @@ +// ©AngelaMos | 2025 +// stylelint.config.js + +/** @type {import('stylelint').Config} */ +export default { + extends: ['stylelint-config-standard-scss', 'stylelint-config-prettier-scss'], + rules: { + 'block-no-empty': true, + 'declaration-no-important': true, + 'color-no-invalid-hex': true, + 'property-no-unknown': true, + 'selector-pseudo-class-no-unknown': [ + true, + { + ignorePseudoClasses: ['global'], + }, + ], + + 'selector-class-pattern': [ + '^[a-z]([a-z0-9-]+)?(__[a-z0-9]([a-z0-9-]+)?)?(--[a-z0-9]([a-z0-9-]+)?)?$|^[a-z][a-zA-Z0-9]*$', + { + message: + 'Selector should be in BEM format (e.g., .block__element--modifier) or CSS Modules camelCase (e.g., .testButton)', + }, + ], + + 'value-keyword-case': [ + 'lower', + { + camelCaseSvgKeywords: true, + ignoreKeywords: [ + 'BlinkMacSystemFont', + 'SFMono-Regular', + 'Menlo', + 'Monaco', + 'Consolas', + 'Roboto', + 'Arial', + 'Helvetica', + 'Times', + 'Georgia', + 'Verdana', + 'Tahoma', + 'Trebuchet', + 'Impact', + 'Comic', + ], + }, + ], + + 'property-no-vendor-prefix': [ + true, + { + ignoreProperties: ['text-size-adjust', 'appearance', 'backdrop-filter'], + }, + ], + 'value-no-vendor-prefix': true, + 'selector-no-vendor-prefix': true, + + 'property-no-deprecated': [ + true, + { + ignoreProperties: ['clip'], + }, + ], + + 'container-name-pattern': null, + 'layer-name-pattern': null, + + 'scss/at-rule-no-unknown': true, + 'scss/declaration-nested-properties-no-divided-groups': true, + 'scss/dollar-variable-no-missing-interpolation': true, + 'scss/dollar-variable-empty-line-before': null, + + 'declaration-empty-line-before': null, + 'custom-property-empty-line-before': null, + + 'no-descending-specificity': null, + + 'media-feature-name-no-unknown': [ + true, + { + ignoreMediaFeatureNames: ['map'], + }, + ], + + 'color-function-notation': null, + 'hue-degree-notation': null, + }, + ignoreFiles: [ + 'node_modules/**', + 'dist/**', + 'build/**', + '**/*.js', + '**/*.ts', + '**/*.tsx', + ], + overrides: [ + { + files: ['**/styles/_reset.scss', '**/styles/_fonts.scss'], + rules: { + 'declaration-no-important': null, + 'scss/comment-no-empty': null, + }, + }, + ], +} diff --git a/PROJECTS/advanced/rveng/frontend/tsconfig.app.json b/PROJECTS/advanced/rveng/frontend/tsconfig.app.json new file mode 100644 index 00000000..afe17f60 --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/tsconfig.app.json @@ -0,0 +1,31 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "types": ["vite/client"], + "skipLibCheck": true, + + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true, + + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["src"] +} diff --git a/PROJECTS/advanced/rveng/frontend/tsconfig.json b/PROJECTS/advanced/rveng/frontend/tsconfig.json new file mode 100644 index 00000000..1ffef600 --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/PROJECTS/advanced/rveng/frontend/tsconfig.node.json b/PROJECTS/advanced/rveng/frontend/tsconfig.node.json new file mode 100644 index 00000000..a96b3e59 --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/tsconfig.node.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "ES2023", + "lib": ["ES2023"], + "module": "ESNext", + "types": ["node"], + "skipLibCheck": true, + + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/PROJECTS/advanced/rveng/frontend/vite.config.ts b/PROJECTS/advanced/rveng/frontend/vite.config.ts new file mode 100644 index 00000000..601fec69 --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/vite.config.ts @@ -0,0 +1,70 @@ +/** + * ©AngelaMos | 2026 + * vite.config.ts + */ + +import path from 'node:path' +import react from '@vitejs/plugin-react' +import { defineConfig, loadEnv } from 'vite' +import tsconfigPaths from 'vite-tsconfig-paths' + +export default defineConfig(({ mode }) => { + const env = loadEnv(mode, __dirname, '') + const isDev = mode === 'development' + + return { + plugins: [react(), tsconfigPaths()], + + resolve: { + alias: { + '@': path.resolve(__dirname, './src'), + }, + }, + + css: { + preprocessorOptions: { + scss: {}, + }, + }, + + server: { + port: 5173, + host: '0.0.0.0', + proxy: { + '/api': { + target: env.VITE_API_TARGET || 'http://localhost:8000', + changeOrigin: true, + }, + }, + }, + + build: { + target: 'esnext', + cssTarget: 'chrome100', + sourcemap: isDev ? true : 'hidden', + minify: 'oxc', + rollupOptions: { + output: { + manualChunks(id: string): string | undefined { + if (id.includes('node_modules')) { + if (id.includes('react-dom') || id.includes('react-router')) { + return 'vendor-react' + } + if (id.includes('@tanstack/react-query')) { + return 'vendor-query' + } + if (id.includes('zustand')) { + return 'vendor-state' + } + } + return undefined + }, + }, + }, + }, + + preview: { + port: 4173, + }, + } +}) diff --git a/PROJECTS/advanced/rveng/src/rveng/api/app.py b/PROJECTS/advanced/rveng/src/rveng/api/app.py index 041f3911..d312006e 100644 --- a/PROJECTS/advanced/rveng/src/rveng/api/app.py +++ b/PROJECTS/advanced/rveng/src/rveng/api/app.py @@ -6,22 +6,30 @@ app.py from pathlib import Path from fastapi import FastAPI, HTTPException, Query +from fastapi.middleware.cors import CORSMiddleware from rveng.api import schemas from rveng.api.limits import ( DEFAULT_SESSION, + DEV_ORIGIN_REGEX, + MAX_BODY_BYTES, MAX_DISASM_BYTES, MAX_HEX_BYTES, + MAX_SESSION_LEN, ) +from rveng.api.middleware import BodySizeLimitMiddleware from rveng.api.store import ( ChallengeStore, InMemoryProgress, ProgressStore, load_store, ) -from rveng.engine import disasm, hex as hexmod, strings +from rveng.engine import cfg as cfgmod +from rveng.engine import disasm, hex as hexmod, strings, xref from rveng.engine.challenge import Challenge, grade +from rveng.engine.discover import discover_functions from rveng.engine.elf import ElfImage, NotAnElf +from rveng.engine.plt import plt_map CHALLENGES_ROOT = Path(__file__).resolve().parents[3] / "challenges" @@ -35,6 +43,13 @@ def create_app( store = store or load_store(CHALLENGES_ROOT) progress = progress or InMemoryProgress() app = FastAPI(title="rveng") + app.add_middleware(BodySizeLimitMiddleware, max_bytes=MAX_BODY_BYTES) + app.add_middleware( + CORSMiddleware, + allow_origin_regex=DEV_ORIGIN_REGEX, + allow_methods=["GET", "POST"], + allow_headers=["*"], + ) def require(cid: str) -> Challenge: challenge = store.get(cid) @@ -48,6 +63,27 @@ def create_app( except NotAnElf as exc: raise HTTPException(422, f"not an ELF: {exc}") + def instructions_for( + image: ElfImage, + symbol: str | None, + address: int | None) -> tuple[list[disasm.Instruction], str]: + if symbol: + sym = image.symbol(symbol) + if sym is None: + raise HTTPException(404, "symbol not found") + if sym.size > MAX_DISASM_BYTES: + raise HTTPException(413, "symbol too large to disassemble") + try: + return disasm.disassemble_symbol(image, sym), symbol + except ValueError as exc: + raise HTTPException(422, str(exc)) + if address is not None: + try: + return disasm.disassemble_at(image, address), f"sub_{address:x}" + except ValueError as exc: + raise HTTPException(422, str(exc)) + raise HTTPException(422, "provide a symbol or an address") + @app.get("/api/challenges") def list_challenges() -> list[schemas.ChallengeSummary]: return [ @@ -96,26 +132,30 @@ def create_app( schemas.FunctionView( name=f.name, value=f.value, size=f.size) for f in image.functions() if f.name + ], + discovered=[ + schemas.DiscoveredFunctionView( + address=d.address, label=d.label) + for d in discover_functions(image) ]) @app.get("/api/challenges/{cid}/disasm") def disasm_view( - cid: str, symbol: str = Query(...)) -> schemas.DisasmView: + cid: str, + symbol: str | None = Query(None), + address: int | None = Query(None, ge=0), + session: str = Query( + DEFAULT_SESSION, max_length=MAX_SESSION_LEN) + ) -> schemas.DisasmView: c = require(cid) image = elf_of(c) - sym = image.symbol(symbol) - if sym is None: - raise HTTPException(404, "symbol not found") - try: - instructions = disasm.disassemble_symbol(image, sym) - except ValueError as exc: - raise HTTPException(422, str(exc)) - if sum(i.size for i in instructions) > MAX_DISASM_BYTES: - raise HTTPException(413, "symbol too large to disassemble") - gate = disasm.find_gate(instructions) + instructions, label = instructions_for(image, symbol, address) + imports = plt_map(image) + solved = c.id in progress.solved(session) + gate = disasm.find_gate(instructions) if solved else None gate_addr = gate.address if gate else None return schemas.DisasmView( - symbol=symbol, + symbol=label, gate_address=gate_addr, instructions=[ schemas.InstructionView( @@ -123,10 +163,49 @@ def create_app( op_str=i.op_str, bytes=i.raw.hex(), immediate=i.immediate, branch_target=i.branch_target, + rip_target=i.rip_target, + call_name=(imports.get(i.branch_target) + if i.mnemonic == "call" else None), is_gate=(i.address == gate_addr)) for i in instructions ]) + @app.get("/api/challenges/{cid}/cfg") + def cfg_view( + cid: str, + symbol: str | None = Query(None), + address: int | None = Query(None, ge=0)) -> schemas.CfgView: + c = require(cid) + image = elf_of(c) + instructions, label = instructions_for(image, symbol, address) + graph = cfgmod.build_cfg(instructions) + return schemas.CfgView( + symbol=label, + blocks=[ + schemas.CfgBlockView( + start=b.start, end=b.end, + instructions=list(b.instructions)) + for b in graph.blocks + ], + edges=[ + schemas.CfgEdgeView(src=e.src, dst=e.dst, kind=e.kind) + for e in graph.edges + ]) + + @app.get("/api/challenges/{cid}/xrefs") + def xrefs_view( + cid: str, target: int = Query(..., ge=0)) -> schemas.XrefsView: + c = require(cid) + image = elf_of(c) + refs = xref.xrefs_to(disasm.disassemble_text(image), target) + return schemas.XrefsView( + target=target, + references=[ + schemas.XrefView( + from_addr=r.from_addr, to_addr=r.to_addr, kind=r.kind) + for r in refs + ]) + @app.get("/api/challenges/{cid}/strings") def strings_view( cid: str, @@ -154,7 +233,9 @@ def create_app( @app.get("/api/progress") def get_progress( - session: str = Query(DEFAULT_SESSION)) -> schemas.ProgressView: + session: str = Query( + DEFAULT_SESSION, + max_length=MAX_SESSION_LEN)) -> schemas.ProgressView: solved = progress.solved(session) return schemas.ProgressView( session=session, diff --git a/PROJECTS/advanced/rveng/src/rveng/api/limits.py b/PROJECTS/advanced/rveng/src/rveng/api/limits.py index 1bf9f70a..7f927a80 100644 --- a/PROJECTS/advanced/rveng/src/rveng/api/limits.py +++ b/PROJECTS/advanced/rveng/src/rveng/api/limits.py @@ -6,4 +6,8 @@ limits.py MAX_HEX_BYTES = 4096 MAX_DISASM_BYTES = 65536 MAX_ANSWER_LEN = 8192 +MAX_SESSION_LEN = 128 +MAX_BODY_BYTES = 65536 DEFAULT_SESSION = "local" + +DEV_ORIGIN_REGEX = r"^http://(localhost|127\.0\.0\.1)(:\d+)?$" diff --git a/PROJECTS/advanced/rveng/src/rveng/api/middleware.py b/PROJECTS/advanced/rveng/src/rveng/api/middleware.py new file mode 100644 index 00000000..6be5daba --- /dev/null +++ b/PROJECTS/advanced/rveng/src/rveng/api/middleware.py @@ -0,0 +1,63 @@ +""" +©AngelaMos | 2026 +middleware.py +""" + +import json + +CONTENT_LENGTH = b"content-length" +PAYLOAD_TOO_LARGE = 413 + + +class BodySizeLimitMiddleware: + """ + Reject request bodies larger than a byte cap before they are parsed + """ + + def __init__(self, app, max_bytes: int): + self.app = app + self.max_bytes = max_bytes + + async def __call__(self, scope, receive, send): + if scope["type"] != "http": + await self.app(scope, receive, send) + return + if self._declared_length(scope) > self.max_bytes: + await self._reject(send) + return + await self.app(scope, self._bounded(receive), send) + + def _declared_length(self, scope) -> int: + for key, value in scope.get("headers", []): + if key.lower() == CONTENT_LENGTH: + try: + return int(value) + except ValueError: + return 0 + return 0 + + def _bounded(self, receive): + seen = 0 + + async def bounded_receive(): + nonlocal seen + message = await receive() + if message["type"] == "http.request": + seen += len(message.get("body", b"")) + if seen > self.max_bytes: + return {"type": "http.disconnect"} + return message + + return bounded_receive + + async def _reject(self, send) -> None: + body = json.dumps({"detail": "request body too large"}).encode() + await send({ + "type": "http.response.start", + "status": PAYLOAD_TOO_LARGE, + "headers": [ + (b"content-type", b"application/json"), + (b"content-length", str(len(body)).encode()), + ], + }) + await send({"type": "http.response.body", "body": body}) diff --git a/PROJECTS/advanced/rveng/src/rveng/api/schemas.py b/PROJECTS/advanced/rveng/src/rveng/api/schemas.py index 8ed12bff..037e2564 100644 --- a/PROJECTS/advanced/rveng/src/rveng/api/schemas.py +++ b/PROJECTS/advanced/rveng/src/rveng/api/schemas.py @@ -3,9 +3,9 @@ schemas.py """ -from pydantic import BaseModel +from pydantic import BaseModel, Field -from rveng.api.limits import DEFAULT_SESSION, MAX_ANSWER_LEN +from rveng.api.limits import DEFAULT_SESSION, MAX_ANSWER_LEN, MAX_SESSION_LEN class ChallengeSummary(BaseModel): @@ -35,6 +35,11 @@ class FunctionView(BaseModel): size: int +class DiscoveredFunctionView(BaseModel): + address: int + label: str + + class SectionView(BaseModel): index: int name: str @@ -51,6 +56,7 @@ class ElfView(BaseModel): entry: int sections: list[SectionView] functions: list[FunctionView] + discovered: list[DiscoveredFunctionView] class InstructionView(BaseModel): @@ -60,6 +66,8 @@ class InstructionView(BaseModel): bytes: str immediate: int | None branch_target: int | None + rip_target: int | None + call_name: str | None is_gate: bool @@ -69,6 +77,35 @@ class DisasmView(BaseModel): gate_address: int | None +class CfgBlockView(BaseModel): + start: int + end: int + instructions: list[int] + + +class CfgEdgeView(BaseModel): + src: int + dst: int + kind: str + + +class CfgView(BaseModel): + symbol: str + blocks: list[CfgBlockView] + edges: list[CfgEdgeView] + + +class XrefView(BaseModel): + from_addr: int + to_addr: int + kind: str + + +class XrefsView(BaseModel): + target: int + references: list[XrefView] + + class StringView(BaseModel): offset: int text: str @@ -80,7 +117,7 @@ class StringsView(BaseModel): class SubmitRequest(BaseModel): answer: str - session: str = DEFAULT_SESSION + session: str = Field(DEFAULT_SESSION, max_length=MAX_SESSION_LEN) def within_limits(self) -> bool: return len(self.answer) <= MAX_ANSWER_LEN diff --git a/PROJECTS/advanced/rveng/src/rveng/api/store.py b/PROJECTS/advanced/rveng/src/rveng/api/store.py index fb30e5f3..8c316d61 100644 --- a/PROJECTS/advanced/rveng/src/rveng/api/store.py +++ b/PROJECTS/advanced/rveng/src/rveng/api/store.py @@ -4,6 +4,7 @@ store.py """ import json +import logging from pathlib import Path from typing import Protocol @@ -15,6 +16,8 @@ from rveng.engine.challenge import ( PatchedBytes, ) +log = logging.getLogger(__name__) + MANIFEST = "challenge.json" TARGET = "target" SOURCE = "source.c" @@ -32,15 +35,18 @@ class ChallengeError(ValueError): def _build_answer(spec: dict, binary: bytes): category = spec.get("category") - if category == CAT_FOUND_VALUE: - return FoundValue(spec["expected"]) - if category == CAT_IDENTIFIED_SYMBOL: - return IdentifiedSymbol(spec["name"]) - if category == CAT_PATCHED_BYTES: - offset = spec["offset"] - replacement = bytes.fromhex(spec["patch"]) - known_good = patch.apply(binary, offset, replacement) - return PatchedBytes(offset=offset, known_good=known_good) + try: + if category == CAT_FOUND_VALUE: + return FoundValue(spec["expected"]) + if category == CAT_IDENTIFIED_SYMBOL: + return IdentifiedSymbol(spec["name"]) + if category == CAT_PATCHED_BYTES: + offset = spec["offset"] + replacement = bytes.fromhex(spec["patch"]) + known_good = patch.apply(binary, offset, replacement) + return PatchedBytes(offset=offset, known_good=known_good) + except (KeyError, ValueError) as exc: + raise ChallengeError(f"bad {category} answer spec: {exc}") from exc raise ChallengeError(f"unknown answer category: {category}") @@ -51,18 +57,21 @@ def load_challenge(directory: Path) -> Challenge: manifest_path = directory / MANIFEST if not manifest_path.is_file(): raise ChallengeError(f"no manifest in {directory}") - manifest = json.loads(manifest_path.read_text()) - binary = (directory / TARGET).read_bytes() - source = (directory / SOURCE).read_text() - return Challenge( - id=manifest["id"], - module=manifest["module"], - title=manifest["title"], - mission=manifest["mission"], - binary=binary, - source=source, - answer=_build_answer(manifest["answer"], binary), - ) + try: + manifest = json.loads(manifest_path.read_text()) + binary = (directory / TARGET).read_bytes() + source = (directory / SOURCE).read_text() + return Challenge( + id=manifest["id"], + module=manifest["module"], + title=manifest["title"], + mission=manifest["mission"], + binary=binary, + source=source, + answer=_build_answer(manifest["answer"], binary), + ) + except (json.JSONDecodeError, KeyError, OSError) as exc: + raise ChallengeError(f"malformed challenge in {directory}: {exc}") from exc class ChallengeStore: @@ -86,8 +95,12 @@ def load_store(root: Path) -> ChallengeStore: """ challenges = [] for directory in sorted(root.iterdir()): - if (directory / MANIFEST).is_file(): + if not (directory / MANIFEST).is_file(): + continue + try: challenges.append(load_challenge(directory)) + except ChallengeError as exc: + log.warning("skipping challenge: %s", exc) return ChallengeStore(challenges) diff --git a/PROJECTS/advanced/rveng/src/rveng/engine/cfg.py b/PROJECTS/advanced/rveng/src/rveng/engine/cfg.py new file mode 100644 index 00000000..37dd9027 --- /dev/null +++ b/PROJECTS/advanced/rveng/src/rveng/engine/cfg.py @@ -0,0 +1,106 @@ +""" +©AngelaMos | 2026 +cfg.py +""" + +from dataclasses import dataclass + +from rveng.engine.disasm import Instruction + +RET = "ret" +JMP = "jmp" + +EDGE_TAKEN = "taken" +EDGE_FALLTHROUGH = "fallthrough" +EDGE_JUMP = "jump" + + +@dataclass(frozen=True) +class BasicBlock: + """ + A straight run of instructions with a single entry and single exit + """ + + start: int + end: int + instructions: tuple[int, ...] + + +@dataclass(frozen=True) +class Edge: + """ + A control-flow edge between two basic blocks, keyed by their starts + """ + + src: int + dst: int + kind: str + + +@dataclass(frozen=True) +class ControlFlowGraph: + """ + The basic blocks and edges of a single function + """ + + blocks: list[BasicBlock] + edges: list[Edge] + + +def _is_terminator(ins: Instruction) -> bool: + return (ins.is_conditional_branch + or ins.mnemonic == JMP + or ins.mnemonic == RET) + + +def _leaders(instructions: list[Instruction], within: set[int]) -> list[int]: + addrs = [i.address for i in instructions] + leaders = {addrs[0]} + for idx, ins in enumerate(instructions): + if not _is_terminator(ins): + continue + if idx + 1 < len(instructions): + leaders.add(addrs[idx + 1]) + if ins.branch_target is not None and ins.branch_target in within: + leaders.add(ins.branch_target) + return sorted(leaders) + + +def build_cfg(instructions: list[Instruction]) -> ControlFlowGraph: + """ + Split a function's instructions into basic blocks and connect them + """ + if not instructions: + return ControlFlowGraph([], []) + + addrs = [i.address for i in instructions] + by_addr = {i.address: i for i in instructions} + within = set(addrs) + starts = _leaders(instructions, within) + + blocks = [] + for pos, start in enumerate(starts): + stop = starts[pos + 1] if pos + 1 < len(starts) else None + body = tuple( + a for a in addrs if a >= start and (stop is None or a < stop)) + blocks.append(BasicBlock(start=start, end=body[-1], instructions=body)) + + block_starts = [b.start for b in blocks] + edges = [] + for pos, block in enumerate(blocks): + last = by_addr[block.end] + following = block_starts[pos + 1] if pos + 1 < len(blocks) else None + if last.mnemonic == RET: + continue + if last.is_conditional_branch: + if last.branch_target in within: + edges.append(Edge(block.start, last.branch_target, EDGE_TAKEN)) + if following is not None: + edges.append(Edge(block.start, following, EDGE_FALLTHROUGH)) + elif last.mnemonic == JMP: + if last.branch_target in within: + edges.append(Edge(block.start, last.branch_target, EDGE_JUMP)) + elif following is not None: + edges.append(Edge(block.start, following, EDGE_FALLTHROUGH)) + + return ControlFlowGraph(blocks, edges) diff --git a/PROJECTS/advanced/rveng/src/rveng/engine/disasm.py b/PROJECTS/advanced/rveng/src/rveng/engine/disasm.py index 850259c4..6c060dab 100644 --- a/PROJECTS/advanced/rveng/src/rveng/engine/disasm.py +++ b/PROJECTS/advanced/rveng/src/rveng/engine/disasm.py @@ -6,7 +6,7 @@ disasm.py from dataclasses import dataclass from capstone import CS_ARCH_X86, CS_MODE_64, CS_OPT_SYNTAX_INTEL, Cs -from capstone.x86 import X86_OP_IMM +from capstone.x86 import X86_OP_IMM, X86_OP_MEM, X86_REG_RIP from rveng.engine.elf import ElfImage, Symbol @@ -31,6 +31,7 @@ class Instruction: raw: bytes immediate: int | None branch_target: int | None = None + rip_target: int | None = None @property def size(self) -> int: @@ -68,6 +69,13 @@ def _immediate(ins) -> int | None: return value +def _rip_target(ins) -> int | None: + for operand in ins.operands: + if operand.type == X86_OP_MEM and operand.mem.base == X86_REG_RIP: + return ins.address + ins.size + operand.mem.disp + return None + + def disassemble(code: bytes, base: int = 0) -> list[Instruction]: """ Decode a byte range into annotated instructions at virtual base @@ -85,10 +93,54 @@ def disassemble(code: bytes, base: int = 0) -> list[Instruction]: raw=bytes(ins.bytes), immediate=None if flow else imm, branch_target=imm if flow else None, + rip_target=_rip_target(ins), )) return out +def disassemble_text(image: ElfImage) -> list[Instruction]: + """ + Disassemble the whole .text section for whole-binary analysis + """ + section = image.section(".text") + if section is None: + return [] + code = image.data[section.offset:section.offset + section.size] + return disassemble(code, section.addr) + + +RET = "ret" + + +def _section_at(image: ElfImage, vaddr: int): + for section in image.sections: + if section.addr == 0 or section.is_nobits: + continue + if section.addr <= vaddr < section.addr + section.size: + return section + return None + + +def disassemble_at( + image: ElfImage, + vaddr: int, + max_bytes: int = 4096) -> list[Instruction]: + """ + Disassemble from a raw virtual address until the first ret + """ + section = _section_at(image, vaddr) + if section is None: + raise ValueError("address is not in a mapped section") + start = vaddr - section.addr + section.offset + end = min(start + max_bytes, section.offset + section.size) + out = [] + for ins in disassemble(image.data[start:end], vaddr): + out.append(ins) + if ins.mnemonic == RET: + break + return out + + def disassemble_symbol( image: ElfImage, symbol: Symbol) -> list[Instruction]: """ diff --git a/PROJECTS/advanced/rveng/src/rveng/engine/discover.py b/PROJECTS/advanced/rveng/src/rveng/engine/discover.py new file mode 100644 index 00000000..e6cde1df --- /dev/null +++ b/PROJECTS/advanced/rveng/src/rveng/engine/discover.py @@ -0,0 +1,41 @@ +""" +©AngelaMos | 2026 +discover.py +""" + +from dataclasses import dataclass + +from rveng.engine.elf import SHF_EXECINSTR, ElfImage + +PROLOGUE = b"\x55\x48\x89\xe5" + + +@dataclass(frozen=True) +class DiscoveredFunction: + """ + A candidate function entry found without a symbol table + """ + + address: int + section: str + + @property + def label(self) -> str: + return f"sub_{self.address:x}" + + +def discover_functions(image: ElfImage) -> list[DiscoveredFunction]: + """ + Find function entries by scanning executable sections for the prologue + """ + found = [] + for section in image.sections: + if section.is_nobits or not section.flags & SHF_EXECINSTR: + continue + blob = image.data[section.offset:section.offset + section.size] + pos = blob.find(PROLOGUE) + while pos != -1: + found.append(DiscoveredFunction( + address=section.addr + pos, section=section.name)) + pos = blob.find(PROLOGUE, pos + 1) + return sorted(found, key=lambda f: f.address) diff --git a/PROJECTS/advanced/rveng/src/rveng/engine/plt.py b/PROJECTS/advanced/rveng/src/rveng/engine/plt.py new file mode 100644 index 00000000..d8e0e5d3 --- /dev/null +++ b/PROJECTS/advanced/rveng/src/rveng/engine/plt.py @@ -0,0 +1,101 @@ +""" +©AngelaMos | 2026 +plt.py +""" + +import struct + +from rveng.engine.elf import ElfImage + +DYNSYM = ".dynsym" +DYNSTR = ".dynstr" +RELA_PLT = ".rela.plt" +PLT = ".plt" + +SYM_ENTRY = 24 +SYM_NAME = 0x00 + +RELA_ENTRY = 24 +RELA_OFFSET = 0x00 +RELA_INFO = 0x08 + +PLT_ENTRY = 16 +JMP_RIP = b"\xff\x25" +JMP_RIP_LEN = 6 + + +def _cstring(blob: bytes, offset: int) -> str: + end = blob.find(b"\x00", offset) + if end == -1: + end = len(blob) + return blob[offset:end].decode("utf-8", "replace") + + +def _dynamic_names(image: ElfImage) -> list[str]: + dynsym = image.section(DYNSYM) + dynstr = image.section(DYNSTR) + if dynsym is None or dynstr is None or dynsym.entsize == 0: + return [] + names = image.data[dynstr.offset:dynstr.offset + dynstr.size] + body = image.data[dynsym.offset:dynsym.offset + dynsym.size] + en = image.header.endian + out = [] + for base in range(0, len(body) - SYM_ENTRY + 1, SYM_ENTRY): + name_off = struct.unpack_from(en + "I", body, base + SYM_NAME)[0] + out.append(_cstring(names, name_off)) + return out + + +def _got_to_name(image: ElfImage, names: list[str]) -> dict[int, str]: + rela = image.section(RELA_PLT) + if rela is None or rela.entsize == 0: + return {} + body = image.data[rela.offset:rela.offset + rela.size] + en = image.header.endian + mapping = {} + for base in range(0, len(body) - RELA_ENTRY + 1, RELA_ENTRY): + r_offset = struct.unpack_from(en + "Q", body, base + RELA_OFFSET)[0] + r_info = struct.unpack_from(en + "Q", body, base + RELA_INFO)[0] + sym_index = r_info >> 32 + if 0 <= sym_index < len(names): + mapping[r_offset] = names[sym_index] + return mapping + + +def _stub_got_slot(entry: bytes, entry_addr: int, endian: str) -> int | None: + jmp = entry.find(JMP_RIP) + if jmp == -1 or jmp + JMP_RIP_LEN > len(entry): + return None + disp = struct.unpack_from(endian + "i", entry, jmp + 2)[0] + return entry_addr + jmp + JMP_RIP_LEN + disp + + +def plt_map(image: ElfImage) -> dict[int, str]: + """ + Map each PLT stub address to the imported symbol it jumps to + """ + plt = image.section(PLT) + if plt is None: + return {} + got_names = _got_to_name(image, _dynamic_names(image)) + if not got_names: + return {} + body = image.data[plt.offset:plt.offset + plt.size] + en = image.header.endian + result = {} + for base in range(0, len(body) - PLT_ENTRY + 1, PLT_ENTRY): + entry = body[base:base + PLT_ENTRY] + got_slot = _stub_got_slot(entry, plt.addr + base, en) + if got_slot is None: + continue + name = got_names.get(got_slot) + if name is not None: + result[plt.addr + base] = name + return result + + +def resolve_call(image: ElfImage, target: int) -> str | None: + """ + Return the import a call target resolves to, or None when it is local + """ + return plt_map(image).get(target) diff --git a/PROJECTS/advanced/rveng/src/rveng/engine/xref.py b/PROJECTS/advanced/rveng/src/rveng/engine/xref.py new file mode 100644 index 00000000..bb74443d --- /dev/null +++ b/PROJECTS/advanced/rveng/src/rveng/engine/xref.py @@ -0,0 +1,55 @@ +""" +©AngelaMos | 2026 +xref.py +""" + +from dataclasses import dataclass + +from rveng.engine.disasm import Instruction + +CALL = "call" +KIND_CALL = "call" +KIND_BRANCH = "branch" +KIND_DATA = "data" + + +@dataclass(frozen=True) +class Reference: + """ + One cross reference from an instruction to a target address + """ + + from_addr: int + to_addr: int + kind: str + + +def references(instructions: list[Instruction]) -> list[Reference]: + """ + Every control-flow and data reference the instructions emit + """ + out = [] + for ins in instructions: + if ins.branch_target is not None: + kind = KIND_CALL if ins.mnemonic == CALL else KIND_BRANCH + out.append(Reference(ins.address, ins.branch_target, kind)) + if ins.rip_target is not None: + out.append(Reference(ins.address, ins.rip_target, KIND_DATA)) + return out + + +def build_xrefs(instructions: list[Instruction]) -> dict[int, list[Reference]]: + """ + Group every reference by the address it points at + """ + table: dict[int, list[Reference]] = {} + for ref in references(instructions): + table.setdefault(ref.to_addr, []).append(ref) + return table + + +def xrefs_to(instructions: list[Instruction], target: int) -> list[Reference]: + """ + Every reference that points at target + """ + return [ref for ref in references(instructions) if ref.to_addr == target] diff --git a/PROJECTS/advanced/rveng/tests/conftest.py b/PROJECTS/advanced/rveng/tests/conftest.py index ea9b2193..5981788e 100644 --- a/PROJECTS/advanced/rveng/tests/conftest.py +++ b/PROJECTS/advanced/rveng/tests/conftest.py @@ -18,3 +18,13 @@ def gate_path() -> Path: @pytest.fixture(scope="session") def gate_bytes(gate_path: Path) -> bytes: return gate_path.read_bytes() + + +@pytest.fixture(scope="session") +def gate_stripped_path() -> Path: + return FIXTURES / "gate_stripped" + + +@pytest.fixture(scope="session") +def gate_stripped_bytes(gate_stripped_path: Path) -> bytes: + return gate_stripped_path.read_bytes() diff --git a/PROJECTS/advanced/rveng/tests/fixtures/gate_stripped b/PROJECTS/advanced/rveng/tests/fixtures/gate_stripped new file mode 100755 index 0000000000000000000000000000000000000000..602810968153700212a4848bef59d31c006723ce GIT binary patch literal 14416 zcmeHOU2IfE6rSBuy8N_VM6gnRE(Q`bblXB{pai&3OD{^a@+UsvcH7-{H@3UY?pDS|a*jNSu@0 z`^`CXzBzNxy)*kWb6=_3zSZmT2o|r{C{Szko6@AtLNruLm27K8kub$VQ7Wc`Wf~l& ztU**IMt`2FVDyn6nvr*c>QCgEvW9V?)C9y(!aYPp-W@Tis4$wZR7T-fI`U){>4L70 z@dgE8bn;|8L$xCeQxyoe6OYl!!`w`iDffvDipN;tRra-n%tIAL!P01LgsZ4uY-7u%gB!r{ZW{=a;ef+E;VI=(NN}GJm&4Z zYKO7>Py5+Gyb5Z#g7VK;Me!Ip-gCgieBQ6{5`NC(v@sz}mU;ePXE!I?BRo)gZYm3Fxon@VC ztIMjZ0;xn`m23yw;`X3;Uos249-6`v+nLI~=pObuup4l=1#>)AJE9|Pr$oq3#6&O% z*VPRKV^+{(yXFz8_XFz8_XFz8_XFzA*LCApBbKPeh%m1=m2`5daU%fEng@e&hCs7JAWY&Z+D{5Yt#FUzNxDK(D;9Epz2X zEAy?@Gcw+=tFE%I@{%<$5S{}~&3hhPe{KmBFL@2ylkN9A)hC5Gy8^yYTO06f&pnG? z*v%QAp&>im(%awSB$(n;>3`&=gv_{g?&c=z+(e$`xoBOvZO_TIP(UrX`9BpezIAv* zA#_Zn=kB&LS4R)RRb0&f9ez-F_VxcI9{OK9ijJA?$s^s9>)=a&y5KD4<66Fqwyz&+ zAknN9GY((s~fg@`rvjz-N5Kax3k$y-Q=;l zNl@N}PQZ090-td!a}k=@E1MXQAx8Iu@3msEHYn@pZZOEIOK>aYJ-+L%Vo)tQ13Cjb z13Cjb13Cjb13Cjb13Cjb13Cl$rwriwnH`M;TiQac!B{F7jU*!?9dAoCzZ41A7|T;L z#erlZ-fG0t?M;!S@Ra7Q$2C@5+Zz8qo9zXx7|&)K02={c2Auzc`tex>I0QNhu6d$( z4(<@1PQRygc7dz*2au_K)p{QKeht37c86#hYn+Yh2{9B7k zo+_Soz;{?|TCo0!)yrhF$lD9Y9_Zs@^xItIf5Tf_IQ<3a2&h=!3CAv|KLnVo_jV}< zFyXo&_7`L0+ARvM{dr|cpL7Ov26P5=26P5=26P5=26P5=26P4<%nX>Y0E^-$bpD<~ z`)oX+&_3xxrHK*pf0T6I&s<76?<-zFy4YyS68^qI;rd^0XA`KO$}aN+)z3r)WS0Vg`&m&+6o`3*47?|El=gM_%|l*>qL1(fiUt-Sz`M*^@sb7 zzco={lc^Mr59_V+diuy8)@!BCd4->qQa?@gME}o#_m8^+R@436O1Ov6?N?u0TVt$v zGSU84al1ez1=?UA_c&e^}w@ws;HHEy8ipZCifu15K}WHbJ^;pFo^e%{}Y|Ccxg z|I=^^zUG0?ERPp{AA{f1;Bm|xe!f{>2ZdkZS!X_v?^84&na_Kp-TkixKIVY=JpKoX z-%Ej}17d9E^EiAOZ0O2-o=+O-LV5lG7b))k?*uE3UFP$6JxzR`7rFoJ$JhvboC{dz z`9Qt|0ChZnxcd)PQgD*MP|WA~XPn|QpX0l)zl-==WNRWfpBY53$c`$`d2W6W*f1y9 zKPfyP_?61@B-P5kav2f-J9F|4;v2*_(a9lX&h6jNK%JA{Nc=`}eARWsn9ohV<>K?_ z)yN#>!SgozQ(9RSZxf&Mf9HK4In*r|^3#Re!_(z{QUmres773F3l+ z+?nqm8_qz%IsW;59DYy7U`IlC|NFrXdx+#1LKpsg=l5XT@mOcP44h&IpFjWQ3mIT% zKPXCJo$)KM; TestClient: return TestClient(create_app()) -def test_list_has_three_seed_challenges(client: TestClient): +def test_list_has_the_seed_challenges(client: TestClient): body = client.get("/api/challenges").json() ids = {c["id"] for c in body} assert ids == { - "03-flip-the-gate", "04-name-the-function", "05-find-the-gate"} + "03-flip-the-gate", "04-name-the-function", "05-find-the-gate", + "06-stripped-gate"} def test_detail_does_not_leak_source_or_answer(client: TestClient): @@ -58,9 +65,25 @@ def test_elf_view_reports_entry_and_functions(client: TestClient): assert "check" in names and "main" in names -def test_disasm_marks_the_gate(client: TestClient): +def test_disasm_hides_gate_before_solve(client: TestClient): body = client.get( - "/api/challenges/05-find-the-gate/disasm?symbol=check").json() + "/api/challenges/05-find-the-gate/disasm" + "?symbol=check&session=fresh").json() + assert body["gate_address"] is None + assert all(i["is_gate"] is False for i in body["instructions"]) + cmp_ins = next( + i for i in body["instructions"] if i["mnemonic"] == "cmp") + assert "0x539" in cmp_ins["op_str"] + + +def test_disasm_reveals_gate_after_solve(client: TestClient): + session = "solver" + client.post( + "/api/challenges/05-find-the-gate/submit", + json={"answer": "1337", "session": session}) + body = client.get( + "/api/challenges/05-find-the-gate/disasm" + f"?symbol=check&session={session}").json() assert body["gate_address"] == 0x40114D gate = next(i for i in body["instructions"] if i["is_gate"]) assert gate["mnemonic"] == "cmp" @@ -129,4 +152,95 @@ def test_progress_tracks_solves(client: TestClient): json={"answer": "1337", "session": "s1"}) body = client.get("/api/progress?session=s1").json() assert body["solved"] == ["05-find-the-gate"] - assert body["total"] == 3 + assert body["total"] == 4 + + +def test_disasm_resolves_import_call_names(client: TestClient): + body = client.get( + "/api/challenges/05-find-the-gate/disasm?symbol=main").json() + names = {i["call_name"] for i in body["instructions"] if i["call_name"]} + assert "atoi" in names and "puts" in names + + +def test_disasm_exposes_rip_target(client: TestClient): + body = client.get( + "/api/challenges/05-find-the-gate/disasm?symbol=main").json() + targets = {i["rip_target"] for i in body["instructions"]} + assert 0x402004 in targets + + +def test_elf_lists_discovered_functions(client: TestClient): + body = client.get("/api/challenges/05-find-the-gate/elf").json() + labels = {d["label"] for d in body["discovered"]} + assert "sub_401146" in labels + + +def test_cfg_of_check_is_a_diamond(client: TestClient): + body = client.get( + "/api/challenges/05-find-the-gate/cfg?symbol=check").json() + assert len(body["blocks"]) == 4 + assert len(body["edges"]) == 4 + + +def test_xrefs_find_caller_of_check(client: TestClient): + body = client.get( + "/api/challenges/05-find-the-gate/xrefs?target=4198726").json() + assert any( + r["from_addr"] == 0x4011A6 and r["kind"] == "call" + for r in body["references"]) + + +def test_stripped_challenge_disassembles_by_address(client: TestClient): + elf = client.get("/api/challenges/06-stripped-gate/elf").json() + assert elf["functions"] == [] + assert any(d["address"] == 0x401146 for d in elf["discovered"]) + dis = client.get( + "/api/challenges/06-stripped-gate/disasm?address=4198726").json() + assert dis["instructions"][-1]["mnemonic"] == "ret" + + +def test_stripped_challenge_grades_found_value(client: TestClient): + r = client.post( + "/api/challenges/06-stripped-gate/submit", + json={"answer": "0x539"}).json() + assert r["correct"] is True + + +def test_load_store_skips_malformed_dir(tmp_path: Path): + root = tmp_path / "challenges" + root.mkdir() + shutil.copytree( + CHALLENGES / "04-name-the-function", + root / "04-name-the-function") + broken = root / "99-broken" + broken.mkdir() + (broken / "challenge.json").write_text("{ not valid json") + store = load_store(root) + assert {c.id for c in store.list()} == {"04-name-the-function"} + + +def test_answer_over_answer_len_is_413(client: TestClient): + r = client.post( + "/api/challenges/05-find-the-gate/submit", + json={"answer": "9" * 10000}) + assert r.status_code == 413 + + +def test_body_over_size_cap_is_413(client: TestClient): + r = client.post( + "/api/challenges/05-find-the-gate/submit", + json={"answer": "9" * 100000}) + assert r.status_code == 413 + + +def test_session_over_max_len_is_422(client: TestClient): + long_session = "s" * (MAX_SESSION_LEN + 1) + r = client.get(f"/api/progress?session={long_session}") + assert r.status_code == 422 + + +def test_cors_allows_localhost_dev_origin(client: TestClient): + r = client.get( + "/api/challenges", headers={"origin": "http://localhost:5173"}) + assert r.headers.get("access-control-allow-origin") == ( + "http://localhost:5173") diff --git a/PROJECTS/advanced/rveng/tests/test_cfg.py b/PROJECTS/advanced/rveng/tests/test_cfg.py new file mode 100644 index 00000000..9dbcb3f0 --- /dev/null +++ b/PROJECTS/advanced/rveng/tests/test_cfg.py @@ -0,0 +1,45 @@ +""" +©AngelaMos | 2026 +test_cfg.py +""" + +from rveng.engine.cfg import EDGE_FALLTHROUGH, EDGE_JUMP, EDGE_TAKEN, build_cfg +from rveng.engine.disasm import disassemble_symbol +from rveng.engine.elf import ElfImage + +B0 = 0x401146 +B1 = 0x401156 +B2 = 0x40115D +B3 = 0x401162 + + +def _check_cfg(gate_bytes: bytes): + image = ElfImage(gate_bytes) + symbol = image.symbol("check") + return build_cfg(disassemble_symbol(image, symbol)) + + +def test_check_has_four_basic_blocks(gate_bytes: bytes): + cfg = _check_cfg(gate_bytes) + assert [b.start for b in cfg.blocks] == [B0, B1, B2, B3] + + +def test_check_block_zero_ends_at_the_gate_jne(gate_bytes: bytes): + cfg = _check_cfg(gate_bytes) + assert cfg.blocks[0].end == 0x401154 + + +def test_check_cfg_is_a_diamond(gate_bytes: bytes): + cfg = _check_cfg(gate_bytes) + edges = {(e.src, e.dst, e.kind) for e in cfg.edges} + assert edges == { + (B0, B2, EDGE_TAKEN), + (B0, B1, EDGE_FALLTHROUGH), + (B1, B3, EDGE_JUMP), + (B2, B3, EDGE_FALLTHROUGH), + } + + +def test_terminal_block_has_no_out_edges(gate_bytes: bytes): + cfg = _check_cfg(gate_bytes) + assert all(e.src != B3 for e in cfg.edges) diff --git a/PROJECTS/advanced/rveng/tests/test_discover.py b/PROJECTS/advanced/rveng/tests/test_discover.py new file mode 100644 index 00000000..3eb5e24d --- /dev/null +++ b/PROJECTS/advanced/rveng/tests/test_discover.py @@ -0,0 +1,42 @@ +""" +©AngelaMos | 2026 +test_discover.py +""" + +from rveng.engine.disasm import disassemble_at, find_gate +from rveng.engine.discover import discover_functions +from rveng.engine.elf import ElfImage + +CHECK = 0x401146 +MAIN = 0x401164 +START = 0x401060 + + +def test_prologue_scan_finds_check_and_main(gate_stripped_bytes: bytes): + image = ElfImage(gate_stripped_bytes) + addrs = {f.address for f in discover_functions(image)} + assert CHECK in addrs + assert MAIN in addrs + + +def test_prologue_scan_skips_the_runtime_start(gate_stripped_bytes: bytes): + image = ElfImage(gate_stripped_bytes) + addrs = {f.address for f in discover_functions(image)} + assert START not in addrs + + +def test_discovered_function_has_an_address_label(gate_stripped_bytes: bytes): + image = ElfImage(gate_stripped_bytes) + functions = discover_functions(image) + check = next(f for f in functions if f.address == CHECK) + assert check.label == "sub_401146" + + +def test_disassemble_at_recovers_the_gate_without_symbols( + gate_stripped_bytes: bytes): + image = ElfImage(gate_stripped_bytes) + instructions = disassemble_at(image, CHECK) + assert instructions[-1].mnemonic == "ret" + gate = find_gate(instructions) + assert gate is not None + assert gate.immediate == 1337 diff --git a/PROJECTS/advanced/rveng/tests/test_plt.py b/PROJECTS/advanced/rveng/tests/test_plt.py new file mode 100644 index 00000000..7f897cf0 --- /dev/null +++ b/PROJECTS/advanced/rveng/tests/test_plt.py @@ -0,0 +1,51 @@ +""" +©AngelaMos | 2026 +test_plt.py +""" + +from rveng.engine.elf import ElfImage +from rveng.engine.plt import _stub_got_slot, plt_map, resolve_call + +PUTS = 0x401030 +PRINTF = 0x401040 +ATOI = 0x401050 + + +def test_plt_map_names_the_three_imports(gate_bytes: bytes): + image = ElfImage(gate_bytes) + assert plt_map(image) == {PUTS: "puts", PRINTF: "printf", ATOI: "atoi"} + + +def test_plt0_stub_is_not_treated_as_an_import(gate_bytes: bytes): + image = ElfImage(gate_bytes) + assert 0x401020 not in plt_map(image) + + +def test_resolve_call_maps_target_to_name(gate_bytes: bytes): + image = ElfImage(gate_bytes) + assert resolve_call(image, ATOI) == "atoi" + assert resolve_call(image, PUTS) == "puts" + + +def test_resolve_call_returns_none_for_local_target(gate_bytes: bytes): + image = ElfImage(gate_bytes) + assert resolve_call(image, 0x401146) is None + + +def test_plt_resolution_survives_stripping(gate_stripped_bytes: bytes): + image = ElfImage(gate_stripped_bytes) + assert plt_map(image) == {PUTS: "puts", PRINTF: "printf", ATOI: "atoi"} + + +def test_stub_decode_of_classic_entry(): + entry = bytes.fromhex("ff25ca2f0000") + b"\x00" * 10 + assert _stub_got_slot(entry, 0x401030, "<") == 0x404000 + + +def test_stub_decode_tolerates_truncated_jmp_pattern(): + entry = b"\x90" * 11 + b"\xff\x25\x00\x00\x00" + assert _stub_got_slot(entry, 0x401030, "<") is None + + +def test_stub_decode_returns_none_without_jmp(): + assert _stub_got_slot(b"\x90" * 16, 0x401030, "<") is None diff --git a/PROJECTS/advanced/rveng/tests/test_xref.py b/PROJECTS/advanced/rveng/tests/test_xref.py new file mode 100644 index 00000000..f575b068 --- /dev/null +++ b/PROJECTS/advanced/rveng/tests/test_xref.py @@ -0,0 +1,40 @@ +""" +©AngelaMos | 2026 +test_xref.py +""" + +from rveng.engine.disasm import disassemble_text +from rveng.engine.elf import ElfImage +from rveng.engine.xref import build_xrefs, xrefs_to + +CHECK = 0x401146 +FLAG = 0x402004 +MAIN_CALLS_CHECK = 0x4011A6 +MAIN_LEA_FLAG = 0x401173 + + +def test_rip_relative_resolves_to_flag_string(gate_bytes: bytes): + image = ElfImage(gate_bytes) + by_addr = {i.address: i for i in disassemble_text(image)} + assert by_addr[MAIN_LEA_FLAG].rip_target == FLAG + + +def test_xref_finds_caller_of_check(gate_bytes: bytes): + image = ElfImage(gate_bytes) + refs = xrefs_to(disassemble_text(image), CHECK) + assert any( + r.from_addr == MAIN_CALLS_CHECK and r.kind == "call" for r in refs) + + +def test_xref_finds_data_reference_to_flag(gate_bytes: bytes): + image = ElfImage(gate_bytes) + refs = xrefs_to(disassemble_text(image), FLAG) + assert any( + r.from_addr == MAIN_LEA_FLAG and r.kind == "data" for r in refs) + + +def test_build_xrefs_groups_by_target(gate_bytes: bytes): + image = ElfImage(gate_bytes) + table = build_xrefs(disassemble_text(image)) + assert CHECK in table + assert FLAG in table