1
//! Built-in MOCK FONTS with fully controlled metrics.
2
//!
3
//! # Why these exist
4
//!
5
//! Every text assertion made against a *system* font is a guess: the engine
6
//! has no control over Arial's advances, and on a CI box Arial may not even
7
//! exist (see `register_named_font` — a family that fontconfig cannot find
8
//! silently falls back, which is exactly how the "8 families → 2 `FontIds`"
9
//! bug hid for so long). So text tests degenerate into "roughly this wide".
10
//!
11
//! The mock fonts fix that by making text layout ARITHMETIC:
12
//!
13
//! | family            | advance | ascent | descent | glyphs        |
14
//! |-------------------|---------|--------|---------|---------------|
15
//! | `Azul Mock Mono`  | 0.5 em  | 0.8 em | 0.2 em  | ASCII 0x20-7E |
16
//! | `Azul Mock Wide`  | 1.0 em  | 0.8 em | 0.2 em  | ASCII 0x20-7E |
17
//!
18
//! At `font-size: 20px`, `Azul Mock Mono` advances exactly 10 px per glyph:
19
//! a 5-character string is exactly 50 px wide and its line box is exactly
20
//! 20 px tall. Caret offsets, selection rectangles, line-break positions and
21
//! bidi run widths become exact integers a test can write down.
22
//!
23
//! # Every glyph draws DIFFERENT ink
24
//!
25
//! Metrics are uniform; ink is not. Each glyph is the same box with a
26
//! codepoint-derived bite out of its interior (a constant frame plus a 3x3
27
//! grid of cells, cell `k` kept iff bit `k` of `codepoint - 0x20` is clear —
28
//! see `scripts/gen_mock_fonts.py`). All 94 inked glyphs therefore rasterise
29
//! to distinct pixels.
30
//!
31
//! This is load-bearing, not decoration. When every glyph was the identical
32
//! filled rectangle, two equal-length strings rendered bit-identically, so a
33
//! text edit like `"tick 1"` -> `"tick 2"` produced damage but no pixel
34
//! change. Any scenario combining a mock font with a pixel-liveness assertion
35
//! (`assert_changed`, `assert_damage_covers_changes`,
36
//! `assert_damage_sound{pixel_identity}`) was then asserting something the
37
//! font made impossible; the ones that passed did so only because their two
38
//! strings happened to differ in LENGTH.
39
//!
40
//! The frame is what keeps this free: it touches all four sides of the glyph
41
//! box, so the tight bounding box — and every metric derived from it — is
42
//! exactly what it was when the box was solid.
43
//!
44
//! # Registration path
45
//!
46
//! These are registered as ordinary rust-fontconfig **memory fonts** in the
47
//! shared [`rust_fontconfig::FcFontCache`] (see
48
//! [`crate::text3::cache::FontManager::register_named_font`]) — the same
49
//! mechanism an embedder uses for a bundled font. They therefore travel the
50
//! *real* resolution path: CSS `font-family` → font-stack collection →
51
//! chain resolution → `FontId` → `load_missing_for_chains` → shaping. There
52
//! is no test-only bypass, which is the point: a test using them exercises
53
//! font resolution rather than skipping it.
54
//!
55
//! # Regenerating, and adding more mock fonts
56
//!
57
//! The `.ttf`s are built by a committed generator, not by hand:
58
//!
59
//! ```text
60
//! python3 scripts/gen_mock_fonts.py
61
//! ```
62
//!
63
//! It rewrites both copies — `assets/fonts/test/` (canonical) and the vendored
64
//! `layout/assets/fonts/test/` that `include_bytes!` below actually reads,
65
//! since that macro cannot reach outside the crate root — and it is
66
//! deterministic: every byte is derived from the `FONTS` table and the
67
//! codepoint, with no hashing, timestamps or iteration order. Re-running it
68
//! without changing the script leaves the working tree clean, so a diff after
69
//! a re-run means the committed fonts are stale.
70
//!
71
//! The script needs no third-party deps (the repo cannot assume `fonttools`).
72
//! To add a font, add an entry to its `FONTS` list — family name, upem,
73
//! advance, ascent, descent, codepoint range — re-run it, commit the `.ttf`,
74
//! and add it to [`BUILTIN_MOCK_FONTS`] below. An RTL mock is the same call with a
75
//! Hebrew/Arabic range; a missing-glyph mock is the same call with a
76
//! truncated range (uncovered chars then take the real fallback path); a
77
//! proportional mock is the same call with a wider advance.
78

            
79
use rust_fontconfig::UnicodeRange;
80

            
81
/// `Azul Mock Mono`: every ASCII glyph advances 0.5 em (10 px at 20 px).
82
pub const MOCK_MONO_TTF: &[u8] = include_bytes!("../../assets/fonts/test/azul-mock-mono.ttf");
83

            
84
/// `Azul Mock Wide`: every ASCII glyph advances 1.0 em (20 px at 20 px).
85
pub const MOCK_WIDE_TTF: &[u8] = include_bytes!("../../assets/fonts/test/azul-mock-wide.ttf");
86

            
87
/// Codepoints the mock fonts cover (printable ASCII). Anything outside this
88
/// range is deliberately *not* covered, so it exercises real fallback.
89
#[must_use]
90
16099
pub fn mock_font_ranges() -> Vec<UnicodeRange> {
91
16099
    vec![UnicodeRange {
92
16099
        start: 0x20,
93
16099
        end: 0x7E,
94
16099
    }]
95
16099
}
96

            
97
/// The mock fonts registered into every `FontManager`: `(family, bytes)`.
98
///
99
/// Registering them unconditionally (rather than behind a test-only flag)
100
/// is intentional: it keeps the production and test font paths identical,
101
/// costs ~30 KiB, and the families are only reachable if a stylesheet asks
102
/// for them by name.
103
pub const BUILTIN_MOCK_FONTS: &[(&str, &[u8])] = &[
104
    ("Azul Mock Mono", MOCK_MONO_TTF),
105
    ("Azul Mock Wide", MOCK_WIDE_TTF),
106
];
107

            
108
/// Advance of one glyph of `family` at `font_size_px`, or `None` if the
109
/// family is not a mock font.
110
///
111
/// Test helper: lets a test compute the expected width of a string without
112
/// hardcoding the em fraction twice.
113
#[must_use]
114
352
pub fn mock_advance_px(family: &str, font_size_px: f32) -> Option<f32> {
115
352
    match family {
116
352
        "Azul Mock Mono" => Some(font_size_px * 0.5),
117
284
        "Azul Mock Wide" => Some(font_size_px),
118
217
        _ => None,
119
    }
120
352
}
121

            
122
#[cfg(test)]
123
#[allow(clippy::float_cmp, clippy::unreadable_literal)]
124
mod autotest_generated {
125
    use super::*;
126

            
127
    // ---------------------------------------------------------------------
128
    // Minimal, panic-free sfnt reader used by the round-trip tests below.
129
    //
130
    // Deliberately written with `get()`/`Option` everywhere: a test helper
131
    // that panics on malformed input would report "the font is broken" as a
132
    // helper bug, and worse, an out-of-bounds index would abort the test
133
    // process rather than fail one assertion.
134
    // ---------------------------------------------------------------------
135

            
136
    fn be_u16(data: &[u8], off: usize) -> Option<u16> {
137
        let b = data.get(off..off.checked_add(2)?)?;
138
        Some(u16::from_be_bytes([b[0], b[1]]))
139
    }
140

            
141
    fn be_i16(data: &[u8], off: usize) -> Option<i16> {
142
        be_u16(data, off).map(|v| v as i16)
143
    }
144

            
145
    fn be_u32(data: &[u8], off: usize) -> Option<u32> {
146
        let b = data.get(off..off.checked_add(4)?)?;
147
        Some(u32::from_be_bytes([b[0], b[1], b[2], b[3]]))
148
    }
149

            
150
    /// Locate a top-level sfnt table by tag, clamped to the file end.
151
    fn sfnt_table<'a>(data: &'a [u8], tag: &[u8; 4]) -> Option<&'a [u8]> {
