Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,9 @@ if(WIN32)
string(REPLACE "." "," PPC_VERSION_COMMAS "${APP_VERSION}")
configure_file(assets/app.rc.in "${CMAKE_BINARY_DIR}/app.rc" @ONLY)
list(APPEND APP_SOURCES "${CMAKE_BINARY_DIR}/app.rc")
# A .manifest source is handed to the manifest tool and merged with the one the linker generates
# anyway, which is why this does not go through the .rc: two RT_MANIFEST resources is an error.
list(APPEND APP_SOURCES assets/app.manifest)
else()
list(APPEND APP_SOURCES
src/platform/hotkeys_x11.cpp src/platform/foreground_x11.cpp src/platform/input_sim_x11.cpp
Expand All @@ -211,6 +214,17 @@ target_link_libraries(PathOfPriceCheck PRIVATE OpenGL::GL)
find_package(Threads REQUIRED)
target_link_libraries(PathOfPriceCheck PRIVATE Threads::Threads)

if(MSVC)
# Control Flow Guard. Off by default in MSVC and on in most of what ships for Windows, so its
# absence is one of the cheap signals a heuristic scanner counts against an unsigned binary —
# and it is worth having on its own terms. `/CETCOMPAT` is deliberately not here: it is a
# promise about the whole image, including the statically linked SDL, curl and zlib, and a
# wrong one crashes on hardware-enforced stack protection rather than degrading.
target_compile_options(ppc_core PRIVATE /guard:cf)
target_compile_options(PathOfPriceCheck PRIVATE /guard:cf)
target_link_options(PathOfPriceCheck PRIVATE /guard:cf)
endif()

if(WIN32)
target_link_libraries(PathOfPriceCheck PRIVATE user32)
else()
Expand Down
30 changes: 30 additions & 0 deletions assets/app.manifest
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<!--
The application manifest, merged into the executable by the linker (CMakeLists.txt lists this
file as a source; CMake hands a .manifest source to the manifest tool).

It declares nothing the program was not already doing. `asInvoker` is the level it has always
run at and matches the installer's PrivilegesRequired=lowest; the OS list is the standard block.
DPI awareness is deliberately absent — SDL sets it at video init, and declaring it here would
take that decision away from SDL and move the overlay on a high-DPI display.

It is here because an executable with no compatibility block at all is one more small mark
against an unsigned download in the heuristics that decide whether it is malware.
-->
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
<security>
<requestedPrivileges>
<requestedExecutionLevel level="asInvoker" uiAccess="false"/>
</requestedPrivileges>
</security>
</trustInfo>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/><!-- Windows 10 and 11 -->
<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/><!-- Windows 8.1 -->
<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/><!-- Windows 8 -->
<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/><!-- Windows 7 -->
</application>
</compatibility>
</assembly>
19 changes: 11 additions & 8 deletions assets/fonts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,16 @@ unrelated problems with unrelated licenses and are kept in separate blobs and se
The overlay renders in **Fontin** — the typeface Path of Exile itself uses — by Jos Buivenga
(exljbris). Homepage: <https://www.exljbris.com/fontin.html>

Four faces are **embedded in the executable** as base85-encoded, stb_compress'd blobs in
Four faces are **embedded in the executable** as their own TTF bytes in
[`src/fontin_data.inc`](../../src/fontin_data.inc): Regular, Bold, Italic, SmallCaps. There is no
runtime asset dependency — the `.ttf` files here are only inputs for regenerating that blob, and
runtime asset dependency — the `.ttf` files here are only inputs for regenerating that array, and
are not committed.

They used to be base85-encoded and `stb_compress`ed, which was smaller in both the source file and
the executable. Uncompressed is deliberate: an opaque high-entropy blob beside a routine that
decodes it into a fresh buffer is a packer's shape, and on an unsigned binary it gets scored as
one. The argument is in [docs/architecture.md](../../docs/architecture.md).

`Fontin-SmallCaps.ttf` is a separate family, not an OpenType `smcp` feature — which is what makes
small caps usable at all here, since ImGui does no text shaping or feature substitution.

Expand Down Expand Up @@ -44,8 +49,8 @@ change. Users can already override at runtime via `$PPC_FONT_DIR`.
./scripts/gen-font-data.sh # rewrites src/fontin_data.inc
```

`gen-font-data.sh` calls `fetch-fonts.sh` itself if the TTFs are missing, and needs a configured
build tree for ImGui's `binary_to_compressed_c.cpp` (or `IMGUI_SOURCE_DIR` pointing at one).
`gen-font-data.sh` calls `fetch-fonts.sh` itself if the TTFs are missing. It needs nothing else —
the bytes go in as they are, via `scripts/bin2c.py`.

### Runtime override

Expand All @@ -62,8 +67,7 @@ Homepage: <https://fontawesome.com>

**Only the codepoints actually used are bundled.** `scripts/fetch-glyphs.sh` runs `pyftsubset` over
the release's `fa-solid-900.ttf` and keeps the two named in it, which is 1.4KB against the 416KB
face; the result is base85'd into [`src/glyph_data.inc`](../../src/glyph_data.inc) exactly as
Fontin is. **That codepoint list is the contract with [`src/ui/glyphs.hpp`](../../src/ui/glyphs.hpp)
face; the result goes into [`src/glyph_data.inc`](../../src/glyph_data.inc) exactly as Fontin does. **That codepoint list is the contract with [`src/ui/glyphs.hpp`](../../src/ui/glyphs.hpp)
— a glyph named there and missing here draws nothing at all**, which is the same silent blank
Fontin's `≤` and `≥` produce; `Fonts::has_glyphs` is the check that catches it. Adding a glyph means
editing both files and rerunning both scripts below.
Expand All @@ -86,5 +90,4 @@ call.
```

`fetch-glyphs.sh` needs `fonttools` (`pip install fonttools`) for `pyftsubset`;
`gen-glyph-data.sh` calls it itself if the subset is missing, and needs the same configured build
tree `gen-font-data.sh` does.
`gen-glyph-data.sh` calls it itself if the subset is missing, and needs nothing further.
59 changes: 47 additions & 12 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -325,20 +325,55 @@ a console-subsystem build pops a console window beside an application whose whol
and a tray icon. Nothing user-facing goes to stdout — `PPC_DEBUG_COPY`'s traces have nowhere to go
there, which is what the debug log is for.

**Icon** (`src/icon.cpp`): `assets/popc_icon.png` embedded as a base85 blob in the generated
`src/icon_data.inc` and decoded at startup with SDL3's own `SDL_LoadPNG_IO` — no image library, no
runtime asset. One surface feeds both the tray and `SDL_SetWindowIcon`. The Windows executable icon
is separate: `assets/popc_icon.ico` via an `.rc` resource configured from `assets/app.rc.in`. That
script also carries the executable's **`VERSIONINFO`** — publisher, product, description and both
version strings, spelled exactly as [packaging/PathOfPriceCheck.iss](../packaging/PathOfPriceCheck.iss)
spells them. It is there for the antivirus heuristics rather than for the properties dialog: an
unsigned binary that declares no publisher and no version is a signal in itself, and until the
release is code-signed it is the cheap half of the answer. `FILEVERSION` wants four numbers where
`APP_VERSION` has three, so the fourth is a constant 0.
**Icon** (`src/icon.cpp`): `assets/popc_icon.png` embedded as its own bytes in the generated
`src/icon_data.inc` and read at startup with SDL3's own `SDL_LoadPNG_IO` — no image library, no
runtime asset. One surface feeds both the tray and `SDL_SetWindowIcon`, which is why it has to be
pixels at runtime and cannot come from the Windows resource: `SDL_CreateTray` takes a surface.
The embedded copy is the 128px master **downscaled to 64**, since the tray draws it at 16 or 32
and a window icon at rather less than 128; the master stays the source of the `.ico`, where the
big entries are the ones Explorer uses.

The Windows executable icon is separate: `assets/popc_icon.ico` via an `.rc` resource configured
from `assets/app.rc.in`. That script also carries the executable's **`VERSIONINFO`** — publisher,
product, description and both version strings, spelled exactly as
[packaging/PathOfPriceCheck.iss](../packaging/PathOfPriceCheck.iss) spells them. `FILEVERSION` wants
four numbers where `APP_VERSION` has three, so the fourth is a constant 0.

**Looking like software rather than like malware** is what that resource is for, and it is not the
only thing here doing that job. Microsoft's cloud classifier flagged an unsigned release as
`Trojan:Win32/Wacatac.B!ml` — one verdict out of seventy-one engines, the shape of a false positive
— and until the release is code-signed the answer is a collection of cheap signals, each of which
is also defensible on its own:

- the executable declares a publisher and a version (`VERSIONINFO`, above), because one that
declares neither is a signal in itself;
- it carries an **application manifest** (`assets/app.manifest`, listed as a source so CMake hands
it to the manifest tool) declaring `asInvoker` and the standard `supportedOS` block. It declares
no DPI awareness on purpose — SDL sets that at video init, and taking the decision away from SDL
would move the overlay on a high-DPI display;
- it is linked with **`/guard:cf`**, which MSVC leaves off by default and most shipped Windows
software has on. `/CETCOMPAT` is deliberately absent: it is a promise about the whole image,
statically linked SDL, curl and zlib included, and a wrong one crashes rather than degrading;
- **nothing in it is packed or encoded.** The fonts and the icon go in as their own bytes, so a
scanner reading `.rdata` finds a TTF table directory and a PNG signature. They used to be
base85-encoded and `stb_compress`ed, which cost a few kilobytes less and looked exactly like a
packer: an opaque high-entropy blob beside a routine that decodes it into a fresh buffer.

The last of those is a rule and not just a past decision — **do not obfuscate anything to get past
a scanner.** String encryption, packing or anti-debug tricks raise the score sharply and are read
as deliberate evasion, which is a worse verdict than the one being fixed. The genuinely
dropper-shaped behaviour in this program is the updater downloading an executable and running it
([updater.md](updater.md)), and hiding that would be dishonest as well as counterproductive. When a
release does trip the classifier, the remedy is a false-positive report to Microsoft's WDSI
submission portal as a software developer; it clears within a day or two, but it keys on the file
hash, so it recurs per release until the binaries are signed.

**Fonts** (`src/fonts.cpp`): the UI renders in **Fontin**, the typeface the game itself uses. Four
faces (Regular/Bold/Italic/SmallCaps) are embedded in the executable as base85 blobs in the generated
`src/fontin_data.inc` — no runtime asset dependency. Regular is the default; Bold marks panel headers;
faces (Regular/Bold/Italic/SmallCaps) are embedded in the executable as their own TTF bytes in the
generated `src/fontin_data.inc` — no runtime asset dependency, and see above for why they are not
compressed. ImGui must be told `FontDataOwnedByAtlas = false` for them, as for a mapped file; the
same config must never reach `AddFontFromFileTTF`, which allocates its own buffer and then leaks it.
Regular is the default; Bold marks panel headers;
**SmallCaps renders item text**, matching the game. SmallCaps is a separate family, *not* an OpenType
`smcp` feature — load-bearing, since ImGui does no shaping or feature substitution. ImGui 1.92 fonts
are dynamically scalable, so it's one `ImFont*` per face at any size: `PushFont(fonts.bold, 22.0f)`.
Expand Down
4 changes: 3 additions & 1 deletion docs/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ name to `src/ui/glyphs.hpp` — they are a contract, and only one side of it dra
`pyftsubset`, which is the only build-time dependency here that is not a system package.
The icon is the same deal — after
changing `assets/popc_icon.png`, run `./scripts/gen-icon-data.sh` (rewrites `src/icon_data.inc` and
`assets/popc_icon.ico`; needs ImageMagick for the latter).
`assets/popc_icon.ico`; needs ImageMagick for both, since the embedded copy is downscaled to 64px).
All three write plain byte arrays through `scripts/bin2c.py`, and
[architecture.md](architecture.md) says why they are not compressed.

