feat(info): report macOS persona font completeness

Add a Mac fonts count to `cloakbrowser info` next to the existing Windows
font check, in the Python, JS, and .NET wrappers. Counts the macOS core
font set via fc-list on Linux and prints Mac fonts: n/20 with a conditional
nudge when incomplete. Tests added in all three wrappers.
This commit is contained in:
CloakHQ committed 2026-09-10 20:55:42 +02:00
1 parent f04c23da28
commit f0c0eedb2f
11 files changed
+234 -8

No files matched your search

+17
View File
@@ -306,6 +306,7 @@ def _collect_diagnostics(quick: bool, proxy: str | None = None) -> dict:
# Omitted entirely off Linux, where it carries no signal.
if platform.system() == "Linux":
from .browser import (
_MAC_FONT_TELLS,
_OFFICE_FONT_TELLS,
_WINDOWS_FONT_TELLS,
_count_fonts_present,
@@ -313,11 +314,16 @@ def _collect_diagnostics(quick: bool, proxy: str | None = None) -> dict:
# Strict count, not "any one present" — real font installs are atomic
# (you have the whole pack or none), so report how complete the set is.
# Both Windows and macOS sets are counted unconditionally: `info` has no
# persona flag, so we report each raw count and let the user read the one
# for the persona they spoof.
win_n = _count_fonts_present(_WINDOWS_FONT_TELLS)
office_n = _count_fonts_present(_OFFICE_FONT_TELLS)
mac_n = _count_fonts_present(_MAC_FONT_TELLS)
diag["fonts"] = {
"windows": None if win_n is None else [win_n, len(_WINDOWS_FONT_TELLS)],
"office": None if office_n is None else [office_n, len(_OFFICE_FONT_TELLS)],
"macos": None if mac_n is None else [mac_n, len(_MAC_FONT_TELLS)],
}
diag["license"] = license_info
@@ -477,6 +483,17 @@ def _print_diagnostics(diag: dict) -> None:
# persona (~53% of real machines have none), so no install nudge.
verdict = "ok" if n == total else "absent" if n == 0 else "partial"
print(f"Office fonts: {verdict} ({n}/{total})")
mac = diag["fonts"].get("macos")
if mac is None:
print("Mac fonts: unknown (fc-list unavailable)")
else:
n, total = mac
verdict = "ok" if n == total else "missing" if n == 0 else "partial"
print(f"Mac fonts: {verdict} ({n}/{total})")
if n < total:
# Phrased conditionally: some Mac fonts also ship on a Windows box,
# so a partial count is expected unless macOS is the intended persona.
print(f" {ARROW} if spoofing macOS on this host, copy the real Mac fonts (Helvetica Neue, Menlo, SF/Apple system fonts)")
lic = diag["license"]
tier = lic["tier"]
+28
View File
@@ -1520,6 +1520,34 @@ _OFFICE_FONT_TELLS = (
"Bookman Old Style",
)
# macOS OS fonts — the core set present on essentially every real Mac. Their
# absence on a macOS-spoofing Linux host degrades results, just as a Windows-
# spoofing host needs the Windows set above. Several entries (Arial, Georgia,
# Times New Roman, Courier New, Tahoma) also ship on a Windows-provisioned box,
# so a partial count on a non-Mac host is normal, not a fault.
_MAC_FONT_TELLS = (
"Apple Color Emoji",
"Arial",
"Arial Narrow",
"Arial Unicode MS",
"Comic Sans MS",
"Courier",
"Courier New",
"Georgia",
"Gill Sans",
"Helvetica",
"Helvetica Neue",
"Impact",
"Menlo",
"Microsoft Sans Serif",
"Monaco",
"Tahoma",
"Times New Roman",
"Trebuchet MS",
"Webdings",
"Wingdings",
)
_font_warning_checked = False
+14
View File
@@ -206,6 +206,20 @@ static void PrintDiagnostics(Dictionary<string, object?> diag)
string verdict = n == total ? "ok" : n == 0 ? "absent" : "partial";
Console.WriteLine($"Office fonts: {verdict} ({n}/{total})");
}
if (fonts.TryGetValue("macos", out var macObj) && macObj is int[] mac)
{
int n = mac[0], total = mac[1];
string verdict = n == total ? "ok" : n == 0 ? "missing" : "partial";
Console.WriteLine($"Mac fonts: {verdict} ({n}/{total})");
if (n < total)
// Phrased conditionally: some Mac fonts also ship on a Windows box,
// so a partial count is expected unless macOS is the intended persona.
Console.WriteLine(" → if spoofing macOS on this host, copy the real Mac fonts (Helvetica Neue, Menlo, SF/Apple system fonts)");
}
else
{
Console.WriteLine("Mac fonts: unknown (fc-list unavailable)");
}
}
var lic = (Dictionary<string, object?>)diag["license"]!;
+14
View File
@@ -480,6 +480,20 @@ public static class CloakLauncher
"Monotype Corsiva", "Bookman Old Style",
};
// macOS OS fonts — the core set present on essentially every real Mac. Their
// absence on a macOS-spoofing Linux host degrades results, just as a Windows-
// spoofing host needs the Windows set above. Several entries (Arial, Georgia,
// Times New Roman, Courier New, Tahoma) also ship on a Windows box, so a
// partial count off-Mac is normal.
internal static readonly string[] MacFontTells =
{
"Apple Color Emoji", "Arial", "Arial Narrow", "Arial Unicode MS",
"Comic Sans MS", "Courier", "Courier New", "Georgia", "Gill Sans",
"Helvetica", "Helvetica Neue", "Impact", "Menlo", "Microsoft Sans Serif",
"Monaco", "Tahoma", "Times New Roman", "Trebuchet MS", "Webdings",
"Wingdings",
};
internal static bool _fontWarningChecked;
/// <summary>
+6 -2
View File
@@ -79,18 +79,22 @@ internal static class Diagnostics
diag["launch"] = launch;
}
// Windows-font probe — only meaningful on a Linux host spoofing Windows.
// Omitted entirely off Linux, where it carries no signal.
// Windows/macOS-font probe — only meaningful on a Linux host spoofing
// another OS. Omitted entirely off Linux, where it carries no signal.
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
// Strict count, not "any one present" — real font installs are atomic
// (you have the whole pack or none), so report how complete the set is.
// Both sets are counted unconditionally: `info` has no persona flag, so
// we report each raw count and let the user read the one they spoof.
int? winN = CloakLauncher.CountFontsPresent(CloakLauncher.WindowsFontTells);
int? officeN = CloakLauncher.CountFontsPresent(CloakLauncher.OfficeFontTells);
int? macN = CloakLauncher.CountFontsPresent(CloakLauncher.MacFontTells);
diag["fonts"] = new Dictionary<string, object?>
{
["windows"] = winN is null ? null : new[] { winN.Value, CloakLauncher.WindowsFontTells.Length },
["office"] = officeN is null ? null : new[] { officeN.Value, CloakLauncher.OfficeFontTells.Length },
["macos"] = macN is null ? null : new[] { macN.Value, CloakLauncher.MacFontTells.Length },
};
}
@@ -192,4 +192,39 @@ public class FontWarningTests
try { Directory.Delete(tmp, recursive: true); } catch { /* best-effort */ }
}
}
[Fact]
public void MacFontTells_is_the_20_font_mac_core_set()
{
Assert.Equal(20, CloakLauncher.MacFontTells.Length);
Assert.Contains("Helvetica Neue", CloakLauncher.MacFontTells);
}
[Fact]
public void CountFontsPresent_counts_the_mac_set_from_fc_list()
{
// Shim an "fc-list" on PATH that lists only 2 of the mac tells (POSIX only).
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) return;
var dir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
Directory.CreateDirectory(dir);
var shim = Path.Combine(dir, "fc-list");
File.WriteAllText(shim, "#!/bin/sh\necho '/x/Menlo.ttc: Menlo:style=Regular'\necho '/x/Monaco.ttf: Monaco:style=Regular'\n");
using (var chmod = Process.Start(new ProcessStartInfo("chmod", $"+x \"{shim}\"") { UseShellExecute = false }))
{
chmod!.WaitForExit();
}
var prevPath = Environment.GetEnvironmentVariable("PATH");
try
{
Environment.SetEnvironmentVariable("PATH", dir + Path.PathSeparator + prevPath);
Assert.Equal(2, CloakLauncher.CountFontsPresent(CloakLauncher.MacFontTells));
}
finally
{
Environment.SetEnvironmentVariable("PATH", prevPath);
try { Directory.Delete(dir, recursive: true); } catch { /* best-effort */ }
}
}
}
+20 -3
View File
@@ -24,7 +24,7 @@ import {
CHROMIUM_VERSION,
WRAPPER_VERSION,
} from "./config.js";
import { countFontsPresent, WINDOWS_FONT_TELLS, OFFICE_FONT_TELLS } from "./fonts.js";
import { countFontsPresent, WINDOWS_FONT_TELLS, OFFICE_FONT_TELLS, MACOS_FONT_TELLS } from "./fonts.js";
import { resolveProxyGeo } from "./geoip.js";
import { resolveLicenseKey, validateLicense, getProLatestRelease, getSessionSeats, type LicenseInfo } from "./license.js";
import { execFileSync, spawn } from "node:child_process";
@@ -268,16 +268,20 @@ export async function collectDiagnostics(
if (!ok) diag.launch.missing_libs = missingSharedLibs(binPath);
}
// Windows-font probe — only meaningful on a Linux host spoofing Windows.
// Omitted entirely off Linux, where it carries no signal.
// Windows/macOS-font probe — only meaningful on a Linux host spoofing another
// OS. Omitted entirely off Linux, where it carries no signal.
if (os.platform() === "linux") {
// Strict count, not "any one present" — real font installs are atomic
// (you have the whole pack or none), so report how complete the set is.
// Both sets are counted unconditionally: `info` has no persona flag, so we
// report each raw count and let the user read the one for the persona they spoof.
const winN = countFontsPresent(WINDOWS_FONT_TELLS);
const officeN = countFontsPresent(OFFICE_FONT_TELLS);
const macN = countFontsPresent(MACOS_FONT_TELLS);
diag.fonts = {
windows: winN === null ? null : [winN, WINDOWS_FONT_TELLS.length],
office: officeN === null ? null : [officeN, OFFICE_FONT_TELLS.length],
macos: macN === null ? null : [macN, MACOS_FONT_TELLS.length],
};
}
@@ -436,6 +440,19 @@ function printDiagnostics(diag: Record<string, any>): void {
const verdict = n === total ? "ok" : n === 0 ? "absent" : "partial";
console.log(`Office fonts: ${verdict} (${n}/${total})`);
}
const mac = diag.fonts.macos;
if (mac === undefined || mac === null) {
console.log("Mac fonts: unknown (fc-list unavailable)");
} else {
const [n, total] = mac;
const verdict = n === total ? "ok" : n === 0 ? "missing" : "partial";
console.log(`Mac fonts: ${verdict} (${n}/${total})`);
if (n < total) {
// Phrased conditionally: some Mac fonts also ship on a Windows box, so a
// partial count is expected unless macOS is the intended persona.
console.log(" → if spoofing macOS on this host, copy the real Mac fonts (Helvetica Neue, Menlo, SF/Apple system fonts)");
}
}
}
const lic = diag.license;
+28
View File
@@ -45,6 +45,34 @@ export const OFFICE_FONT_TELLS = [
"Bookman Old Style",
];
// macOS OS fonts — the core set present on essentially every real Mac. Their
// absence on a macOS-spoofing Linux host degrades results, just as a Windows-
// spoofing host needs the Windows set above. Several entries (Arial, Georgia,
// Times New Roman, Courier New, Tahoma) also ship on a Windows box, so a partial
// count off-Mac is normal.
export const MACOS_FONT_TELLS = [
"Apple Color Emoji",
"Arial",
"Arial Narrow",
"Arial Unicode MS",
"Comic Sans MS",
"Courier",
"Courier New",
"Georgia",
"Gill Sans",
"Helvetica",
"Helvetica Neue",
"Impact",
"Menlo",
"Microsoft Sans Serif",
"Monaco",
"Tahoma",
"Times New Roman",
"Trebuchet MS",
"Webdings",
"Wingdings",
];
let fontWarningChecked = false;
/**
+8 -3
View File
@@ -72,9 +72,14 @@ describe("collectDiagnostics", () => {
const diag = (await collectDiagnostics(true)) as Record<string, any>;
expect(diag.binary).toBeDefined();
// fonts section only present on Linux
if (os.platform() === "linux") expect(diag.fonts.windows).toBeDefined();
else expect(diag.fonts).toBeUndefined();
// fonts section only present on Linux — reports both the Windows and macOS
// persona sets so a user can read the count for whichever they spoof.
if (os.platform() === "linux") {
expect(diag.fonts.windows).toBeDefined();
expect(diag.fonts.macos).toBeDefined();
} else {
expect(diag.fonts).toBeUndefined();
}
expect(typeof diag.geoip.db_present).toBe("boolean");
expect(Object.keys(diag.modules).length).toBeGreaterThan(0);
});
+14
View File
@@ -138,3 +138,17 @@ describe("maybeWarnWindowsFonts", () => {
expect(String(warn.mock.calls[0][0])).toContain(MSG);
});
});
describe("MACOS_FONT_TELLS", () => {
it("holds the 20-font mac core set and counts via fc-list", async () => {
const { MACOS_FONT_TELLS, countFontsPresent } = await import("../src/fonts.js");
expect(MACOS_FONT_TELLS.length).toBe(20);
expect(MACOS_FONT_TELLS).toContain("Helvetica Neue");
const cp = await import("node:child_process");
// fc-list listing with only 2 of the mac tells present.
vi.mocked(cp.execFileSync).mockReturnValue(
"/x/Menlo.ttc: Menlo:style=Regular\n/x/Monaco.ttf: Monaco:style=Regular" as any,
);
expect(countFontsPresent(MACOS_FONT_TELLS as string[])).toBe(2);
});
});
+50
View File
@@ -306,6 +306,56 @@ def test_free_license_shows_upgrade_hint(capsys):
assert "cloakbrowser.dev" in out
# ---------------------------------------------------------------------------
# Font probe — the Linux-only Windows/macOS-persona font check. Forces the Linux
# branch and mocks fc-list so the count is deterministic on any host.
# ---------------------------------------------------------------------------
def _fonts_linux(mac_present):
"""Patch context: force the Linux font branch and stub fc-list so the Windows
and Office sets read fully present while the mac set reports `mac_present`.
"""
from cloakbrowser.browser import _MAC_FONT_TELLS
def fake_count(tells):
return mac_present if tells is _MAC_FONT_TELLS else len(tells)
return (
patch("cloakbrowser.__main__.platform.system", return_value="Linux"),
patch("cloakbrowser.browser._count_fonts_present", side_effect=fake_count),
)
def test_info_reports_partial_mac_fonts_with_conditional_nudge(capsys):
# 5/20 = the Windows-box overlap (Arial, Georgia, Times New Roman, ...).
p_platform, p_count = _fonts_linux(5)
with p_platform, p_count:
_run(Namespace(quick=True, json=False))
out = capsys.readouterr().out
assert "Mac fonts: partial (5/20)" in out
assert "if spoofing macOS on this host" in out
def test_info_full_mac_fonts_has_no_nudge(capsys):
from cloakbrowser.browser import _MAC_FONT_TELLS
p_platform, p_count = _fonts_linux(len(_MAC_FONT_TELLS))
with p_platform, p_count:
_run(Namespace(quick=True, json=False))
out = capsys.readouterr().out
assert "Mac fonts: ok (20/20)" in out
assert "if spoofing macOS on this host" not in out
def test_info_json_includes_mac_font_count_on_linux(capsys):
p_platform, p_count = _fonts_linux(5)
with p_platform, p_count:
_run(Namespace(quick=True, json=True))
data = json.loads(capsys.readouterr().out)
assert data["fonts"]["macos"] == [5, 20]
# ---------------------------------------------------------------------------
# Launch test — exercises the real subprocess path (not --quick) against a stub
# executable, so the launch-test code is actually covered by CI.