152
        let num_tables = be_u16(data, 4)? as usize;
153
        for i in 0..num_tables {
154
            let rec = 12usize.checked_add(i.checked_mul(16)?)?;
155
            let record = data.get(rec..rec.checked_add(16)?)?;
156
            if record.get(..4)? == &tag[..] {
157
                let off = be_u32(data, rec + 8)? as usize;
158
                let len = be_u32(data, rec + 12)? as usize;
159
                let end = off.checked_add(len)?.min(data.len());
160
                return data.get(off..end);
161
            }
162
        }
163
        None
164
    }
165

            
166
    /// `(units_per_em, ascender, descender, num_glyphs, advances)` —
167
    /// `advances` holds one entry per `numberOfHMetrics`.
168
    fn font_metrics(data: &[u8]) -> Option<(u16, i16, i16, u16, Vec<u16>)> {
169
        let head = sfnt_table(data, b"head")?;
170
        let hhea = sfnt_table(data, b"hhea")?;
171
        let maxp = sfnt_table(data, b"maxp")?;
172
        let hmtx = sfnt_table(data, b"hmtx")?;
173

            
174
        let upem = be_u16(head, 18)?;
175
        let ascender = be_i16(hhea, 4)?;
176
        let descender = be_i16(hhea, 6)?;
177
        let num_h_metrics = be_u16(hhea, 34)? as usize;
178
        let num_glyphs = be_u16(maxp, 4)?;
179

            
180
        let mut advances = Vec::with_capacity(num_h_metrics);
181
        for i in 0..num_h_metrics {
182
            advances.push(be_u16(hmtx, i.checked_mul(4)?)?);
183
        }
184
        Some((upem, ascender, descender, num_glyphs, advances))
185
    }
186

            
187
    /// Codepoint coverage from the first `cmap` format-4 subtable, with the
188
    /// mandatory `0xFFFF` sentinel segment dropped.
189
    fn cmap_coverage(data: &[u8]) -> Option<Vec<(u32, u32)>> {
190
        let cmap = sfnt_table(data, b"cmap")?;
191
        let num_encodings = be_u16(cmap, 2)? as usize;
192
        for i in 0..num_encodings {
193
            let rec = 4usize.checked_add(i.checked_mul(8)?)?;
194
            let sub_off = be_u32(cmap, rec.checked_add(4)?)? as usize;
195
            let sub = cmap.get(sub_off..)?;
196
            if be_u16(sub, 0)? != 4 {
197
                continue;
198
            }
199
            let seg_count_x2 = be_u16(sub, 6)? as usize;
200
            let seg_count = seg_count_x2 / 2;
201
            let mut out = Vec::with_capacity(seg_count);
202
            for s in 0..seg_count {
203
                let end = be_u16(sub, 14usize.checked_add(s.checked_mul(2)?)?)?;
204
                let start = be_u16(sub, 16usize.checked_add(seg_count_x2)?.checked_add(s * 2)?)?;
205
                if start == 0xFFFF && end == 0xFFFF {
206
                    continue; // required terminator, not real coverage
207
                }
208
                out.push((u32::from(start), u32::from(end)));
209
            }
210
            return Some(out);
211
        }
212
        None
213
    }
214

            
215
    /// `name` table nameID 1 (family) for the Windows/UTF-16BE record.
216
    fn family_name(data: &[u8]) -> Option<String> {
217
        let name = sfnt_table(data, b"name")?;
218
        let count = be_u16(name, 2)? as usize;
219
        let string_off = be_u16(name, 4)? as usize;
220
        for i in 0..count {
221
            let rec = 6usize.checked_add(i.checked_mul(12)?)?;
222
            let platform = be_u16(name, rec)?;
223
            let name_id = be_u16(name, rec.checked_add(6)?)?;
224
            if platform != 3 || name_id != 1 {
225
                continue;
226
            }
227
            let len = be_u16(name, rec.checked_add(8)?)? as usize;
228
            let off = be_u16(name, rec.checked_add(10)?)? as usize;
229
            let start = string_off.checked_add(off)?;
230
            let bytes = name.get(start..start.checked_add(len)?)?;
231
            let units: Vec<u16> = bytes
232
                .chunks_exact(2)
233
                .map(|c| u16::from_be_bytes([c[0], c[1]]))
234
                .collect();
235
            return String::from_utf16(&units).ok();
236
        }
237
        None
238
    }