`-fsanitize=address,undefined` for debug builds is not wired into CMake yet; pass it by hand:

Expand Down
26 changes: 26 additions & 0 deletions scripts/bin2c.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
#!/usr/bin/env python3
"""Emit one C byte array for a binary file, on stdout: `bin2c.py <symbol> <file>`.

The bytes go in **as they are** — not compressed, not encoded. That costs a few kilobytes of
executable over the base85-and-stb_compress idiom this replaced, and buys a `.rdata` a scanner can
recognise: a TTF still starts with a table directory and a PNG with its signature, where an encoded
blob paired with a routine that decodes it into a fresh buffer is shaped exactly like a packer, and
gets scored that way on an unsigned binary. See docs/architecture.md.
"""

import sys

if len(sys.argv) != 3:
sys.exit("usage: bin2c.py <symbol> <file>")

symbol, path = sys.argv[1], sys.argv[2]
with open(path, "rb") as f:
data = f.read()
if not data:
sys.exit(f"{path} is empty")

out = sys.stdout
out.write(f"static const unsigned char {symbol}[{len(data)}] = {{\n")
for i in range(0, len(data), 16):
out.write(" " + "".join(f"0x{b:02x}," for b in data[i : i + 16]) + "\n")
out.write("};\n")
17 changes: 4 additions & 13 deletions scripts/gen-font-data.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,23 @@
# Regenerate src/fontin_data.inc — the Fontin faces embedded in the executable.
# Run this only when changing the bundled typeface; the output is committed.
#
# Needs the TTFs in assets/fonts (scripts/fetch-fonts.sh) and a configured build
# tree, for ImGui's binary_to_compressed_c.cpp.
# Needs the TTFs in assets/fonts (scripts/fetch-fonts.sh). The faces go in as their own
# bytes — scripts/bin2c.py says why that is worth the few kilobytes.
set -euo pipefail

