From 19c3c8b129a05942197510abd5606c0635100f00 Mon Sep 17 00:00:00 2001 From: Nick Nyssen <314538108+amoisttowe1@users.noreply.github.com> Date: Mon, 19 Oct 2026 22:13:15 -0700 Subject: [PATCH 1/6] M14: find Apple's Devanagari faces in the folders current macOS uses MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two of the five paths in the macOS Devanagari list do not exist — `Supplemental/DevanagariSangamMN.ttc`, where Apple spells the family `Devanagari Sangam MN.ttc` with spaces as this file already does for its Telugu and Tamil siblings, and `Supplemental/Kohinoor.ttc`, one level above where Kohinoor.ttc actually lives — so the family documented as second choice has never been reachable. Both arrived together in #1033 and are present from v0.7.1 through main; no CI could notice, since ci.yml has no macOS runner and read() logs only on a hit. List the families best-first, correctly spelled, and look each up in both macOS system font folders. Preference is unchanged: all of Devanagari MT's folders are tried before Sangam MN starts, Arial Unicode stays last, and at most one installed face per script is still loaded (§1.4). Devanagari MT wins on the machine tested, so Hindi renders identically here; the change is that a Mac without it lands on the intended fallback instead of skipping past it. That missing-Mac case is read off the code path, not observed — no screenshot or log claims otherwise. Tests: an exact assertion of the whole list (spellings, folders, order) that runs on every OS and also pins devanagari_candidates() to it; a loader test proving the first readable candidate wins, using two bundled faces of different byte length; a spaced-name resolution test against a temp macOS-shaped tree that panics rather than passing silently when the temp dir is unusable; and a macOS-only disk oracle requiring every family folder that exists here to be reachable, the list to cover Devanagari, Arial Unicode to load under MAX_BYTES, and devanagari_fallback() to land on the same bytes and face as this list. Families Apple stops shipping are reported, not failed — that absence is what the list exists for. Verified on macOS 26.6 (Apple M5 Max): fmt --all clean; clippy --all-targets -D warnings quiet; system_fonts 12 passed; whole crate 1340 passed / 0 failed; xtask layers (37 crates) and assets (272/6/22). Eight of ten deliberate breakages turn a test red; MAX_FACES depth and the PDFCRAFT_SYSTEM_FONTS=0 guarantee do not, both disclosed rather than left implicit. --- crates/ui-egui/src/system_fonts.rs | 160 +++++++++++++++++++++++++++-- 1 file changed, 150 insertions(+), 10 deletions(-) diff --git a/crates/ui-egui/src/system_fonts.rs b/crates/ui-egui/src/system_fonts.rs index 606fa923dc..5eb6f80bed 100644 --- a/crates/ui-egui/src/system_fonts.rs +++ b/crates/ui-egui/src/system_fonts.rs @@ -110,21 +110,52 @@ fn script_fallback(candidates: &[PathBuf], probe: char) -> Option> candidates.iter().find_map(|path| read(path, probe, MAX_BYTES)) } +/// macOS folders that hold Apple's own families, system folder first. On macOS 26.6 (Apple M5 +/// Max) `Kohinoor.ttc` is in `/System/Library/Fonts` while the Sangam MN faces are under +/// `Supplemental/`, so looking one family up in both folders stops a move by Apple from silently +/// dropping it — which is what happened to `Supplemental/Kohinoor.ttc`, listed by the code but +/// not present on that OS. `/Library/Fonts` is deliberately not listed here: it holds +/// compatibility symlinks into `Supplemental/`, and the one face wanted from it is named in full +/// below. On that machine each family below lives in exactly one of the two folders, so their +/// order is a deterministic choice rather than a tie-break that was ever exercised. +/// +/// Only [`macos_devanagari_candidates`] uses this today; the Telugu, Sinhala and Tamil lists still +/// name one folder each. Their first choices all exist on macOS 26.6, so nothing is dropping there +/// now — the same expansion would fit them, and is left for a change that tests it. +const MACOS_FONT_DIRS: [&str; 2] = ["/System/Library/Fonts", "/System/Library/Fonts/Supplemental"]; + +/// The Devanagari families this list tries, best first. Spelled as the files are on disk — +/// `Devanagari Sangam MN.ttc` has spaces, like the other Sangam MN families below. Apple ships at +/// least one more Devanagari-capable collection on macOS 26.6 (`Supplemental/ITFDevanagari.ttc`, +/// 10 faces), and this list does not try it. +const MACOS_DEVANAGARI_FAMILIES: &[&str] = &["DevanagariMT.ttc", "Devanagari Sangam MN.ttc", "Kohinoor.ttc"]; + +/// `families`, best first, each looked for in every folder of `dirs`. Split out from +/// [`macos_candidates`] so the macOS layout can be tested on a machine with no Apple fonts. +fn macos_families_in(dirs: &[&str], families: &[&str]) -> Vec { + families.iter().flat_map(|family| dirs.iter().map(move |dir| Path::new(dir).join(family))).collect() +} + +/// [`macos_families_in`] against the folders Apple actually uses. +fn macos_candidates(families: &[&str]) -> Vec { + macos_families_in(&MACOS_FONT_DIRS, families) +} + +/// The macOS Devanagari list: Apple's installed families, then the last-resort Unicode face. +/// `Arial Unicode.ttf` is listed from `/Library/Fonts`, where it is a symlink into +/// `Supplemental/` on current macOS, and is read only when no family above covers the script. +fn macos_devanagari_candidates() -> Vec { + let mut list = macos_candidates(MACOS_DEVANAGARI_FAMILIES); + list.push(PathBuf::from("/Library/Fonts/Arial Unicode.ttf")); + list +} + /// Well-known locations of Devanagari (Hindi) faces, best first. fn devanagari_candidates() -> Vec { if cfg!(windows) { ["Nirmala.ttf", "NirmalaB.ttf", "mangal.ttf", "aparaj.ttf", "utsah.ttf"].iter().map(|f| windows_fonts().join(f)).collect() } else if cfg!(target_os = "macos") { - [ - "/System/Library/Fonts/Supplemental/DevanagariMT.ttc", - "/System/Library/Fonts/Supplemental/DevanagariSangamMN.ttc", - "/System/Library/Fonts/Supplemental/Kohinoor.ttc", - "/System/Library/Fonts/Kohinoor.ttc", - "/Library/Fonts/Arial Unicode.ttf", - ] - .iter() - .map(PathBuf::from) - .collect() + macos_devanagari_candidates() } else { unix_fonts(&[ "google-noto/NotoSansDevanagariUI-Regular.ttf", @@ -398,6 +429,33 @@ mod tests { } } + /// `script_fallback` honours the documented installed-font switch, under which it returns + /// nothing before order is ever consulted — so both loader tests skip when it is off, the way + /// `tests/fonts.rs` skips for the same reason. Without this the suite goes red in exactly the + /// mode `cargo xtask screenshots` uses. + fn installed_faces_switched_off() -> bool { + std::env::var_os("PDFCRAFT_SYSTEM_FONTS").is_some_and(|v| v == "0") + } + + #[test] + fn the_first_readable_candidate_wins_not_the_last() { + // List order *is* the preference, so a loader that scanned back-to-front would pick a + // different face and nobody would notice. Two bundled faces both cover 'A' and differ in + // size, so the winner is identifiable by length alone. + if installed_faces_switched_off() { + eprintln!("skipping: PDFCRAFT_SYSTEM_FONTS=0 turns the loader off before order matters"); + return; + } + let dir = Path::new(concat!(env!("CARGO_MANIFEST_DIR"), "/../../assets/fonts")); + let (inter, mono) = (dir.join("Inter-Regular.ttf"), dir.join("JetBrainsMono-Regular.ttf")); + let size = |p: &Path| std::fs::metadata(p).expect("bundled face").len(); + assert_ne!(size(&inter), size(&mono), "the two fixtures must be tellable apart"); + let first = script_fallback(&[inter.clone(), mono.clone()], 'A').expect("Inter covers 'A'"); + assert_eq!(first.font.len() as u64, size(&inter), "the first readable candidate must win"); + let reversed = script_fallback(&[mono.clone(), inter], 'A').expect("JetBrains Mono covers 'A'"); + assert_eq!(reversed.font.len() as u64, size(&mono), "reversing the list must reverse the pick"); + } + #[test] fn a_script_fallback_is_the_first_candidate_with_the_probe() { let inter = PathBuf::from(concat!(env!("CARGO_MANIFEST_DIR"), "/../../assets/fonts/Inter-Regular.ttf")); @@ -427,4 +485,86 @@ mod tests { } } } + + /// The macOS Devanagari list, exactly: families best first, each tried in the system folder + /// before `Supplemental/`. This pins the spellings Apple uses on disk and the order that + /// decides which face wins. A missing space (`DevanagariSangamMN.ttc`), swapping the two + /// folders, or walking folder-major instead of family-major all fail here — and they fail on + /// every OS, because this test reads no filesystem. + #[test] + fn the_macos_devanagari_list_is_apples_layout() { + assert_eq!( + macos_devanagari_candidates(), + vec![ + PathBuf::from("/System/Library/Fonts/DevanagariMT.ttc"), + PathBuf::from("/System/Library/Fonts/Supplemental/DevanagariMT.ttc"), + PathBuf::from("/System/Library/Fonts/Devanagari Sangam MN.ttc"), + PathBuf::from("/System/Library/Fonts/Supplemental/Devanagari Sangam MN.ttc"), + PathBuf::from("/System/Library/Fonts/Kohinoor.ttc"), + PathBuf::from("/System/Library/Fonts/Supplemental/Kohinoor.ttc"), + PathBuf::from("/Library/Fonts/Arial Unicode.ttf"), + ] + ); + // Pin what the application actually consults, not just the helper that builds it. + #[cfg(target_os = "macos")] + assert_eq!(devanagari_candidates(), macos_devanagari_candidates(), "the app must consult the list this test pins"); + } + + #[test] + fn a_family_whose_name_has_spaces_is_still_found() { + let nanos = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|d| d.subsec_nanos()).unwrap_or(0); + let root = std::env::temp_dir().join(format!("pdfcraft-macos-fonts-{}-{nanos}", std::process::id())); + let supplemental = root.join("Supplemental"); + std::fs::create_dir_all(&supplemental).expect("build a temp macOS-shaped font tree"); + let dirs = [root.to_string_lossy().to_string(), supplemental.to_string_lossy().to_string()]; + let names: Vec<&str> = dirs.iter().map(|d| d.as_str()).collect(); + let inter: &[u8] = include_bytes!("../../../assets/fonts/Inter-Regular.ttf"); + let spaced = "Devanagari Sangam MN.ttc"; + std::fs::write(supplemental.join(spaced), inter).expect("place a face at the spaced path"); + let found = macos_families_in(&names, &[spaced]).iter().any(|p| read(p, 'A', MAX_BYTES).is_some()); + let _ = std::fs::remove_dir_all(&root); + assert!(found, "a family named with spaces did not resolve under {dirs:?}"); + } + + /// On a Mac, where Apple's fonts are installed: every family folder that really exists is + /// reachable from the candidate list, and something in the list covers Devanagari. A family + /// Apple no longer ships is reported rather than failed — this list exists precisely for the + /// case where one of them is missing. CI has no macOS runner (`.github/workflows/ci.yml` + /// keeps the org's Mac slots for release builds), so this runs on contributors' and + /// maintainers' machines. + #[cfg(target_os = "macos")] + #[test] + fn every_installed_devanagari_face_is_reachable_on_this_mac() { + if installed_faces_switched_off() { + eprintln!("skipping on this machine: PDFCRAFT_SYSTEM_FONTS=0 turns installed faces off"); + return; + } + let list = devanagari_candidates(); + for family in MACOS_DEVANAGARI_FAMILIES { + let installed: Vec = MACOS_FONT_DIRS.iter().map(|dir| Path::new(dir).join(family)).filter(|p| p.is_file()).collect(); + if installed.is_empty() { + eprintln!("{family}: not installed on this machine, so the list offers nothing that exists — fine, and reported only"); + continue; + } + for path in &installed { + assert!(list.contains(path), "{path:?} is installed here but the candidate list skips it: {list:?}"); + } + } + assert!(script_fallback(&devanagari_candidates(), DEVANAGARI_PROBE).is_some(), "no installed face on this Mac covers Devanagari"); + // And the function callers use, not just its inputs: asserting *some* face loads is not + // enough, because both lists end at the same last-resort Unicode face and would rescue a + // mix-up. The app-facing function must land on the same bytes, and the same face within + // them, as this list produces. + let direct = script_fallback(&devanagari_candidates(), DEVANAGARI_PROBE).expect("the list above resolves"); + let cached = devanagari_fallback().expect("devanagari_fallback() resolves where these faces are installed"); + assert_eq!(cached.font.len(), direct.font.len(), "devanagari_fallback() must load the face this list points at, not another script's"); + assert_eq!(cached.index, direct.index, "and the same face within that file"); + // The advertised last resort is the biggest file on the list and reaches its target + // through a symlink, so both the size cap and the link are worth checking here rather + // than in a shell. `MAX_BYTES` is what the loader applies to non-Han scripts. + assert!( + read(Path::new("/Library/Fonts/Arial Unicode.ttf"), DEVANAGARI_PROBE, MAX_BYTES).is_some(), + "the last-resort Unicode face did not resolve and load under MAX_BYTES" + ); + } } From 21dc64ea5f4011319a1c6bf9140108771e29edf7 Mon Sep 17 00:00:00 2001 From: Nick Nyssen <314538108+amoisttowe1@users.noreply.github.com> Date: Mon, 19 Oct 2026 22:13:38 -0700 Subject: [PATCH 2/6] M14: skip the loader tests when installed faces are switched off MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `a_script_fallback_is_the_first_candidate_with_the_probe` calls script_fallback, which returns nothing when PDFCRAFT_SYSTEM_FONTS=0 — the switch cargo xtask screenshots sets (AGENTS.md §1.4) — so it failed in that mode on main, independently of the Devanagari fix. Skip both loader tests with a printed reason instead, matching crates/ui-egui/tests/fonts.rs. Split out from the previous commit so it can be dropped without touching the fix: PDFCRAFT_SYSTEM_FONTS=0 cargo test -p pdfcraft-ui-egui --lib system_fonts now reports 12 passed rather than one failure. --- crates/ui-egui/src/system_fonts.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/crates/ui-egui/src/system_fonts.rs b/crates/ui-egui/src/system_fonts.rs index 5eb6f80bed..360eec9de9 100644 --- a/crates/ui-egui/src/system_fonts.rs +++ b/crates/ui-egui/src/system_fonts.rs @@ -458,6 +458,12 @@ mod tests { #[test] fn a_script_fallback_is_the_first_candidate_with_the_probe() { + // This one calls `script_fallback` too, so it fails under the documented switch that + // published screenshots use — skip with a reason, as tests/fonts.rs does. + if installed_faces_switched_off() { + eprintln!("skipping: PDFCRAFT_SYSTEM_FONTS=0 turns the loader off"); + return; + } let inter = PathBuf::from(concat!(env!("CARGO_MANIFEST_DIR"), "/../../assets/fonts/Inter-Regular.ttf")); let candidates = [PathBuf::from("definitely/not/here.ttf"), inter]; assert!(script_fallback(&candidates, 'A').is_some()); From 1688bd407a2813d45a9888e2cd37d34031b89c61 Mon Sep 17 00:00:00 2001 From: Nick Nyssen <314538108+amoisttowe1@users.noreply.github.com> Date: Mon, 19 Oct 2026 23:56:36 -0700 Subject: [PATCH 3/6] M14: check the Arial last resort only where it is installed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CodeRabbit caught that the macOS oracle asserted unconditionally that /Library/Fonts/Arial Unicode.ttf resolves and loads under MAX_BYTES. On a Mac without that font the assertion fails while Devanagari coverage is perfectly fine — the loader treats candidates as optional, and so should this test. Hardened beyond an is_file() guard: is_file() follows symlinks, so a link whose target Apple has moved would read as "not installed" and skip — the very dead-path defect this branch exists to catch. Gate on symlink_metadata so a present-but-unresolved entry fails loudly, keep the resolution and MAX_BYTES assertions where the face exists, report otherwise. The path is taken from the list instead of repeated here, so it cannot drift from what ships. Also pin three load-bearing constants in the CI-visible list test, because each could otherwise be retuned silently (verified: all three were green before): MAX_BYTES 32 MiB — the cap that decides whether a 23 MB Unicode face is accepted, raised or lowered both now fail; DEVANAGARI_PROBE U+0915 — what makes a Hindi check a Hindi check, since Arial covers both scripts and was masking a probe swap; MAX_FACES 16 — the depth into .ttc collections the loader trusts. Verified on macOS 26.6 (Apple M5 Max): cargo fmt --all clean; clippy --all-targets -D warnings quiet; system_fonts 12 passed in normal mode and under PDFCRAFT_SYSTEM_FONTS=0; whole crate 1340 passed / 0 failed. Mutations: cap lowered (2 tests red), cap raised to 512 MiB, probe swapped to Arabic alef and MAX_FACES cut to 1 all red via the pins. One limit stays open, stated rather than glossed: nothing verifies 16 faces is deep enough for real collections — that needs a synthetic .ttc with the covering glyph past index 16, and here every needed face sits at index 0. --- crates/ui-egui/src/system_fonts.rs | 32 +++++++++++++++++++++++------- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/crates/ui-egui/src/system_fonts.rs b/crates/ui-egui/src/system_fonts.rs index 360eec9de9..08615ef4af 100644 --- a/crates/ui-egui/src/system_fonts.rs +++ b/crates/ui-egui/src/system_fonts.rs @@ -511,6 +511,13 @@ mod tests { PathBuf::from("/Library/Fonts/Arial Unicode.ttf"), ] ); + // These two constants decide what the loader accepts, and one of them is what makes a Hindi + // check a Hindi check. Pin them here so neither can be retuned out from under this test — or + // from under the macOS oracle, whose Arial assertion is now conditional on installation. + assert_eq!(MAX_BYTES, 32 << 20, "the non-Han installed-face size cap is part of this module's contract (Arial Unicode is 23,278,008 B)"); + assert_eq!(DEVANAGARI_PROBE, '\u{0915}', "Devanagari ka is what the Hindi candidates are probed for, not another script's letter"); + assert_eq!(MAX_FACES, 16, "how deep into a .ttc collection the loader looks; the macOS oracle trusts this depth"); + // Pin what the application actually consults, not just the helper that builds it. #[cfg(target_os = "macos")] assert_eq!(devanagari_candidates(), macos_devanagari_candidates(), "the app must consult the list this test pins"); @@ -565,12 +572,23 @@ mod tests { let cached = devanagari_fallback().expect("devanagari_fallback() resolves where these faces are installed"); assert_eq!(cached.font.len(), direct.font.len(), "devanagari_fallback() must load the face this list points at, not another script's"); assert_eq!(cached.index, direct.index, "and the same face within that file"); - // The advertised last resort is the biggest file on the list and reaches its target - // through a symlink, so both the size cap and the link are worth checking here rather - // than in a shell. `MAX_BYTES` is what the loader applies to non-Han scripts. - assert!( - read(Path::new("/Library/Fonts/Arial Unicode.ttf"), DEVANAGARI_PROBE, MAX_BYTES).is_some(), - "the last-resort Unicode face did not resolve and load under MAX_BYTES" - ); + // The advertised last resort is the biggest file on the list and reaches its target through + // a compatibility symlink in `/Library/Fonts`, a folder installers and users may modify — so + // it is optional in the same sense the families above are, and a Mac without it must not go + // red for an unrelated reason. But `is_file()` follows symlinks: a link whose target has + // moved reads as "no file", which is exactly the dead-path defect this change exists to + // catch. So gate on the entry existing (lstat), then require that it resolves, then check + // size cap and coverage. `MAX_BYTES` is what the loader applies to non-Han scripts; the path + // comes from the list itself rather than being written out again, so it cannot drift. + let last_resort = list.last().expect("the macOS Devanagari list always ends at the Unicode face").clone(); + if std::fs::symlink_metadata(&last_resort).is_ok() { + assert!(last_resort.is_file(), "{last_resort:?} exists but does not resolve — Apple moved the target and this path has gone dead"); + assert!( + read(&last_resort, DEVANAGARI_PROBE, MAX_BYTES).is_some(), + "the last-resort Unicode face is installed here but did not resolve and load under MAX_BYTES" + ); + } else { + eprintln!("{last_resort:?} is not installed on this machine — optional to the loader, so nothing to check"); + } } } From 4e69ca14d3734887b82e32d7a4aa725f77637d58 Mon Sep 17 00:00:00 2001 From: Nick Nyssen <314538108+amoisttowe1@users.noreply.github.com> Date: Tue, 20 Oct 2026 01:01:27 -0700 Subject: [PATCH 4/6] M14: name the last resort and keep the pins honest MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up to review of this branch, no production change. The oracle now asserts the list still ends at Arial Unicode before checking it. Without that, deriving the path from list.first() instead of last() stayed green on this machine and printed "not installed" — the check disappeared because /System/Library/Fonts/DevanagariMT.ttc is absent here. Deriving the path from the list is still better than repeating a literal, but only if something verifies it points where the test thinks. The three retune tripwires (MAX_BYTES, DEVANAGARI_PROBE, MAX_FACES) move into their own test, so the layout test stays about Apple's directory layout and these are labelled for what they are: constants nothing else pins, each confirmed to be the only guard by deleting it and seeing the matching regression go green again. Comments corrected accordingly, including not claiming Apple moved a target when a user could equally have removed it. Verified on macOS 26.6 (Apple M5 Max): fmt --all clean; clippy -D warnings all-targets quiet; system_fonts 13 passed with and without PDFCRAFT_SYSTEM_FONTS=0; whole crate 1341 passed / 0 failed / 5 ignored; xtask layers (37 crates) and assets (272/6/22). Mutations re-run at this commit: folder-major expansion, folder-order swap, typo restore, loader reversal, cap raised to 512 MiB, probe swapped to Arabic alef and MAX_FACES cut to 1 are all red; deleting the PDFCRAFT_SYSTEM_FONTS=0 early return leaves all 1341 green, which is stated rather than hidden. --- crates/ui-egui/src/system_fonts.rs | 34 ++++++++++++++++++++++-------- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/crates/ui-egui/src/system_fonts.rs b/crates/ui-egui/src/system_fonts.rs index 08615ef4af..944f424a2c 100644 --- a/crates/ui-egui/src/system_fonts.rs +++ b/crates/ui-egui/src/system_fonts.rs @@ -492,6 +492,21 @@ mod tests { } } + /// Retune tripwires, deliberately separate from the layout test above: these assert that three + /// constants still hold the values this module's contract depends on. They cannot fail on their + /// own — they exist so that editing one is a decision someone has to make twice. Each was + /// checked by deleting the pin and watching the matching regression go green again: raising + /// `MAX_BYTES` above Arial Unicode's ~23 MB, swapping the Devanagari probe for Arabic `U+0627` + /// (which that face also covers, so every other test stays green), and cutting `MAX_FACES` to 1. + /// The oracle in the macOS test below also depends on all three, and it is conditional on + /// installation — so on CI these are the only guard. + #[test] + fn the_load_constants_are_the_documented_contract() { + assert_eq!(MAX_BYTES, 32 << 20, "the non-Han installed-face size cap is this module's contract (Arial Unicode is ~23 MB)"); + assert_eq!(DEVANAGARI_PROBE, '\u{0915}', "Devanagari ka is what the Hindi candidates are probed for, not another script's letter"); + assert_eq!(MAX_FACES, 16, "how deep into a .ttc collection the loader looks; the macOS oracle trusts this depth"); + } + /// The macOS Devanagari list, exactly: families best first, each tried in the system folder /// before `Supplemental/`. This pins the spellings Apple uses on disk and the order that /// decides which face wins. A missing space (`DevanagariSangamMN.ttc`), swapping the two @@ -511,13 +526,6 @@ mod tests { PathBuf::from("/Library/Fonts/Arial Unicode.ttf"), ] ); - // These two constants decide what the loader accepts, and one of them is what makes a Hindi - // check a Hindi check. Pin them here so neither can be retuned out from under this test — or - // from under the macOS oracle, whose Arial assertion is now conditional on installation. - assert_eq!(MAX_BYTES, 32 << 20, "the non-Han installed-face size cap is part of this module's contract (Arial Unicode is 23,278,008 B)"); - assert_eq!(DEVANAGARI_PROBE, '\u{0915}', "Devanagari ka is what the Hindi candidates are probed for, not another script's letter"); - assert_eq!(MAX_FACES, 16, "how deep into a .ttc collection the loader looks; the macOS oracle trusts this depth"); - // Pin what the application actually consults, not just the helper that builds it. #[cfg(target_os = "macos")] assert_eq!(devanagari_candidates(), macos_devanagari_candidates(), "the app must consult the list this test pins"); @@ -579,10 +587,18 @@ mod tests { // moved reads as "no file", which is exactly the dead-path defect this change exists to // catch. So gate on the entry existing (lstat), then require that it resolves, then check // size cap and coverage. `MAX_BYTES` is what the loader applies to non-Han scripts; the path - // comes from the list itself rather than being written out again, so it cannot drift. + // comes from the list itself rather than being written out again, so this oracle cannot + // drift from what production offers. let last_resort = list.last().expect("the macOS Devanagari list always ends at the Unicode face").clone(); + assert!( + last_resort.to_string_lossy().ends_with("Arial Unicode.ttf"), + "this block checks the Unicode last resort, so the list must still end at it: {last_resort:?}" + ); if std::fs::symlink_metadata(&last_resort).is_ok() { - assert!(last_resort.is_file(), "{last_resort:?} exists but does not resolve — Apple moved the target and this path has gone dead"); + assert!( + last_resort.is_file(), + "{last_resort:?} exists but does not resolve — its target was moved or removed, so this path has gone dead" + ); assert!( read(&last_resort, DEVANAGARI_PROBE, MAX_BYTES).is_some(), "the last-resort Unicode face is installed here but did not resolve and load under MAX_BYTES" From 79cfdc0020eecfbba9508352e4e97dc3e981c65b Mon Sep 17 00:00:00 2001 From: Nick Nyssen <314538108+amoisttowe1@users.noreply.github.com> Date: Tue, 20 Oct 2026 01:59:35 -0700 Subject: [PATCH 5/6] M14: apply the dead-path rule to families, not just the last resort MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Round-6 review caught that this branch rejected is_file() for one path and kept using it forty lines above. The family loop filtered candidates with is_file(), which follows symlinks, so an entry that exists but no longer resolves was classified "not installed" and skipped — exactly the defect this branch fixes elsewhere. Now both blocks share one rule: presence via symlink_metadata, then a hard requirement that the entry resolves. Absence is still tolerated and reported; a broken entry fails loudly with the family named, because that is the failure we are trying to catch rather than a machine difference. Checked against this Mac first: no family or last-resort path is present-but-dangling here, so the stricter rule cannot produce a spurious red on current macOS. Also corrected two stale comments — the tripwire test is separate from the layout test *below* it, and the oracle's doc comment now states the exception the PR description already does: a Mac where nothing in the list covers Devanagari fails, since such a machine cannot validate anything. Verified solo on macOS 26.6 (Apple M5 Max), no concurrent writers this time: fmt --all clean; clippy -D warnings all-targets quiet; system_fonts 13 passed with and without PDFCRAFT_SYSTEM_FONTS=0; whole crate 1341 passed / 0 failed / 5 ignored; xtask layers (37 crates) and assets (272/6/22). Mutation evidence for the change, via font folders redirected to /tmp so system paths were untouched: with a dangling DevanagariMT.ttc in the system folder the current code fails at "exists but does not resolve", while reverting to is_file() filtering makes the identical defect pass silently. Re-ran the pinned regressions too: cap raised to 512 MiB, MAX_FACES cut to 1, probe swapped to Arabic alef, last_resort derived from first(), misspelling restored — all red. --- crates/ui-egui/src/system_fonts.rs | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/crates/ui-egui/src/system_fonts.rs b/crates/ui-egui/src/system_fonts.rs index 944f424a2c..a9c71ccecd 100644 --- a/crates/ui-egui/src/system_fonts.rs +++ b/crates/ui-egui/src/system_fonts.rs @@ -492,7 +492,7 @@ mod tests { } } - /// Retune tripwires, deliberately separate from the layout test above: these assert that three + /// Retune tripwires, deliberately separate from `the_macos_devanagari_list_is_apples_layout` below: these assert that three /// constants still hold the values this module's contract depends on. They cannot fail on their /// own — they exist so that editing one is a decision someone has to make twice. Each was /// checked by deleting the pin and watching the matching regression go green again: raising @@ -549,8 +549,9 @@ mod tests { /// On a Mac, where Apple's fonts are installed: every family folder that really exists is /// reachable from the candidate list, and something in the list covers Devanagari. A family - /// Apple no longer ships is reported rather than failed — this list exists precisely for the - /// case where one of them is missing. CI has no macOS runner (`.github/workflows/ci.yml` + /// Apple no longer ships is reported rather than failed — this list exists precisely for the case + /// where one of them is missing, with one exception: if *nothing* here covers Devanagari the test + /// fails loudly, because a machine with no face to check cannot validate the list at all. CI has no macOS runner (`.github/workflows/ci.yml` /// keeps the org's Mac slots for release builds), so this runs on contributors' and /// maintainers' machines. #[cfg(target_os = "macos")] @@ -562,12 +563,21 @@ mod tests { } let list = devanagari_candidates(); for family in MACOS_DEVANAGARI_FAMILIES { - let installed: Vec = MACOS_FONT_DIRS.iter().map(|dir| Path::new(dir).join(family)).filter(|p| p.is_file()).collect(); - if installed.is_empty() { + // Same rule as the last resort below: an entry that exists counts, even a link whose + // target has gone — "present but unresolved" is precisely the dead-path defect this change + // is about, and filtering on `is_file()` would quietly reclassify it as "not installed". + // Absence is tolerated; a broken entry is not. + let present: Vec = + MACOS_FONT_DIRS.iter().map(|dir| Path::new(dir).join(family)).filter(|p| std::fs::symlink_metadata(p).is_ok()).collect(); + if present.is_empty() { eprintln!("{family}: not installed on this machine, so the list offers nothing that exists — fine, and reported only"); continue; } - for path in &installed { + for path in &present { + assert!( + path.is_file(), + "{family}: {path:?} exists but does not resolve — its target was moved or removed, so this candidate path has gone dead" + ); assert!(list.contains(path), "{path:?} is installed here but the candidate list skips it: {list:?}"); } } From 4d2e40155adc9ba6772a686f66650fd377772ce9 Mon Sep 17 00:00:00 2001 From: Nick Nyssen <314538108+amoisttowe1@users.noreply.github.com> Date: Tue, 20 Oct 2026 03:00:33 -0700 Subject: [PATCH 6/6] M14: correct two drifted comments and cite why candidates are optional MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A pre-publish review caught both comment errors, and Apple's own documentation settles the rest of it: - installed_faces_switched_off said "both loader tests". It now guards three sites — two loader tests plus the macOS oracle — ever since 21dc64e added the third call without touching this sentence. - MACOS_FONT_DIRS claimed the Telugu, Sinhala and Tamil lists "still name one folder each". They do not: Telugu names /System/Library/Fonts, Supplemental/ and /Library/Fonts (four paths), Tamil three, Sinhala two. The accurate statement is that no family in those lists is looked up in both system folders, which is the property Devanagari now has and they do not. Their first choices all exist on macOS 26.6, verified again here. - The oracle's doc comment now records the authority for treating listed faces as optional: Apple's "Fonts included with macOS Tahoe" page (support.apple.com/122869) is titled "Installed or downloadable fonts" and states that most install automatically while others download via Font Book — without marking which entries are which. Absence of a face is therefore expected on some Macs, which is exactly what the reported-not-failed behaviour accommodates. Comment-only change: fmt --all clean, clippy -D warnings quiet, system_fonts 13 passed with and without PDFCRAFT_SYSTEM_FONTS=0. --- crates/ui-egui/src/system_fonts.rs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/crates/ui-egui/src/system_fonts.rs b/crates/ui-egui/src/system_fonts.rs index a9c71ccecd..add591f05c 100644 --- a/crates/ui-egui/src/system_fonts.rs +++ b/crates/ui-egui/src/system_fonts.rs @@ -119,9 +119,10 @@ fn script_fallback(candidates: &[PathBuf], probe: char) -> Option> /// below. On that machine each family below lives in exactly one of the two folders, so their /// order is a deterministic choice rather than a tie-break that was ever exercised. /// -/// Only [`macos_devanagari_candidates`] uses this today; the Telugu, Sinhala and Tamil lists still -/// name one folder each. Their first choices all exist on macOS 26.6, so nothing is dropping there -/// now — the same expansion would fit them, and is left for a change that tests it. +/// Only [`macos_devanagari_candidates`] uses this today: in the Telugu, Sinhala and Tamil lists each +/// family is named once, in a single folder (some of those lists also name `/Library/Fonts` for the +/// Unicode face). Their first choices all exist on macOS 26.6, so nothing is dropping there now — the +/// same two-folder expansion would fit them, and is left for a change that tests it. const MACOS_FONT_DIRS: [&str; 2] = ["/System/Library/Fonts", "/System/Library/Fonts/Supplemental"]; /// The Devanagari families this list tries, best first. Spelled as the files are on disk — @@ -430,8 +431,8 @@ mod tests { } /// `script_fallback` honours the documented installed-font switch, under which it returns - /// nothing before order is ever consulted — so both loader tests skip when it is off, the way - /// `tests/fonts.rs` skips for the same reason. Without this the suite goes red in exactly the + /// nothing before order is ever consulted — so both loader tests and the macOS oracle skip when + /// it is off, the way `tests/fonts.rs` skips for the same reason. Without this the suite goes red in exactly the /// mode `cargo xtask screenshots` uses. fn installed_faces_switched_off() -> bool { std::env::var_os("PDFCRAFT_SYSTEM_FONTS").is_some_and(|v| v == "0") @@ -551,7 +552,11 @@ mod tests { /// reachable from the candidate list, and something in the list covers Devanagari. A family /// Apple no longer ships is reported rather than failed — this list exists precisely for the case /// where one of them is missing, with one exception: if *nothing* here covers Devanagari the test - /// fails loudly, because a machine with no face to check cannot validate the list at all. CI has no macOS runner (`.github/workflows/ci.yml` + /// fails loudly, because a machine with no face to check cannot validate the list at all. That + /// leniency is not a guess: Apple's "Fonts included with macOS Tahoe" page (support.apple.com/122869) + /// titles its list "Installed or downloadable fonts" and says most install automatically while + /// others download on demand via Font Book — without marking which is which, so no entry in these + /// lists can be assumed present on a given Mac. CI has no macOS runner (`.github/workflows/ci.yml` /// keeps the org's Mac slots for release builds), so this runs on contributors' and /// maintainers' machines. #[cfg(target_os = "macos")]