239

            
240
    /// Family names that must NOT resolve — near misses of the two real ones.
241
    const NEAR_MISSES: &[&str] = &[
242
        "",
243
        " ",
244
        "Azul",
245
        "Azul Mock",
246
        "Azul Mock ",
247
        " Azul Mock Mono",
248
        "Azul Mock Mono ",
249
        "Azul Mock Mon",
250
        "Azul Mock Monospace",
251
        "Azul Mock Wid",
252
        "Azul Mock Wider",
253
        "Azul  Mock Mono",
254
        "Azul Mock  Mono",
255
        "AzulMockMono",
256
        "Azul\tMock Mono",
257
        "Azul\nMock Mono",
258
        "Azul-Mock-Mono",
259
        "azul mock mono",
260
        "AZUL MOCK MONO",
261
        "Azul mock Mono",
262
        "\"Azul Mock Mono\"",
263
        "'Azul Mock Mono'",
264
        "Azul Mock Mono;garbage",
265
        "Azul Mock Mono, sans-serif",
266
        "sans-serif",
267
        "monospace",
268
        "Arial",
269
        "Azul Mock Mono\u{0}",
270
        "\u{0}Azul Mock Mono",
271
        "Azul Mock Mono\u{FEFF}",
272
    ];
273

            
274
    // ---------------------------------------------------------------------
275
    // mock_advance_px — positive control
276
    // ---------------------------------------------------------------------
277

            
278
    #[test]
279
    fn mock_advance_px_valid_minimal_matches_module_doc() {
280
        // The module header promises: at 20 px, Mono advances exactly 10 px
281
        // and Wide exactly 20 px, so a 5-char Mono string is exactly 50 px.
282
        assert_eq!(mock_advance_px("Azul Mock Mono", 20.0), Some(10.0));
283
        assert_eq!(mock_advance_px("Azul Mock Wide", 20.0), Some(20.0));
284
        assert_eq!(
285
            mock_advance_px("Azul Mock Mono", 20.0).unwrap() * 5.0,
286
            50.0,
287
            "5-char Mono string must be exactly 50 px at 20 px"
288
        );
289
    }
290

            
291
    #[test]
292
    fn mock_advance_px_accepts_every_builtin_family() {
293
        for (family, _) in BUILTIN_MOCK_FONTS {
294
            assert!(
295
                mock_advance_px(family, 16.0).is_some(),
296
                "registered family {family:?} has no advance"
297
            );
298
        }
299
    }
300

            
301
    // ---------------------------------------------------------------------
302
    // mock_advance_px — malformed / hostile family strings
303
    // ---------------------------------------------------------------------
304

            
305
    #[test]
306
    fn mock_advance_px_empty_input_is_none() {
307
        assert_eq!(mock_advance_px("", 20.0), None);
308
        assert_eq!(mock_advance_px("", 0.0), None);
309
        assert_eq!(mock_advance_px("", f32::NAN), None);
310
    }
311

            
312
    #[test]
313
    fn mock_advance_px_whitespace_only_is_none() {
314
        for family in ["   ", "\t", "\n", "\r\n", "\t\n", "\u{A0}", "\u{2003}"] {
315
            assert_eq!(
316
                mock_advance_px(family, 20.0),
317
                None,
318
                "whitespace-only {family:?} must not resolve"
319
            );
320
        }
321
    }
322

            
323
    #[test]
324
    fn mock_advance_px_garbage_never_panics() {
325
        // Every byte value that is a valid single-char &str, plus some
326
        // classic parser-breaker payloads.
327
        for b in 0u8..=127 {
328
            let s = (b as char).to_string();
329
            assert_eq!(mock_advance_px(&s, 20.0), None);
330
        }
331
        for family in [
332
            "%s%s%s%n",
333
            "../../etc/passwd",
334
            "\u{0}\u{1}\u{2}\u{7F}",
335
            "\\x00\\xff",
336
            "{}[]()<>",
337
            "\u{FFFD}",
338
        ] {
339
            assert_eq!(mock_advance_px(family, 20.0), None, "{family:?}");
340
        }
341
    }
342

            
343
    #[test]
344
    fn mock_advance_px_near_miss_families_are_all_rejected() {
345
        for family in NEAR_MISSES {
346
            assert_eq!(
347
                mock_advance_px(family, 20.0),
348
                None,
349
                "{family:?} must not be treated as a mock font (matching is \
350
                 exact and case-sensitive: no trimming, no normalisation)"
351
            );
352
        }
353
    }
354

            
355
    #[test]
356
    fn mock_advance_px_extremely_long_input_terminates() {
357
        let huge = "a".repeat(1_000_000);
358
        assert_eq!(mock_advance_px(&huge, 20.0), None);
359

            
360
        // Valid name as a prefix of a 1M-char string: still not a match.
361
        let mut prefixed = String::from("Azul Mock Mono");
362
        prefixed.push_str(&"x".repeat(1_000_000));
363
        assert_eq!(mock_advance_px(&prefixed, 20.0), None);
364

            
365
        // The valid name repeated: not a match either.
366
        assert_eq!(mock_advance_px(&"Azul Mock Mono".repeat(50_000), 20.0), None);
367
    }
368

            
369
    #[test]
370
    fn mock_advance_px_unicode_input_is_none() {
371
        let many_marks = "\u{0301}".repeat(10_000); // 10k combining marks
372
        for family in [
373
            "\u{1F600}",
374
            "Azul Mock Mono\u{1F600}",
375
            "A\u{301}zul Mock Mono", // NFD: 'A' + combining acute
376
            "Azul Mock Mono",       // fullwidth A
377
            "\u{202E}Azul Mock Mono",
378
            "אזול מוק מונו",
379
            "\u{200B}Azul Mock Mono",
380
            "Azul\u{200D}Mock\u{200D}Mono",
381
            "🅰🅱🅲",
382
            many_marks.as_str(),
383
        ] {
384
            assert_eq!(mock_advance_px(family, 20.0), None, "{family:?}");
385
        }
386
    }