root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
fonts="$root/assets/fonts"
out="$root/src/fontin_data.inc"
imgui="${IMGUI_SOURCE_DIR:-$root/build/_deps/imgui-src}"

[ -f "$fonts/Fontin-Regular.ttf" ] || "$root/scripts/fetch-fonts.sh"
if [ ! -f "$imgui/misc/fonts/binary_to_compressed_c.cpp" ]; then
echo "no ImGui source at $imgui — configure the build first, or set IMGUI_SOURCE_DIR" >&2
exit 1
fi

tmp="$(mktemp -d)"
trap 'rm -rf "$tmp"' EXIT
c++ -O2 -o "$tmp/b2c" "$imgui/misc/fonts/binary_to_compressed_c.cpp"

{
echo "// Generated by scripts/gen-font-data.sh — do not edit."
echo "// Fontin by Jos Buivenga (exljbris), base85-encoded + stb_compress."
echo "// Fontin by Jos Buivenga (exljbris), embedded as the TTF bytes themselves."
echo "// See assets/fonts/README.md for the license this is bundled under."
for face in Regular:regular Bold:bold Italic:italic SmallCaps:small_caps; do
echo
"$tmp/b2c" -base85 "$fonts/Fontin-${face%%:*}.ttf" "fontin_${face##*:}" | tail -n +3
python3 "$root/scripts/bin2c.py" "fontin_${face##*:}_ttf" "$fonts/Fontin-${face%%:*}.ttf"
done
} > "$out"