387

            
388
    #[test]
389
    fn mock_advance_px_deeply_nested_input_does_not_stack_overflow() {
390
        let nested = format!("{}{}", "[".repeat(10_000), "]".repeat(10_000));
391
        assert_eq!(mock_advance_px(&nested, 20.0), None);
392
        let nested_parens = format!("{}{}", "(".repeat(10_000), ")".repeat(10_000));
393
        assert_eq!(mock_advance_px(&nested_parens, 20.0), None);
394
    }
395

            
396
    #[test]
397
    fn mock_advance_px_numeric_looking_families_are_none() {
398
        for family in [
399
            "0",
400
            "-0",
401
            "NaN",
402
            "inf",
403
            "-inf",
404
            "9223372036854775807",
405
            "1e309",
406
            "0x20",
407
        ] {
408
            assert_eq!(mock_advance_px(family, 20.0), None, "{family:?}");
409
        }
410
    }
411

            
412
    // ---------------------------------------------------------------------
413
    // mock_advance_px — numeric edge cases on font_size_px
414
    // ---------------------------------------------------------------------
415

            
416
    #[test]
417
    fn mock_advance_px_zero_preserves_sign() {
418
        assert_eq!(mock_advance_px("Azul Mock Mono", 0.0), Some(0.0));
419
        assert_eq!(mock_advance_px("Azul Mock Wide", 0.0), Some(0.0));
420

            
421
        // -0.0 * 0.5 == -0.0, and Wide returns the input verbatim.
422
        assert!(mock_advance_px("Azul Mock Mono", -0.0).unwrap().is_sign_negative());
423
        assert!(mock_advance_px("Azul Mock Wide", -0.0).unwrap().is_sign_negative());
424
    }
425

            
426
    #[test]
427
    fn mock_advance_px_nan_propagates_without_panicking() {
428
        for family in ["Azul Mock Mono", "Azul Mock Wide"] {
429
            assert!(mock_advance_px(family, f32::NAN).unwrap().is_nan(), "{family}");
430
            assert!(mock_advance_px(family, -f32::NAN).unwrap().is_nan(), "{family}");
431
        }
432
    }
433

            
434
    #[test]
435
    fn mock_advance_px_infinities_propagate() {
436
        assert_eq!(mock_advance_px("Azul Mock Mono", f32::INFINITY), Some(f32::INFINITY));
437
        assert_eq!(
438
            mock_advance_px("Azul Mock Mono", f32::NEG_INFINITY),
439
            Some(f32::NEG_INFINITY)
440
        );
441
        assert_eq!(mock_advance_px("Azul Mock Wide", f32::INFINITY), Some(f32::INFINITY));
442
        assert_eq!(
443
            mock_advance_px("Azul Mock Wide", f32::NEG_INFINITY),
444
            Some(f32::NEG_INFINITY)
445
        );
446
    }
447

            
448
    #[test]
449
    fn mock_advance_px_extremes_do_not_overflow() {
450
        // Halving can never overflow, and Wide is the identity, so every
451
        // finite input must map to a finite output.
452
        for size in [
453
            f32::MAX,
454
            f32::MIN,
455
            f32::MIN_POSITIVE,
456
            -f32::MIN_POSITIVE,
457
            1e38,
458
            -1e38,
459
            i64::MAX as f32,
460
            i64::MIN as f32,
461
            u64::MAX as f32,
462
        ] {
463
            for family in ["Azul Mock Mono", "Azul Mock Wide"] {
464
                let got = mock_advance_px(family, size).unwrap();
465
                assert!(got.is_finite(), "{family} @ {size:e} produced {got:e}");
466
            }
467
        }
468
    }
469

            
470
    #[test]
471
    fn mock_advance_px_smallest_subnormal_underflows_to_zero_not_garbage() {
472
        let tiny = f32::from_bits(1); // ~1e-45, smallest positive subnormal
473
        let mono = mock_advance_px("Azul Mock Mono", tiny).unwrap();
474
        assert!(mono.is_finite() && !mono.is_sign_negative());
475
        assert!(mono <= tiny, "halving a subnormal must not grow it: {mono:e}");
476
        assert_eq!(mock_advance_px("Azul Mock Wide", tiny), Some(tiny));
477
    }
478

            
479
    #[test]
480
    fn mock_advance_px_negative_sizes_are_passed_through_unclamped() {
481
        // Documents current behaviour rather than asserting a clamp: the
482
        // helper is a pure arithmetic mirror of font-size and performs no
483
        // validation, so a negative size yields a negative advance.
484
        assert_eq!(mock_advance_px("Azul Mock Mono", -20.0), Some(-10.0));
485
        assert_eq!(mock_advance_px("Azul Mock Wide", -20.0), Some(-20.0));
486
    }
487

            
488
    // ---------------------------------------------------------------------
489
    // mock_advance_px — invariants
490
    // ---------------------------------------------------------------------
491

            
492
    #[test]
493
    fn mock_advance_px_wide_is_exactly_double_mono() {
494
        for size in [0.0, 0.1, 1.0, 12.0, 16.0, 20.0, 1234.5, 1e30, -7.5] {
495
            let mono = mock_advance_px("Azul Mock Mono", size).unwrap();
496
            let wide = mock_advance_px("Azul Mock Wide", size).unwrap();
497
            assert_eq!(wide, mono * 2.0, "at size {size}");
498
            // Halving is exact in binary floating point: round-trip must be
499
            // bit-identical, with no accumulated error.
500
            assert_eq!(mono * 2.0, size, "mono round-trip at size {size}");
501
        }
502
    }
503

            
504
    #[test]
505
    fn mock_advance_px_scales_linearly_and_monotonically() {
506
        let sizes = [0.0, 0.5, 1.0, 8.0, 12.0, 16.0, 20.0, 64.0, 1000.0, 1e20];
507
        for family in ["Azul Mock Mono", "Azul Mock Wide"] {
508
            let mut prev = f32::NEG_INFINITY;
509
            for size in sizes {
510
                let got = mock_advance_px(family, size).unwrap();
511
                assert!(got >= prev, "{family} not monotonic at {size}");
512
                prev = got;
513
                assert_eq!(
514
                    mock_advance_px(family, size * 2.0).unwrap(),
515
                    got * 2.0,
516
                    "{family} not linear at {size}"
517
                );
518
            }
519
        }
520
    }
521

            
522
    #[test]
523
    fn mock_advance_px_is_deterministic() {
524
        let inputs = ["Azul Mock Mono", "Azul Mock Wide", "Arial", ""];
525
        for family in inputs {
526
            for size in [20.0, 0.0, -1.0, f32::NAN, f32::INFINITY] {
527
                let a = mock_advance_px(family, size);
528
                let b = mock_advance_px(family, size);
529
                assert_eq!(
530
                    a.map(f32::to_bits),
531
                    b.map(f32::to_bits),
532
                    "{family:?} @ {size} not pure"
533
                );
534
            }
535
        }
536
    }
537

            
538
    // ---------------------------------------------------------------------
539
    // mock_font_ranges — getter invariants
540
    // ---------------------------------------------------------------------
541

            
542
    #[test]
543
    fn mock_font_ranges_is_exactly_printable_ascii() {
544
        let ranges = mock_font_ranges();
545
        assert_eq!(ranges.len(), 1);
546
        assert_eq!(ranges[0], UnicodeRange { start: 0x20, end: 0x7E });
547
    }
548

            
549
    #[test]
550
    fn mock_font_ranges_holds_structural_invariants() {
551
        let ranges = mock_font_ranges();
552
        assert!(!ranges.is_empty(), "an empty range set would cover nothing");
553

            
554
        let mut prev_end: Option<u32> = None;
555
        for r in &ranges {
556
            assert!(r.start <= r.end, "inverted range {r:?}");
557
            assert!(r.end <= u32::from(char::MAX), "range past U+10FFFF: {r:?}");
558
            assert!(
559
                char::from_u32(r.start).is_some() && char::from_u32(r.end).is_some(),
560
                "range endpoints are not valid scalar values: {r:?}"
561
            );
562
            // Endpoints must not be surrogates and must not overflow when the
563
            // caller iterates start..=end and adds one.
564
            assert!(r.end.checked_add(1).is_some(), "end+1 overflows: {r:?}");
565
            if let Some(prev) = prev_end {
566
                assert!(r.start > prev, "ranges overlap or are unsorted: {r:?}");
567
            }
568
            prev_end = Some(r.end);
569
        }
570
    }
571

            
572
    #[test]
573
    fn mock_font_ranges_covers_only_printable_ascii_codepoints() {
574
        let ranges = mock_font_ranges();
575
        let mut count = 0u32;
576
        for r in &ranges {
577
            for cp in r.start..=r.end {
578
                let c = char::from_u32(cp).expect("covered codepoint must be a scalar value");
579
                assert!(
580
                    c == ' ' || c.is_ascii_graphic(),
581
                    "U+{cp:04X} ({c:?}) is covered but is not printable ASCII"
582
                );
583
                count += 1;
584
            }
585
        }
586
        assert_eq!(count, 95, "0x20..=0x7E is 95 codepoints");
587
    }
588

            
589
    #[test]
590
    fn mock_font_ranges_deliberately_excludes_everything_else() {
591
        let ranges = mock_font_ranges();
592
        let covers = |cp: u32| ranges.iter().any(|r| cp >= r.start && cp <= r.end);
593

            
594
        // Boundary neighbours first — the classic off-by-one.
595
        assert!(!covers(0x1F), "0x1F (just below) must not be covered");
596
        assert!(covers(0x20), "0x20 (first) must be covered");
597
        assert!(covers(0x7E), "0x7E (last) must be covered");
598
        assert!(!covers(0x7F), "0x7F DEL (just above) must not be covered");
599

            
600
        for cp in [
601
            0x00,
602
            0x09,
603
            0x0A,
604
            0x80,
605
            0xA0,
606
            0x5D0,      // Hebrew
607
            0x627,      // Arabic
608
            0x4E00,     // CJK
609
            0xFFFD,
610
            0x1_0000,
611
            0x1_F600,   // emoji
612
            0x10_FFFF,  // char::MAX
613
        ] {
614
            assert!(!covers(cp), "U+{cp:04X} must fall through to real fallback");
615
        }
616
    }
617

            
618
    #[test]
619
    fn mock_font_ranges_returns_an_independent_owned_vec() {
620
        let mut first = mock_font_ranges();
621
        first.clear();
622
        first.push(UnicodeRange { start: 0, end: 0x10_FFFF });
623

            
624
        let second = mock_font_ranges();
625
        assert_eq!(
626
            second,
627
            vec![UnicodeRange { start: 0x20, end: 0x7E }],
628
            "mutating a returned Vec must not affect later calls"
629
        );
630
        assert_eq!(second, mock_font_ranges(), "not deterministic");
631
    }
632

            
633
    // ---------------------------------------------------------------------
634
    // Round-trip: declared constants vs. the bytes actually embedded
635
    // ---------------------------------------------------------------------
636

            
637
    #[test]
638
    fn builtin_mock_fonts_table_is_self_consistent() {
639
        assert_eq!(BUILTIN_MOCK_FONTS.len(), 2);
640

            
641
        let mut seen: Vec<&str> = Vec::new();
642
        for (family, bytes) in BUILTIN_MOCK_FONTS {
643
            assert!(!family.is_empty(), "empty family name");
644
            assert!(!seen.contains(family), "duplicate family {family:?}");
645
            seen.push(*family);
646
            assert!(!bytes.is_empty(), "{family:?} has no font bytes");
647
            assert!(
648
                mock_advance_px(family, 20.0).is_some(),
649
                "{family:?} is registered but mock_advance_px does not know it"
650
            );
651
        }
652

            
653
        assert_eq!(BUILTIN_MOCK_FONTS[0], ("Azul Mock Mono", MOCK_MONO_TTF));
654
        assert_eq!(BUILTIN_MOCK_FONTS[1], ("Azul Mock Wide", MOCK_WIDE_TTF));
655

            
656
        // Same byte length, different content: a copy-paste of one .ttf over
657
        // the other would slip past a length-only check.
658
        assert_ne!(
659
            MOCK_MONO_TTF, MOCK_WIDE_TTF,
660
            "the two mock fonts must not be the same file"
661
        );
662
    }