Expand Down
16 changes: 3 additions & 13 deletions scripts/gen-glyph-data.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,22 @@
# Regenerate src/glyph_data.inc — the UI glyph font embedded in the executable.
# Run this only when changing which glyphs are bundled; the output is committed.
#
# Needs assets/fonts/PPCGlyphs.ttf (scripts/fetch-glyphs.sh) and a configured build
# tree, for ImGui's binary_to_compressed_c.cpp.
# Needs assets/fonts/PPCGlyphs.ttf (scripts/fetch-glyphs.sh).
set -euo pipefail

root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
fonts="$root/assets/fonts"
out="$root/src/glyph_data.inc"
imgui="${IMGUI_SOURCE_DIR:-$root/build/_deps/imgui-src}"

[ -f "$fonts/PPCGlyphs.ttf" ] || "$root/scripts/fetch-glyphs.sh"
if [ ! -f "$imgui/misc/fonts/binary_to_compressed_c.cpp" ]; then
echo "no ImGui source at $imgui — configure the build first, or set IMGUI_SOURCE_DIR" >&2
exit 1
fi

tmp="$(mktemp -d)"
trap 'rm -rf "$tmp"' EXIT
c++ -O2 -o "$tmp/b2c" "$imgui/misc/fonts/binary_to_compressed_c.cpp"