663

            
664
    #[test]
665
    fn mock_ttf_bytes_are_well_formed_sfnt() {
666
        for (family, data) in BUILTIN_MOCK_FONTS {
667
            assert_eq!(
668
                be_u32(data, 0),
669
                Some(0x0001_0000),
670
                "{family:?} is not a TrueType sfnt"
671
            );
672
            let num_tables = be_u16(data, 4).unwrap() as usize;
673
            assert!(num_tables > 0 && num_tables < 64, "{family:?}: {num_tables} tables");
674
            assert!(
675
                data.len() >= 12 + num_tables * 16,
676
                "{family:?}: table directory is truncated"
677
            );
678
            for tag in [
679
                b"head", b"hhea", b"hmtx", b"maxp", b"cmap", b"glyf", b"loca", b"name",
680
            ] {
681
                assert!(
682
                    sfnt_table(data, tag).is_some(),
683
                    "{family:?} is missing the {} table",
684
                    std::str::from_utf8(tag).unwrap()
685
                );
686
            }
687
            let head = sfnt_table(data, b"head").unwrap();
688
            assert_eq!(
689
                be_u32(head, 12),
690
                Some(0x5F0F_3CF5),
691
                "{family:?}: bad head magic"
692
            );
693
        }
694
    }
695

            
696
    #[test]
697
    fn mock_ttf_advances_match_mock_advance_px() {
698
        for (family, data) in BUILTIN_MOCK_FONTS {
699
            let (upem, _, _, num_glyphs, advances) =
700
                font_metrics(data).unwrap_or_else(|| panic!("{family:?}: unparseable metrics"));
701

            
702
            assert!(upem > 0, "{family:?}: units_per_em is zero");
703
            assert_eq!(
704
                advances.len(),
705
                usize::from(num_glyphs),
706
                "{family:?}: not every glyph has an explicit advance"
707
            );
708
            assert!(!advances.is_empty(), "{family:?}: hmtx has no entries");
709
            let first = advances[0];
710
            assert!(
711
                advances.iter().all(|a| *a == first),
712
                "{family:?} is not monospaced — advances differ across glyphs"
713
            );
714

            
715
            // The em fraction the font actually encodes must equal the one
716
            // mock_advance_px hardcodes, at every size, exactly.
717
            let em_fraction = f32::from(first) / f32::from(upem);
718
            for size in [0.0, 1.0, 12.0, 16.0, 20.0, 100.0, 1e20] {
719
                assert_eq!(
720
                    mock_advance_px(family, size),
721
                    Some(em_fraction * size),
722
                    "{family:?} @ {size} px: .ttf says {em_fraction} em"
723
                );
724
            }
725
        }
726
        // And the documented table itself.
727
        assert_eq!(mock_advance_px("Azul Mock Mono", 1.0), Some(0.5));
728
        assert_eq!(mock_advance_px("Azul Mock Wide", 1.0), Some(1.0));
729
    }
730

            
731
    #[test]
732
    fn mock_ttf_vertical_metrics_match_module_doc() {
733
        // Doc table: ascent 0.8 em, descent 0.2 em for both families, so a
734
        // line box at 20 px is exactly 20 px tall.
735
        for (family, data) in BUILTIN_MOCK_FONTS {
736
            let (upem, ascender, descender, _, _) = font_metrics(data).unwrap();
737
            let upem = f32::from(upem);
738
            assert_eq!(f32::from(ascender) / upem, 0.8, "{family:?}: ascent");
739
            assert_eq!(f32::from(descender) / upem, -0.2, "{family:?}: descent");
740
            assert_eq!(
741
                (f32::from(ascender) - f32::from(descender)) / upem * 20.0,
742
                20.0,
743
                "{family:?}: line box at 20 px is not exactly 20 px"
744
            );
745
        }
746
    }
747

            
748
    #[test]
749
    fn mock_ttf_cmap_coverage_matches_mock_font_ranges() {
750
        let declared: Vec<(u32, u32)> = mock_font_ranges()
751
            .iter()
752
            .map(|r| (r.start, r.end))
753
            .collect();
754

            
755
        for (family, data) in BUILTIN_MOCK_FONTS {
756
            let actual = cmap_coverage(data)
757
                .unwrap_or_else(|| panic!("{family:?}: no format-4 cmap subtable"));
758
            assert_eq!(
759
                actual, declared,
760
                "{family:?}: cmap coverage disagrees with mock_font_ranges(), so \
761
                 registration would claim glyphs the font does not have (or hide \
762
                 ones it does)"
763
            );
764

            
765
            // .notdef plus one glyph per covered codepoint.
766
            let (_, _, _, num_glyphs, _) = font_metrics(data).unwrap();
767
            let covered: u32 = actual.iter().map(|(s, e)| e - s + 1).sum();
768
            assert_eq!(
769
                u32::from(num_glyphs),
770
                covered + 1,
771
                "{family:?}: glyph count does not match cmap coverage + .notdef"
772
            );
773
        }
774
    }
775

            
776
    /// Every glyph's raw `glyf` entry, indexed by glyph id, via `loca`.
777
    ///
778
    /// Assumes the long `loca` format, which these fonts declare
779
    /// (`head.indexToLocFormat == 1`); the assert below states that rather
780
    /// than silently misparsing if the generator ever switches.
781
    fn glyph_outlines(data: &[u8]) -> Option<Vec<&[u8]>> {
782
        let head = sfnt_table(data, b"head")?;
783
        assert_eq!(be_i16(head, 50)?, 1, "expected long loca format");
784
        let loca = sfnt_table(data, b"loca")?;
785
        let glyf = sfnt_table(data, b"glyf")?;
786
        let num_glyphs = be_u16(sfnt_table(data, b"maxp")?, 4)? as usize;
787

            
788
        let mut out = Vec::with_capacity(num_glyphs);
789
        for gid in 0..num_glyphs {
790
            let start = be_u32(loca, gid.checked_mul(4)?)? as usize;
791
            let end = be_u32(loca, gid.checked_add(1)?.checked_mul(4)?)? as usize;
792
            out.push(glyf.get(start..end.min(glyf.len()))?);
793
        }
794
        Some(out)
795
    }
796

            
797
    #[test]
798
    fn mock_ttf_every_inked_glyph_draws_distinct_ink() {
799
        // THE regression guard for the reason the ink is codepoint-derived at
800
        // all. These fonts used to give every glyph the identical filled
801
        // rectangle, which made two equal-length strings rasterise to
802
        // BIT-IDENTICAL pixels: a text edit then produced damage but no pixel
803
        // change, and every pixel-liveness assertion over a mock-font text
804
        // mutation (`assert_changed` and friends) was asserting something the
805
        // font made impossible. Collapse the outlines back onto one shape and
806
        // this fails instead of silently making those assertions vacuous.
807
        for (family, data) in BUILTIN_MOCK_FONTS {
808
            let outlines = glyph_outlines(data)
809
                .unwrap_or_else(|| panic!("{family:?}: cannot read glyf/loca"));
810

            
811
            let blank: Vec<usize> = outlines
812
                .iter()
813
                .enumerate()
814
                .filter(|(_, o)| o.is_empty())
815
                .map(|(gid, _)| gid)
816
                .collect();
817
            assert_eq!(
818
                blank,
819
                vec![0, 1],
820
                "{family:?}: exactly .notdef (gid 0) and U+0020 (gid 1) may be \
821
                 blank — they still advance, they just draw nothing"
822
            );
823

            
824
            let inked: Vec<&[u8]> = outlines.iter().copied().filter(|o| !o.is_empty()).collect();
825
            let distinct: std::collections::BTreeSet<&[u8]> = inked.iter().copied().collect();
826
            assert_eq!(
827
                distinct.len(),
828
                inked.len(),
829
                "{family:?}: {} of {} inked glyphs share an outline with another \
830
                 glyph, so the characters they encode are indistinguishable in \
831
                 pixels",
832
                inked.len() - distinct.len(),
833
                inked.len()
834
            );
835
        }
836
    }
837

            
838
    #[test]
839
    fn mock_ttf_glyph_boxes_are_identical_across_glyphs() {
840
        // The companion to the test above, and the reason varying the ink is
841
        // FREE: the per-glyph pattern lives strictly INSIDE a frame that
842
        // touches all four sides of the box, so every glyph declares the same
843
        // bounding box and it still equals `head`'s global one. If a future
844
        // pattern let ink drive the box, glyph extents would start varying by
845
        // character and the mock fonts would stop being arithmetic.
846
        for (family, data) in BUILTIN_MOCK_FONTS {
847
            let head = sfnt_table(data, b"head").unwrap();
848
            let global = (
849
                be_i16(head, 36).unwrap(),
850
                be_i16(head, 38).unwrap(),
851
                be_i16(head, 40).unwrap(),
852
                be_i16(head, 42).unwrap(),
853
            );
854

            
855
            for (gid, outline) in glyph_outlines(data).unwrap().iter().enumerate() {
856
                if outline.is_empty() {
857
                    continue; // blank glyphs carry no bbox at all
858
                }
859
                let bbox = (
860
                    be_i16(outline, 2).unwrap(),
861
                    be_i16(outline, 4).unwrap(),
862
                    be_i16(outline, 6).unwrap(),
863
                    be_i16(outline, 8).unwrap(),
864
                );
865
                assert_eq!(
866
                    bbox, global,
867
                    "{family:?}: glyph {gid} has bbox {bbox:?} but head declares \
868
                     {global:?} — glyph extents must not vary by character"
869
                );
870
            }
871
        }
872
    }
873

            
874
    #[test]
875
    fn mock_ttf_family_name_matches_its_registration_key() {
876
        // If the embedded name table disagreed with the key in
877
        // BUILTIN_MOCK_FONTS, resolution would silently fall back — exactly
878
        // the failure mode the module doc warns about.
879
        for (family, data) in BUILTIN_MOCK_FONTS {
880
            let embedded = family_name(data)
881
                .unwrap_or_else(|| panic!("{family:?}: no Windows family name record"));
882
            assert_eq!(
883
                embedded.as_str(),
884
                *family,
885
                "font self-identifies as {embedded:?} but is registered as {family:?}"
886
            );
887
        }
888
    }