{
echo "// Generated by scripts/gen-glyph-data.sh — do not edit."
echo "// A subset of Font Awesome Free Solid, base85-encoded + stb_compress."
echo "// A subset of Font Awesome Free Solid, embedded as the TTF bytes themselves."
echo "// Which codepoints: scripts/fetch-glyphs.sh. What they are called: src/ui/glyphs.hpp."
echo "// See assets/fonts/README.md for the license this is bundled under."
echo
"$tmp/b2c" -base85 "$fonts/PPCGlyphs.ttf" "ppc_glyphs" | tail -n +3
python3 "$root/scripts/bin2c.py" ppc_glyphs_ttf "$fonts/PPCGlyphs.ttf"
} > "$out"

echo "wrote $out ($(wc -c < "$out") bytes)"
65 changes: 24 additions & 41 deletions scripts/gen-icon-data.sh
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
#!/usr/bin/env bash
# Regenerate the embedded application icon from assets/popc_icon.png:
# src/icon_data.inc — the PNG, base85-encoded (same idiom as the fonts)
# src/icon_data.inc — the PNG bytes, for the tray and the window icon
# assets/popc_icon.ico — the Windows executable resource
# Both outputs are committed; run this only when the artwork changes.
# Needs ImageMagick for the .ico.
# Both outputs are committed; run this only when the artwork changes. Needs ImageMagick.
#
# The embedded copy is downscaled first. It is only ever drawn at tray size — 16 or 32px on
# Windows, a little more under a GTK panel — and as the window icon a switcher may show larger,
# which is what 64 is for. The 128px master stays the source of the .ico, where the big entries
# are the ones Explorer actually uses.
set -euo pipefail

root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
Expand All @@ -12,41 +16,20 @@ out="$root/src/icon_data.inc"
ico="$root/assets/popc_icon.ico"

[ -f "$src" ] || { echo "no icon at $src" >&2; exit 1; }

python3 - "$src" "$out" <<'PY'
import sys

src, out = sys.argv[1], sys.argv[2]
data = open(src, 'rb').read()
size = len(data)
padded = data + b'\0' * (-size % 4) # the decoder works in 4-byte groups

def enc(v):
v = v % 85 + 35
return chr(v + 1 if v >= ord('\\') else v) # backslash would escape the next char

chars = []
for i in range(0, len(padded), 4):
word = int.from_bytes(padded[i:i+4], 'little')
for _ in range(5):
chars.append(enc(word))
word //= 85

with open(out, 'w') as f:
f.write("// Generated by scripts/gen-icon-data.sh from assets/popc_icon.png — do not edit.\n\n")
f.write(f"static const unsigned int popc_icon_png_size = {size};\n")
f.write(f"static const char popc_icon_png_base85[{len(chars)}+1] =\n")
for i in range(0, len(chars), 120):
# '\?' keeps a stray '??x' from being read as a trigraph (a warning, not an error).
line = ''.join(chars[i:i+120]).replace('?', '\\?')
f.write(' "%s"%s\n' % (line, ';' if i + 120 >= len(chars) else ''))
print(f"wrote {out} ({size} bytes of PNG -> {len(chars)} chars)")
PY

if command -v magick >/dev/null; then
# No entry above the source's own 128px — upscaled sizes are just bloat.
magick "$src" -define icon:auto-resize=128,64,48,32,16 "$ico"
echo "wrote $ico"
else
echo "ImageMagick not found — $ico left unchanged" >&2
fi
command -v magick >/dev/null || { echo "ImageMagick (magick) is required" >&2; exit 1; }

tmp="$(mktemp -d)"
trap 'rm -rf "$tmp"' EXIT
magick "$src" -resize 64x64 -strip "$tmp/icon64.png"

{
echo "// Generated by scripts/gen-icon-data.sh from assets/popc_icon.png — do not edit."
echo "// The 128px master downscaled to 64; the .ico resource is where the big sizes live."
echo
python3 "$root/scripts/bin2c.py" popc_icon_png "$tmp/icon64.png"
} > "$out"
echo "wrote $out ($(wc -c < "$tmp/icon64.png") bytes of PNG)"

# No entry above the source's own 128px — upscaled sizes are just bloat.
magick "$src" -define icon:auto-resize=128,64,48,32,16 "$ico"
echo "wrote $ico"
Loading