889
}
890

            
891
#[cfg(all(test, feature = "font_loading"))]
892
mod distinguishable_glyphs {
893
    use std::collections::BTreeMap;
894

            
895
    use super::*;
896
    use crate::font::parsed::ParsedFont;
897

            
898
    /// The decoded ink of one character: contour end indices plus the raw
899
    /// outline points, in font units.
900
    ///
901
    /// Two characters whose signatures compare equal cover exactly the same
902
    /// pixels at every size and every transform, so no rasteriser can tell
903
    /// them apart. Reading the DECODED outline (rather than a rendered bitmap)
904
    /// keeps this free of a rasteriser while still being the thing a
905
    /// rasteriser consumes.
906
192
    fn ink_of(font: &ParsedFont, ch: char) -> (Vec<u16>, Vec<(i16, i16)>) {
907
192
        let gid = font
908
192
            .lookup_glyph_index(ch as u32)
909
192
            .unwrap_or_else(|| panic!("{ch:?} is in the mock font's ASCII range"));
910
192
        let glyph = font
911
192
            .get_or_decode_glyph(gid)
912
192
            .unwrap_or_else(|| panic!("{ch:?} (gid {gid}) must decode"));
913
192
        let ends = glyph.raw_contour_ends.clone().unwrap_or_else(|| {
914
            panic!(
915
                "{ch:?} (gid {gid}) decoded with no contour list — every inked mock glyph is a \
916
                 simple TrueType glyph, so this means the outline was not read at all"
917
            )
918
        });
919
192
        (ends, glyph.raw_points.clone().unwrap_or_default())
920
192
    }
921

            
922
    /// Two different ASCII characters must not decode to the same ink.
923
    ///
924
    /// This is the property `mock_ttf_every_inked_glyph_draws_distinct_ink`
925
    /// pins in the raw `glyf` bytes, asserted one layer further down: through
926
    /// the real font parser, on the outline a rasteriser actually consumes. A
927
    /// generator emitting distinct `glyf` entries that nonetheless decoded to
928
    /// the same contours would satisfy the byte-level test and still make text
929
    /// edits invisible.
930
    ///
931
    /// Why it is load-bearing: when every glyph was the identical filled
932
    /// rectangle, two equal-length strings rasterised bit-identically, so a
933
    /// text edit like `"tick 1"` -> `"tick 2"` produced damage but NO pixel
934
    /// change. Every scenario combining a mock font with a pixel-liveness
935
    /// assertion (`assert_changed`, `assert_damage_covers_changes`,
936
    /// `assert_damage_sound`) was then asserting something the font made
937
    /// impossible; the ones that passed did so only because their two strings
938
    /// differed in LENGTH — an accident, not a property. The gen-e2e prompt
939
    /// pushes mock fonts hard and is right to (deterministic metrics, no OS
940
    /// font dependency, and real family names collapse onto one shared
941
    /// `FontId` on a CI box, which makes font-identity and leak assertions
942
    /// vacuously green), so the font is what had to change.
943
    ///
944
    /// Note what this deliberately does NOT assert on: GLYPH IDS. The broken
945
    /// fonts already gave every codepoint its own id — `'A'` was gid 34 and
946
    /// `'B'` gid 35 before the ink was fixed exactly as after it — while all
947
    /// 94 inked glyphs shared ONE outline. An id-based check passes just as
948
    /// happily on a font whose glyphs are indistinguishable, so it cannot fail
949
    /// for the reason stated above; only the outline can.
950
    #[test]
951
1
    fn mock_font_glyphs_differ_between_characters() {
952
3
        for (family, bytes) in BUILTIN_MOCK_FONTS {
953
2
            let mut warnings = Vec::new();
954
2
            let font = ParsedFont::from_bytes(bytes, 0, &mut warnings)
955
2
                .unwrap_or_else(|| panic!("{family:?} must parse"));
956

            
957
            // The pair from the original bug report, named explicitly so a
958
            // failure reads as characters rather than glyph indices.
959
2
            assert_ne!(
960
2
                ink_of(&font, 'A'),
961
2
                ink_of(&font, 'B'),
962
                "{family:?}: 'A' and 'B' decode to the SAME outline, so every string of a given \
963
                 length renders identically and no pixel assertion over a text edit can hold"
964
            );
965

            
966
            // ...and the whole inked range is pairwise distinct, not just that
967
            // one pair. U+0020 is excluded: it is blank by design (it still
968
            // advances, it just draws nothing).
969
2
            let mut seen: BTreeMap<(Vec<u16>, Vec<(i16, i16)>), char> = BTreeMap::new();
970
190
            for cp in 0x21..=0x7E_u32 {
971
188
                let ch = char::from_u32(cp).expect("printable ASCII is a scalar value");
972
188
                if let Some(prev) = seen.insert(ink_of(&font, ch), ch) {
973
                    panic!(
974
                        "{family:?}: {ch:?} and {prev:?} decode to the same outline, so the two \
975
                         characters are indistinguishable in pixels"
976
                    );
977
188
                }
978
            }
979
2
            assert_eq!(seen.len(), 94, "{family:?}: 0x21..=0x7E is 94 inked glyphs");
980
        }
981
1
    }
982

            
983
    /// The companion property, and the reason varying the ink is FREE: the ink
984
    /// varies, the METRICS DO NOT.
985
    ///
986
    /// The per-glyph pattern lives strictly inside a frame that touches all
987
    /// four sides of the glyph box, so every glyph decodes to the same tight
988
    /// bounding box and the same advance. `mock_ttf_glyph_boxes_are_identical_across_glyphs`
989
    /// checks the bbox each glyph DECLARES in its `glyf` header; this checks
990
    /// the one the parser COMPUTES from the outline, which is what layout and
991
    /// rasterisation actually use. A future pattern that let ink escape the
992
    /// frame would keep the declared box intact and still make glyph extents
993
    /// vary by character — that is the gap this closes.
994
    #[test]
995
1
    fn mock_font_ink_varies_without_moving_any_metric() {
996
3
        for (family, bytes) in BUILTIN_MOCK_FONTS {
997
2
            let mut warnings = Vec::new();
998
2
            let font = ParsedFont::from_bytes(bytes, 0, &mut warnings)
999
2
                .unwrap_or_else(|| panic!("{family:?} must parse"));
2
            let mut advances = BTreeMap::new();
2
            let mut boxes = BTreeMap::new();
192
            for cp in 0x20..=0x7E_u32 {
190
                let ch = char::from_u32(cp).expect("printable ASCII is a scalar value");
190
                let gid = font
190
                    .lookup_glyph_index(cp)
190
                    .unwrap_or_else(|| panic!("{family:?}: {ch:?} has no glyph"));
190
                advances.entry(font.get_horizontal_advance(gid)).or_insert(ch);
190
                if cp == 0x20 {
2
                    continue; // blank: its bbox is the seeded one, not ink
188
                }
188
                let glyph = font
188
                    .get_or_decode_glyph(gid)
188
                    .unwrap_or_else(|| panic!("{family:?}: {ch:?} must decode"));
188
                let bbox = glyph.bounding_box;
188
                boxes
188
                    .entry((bbox.min_x, bbox.min_y, bbox.max_x, bbox.max_y))
188
                    .or_insert(ch);
            }
2
            assert_eq!(
2
                advances.len(),
                1,
                "{family:?}: advances differ across characters ({advances:?}) — the mock fonts \
                 exist to make text layout arithmetic, which requires one advance for all of them"
            );
2
            assert_eq!(
2
                boxes.len(),
                1,
                "{family:?}: the DECODED tight bounding box varies by character ({boxes:?}), so \
                 ink escaped the constant frame and glyph extents are no longer deterministic"
            );
        }
1
    }
}